【JDBC】自定义事务注解实现
参考自:
https://blog.csdn.net/qq_28986619/article/details/94451889
数据源选型,我采用的是C3P0,下面是需要的依赖:
<?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>cn.dzz</groupId> <artifactId>Persist-Framework</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>8</maven.compiler.source> <maven.compiler.target>8</maven.compiler.target> </properties> <dependencies> <!-- https://mvnrepository.com/artifact/com.mchange/c3p0 --> <dependency> <groupId>com.mchange</groupId> <artifactId>c3p0</artifactId> <version>0.9.5.2</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.20</version> </dependency> <!-- https://mvnrepository.com/artifact/junit/junit --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> <!-- https://mvnrepository.com/artifact/dom4j/dom4j --> <dependency> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> <version>1.6.1</version> </dependency> </dependencies> </project>
c3p0-config.xml配置信息
我设有本机两套MySQL实例,一个8 一个5
<?xml version="1.0" encoding="UTF-8"?> <c3p0-config> <!-- 默认配置,如果没有指定则使用这个配置 --> <!-- <default-config>--> <!-- <property name="user">zhanghanlun</property>--> <!-- <property name="password">123456</property>--> <!-- <property name="jdbcUrl">jdbc:mysql://localhost:3306/zhanghanlun</property>--> <!-- <property name="driverClass">com.mysql.jdbc.Driver</property>--> <!-- <property name="checkoutTimeout">30000</property>--> <!-- <property name="idleConnectionTestPeriod">30</property>--> <!-- <property name="initialPoolSize">3</property>--> <!-- <property name="maxIdleTime">30</property>--> <!-- <property name="maxPoolSize">100</property>--> <!-- <property name="minPoolSize">2</property>--> <!-- <property name="maxStatements">200</property>--> <!-- </default-config>--> <!-- 命名的配置,可以通过方法调用实现 --> <named-config name="my-info"> <property name="user">root</property> <property name="password">123456</property> <property name="jdbcUrl">jdbc:mysql://localhost:3308/my-info?serverTimezone=Asia/Shanghai</property> <property name="driverClass">com.mysql.cj.jdbc.Driver</property> <!-- 如果池中数据连接不够时一次增长多少个 --> <property name="acquireIncrement">5</property> <!-- 初始化数据库连接池时连接的数量 --> <property name="initialPoolSize">20</property> <!-- 数据库连接池中的最大的数据库连接数 --> <property name="maxPoolSize">25</property> <!-- 数据库连接池中的最小的数据库连接数 --> <property name="minPoolSize">5</property> </named-config> <named-config name="dev-base"> <property name="user">root</property> <property name="password">123456</property> <property name="jdbcUrl">jdbc:mysql://localhost:3307/devbase?serverTimezone=Asia/Shanghai</property> <property name="driverClass">com.mysql.cj.jdbc.Driver</property> <!-- 如果池中数据连接不够时一次增长多少个 --> <property name="acquireIncrement">5</property> <!-- 初始化数据库连接池时连接的数量 --> <property name="initialPoolSize">20</property> <!-- 数据库连接池中的最大的数据库连接数 --> <property name="maxPoolSize">25</property> <!-- 数据库连接池中的最小的数据库连接数 --> <property name="minPoolSize">5</property> </named-config> </c3p0-config>
获取全部数据源:
package cn.dzz.persist.util; import com.mchange.v2.c3p0.ComboPooledDataSource; import org.dom4j.Attribute; import org.dom4j.Document; import org.dom4j.Element; import org.dom4j.io.SAXReader; import javax.sql.DataSource; import java.io.File; import java.net.URL; import java.sql.SQLException; import java.util.HashMap; import java.util.List; import java.util.Map; public class DataSourceUtil { private DataSourceUtil() {} private static Map<String, DataSource> dataSourceMap; static { try { dataSourceMap = new HashMap<>(); // 读取配置文件封装成文件对象 URL resource = DataSourceUtil.class.getClassLoader().getResource("c3p0-config.xml"); File f = new File(resource.getFile()); // Dom4J转换成Dom对象 SAXReader reader = new SAXReader(); Document doc = reader.read(f); // 得到节点对象根据xml配置信息读取 Element root = doc.getRootElement(); List<Element> elements = root.elements("named-config"); for (Element element : elements) { Attribute attribute = element.attribute("name"); String value = attribute.getValue(); // 逐一创建获取 dataSourceMap.put(value, new ComboPooledDataSource(value)); } } catch (Exception exception) { exception.printStackTrace(); } } public static DataSource getDataSourceByConfigName(String configName) { return dataSourceMap.get(configName); } // 测试 public static void main(String[] args) throws SQLException { System.out.println(getDataSourceByConfigName("my-info").getConnection()); } }
跨库暂时不考虑,要实现统一事务,这里要统一从线程中获取连接对象
package cn.dzz.persist.util; import javax.sql.DataSource; import java.sql.Connection; public class ConnectionUtil { private static DataSource dataSource; private static ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); static { dataSource = DataSourceUtil.getDataSourceByConfigName("my-info"); } public static Connection getConnection() { try { Connection connection = threadLocal.get(); if (null == connection) { connection = dataSource.getConnection(); threadLocal.set(connection); } return connection; } catch (Exception e) { e.printStackTrace(); return null; } } }
用于标记声明事务类的注解
package cn.dzz.persist.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 用于标记需要事务操作的类 */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface TxConn { }
JDK代理实现:
package cn.dzz.persist.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.sql.Connection; public class ProxyBean { private Object target; private Connection connection; public ProxyBean(Object target, Connection connection) { this.target = target; this.connection = connection; } public Object getBean() { Object o = Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { connection.setAutoCommit(false); method.invoke(target, args); connection.commit(); } catch (Exception e) { connection.rollback(); } finally { connection.close(); } return null; } }); return o; } }
然后通过工厂类去获取代理对象
package cn.dzz.persist.proxy; import cn.dzz.persist.annotation.TxConn; import cn.dzz.persist.util.ConnectionUtil; import java.sql.Connection; /** * 目标对象工程 */ public class TargetBeanFactory { public static Object getTargetBean(Class<?> targetClass) throws Exception { Object t = targetClass.newInstance(); if (t.getClass().isAnnotationPresent(TxConn.class)) { Connection connection = ConnectionUtil.getConnection(); ProxyBean proxyBean = new ProxyBean(t , connection); return proxyBean.getBean(); } return t; } }
测试的业务类:
注意一些问题,就是里面的SQL执行和业务逻辑全都把异常抛出去,这样才能触发代理对象的事务
如果自己TryCatch了,直接方法调用里面自行处理异常,那代理的对象触发不到回滚就没意义了
package cn.dzz.persist.service; import cn.dzz.persist.annotation.TxConn; import cn.dzz.persist.util.ConnectionUtil; import java.sql.Connection; import java.sql.PreparedStatement; @TxConn public class TestServiceImpl implements TestService{ @Override public void updateTest() throws Exception { Connection connection = ConnectionUtil.getConnection(); final String sql = "UPDATE `my-info`.`school_class` SET `CLASS_NAME` = '修改1班级 - 0001' WHERE `CLASS_ID` = 1;\n"; PreparedStatement preparedStatement = connection.prepareStatement(sql); preparedStatement.execute(); int a = 10 / 0; final String sql2 = "UPDATE `my-info`.`school_class` SET `CLASS_NAME` = '修改2班级 - 0002' WHERE `CLASS_ID` = 2;\n"; PreparedStatement preparedStatement2 = connection.prepareStatement(sql2); preparedStatement2.execute(); } }
测试事务是否有效:
import cn.dzz.persist.proxy.TargetBeanFactory; import cn.dzz.persist.service.TestService; import cn.dzz.persist.service.TestServiceImpl; import cn.dzz.persist.util.JdbcUtil; import org.junit.Test; import java.util.List; import java.util.Map; public class TransactionTest { @Test public void transactionTest() throws Exception { TestService testService = (TestService)TargetBeanFactory.getTargetBean(TestServiceImpl.class); testService.updateTest(); } }