上篇文章我们讲到了Spring中AOP的引入及相关概念,接下来我们来讲Spring中基于注解的AOP配置
Spring 中基于注解的 AOP 配置
基于XML方式
在上篇文章我们知道了通知的类型有四种:前置通知,后置通知,异常通知,最终通知,环绕通知。现在在这我们会详细的介绍这四种类型。
- 导入依赖:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.2.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.7</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
有问题的看这篇文章传送门
- 编写业务层接口和实现类:
业务层接口:
package com.cz.service;
/**
* 账户的业务层接口
*/
public interface IAccountService {
/**
* 模拟保存账户
*/
void saveAccount();
/**
* 模拟更新账户
* @param i
*/
void updateAccount(int i);
/**
* 模拟删除账户
* @return
*/
int deleteAccount();
}
业务层实现类:
package com.cz.service.impl;
import com.cz.service.IAccountService;
/**
* 账户的业务层实现类
* 事务控制都在业务层
*/
public class AccountServiceImpl implements IAccountService {
public void saveAccount() {
System.out.println("执行了保存");
}
public void updateAccount(int i) {
System.out.println("执行了更新" +i);
}
public int deleteAccount() {
System.out.println("执行了删除");
return 0;
}
}
- 编写日志工具类:
package com.cz.utils;
/**
* 用于记录日志的工具类,里面提供了公共代码
*/
public class Logger {
/**
* 用于打印日志,计划让其在切入点方法之前执行(切入点方法就是业务层方法)
*/
public void printLog(){
System.out.println("Logger类中的printLog开始记录日志");
}
}
- 测试类:
package com.cz.test;
import com.cz.service.IAccountService;
import org.junit.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* 测试AOP配置
*/
public class AOPTest {
@Test
public void one(){
//1.获取容器
ApplicationContext ac = new ClassPathXmlApplicationContext("bean.xml");
//2.获取对象
IAccountService as = (IAccountService) ac.getBean("accountService");
//3.执行方法
as.saveAccount();
}
}
- 配置
bean.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: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">
<!-- 配置 service -->
<bean id="accountService" class="com.cz.service.impl.AccountServiceImpl"></bean>
<!--配置logger类-->
<bean id="logger" class="com.cz.utils.Logger"></bean>
<!--配置AOP-->
<aop:config>
<!--配置切面。-->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置通知的类型,并且建立通知方法和切入点方法的关联-->
<aop:before method="printLog" pointcut="execution(* com.cz.service.impl.*.*(..))"></aop:before>
</aop:aspect>
</aop:config>
</beans>
aop:config标签
:表明开始AOP的配置
aop:aspect标签
:用于配置切面
id属性
:是给切面提供一个唯一标识ref属性
:是指定通知类bean的Id。
在aop:aspect
标签的内部使用对应标签来配置通知的类型
通知的类型在下面进行讲解
切入点表达式的写法:
关键字:
execution(表达式)
表达式:
访问修饰符 返回值 包名.包名.包名...类名.方法名(参数列表)
标准的表达式写法:
public void com.cz.service.impl.AccountServiceImpl.saveAccount()
访问修饰符可以省略
void com.cz.service.impl.AccountServiceImpl.saveAccount()
返回值可以使用通配符,表示任意返回值
* com.cz.service.impl.AccountServiceImpl.saveAccount()
包名可以使用通配符,表示任意包。但是有几级包,就需要写几个
*.
* *.*.*.*.AccountServiceImpl.saveAccount())
包名可以使用
..
表示当前包及其子包
* *..AccountServiceImpl.saveAccount()
类名和方法名都可以使用
*
来实现通配
* *..*.*()
参数列表:
可以直接写数据类型:
- 基本类型直接写名称
int
- 引用类型写包名.类名的方式
java.lang.String
可以使用通配符表示任意类型,但是必须有参数
可以使用…表示有无参数均可,有参数可以是任意类型
全通配写法:
* *..*.*(..)
实际开发中切入点表达式的通常写法:
- 切到业务层实现类下的所有方法
* com.cz.service.impl.*.*(..)
- 现在我们来继续看看其他类型的通知:
- 修改日志的工具类:
/**
* 前置通知
*/
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog开始记录日志");
}
/**
* 后置通知
*/
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog开始记录日志");
}
/**
* 异常通知
*/
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog开始记录日志");
}
/**
* 最终通知
*/
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog开始记录日志");
}
- 修改
bean.xml
:
<!--配置AOP-->
<aop:config>
<aop:pointcut id="pt1" expression="execution(* com.cz.service.impl.*.*(..))"></aop:pointcut>
<!--配置切面。-->
<aop:aspect id="logAdvice" ref="logger">
<aop:before method="beforePrintLog" pointcut-ref="pt1"></aop:before>
<aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1"></aop:after-returning>
<aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1"></aop:after-throwing>
<aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
</aop:aspect>
</aop:config>
aop:pointcut
:
- 作用:
- 用于配置切入点表达式。就是指定对哪些类的哪些方法进行增强。
- 属性:
expression
:用于定义切入点表达式。id
:用于给切入点表达式提供一个唯一标识
此标签写在aop:aspect标签内部
只能当前切面使用。它还可以写在aop:aspect之前
,此时就变成了所有切面可用
aop:before:
- 作用:
- 表示配置前置通知,指定增强的方法在切入点方法之前执行
- 属性:
method属性:
用于指定Logger类中哪个方法是前置通知pointcut属性:
用于指定切入点表达式,该表达式的含义指的是对业务层中哪些方法增强ponitcut-ref
:用于指定切入点的表达式的引用
- 执行时间点:
- 切入点方法执行之前执行
因为我们在
aop:aspect
之前使用了aop:pointcut
,所以在这里面就不在使用,而是直接使用ponitcut-ref
,下面的其他类型也一样。
aop:after-returning:
- 作用:
- 用于配置后置通知
- 属性: 和前置通知一样,后面的两个类型也是一样,将会省略,大家知道就好
- 执行时间点:
- 切入点方法正常执行之后。它和异常通知只能有一个执行
aop:after-throwing:
- 作用:
- 用于配置异常通知
- 执行时间点:
- 切入点方法执行产生异常后执行。它和后置通知只能执行一个
aop:after:
- 作用:
- 用于配置最终通知
- 执行时间点:
- 无论切入点方法执行时是否有异常,它都会在其后面执行。
最后说一下环绕通知:
- 修改
bean.xml
<!--配置切面。-->
<aop:aspect id="logAdvice" ref="logger">
<!-- 配置环绕通知 详细的注释请看Logger类中-->
<aop:around method="aroundPrintLog" pointcut-ref="pt1"></aop:around>
</aop:aspect>
- 修改日志的工具类:
/**
* 环绕通知
*/
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null ;
try {
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。最终");
}
}
aop:around:
- 作用:
- 用于配置环绕通知
- 属性: 跟上面的一样
- 说明::
- 它是 spring 框架为我们提供的一种可以在代码中手动控制增强代码什么时候执行的方式。
- 注意:
- 通常情况下,环绕通知都是独立使用的
spring 框架为我们提供了一个接口:
ProceedingJoinPoint
,它可以作为环绕通知的方法参数。 在环绕通知执行时,spring 框架会为我们提供该接口的实现类对象,我们直接使用就行。
到这里我们就已经把通知的四种类型讲完了,Spring基于XML的AOP配置也完成了。
基于半注解(XML + 注解)方式
- 修改
bean.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: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">
<!-- 配置spring创建容器时要扫描的包-->
<context:component-scan base-package="com.cz"></context:component-scan>
<!-- 配置spring开启注解AOP的支持 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
- 修改日志的工具类:
package com.cz.utils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* 用于记录日志的工具类,里面提供了公共代码
*/
@Component("logger")
@Aspect//表示当前类是一个切面类
public class Logger {
@Pointcut("execution(* com.cz.service.impl.*.*(..))")
private void pt1(){
}
/**
* 前置通知
*/
@Before("pt1()")
public void beforePrintLog(){
System.out.println("前置通知Logger类中的beforePrintLog开始记录日志");
}
/**
* 后置通知
*/
@AfterReturning("pt1()")
public void afterReturningPrintLog(){
System.out.println("后置通知Logger类中的afterReturningPrintLog开始记录日志");
}
/**
* 异常通知
*/
@AfterThrowing("pt1()")
public void afterThrowingPrintLog(){
System.out.println("异常通知Logger类中的afterThrowingPrintLog开始记录日志");
}
/**
* 最终通知
*/
@After("pt1()")
public void afterPrintLog(){
System.out.println("最终通知Logger类中的afterPrintLog开始记录日志");
}
/**
* 环绕通知
* @return
*/
//@Around("pt1()")
public Object aroundPrintLog(ProceedingJoinPoint pjp){
Object rtValue = null;
try {
Object[] args = pjp.getArgs();//得到方法执行所需的参数
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。前置");
rtValue = pjp.proceed(args);//明确调用业务层方法(切入点方法)
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。后置");
return rtValue;
}catch (Throwable t){
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。异常");
throw new RuntimeException(t);
}finally {
System.out.println("Logger类中的aroundPrintLog方法开始记录日志了。。。最终");
}
}
}
@Aspect
注解用于将当前类声明为切面@Pointcut
注解用于指定切入点表达式,其属性value
指定表达式的内容
- 该注解只能作用在方法上
@Before
注解将当前方法声明为前置通知,其属性 value 既可以指定切入点表达式,也可以指定切入点表达式的引用
- 当引用切入点表达式的时候,写法为:
@Before("pt1()")
,要注意()
不可以省略@AfterReturning
注解将当前方法声明为后置通知@AfterThrowing
注解将当前方法声明为异常通知@After
注解将当前方法声明为最终通知@Around
注解将当前方法声明为环绕通知通知
执行不是环绕通知的运行结果:
执行环绕通知的运行结果:
大家发现什么问题没有?
可以看出来,执行不是环绕通知的,原本正常的调用顺序应该为:前置通知 --> 后置通知/异常通知 --> 最终通知
,而此时的调用顺序变成了 前置通知 --> 最终通知 --> 后置通知/异常通知
,如果是在事务管理中,一旦先执行了最终通知,连接被释放,那么再执行事务的提交或者回滚就行不通了。
如果想解决这个问题,那么只需要使用环绕通知即可,因为在环绕通知中每种通知的调用顺序是由我们自己决定的,不会受 Spring 的影响。
所以使用注解推荐使用环绕通知!!!
基于纯注解方式
如果要使用纯注解的方式,那么只需要在配置上用 @EnableAspectJAutoProxy
注解开启对 AOP 的支持即可