Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

Spring AOP详解

Spring AOP详解

AOP介绍

AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,不利于模块的复用。AOP利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可复用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。使用”切面”技术,AOP把软件系统分为两个部分:核心关注点横切关注点 。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事物。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

  AOP(Aspect Orient Programming),我们一般称为面向方面(切面)编程,作为面向对象的一种补充,用于处理系统中分布于各个模块的横切关注点,比如事务管理、日志、缓存等等。AOP实现的关键在于AOP框架自动创建的AOP代理,AOP代理主要分为静态代理和动态代理,静态代理的代表为AspectJ;而动态代理则以Spring AOP为代表。

  与AspectJ的静态代理不同,Spring AOP使用的动态代理,所谓的动态代理就是说AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。Spring AOP中的动态代理主要有两种方式,JDK动态代理CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且 要求被代理的类必须实现一个接口 。JDK动态代理的核心是InvocationHandler接口和Proxy类。__如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类__。CGLIB(Code Generation Library),是一个代码生成的类库,是利用asm开源包,可以在运行时动态的生成某个类的子类。注意, __CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的__。
这里有注意的几点如下:

  • 从Spring 3.2以后不再将CGLIB放在项目的classpath下,而是将CGLIB类打包放在spring-core下面的org.springframework中。这个就意味着基于CGLIB的动态代理与JDK的动态代理在支持“just works”就一样了。
  • 在Spring 4.0中,因为CGLIB代理实例是通过Objenesis创建的,所以代理对象的构造器不再有两次调用。
  • 在 Spring Boot 2.0 中,Spring Boot现在默认使用CGLIB动态代理(基于类的动态代理), 包括AOP. 如果需要基于接口的动态代理(JDK基于接口的动态代理) , 需要设置spring.aop.proxy-target-class属性为false。

AOP核心概念

  • 横切关注点
    对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
  • 切面(aspect)
    类是对物体特征的抽象,切面就是对横切关注点的抽象
  • 连接点(joinpoint)
    被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法
  • 切入点(pointcut)
    对连接点进行拦截的定义
  • 通知(advice)
    所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类
  • 目标对象
    代理的目标对象
  • 织入(weave)
    将切面应用到目标对象并导致代理对象创建的过程
  • 引入(introduction)
    在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段

Spring对AOP的支持

Spring中AOP代理由Spring的IOC容器负责生成、管理,其依赖关系也由IOC容器负责管理。因此,AOP代理可以直接使用容器中的其它bean实例作为目标,这种关系可由IOC容器的依赖注入提供。Spring创建代理的规则为:

  1. 默认使用Java动态代理来创建AOP代理,这样就可以为任何接口实例创建代理了
  2. 当需要代理的类没有实现任何接口的时候,Spring会切换为使用CGLIB代理,也可强制使用CGLIB

Spring除了支持Schema方式配置AOP,还支持注解方式:使用@AspectJ风格的切面声明。@AspectJ 作为通过 Java 5 注释注释的普通的 Java 类,它指的是声明 aspects 的一种风格。AspectJ是静态代理的增强,所谓的静态代理就是AOP框架会在编译阶段生成AOP代理类,因此也称为编译时增强。

AspectJ: 基于字节码操作(Bytecode Manipulation),通过编织阶段(Weaving Phase),对目标Java类型的字节码进行操作,将需要的Advice逻辑给编织进去,形成新的字节码。毕竟JVM执行的都是Java源代码编译后得到的字节码,所以AspectJ相当于在这个过程中做了一点手脚,让Advice能够参与进来。

而编织阶段可以有两个选择,分别是加载时编织(也可以成为运行时编织)和编译时编织

  • 加载时编织(Load-Time Weaving):顾名思义,这种编织方式是在JVM加载类的时候完成的。
  • 编译时编织(Compile-Time Weaving):需要使用AspectJ的编译器来替换JDK的编译器。

添加spirng aop支持和AspectJ依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>

或(springboot)

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

使用场景是比如记录核心服务方法的执行耗时。

启用@AspectJ的支持

Spring默认不支持@AspectJ风格的切面声明,为了支持需要使用如下配置:

1
2
<!-- 自动扫描使用了aspectj注解的类 -->
<aop:aspectj-autoproxy/>

1
2
3
4
5
@Configuration
@ComponentScan("com.xxx")
@EnableAspectJAutoProxy//开启AspectJ注解
public class AopConfigurer {
}

声明切面

1
2
3
4
@Aspect
@Component
public class CustomLogAspect {
}

声明切入点

@AspectJ风格的命名切入点使用org.aspectj.lang.annotation包下的@Pointcut+方法(方法必须是返回void类型)实现。

1
2
@Pointcut(value="切入点表达式", argNames = "参数名列表")  
public void pointcutName(...) {}
  • value:指定切入点表达式;
  • argNames:指定命名切入点方法参数列表参数名字,可以有多个用“,”分隔,这些参数将传递给通知方法同名的参数,同时比如切入点表达式“args(param)”将匹配参数类型为命名切入点方法同名参数指定的参数类型。
  • pointcutName:切入点名字,可以使用该名字进行引用该切入点表达式。

切入点表达式

execution表达式

1
2
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
throws-pattern?)

上面的就是execution表达式的格式,execution匹配的就是连接点(Joinpoint,一般是方法),看上面的表达式execution是固定的,方法的修饰符是可选的,返回类型是必须的,定义的全限类型也是可选的,名称是必须的,参数是必须的,这些都可以使用通配符。

任何的public方法

1
execution(public * *(..))

以set开始的方法

1
execution(* set*(..))

定义在cn.freemethod.business.pack.Say接口中的方法

1
execution(* cn.freemethod.business.pack.Say.*(..))

任何cn.freemethod.business包中的方法

1
execution(* cn.freemethod.business.*.*(..))

任何定义在com.xyz.service包或者其子包中的方法

1
execution(* cn.freemethod.business..*.*(..))

其他表达式

任何在com.xyz.service包中的方法

1
within(com.xyz.service.*)

任何定义在com.xyz.service包或者其子包中的方法

1
within(com.xyz.service..*)

任何实现了com.xyz.service.AccountService接口中的方法

1
this(com.xyz.service.AccountService)

任何目标对象实现了com.xyz.service.AccountService的方法

1
target(com.xyz.service.AccountService)

一般情况下代理类(Proxy)和目标类(Target)都实现了相同的接口,所以上面的2个基本是等效的。

有且只有一个Serializable参数的方法
args(java.io.Serializable)
只要这个参数实现了java.io.Serializable接口就可以,不管是java.io.Serializable还是Integer,还是String都可以。

目标(target)使用了@Transactional注解的方法

1
@target(org.springframework.transaction.annotation.Transactional)

目标类(target)如果有Transactional注解中的所有方法

1
@within(org.springframework.transaction.annotation.Transactional)

任何方法有Transactional注解的方法

1
@annotation(org.springframework.transaction.annotation.Transactional)

有且仅有一个参数并且参数上类型上有Transactional注解

1
@args(org.springframework.transaction.annotation.Transactional)

注意是参数类型上有Transactional注解,而不是方法的参数上有注解。

bean的名字为tradeService中的方法

1
bean(simpleSay)

bean名字为simpleSay中的所有方法。

bean名字能匹配

1
bean(*Impl)

bean名字匹配*Impl的bean中的所有方法。

实例

计算核心服务的耗时

定义注解

1
2
3
4
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MethodCost {
}

定义切面

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
@Aspect
@Component
public class MethodCostAspect {

private final Logger logger = LoggerFactory.getLogger(MethodCostAspect.class);
@Pointcut("@annotation(com.forg.pan.component.aspect.MethodCost)")
public void methodCostPointcut() {
}

@SuppressWarnings("unchecked")
@Around("methodCostPointcut()")
public Object methodCost(ProceedingJoinPoint joinPoint) throws Throwable {

long startTime = System.currentTimeMillis();
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - startTime;

if(result != null && Result.class.isAssignableFrom(result.getClass())){
Result r = (Result)result;
r.setCost(cost);
}
if(logger.isDebugEnabled()){
String method = joinPoint.getSignature().getName();
String clazz = joinPoint.getTarget().getClass().getName();
Object []args = joinPoint.getArgs();

logger.debug("method: {}#{} cost {}ms , args: {}",clazz,method,cost, JSON.toJSONString(args));
}

return result;
}
}

使用注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@MethodCost
public ListResult<FileModel> listFile(String path){

String p = correctPath(path,root);

File rt = new File(p);

if(!rt.exists() || rt.isFile()){
//
return ListResult.createErrorResult(INVALID_PARAMTER,"无效目录");
}

File[] fs = rt.listFiles();

ListResult<FileModel> listResult
= ListResult.createSuccessResult(covertFile2FileModel(fs));

listResult.setListExt(path);

return listResult;
}

如果要改变参数需要使用AroundBefore只能读取而不能改变参数内容。
根据参数注解改变参数内容,设置默认值

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
@Pointcut("execution(public * *(.., @com.forg.pan.component.aspect.ReviseParamPath (*), ..))")
public void reviseParamPathPointcut() {

}

@SuppressWarnings("unchecked")
@Around("reviseParamPathPointcut()")
public Object methodCost(ProceedingJoinPoint joinPoint) throws Throwable {

joinPoint.getSignature().getDeclaringTypeName();
Object[] args = joinPoint.getArgs();

if(args.length <= 0){
return joinPoint.proceed();
}

if(args.length == 1){
if(StringUtils.isEmpty(args[0])){
args[0] = revisePath(args[0],SEPARATOR);
}
return joinPoint.proceed(args);
}

//多个参数的时候,获取指定注解参数
//目标对象
Object target = joinPoint.getTarget();

String longString = joinPoint.getSignature().toLongString();

String [] paramClass = longString.substring(
longString.lastIndexOf("(")+1,
longString.lastIndexOf(")")).split(",");

Class [] clz = new Class[args.length];

if(paramClass.length != args.length){
return joinPoint.proceed(args);
}

for(int i = 0; i< args.length ;i++){
if(args[i] ==null){

Class c = Class.forName(paramClass[i]);
clz[i] = c;
continue;
}
clz[i] = args[i].getClass();
}

try {
Method method = target.getClass().getDeclaredMethod(joinPoint.getSignature().getName(),clz);

Annotation[][] annotations = method.getParameterAnnotations();

if(annotations == null || annotations.length == 0){
return joinPoint.proceed(args);
}

List<Integer> pos = new ArrayList<>(args.length);
int p = 0;
for(Annotation[] as : annotations){
for(Annotation a : as){
if(a instanceof ReviseParamPath){
//
pos.add(p);
break;
}
}
p++;
}

if(pos.size() <= 0){
return joinPoint.proceed(args);
}

for (Integer _p : pos){
if(!(clz[_p].equals(String.class) )){
continue;
}
if(StringUtils.isEmpty(args[_p])){
args[_p] =revisePath(args[_p],SEPARATOR);
}
}

} catch (NoSuchMethodException e) {
logger.error("error",e);
return joinPoint.proceed(args);
}

Object result = joinPoint.proceed(args);

return result;
}

参考

Spring AOP 之一:基本概念与流程
Spring AOP 之二:Pointcut注解表达式
Spring AOP 介绍