需求

结合AOP技术和SpEL表达式实现一个简单的AOP切面

  • 计算耗时,并打印日志
  • 用户可以在注解中添加指定的字段名称,切面自动解析出值,放在日志里一起打印。

示例:在添加注解的时候,指定bizCode = "#msg",那么在方法被调用的时候,就会计算变量articleId在当前上下文中的值,并在日志中打印出来。

1
2
3
4
5
6
@MdcDot(bizCode = "#articleId")
@RequestMapping("/hello")
public String sayHello(String articleId) {
System.out.println("Hello, World :" + articleId);
return "success";
}

测试效果如下:

image-20240402195018995

实现

定义注解MdcDot

1
2
3
4
5
6
7
8
9
10
11
12
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MdcDot {
String bizCode() default ""; //业务代码
}

编写切面Aspect

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
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Slf4j
@Aspect
@Component
//通过继承ApplicationContextAware,Spring会自动调用setApplicationContext,这样可以获取到应用上下文ApplicationContext
public class MdcAspect implements ApplicationContextAware {
private ExpressionParser parser = new SpelExpressionParser();
private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

@Pointcut("@annotation(MdcDot) || @within(MdcDot)")
public void getLogAnnotation() {
}

@Around("getLogAnnotation()")
public Object handle(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
//判断有没有bizCode被解析的标志,如果MDC中还存储了其他变量(比如traceId),可以作为是否重置MDC的标志
boolean hasTag = addMdcCode(joinPoint);
try {
//执行原方法
Object ans = joinPoint.proceed();
return ans;
} finally {
log.info("执行耗时: {}#{} = {}ms",
joinPoint.getSignature().getDeclaringType().getSimpleName(),
joinPoint.getSignature().getName(),
System.currentTimeMillis() - start);
MdcUtil.clear();
}
}
}

private boolean addMdcCode(ProceedingJoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
MdcDot dot = method.getAnnotation(MdcDot.class);
if (dot == null) {
//这段代码检查前面获取的注解对象dot是否为null。
// 如果是null,则表示方法上没有直接的MdcDot注解。
// 在这种情况下,它通过joinPoint.getSignature().getDeclaringType()获取声明切点的类,
// 然后再次尝试获取该类上的MdcDot注解。如果找到了注解,将其赋值给dot变量。
dot = (MdcDot) joinPoint.getSignature().getDeclaringType().getAnnotation(MdcDot.class);
}

if (dot != null) {
MdcUtil.add("bizCode", loadBizCode(dot.bizCode(), joinPoint));
return true;
}
return false;
}

private String loadBizCode(String key, ProceedingJoinPoint joinPoint) {
if (StringUtils.isBlank(key)) {
return "";
}

StandardEvaluationContext context = new StandardEvaluationContext();

//设置上下文
context.setBeanResolver(new BeanFactoryResolver(applicationContext));
String[] params = parameterNameDiscoverer.getParameterNames(((MethodSignature) joinPoint.getSignature()).getMethod());
Object[] args = joinPoint.getArgs();
for (int i = 0; i < args.length; i++) {
context.setVariable(params[i], args[i]);
}
return parser.parseExpression(key).getValue(context, String.class);
}

private ApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

MDCUtil工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.slf4j.MDC;

public class MdcUtil {
public static final String TRACE_ID_KEY = "traceId";

public static void add(String key, String val) {
MDC.put(key, val);
}

public static void addTraceId() {
MDC.put(TRACE_ID_KEY, SelfTraceIdGenerator.generate());
}

public static String getTraceId() {
return MDC.get(TRACE_ID_KEY);
}

public static void clear() {
MDC.clear();
}
}

修改log4j.properties配置文件

1
2
# 添加%X,代表着MDC
log4j.appender.stdout.layout.ConversionPattern=[%t] [%X{bizCode}](%F:%L) - %m%n

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.raining.aspect.MdcDot;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("alpha")
public class AlphaController {

@MdcDot(bizCode = "#articleId")
@RequestMapping("/hello")
public String sayHello(String articleId) {
System.out.println("Hello, World :" + articleId);
return "success";
}
}

image-20240402195018995

参考