需求:使用 spring 框架整合 DBUtils 技术,实现用户转账功能
步骤分析:
1 . 创建 java 项目,导入坐标
<dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.15</version> </dependency> <dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.6</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.5.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency> </dependencies>
2. 编写 AccountDao 接口和实现类
public interface AccountDao { /** * 转出操作 * @param outUser * @param money */ void out(String outUser,Double money); /** * 转入操作 * @param inUser * @param money */ void in(String inUser,Double money); } @Repository("AccountDao") public class AccountDaoImpl implements AccountDao { @Autowired private QueryRunner queryRunner; @Override public void out(String outUser, Double money) { String sql = "update account set money = money - ? where name = ?"; try { queryRunner.update(sql, money, outUser); } catch (SQLException throwables) { throwables.printStackTrace(); } } @Override public void in(String inUser, Double money) { String sql = "update account set money = money + ? where name = ?"; try { queryRunner.update(sql, money, inUser); } catch (SQLException throwables) { throwables.printStackTrace(); } } }
3. 编写 AccountService 接口和实现类
public interface AccountService { /** * 转账方法 */ void transfer(String outUser,String inUser,Double money); } @Service("accountService") public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; /** * 转账方法 */ @Override public void transfer(String outUser, String inUser, Double money) { // 编写了事务相关代码 // 调用了减钱方法 accountDao.out(outUser,money); // 模拟出错 // int i= 1/0; // 调用了加钱方法 accountDao.in(inUser,money); } }
4. 编写 spring 核心配置文件
<?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" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.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 "> <!-- 开启注解扫描 --> <context:component-scan base-package="com.zm"/> <!-- 引入 properties --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置 DataSource --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置 queryRunner --> <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner"> <constructor-arg name="ds" ref="dataSource"/> </bean> </beans>
5. 编写测试代码
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath:applicationContext.xml"}) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer() { accountService.transfer("tom", "jerry", 100d); } }
问题分析
上面的代码事务在 Dao 层,转出转入操作都是一个独立的事务,但实际开发,应该把业务逻辑控制在一个事务中,所以应该将事务挪到 Service 层。
步骤分析:
1. 编写线程绑定工具类
@Component public class ConnectionUtils { @Autowired private DataSource dataSource; private ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); /** * 获取当前线程上绑定连接:如果获取到的连接为空,那么就要从数据源中获取连接,并且放到 ThreadLocal 中(绑定到当前线程) */ public Connection getThreadConnection() { // 1.先从 ThreadLocal 上获取连接 Connection connection = threadLocal.get(); // 2.判断当前线程中是否是有 Connection if(connection == null){ // 3.从数据源中获取一个连接,并且存入 ThreadLocal 中 try { // 不为 null connection = dataSource.getConnection(); threadLocal.set(connection); } catch (SQLException e) { e.printStackTrace(); } } return connection; } /** * 解除当前线程的连接绑定 */ public void removeThreadConnection(){ threadLocal.remove(); } }
2. 编写事务管理器
@Component("transactionManager") public class TransactionManager { @Autowired private ConnectionUtils connectionUtils; /** * 开启事务 */ public void beginTransaction(){ // 获取 connection 对象 Connection connection = connectionUtils.getThreadConnection(); try { // 开启了一个手动事务 connection.setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); } } /** * 提交事务 */ public void commit(){ Connection connection = connectionUtils.getThreadConnection(); try { connection.commit(); } catch (SQLException e) { e.printStackTrace(); } } /** * 回滚事务 */ public void rollback(){ Connection connection = connectionUtils.getThreadConnection(); try { connection.rollback(); } catch (SQLException e) { e.printStackTrace(); } } /** * 释放资源 */ public void release(){ // 将手动事务改回成自动提交事务 Connection connection = connectionUtils.getThreadConnection(); try { connection.setAutoCommit(true); // 将连接归还到连接池 connectionUtils.getThreadConnection().close(); // 解除线程绑定 connectionUtils.removeThreadConnection(); } catch (SQLException e) { e.printStackTrace(); } } }
3. 修改 service 层代码
@Service("accountService") public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Autowired private TransactionManager transactionManager; @Override public void transfer(String outUser, String inUser, Double money) { try { // 1.开启事务 transactionManager.beginTransaction(); // 2.业务操作 // 编写了事务相关代码 // 调用了减钱方法 accountDao.out(outUser, money); // 模拟出错 // int i= 1/0; // 调用了加钱方法 accountDao.in(inUser, money); // 3.提交事务 transactionManager.commit(); } catch (Exception e) { // 4.回滚事务 transactionManager.rollback(); e.printStackTrace(); } finally { // 5.释放资源 transactionManager.release(); } } }
4. 修改 Dao 层代码
@Repository("AccountDao") public class AccountDaoImpl implements AccountDao { @Autowired private QueryRunner queryRunner; @Autowired private ConnectionUtils connectionUtils; /** * 转出操作 */ @Override public void out(String outUser, Double money) { String sql = "update account set money = money - ? where name = ?"; try { queryRunner.update(connectionUtils.getThreadConnection(), sql, money, outUser); } catch (SQLException throwables) { throwables.printStackTrace(); } } /** * 转入操作 */ @Override public void in(String inUser, Double money) { String sql = "update account set money = money + ? where name = ?"; try { queryRunner.update(connectionUtils.getThreadConnection(), sql, money, inUser); } catch (SQLException throwables) { throwables.printStackTrace(); } } }
问题分析
上面代码,通过对业务层改造,已经可以实现事务控制了,但是由于添加了事务控制,也产生了一个新的问题: 业务层方法变得臃肿了,里面充斥着很多重复代码。并且业务层方法和事务控制方法耦合了,违背了面向对象的开发思想。
可以将业务代码和事务代码进行拆分,通过动态代理的方式,对业务方法进行事务的增强。这样就不会对业务层产生影响,解决了耦合性的问题。
常用的动态代理技术:
JDK 代理,基于接口的动态代理技术 - 利用拦截器(必须实现 invocationHandler)加上反射机制生成一个代理接口的匿名类,在调用具体方法前调用 InvokeHandler 来处理,从而实现方法增强
CGLIB 代理,基于父类的动态代理技术 - 动态生成一个要代理的子类,子类重写要代理的类的所有不是 final 的方法;在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,对方法进行增强
去掉 AccountServiceImpl 的事务控制代码
@Service("accountService") public class AccountServiceImpl implements AccountService { @Autowired private AccountDao accountDao; @Override public void transfer(String outUser, String inUser, Double money) { accountDao.out(outUser, money); // 模拟出错 int i= 1/0; accountDao.in(inUser, money); } }
JDK 工厂类
@Component public class JDKProxyFactory { @Autowired private AccountService accountService; @Autowired private TransactionManager transactionManager; /** * 采用 JDK 动态代理技术来生成目标类的代理对象 * ClassLoader loader : 类加载器:借助被代理对象获取到类加载器 * Class<?>[] interfaces : 被代理类所需要实现的全部接口 * InvocationHandler h : 当代理对象调用接口中的任意方法时,那么都会执行 InvocationHandler 中 invoke 方法 */ public AccountService createAccountServiceJdkProxy() { return (AccountService) Proxy.newProxyInstance( accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() { /** * @param proxy 当前的代理对象引用 * @param method 被调用的目标方法的引用 * @param args 被调用的目标方法所用到的参数 */ @Override public Object invoke(Object proxy, Method method, Object[] args) { try { if ("transfer".equalsIgnoreCase(method.getName())) { System.out.println("JDK Proxy: Pre-Enhance ..."); transactionManager.beginTransaction(); method.invoke(accountService, args); System.out.println("JDK Proxy: Post-Enhance ..."); transactionManager.commit(); } else { method.invoke(accountService, args); } } catch (Exception e) { transactionManager.rollback(); e.printStackTrace(); } finally { transactionManager.release(); } return null; } }); } }
测试代码
@Autowired private JDKProxyFactory jdkProxyFactory; @Test public void testTransferProxyJDK(){ // 当前返回的实际上是 AccountService 的代理对象 proxy AccountService accountServiceJDKProxy = jdkProxyFactory.createAccountServiceJDKProxy(); // 代理对象 proxy 调用接口中的任意方法时,都会执行底层的 invoke 方法 accountServiceJDKProxy.transfer("tom", "jerry", 100d); }
CGLIB 工厂类
@Component public class CglibProxyFactory { @Autowired private AccountService accountService; @Autowired private TransactionManager transactionManager; /** * 编写 cglib 对应的 API 来生成代理对象进行返回 * 参数 1 : 目标类的字节码对象 * 参数 2: 动作类,当代理对象调用目标对象中原方法时,那么会执行 intercept 方法 */ public AccountService createAccountServiceCglibProxy() { return (AccountService) Enhancer.create(accountService.getClass(), new MethodInterceptor() { /** * @param o 代表生成的代理对象 * @param method 调用目标方法的引用 * @param objects 方法入参 * @param methodProxy 代理方法 */ @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) { try { System.out.println("CGLIB Proxy: Pre-Enhance ..."); // 手动开启事务:调用事务管理器类中的开启事务方法 transactionManager.beginTransaction(); method.invoke(accountService, objects); System.out.println("CGLIB Proxy: Post-Enhance ..."); transactionManager.commit(); } catch (Exception e) { // 手动回滚事务 transactionManager.rollback(); e.printStackTrace(); } finally { // 手动释放资源 transactionManager.release(); } return null; } }); } }
AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程。
AOP 是 OOP(面向对象编程) 的延续,是软件开发中的一个热点,也是 Spring 框架中的一个重要内容,利用 AOP 可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
优势:
实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring 通过动态代理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从而完成功能的增强。
Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的部分进行代码编写,并通过配置的方式完成指定目标的方法增强。
在正式讲解 AOP 的操作之前,我们必须理解 AOP 的相关术语,常用的术语如下:
* Target(目标对象):代理的目标对象;被代理类 * Proxy(代理):一个类被 AOP 织入增强后,就产生一个结果代理类;生成代理对象 * Joinpoint(连接点):所谓连接点是指那些可以被拦截到的点;在 spring 中,这些点指的是方法,因为 spring 只支持方法类型的连接点;可以被拦截增强的方法 * Pointcut(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义;即真正被拦截增强的方法 * Advice(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知分类:前置通知、后置通知、异常通知、最终通知、环绕通知 - 一种可以通过代码的方式来手动控制的类型;即增强的业务逻辑 * Aspect(切面):是切入点和通知(引介)的结合 * Weaving(织入):是指把增强应用到目标对象来创建新的代理对象的过程。Spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入
开发阶段
运行阶段(Spring 框架自动完成)
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
底层代理实现
在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式:
步骤分析:
1. 创建 java 项目,导入 AOP 相关坐标
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.11</java.version> <maven.compiler.source>1.11</maven.compiler.source> <maven.compiler.target>1.11</maven.compiler.target> </properties> <dependencies> <!-- 导入 spring 的 context 坐标,context 依赖 aop --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.5.RELEASE</version> </dependency> <!-- aspectj 的织入(切点表达式需要用到该 jar 包) --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency> <!-- spring 整合 junit --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.5.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies>
2. 创建目标接口和目标实现类
public interface AccountService { /** * 目标方法:(切入点:要进行拦截增强的方法) */ void transfer(); } public class AccountServiceImpl implements AccountService { @Override public void transfer() { System.out.println("转账方法执行了...."); //int i = 1/0; } }
3. 创建通知类
public class MyAdvice { public void before(){ System.out.println("前置通知执行了...."); } }
4. 将目标类和通知类对象创建权交给 spring
<!-- 目标类交给 IOC 容器 --> <bean id="accountServcie" class="com.zm.service.impl.AccountServiceImpl"/> <!-- 通知类交给 IOC 容器 --> <bean id="myAdvice" class="com.zm.advice.MyAdvice"/>
5. 在核心配置文件中配置织入关系,及切面
导入 AOP 命名空间
<?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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="accountServcie" class="com.zm.service.impl.AccountServiceImpl"/> <bean id="myAdvice" class="com.zm.advice.MyAdvice"/> <aop:config> <aop:aspect ref="myAdvice"> <aop:before method="before" pointcut="execution(public void com.zm.service.impl.AccountServiceImpl.transfer())"/> </aop:aspect> </aop:config> </beans>
6. 编写测试代码
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath:applicationContext.xml"}) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer() { accountService.transfer(); } }
切点表达式
表达式语法:
excution( [修饰符] 返回值类型 包名.类名.方法名(参数))
- 访问修饰符可以省略
- 返回值类型、包名、类名、方法名可以使用星号 * 代替,代表任意
- 包名与类名之间一个点
.
代表当前包下的类,两个点..
表示当前包及其子包下的类- 参数列表可以使用两个点
..
表示任意个数,任意类型的参数列表
例子:
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
execution(public void com.renda.service.impl.AccountServiceImpl.transfer(java.lang.String))访问修饰符可以省略
execution(void com.renda.service.impl.AccountServiceImpl.transfer(java.lang.String))返回值类型、包名、类名、方法名可以使用星号 * 代替,代表任意
execution(* .....())包名与类名之间一个点 . 代表当前包下的类,两个点 .. 表示当前包及其子包下的类
execution(* ...*())参数列表可以使用两个点 .. 表示任意个数,任意类型的参数列表
execution(* ...*(..))
切点表达式抽取:
当多个增强的切点表达式相同时,可以将切点表达式进行抽取,在增强中使用 pointcut-ref 属性代替 pointcut 属性来引用抽取后的切点表达式。
public class MyAdvice { public void before(){ System.out.println("前置通知执行了...."); } public void afterReturning(){ System.out.println("后置通知执行了...."); } public void afterThrowing(){ System.out.println("异常通知执行了...."); } public void after(){ System.out.println("最终通知执行了...."); } /** * @param pjp Proceeding JoinPoint - 正在执行的连接点:切点 */ public Object around(ProceedingJoinPoint pjp){ Object proceed = null; try { System.out.println("前置通知执行了"); // 切点方法执行 proceed = pjp.proceed(); System.out.println("后置通知执行了"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("异常通知执行了"); }finally { System.out.println("最终通知执行了"); } return proceed; } }
<aop:config> <!-- 抽取的切点表达式 --> <aop:pointcut id="myPointcut" expression="execution(* com.zm.service.impl.AccountServiceImpl.*(..))"/> <!-- 配置切面:切入点 + 通知 --> <aop:aspect ref="myAdvice"> <aop:before method="before" pointcut-ref="myPointcut"/> <aop:after-returning method="afterReturning" pointcut-ref="myPointcut"/> <aop:after-throwing method="afterThrowing" pointcut-ref="myPointcut"/> <aop:after method="after" pointcut-ref="myPointcut"/> <!-- <aop:around method="around" pointcut-ref="myPointcut"/> --> </aop:aspect> </aop:config>
通知类型
通知的配置语法:
<aop:通知类型 method=“通知类中方法名” pointcut=“切点表达式"></aop:通知类型>
名称 | 标签 | 说明 |
---|---|---|
前置通知 | <aop:before> | 用于配置前置通知。指定增强的方法在切入点方法之前执行 |
后置通知 | <aop:afterReturning> | 用于配置后置通知。指定增强的方法在切入点方法之后执行 |
异常通知 | <aop:afterThrowing> | 用于配置异常通知;指定增强的方法出现异常后执行 |
最终通知 | <aop:after> | 用于配置最终通知;无论切入点方法执行时是否有异常,都会执行 |
环绕通知 | <aop:around> | 用于配置环绕通知;开发者可以手动控制增强代码在什么时候执行 |
注意:通常情况下,环绕通知都是独立使用的
aop 织入的配置
<aop:config>
<aop:aspect ref=“通知类”>
<aop:before method=“通知方法名称” pointcut=“切点表达式"></aop:before>
</aop:aspect>
</aop:config>通知的类型
前置通知、后置通知、异常通知、最终通知
环绕通知切点表达式
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
步骤分析:
1. 创建 java 项目,导入 AOP 相关坐标
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.encoding>UTF-8</maven.compiler.encoding> <java.version>1.11</java.version> <maven.compiler.source>1.11</maven.compiler.source> <maven.compiler.target>1.11</maven.compiler.target> </properties> <dependencies> <!-- 导入 spring 的 context 坐标,context 依赖 aop --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.1.5.RELEASE</version> </dependency> <!-- aspectj 的织入 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.8.13</version> </dependency> <!-- spring 整合 junit --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>5.1.5.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies>
2. 创建目标接口和目标实现类
public interface AccountService { void transfer(); } public class AccountServiceImpl implements AccountService { @Override public void transfer() { System.out.println("转账方法执行了...."); } }
3. 创建通知类
public class MyAdvice { public void before(){ System.out.println("前置通知执行了...."); } }
4. 将目标类和通知类对象创建权交给 spring
@Service public class AccountServiceImpl implements AccountService { @Override public void transfer() { System.out.println("转账方法执行了...."); } } @Component public class MyAdvice { ... }
5. 在通知类中使用注解配置织入关系,升级为切面类
@Component @Aspect // 升级为切面类:配置切入点和通知的关系 public class MyAdvice { @Before("execution(* com.zm.service.impl.AccountServiceImpl.*(..))") public void before(){ System.out.println("前置通知执行了...."); } }
6. 在配置文件中开启组件扫描和 AOP 的自动代理
<?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:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!-- 开启 IOC 注解扫描 --> <context:component-scan base-package="com.zm"/> <!-- aop 的自动代理:采用动态代理完成织入增强,并且生成代理;proxy-target-class="true" 表示强制使用 cglib 动态代理--> <aop:aspectj-autoproxy proxy-target-class="false"/> </beans>
7. 编写测试代码
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:applicationContext.xml") public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer(){ accountService.transfer(); } }
切点表达式
切点表达式的抽取
@Component @Aspect // 升级为切面类:配置切入点和通知的关系 public class MyAdvice { @Pointcut("execution(* com.zm.service.impl.AccountServiceImpl.*(..))") public void myPoint(){ } @Before("MyAdvice.myPoint()") public void before(){ System.out.println("前置通知执行了...."); } @AfterReturning("MyAdvice.myPoint()") public void afterReturning(){ System.out.println("后置通知执行了...."); } @AfterThrowing("MyAdvice.myPoint()") public void afterThrowing(){ System.out.println("异常通知执行了...."); } @After("MyAdvice.myPoint()") public void after(){ System.out.println("最终通知执行了...."); } /** * @param pjp Proceeding JoinPoint - 正在执行的连接点:切点 */ public Object around(ProceedingJoinPoint pjp){ Object proceed = null; try { System.out.println("前置通知执行了"); // 切点方法执行 proceed = pjp.proceed(); System.out.println("后置通知执行了"); } catch (Throwable throwable) { throwable.printStackTrace(); System.out.println("异常通知执行了"); }finally { System.out.println("最终通知执行了"); } return proceed; } }
通知类型
通知的配置语法:@通知注解(“切点表达式")
名称 | 标签 | 说明 |
---|---|---|
前置通知 | @Before | 用于配置前置通知。指定增强的方法在切入点方法之前执行 |
后置通知 | @AfterReturning | 用于配置后置通知。指定增强的方法在切入点方法之后执行 |
异常通知 | @AfterThrowing | 用于配置异常通知;指定增强的方法出现异常后执行 |
最终通知 | @After | 用于配置最终通知;无论切入点方法执行时是否有异常,都会执行 |
环绕通知 | @Around | 用于配置环绕通知;开发者可以手动控制增强代码在什么时候执行 |
注意
当前四个通知组合在一起时,出现一个 Spring 的执行顺序的 Bug,错误的执行顺序如下:
@Before -> @After -> @AfterReturning(如果有异常:@AfterThrowing)
如果单独使用环绕通知 @Around 注解则不会有这个 Bug,执行顺序如下:
@Before -> @AfterReturning(如果有异常:@AfterThrowing)-> @After
纯注解配置
去掉 applicationContext.xml 配置文件,增加 SpringConfig 配置类
@Configuration @ComponentScan("com.zm") @EnableAspectJAutoProxy // 开启 AOP 的自动代理,替代 xml 配置的 <aop:aspectj-autoproxy /> public class SpringConfig { }
修改测试类
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringConfig.class) public class AccountServiceTest { @Autowired private AccountService accountService; @Test public void testTransfer(){ accountService.transfer(); } }
- 使用 @Aspect 注解,标注切面类
- 使用 @Before 等注解,标注通知方法
- 使用 @Pointcut 注解,抽取切点表达式
- 配置 aop 自动代理 <aop:aspectj-autoproxy/> 或 @EnableAspectJAutoProxy
依然使用前面的转账案例,将两个代理工厂对象直接删除,改为 spring 的 AOP 思想来实现
配置文件
<?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" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.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 "> <!-- 开启注解扫描 --> <context:component-scan base-package="com.zm"/> <!-- 引入 properties,加载jdbc配置文件 --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置 DataSource --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置 queryRunner --> <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner"> <constructor-arg name="ds" ref="dataSource"/> </bean> <!-- AOP 配置 --> <aop:config> <!-- 1.切点表达式 --> <aop:pointcut id="myPointcut" expression="execution(* com.zm.service.impl.AccountServiceImpl.*(..))"/> <!-- 2.切面配置 --> <aop:aspect ref="transactionManager"> <aop:before method="beginTransaction" pointcut-ref="myPointcut"/> <aop:after-returning method="commit" pointcut-ref="myPointcut"/> <aop:after-throwing method="rollback" pointcut-ref="myPointcut"/> <aop:after method="release" pointcut-ref="myPointcut"/> </aop:aspect> </aop:config> </beans>
事务管理器(通知)
package com.zm.utils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.sql.Connection; import java.sql.SQLException; /** * 事务管理器工具类:包含:开启事务、提交事务、回滚事务、释放资源 * Spring AOP 的通知类 * */ @Component("transactionManager") public class TransactionManager { @Autowired private ConnectionUtils connectionUtils; /** * 开启事务 */ public void beginTransaction(){ // 获取 connection 对象 Connection connection = connectionUtils.getThreadConnection(); try { // 开启了一个手动事务 connection.setAutoCommit(false); System.out.println("开启事务"); } catch (SQLException e) { e.printStackTrace(); } } /** * 提交事务 */ public void commit(){ Connection connection = connectionUtils.getThreadConnection(); try { connection.commit(); System.out.println("提交事务"); } catch (SQLException e) { e.printStackTrace(); } } /** * 回滚事务 */ public void rollback(){ Connection connection = connectionUtils.getThreadConnection(); try { connection.rollback(); System.out.println("回滚事务"); } catch (SQLException e) { e.printStackTrace(); } } /** * 释放资源 */ public void release(){ // 将手动事务改回成自动提交事务 Connection connection = connectionUtils.getThreadConnection(); try { connection.setAutoCommit(true); // 将连接归还到连接池 connectionUtils.getThreadConnection().close(); // 解除线程绑定 connectionUtils.removeThreadConnection(); System.out.println("释放资源"); } catch (SQLException e) { e.printStackTrace(); } } }
配置文件
<?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" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.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 "> <!-- 开启注解扫描 --> <context:component-scan base-package="com.zm"/> <!-- 引入 properties --> <context:property-placeholder location="classpath:jdbc.properties"/> <!-- 配置 DataSource --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driverClassName}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <!-- 配置 queryRunner --> <bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner"> <constructor-arg name="ds" ref="dataSource"/> </bean> <!-- 开启 AOP 的自动代理 --> <aop:aspectj-autoproxy/> </beans>
事务管理器(通知)
@Component("transactionManager") @Aspect // 表明该类为切面类 public class TransactionManager { @Autowired private ConnectionUtils connectionUtils; @Around("execution(* com.zm.service.impl.AccountServiceImpl.*(..))") public Object around(ProceedingJoinPoint pjp) throws SQLException { Object proceed = null; try { // 开启手动事务 System.out.println("开启事务"); connectionUtils.getThreadConnection().setAutoCommit(false); // 切入点方法执行 proceed = pjp.proceed(); // 手动提交事务 System.out.println("提交事务"); connectionUtils.getThreadConnection().commit(); } catch (Throwable throwable) { throwable.printStackTrace(); // 手动回滚事务 System.out.println("回滚事务"); connectionUtils.getThreadConnection().rollback(); } finally { System.out.println("释放资源"); // 将手动事务恢复成自动事务 connectionUtils.getThreadConnection().setAutoCommit(true); // 将连接归还到连接池 connectionUtils.getThreadConnection().close(); // 解除线程绑定 connectionUtils.removeThreadConnection(); } return proceed; } }