Spring aop+自定义注解统一记录用户行为日志
写在前面
本文不涉及过多的Spring aop
基本概念以及基本用法介绍,以实际场景使用为主。
场景
我们通常有这样一个需求:打印后台接口请求的具体参数,打印接口请求的最终响应结果,以及记录哪个用户在什么时间点,访问了哪些接口,接口响应耗时多长时间等等。这样做的目的是为了记录用户的访问行为,同时便于跟踪接口调用情况,以便于出现问题时能够快速定位问题所在。
最简单的做法是这样的:
1 @GetMapping(value = "/info")
2 public BaseResult userInfo() {
3 //1.打印接口入参日志信息,标记接口访问时间戳
4 BaseResult result = mUserService.userInfo();
5 //2.打印/入库 接口响应信息,响应时间等
6 return result;
7 }
这种做法没毛病,但是稍微比较敏感的同学就会发觉有以下缺点:
- 每个接口都充斥着重复的代码,有没有办法提取这部分代码,做到统一管理呢?答案是使用
Spring aop
面向切面执行这段公共代码。 - 充斥着 硬编码 的味道,有些场景会要求在接口响应结束后,打印日志信息,保存到数据库,甚至要把日志记录到
elk
日志系统等待,同时这些操作要做到可控,有没有什么操作可以直接声明即可?答案是使用自定义注解
,声明式的处理访问日志。
自定义注解
新增日志注解类,注解作用于方法级别,运行时起作用。
1@Target({ElementType.METHOD}) //注解作用于方法级别
2@Retention(RetentionPolicy.RUNTIME) //运行时起作用
3public @interface Loggable {
4
5 /**
6 * 是否输出日志
7 */
8 boolean loggable() default true;
9
10 /**
11 * 日志信息描述,可以记录该方法的作用等信息。
12 */
13 String descp() default "";
14
15 /**
16 * 日志类型,可能存在多种接口类型都需要记录日志,比如dubbo接口,web接口
17 */
18 LogTypeEnum type() default LogTypeEnum.WEB;
19
20 /**
21 * 日志等级
22 */
23 String level() default "INFO";
24
25 /**
26 * 日志输出范围,用于标记需要记录的日志信息范围,包含入参、返回值等。
27 * ALL-入参和出参, BEFORE-入参, AFTER-出参
28 */
29 LogScopeEnum scope() default LogScopeEnum.ALL;
30
31 /**
32 * 入参输出范围,值为入参变量名,多个则逗号分割。不为空时,入参日志仅打印include中的变量
33 */
34 String include() default "";
35
36 /**
37 * 是否存入数据库
38 */
39 boolean db() default true;
40
41 /**
42 * 是否输出到控制台
43 *
44 * @return
45 */
46 boolean console() default true;
47}
日志类型枚举类:
1public enum LogTypeEnum {
2
3 WEB("-1"), DUBBO("1"), MQ("2");
4
5 private final String value;
6
7 LogTypeEnum(String value) {
8 this.value = value;
9 }
10
11 public String value() {
12 return this.value;
13 }
14}
日志作用范围枚举类:
1public enum LogScopeEnum {
2
3 ALL, BEFORE, AFTER;
4
5 public boolean contains(LogScopeEnum scope) {
6 if (this == ALL) {
7 return true;
8 } else {
9 return this == scope;
10 }
11 }
12
13 @Override
14 public String toString() {
15 String str = "";
16 switch (this) {
17 case ALL:
18 break;
19 case BEFORE:
20 str = "REQUEST";
21 break;
22 case AFTER:
23 str = "RESPONSE";
24 break;
25 default:
26 break;
27 }
28 return str;
29 }
30}
相关说明已在代码中注释,这里不再说明。
使用 Spring aop 重构
引入依赖:
1 <dependency>
2 <groupId>org.aspectj</groupId>
3 <artifactId>aspectjweaver</artifactId>
4 <version>1.8.8</version>
5 </dependency>
6 <dependency>
7 <groupId>org.aspectj</groupId>
8 <artifactId>aspectjrt</artifactId>
9 <version>1.8.13</version>
10 </dependency>
11 <dependency>
12 <groupId>org.javassist</groupId>
13 <artifactId>javassist</artifactId>
14 <version>3.22.0-GA</version>
15 </dependency>
配置文件启动aop
注解,基于类的代理,并且在 spring
中注入 aop
实现类。
1<?xml version="1.0" encoding="UTF-8"?>
2<beans xmlns="http://www.springframework.org/schema/beans"
3 .....省略部分代码">
4
5 <!-- 扫描controller -->
6 <context:component-scan base-package="**.*controller"/>
7 <context:annotation-config/>
8
9 <!-- 启动aop注解基于类的代理(这时需要cglib库),如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用 -->
10 <aop:config proxy-target-class="true"/>
11
12 <!-- web层日志记录AOP实现 -->
13 <bean class="com.easywits.common.aspect.WebLogAspect"/>
14</beans>
15
新增 WebLogAspect
类实现
1/**
2 * 日志记录AOP实现
3 * create by zhangshaolin on 2018/5/1
4 */
5@Aspect
6@Component
7public class WebLogAspect {
8
9 private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);
10
11 // 开始时间
12 private long startTime = 0L;
13
14 // 结束时间
15 private long endTime = 0L;
16
17 /**
18 * Controller层切点
19 */
20 @Pointcut("execution(* *..controller..*.*(..))")
21 public void controllerAspect() {
22 }
23
24 /**
25 * 前置通知 用于拦截Controller层记录用户的操作
26 *
27 * @param joinPoint 切点
28 */
29 @Before("controllerAspect()")
30 public void doBeforeInServiceLayer(JoinPoint joinPoint) {
31 }
32
33 /**
34 * 配置controller环绕通知,使用在方法aspect()上注册的切入点
35 *
36 * @param point 切点
37 * @return
38 * @throws Throwable
39 */
40 @Around("controllerAspect()")
41 public Object doAround(ProceedingJoinPoint point) throws Throwable {
42 // 获取request
43 RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
44 ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
45 HttpServletRequest request = servletRequestAttributes.getRequest();
46
47 //目标方法实体
48 Method method = ((MethodSignature) point.getSignature()).getMethod();
49 boolean hasMethodLogAnno = method
50 .isAnnotationPresent(Loggable.class);
51 //没加注解 直接执行返回结果
52 if (!hasMethodLogAnno) {
53 return point.proceed();
54 }
55
56 //日志打印外部开关默认关闭
57 String logSwitch = StringUtils.equals(RedisUtil.get(BaseConstants.CACHE_WEB_LOG_SWITCH), BaseConstants.YES) ? BaseConstants.YES : BaseConstants.NO;
58
59 //记录日志信息
60 LogMessage logMessage = new LogMessage();
61
62 //方法注解实体
63 Loggable methodLogAnnon = method.getAnnotation(Loggable.class);
64
65 //处理入参日志
66 handleRequstLog(point, methodLogAnnon, request, logMessage, logSwitch);
67
68 //执行目标方法内容,获取执行结果
69 Object result = point.proceed();
70
71 //处理接口响应日志
72 handleResponseLog(logSwitch, logMessage, methodLogAnnon, result);
73 return result;
74 }
75
76 /**
77 * 处理入参日志
78 *
79 * @param point 切点
80 * @param methodLogAnnon 日志注解
81 * @param logMessage 日志信息记录实体
82 */
83 private void handleRequstLog(ProceedingJoinPoint point, Loggable methodLogAnnon, HttpServletRequest request,
84 LogMessage logMessage, String logSwitch) throws Exception {
85
86 String paramsText = "";
87 //参数列表
88 String includeParam = methodLogAnnon.include();
89 Map<String, Object> methodParamNames = getMethodParamNames(
90 point.getTarget().getClass(), point.getSignature().getName(), includeParam);
91 Map<String, Object> params = getArgsMap(
92 point, methodParamNames);
93 if (params != null) {
94 //序列化参数列表
95 paramsText = JSON.toJSONString(params);
96 }
97 logMessage.setParameter(paramsText);
98 //判断是否输出日志
99 if (methodLogAnnon.loggable()
100 && methodLogAnnon.scope().contains(LogScopeEnum.BEFORE)
101 && methodLogAnnon.console()
102 && StringUtils.equals(logSwitch, BaseConstants.YES)) {
103 //打印入参日志
104 LOGGER.info("【{}】 接口入参成功!, 方法名称:【{}】, 请求参数:【{}】", methodLogAnnon.descp().toString(), point.getSignature().getName(), paramsText);
105 }
106 startTime = System.currentTimeMillis();
107 //接口描述
108 logMessage.setDescription(methodLogAnnon.descp().toString());
109
110 //...省略部分构造logMessage信息代码
111 }
112
113 /**
114 * 处理响应日志
115 *
116 * @param logSwitch 外部日志开关,用于外部动态开启日志打印
117 * @param logMessage 日志记录信息实体
118 * @param methodLogAnnon 日志注解实体
119 * @param result 接口执行结果
120 */
121 private void handleResponseLog(String logSwitch, LogMessage logMessage, Loggable methodLogAnnon, Object result) {
122 endTime = System.currentTimeMillis();
123 //结束时间
124 logMessage.setEndTime(DateUtils.getNowDate());
125 //消耗时间
126 logMessage.setSpendTime(endTime - startTime);
127 //是否输出日志
128 if (methodLogAnnon.loggable()
129 && methodLogAnnon.scope().contains(LogScopeEnum.AFTER)) {
130 //判断是否入库
131 if (methodLogAnnon.db()) {
132 //...省略入库代码
133 }
134 //判断是否输出到控制台
135 if (methodLogAnnon.console()
136 && StringUtils.equals(logSwitch, BaseConstants.YES)) {
137 //...省略打印日志代码
138 }
139 }
140 }
141 /**
142 * 获取方法入参变量名
143 *
144 * @param cls 触发的类
145 * @param methodName 触发的方法名
146 * @param include 需要打印的变量名
147 * @return
148 * @throws Exception
149 */
150 private Map<String, Object> getMethodParamNames(Class cls,
151 String methodName, String include) throws Exception {
152 ClassPool pool = ClassPool.getDefault();
153 pool.insertClassPath(new ClassClassPath(cls));
154 CtMethod cm = pool.get(cls.getName()).getDeclaredMethod(methodName);
155 LocalVariableAttribute attr = (LocalVariableAttribute) cm
156 .getMethodInfo().getCodeAttribute()
157 .getAttribute(LocalVariableAttribute.tag);
158
159 if (attr == null) {
160 throw new Exception("attr is null");
161 } else {
162 Map<String, Object> paramNames = new HashMap<>();
163 int paramNamesLen = cm.getParameterTypes().length;
164 int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;
165 if (StringUtils.isEmpty(include)) {
166 for (int i = 0; i < paramNamesLen; i++) {
167 paramNames.put(attr.variableName(i + pos), i);
168 }
169 } else { // 若include不为空
170 for (int i = 0; i < paramNamesLen; i++) {
171 String paramName = attr.variableName(i + pos);
172 if (include.indexOf(paramName) > -1) {
173 paramNames.put(paramName, i);
174 }
175 }
176 }
177 return paramNames;
178 }
179 }
180
181 /**
182 * 组装入参Map
183 *
184 * @param point 切点
185 * @param methodParamNames 参数名称集合
186 * @return
187 */
188 private Map getArgsMap(ProceedingJoinPoint point,
189 Map<String, Object> methodParamNames) {
190 Object[] args = point.getArgs();
191 if (null == methodParamNames) {
192 return Collections.EMPTY_MAP;
193 }
194 for (Map.Entry<String, Object> entry : methodParamNames.entrySet()) {
195 int index = Integer.valueOf(String.valueOf(entry.getValue()));
196 if (args != null && args.length > 0) {
197 Object arg = (null == args[index] ? "" : args[index]);
198 methodParamNames.put(entry.getKey(), arg);
199 }
200 }
201 return methodParamNames;
202 }
203}
使用注解的方式处理接口日志
接口改造如下:
1 @Loggable(descp = "用户个人资料", include = "")
2 @GetMapping(value = "/info")
3 public BaseResult userInfo() {
4 return mUserService.userInfo();
5 }
可以看到,只添加了注解@Loggable
,所有的web
层接口只需要添加@Loggable
注解就能实现日志处理了,方便简洁!最终效果如下:
访问入参,响应日志信息:
用户行为日志入库部分信息:
简单总结
- 编写代码时,看到重复性代码应当立即重构,杜绝重复代码。
Spring aop
可以在方法执行前,执行时,执行后切入执行一段公共代码,非常适合用于公共逻辑处理。- 自定义注解,声明一种行为,使配置简化,代码层面更加简洁。
最后
更多原创文章会第一时间推送公众号【张少林同学】,欢迎关注!
Spring aop+自定义注解统一记录用户行为日志的更多相关文章
- spring AOP自定义注解方式实现日志管理
今天继续实现AOP,到这里我个人认为是最灵活,可扩展的方式了,就拿日志管理来说,用Spring AOP 自定义注解形式实现日志管理.废话不多说,直接开始!!! 关于配置我还是的再说一遍. 在appli ...
- spring AOP自定义注解 实现日志管理
今天继续实现AOP,到这里我个人认为是最灵活,可扩展的方式了,就拿日志管理来说,用Spring AOP 自定义注解形式实现日志管理.废话不多说,直接开始!!! 关于配置我还是的再说一遍. 在appli ...
- 利用Spring AOP自定义注解解决日志和签名校验
转载:http://www.cnblogs.com/shipengzhi/articles/2716004.html 一.需解决的问题 部分API有签名参数(signature),Passport首先 ...
- (转)利用Spring AOP自定义注解解决日志和签名校验
一.需解决的问题 部分API有签名参数(signature),Passport首先对签名进行校验,校验通过才会执行实现方法. 第一种实现方式(Origin):在需要签名校验的接口里写校验的代码,例如: ...
- Spring AOP 自定义注解实现统一日志管理
一.AOP的基本概念: AOP,面向切面编程,常用于日志,事务,权限等业务处理.AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容(Spring核心之一),是函数式编程 ...
- 使用Spring Aop自定义注解实现自动记录日志
百度加自己琢磨,以下亲测有效,所以写下来记录,也方便自己回顾浏览加深印象之类,有什么问题可以评论一起解决,不完整之处也请大佬指正,一起进步哈哈(1)首先配置文件: <!-- 声明自动为sprin ...
- Spring AOP 自定义注解获取http接口及WebService接口入参和出参
注解方法实现过程中可以采用如下获取方式:—以下为例 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHo ...
- redis分布式锁-spring boot aop+自定义注解实现分布式锁
接这这一篇redis分布式锁-java实现末尾,实现aop+自定义注解 实现分布式锁 1.为什么需要 声明式的分布式锁 编程式分布式锁每次实现都要单独实现,但业务量大功能复杂时,使用编程式分布式锁无疑 ...
- SpringBoot系列(十三)统一日志处理,logback+slf4j AOP+自定义注解,走起!
往期精彩推荐 SpringBoot系列(一)idea新建Springboot项目 SpringBoot系列(二)入门知识 springBoot系列(三)配置文件详解 SpringBoot系列(四)we ...
随机推荐
- jQuery绑定事件的四種方式
这篇文章主要介绍的是jQuery绑定事件的四种方式相关内容,下面我们就与大家一起分享. jQuery绑定事件的四种方式 jQuery提供了多种绑定事件的方式,每种方式各有其特点,明白了它们之间的异同点 ...
- python 开发简单的聊天工具-乾颐堂
python 太强大了,以至于它什么都可以做,哈哈,开个玩笑.但是今天要讲的真的是一个非常神奇的应用. 使用python写一个聊天工具 其实大家平时用的QQ类似的聊天工具,也是使用socket进行聊天 ...
- python进行数据清理之pandas中的drop用法
好久好久没有更新博客了,之前自学的估计也都忘记差不多了.由于毕业选择从事的行业与自己的兴趣爱好完全两条路,心情也难过了很久,既然入职了就要好好干,仍要保持自己的兴趣,利用业余时间重拾之前的乐趣. 从基 ...
- PV对第三方存储的访问模式支持
访问模式 PV可以使用存储资源提供商支持的任何方法来映射到host中.如下的表格中所示,提供商有着不同的功能,每个PV的访问模式被设置为卷支持的指定模式.比如,NFS可以支持多个读/写的客户端,但可以 ...
- mysql 更新替换字符串
update zxg set newlevel = REPLACE(newlevel,'b','') 把表zxg中的newlevel字段中的b删除
- js 根本没有“JSON对象”这回事! JSON对象——转
前言 写这篇文章的目的是经常看到开发人员说:把字符串转化为JSON对象,把JSON对象转化成字符串等类似的话题,所以把之前收藏的一篇老外的文章整理翻译了一下,供大家讨论,如有错误,请大家指出,多谢. ...
- BTrace追踪Hadoop部署
Hadoop集成BTrace 1.BTrace 1.1BTrace部署 1.下载BTrace工具包,官网地址:https://kenai.com/projects/btrace/downloads/d ...
- 初级 Web 开发人员的 Tomcat
介绍使用 Tomcat 对 JavaServer Pages (JSP).servlet 和 Web 服务进行编程,Tomcat 是来自 Apache Foundation 的开源应用服务器.本教程引 ...
- Tomcat 系统架构与设计模式1
从 Tomcat 如何分发请求.如何处理多用户同时请求,还有它的多级容器是如何协调工作的角度来分析 Tomcat 的工作原理,这也是一个 Web 服务器首要解决的关键问题 Tomcat 总体结构 To ...
- Oracle EBS 快捷键
打开菜单 Help > Keyboard Help... 功能 快捷键 =================================== 1 ...