熟悉Spring和“楼上”平台的同事都知道,在基于这两种框架进行开发时,如果某个方法需要放到事务中执行,一般只需要在该方法头上打个“Trans”标记就可以了,这种实现方式,由于其低侵入式、简单、易用等特点,颇受广大开发者的喜爱,然而其内在的实现原理是什么呢?相信很多开发者并不清楚,本文就为你揭开这层神秘的面纱,自己动手实现这种看似神奇的效果。
关键知识点:
1、动态代理,通过动态代理,切入 事务启动、提交、回滚处理等操作。
2、反射,通过反射,动态创建实例对象,动态执行方法、并根据注解信息执行其他一些操作。
3、线程局部变量,通过线程局部变量实现在不同方法间完成事务的传递。
示例:
1、创建一个注解Trans,该注解所标注的方法将放在事务中执行,如果方法执行出错(抛异常),则回滚事务,如果成功执行,则提交事务。
package framework.db; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(value=RetentionPolicy.RUNTIME) public @interface Trans { }
2、创建一个数据库连接的工具类,数据库连接统一从该工具类中取得,获取数据库连接时,首先从线程局部变量中查找,如果线程局部变量中不存在,则通过工具类新创建一个,并放入线程局部变量。
扫描二维码关注公众号,回复:
1383103 查看本文章
package framework.db; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static String url = null; private static String driver = null; private static String username = null; private static String password = null; static{ Properties p = new Properties();//加载数据源配置文件 InputStream inputStream = null; try { inputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("dataSource.properties"); p.load(inputStream); url = p.getProperty("url"); driver = p.getProperty("driver"); username = p.getProperty("username"); password = p.getProperty("password"); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); }finally{ try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } //线程局部变量 protected static ThreadLocal<Connection> threadLocalCon = new ThreadLocal<Connection>(); /* * 获取数据库连接 */ public static Connection getCon() { Connection con = threadLocalCon.get(); try { if (con == null || con.isClosed()) { Class.forName(driver); con = DriverManager.getConnection(url, username, password); threadLocalCon.set(con); } } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (SQLException e) { e.printStackTrace(); } return con; } /* * 关闭结果集 ResultSet */ public static void closeResultSet(ResultSet rs){ if(rs != null){ try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } } /* * 关闭 句柄 */ public static void closeStatement(Statement st){ if(st != null){ try { st.close(); } catch (SQLException e) { e.printStackTrace(); } } } /* * 在事务中调用dao层方法时,会首先设置事务自动提交为false,该场景下,关闭连接由事务管理器负责。 * 如果dao层方法没有在事务中执行,则此时事务自动提交为true,该场景下,由本方法负责关闭连接。 */ public static void closeConnectionIfAutoCommit(Connection con){ if(con != null){ try { if(con.getAutoCommit()){ con.close(); } } catch (SQLException e) { e.printStackTrace(); } } } /* * 依次关闭ResultSet、Statement、Connection */ public static void close(ResultSet rs,Statement st,Connection con){ closeResultSet(rs); closeStatement(st); closeConnectionIfAutoCommit(con); } }
3、创建事务管理器,事务管理器负责事务的开启、提交、回滚。
package framework.db; import java.sql.Connection; import java.sql.SQLException; public class TransactionManager { private Connection con; private TransactionManager(Connection con){ this.con = con; } /* * 开启事务 */ public void beginTransaction(){ try { con.setAutoCommit(false); } catch (SQLException e) { throw new RuntimeException("开启事务失败!",e); } } /* * 提交事务 */ public void commitTransaction(){ try { con.commit(); } catch (SQLException e) { throw new RuntimeException("提交事务失败!",e); }finally{ closeConnection(); DBUtil.threadLocalCon.remove();//将数据库连接从线程局部变量中卸载。 } } /* * 回滚事务 */ public void rollbackTransaction(){ try { con.rollback(); } catch (SQLException e) { throw new RuntimeException("回滚事务失败!",e); }finally{ closeConnection(); DBUtil.threadLocalCon.remove();//将数据库连接从线程局部变量中卸载。 } } /* * 获取事务管理器 */ public static TransactionManager getTransManager(){ return new TransactionManager(DBUtil.getCon()); } /* * 关闭数据库连接,仅限事务管理器内部使用,故private */ private void closeConnection(){ if(con != null){ try { con.close(); } catch (SQLException e) { e.printStackTrace(); } } } }
4、创建方法拦截器,负责拦截指定的方法,拦截到指定方法后,通过反射取得该方法的注解信息,如果该方法被Trans注解,则将该方法置于事务中执行。
package framework.intercept; import java.lang.reflect.Method; import framework.db.Trans; import framework.db.TransactionManager; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy; public class MyMethodInterceptor implements MethodInterceptor{ public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxyMethod) throws Throwable { Trans myAnnotation = method.getAnnotation(Trans.class); Object returnValue = null; TransactionManager manger = null; System.out.println("拦截方法:"+method); try { //开启事务 if(myAnnotation != null){ manger = TransactionManager.getTransManager(); manger.beginTransaction(); System.out.println("开启事务"+method); } //执行源对象的method方法 System.out.println("---before---"+method); returnValue = proxyMethod.invokeSuper(obj, args); System.out.println("---after---"+method); //提交事务 if(myAnnotation != null){ manger.commitTransaction(); System.out.println("提交事务"); } } catch (Exception e) { System.out.println("执行方法"+method+"抛异常了"); //回滚事务 if(myAnnotation != null){ manger.rollbackTransaction(); System.out.println("回滚事务"); } } return returnValue; } }
5、创建对象工厂,负责生成指定类的对象或代理对象(Service层的生成代理对象,非Service层的生成普通实例对象),代理对象执行时,会首先提交给第4步创建的方法拦截器处理。
package framework.factory; import java.util.HashMap; import java.util.Map; import net.sf.cglib.proxy.Enhancer; import framework.intercept.MyMethodInterceptor; /* * 实例工厂类,用于统一管理cmd、service、dao的实例。 */ public class ObjectFactory { //创建一个对象池 private static Map<String,Object> objPool = new HashMap<String, Object>(); /* * 根据类的包路径名称,返回该类的一个实例。 * 约定:业务逻辑层(Service层)的类名endsWith Service * 为Service层创建代理对象,非Service层直接创建实例。 */ public static Object getObject(String clazz){ Object obj = null; if(objPool.containsKey(clazz)){//如果对象池中已存在,则直接从对象池中获取。 obj = objPool.get(clazz); }else{ try { if(clazz.endsWith("Service")){ System.out.println("创建"+clazz+"代理实例对象!"); //如果对象池中不存在,则动态创建一个该类的实例,并将新创建的实例放入对象池。 Enhancer hancer = new Enhancer(); //设置代理对象的父类 hancer.setSuperclass(Class.forName(clazz)); //设置回调对象,即调用代理对象里面的方法时,实际上执行的是回调对象里的intercept方法。 hancer.setCallback(new MyMethodInterceptor()); //创建代理对象 obj = hancer.create(); }else{ System.out.println("创建"+clazz+"普通实例对象!"); obj = Class.forName(clazz).newInstance(); } //将新创建的对象放入代理对象池。 objPool.put(clazz, obj); }catch (Exception e) { throw new RuntimeException("创建 "+clazz+" 实例对象失败!",e); } } return obj; } }
6、使用示例:
package folder.service; import java.util.List; import file.dao.FileDao; import folder.bean.Folder; import folder.dao.FolderDao; import framework.db.Trans; import framework.factory.ObjectFactory; public class FolderService { //通过对象工厂拿到FolderDao和FileDao的实例对象 private FolderDao folderDao = (FolderDao) ObjectFactory.getObject(FolderDao.class.getName()); private FileDao fileDao = (FileDao) ObjectFactory.getObject(FileDao.class.getName()); @Trans public void insert(Folder folder) { folderDao.insert(folder); } /* * 根据档案袋名称进行查询 */ public List<Folder> queryByFolderName(String foldName){ return folderDao.queryByFolderName(foldName); } /* * 根据档案袋Id删除档案袋信息 */ @Trans public void deleteByFolderId(String folderId) { fileDao.deleteByFolderId(folderId); folderDao.deleteByFolderId(folderId); } }
7、测试
import folder.bean.Folder; import folder.service.FolderService; import framework.factory.ObjectFactory; public class Test { public static void main(String[] args) { FolderService s = (FolderService)ObjectFactory.getObject(FolderService.class.getName()); Folder folder = new Folder(); folder.setFolderId("001"); folder.setFolderCode("code001"); folder.setFolderName("学生档案"); folder.setFolderYear("2012"); s.insert(folder); } }
首次执行,预期结果应该是成功插入一条记录,并成功提交事务。
创建folder.service.FolderService代理实例对象! 创建folder.dao.FolderDao普通实例对象! 创建file.dao.FileDao普通实例对象! 拦截方法:public void folder.service.FolderService.insert(folder.bean.Folder) 开启事务public void folder.service.FolderService.insert(folder.bean.Folder) ---before---public void folder.service.FolderService.insert(folder.bean.Folder) ---after---public void folder.service.FolderService.insert(folder.bean.Folder) 提交事务
再次执行,预期结果,由于数据库中已经存在该记录,再次插入会由于主键唯一性约束而插入失败,事务回滚。
创建folder.service.FolderService代理实例对象! 创建folder.dao.FolderDao普通实例对象! 创建file.dao.FileDao普通实例对象! 拦截方法:public void folder.service.FolderService.insert(folder.bean.Folder) 开启事务public void folder.service.FolderService.insert(folder.bean.Folder) ---before---public void folder.service.FolderService.insert(folder.bean.Folder) 回滚事务 Exception in thread "main" java.lang.RuntimeException: 出错了! at framework.intercept.MyMethodInterceptor.intercept(MyMethodInterceptor.java:50) at folder.service.FolderService$$EnhancerByCGLIB$$9a9d9f19.insert(<generated>) at Test.main(Test.java:15) Caused by: java.lang.RuntimeException: 保存档案袋时出错! at folder.dao.FolderDao.insert(FolderDao.java:50) at folder.service.FolderService.insert(FolderService.java:19) at folder.service.FolderService$$EnhancerByCGLIB$$9a9d9f19.CGLIB$insert$2(<generated>) at folder.service.FolderService$$EnhancerByCGLIB$$9a9d9f19$$FastClassByCGLIB$$8fcee500.invoke(<generated>) at net.sf.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228) at framework.intercept.MyMethodInterceptor.intercept(MyMethodInterceptor.java:35) ... 2 more Caused by: java.sql.SQLException: ORA-00001: 违反唯一约束条件 (APITEST.T_FOLDER_PK) at oracle.jdbc.driver.SQLStateMapping.newSQLException(SQLStateMapping.java:74) at oracle.jdbc.driver.DatabaseError.newSQLException(DatabaseError.java:131) at oracle.jdbc.driver.DatabaseError.throwSqlException(DatabaseError.java:204) at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:455) at oracle.jdbc.driver.T4CTTIoer.processError(T4CTTIoer.java:413) at oracle.jdbc.driver.T4C8Oall.receive(T4C8Oall.java:1034) at oracle.jdbc.driver.T4CPreparedStatement.doOall8(T4CPreparedStatement.java:194) at oracle.jdbc.driver.T4CPreparedStatement.executeForRows(T4CPreparedStatement.java:953) at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStatement.java:1222) at oracle.jdbc.driver.OraclePreparedStatement.executeInternal(OraclePreparedStatement.java:3387) at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate(OraclePreparedStatement.java:3468) at oracle.jdbc.driver.OraclePreparedStatementWrapper.executeUpdate(OraclePreparedStatementWrapper.java:1062) at folder.dao.FolderDao.insert(FolderDao.java:48) ... 7 more