AOP 术语

在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往是权限校验代码、日志代码、事务控制代码、性能监控代码。
image.png

名词 解释
Joinpoint(连接点) 它指的是那些可以用于把增强代码加入到业务主线中的点。在方法执行的前后通过动态代理技术加入增强的代码。在 Spring 框架 AOP 思想的技术实现中,也只支持方法类型的连接点。
Pointcut(切入点) 它指的是那些已经把增强代码加入到业务主线进来之后的连接点。由上图中,我们看出表现层 register 方法就只是连接点,因为判断访问权限的功能并没有对其增强。
Advice(通知/增强) 它指的是切面类中用于提供增强功能的方法。并且不同的方法增强的时机是不一样的。比如,开启事务肯定要在业务方法执行之前执行;提交事务要在业务方法正常执行之后执行,而回滚事务要在业务方法执行产生异常之后执行等等。这些就是通知的类型。其分类有:前置通知 后置通知 异常通知 最终通知 环绕通知。
Target(目标对象) 它指的是代理的目标对象。即被代理对象。
Proxy(代理) 它指的是一个类被 AOP 织入增强后,产生的代理类。即代理对象。
Weaving(织 入) 它指的是把增强应用到目标对象来创建新的代理对象的过程。spring 采用动态代理织入,而 AspectJ 采用编译期织入和类装载期织入。
Aspect(切面) 它指定是增强的代码所关注的方面,把这些相关的增强代码定义到一个类中,这个类就是切面类。例如,事务切面,它里面定义的方法就是和事务相关的,像开启事务,提交事务,回滚事务等等,不会定义其他与事务无关的方法。我们前面的案例中 TrasnactionManager 就是一个切面。

Aspect 切面 = 切入点 + 通知
Aspect 切面 = 切入点(锁定方法)+ 方位点(锁定方法中的特殊时机)+ 横切逻辑

众多的概念,目的就是为了锁定要在哪个地方插入什么横切逻辑代码。

AspectJ 切入点表达式

  • 关键字:execution(表达式)
  • 表达式:
    • 访问修饰符返回值包名.包名…类名.方法名(参数列表)
  • 标准写法示例:
    • public void li.service.impl.AccountServiceImpl.saveAccount()
  • 访问修饰符可以省略
    • void li.service.impl.AccountServiceImpl.saveAccount()
  • 返回值可以使用通配符,表示任意返回值
    • * li.service.impl.AccountServiceImpl.saveAccount()
  • 包名可以使用通配符,表示任意包,但是有几级包就需要写几个*.
    • * *.*.*.AccountServiceImpl.saveAccount()
  • 包名可以使用..表示当前包及其子包
    • * *..AccountServiceImpl.saveAccount()
  • 类名和方法名都可以使用 * 实现通配
    • * *..*.*()
  • 参数列表:
    • 可以直接写数据类型:
      • 基本类型直接写名称 int
      • 引用类型写包名.类名 java.lang.String
    • 可以使用通配符,表示任意类型,但是必须有参数
    • 可以使用..有无参数均可,有参数可以是任意类型
  • 全通配写法:
    • * *..*.*(..)
  • 实际开发中切入点表达式的通常写法:
    • 切到业务层实现类下的所有方法 li.service.impl.*.*(..)

五种通知类型

前置通知

配置方式:aop:before 标签

1
2
3
4
5
6
7
8
9
10
<!--
作用:用于配置前置通知。
出现位置:它只能出现在aop:aspect标签内部
属性:
method:用于指定前置通知的方法名称
pointcut:用于指定切入点表达式
pointcut-ref:用于指定切入点表达式的引用
arg-names:用于指定通知方法的参数名称,要求表达式中必须有描述 args 的语句
-->
<aop:before method="beforeMethod" pointcut-ref="pt1"/>
1
2
3
4
5
6
7
public void beforeMethod(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
System.out.println("业务逻辑之前...");
}

执行时机:在切入点方法(业务核心方法)执行之前执行。
说明:前置通知可以获取切入点方法的参数,并对其进行增强。

后置通知

配置方式:aop:after-returning 标签

1
2
3
4
5
6
7
8
9
10
11
<!--
作用:用于配置后置正常执行通知
出现位置:它只能出现在aop:aspect标签内部
属性:
method:用于指定前置通知的方法名称
pointcut:用于指定切入点表达式
pointcut-ref:用于指定切入点表达式的引用
arg-names:用于指定通知方法的参数名称,要求表达式中必须有描述 args 的语句
returning:用于指定返回值
-->
<aop:after-returning method="afterMethod" pointcut-ref="pt1" returning="result"/>
1
2
3
4
5
6
7
public void afterMethod(JoinPoint joinPoint, Object result) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
System.out.println("业务逻辑之后..." + result);
}

执行时机:在切入点方法(业务核心方法)正常执行之后执行,出现异常后后置正常执行通知不再执行了,而是执行异常通知。
说明:后置通知即可以获取切入点方法的参数,也可以获取切入点方法的返回值。

异常通知

配置方式:aop:after-throwing 标签

1
2
3
4
5
6
7
8
9
10
11
<!--
作用:用于配置后置异常执行通知
出现位置:它只能出现在aop:aspect标签内部
属性:
method:用于指定前置通知的方法名称
pointcut:用于指定切入点表达式
pointcut-ref:用于指定切入点表达式的引用
arg-names:用于指定通知方法的参数名称,要求表达式中必须有描述 args 的语句
throwing:用于指定异常通知中异常的变量名称
-->
<aop:after-throwing method="afterMethod" pointcut-ref="pt1"/>

执行时机:在切入点方法(业务核心方法)执行产生异常之后执行,如果没有出现异常则不会执行。
说明:异常通知即可以获取切入点方法的参数,也可以获取切入点方法执行产生的异常信息。

最终通知

配置方式:aop:after-throwing 标签

1
2
3
4
5
6
7
8
9
10
<!--
作用:用于指定最终通知
出现位置:它只能出现在aop:aspect标签内部
属性:
method:用于指定前置通知的方法名称
pointcut:用于指定切入点表达式
pointcut-ref:用于指定切入点表达式的引用
arg-names:用于指定通知方法的参数名称,要求表达式中必须有描述 args 的语句
-->
<aop:after method="finalMethod" pointcut-ref="pt1"/>

执行时机:最终通知的执行时机是在切入点方法(业务核心方法)执行完成之后,切入点方法返回之前执行。 换句话说,无论切入点方法执行是否产生异常,它都会在返回之前执行。
说明:最终通知执行时,可以获取到通知方法的参数。同时它可以做一些清理操作。

环绕通知

配置方式:aop:after-throwing 标签

1
2
3
4
5
6
7
8
9
10
<!--
作用:用于指定最终通知
出现位置:它只能出现在aop:aspect标签内部
属性:
method:用于指定前置通知的方法名称
pointcut:用于指定切入点表达式
pointcut-ref:用于指定切入点表达式的引用
arg-names:用于指定通知方法的参数名称,要求表达式中必须有描述 args 的语句
-->
<aop:around method="aroundMethod" pointcut-ref="pt1"/>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
Object result;

try {
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("环绕通知的前置通知...");
result = proceedingJoinPoint.proceed(args);
System.out.println("环绕通知的后置通知...");
} catch (Throwable e) {
System.out.println("环绕通知的异常通知...");
throw new RuntimeException(e);
} finally {
System.out.println("环绕通知的最终通知...");
}

return result;
}

执行时机:Spring 框架为我们提供了一个接口 ProceedingJoinPoint。该接口有一个方法proceed(),此方法就相当于明确调用切入点方法。
说明:可以控制原有逻辑是否执行。

使用 Spring AOP 的方式

XML 方式

xml 文件头

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="utf-8" ?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">

</beans>

配置横切逻辑 bean

1
<bean id="logUtils" class="com.lagou.edu.utils.LogUtils"/>

使用 aop:config 标签开始 aop 的配置

1
2
3
4
5
6
7
8
9
10
11
<!--配置 aop 的过程其实就是把 aop 相关概念落地-->
<!--aspect(切面) = 切入点(方法) + 方位点(方法中的特殊时机) + 横切逻辑-->
<aop:config>
<aop:aspect id="logAspect" ref="logUtils">
<!--切入点锁定需要通知的方法,使用 aspectj 语法表达式-->
<aop:pointcut id="pt1" expression="execution(public void com.lagou.edu.service.impl.TransferServiceImpl.transfer(String,String,int))"/>

<!--方位点,pointcut-ref 关联切入点,pointcut 可以直接写aspectj 语法表达式-->
<aop:before method="beforeMethod" pointcut-ref="pt1"/>
</aop:aspect>
</aop:config>

改变代理方式的配置

Spring 在选择创建代理对象时,会根据被代理对象的实际情况来选择。被代理对象实现了接口,则采用基于接口的动态代理。当被代理对象没有实现任何接口的时候,Spring会自动切换到基于子类的动态代理方式。
无论被代理对象是否实现接口,只要不是 final 修饰的类都可以采用 cglib 提供的方式创建代理对象。所以 Spring 也考虑到了这个情况,提供了配置的方式实现强制使用基于子类的动态代理(即cglib的方式),配置的方式有两种:

  • 使用 aop:config 标签配置
    1
    <aop:config proxy-target-class="true">
  • 使用 aop:aspectj-autoproxy 标签配置
    1
    2
    <!--此标签是基于 XML 和注解组合配置 AOP 时的必备标签,表示 Spring 开启注解配置 AOP 的支持-->
    <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>

XML + 注解方式

XML 中开启 Spring 对注解 AOP 的支持

1
<aop:aspectj-autoproxy/>

Java 代码示例

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@Component
@Aspect
public class LogUtils {

@Pointcut("execution(* com.lagou.edu.service..*.*(..))")
public void pointCut() {
}

@Before("pointCut()")
public void beforeMethod(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
System.out.println("业务逻辑之前...");
}

@AfterReturning(value = "pointCut()", returning = "result")
public void afterMethod(JoinPoint joinPoint, Object result) {
Object[] args = joinPoint.getArgs();
for (Object arg : args) {
System.out.println(arg);
}
System.out.println("业务逻辑之后..." + result);
}

@AfterThrowing("pointCut()")
public void exceptionMethod() {
System.out.println("业务逻辑异常...");
}

@After("pointCut()")
public void finalMethod() {
System.out.println("业务逻辑结束...");
}

@Around("pointCut()")
public Object aroundMethod(ProceedingJoinPoint proceedingJoinPoint) {
Object result;

try {
Object[] args = proceedingJoinPoint.getArgs();
System.out.println("环绕通知的前置通知...");
result = proceedingJoinPoint.proceed(args);
System.out.println("环绕通知的后置通知...");
} catch (Throwable e) {
System.out.println("环绕通知的异常通知...");
throw new RuntimeException(e);
} finally {
System.out.println("环绕通知的最终通知...");
}

return result;
}

}

注解方式

在使用注解驱动开发 aop 时,是用注解替换掉配置文件中的下面这行配置

1
<aop:aspectj-autoproxy/>

在配置类中使用如下注解替换上述配置

1
2
3
@EnableAspectJAutoProxy //开启spring对注解AOP的支持
public class SpringConfig {
}