Spring4

参考

B站颜群老师Spring教程,B站雷丰阳老师Spring教程,《Spring实战》
https://zhuanlan.zhihu.com/p/137507309
https://liayun.blog.csdn.net/article/details/115053350

AOP切面

AOP(Aspect Orient Programming),直译过来就是面向切面编程。AOP
是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程
序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面
所谓切面,其实就相当于应用对象间的横切点,我们可以将其单独抽象为
单独的模块。总之一句话:AOP是指在程序的运行期间动态地将某段代码
切入到指定方法、指定位置进行运行的编程方式。AOP的底层是使用动态
代理实现的

实战案例

导入AOP依赖,Spring AOP对面向切面编程做了一些简化操作,我们只
需要加上几个核心注解,AOP就能工作起来

1
2
3
4
5
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>4.3.12.RELEASE</version>
</dependency>

定义目标类,在com.atguigu.aop 包下创建一个业务逻辑类,用于处理
数学计算上的一些逻辑。比如,我们在MathCalculator类中定义了一个
除法操作,返回两个整数类型值相除之后的结果

1
2
3
4
5
6
public class MathCalculator {
public int div(int i, int j) {
System.out.println("MathCalculator...div...");
return i / j;
}
}

现在我们希望在以上这个业务逻辑类中的除法运算之前,记录一下日志
,例如记录一下哪个方法运行了,用的参数是什么,运行结束之后它的
返回值又是什么,顺便可以将其打印出来,还有如果运行出异常了,那
么就捕获一下异常信息

定义切面类

创建一个切面类,例如LogAspects,在该切面类中定义几个打印日志的
方法,以这些方法来动态地感知MathCalculator类中的div()方法的运
行情况。如果需要切面类来动态地感知目标类方法的运行情况,那么就
需要使用Spring AOP中的一系列通知方法了。AOP中的通知方法及其对
应的注解与含义如下:

  1. 前置通知(对应的注解是@Before):在目标方法运行之前运行
  2. 后置通知(对应的注解是@After):在目标方法运行结束之后运行,
    无论目标方法是正常结束还是异常结束都会执行
  3. 返回通知(对应的注解是@AfterReturning):在目标方法正常返回
    之后运行
  4. 异常通知(对应的注解是@AfterThrowing):在目标方法运行出现
    异常之后运行
  5. 环绕通知(对应的注解是@Around):动态代理,我们可以直接手动
    推进目标方法运行(joinPoint.procced())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class LogAspects {
/* @Before:在目标方法(即div方法)运行之前切入,
public int com.meimeixia.aop.MathCalculator.div(int, int)
这一串就是切入点表达式,指定在哪个方法切入 */
//*是不区分方法,..是不区分参数
@Before("public int com.atguigu.aop.MathCalculator.*(..)")
public void logStart() {
System.out.println("除法运行......@Before,参数列表是:{}");
}

// 在目标方法(即div方法)结束时被调用
@After("public int com.atguigu.aop.MathCalculator.*(..)")
public void logEnd() {
System.out.println("除法结束......@After");
}

// 在目标方法(即div方法)正常返回了,有返回值,被调用
@AfterReturning("public int com.atguigu.aop.MathCalculator.*(..)")
public void logReturn() {
System.out.println("除法正常返回......@AfterReturning,运行结果是:{}");
}

// 在目标方法(即div方法)出现异常,被调用
@AfterThrowing("public int com.atguigu.aop.MathCalculator.*(..)")
public void logException() {
System.out.println("除法出现异常......异常信息:{}");
}
}

如果切入点表达式都一样的情况下,那么我们可以抽取出一个公共的
切入点表达式

1
2
3
4
5
6
public class LogAspects {
// 如果切入点表达式都一样的情况下,那么我们可以抽取出一个公共的切入点表达式
@Pointcut("execution(public int com.meimeixia.aop.MathCalculator.*(..))")
public void pointCut() {}
...
}

pointCut()方法就是抽取出来的一个公共的切入点表达式,其实该方
法的方法名随便写啥都行,但是方法体中啥都别写。第一种情况,如
果是本类引用,那么可以像下面这样写

1
2
3
4
5
6
7
8
9
10
public class LogAspects {
// 如果切入点表达式都一样的情况下,那么我们可以抽取出一个公共的切入点表达式
@Pointcut("execution(public int com.meimeixia.aop.MathCalculator.*(..))")
public void pointCut() {}
@Before("pointCut()")
public void logStart() {
System.out.println("除法运行......@Before,参数列表是:{}");
}
...
}

第二种情况,如果是外部类(即其他的切面类)引用,那么就得在
通知注解中写方法的全名了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*最后,千万别忘了一点,那就是必须告诉Spring哪个类是切面类,要做到这一点很
简单,只需要给切面类上加上一个@Aspect注解即可*/
@Aspect
public class LogAspects {
// 如果切入点表达式都一样的情况下,那么我们可以抽取出一个公共的切入点表达式
@Pointcut("execution(public int com.meimeixia.aop.MathCalculator.*(..))")
public void pointCut() {}

// 在目标方法(即div方法)结束时被调用
// @After("pointCut()")
@After("com.meimeixia.aop.LogAspects.pointCut()")
public void logEnd() {
System.out.println("除法结束......@After");
}
}

将目标类和切面类加入到IOC容器

新建一个配置类,例如MainConfigOfAOP,并使用@Configuration注解
标注这是一个Spring 的配置类,同时使用@EnableAspectJAutoProxy
注解开启基于注解的AOP模式。在该配置类中,使用@Bean注解将业务逻
辑类(目标方法所在类)和切面类都加入到IOC容器中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@EnableAspectJAutoProxy
@Configuration
public class MainConfigOfAOP {
// 将业务逻辑类(目标方法所在类)加入到容器中
@Bean
public MathCalculator calculator() {
return new MathCalculator();
}
// 将切面类加入到容器中
@Bean
public LogAspects logAspects() {
return new LogAspects();
}
}

测试

创建一个单元测试类IOCTest_AOP,并在该测试类中创建一个test01()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class IOCTest_AOP {
@Test
public void test01() {
AnnotationConfigApplicationContext applicationContext =
new AnnotationConfigApplicationContext(MainConfigOfAOP.class);
// 不要自己创建这个对象
// MathCalculator mathCalculator = new MathCalculator();
// mathCalculator.div(1, 1);

// 我们要使用Spring容器中的组件
MathCalculator mathCalculator = applicationContext.getBean(
MathCalculator.class);
mathCalculator.div(1, 1);
// 关闭容器
applicationContext.close();
}
}

执行了切面类中的方法,并打印出了相关信息,顺序是Before After
AfterReturning。但是并没有打印参数列表和运行结果,要想打印出
参数列表和运行结果,就需要对LogAspects切面类中的方法进行优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Aspect
public class LogAspects {

@Pointcut("execution(public int com.meimeixia.aop.MathCalculator.*(..))")
public void pointCut() {}

@Before("pointCut()")
public void logStart(JoinPoint joinPoint) {
// 拿到参数列表,即目标方法运行需要的参数列表
Object[] args = joinPoint.getArgs();
System.out.println(joinPoint.getSignature().getName() +
"运行......@Before,参数列表是:{" + Arrays.asList(args) + "}");
}

// returning来指定我们这个方法的参数谁来封装返回值
@AfterReturning(value="pointCut()", returning="result")
public void logReturn(JoinPoint joinPoint, Object result) {
/*一定要注意:JoinPoint这个参数要写,一定不能写到后面,它必须出
现在参数列表的第一位,否则Spring也是无法识别的,就会报错*/
System.out.println(joinPoint.getSignature().getName() +
"正常返回......@AfterReturning,运行结果是:{" + result + "}");
}
}

如果目标方法运行时出现了异常,而我们又想拿到这个异常信息,
只须对LogAspects切面类中的logException()方法进行优化即可

1
2
3
4
5
@AfterThrowing(value="pointCut()", throwing="exception")
public void logException(JoinPoint joinPoint, Exception exception) {
System.out.println(joinPoint.getSignature().getName() +
"出现异常......异常信息:{" + exception + "}");
}

执行了切面类中的方法,并打印出了相关信息,顺序是Before After
和异常信息

小结

搭建AOP测试环境时,只要牢牢记住以下三点

  1. 将切面类和业务逻辑组件(目标方法所在类)都加入到容器中,并且
    要告诉Spring哪个类是切面类(标注了@Aspect注解的那个类)
  2. 在切面类上的每个通知方法上标注通知注解,告诉Spring何时何地运
    行,当然最主要的是要写好切入点表达式,这个切入点表达式可以参照官
    方文档来写
  3. 开启基于注解的AOP模式,即加上@EnableAspectJAutoProxy注解,
    这是最关键的一点

@EnableAspectJAutoProxy注解

在配置类上添加@EnableAspectJAutoProxy注解,便能够开启注解版的
AOP功能。也就是说,如果要使注解版的AOP功能起作用的话,那么就得
需要在配置类上添加@EnableAspectJAutoProxy注解

1
2
3
4
5
6
7
8
9
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AspectJAutoProxyRegistrar.class})
public @interface EnableAspectJAutoProxy {
boolean proxyTargetClass() default false;

boolean exposeProxy() default false;
}

使用@Import注解给容器中引入了AspectJAutoProxyRegister组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
AspectJAutoProxyRegistrar() {
}

public void registerBeanDefinitions(AnnotationMetadata
importingClassMetadata, BeanDefinitionRegistry registry) {
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary
(registry);
AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils
.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
if (enableAspectJAutoProxy != null) {
if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}

}
}

可以通过ImportBeanDefinitionRegistrar接口实现将自定义的组件添
加到IOC容器中

1
2
3
4
5
6
7
8
9
10
11
public interface ImportBeanDefinitionRegistrar {
default void registerBeanDefinitions(AnnotationMetadata
importingClassMetadata, BeanDefinitionRegistry
registry, BeanNameGenerator importBeanNameGenerator) {
this.registerBeanDefinitions(importingClassMetadata, registry);
}

default void registerBeanDefinitions(AnnotationMetadata
importingClassMetadata, BeanDefinitionRegistry registry) {
}
}

也就是说,@EnableAspectJAutoProxy注解使用AspectJAutoProxyRegistrar
对象自定义组件,并将相应的组件添加到了IOC容器中

Author: 高明
Link: https://skysea-gaoming.github.io/2021/04/22/Spring4/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.