spring boot+AOP+自定义注解记录日志

java 在 jdk1.5 中引入了注解,spring 框架也正好把 java 注解发挥得淋漓尽致。
下面会讲解 Spring 中自定义注解的简单流程。

前提

引入 maven 依赖:

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

一、自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

/**
* 自定义 操作记录 注解
* 增加操作记录
* @author hanfeng
* @version 1.0
*/
@Target({ElementType.PARAMETER, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ActionRecord {
/**
* 自定义参数
* */
String parma;
/**
* 自定义参数 可以设置默认值
* */
String parma default "";

}

如上,一个自定义注解就已经定义结束,当然你也可以加上一些约束,比如只能加在类上,方法上等,这里不做举例。

二、解析注解

此处使用了spring 的 AOP(面向切面编程)特性,通过 @Aspect 注解使该类成为切面类,通过 @Pointcut 指定切入点 ,这里指定的切入点为 ActionRecord 注解类型,也就是被 @ActionRecord 注解修饰的方法,进入该切入点。

注解简述:

  • @Before 前置通知:在某连接点之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
  • @Around 环绕通知:可以实现方法执行前后操作,需要在方法内执行point.proceed(); 并返回结果。
  • @AfterReturning 后置通知:在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
  • @AfterThrowing 异常通知:在方法抛出异常退出时执行的通知。
  • @After 后置通知:在某连接点正常完成后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
  • 切入点参数说明:
    controllerAspect: 指定要扫描的包。
    @Pointcut中符号的解释:

    第一个 * 代表任意修饰符及任意返回值. 
    第二个 * 任意包名 
    第三个 * 代表任意方法. 
    第四个 * 定义在web包或者子包 
    第五个 * 任意方法 
    .. 匹配任意数量的参数.
    
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
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
import com.thunisoft.dzjz.commons.utils.UUIDUtils;
import com.thunisoft.dzjz.model.CzrzEntity;
import com.thunisoft.dzjz.service.CzrzService;
import javassist.*;
import javassist.bytecode.CodeAttribute;
import javassist.bytecode.LocalVariableAttribute;
import javassist.bytecode.MethodInfo;
import lombok.extern.log4j.Log4j;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.*;

/**
* 操作记录aop
*
* @author hanfeng
* @version 4.0
*/
@Aspect
@Component
@Log4j
public class CzrzAopAction {
//保证并发下 数据的纯洁
private static ThreadLocal<MetaData> metaDataContainer = ThreadLocal.withInitial(MetaData::new);

/**
* 获取开始时间
*/
private long BEGIN_TIME;
/**
* 获取结束时间
*/
private long END_TIME;
/**
* 获取监控的方法名称
*/
private String METHOD_NAME;
/**
* 定义本次log实体
*/
private List<CzrzEntity> czrzEntities = new ArrayList<>();

/**
* 切入点
*/
@Pointcut("execution(* com.thunisoft.dzjz.server.controller..*.*(..))")
private void controllerAspect() {
}

/**
* 前置通知
*/
@Before("controllerAspect()")
public void doBefore() {
BEGIN_TIME = System.currentTimeMillis();
}

/**
* 后置通知
*/
@After("controllerAspect()")
public void after() {
END_TIME = System.currentTimeMillis();
if (StringUtils.isNotBlank(METHOD_NAME)) {
log.debug("方法:【" + METHOD_NAME + "】运行时间为:" + (END_TIME - BEGIN_TIME));
}
}

/**
* 方法结束执行后的操作
*
* @param object response
*/
@AfterReturning(returning = "object", pointcut = "controllerAspect()")
public void doAfter(Object object) {
try {
//因为我的接口都是 RESTful 风格的 所以我才这么梳理 最终的验证视业务而定
ResponseEntity responseEntity = (ResponseEntity) object;
if (responseEntity.getStatusCode().is2xxSuccessful()) {
//保存记录 数据入库 不想用异步线程的 直接插入 保存方式视情况选择吧
CzrzRunner czrzRunner = new CzrzRunner(czrzEntities, czrzService);
czrzRunner.run();
for (CzrzEntity czrzEntity : czrzEntities) {
czrzService.addCzrz(czrzEntity);
}
}
} catch (Exception e) {
log.info("当前方法执行结束后拦截不到操作状态", e);
}

}

/**
* 方法有异常时的操作
*/
@AfterThrowing("controllerAspect()")
public void doAfterThrow() {
log.error("增加操作记录异常");
}

/**
* 环绕通知
*
* @param pjp 切片连接点
* @return 返回结果
* @throws Throwable 异常
*/
@Around("controllerAspect()")
public Object around(JoinPoint pjp) throws Throwable {
//自定义的当前线程的全局信息 可有可无 视业务增加
MetaData metaData = CzrzAopAction.metaDataContainer.get();
//获取被拦截的方法
Method method = getActionRecordMethod(metaData, pjp);
if (method == null) {
//log.info(……);
return;
}
//获取注解信息
ActionRecord actionRecord = method.getAnnotation(ActionRecord.class);
if (actionRecord == null) {
//log.info(……);
return;
}
// 默认不需要记录操作日志,如果有 ActionRecord 注解,说明需要记录日志
metaData.setNeedRecord(true);
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
metaData.fillReqInfo(request);

//获取请求中的参数 这里面包含了controller不需要的参数,需单独处理
String queryString = request.getQueryString();
//获取controller参数
Map<String, String[]> queryMap = dealQueryData(pjp, queryString);
//获取参数键值对
Map<String, String[]> queryRequestMap = request.getParameterMap();
//做去重处理 兼容各种参数形式
queryMap.putAll(queryRequestMap);
/**
*说明 :上面为什么样这么获取参数,是因为单存的用一种不能拦截到所有情况的参数。
*/

/**开始组装 日志实体信息 dosomeThing 这里不举例了
* 操作记录模板需要定制化的 要自己来实现模板数据的解析
* 如果 不同的功能记录不同的信息模板 则可以用下面的方式 否则只需要一条就ok
* 条件视业务定 如果定义了 metaData 需要将结果放入其中 如果没有 直接做入库操作即可
*/
switch (actionRecord.operation()) {
case 1:
dosomeThingToBuildczrzEntities;
metaData.czrzEntities.add(czrzEntity);
break;
case 2:
case 3:
dosomeThingToBuildczrzEntities;
metaData.czrzEntities.add(czrzEntity);
break;
default:
dosomeThingToBuildczrzEntities;
metaData.czrzEntities.add(czrzEntity);
break;
}
}
/**
* The type Meta data.
*/
@Data
private static class MetaData {……}

上面方法:getActionRecordMethod 的实现

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
/**
* 获取监控的方法
*
*
* @param metaData
* @param pjp 切片点
* @return 方法
*/
private Method getActionRecordMethod(MetaData metaData, JoinPoint pjp) {
// 拦截的方法名称。当前正在执行的方法
Signature sig = pjp.getSignature();
metaData.setMethodName(sig.getName());
// 拦截的放参数类型
if (!(sig instanceof MethodSignature)) {
throw new BadRequestException("添加操作记录失败。原因:@ActionRecord 该注解只能用于方法");
}
MethodSignature msig = (MethodSignature) sig;
Class[] parameterTypes = msig.getMethod().getParameterTypes();
try {
return pjp.getTarget().getClass().getMethod(sig.getName(), parameterTypes);
} catch (NoSuchMethodException e) {
log.warn("添加操作记录失败。原因:增加操作记录,监控方法时出现异常,方法名称:{}", sig.getName(), e);
return null;
}

}

上面方法: dealQueryData 的实现

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
 /**
* 处理拦截导弹参数和参数名称
*
* @param pjp 切片连接点
* @param queryString requset请求中参数
* @return 参数键值对
* @throws Exception 异常
*/
private Map<String, String[]> dealQueryData(JoinPoint pjp, String queryString) throws Exception {
//获取请求方法中的参数名称
String[] paramName = getFieldsName(pjp.getTarget().getClass().getName(), metaDataContainer.get().getMethodName());

// 拦截的controller方法参数值
Object[] args = pjp.getArgs();
if (ObjectUtils.isEmpty(paramName) || args.length != paramName.length) {
return new HashMap<>();
}
String paramterConnector = "&";
String paramterAssignment = "=";
Map<String, String[]> resultMap = new HashMap<>();
if (StringUtils.isNotBlank(queryString)) {
//这里获取到的结构是 key=value 的格式
String[] queryKeyAndValue = queryString.split(paramterConnector);
for (String parameter : queryKeyAndValue) {
//这里获取到的结构是 key & value 的值
String[] queryValue = parameter.split(paramterAssignment);
if (resultMap.containsKey(queryValue[0])) {
continue;
}
resultMap.put(queryValue[0], new String[] { queryValue[1] });
}
}
for (int i = 0; i < paramName.length; i++) {
if (resultMap.containsKey(paramName[i])) {
continue;
}
//做下特殊处理,有的解析后是个数组兼容下 可以视自己的业务来处理解析结果
if (args[i] instanceof String[]) {
resultMap.put(paramName[i], (String[]) args[i]);
} else if (args[i] instanceof String) {
resultMap.put(paramName[i], new String[] { args[i].toString() });
} else if(ObjectUtils.isEmpty(args[i])){
resultMap.put(paramName[i],new String[]{});
} else {
Object obj = args[i];
BeanInfo beanInfo = Introspector.getBeanInfo(obj.getClass());
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor property : propertyDescriptors) {
String key = property.getName();
if (key.compareToIgnoreCase("class") == 0 || key.compareToIgnoreCase("writer") == 0) {
continue;
}
Method getter = property.getReadMethod();
Object value = getter != null ? getter.invoke(obj) : null;
String objV = ObjectUtils.isEmpty(value) ? "" : value.toString();
resultMap.put(key, new String[] { objV });
}
}
}
return resultMap;
}

方法:getFieldsName 的实现

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
/**
* 使用javassist来获取方法参数名称
*
* @param className 类名
* @param methodName 方法名
* @return 名字集合
* @throws Exception 异常
*/
private String[] getFieldsName(String className, String methodName) throws Exception {
Class<?> clazz = Class.forName(className);
String clazzName = clazz.getName();
ClassPool pool = ClassPool.getDefault();
ClassClassPath classPath = new ClassClassPath(clazz);
pool.insertClassPath(classPath);

CtClass ctClass = pool.get(clazzName);
CtMethod ctMethod = ctClass.getDeclaredMethod(methodName);
MethodInfo methodInfo = ctMethod.getMethodInfo();
CodeAttribute codeAttribute = methodInfo.getCodeAttribute();
LocalVariableAttribute attr = (LocalVariableAttribute) codeAttribute.getAttribute(LocalVariableAttribute.tag);
if (attr == null) {
String message = "添加操作记录失败。原因:拦截请求:【" + metaDataContainer.get().getMethodName() + " 】的方法中的参数名称失败";
log.warn(message);
throw new BadRequestException(message);
}
String[] paramsArgsName = new String[ctMethod.getParameterTypes().length];
int pos = Modifier.isStatic(ctMethod.getModifiers()) ? 0 : 1;
for (int i = 0; i < paramsArgsName.length; i++) {
paramsArgsName[i] = attr.variableName(i + pos);
}
if (ObjectUtils.isEmpty(paramsArgsName)) {
String message = "添加操作记录失败。原因:拦截请求:【" + metaDataContainer.get().getMethodName() + " 】的方法中的参数名称失败";
log.warn(message);
throw new BadRequestException(message);
}
return paramsArgsName;
}

小结

小编认为,如果没有特殊 @Before@Around@AfterReturning@AfterThrowing@After 这些功能只需要保留 @Before@AfterReturning@AfterThrowing 这三个就可以慢足需求,
@Before 中组装信息,
@AfterReturning 来确认数据是否需要保留,
@AfterThrowing 来处理异常情况。
使用 AOP 还是很方便的。