JDBC工具类(反射版)——新增事务处理
JDBC工具类反射版完善讲义——新增事务处理一、讲义前言本节课核心目标在原有“通用JDBC工具类反射版”基础上新增事务处理功能解决多SQL操作如转账、批量新增的“原子性”问题要么全部成功要么全部失败同时保证工具类的线程安全适配多线程/Web环境使用。前置基础掌握JDBC基础操作、反射机制、Druid连接池使用了解事务的核心概念ACID特性。最终效果工具类保留原有通用增删改、通用查询功能新增事务开启、提交、回滚方法且完全兼容原有业务代码无需修改业务逻辑即可使用事务。二、原有工具类痛点分析为什么要加事务原有工具类已实现的功能Druid连接池初始化获取连接替代DriverManager提升性能通用增删改update方法、通用查询query方法反射封装结果集资源关闭方法分查询/增删改两种场景核心痛点无事务控制多个SQL操作如“扣钱”“加钱”无法保证原子性若中间某一步失败前面的操作无法撤销导致数据不一致例转账时A扣钱成功B加钱失败钱凭空消失。线程不安全多线程环境下可能出现“一个线程的连接被另一个线程占用”导致事务混乱、连接泄露。代码冗余参数设置逻辑重复可进一步抽取优化。三、完善思路核心逻辑事务核心用ThreadLocal绑定当前线程的连接保证“同一个线程全程使用同一个连接”——事务的本质是“同一个连接下的多个SQL要么一起提交要么一起回滚”。事务方法新增3个核心方法开启、提交、回滚控制连接的“自动提交”状态默认true开启事务后设为false。资源管理优化关闭逻辑事务中的连接不提前归还由事务统一管理提交/回滚后再关闭连接、清空ThreadLocal。代码优化抽取参数设置逻辑减少冗余提升可维护性。四、完善后完整代码带详细注释知识点标注packagecom.demo2;importcom.alibaba.druid.pool.DruidDataSource;importcom.alibaba.druid.pool.DruidDataSourceFactory;importjava.lang.reflect.Field;importjava.sql.Connection;importjava.sql.PreparedStatement;importjava.sql.ResultSet;importjava.sql.ResultSetMetaData;importjava.sql.SQLException;importjava.util.ArrayList;importjava.util.List;importjava.util.Properties;/** * 通用JDBC工具类反射版 事务支持 线程安全 * 注释说明 * 1. 保留原有反射查询、通用增删改、Druid连接池功能 * 2. 新增事务控制ThreadLocal绑定连接 * 3. 优化线程安全、代码冗余问题 */publicclassJdbcUtils{// 数据库配置和原有一致无需修改可根据自己的数据库调整privatestaticfinalStringDRIVERcom.mysql.cj.jdbc.Driver;privatestaticfinalStringURLjdbc:mysql://localhost:3306/bookdb0402?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue;privatestaticfinalStringUSERroot;privatestaticfinalStringPASSWORD123456;// 德鲁伊连接池核心和原有一致负责管理数据库连接提升性能privatestaticDruidDataSourcedataSource;// 核心线程本地变量ThreadLocal// 作用将连接和当前线程绑定保证同一个线程全程使用同一个连接事务的关键// 原理ThreadLocal为每个线程维护一个独立的变量副本互不干扰解决线程安全问题privatestaticfinalThreadLocalConnectionCONNECTION_THREAD_LOCALnewThreadLocal();/** * 静态代码块类加载时执行一次初始化Druid连接池 * 注意静态代码块只执行一次避免重复初始化连接池提升性能 */static{try{// 1. 封装数据库配置和连接池配置PropertiespropsnewProperties();props.setProperty(url,URL);// 数据库地址props.setProperty(username,USER);// 数据库用户名props.setProperty(password,PASSWORD);// 数据库密码props.setProperty(driverClassName,DRIVER);// 驱动类// 2. Druid连接池核心配置按需调整和原有一致props.setProperty(initialSize,5);// 初始连接数启动时创建5个连接避免首次请求耗时props.setProperty(maxActive,20);// 最大活跃连接数控制并发上限防止连接过多拖垮数据库props.setProperty(maxWait,60000);// 最大等待时间请求连接时最多等60秒避免无限阻塞props.setProperty(minIdle,3);// 最小空闲连接数空闲时保留3个连接减少创建连接频率// 3. 初始化连接池Druid工厂创建连接池实例dataSource(DruidDataSource)DruidDataSourceFactory.createDataSource(props);}catch(Exceptione){// 初始化失败直接抛出运行时异常提醒开发者排查配置如驱动、URL、密码是否正确thrownewRuntimeException(初始化Druid连接池失败,e);}}// 新增事务核心方法重点 /** * 开启事务 * 核心逻辑获取当前线程的连接关闭“自动提交”默认true即执行SQL后立即提交 * 开启事务后所有SQL操作都不会立即提交需手动调用commitTransaction() */publicstaticvoidbeginTransaction(){try{// 1. 获取当前线程绑定的连接没有则从连接池获取自动绑定ConnectionconngetConnection();// 2. 关闭自动提交开启事务的核心操作conn.setAutoCommit(false);}catch(SQLExceptione){thrownewRuntimeException(开启事务失败,e);}}/** * 提交事务 * 核心逻辑提交当前线程连接中的所有SQL操作然后关闭连接、清空ThreadLocal * 只有所有SQL都执行成功才调用此方法 */publicstaticvoidcommitTransaction(){try{// 1. 获取当前线程绑定的连接事务中一定有连接无需重新获取ConnectionconnCONNECTION_THREAD_LOCAL.get();if(conn!null){// 2. 提交事务将所有未提交的SQL一次性提交到数据库conn.commit();}}catch(SQLExceptione){thrownewRuntimeException(提交事务失败,e);}finally{// 无论提交成功与否都要关闭连接、清空ThreadLocal释放资源closeConnection();}}/** * 回滚事务 * 核心逻辑撤销当前线程连接中的所有SQL操作恢复到事务开启前的状态然后关闭连接 * 只要有一个SQL执行失败就调用此方法 */publicstaticvoidrollbackTransaction(){try{// 1. 获取当前线程绑定的连接ConnectionconnCONNECTION_THREAD_LOCAL.get();if(conn!null){// 2. 回滚事务撤销所有已执行但未提交的SQLconn.rollback();}}catch(SQLExceptione){thrownewRuntimeException(回滚事务失败,e);}finally{// 回滚后必须关闭连接、清空ThreadLocal避免资源泄露closeConnection();}}// 优化获取连接线程安全重点 /** * 获取数据库连接线程安全 * 优化点优先获取当前线程已绑定的连接事务中没有则从连接池获取并绑定 * 保证同一个线程全程使用同一个连接支撑事务的原子性 */publicstaticConnectiongetConnection(){try{// 1. 先从ThreadLocal中获取当前线程的连接事务中已绑定ConnectionconnCONNECTION_THREAD_LOCAL.get();if(connnull){// 2. 无事务从连接池获取新连接并绑定到当前线程conndataSource.getConnection();CONNECTION_THREAD_LOCAL.set(conn);}returnconn;}catch(SQLExceptione){thrownewRuntimeException(获取数据库连接失败,e);}}// 优化资源关闭方法适配事务重点 /** * 关闭资源查询场景连接、PreparedStatement、ResultSet * 优化点事务中的连接不在这里关闭由事务统一管理commit/rollback后关闭 */publicstaticvoidclose(Connectionconn,PreparedStatementpstmt,ResultSetrs){// 1. 关闭ResultSet查询结果集必须先关try{if(rs!null)rs.close();}catch(SQLExceptionignored){}// 忽略关闭异常避免影响后续资源关闭// 2. 关闭PreparedStatementSQL执行对象try{if(pstmt!null)pstmt.close();}catch(SQLExceptionignored){}// 3. 关键判断当前连接是否是线程绑定的连接事务中的连接ConnectionthreadConnCONNECTION_THREAD_LOCAL.get();// 若是事务中的连接不在这里关闭交给事务方法commit/rollback统一关闭if(conn!nullconnthreadConn){return;}// 若非事务中的连接直接关闭归还到连接池try{if(conn!null)conn.close();}catch(SQLExceptionignored){}}/** * 关闭资源增删改场景连接、PreparedStatement * 重载方法调用上面的关闭方法省略ResultSet */publicstaticvoidclose(Connectionconn,PreparedStatementpstmt){close(conn,pstmt,null);}/** * 私有方法关闭连接并清空ThreadLocal事务专用 * 作用事务提交/回滚后释放连接归还到连接池清空线程绑定的连接避免内存泄露 */privatestaticvoidcloseConnection(){// 1. 获取当前线程绑定的连接ConnectionconnCONNECTION_THREAD_LOCAL.get();if(conn!null){try{// 恢复连接的自动提交状态避免下次使用时连接仍处于事务模式conn.setAutoCommit(true);// 关闭连接归还到Druid连接池不是真正关闭物理连接conn.close();}catch(SQLExceptionignored){}}// 2. 清空ThreadLocal解除线程和连接的绑定CONNECTION_THREAD_LOCAL.remove();}// 保留通用增删改方法无修改适配事务 /** * 通用增删改insert / update / delete * param sqlSQL语句占位符? 替代具体参数 * param paramsSQL语句中的参数和占位符一一对应 * return 受影响的行数 */publicstaticintupdate(Stringsql,Object...params){Connectionconnnull;PreparedStatementpstmtnull;try{// 注意这里获取的连接是线程绑定的连接事务中或连接池连接无事务conngetConnection();pstmtconn.prepareStatement(sql);// 抽取参数设置逻辑减少冗余新增优化setParams(pstmt,params);// 执行增删改返回受影响行数returnpstmt.executeUpdate();}catch(SQLExceptione){thrownewRuntimeException(执行增删改SQL失败,e);}finally{// 关闭资源事务中的连接不会被关闭非事务的会被关闭close(conn,pstmt);}}// 保留通用查询方法无修改适配事务 /** * 通用查询多行数据返回ListT * 核心反射机制将ResultSet结果集封装为指定实体类的集合 * param clazz要封装的实体类字节码对象如Book.class * param sql查询SQL语句占位符? 替代具体参数 * param paramsSQL语句中的参数和占位符一一对应 * return 实体类集合每行数据对应一个实体对象 */publicstaticTListTquery(ClassTclazz,Stringsql,Object...params){Connectionconnnull;PreparedStatementpstmtnull;ResultSetrsnull;ListTlistnewArrayList();try{conngetConnection();pstmtconn.prepareStatement(sql);setParams(pstmt,params);rspstmt.executeQuery();// 获取结果集的元数据列信息列名、列数ResultSetMetaDatametaDatars.getMetaData();intcolumnCountmetaData.getColumnCount();// 遍历结果集每一行封装为一个实体对象while(rs.next()){// 反射创建实体对象调用无参构造方法Tentityclazz.getDeclaredConstructor().newInstance();// 遍历每一列给实体对象的对应属性赋值for(inti1;icolumnCount;i){// 获取列名getColumnLabel获取别名无别名则获取列名适配SQL别名场景StringcolumnNamemetaData.getColumnLabel(i);// 获取当前列的值Object类型适配所有数据类型Objectvaluers.getObject(i);// 反射获取实体类的对应字段忽略访问权限即使是private字段也能操作Fieldfieldentity.getClass().getDeclaredField(columnName);field.setAccessible(true);// 给实体对象的字段赋值field.set(entity,value);}// 将封装好的实体对象添加到集合list.add(entity);}}catch(Exceptione){thrownewRuntimeException(执行查询SQL失败,e);}finally{// 关闭资源ResultSet、PreparedStatement连接根据是否在事务中决定是否关闭close(conn,pstmt,rs);}returnlist;}// 新增抽取参数设置方法优化代码冗余 /** * 私有方法给PreparedStatement设置参数代码复用 * 避免在update和query方法中重复写参数设置的for循环 * param pstmtSQL执行对象 * param params要设置的参数数组 * throws SQLException参数设置失败异常 */privatestaticvoidsetParams(PreparedStatementpstmt,Object...params)throwsSQLException{if(params!nullparams.length0){// 循环设置参数占位符索引从1开始JDBC规范参数数组索引从0开始for(inti0;iparams.length;i){pstmt.setObject(i1,params[i]);}}}}五、核心知识点详解重点突破1. ThreadLocal 核心作用事务的关键为什么要用ThreadLocal事务要求“同一个连接下的多个SQL要么一起提交要么一起回滚”。多线程环境下若不绑定连接可能出现“线程A的连接被线程B占用”导致线程A的事务被线程B的操作干扰。ThreadLocal为每个线程维护独立的连接副本线程之间互不干扰保证线程安全和事务完整性。关键细节事务结束后提交/回滚必须调用CONNECTION_THREAD_LOCAL.remove()否则会导致内存泄露线程销毁后ThreadLocal仍持有连接引用。2. 事务三大方法的执行流程// 标准执行流程try-catch包裹保证异常时回滚try{JdbcUtils.beginTransaction();// 1. 开启事务关闭自动提交// 2. 执行多个SQL操作共用同一个连接JdbcUtils.update(sql1,params1);JdbcUtils.update(sql2,params2);JdbcUtils.commitTransaction();// 3. 所有操作成功提交事务}catch(Exceptione){JdbcUtils.rollbackTransaction();// 4. 有异常回滚事务e.printStackTrace();}注意事务中只能调用工具类的update/query方法不能自己创建Connection否则会脱离事务控制。3. 资源关闭逻辑优化适配事务原有关闭方法的问题无论是否在事务中都会关闭连接导致事务中连接被提前归还事务失效。优化后逻辑判断连接是否是当前线程绑定的连接事务中的连接。若是不关闭交给事务方法commit/rollback统一关闭。若不是直接关闭归还到连接池。4. 代码优化点抽取setParams方法原有update和query方法中都有“循环设置PreparedStatement参数”的逻辑代码冗余。抽取后将参数设置逻辑封装为私有方法setParams减少代码重复提升可维护性后续修改参数设置逻辑只需改这一个方法。六、事务使用示例实操演示以“转账业务”为例演示事务的使用核心扣钱和加钱必须同时成功或同时失败。/** * 转账业务示例A用户扣钱B用户加钱 * 测试事务要么都成功要么都失败 */publicclassTransactionDemo{publicstaticvoidmain(String[]args){// 模拟转账用户1id1给用户2id2转100元Stringsql1UPDATE user SET balance balance - 100 WHERE id ?;// A扣钱Stringsql2UPDATE user SET balance balance 100 WHERE id ?;// B加钱try{// 1. 开启事务JdbcUtils.beginTransaction();System.out.println(事务开启成功);// 2. 执行两个SQL操作introw1JdbcUtils.update(sql1,1);// 给id1的用户扣钱introw2JdbcUtils.update(sql2,2);// 给id2的用户加钱// 模拟异常测试回滚可注释掉// int i 1 / 0;// 3. 提交事务只有两个SQL都执行成功才提交JdbcUtils.commitTransaction();System.out.println(转账成功A扣钱row1行B加钱row2行);}catch(Exceptione){// 4. 异常回滚只要有一个SQL失败就回滚所有操作JdbcUtils.rollbackTransaction();System.out.println(转账失败事务回滚);e.printStackTrace();}}}测试场景正常情况两个SQL都执行成功事务提交数据库中两个用户的余额正确更新。异常情况如注释中的1/0执行到异常时进入catch事务回滚两个用户的余额都恢复到转账前的状态数据一致。七、注意事项避坑指南数据库驱动和Druid依赖必须导入否则会报“类找不到”异常pom.xml中添加依赖。URL中添加allowPublicKeyRetrievaltrue避免MySQL8.0以上版本出现“公钥检索失败”异常。事务必须用try-catch包裹否则异常时无法触发回滚导致事务失效。实体类必须有无参构造方法否则反射创建对象时会报错clazz.getDeclaredConstructor().newInstance()。实体类的字段名必须和数据库表的列名一致或SQL中给列起别名别名和字段名一致否则反射赋值时会找不到字段。事务执行期间不要手动关闭连接否则会导致事务提前结束出现数据不一致。八、总结本节课我们完成了JDBC工具类的完善核心收获理解了事务的核心需求原子性掌握了ThreadLocal绑定连接的实现原理。新增了事务三大方法开启、提交、回滚实现了多SQL操作的原子性控制。优化了工具类的线程安全和代码冗余提升了工具类的实用性和可维护性。掌握了事务的使用场景和实操方法能解决实际开发中的数据一致性问题如转账、批量操作。