Hello World

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

0%

基于方法参数注解的切面

基于方法参数注解切面的实现方式参考
https://stackoverflow.com/questions/2766844/pointcut-matching-methods-with-annotated-parameters

背景

实现文件管理系统的时候对路径的修正采用了切面的方式:

RevisePath

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

RevisePathAspect
切点的声明

1
2
3
@Pointcut("execution(public * *(.., @com.forg.kfile.component.aspect.RevisePath (*), ..))")
public void reviseParamPathPointcut() {
}

修正路径参数需要改变方法的参数

1
2
3
4
@Around("reviseParamPathPointcut()")
public Object revisePath(ProceedingJoinPoint joinPoint) throws Throwable {
......
}

注解参数的识别与修改

Around注解方法的第一个参数类型是 ProceedingJoinPoint ,注意这个类是所属的包是org.aspectj.lang , spring 实现的类是 MethodInvocationProceedingJoinPoint

获取切点的参数是有直接的方法的

1
2
java.lang.Object[]	getArgs()
Returns the arguments at this join point.

但是哪一个参数是有注解的参数并不能直接获取到。起初我的实现比较复杂:
通过解析方法签名参数类型,通过反射找到对应对应的方法。在从方法上找到参数对应的所有的注解判断参数位置,确定参数位置后修改参数内容,代码如下

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
@Around("reviseParamPathPointcut()")
public Object revisePath(ProceedingJoinPoint joinPoint) throws Throwable {

Object[] args = joinPoint.getArgs();

//无参数不处理
if(args.length <= 0){
return joinPoint.proceed();
}

if(args.length == 1 ){

if( args[0] instanceof String || args[0] == null){
String path = (String)args[0];

args[0] = revisePath(path);
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 RevisePath){
//
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((String)args[_p]);
}
}

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

Object result = joinPoint.proceed(args);
return result;
}

其中确定调用方法就很麻烦,反射通过方法名称与参数类型去查找。因为我是通过spring做的aop,所以ProceedingJoinPoint的类型应该是MethodInvocationProceedingJoinPoint,而MethodInvocationProceedingJoinPoint类的说明很清楚的指名:

1
An implementation of the AspectJ ProceedingJoinPoint interface wrapping an AOP Alliance MethodInvocation.

它是MethodInvocation的包装。拿到MethodInvocation就能拿到method。而MethodInvocationProceedingJoinPoint中的methodInvocation并没有暴露出来,但是可以通过其中的MethodSignature获取

1
2
3
4
5
6
MethodInvocationProceedingJoinPoint methodInvocationProceedingJoinPoint =
(MethodInvocationProceedingJoinPoint)joinPoint;

Method method = ((MethodSignature)methodInvocationProceedingJoinPoint.getSignature()).getMethod();

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

当然也可以直接通过反射获取

1
2
3
4
5
6
MethodInvocationProceedingJoinPoint methodJoinPoint = (MethodInvocationProceedingJoinPoint) pjp;
Field methodInvocationfield = methodJoinPoint.getClass().getDeclaredField("methodInvocation");

methodInvocationfield.setAccessible(true);

Annotation[][] argAnnotations = ((ReflectiveMethodInvocation) methodInvocationfield.get(methodJoinPoint)).getMethod().getParameterAnnotations();

后面就是简单的定位到参数然后修改值后在传入proceed方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
List<Integer> positionList = new ArrayList<>(1);
int p = 0;
for(Annotation[] as : annotations){
for(Annotation a : as){
if(a instanceof RevisePath){
//
positionList.add(p);
break;
}
}
p++;
}

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

for (Integer position : positionList){
if(args[position] == null || args[position] instanceof String){
args[position] = revisePath((String)args[position]);
}
}
return joinPoint.proceed(args);

其他需要知道的

刚开始我使用的是@Before来修改参数,发现@Before是不能修改参数内容的,原因是
MethodInvocationProceedingJoinPoint

1
2
3
4
5
6
public Object[] getArgs() {
if (this.args == null) {
this.args = this.methodInvocation.getArguments().clone();
}
return this.args;
}

参考

MethodInvocationProceedingJoinPoint
https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/aop/aspectj/MethodInvocationProceedingJoinPoint.html

MethodInvocationProceedingJoinPoint example
https://www.programcreek.com/java-api-examples/?api=org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint

Pointcut matching methods with annotated parameters
https://stackoverflow.com/questions/2766844/pointcut-matching-methods-with-annotated-parameters

AspectJ: Pointcut to declare and retrieve an annotation of a method’s parameter
https://stackoverflow.com/questions/63894176/aspectj-pointcut-to-declare-and-retrieve-an-annotation-of-a-methods-parameter