【Spring】09 后续的学习补充 vol3
原生JDBC事务:
package dao; import cn.dzz.util.DruidUtil; import org.apache.commons.dbutils.QueryRunner; import org.junit.Test; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; /** * @author DaiZhiZhou * @file OA-Project * @create 2020-08-03 15:13 */ public class TransactionTest { QueryRunner queryRunner = new QueryRunner(); DataSource dataSource = DruidUtil.getDataSource(); @Test public void tx01() throws SQLException { Connection connection = null; try { // 事务的前提是我们使用的连接需要保持一致 connection = dataSource.getConnection(); // 开启事务,关闭MYSQL的自动提交 connection.setAutoCommit(false); int update01 = queryRunner.update(connection, "这是执行影响数据库的SQL1"); // 中间的执行过程将会产生异常 int update02 = queryRunner.update(connection, "这是执行影响数据库的SQL2"); // 如果一切正常,则进行提交 connection.commit(); } catch (Exception exception) { exception.printStackTrace(); // 发生了异常,触发Catch,执行回滚 connection.rollback(); } finally { // 无论回滚还是提交,最终释放资源 connection.close(); } } }
事务封装类:
即数据源提供给这个封装类,当我们Dao层执行SQL的连接对象就要从这里获取,
不能独立获取,否则事务将无法作用了,因为获取的不是同一个连接对象就没有办法处理了
package cn.dzz.util; /** * @author DaiZhiZhou * @file OA-Project * @create 2020-08-03 15:27 */ import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class TransactionManager { // 私有化 private TransactionManager(){} // 当前线程对象,用以存放和绑定连接对象 private static final ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); // 我们需要一个数据源来获取连接,且事务正常执行的条件是来自于同一个连接对象才会有效 private static DataSource dataSource ; /** * 我不希望只能从一个数据源获取,这个对象可以是任意类型的 * @param dataSource */ public static void setDataSource(DataSource dataSource) { TransactionManager.dataSource = dataSource; } /** * 首先是获取连接对象获取,如果当前线程实例中没有连接对象,则从这个数据源中获取 * @return */ public static Connection getCurrentThreadConnection(){ Connection connection = null; try { connection = threadLocal.get(); if (connection == null) { connection = dataSource.getConnection(); threadLocal.set(connection); } } catch (SQLException sqlException) { sqlException.printStackTrace(); } return connection; } /** * 开启事务 */ public static void begin(){ Connection connection = getCurrentThreadConnection(); try { connection.setAutoCommit(false); } catch (SQLException sqlException) { sqlException.printStackTrace(); } } /** * 如果业务逻辑的每一步代码都正常执行,无任何异常,则开始提交 */ public static void commit(){ Connection connection = getCurrentThreadConnection(); try { connection.commit(); } catch (SQLException sqlException) { sqlException.printStackTrace(); } } /** * 如果业务逻辑发生了错误,立刻捕获,进入catch,执行回滚 */ public static void rollback(){ Connection connection = getCurrentThreadConnection(); try { connection.rollback(); } catch (SQLException sqlException) { sqlException.printStackTrace(); } } /** * 最后,不管是失败还是成功,都需要释放资源 */ public static void close(){ Connection connection = getCurrentThreadConnection(); try { connection.close(); threadLocal.remove(); } catch (SQLException sqlException) { sqlException.printStackTrace(); } } }
业务层的应用:
如果要在业务添加就必须要为每一个业务的方法这样添加
public JsonResult altUserById2(HttpServletRequest request) { try { // 开启事务 TransactionManager.begin(); int i = userDao.updateUserById(new User( Integer.valueOf(request.getParameter("user_id")), request.getParameter("user_name"), request.getParameter("user_password"), Integer.valueOf(request.getParameter("user_status")), Integer.valueOf(request.getParameter("user_is_del")) )); // 提交 TransactionManager.commit(); return new JsonResult(100, "修改成功"); } catch (Exception exception) { // 回滚 TransactionManager.rollback(); exception.printStackTrace(); } finally { // 释放 TransactionManager.close(); } return new JsonResult(200, "修改失败"); }
但是业务方法那么多,怎么可能一个个添加,
何况事务并不是业务的范畴之内,但是这出现了异常就得撤销SQL执行恢复原状啊
演化到动态代理完成:
package cn.dzz.util; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * @author DaiZhiZhou * @file OA-Project * @create 2020-08-03 15:49 */ public class ServiceTxProxy <GenericService>{ /** * 目标对象,也就是业务对象 * 我们希望不仅仅是一个业务逻辑接口,应该整个业务逻辑层的接口都可以被代理 */ private GenericService genericService; /** * 我创建这个代理对象可以不先设置目标对象 * 对实例重载 */ public ServiceTxProxy() {} /** * 创建实例时就注入目标对象 * @param genericService */ public ServiceTxProxy(GenericService genericService) { this.genericService = genericService; } /** * 调用注入方法来注入目标对象 * @param genericService */ public void setGenericService(GenericService genericService) { this.genericService = genericService; } /** * 得到代理对象,我们将由代理对象来操作业务 * 在得到代理对象之前注入数据源,我们不希望只能使用单一的数据源 * 业务层的调用,一定是在代理之后执行,所以在这里注入数据源不会造成空指针的问题 * @param * @return */ public GenericService getGenericService() { // 调用代理类的创建代理实例方法 return (GenericService) Proxy.newProxyInstance( // 第一参数:类加载器实例,可以反射任意实例获取 genericService.getClass().getClassLoader(), // 第二参数:目标对象的接口数组,因为接口是允许多实现的 genericService.getClass().getInterfaces(), // 第三参数: 调用处理器的实现实例,需要重新调用方法,对目标对象增强 new InvocationHandler() { /** * * @param proxy ? 代理对象? * @param method * @param args * @return * @throws Throwable */ @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 目标对象就是我们的Service对象,在对象调用之前,开启事务 TransactionManager.begin(); Object theGenericServiceData = null; try { // 目标对象的方法执行!!! theGenericServiceData = method.invoke(genericService, args); // 对执行完毕的Service对象 进行提交或者回滚 TransactionManager.commit(); } catch (Exception exception) { exception.printStackTrace(); TransactionManager.rollback(); } return theGenericServiceData; } } ); } }
注意事务管理器的数据源设置问题。。。
Spring事务管理:
依赖的组件坐标:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.2.8.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.2.8.RELEASE</version> </dependency>
XML文件注意约束信息:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> </beans>
配置Spring提供的事务管理器:
需要数据源注入
<!--事务--> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean>
配置事务的通知,引用事务管理器:
<tx:advice id="txAdvice" transaction-manager="txManager"> <!--在 tx:advice 标签内部 配置事务的属性 --> <tx:attributes> <!-- 指定方法名称:是业务核心方法 read-only:是否是只读事务。默认 false,不只读。 isolation:指定事务的隔离级别。默认值是使用数据库的默认隔离级别。 propagation:指定事务的传播行为。 timeout:指定超时时间。默认值为:-1。永不超时。 rollback-for:用于指定一个异常,当执行产生该异常时,事务回滚。产生其他异常,事务不回滚。 没有默认值,任何异常都回滚。 no-rollback-for:用于指定一个异常,当产生该异常时,事务不回滚,产生其他异常时,事务回 滚。没有默认值,任何异常都回滚。 --> <tx:method name="*" read-only="false" propagation="REQUIRED"/> <tx:method name="find*" read-only="true" propagation="SUPPORTS"/> </tx:attributes> </tx:advice>
结合动态代理:
<!-- 配置 aop --> <aop:config> <!-- 配置切入点表达式 --> <aop:pointcut expression="execution(* com.tc51.service.impl.*.*(..))" id="pt1"/> </aop:config>
配置切入点表达式和事务通知的对应关系:
<!-- 配置 aop --> <aop:config> <!-- 配置切入点表达式 --> <aop:pointcut expression="execution(* com.tc51.service.impl.*.*(..))" id="pt1"/> <aop:advisor advice-ref="txAdvice" pointcut-ref="pt1" /> </aop:config>
Dao层的设计:
查询功能不需要这个事务,注入好的数据源给的连接对象是独立的
但是如果是更新操作,就需要从事务管理器中获取连接保证连接一致
@Component public class AccountDao implements IAccountDao { @Autowired private QueryRunner queryRunner; public Account getAccount(String name) throws SQLException { String sql = "SELECT * FROM `account` WHERE `name`=?"; return queryRunner.query(sql, new BeanHandler<Account>(Account.class),name); } public int updateAccount(Account account) throws SQLException { String sql = "update account set name=?,money=? where id=?"; Connection conn= DataSourceUtils.getConnection(queryRunner.getDataSource()); System.out.println(conn); Object[] object = {account.getName(), account.getMoney(), account.getId()}; return queryRunner.update(conn,sql, object); } }