写在前面

本文不涉及过多的Spring aop基本概念以及基本用法介绍,以实际场景使用为主。

场景

我们通常有这样一个需求:打印后台接口请求的具体参数,打印接口请求的最终响应结果,以及记录哪个用户在什么时间点,访问了哪些接口,接口响应耗时多长时间等等。这样做的目的是为了记录用户的访问行为,同时便于跟踪接口调用情况,以便于出现问题时能够快速定位问题所在。

最简单的做法是这样的:

    @GetMapping(value = "/info")public BaseResult userInfo() {//1.打印接口入参日志信息,标记接口访问时间戳BaseResult result = mUserService.userInfo();//2.打印/入库 接口响应信息,响应时间等return result;}

这种做法没毛病,但是稍微比较敏感的同学就会发觉有以下缺点:

  • 每个接口都充斥着重复的代码,有没有办法提取这部分代码,做到统一管理呢?答案是使用 Spring aop 面向切面执行这段公共代码。
  • 充斥着 硬编码 的味道,有些场景会要求在接口响应结束后,打印日志信息,保存到数据库,甚至要把日志记录到elk日志系统等待,同时这些操作要做到可控,有没有什么操作可以直接声明即可?答案是使用自定义注解,声明式的处理访问日志。

自定义注解

新增日志注解类,注解作用于方法级别,运行时起作用。

@Target({ElementType.METHOD}) //注解作用于方法级别
@Retention(RetentionPolicy.RUNTIME) //运行时起作用
public @interface Loggable {/*** 是否输出日志*/boolean loggable() default true;/*** 日志信息描述,可以记录该方法的作用等信息。*/String descp() default "";/*** 日志类型,可能存在多种接口类型都需要记录日志,比如dubbo接口,web接口*/LogTypeEnum type() default LogTypeEnum.WEB;/*** 日志等级*/String level() default "INFO";/*** 日志输出范围,用于标记需要记录的日志信息范围,包含入参、返回值等。* ALL-入参和出参, BEFORE-入参, AFTER-出参*/LogScopeEnum scope() default LogScopeEnum.ALL;/*** 入参输出范围,值为入参变量名,多个则逗号分割。不为空时,入参日志仅打印include中的变量*/String include() default "";/*** 是否存入数据库*/boolean db() default true;/*** 是否输出到控制台** @return*/boolean console() default true;
}

日志类型枚举类:

public enum LogTypeEnum {WEB("-1"), DUBBO("1"), MQ("2");private final String value;LogTypeEnum(String value) {this.value = value;}public String value() {return this.value;}
}

日志作用范围枚举类:

public enum LogScopeEnum {ALL, BEFORE, AFTER;public boolean contains(LogScopeEnum scope) {if (this == ALL) {return true;} else {return this == scope;}}@Overridepublic String toString() {String str = "";switch (this) {case ALL:break;case BEFORE:str = "REQUEST";break;case AFTER:str = "RESPONSE";break;default:break;}return str;}
}

相关说明已在代码中注释,这里不再说明。

使用 Spring aop 重构

引入依赖:

    <dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.8.8</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjrt</artifactId><version>1.8.13</version></dependency><dependency><groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.22.0-GA</version></dependency>

配置文件启动aop注解,基于类的代理,并且在 spring 中注入 aop 实现类。

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans".....省略部分代码"><!-- 扫描controller --><context:component-scan base-package="**.*controller"/><context:annotation-config/><!-- 启动aop注解基于类的代理(这时需要cglib库),如果proxy-target-class属值被设置为false或者这个属性被省略,那么标准的JDK 基于接口的代理将起作用 --><aop:config proxy-target-class="true"/><!-- web层日志记录AOP实现 --><bean class="com.easywits.common.aspect.WebLogAspect"/>
</beans>

新增 WebLogAspect 类实现

/*** 日志记录AOP实现* create by zhangshaolin on 2018/5/1*/
@Aspect
@Component
public class WebLogAspect {private static final Logger LOGGER = LoggerFactory.getLogger(WebLogAspect.class);// 开始时间private long startTime = 0L;// 结束时间private long endTime = 0L;/*** Controller层切点*/@Pointcut("execution(* *..controller..*.*(..))")public void controllerAspect() {}/*** 前置通知 用于拦截Controller层记录用户的操作** @param joinPoint 切点*/@Before("controllerAspect()")public void doBeforeInServiceLayer(JoinPoint joinPoint) {}/*** 配置controller环绕通知,使用在方法aspect()上注册的切入点** @param point 切点* @return* @throws Throwable*/@Around("controllerAspect()")public Object doAround(ProceedingJoinPoint point) throws Throwable {// 获取requestRequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;HttpServletRequest request = servletRequestAttributes.getRequest();//目标方法实体Method method = ((MethodSignature) point.getSignature()).getMethod();boolean hasMethodLogAnno = method.isAnnotationPresent(Loggable.class);//没加注解 直接执行返回结果if (!hasMethodLogAnno) {return point.proceed();}//日志打印外部开关默认关闭String logSwitch = StringUtils.equals(RedisUtil.get(BaseConstants.CACHE_WEB_LOG_SWITCH), BaseConstants.YES) ? BaseConstants.YES : BaseConstants.NO;//记录日志信息LogMessage logMessage = new LogMessage();//方法注解实体Loggable methodLogAnnon = method.getAnnotation(Loggable.class);//处理入参日志handleRequstLog(point, methodLogAnnon, request, logMessage, logSwitch);//执行目标方法内容,获取执行结果Object result = point.proceed();//处理接口响应日志handleResponseLog(logSwitch, logMessage, methodLogAnnon, result);return result;}/*** 处理入参日志** @param point           切点* @param methodLogAnnon  日志注解* @param logMessage      日志信息记录实体*/private void handleRequstLog(ProceedingJoinPoint point, Loggable methodLogAnnon, HttpServletRequest request,LogMessage logMessage, String logSwitch) throws Exception {String paramsText = "";//参数列表String includeParam = methodLogAnnon.include();Map<String, Object> methodParamNames = getMethodParamNames(point.getTarget().getClass(), point.getSignature().getName(), includeParam);Map<String, Object> params = getArgsMap(point, methodParamNames);if (params != null) {//序列化参数列表paramsText = JSON.toJSONString(params);}logMessage.setParameter(paramsText);//判断是否输出日志if (methodLogAnnon.loggable()&& methodLogAnnon.scope().contains(LogScopeEnum.BEFORE)&& methodLogAnnon.console()&& StringUtils.equals(logSwitch, BaseConstants.YES)) {//打印入参日志LOGGER.info("【{}】 接口入参成功!, 方法名称:【{}】, 请求参数:【{}】", methodLogAnnon.descp().toString(), point.getSignature().getName(), paramsText);}startTime = System.currentTimeMillis();//接口描述logMessage.setDescription(methodLogAnnon.descp().toString());//...省略部分构造logMessage信息代码}/*** 处理响应日志** @param logSwitch         外部日志开关,用于外部动态开启日志打印* @param logMessage        日志记录信息实体* @param methodLogAnnon    日志注解实体* @param result           接口执行结果*/private void handleResponseLog(String logSwitch, LogMessage logMessage, Loggable methodLogAnnon, Object result) {endTime = System.currentTimeMillis();//结束时间logMessage.setEndTime(DateUtils.getNowDate());//消耗时间logMessage.setSpendTime(endTime - startTime);//是否输出日志if (methodLogAnnon.loggable()&& methodLogAnnon.scope().contains(LogScopeEnum.AFTER)) {//判断是否入库if (methodLogAnnon.db()) {//...省略入库代码}//判断是否输出到控制台if (methodLogAnnon.console() && StringUtils.equals(logSwitch, BaseConstants.YES)) {//...省略打印日志代码}}}/*** 获取方法入参变量名** @param cls        触发的类* @param methodName 触发的方法名* @param include    需要打印的变量名* @return* @throws Exception*/private Map<String, Object> getMethodParamNames(Class cls,String methodName, String include) throws Exception {ClassPool pool = ClassPool.getDefault();pool.insertClassPath(new ClassClassPath(cls));CtMethod cm = pool.get(cls.getName()).getDeclaredMethod(methodName);LocalVariableAttribute attr = (LocalVariableAttribute) cm.getMethodInfo().getCodeAttribute().getAttribute(LocalVariableAttribute.tag);if (attr == null) {throw new Exception("attr is null");} else {Map<String, Object> paramNames = new HashMap<>();int paramNamesLen = cm.getParameterTypes().length;int pos = Modifier.isStatic(cm.getModifiers()) ? 0 : 1;if (StringUtils.isEmpty(include)) {for (int i = 0; i < paramNamesLen; i++) {paramNames.put(attr.variableName(i + pos), i);}} else { // 若include不为空for (int i = 0; i < paramNamesLen; i++) {String paramName = attr.variableName(i + pos);if (include.indexOf(paramName) > -1) {paramNames.put(paramName, i);}}}return paramNames;}}/*** 组装入参Map** @param point       切点* @param methodParamNames 参数名称集合* @return*/private Map getArgsMap(ProceedingJoinPoint point,Map<String, Object> methodParamNames) {Object[] args = point.getArgs();if (null == methodParamNames) {return Collections.EMPTY_MAP;}for (Map.Entry<String, Object> entry : methodParamNames.entrySet()) {int index = Integer.valueOf(String.valueOf(entry.getValue()));if (args != null && args.length > 0) {Object arg = (null == args[index] ? "" : args[index]);methodParamNames.put(entry.getKey(), arg);}}return methodParamNames;}
}

使用注解的方式处理接口日志

接口改造如下:

    @Loggable(descp = "用户个人资料", include = "")@GetMapping(value = "/info")public BaseResult userInfo() {return mUserService.userInfo();}

可以看到,只添加了注解@Loggable,所有的web层接口只需要添加@Loggable注解就能实现日志处理了,方便简洁!最终效果如下:

访问入参,响应日志信息:

用户行为日志入库部分信息:

简单总结

  • 编写代码时,看到重复性代码应当立即重构,杜绝重复代码。
  • Spring aop 可以在方法执行前,执行时,执行后切入执行一段公共代码,非常适合用于公共逻辑处理。
  • 自定义注解,声明一种行为,使配置简化,代码层面更加简洁。

最后

更多原创文章会第一时间推送公众号【张少林同学】,欢迎关注!

Spring aop+自定义注解统一记录用户行为日志相关推荐

  1. 使用Spring AOP自定义注解方式实现用户操作日志记录

    1,开发环境 操作系统:Windows 7 JDK:1.8.0_161 Eclipse:Mars.2 Release (4.5.2) 2,自定义注解类UserLog @Target({ElementT ...

  2. spring AOP自定义注解方式实现日志管理

    转:spring AOP自定义注解方式实现日志管理 今天继续实现AOP,到这里我个人认为是最灵活,可扩展的方式了,就拿日志管理来说,用Spring AOP 自定义注解形式实现日志管理.废话不多说,直接 ...

  3. spring aop 自定义注解配合swagger注解保存操作日志到mysql数据库含(源码)

    spring aop 自定义注解保存操作日志到mysql数据库 一.思路 二.自定义注解 三.编写操作日志 四.编写操作日志切面\增强 五.使用 六.`注意` 一.思路 利用spring aop 对方 ...

  4. Spring AOP自定义注解并获取注解的参数

    环境 springboot:1.5 Intellij IDEA:2021.1 序言 最近有个需求,要做方法层面的权限控制.以前在公司使用的是spring security,然后使用注解 如下: @Pr ...

  5. Spring AOP 自定义注解记录操作日志

    1.自定义注释 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface ...

  6. Java之——Spring AOP自定义注解实现日志管理

    转载请注明出处:https://blog.csdn.net/l1028386804/article/details/80295737 1.定义日志类SystemLog package io.mykit ...

  7. spring中自定义注解(annotation)与AOP中获取注解___使用aspectj的@Around注解实现用户操作和操作结果日志

    spring中自定义注解(annotation)与AOP中获取注解 一.自定义注解(annotation) 自定义注解的作用:在反射中获取注解,以取得注解修饰的类.方法或属性的相关解释. packag ...

  8. Spring Boot + Aop 记录用户操作日志

    目录 一.前言 二.实战 1.设计用户操作日志表: sys_oper_log 2.引入依赖 3.自定义用户操作日志注解 4.自定义用户操作日志切面 5.MyLog注解的使用 6.最终效果 三.总结 一 ...

  9. JPOM - AOP+自定义注解实现操作日志记录

    文章目录 地址 版本 源码解析-AOP+自定义注解实现操作日志记录 地址 Gitee: https://gitee.com/dromara/Jpom 官网: https://jpom.io/ 一款简而 ...

  10. Spring Aop 常见注解和执行顺序

    欢迎关注方志朋的博客,回复"666"获面试宝典 来源:juejin.cn/post/7062506923194581029 Spring 一开始最强大的就是 IOC / AOP 两 ...

最新文章

  1. C语言经典例85-判断一个素数能被几个9整除
  2. QT的QWhatsThis类的使用
  3. linux驱动导出文件属性,将Linux配置文件和设置备份到USB闪存驱动器的方法
  4. Redis数据库(二)——数据类型
  5. 使用async,await关键字进行API Access Token的获取
  6. 【转】jQuery中的bind(),live(),delegate(),on()事件绑定方式的区别
  7. 实现DDD领域驱动设计: Part 1
  8. Scala学习之字符串篇(六):使用正则表达式
  9. 桌面HTML更换图标,系统图标替换教程,美化您的电脑图标
  10. 计算机坐标公式,经纬度换算坐标公式(经纬度转大地坐标公式)
  11. 孤独星球android app,《孤独星球》终于出了全套免费的旅行指南APP!
  12. snapchat_如何重播Snapchat快照
  13. 医保业务综合服务终端技术规范_增值税发票综合服务平台出口退税业务操作指引...
  14. java公路车的气嘴_5分钟了解运动自行车常见的两种气嘴
  15. erlang httpc
  16. 数学建模overleaf模板_数学建模从入门到精通必备资料,大神经验助你赢战9月数模国赛!...
  17. 【C#】无法从命令行或调试器启动服务,必须首先安装Windows服务(使用installutil.exe)
  18. 脚手架的logo字符图片生成
  19. matlab学霸表白公式,《爱的数学公式》上线 帅气学霸的正确表白方式
  20. 2021年熔化焊接与热切割报名考试及熔化焊接与热切割模拟考试题

热门文章

  1. uniapp获取用户数据昵称为“微信用户”(小程序)@杨章隐
  2. 新语法之CORRESPONDING
  3. 用python实现多人聊天室小项目笔记
  4. 从Palm到Pocket PC(转)
  5. Pocket PC模拟器设置上网
  6. php怎样给搜索框加放大镜,Win10系统给小娜搜索框添加放大镜和箭头图标的方法...
  7. F - Game on Plane ( SG博弈 )
  8. R语言基于方差分析ANOVA检验模型拟合度(Fit Test)实战:检验同一数据集简单模型和复杂模型的拟合度差异
  9. 我只会HelloWorld,但是我却完成了一个SpringBoot项目!(1)
  10. ubuntu18.04安装0.6以上版本的flameshot