В данной статье я хочу рассказать о своем опыте интеграции таких вещей как JavaFX и Spring. И заодно использовать базу данных Derby и Maven для сборки приложения.
Введение
JavaFX выглядит довольно удобной и привлекательной технологией для реализации десктопных решений на платформе Java. Начиная с версии Java SE 7 Update 6, JavaFX является частью реализации Oracle Java SE, т.е. никаких дополнительных установок на стороне пользователя не требуется.
Spring со своей стороны, дает удобные фишечки в виде IoC, управление транзакциями и т.д., которые не хочется реализовывать самому.
Hello world
Начнем с простого приложения использующего FXML.
Класс приложения:
package ru.todolist;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class TodoApplication extends Application {
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) throws Exception {
Parent root = FXMLLoader.load(getClass().getResource("/fxml/main.fxml"));
Scene scene = new Scene(root, 300, 275);
primaryStage.setTitle("Todolist");
primaryStage.setScene(scene);
primaryStage.show();
}
}
В данном коде у нас есть класс TodoApplication, которое является точкой входа для JavaFX приложения. С помощью FXMLLoader’а мы загружаем необходимый View из ресурсов. Загрузчик инициализирует вместе с View так же и контроллер.
main.fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import java.net.*?>
<?import javafx.geometry.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.text.*?>
<GridPane fx:controller="ru.todolist.controller.MainController" xmlns:fx="http://javafx.com/fxml" alignment="center" hgap="10" vgap="10" styleClass="root">
<Text id="welcome-text" text="Hello world!" GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2"/>
</GridPane>
В общем ничего особенного, можно ехать дальше.
Собираем с помощью Maven
Для сборки можно использовать специальный плагин от Zen Java. Помимо сборки JavaFX приложения, он умеет собирать нативные установщики для него (MSI, EXE, DMG, RPM) вместе с JRE.
Пример pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.todolist</groupId>
<artifactId>application</artifactId>
<packaging>jar</packaging>
<version>1.0.0</version>
<properties>
<log4j.version>1.2.17</log4j.version>
</properties>
<dependencies>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>com.zenjava</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>2.0</version>
<configuration>
<mainClass>ru.todolist.TodoApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
Как видно, в конфигурации плагина нужно указать путь к главному классу приложения. Но это еще не все, так же необходимо перед запуском приложения выполнить следующую команду:
mvn com.zenjava:javafx-maven-plugin:2.0:fix-classpath
Подробно о том зачем это можно почитать в документации плагина.
Подключаем Derby
Для полного счастья нам не хватает полноценной БД в нашем приложении.
Нужно добавить зависимости для управления сервисом Derby и драйвер для доступа к БД:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.todolist</groupId>
<artifactId>application</artifactId>
<packaging>jar</packaging>
<version>1.0.0</version>
<properties>
<derby.version>10.10.1.1</derby.version>
...
</properties>
<dependencies>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbynet</artifactId>
<version>${derby.version}</version>
</dependency>
<dependency>
<groupId>org.apache.derby</groupId>
<artifactId>derbyclient</artifactId>
<version>${derby.version}</version>
</dependency>
...
</dependencies>
<build>
...
</build>
</project>
Немного модифицируем класс TodoApplication так, что бы он запускал и останавливал БД.
public class TodoApplication extends Application {
private static Logger LOG = Logger.getLogger(TodoApplication.class);
...
@Override
public void init() {
try {
DbUtils.startDB();
} catch (Exception e) {
LOG.error("Problem with start DB", e);
}
}
@Override
public void stop() {
try {
DbUtils.stopDB();
} catch (Exception e) {
LOG.error("Problem with stop DB", e);
}
}
}
Сам класс DbUtils:
package ru.todolist.utils;
import org.apache.derby.drda.NetworkServerControl;
import org.apache.log4j.Logger;
import java.net.InetAddress;
public class DbUtils {
private static Logger LOG = Logger.getLogger(DbUtils.class);
private static NetworkServerControl server;
public static void startDB() throws Exception {
LOG.info("Start DB");
server = new NetworkServerControl(InetAddress.getByName("localhost"), 1527);
server.start(null);
}
public static void stopDB() throws Exception {
LOG.info("Stop DB");
server.shutdown();
}
}
Добавляем Spring
Теперь добавляем нужные зависимости для Spring, а заодно и Hibernate в pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.todolist</groupId>
<artifactId>application</artifactId>
<packaging>jar</packaging>
<version>1.0.0</version>
<properties>
...
<spring.version>3.2.4.RELEASE</spring.version>
<hibernate.version>4.2.6.Final</hibernate.version>
</properties>
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-entitymanager</artifactId>
<version>${hibernate.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>${spring.version}</version>
</dependency>
...
</dependencies>
<build>
...
</build>
</project>
Нам необходимо реализовать свой загрузчик, который будет отвечает за загрузку контроллеров и View-компонентов для них:
package ru.todolist.utils;
import javafx.fxml.FXMLLoader;
import javafx.scene.Node;
import javafx.util.Callback;
import org.apache.log4j.Logger;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import ru.todolist.config.AppConfig;
import ru.todolist.controller.Controller;
import java.io.IOException;
import java.io.InputStream;
public class SpringFXMLLoader {
private static Logger LOG = Logger.getLogger(SpringFXMLLoader.class);
private static final ApplicationContext APPLICATION_CONTEXT = new AnnotationConfigApplicationContext(AppConfig.class);
public static Controller load(String url) {
InputStream fxmlStream = null;
try {
fxmlStream = SpringFXMLLoader.class.getResourceAsStream(url);
FXMLLoader loader = new FXMLLoader();
loader.setControllerFactory(new Callback<Class<?>, Object>() {
@Override
public Object call(Class<?> aClass) {
return APPLICATION_CONTEXT.getBean(aClass);
}
});
Node view = (Node) loader.load(fxmlStream);
Controller controller = loader.getController();
controller.setView(view);
return controller;
} catch (IOException e) {
LOG.error("Can't load resource", e);
throw new RuntimeException(e);
} finally {
if (fxmlStream != null) {
try {
fxmlStream.close();
} catch (IOException e) {
LOG.error("Can't close stream", e);
}
}
}
}
}
Как видно в качестве фабрики контроллеров используется контекст приложения Spring’a. Мы загружаем сначала по URL необходимую View, после загружаем соответствующий контроллер.
package ru.todolist.config;
import org.hibernate.ejb.HibernatePersistence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.DriverManagerDataSource;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;
@Configuration
@ComponentScan("ru.todolist")
public class AppConfig {
@Bean
public DataSource dataSource() {
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.apache.derby.jdbc.ClientDriver");
dataSource.setUrl("jdbc:derby://localhost:1527/todo;create=true"); //Create DB if not exist
dataSource.setUsername("user");
dataSource.setPassword("password");
return dataSource;
}
@Autowired
@Bean
public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean(DataSource dataSource) {
LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
Properties properties = new Properties();
properties.put("hibernate.dialect", "org.hibernate.dialect.DerbyDialect");
properties.put("hibernate.hbm2ddl.auto", "create");
bean.setPersistenceProviderClass(HibernatePersistence.class);
bean.setDataSource(dataSource);
bean.setJpaProperties(properties);
bean.setPackagesToScan("ru.todolist.model");
return bean;
}
@Autowired
@Bean
public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory, DataSource dataSource) {
JpaTransactionManager bean = new JpaTransactionManager(entityManagerFactory);
bean.setDataSource(dataSource);
return bean;
}
}
Для наших контроллеров мы используем следующий интерфейс, который позволяет связывать контроллер и вид:
package ru.todolist.controller;
import javafx.scene.Node;
public interface Controller {
Node getView();
void setView (Node view);
}
Вынесем реализацию этих методов в абстрактный класс AbstractController.java:
package ru.todolist.controller;
import javafx.scene.Node;
public abstract class AbstractController implements Controller {
private Node view;
public Node getView() {
return view;
}
public void setView (Node view){
this.view = view;
}
}
И последний штрих, используем SprinFXMLLoader взамен стандартного загрузчика в классе TodoApplication:
public class TodoApplication extends Application {
...
@Override
public void start(Stage primaryStage) throws Exception {
MainController controller = (MainController) SpringFXMLLoader.load("/fxml/main.fxml");
Scene scene = new Scene((Parent) controller.getView(), 300, 275);
primaryStage.setTitle("Todolist");
primaryStage.setScene(scene);
primaryStage.show();
}
...
}
Итоги
Код получился довольно простым, без особых извращений. В результате мы можем использовать JavaFX с привычным стеком технологий (для Java EE) и использовать знакомые паттерны для проектирования архитектуры приложения.
В качестве дополнения хочется сказать, что можно использовать данный подход и для интеграции с Guice.
Ресурсы
Автор: Tsivarev