一、开启注解支持
1、概述
1、Spring AOP如同IoC一样支持基于XML和基于注解两种配置方式,基于注解所需的依赖和基于XML所需的依赖一致,其中spring-context包含了Spring IoC、Spring AOP等核心依赖,而aspectjweaver则是AspectJ框架的依赖,Spring使用该依赖来解析AspectJ的切入点表达式语法,以及AOP的注解支持。
2、开启AOP注解支持的方式:
-
使用XML配置启用AOP注解。
-
使用Java配置启用AOP注解。
2、使用XML方式的配置
1、加入aop命名空间,使用<aop:aspectj-autoproxy />
标签即可,Spring将会查找被@Aspect注解标注的Bean,这表明它是一个切面Bean,然后就会进行AOP的自动配置。
2、属性说明:
-
proxy-target-class
:是否为被代理类生成CGLIB子类,只为接口生成代理子类(即:是否使用CGlib动态代理);默认为false,使用的是JDK代理
。
-
expose-proxy
:是否将代理的Bean暴露给用户,如果暴露,就可以通过AopContext类获得,默认不暴露
。
<?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">
<!--开启注解自动配置-->
<aop:aspectj-autoproxy/>
</beans>
3、使用Java方式的配置
1、AOP的注解支持同样可以使用Java配置方式开启,从而彻底舍弃XML配置文件。
2、配置类上添加@EnableAspectJAutoProxy
,该注解用于开启Spring AOP的注解自动配置支持。
3、属性说明:
-
proxyTargetClass
:是否为被代理类生成CGLIB子类,只为接口生成代理子类(即:是否使用CGlib动态代理);默认为false,使用的是JDK代理
。
-
exposeProxy
:是否将代理的Bean暴露给用户,如果暴露,就可以通过AopContext类获得,默认不暴露
。
/**
* @Date: 2023/2/11
* 开启AOP注解支持配置类
* @Configuration注解:表示该类为一个配置类
* @ComponentScan注解:用于扫描包上的注解将组件加载到IoC容器中
*/
@Configuration
@ComponentScan("com.itan.aop.*")
@EnableAspectJAutoProxy
public class AopConfig {
}
二、切面相关注解
1、@Aspect注解
1、@Aspect
注解对应着XML配置中的<aop:aspect />
标签,被该注解标注的类,会被当做切面类并且用于自动配置Spring AOP。如果类仅仅只标注了该注解,那么是不会被Spring组件扫描工具自动扫描到的,并且不会加入到IoC容器中,因此还需要搭配组件注册相关的注解一起使用(如:@Component)
。
2、切面类和普通类一样,可以有自己的方法和字段,还可以包含切入点(pointcut)、通知(advice)、声明(introduction),这些都是通过方法来绑定的。
3、切面类本身是不能成为其他切面通知的目标类,类上面标注了@Aspect注解之后,该类的Bean将从AOP自动配置Bean中排除,因此切面类里面的方法是不能被代理的。
2、@Pointcut注解
1、@Pointcut
注解对应着XML配置中的<aop:pointcut />
标签,用来定义一个切入点,在匹配的情况下执行通知,切入点表达式的语法都是一样的,请参考切入点声明规则。
2、@Pointcut注解标注在一个切面类的方法上,方法名就是该切入点的名字,在通知中通过名字引用该切入点(XXX(),要带上括号),也可以将多个切入点组合成一个新的切入点(可以使用&&、||、!等运算符连接起来),XML配置中不具备该组合方式
。
3、通知相关注解
1、advice通知同样可以使用注解声明,并且绑定到一个方法上,一共有五种通知注解,分别和XML中的五中通知标签一一对应。
2、五种通知注解:
-
@Before
:用于定义前置通知,在使用时,通常需要指定一个value属性值,该属性值用于指定一个切入点表达式(可以是已有的切入点,也可以直接定义切入点表达式)。
-
@AfterReturning
:用于定义后置通知,在使用时可以指定pointcut或value和returning属性;其中pointcut/value这两个属性的作用一样,都用于指定切入点表达式;returning属性值是后置通知方法中的参数名,用来将切入点方法的返回值绑定到该参数上
。
-
@AfterThrowing
:用于定义异常通知,在前置通知、切入点方法和后置通知中抛出异常之后可能会执行;在使用时可指定pointcut或value和throwing属性,其中pointcut/value用于指定切入点表达式;throwing属性值是异常通知方法中的参数名,用来将前置通知、切入点方法、后置通知执行过程中抛出的异常绑定到该参数上
。
-
@After
:用于定义最终通知,不管是否异常,该通知都会执行;使用时需要指定一个value属性,用于指定一个切入点表达式。
-
@Around
:用于定义环绕通知,使用时需要指定一个value属性,用于指定一个切入点表达式。
/**
* @Date: 2023/2/11
* 切面类
* @Aspect:用于定义该类为一个切面类
* @Component:将组件注册到IoC容器中,否则单单使用@Aspect是不会被组件扫描注解检测到的
*/
@Aspect
@Component
public class AnnoAspect {
/**
* 切入点,该切入点匹配service包下所有类所有方法
*/
@Pointcut("execution(* com.itan.aop.service..*.*(..))")
public void pointCut() {}
/**
* 前置通知
*/
@Before(value = "pointCut()")
public void before() {
System.out.println("前置通知");
}
/**
* 异常通知
*/
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowing(Exception e) {
System.out.println("异常通知,异常为:" + e.getMessage());
}
/**
* 后置通知
*/
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturning(Object result) {
System.out.println("后置通知,返回值为:" + result);
}
/**
* 最终通知
*/
@After(value = "pointCut()")
public void afterFinally() {
System.out.println("最终通知");
}
/**
* 环绕通知
* 一定要有ProceedingJoinPoint类型的参数
*/
@Around(value = "pointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
System.out.println("环绕通知 --- 进入方法");
Object[] args = point.getArgs();
System.out.println("外部传入的参数:" + Arrays.toString(args));
System.out.println(point.toString());
System.out.println(point.toShortString());
System.out.println(point.toLongString());
System.out.println(point.getThis());
System.out.println(point.getTarget());
System.out.println(point.getSignature());
System.out.println(point.getSourceLocation());
System.out.println(point.getKind());
System.out.println(point.getStaticPart());
// proceed方法表示调用切入点方法,否则方法不会执行,args表示参数,proceed就是切入点方法的返回值
Object proceed = point.proceed(args);
System.out.println("环绕通知 --- 退出方法");
return proceed;
}
}
/**
* @Date: 2023/2/11
* 目标接口实现类
*/
@Service
public class CalculateServiceImpl implements CalculateService {
public Integer add(int i, int j) {
return i + j;
}
public int sub(int i, int j) {
return i + j;
}
public int multiply(int i, int j) {
return i * j;
}
public double divide(int i, int j) {
return i / j;
}
}
@Test
public void test04() {
// 通过配置文件创建容器对象
ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
CalculateService calc = context.getBean(CalculateService.class);
System.out.println(calc.getClass());
calc.add(2,3);
}
/**
* 运行结果:可以看到默认情况下使用JDK代理:class com.sun.proxy.$Proxy28
* class com.sun.proxy.$Proxy28
* 环绕通知 --- 进入方法
* 外部传入的参数:[2, 3]
* execution(Integer com.itan.aop.service.CalculateService.add(int,int))
* execution(CalculateService.add(..))
* execution(public abstract java.lang.Integer com.itan.aop.service.CalculateService.add(int,int))
* com.itan.aop.service.impl.CalculateServiceImpl@3e6f3f28
* com.itan.aop.service.impl.CalculateServiceImpl@3e6f3f28
* Integer com.itan.aop.service.CalculateService.add(int,int)
* org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint$SourceLocationImpl@7357a011
* method-execution
* execution(Integer com.itan.aop.service.CalculateService.add(int,int))
* 前置通知
* 后置通知,返回值为:5
* 最终通知
* 环绕通知 --- 退出方法
*/
4、通知顺序
1、当同一个连接点方法中绑定了多个同一个类型的通知时,有时需要指定通知的执行顺序,在XML中通过<aop:aspect />切面标签中的order属性指定执行通知顺序
,注解配置中也能实现通知执行顺序。
2、实现方式:
-
通知类实现Ordered接口
-
通知类标注@Order注解
-
提示:未设置order值时,默认值为Integer.MAX_VALUE;值越小的切面,其内部的前置通知越先执行,后置通知越后执行。
/**
* @Date: 2023/2/11
* 通过@Order注解方式设置顺序
*/
@Aspect
@Component
@Order(Integer.MAX_VALUE - 2)
public class AnnoOrderAspect1 {
/**
* 切入点,该切入点匹配service包下所有类所有方法
*/
@Pointcut("execution(* com.itan.aop.service..*.multiply(..))")
public void pointCut() {}
/**
* 前置通知
*/
@Before(value = "pointCut()")
public void before() {
System.out.println("AnnoOrderAspect1前置通知");
}
/**
* 后置通知
*/
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturning(Object result) {
System.out.println("AnnoOrderAspect1后置通知,返回值为:" + result);
}
/**
* 最终通知
*/
@After(value = "pointCut()")
public void afterFinally() {
System.out.println("AnnoOrderAspect1最终通知");
}
}
/**
* @Date: 2023/2/11
* 通过实现Ordered接口方式设置顺序
*/
@Aspect
@Component
public class AnnoOrderAspect2 implements Ordered {
/**
* 切入点,该切入点匹配service包下所有类所有方法
*/
@Pointcut("execution(* com.itan.aop.service..*.multiply(..))")
public void pointCut() {}
/**
* 前置通知
*/
@Before(value = "pointCut()")
public void before() {
System.out.println("AnnoOrderAspect2前置通知");
}
/**
* 后置通知
*/
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturning(Object result) {
System.out.println("AnnoOrderAspect2后置通知,返回值为:" + result);
}
/**
* 最终通知
*/
@After(value = "pointCut()")
public void afterFinally() {
System.out.println("AnnoOrderAspect2最终通知");
}
/**
* 获取顺序值
* @return
*/
@Override
public int getOrder() {
return Integer.MAX_VALUE - 1;
}
}
@Test
public void test05() {
// 通过配置文件创建容器对象
ApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class);
CalculateService calc = context.getBean(CalculateService.class);
System.out.println(calc.getClass());
calc.multiply(2,3);
}
/**
* 运行结果:值越小的切面的前置通知越先执行,后置通知越后执行
* AnnoOrderAspect1前置通知
* AnnoOrderAspect2前置通知
* AnnoOrderAspect2后置通知,返回值为:6
* AnnoOrderAspect2最终通知
* AnnoOrderAspect1后置通知,返回值为:6
* AnnoOrderAspect1最终通知
*/