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 事务处理机制。据我了解,以下代码应该:
- 初始化并启动 JavaFX 应用程序 - 创建并显示 GUI 线框
- 初始化Spring
- 配置并注入 JdbcTemplate 依赖项
- 启动交易处理机制并开始交易
- 使用 jdbcTemplate 对象在
for loop
的数据库中创建 5 个条目
- 模拟错误(通过抛出
RuntimeException
)
- 还原数据库上的操作
- 退出
所以,总结一下:当 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(); }
并且在SpringTransactional
classdb_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();
}
});
}
我想使用 JavaFX 和数据库访问 Spring JDBC。然而,我对 Spring 完全陌生,似乎我无法完全理解它的功能,尤其是交易处理...
我已将以下依赖项添加到我的项目中:
compile 'org.springframework.boot:spring-boot-starter-jdbc'
runtime 'mysql:mysql-connector-java'
... 我想在 GUI 应用程序对数据库执行操作时使用 Spring 事务处理机制。据我了解,以下代码应该:
- 初始化并启动 JavaFX 应用程序 - 创建并显示 GUI 线框
- 初始化Spring
- 配置并注入 JdbcTemplate 依赖项
- 启动交易处理机制并开始交易
- 使用 jdbcTemplate 对象在
for loop
的数据库中创建 5 个条目
- 模拟错误(通过抛出
RuntimeException
) - 还原数据库上的操作
- 退出
所以,总结一下:当 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(); }
并且在SpringTransactional
classdb_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();
}
});
}