JavaFX и Spring. Вместе веселей



    В данной статье я хочу рассказать о своем опыте интеграции таких вещей как 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, после загружаем соответствующий контроллер.

    Пример AppConfig.java
    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.

    Ресурсы


    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 9

      0
      Я конечно понимаю, что это не относится к основной теме, но зачем Derby стартовать как сервер? Я за пределеами Unit-тестов с такими базами не работал, просто хочу понять зачем так делать. Пока на ум приходит только возможность дополнительно подключиться к этой базе снаружи приложения.
        0
        Да, видимо мне стоит дополнить этот момент, информацией о том, что Derby здесь запускается как сервер, исключительно для того, что бы показать взаимодействие с БД из коробки. У реального пользователя решение не сработает, т.к. Derby доступен лишь в JDK.
          0
          Derby доступен лишь в JDK.

          почему? через мавен же интегрировал
            0
            Честно говоря с Derby я не силен, т.к. использовал только в «for fun» целях. Но интегрировал я лишь derbynet, который ответственен за «Derby Network Server», а в частности за запуск БД в отдельной JVM. Т.е. нужно добавить зависимость для самого derby.jar, что бы интегрировать полностью.

            И вопрос как раз таки и был на тему зачем стартовать в отдельной JVM вместо embeeded вариации. Что для данного примера абсолютно лишнее.
        +2
        А можно по-подробней, что нам даст Spring в JavaFX приложении? если только для DI — почему бы не использовать стандартный стек?
          0
          DI, управление транзакциями, работа с БД, работа с асинхронными задачами — что пришло первым в голову. Я не призываю повсеместно использовать связку Spring с JavaFX, но если есть потребность в привычных технологиях, то почему нет.
            0
            мой вопрос был скорее о том, что может предложить Spring для JavaFX по сравнению со стандартным JavaEE стеком? какие-то особые JavaFX-плюшки есть? или только DI и всякие JMS/JDBC-templates?
              0
              Если смотреть с этой стороны, но никаких особых JavaFX-плюшек здесь нету.
          0
          а где результат то? Хочется потыкать по кнопочкам же

          Only users with full accounts can post comments. Log in, please.