JavaFX + Spring (JDBC & @SpringBootApplication & @Autowired & @Transactional)

JavaFX + Spring (JDBC & @SpringBootApplication & @Autowired & @Transactional)

我想使用 JavaFX 和数据库访问 Spring JDBC。然而,我对 Spring 完全陌生,似乎我无法完全理解它的功能,尤其是交易处理...

我已将以下依赖项添加到我的项目中:

compile 'org.springframework.boot:spring-boot-starter-jdbc'
runtime 'mysql:mysql-connector-java'

... 我想在 GUI 应用程序对数据库执行操作时使用 Spring 事务处理机制。据我了解,以下代码应该:

所以,总结一下:当 RuntimeException 在注释为 @Transactional 的方法中被抛出时,应该在应用程序退出之前恢复所有已经由该方法创建的条目,不是吗?

然而,所有创建的条目都永久保留在数据库中(我可以在应用程序退出后在那里看到它们)。所以首先 - 我是否正确理解这些交易应该如何运作?如果是这样,那么如何让它们真正按照我的预期工作?

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.transaction.annotation.Transactional;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;


@SpringBootApplication
public class SpringTransactional extends Application {
    private Pane viewPane;

    private ConfigurableApplicationContext springContext;

    /** application.properties:
     spring.datasource.driver-class-name = com.mysql.jdbc.Driver
     spring.datasource.url = jdbc:mysql://localhost:3306/db_name?useSSL=false&serverTimezone=UTC
     spring.datasource.username = db_username
     spring.datasource.password = username123
     */
    @Autowired
    private JdbcTemplate jdbcTemplate;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void init() throws Exception {
        springContext = SpringApplication.run(SpringTransactional.class);
        springContext.getAutowireCapableBeanFactory().autowireBean(this);
    }

    @Override
    public void stop() throws Exception {
        springContext.close();
    }

    @Override
    public void start(Stage primaryStage) {
        viewPane = assembleView(primaryStage);

        try {
            db_transaction_test();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }

        Platform.exit();
    }

    private Pane assembleView(Stage primaryStage) {
        VBox rootPane = new VBox();
        rootPane.setSpacing(10);
        rootPane.setPadding(new Insets(10));
        rootPane.setStyle("-fx-base: #84a7ad;");
        rootPane.getChildren().add(new Label("GUI goes here."));

        primaryStage.setScene(new Scene(rootPane));
        primaryStage.setResizable(false);
        primaryStage.show();

        return rootPane;
    }

    @Transactional
    private void db_transaction_test() {
        for (int i = 0; i < 10; i++) {
            try {
                int entry_name = getEntryId("entry_" + i);
                System.out.println("Created entry id=" + entry_name);
            } catch (DaoException e) {
                e.printStackTrace();
            }

            if (i == 5) {
                throw new RuntimeException("Testing data upload procedure break.");
            }
        }
    }

    /** DB creation and schema:
     CREATE DATABASE db_name;
     CREATE USER db_username;

     USE db_name;
     GRANT ALL ON db_name.* TO db_username;

     SET PASSWORD FOR spz = PASSWORD('username123');
     FLUSH PRIVILEGES;

     CREATE TABLE Entry (
     entry_ID INT NOT NULL AUTO_INCREMENT,
     name   TEXT NOT NULL,

     PRIMARY KEY (entry_ID)
     );
     */
    private int getEntryId(String entryName) throws DaoException {
        List<DbEntry> dbEntries = retrieveEntriesFor(entryName);

        if (dbEntries.size() == 1) {
            return dbEntries.get(0).getEntry_ID();
        } else if (dbEntries.size() == 0) {
            String sqlInsert = "INSERT INTO Entry (name) VALUES (?)";
            jdbcTemplate.update(sqlInsert, entryName);
            dbEntries = retrieveEntriesFor(entryName);
            if (dbEntries.size() == 1) {
                return dbEntries.get(0).getEntry_ID();
            } else {
                throw new DaoException("Invalid results amount received after creating new (" + dbEntries.size() + ") when getting entry for name: " + entryName);
            }
        } else {
            throw new DaoException("Invalid results amount received (" + dbEntries.size() + ") when getting entry for name: " + entryName);
        }
    }

    private List<DbEntry> retrieveEntriesFor(String entryName) {
        return jdbcTemplate.query("SELECT * FROM Entry WHERE name=?;", (ResultSet result, int rowNum) -> unMarshal(result), entryName);
    }

    private DbEntry unMarshal(ResultSet result) throws SQLException {
        DbEntry dbEntry = new DbEntry();
        dbEntry.setEntry_ID(result.getInt("entry_ID"));
        dbEntry.setName(result.getString("name"));
        return dbEntry;
    }

    public class DbEntry {
        private int entry_ID;
        private String name;

        int getEntry_ID() { return entry_ID; }
        void setEntry_ID(int entry_ID) { this.entry_ID = entry_ID; }
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
    }

    private class DaoException extends Throwable {
        DaoException(String err_msg) { super(err_msg); }
    }
}

Spring 中的事务与 AOP 在 Spring 中的工作方式相同:当您从 Spring 请求一个具有标记为事务的方法的 bean 时,您实际上会收到一个代理该 bean 的事务方法实现 "decorates" 您在实现中提供的实现 class。简而言之,代理 class 中方法的实现开始一个事务,然后调用您的实现中定义的方法 class,然后提交或回滚事务。

所以我认为问题是 SpringTransactional 实例不是由 Spring 应用程序上下文创建的,而是由 JavaFX 启动进程创建的(即它是在您调用 Application.launch() 时由 JavaFX 框架创建的)。因此,Spring 无法创建实现事务行为的代理对象。

尝试将数据库功能分解到一个单独的 class 中,该 class 由 spring 管理,并将其实例注入您的应用程序 class。 IE。做一些像

// Note: I'm only familiar with "traditional" Spring, not Spring boot. 
// Not sure if this annotation is picked up by Spring boot, you may need to 
// make some changes to the config or something to get this working.
@Component
public class DAO {

    @Autowired
    private JdbcTemplate jdbcTemplate ;

    @Transactional
    private void db_transaction_test() {
        // ...
    }

    // ...
}

然后在您的应用程序中 class:

@SpringBootApplication
public class SpringTransactional extends Application {
    private Pane viewPane;

    private ConfigurableApplicationContext springContext;

    @Autowired
    private DAO dao ;

    // ...

     @Override
    public void start(Stage primaryStage) {
        viewPane = assembleView(primaryStage);

        try {
            dao.db_transaction_test();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }

        Platform.exit();
    }  

    // ...
}

经过更多测试,创建单独的 Spring 组件 EntryDao 似乎可行(感谢 James_D),但前提是 db_transaction_test 注释为 @Transactional class - 下面代码中的选项 A。

但我真正感兴趣的是选项 B - 当 db_transaction_test 注释为 @Transactional 时在另一个 class 中。这是因为 DAO class 不(也不应该)知道数据库未实现的问题,这些问题是恢复一系列以前的数据库操作的原因。此信息来自其他 'controllers',这些信息不能导致数据完整性问题。因此,在下面的示例中,SpringTransactional 应该是唯一可以抛出此特定 RuntimeException("Testing data upload procedure break."); 的示例(作为现实生活中 system/environment 问题的示例)。然而,正如最后的堆栈跟踪所示 - 事务未在那里初始化。

那么有没有办法让它按照我的需要使用 Spring @Transactional(又名。声明式交易)或仅使用手动(又名程序化)Spring 交易控制?如果这是唯一的方法,那么如何在对 "auto-configuration" 使用 @SpringBootApplication 和对 jdbcTemplate 对象使用 @Autowired 的同时配置 DataSourceTransactionManager

主要class:

package tmp;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.transaction.annotation.Transactional;
import tmp.dao.EntryDao;


@SpringBootApplication
public class SpringTransactional extends Application {
    private Pane viewPane;

    private ConfigurableApplicationContext springContext;

    @Autowired
    private EntryDao dao;

    public static void main(String[] args) { launch(args); }

    @Override
    public void init() throws Exception {
        springContext = SpringApplication.run(SpringTransactional.class);
        springContext.getAutowireCapableBeanFactory().autowireBean(this);
    }

    @Override
    public void stop() throws Exception { springContext.close(); }

    @Override
    public void start(Stage primaryStage) {
        viewPane = assembleView(primaryStage);

        // OPTION A:
        try {
            dao.db_transaction_test();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }

        // OPTION B:
        try {
            db_transaction_test();
        } catch (RuntimeException e) {
            e.printStackTrace();
        }

        Platform.exit();
    }

    @Transactional
    private void db_transaction_test() {
        for (int i = 0; i < 10; i++) {
            try {
                int entry_name = dao.getEntryId("entry_" + i);
                System.out.println("Created entry id=" + entry_name);
            } catch (EntryDao.DaoException e) {
                e.printStackTrace();
            }

            if (i == 5) {
                throw new RuntimeException("Testing data upload procedure break.");
            }
        }
    }

    private Pane assembleView(Stage primaryStage) {
        VBox rootPane = new VBox();
        rootPane.setSpacing(10);
        rootPane.setPadding(new Insets(10));
        rootPane.setStyle("-fx-base: #84a7ad;");
        rootPane.getChildren().add(new Label("GUI goes here."));

        primaryStage.setScene(new Scene(rootPane));
        primaryStage.setResizable(false);
        primaryStage.show();

        return rootPane;
    }
}

EntryDao class:

package tmp.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

/**
 * DB creation and schema:
 * CREATE DATABASE db_name;
 * CREATE USER db_username;
 * <p>
 * USE db_name;
 * GRANT ALL ON db_name.* TO db_username;
 * <p>
 * SET PASSWORD FOR spz = PASSWORD('username123');
 * FLUSH PRIVILEGES;
 * <p>
 * CREATE TABLE Entry (
 * entry_ID INT NOT NULL AUTO_INCREMENT,
 * name   TEXT NOT NULL,
 * <p>
 * PRIMARY KEY (entry_ID)
 * );
 */
@Component
public class EntryDao {
    /**
     * application.properties:
     * spring.datasource.driver-class-name = com.mysql.jdbc.Driver
     * spring.datasource.url = jdbc:mysql://localhost:3306/db_name?useSSL=false&serverTimezone=UTC
     * spring.datasource.username = db_username
     * spring.datasource.password = username123
     */
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Transactional
    public void db_transaction_test() {
        for (int i = 0; i < 10; i++) {
            try {
                int entry_name = getEntryId("entry_" + i);
                System.out.println("Created entry id=" + entry_name);
            } catch (EntryDao.DaoException e) {
                e.printStackTrace();
            }

            if (i == 5) {
                throw new RuntimeException("Testing data upload procedure break.");
            }
        }
    }

    public int getEntryId(String entryName) throws DaoException {
        List<DbEntry> dbEntries = retrieveEntriesFor(entryName);

        if (dbEntries.size() == 1) {
            return dbEntries.get(0).getEntry_ID();
        } else if (dbEntries.size() == 0) {
            String sqlInsert = "INSERT INTO Entry (name) VALUES (?)";
            jdbcTemplate.update(sqlInsert, entryName);
            dbEntries = retrieveEntriesFor(entryName);
            if (dbEntries.size() == 1) {
                return dbEntries.get(0).getEntry_ID();
            } else {
                throw new DaoException("Invalid results amount received after creating new (" + dbEntries.size() + ") when getting entry for name: " + entryName);
            }
        } else {
            throw new DaoException("Invalid results amount received (" + dbEntries.size() + ") when getting entry for name: " + entryName);
        }
    }

    private List<DbEntry> retrieveEntriesFor(String entryName) {
        return jdbcTemplate.query("SELECT * FROM Entry WHERE name=?;", (ResultSet result, int rowNum) -> unMarshal(result), entryName);
    }

    private DbEntry unMarshal(ResultSet result) throws SQLException {
        DbEntry dbEntry = new DbEntry();
        dbEntry.setEntry_ID(result.getInt("entry_ID"));
        dbEntry.setName(result.getString("name"));
        return dbEntry;
    }

    public class DbEntry {
        private int entry_ID;
        private String name;

        int getEntry_ID() { return entry_ID; }
        void setEntry_ID(int entry_ID) { this.entry_ID = entry_ID; }
        public String getName() { return name; }
        public void setName(String name) { this.name = name; }
    }

    public class DaoException extends Throwable { DaoException(String err_msg) { super(err_msg); } }
}

堆栈跟踪

  .   ____          _            __ _ _
 /\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.4.3.RELEASE)

2017-01-10 09:41:48.902  INFO 1860 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : Starting application on alwihasolaptop with PID 1860 (started by alwi in C:\alwi\Workspace_SPZ\GCodeClient)
2017-01-10 09:41:48.905  INFO 1860 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : No active profile set, falling back to default profiles: default
2017-01-10 09:41:48.965  INFO 1860 --- [JavaFX-Launcher] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@18660f3: startup date [Tue Jan 10 09:41:48 CET 2017]; root of context hierarchy
2017-01-10 09:41:49.917  INFO 1860 --- [JavaFX-Launcher] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-01-10 09:41:49.927  INFO 1860 --- [JavaFX-Launcher] o.s.boot.SpringApplication               : Started application in 1.384 seconds (JVM running for 1.969)
Created entry id=73
Created entry id=74
Created entry id=75
Created entry id=76
Created entry id=77
Created entry id=78
java.lang.RuntimeException: Testing data upload procedure break.
    at tmp.dao.EntryDao.db_transaction_test(EntryDao.java:53)
    at tmp.dao.EntryDao$$FastClassBySpringCGLIB$$a857b433.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:721)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    at org.springframework.transaction.interceptor.TransactionInterceptor.proceedWithInvocation(TransactionInterceptor.java:99)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:282)
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:656)
    at tmp.dao.EntryDao$$EnhancerBySpringCGLIB$e8651e.db_transaction_test(<generated>)
    at tmp.SpringTransactional.start(SpringTransactional.java:45)
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplication12(LauncherImpl.java:863)
    at com.sun.javafx.application.PlatformImpl.lambda$runAndWait5(PlatformImpl.java:326)
    at com.sun.javafx.application.PlatformImpl.lambda$null3(PlatformImpl.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater4(PlatformImpl.java:294)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null8(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)
Created entry id=73
Created entry id=74
Created entry id=75
Created entry id=76
Created entry id=77
Created entry id=78
2017-01-10 09:41:50.545  INFO 1860 --- [lication Thread] s.c.a.AnnotationConfigApplicationContext : Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@18660f3: startup date [Tue Jan 10 09:41:48 CET 2017]; root of context hierarchy
java.lang.RuntimeException: Testing data upload procedure break.
    at tmp.SpringTransactional.db_transaction_test(SpringTransactional.java:71)
    at tmp.SpringTransactional.start(SpringTransactional.java:52)
    at com.sun.javafx.application.LauncherImpl.lambda$launchApplication12(LauncherImpl.java:863)
    at com.sun.javafx.application.PlatformImpl.lambda$runAndWait5(PlatformImpl.java:326)
    at com.sun.javafx.application.PlatformImpl.lambda$null3(PlatformImpl.java:295)
    at java.security.AccessController.doPrivileged(Native Method)
    at com.sun.javafx.application.PlatformImpl.lambda$runLater4(PlatformImpl.java:294)
    at com.sun.glass.ui.InvokeLaterDispatcher$Future.run(InvokeLaterDispatcher.java:95)
    at com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
    at com.sun.glass.ui.win.WinApplication.lambda$null8(WinApplication.java:191)
    at java.lang.Thread.run(Thread.java:745)
2017-01-10 09:41:50.546  INFO 1860 --- [lication Thread] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown

Process finished with exit code 0

解决方案:

到目前为止我发现的最佳解决方案是使用 Spring TransactionTemplate 以及额外的回调 class:

package tmp.dao;

public abstract class DbTransactionTask { public abstract void executeTask(); }

并且在SpringTransactionalclassdb_transaction_test()方法中(注意@Transactional出局了):

private void db_transaction_test() {
    DbTransactionTask dbTask = new DbTransactionTask() {
        @Override
        public void executeTask() {
            for (int i = 0; i < 10; i++) {
                try {
                    int entry_name = dao.getEntryId("entry_" + i);
                    System.out.println("Created entry id=" + entry_name);
                } catch (EntryDao.DaoException e) {
                    e.printStackTrace();
                }

                if (i == 5) {
                    throw new RuntimeException("Testing data upload procedure break.");
                }
            }
        }
    };

    dao.executeTransactionWithoutResult(dbTask);
}

EntryDao class 需要此附加代码:

@Autowired
private TransactionTemplate transactionTemplate;

public void executeTransactionWithoutResult(DbTransactionTask dbTask) {
    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            dbTask.executeTask();
        }
    });
}