SpringBoot自定义注解+AOP实现系统操作日志

在之前我们记录一些后台操作日志都是通过 logService.insert(log) 的方式,每次都要获取一堆信息,代码比较冗余和侵入性太强。我们想能不能通过一个东西抽取公共的代码,通过注解设置简单的日志描述即可自动完成一系列信息获取和日志的持久化操作。下面将介绍基于Spring 面向切面的思想和自定义注解来解决。

最终我们只需要如下图一个注解就能实现日志的记录

SpringBoot自定义注解+AOP实现系统操作日志

 

数据库

SpringBoot自定义注解+AOP实现系统操作日志

 

下面是具体实现

完整代码地址:https://github.com/saysky/sensboot

一、注解和面向切面的基本实现

1.注解类 SystemLog

  1. package com.liuyanzhao.sens.annotation;
  2. import com.liuyanzhao.sens.enums.LogType;
  3. import java.lang.annotation.*;
  4. /**
  5.  * 系统日志自定义注解
  6.  *
  7.  * @author liuyanzhao
  8.  */
  9. @Target({ElementType.PARAMETER, ElementType.METHOD})//作用于参数或方法上
  10. @Retention(RetentionPolicy.RUNTIME)
  11. @Documented
  12. public @interface SystemLog {
  13.     /**
  14.      * 日志名称
  15.      *
  16.      * @return
  17.      */
  18.     String description() default "";
  19.     /**
  20.      * 日志类型
  21.      *
  22.      * @return
  23.      */
  24.     LogType type() default LogType.OPERATION;
  25. }

 

2.枚举类 LogType

  1. package com.liuyanzhao.sens.enums;
  2. /**
  3.  * @author liuyanzhao
  4.  */
  5. public enum LogType {
  6.     /**
  7.      * 默认0操作
  8.      */
  9.     OPERATION,
  10.     /**
  11.      * 1登录
  12.      */
  13.     LOGIN
  14. }

 

3.切面类 SystemLogAspect

  1. package com.liuyanzhao.sens.aop;
  2. import com.liuyanzhao.sens.annotation.SystemLog;
  3. import com.liuyanzhao.sens.entity.Log;
  4. import com.liuyanzhao.sens.entity.User;
  5. import com.liuyanzhao.sens.enums.LogType;
  6. import com.liuyanzhao.sens.service.LogService;
  7. import com.liuyanzhao.sens.service.UserService;
  8. import com.liuyanzhao.sens.utils.IpInfoUtil;
  9. import com.liuyanzhao.sens.utils.ObjectUtil;
  10. import com.liuyanzhao.sens.utils.ThreadPoolUtil;
  11. import lombok.extern.slf4j.Slf4j;
  12. import org.aspectj.lang.JoinPoint;
  13. import org.aspectj.lang.annotation.AfterReturning;
  14. import org.aspectj.lang.annotation.Aspect;
  15. import org.aspectj.lang.annotation.Before;
  16. import org.aspectj.lang.annotation.Pointcut;
  17. import org.checkerframework.checker.units.qual.A;
  18. import org.springframework.beans.factory.annotation.Autowired;
  19. import org.springframework.core.NamedThreadLocal;
  20. import org.springframework.stereotype.Component;
  21. import javax.servlet.http.HttpServletRequest;
  22. import java.lang.reflect.Method;
  23. import java.util.Date;
  24. import java.util.HashMap;
  25. import java.util.Map;
  26. import java.util.Objects;
  27. /**
  28.  * Spring AOP实现日志管理
  29.  *
  30.  * @author liuyanzhao
  31.  */
  32. @Aspect
  33. @Component
  34. @Slf4j
  35. public class SystemLogAspect {
  36.     private static final ThreadLocal<Date> beginTimeThreadLocal = new NamedThreadLocal<Date>("ThreadLocal beginTime");
  37.     @Autowired
  38.     private LogService logService;
  39.     @Autowired
  40.     private UserService userService;
  41.     @Autowired
  42.     private IpInfoUtil ipInfoUtil;
  43.     @Autowired(required = false)
  44.     private HttpServletRequest request;
  45.     /**
  46.      * Controller层切点,注解方式
  47.      */
  48.     //@Pointcut("execution(* *..controller..*Controller*.*(..))")
  49.     @Pointcut("@annotation(com.liuyanzhao.sens.annotation.SystemLog)")
  50.     public void controllerAspect() {
  51.     }
  52.     /**
  53.      * 前置通知 (在方法执行之前返回)用于拦截Controller层记录用户的操作的开始时间
  54.      *
  55.      * @param joinPoint 切点
  56.      * @throws InterruptedException
  57.      */
  58.     @Before("controllerAspect()")
  59.     public void doBefore(JoinPoint joinPoint) throws InterruptedException {
  60.         //线程绑定变量(该数据只有当前请求的线程可见)
  61.         Date beginTime = new Date();
  62.         beginTimeThreadLocal.set(beginTime);
  63.     }
  64.     /**
  65.      * 后置通知(在方法执行之后并返回数据) 用于拦截Controller层无异常的操作
  66.      *
  67.      * @param joinPoint 切点
  68.      */
  69.     @AfterReturning("controllerAspect()")
  70.     public void after(JoinPoint joinPoint) {
  71.         try {
  72.             String username = "";
  73.             String description = getControllerMethodInfo(joinPoint).get("description").toString();
  74.             Map<String, String[]> requestParams = request.getParameterMap();
  75.             Log log = new Log();
  76.             //请求用户
  77.             //后台操作(非登录)
  78.             if (Objects.equals(getControllerMethodInfo(joinPoint).get("type"), 0)) {
  79.                 //后台操作请求(已登录)
  80.                 User user = userService.getLoginUser(request);
  81.                 if (user != null) {
  82.                     username = user.getUsername();
  83.                 }
  84.                 log.setUsername(username);
  85.             }
  86.             //日志标题
  87.             log.setName(description);
  88.             //日志类型
  89.             log.setLogType((int) getControllerMethodInfo(joinPoint).get("type"));
  90.             //日志请求url
  91.             log.setRequestUrl(request.getRequestURI());
  92.             //请求方式
  93.             log.setRequestType(request.getMethod());
  94.             //请求参数
  95.             log.setRequestParam(ObjectUtil.mapToString(requestParams));
  96.             //其他属性
  97.             log.setIp(ipInfoUtil.getIpAddr(request));
  98.             log.setCreateBy("system");
  99.             log.setUpdateBy("system");
  100.             log.setCreateTime(new Date());
  101.             log.setUpdateTime(new Date());
  102.             log.setDelFlag(0);
  103.             //.......
  104.             //请求开始时间
  105.             long beginTime = beginTimeThreadLocal.get().getTime();
  106.             long endTime = System.currentTimeMillis();
  107.             //请求耗时
  108.             Long logElapsedTime = endTime - beginTime;
  109.             log.setCostTime(logElapsedTime.intValue());
  110.             //持久化(存储到数据或者ES,可以考虑用线程池)
  111.             //logService.insert(log);
  112.             ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(log, logService));
  113.         } catch (Exception e) {
  114.             log.error("AOP后置通知异常", e);
  115.         }
  116.     }
  117.     /**
  118.      * 保存日志至数据库
  119.      */
  120.     private static class SaveSystemLogThread implements Runnable {
  121.         private Log log;
  122.         private LogService logService;
  123.         public SaveSystemLogThread(Log esLog, LogService logService) {
  124.             this.log = esLog;
  125.             this.logService = logService;
  126.         }
  127.         @Override
  128.         public void run() {
  129.             logService.insert(log);
  130.         }
  131.     }
  132.     /**
  133.      * 获取注解中对方法的描述信息 用于Controller层注解
  134.      *
  135.      * @param joinPoint 切点
  136.      * @return 方法描述
  137.      * @throws Exception
  138.      */
  139.     public static Map<String, Object> getControllerMethodInfo(JoinPoint joinPoint) throws Exception {
  140.         Map<String, Object> map = new HashMap<String, Object>(16);
  141.         //获取目标类名
  142.         String targetName = joinPoint.getTarget().getClass().getName();
  143.         //获取方法名
  144.         String methodName = joinPoint.getSignature().getName();
  145.         //获取相关参数
  146.         Object[] arguments = joinPoint.getArgs();
  147.         //生成类对象
  148.         Class targetClass = Class.forName(targetName);
  149.         //获取该类中的方法
  150.         Method[] methods = targetClass.getMethods();
  151.         String description = "";
  152.         Integer type = null;
  153.         for (Method method : methods) {
  154.             if (!method.getName().equals(methodName)) {
  155.                 continue;
  156.             }
  157.             Class[] clazzs = method.getParameterTypes();
  158.             if (clazzs.length != arguments.length) {
  159.                 //比较方法中参数个数与从切点中获取的参数个数是否相同,原因是方法可以重载哦
  160.                 continue;
  161.             }
  162.             description = method.getAnnotation(SystemLog.class).description();
  163.             type = method.getAnnotation(SystemLog.class).type().ordinal();
  164.             map.put("description", description);
  165.             map.put("type", type);
  166.         }
  167.         return map;
  168.     }
  169. }

注释已经很完善了,这里就不多说了,里面有一些工具类和 Log 相关的类后面会补充

至此,仅仅三个类即可实现我们之前的功能

 

 

二、日志的实体、DAO和Service层代码

  1. package com.liuyanzhao.sens.entity;
  2. import com.baomidou.mybatisplus.annotations.TableField;
  3. import com.baomidou.mybatisplus.annotations.TableId;
  4. import com.baomidou.mybatisplus.annotations.TableLogic;
  5. import com.baomidou.mybatisplus.annotations.TableName;
  6. import com.baomidou.mybatisplus.enums.IdType;
  7. import lombok.Data;
  8. import java.io.Serializable;
  9. import java.util.Date;
  10. /**
  11.  * @author liuyanzhao
  12.  */
  13. @Data
  14. @TableName("log")
  15. public class Log  implements Serializable {
  16.     private static final long serialVersionUID = 1L;
  17.     /**
  18.      * ID,自增
  19.      */
  20.     @TableId(type = IdType.AUTO)
  21.     private Long id;
  22.     /**
  23.      * 方法操作名称
  24.      */
  25.     private String name;
  26.     /**
  27.      * 日志类型 0登陆日志 1操作日志
  28.      */
  29.     private Integer logType;
  30.     /**
  31.      * 请求路径
  32.      */
  33.     private String requestUrl;
  34.     /**
  35.      * 请求类型
  36.      */
  37.     private String requestType;
  38.     /**
  39.      * 请求参数
  40.      */
  41.     private String requestParam;
  42.     /**
  43.      * 请求用户
  44.      */
  45.     private String username;
  46.     /**
  47.      * ip
  48.      */
  49.     private String ip;
  50.     /**
  51.      * ip信息
  52.      */
  53.     private String ipInfo;
  54.     /**
  55.      * 花费时间
  56.      */
  57.     private Integer costTime;
  58.     /**
  59.      * 删除状态:1删除,0未删除
  60.      */
  61.     @TableField(value = "del_flag")
  62.     @TableLogic
  63.     private Integer delFlag = 0;
  64.     /**
  65.      * 创建人用户名
  66.      */
  67.     private String createBy;
  68.     /**
  69.      * 创建时间
  70.      */
  71.     private Date createTime;
  72.     /**
  73.      * 更新人
  74.      */
  75.     private String updateBy;
  76.     /**
  77.      * 更新时间
  78.      */
  79.     private Date updateTime;
  80. }

 

 

2.日志DAO层,采用MyBatis-Plus

  1. package com.liuyanzhao.sens.mapper;
  2. import com.baomidou.mybatisplus.mapper.BaseMapper;
  3. import com.liuyanzhao.sens.entity.Log;
  4. import org.apache.ibatis.annotations.Mapper;
  5. /**
  6.  * @author 言曌
  7.  * @date 2019-08-09 15:15
  8.  */
  9. @Mapper
  10. public interface LogMapper extends BaseMapper<Log> {
  11. }

 

 

3.Service实现

  1. package com.liuyanzhao.sens.service.impl;
  2. import com.liuyanzhao.sens.entity.Log;
  3. import com.liuyanzhao.sens.mapper.LogMapper;
  4. import com.liuyanzhao.sens.service.LogService;
  5. import org.springframework.beans.factory.annotation.Autowired;
  6. import org.springframework.stereotype.Service;
  7. /**
  8.  * @author 言曌
  9.  * @date 2019-08-19 21:51
  10.  */
  11. @Service
  12. public class LogServiceImpl implements LogService {
  13.     @Autowired
  14.     private LogMapper logMapper;
  15.     @Override
  16.     public Integer insert(Log log) {
  17.         return logMapper.insert(log);
  18.     }
  19. }

 

关于用户的service实现这里就不贴了,我觉得没有必要哈,完整代码文末会贴 GitHub地址

 

三、线程池的使用

阿里巴巴代码规范中说明了,不建议使用 Executors.newFixedThreadPool(num); 这种形式创建线程,最好自己手动设置线程核心数和最大数以及队列大小,我们这里可以抽出个线程池静态工具类。

  1. package com.liuyanzhao.sens.utils;
  2. import java.util.concurrent.ArrayBlockingQueue;
  3. import java.util.concurrent.BlockingQueue;
  4. import java.util.concurrent.ThreadPoolExecutor;
  5. import java.util.concurrent.TimeUnit;
  6. /**
  7.  * @author liuyanzhao
  8.  */
  9. public class ThreadPoolUtil {
  10.     /**
  11.      * 线程缓冲队列
  12.      */
  13.     private static BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(100);
  14.     /**
  15.      * 核心线程数,会一直存活,即使没有任务,线程池也会维护线程的最少数量
  16.      */
  17.     private static final int SIZE_CORE_POOL = 5;
  18.     /**
  19.      * 线程池维护线程的最大数量
  20.      */
  21.     private static final int SIZE_MAX_POOL = 10;
  22.     /**
  23.      * 线程池维护线程所允许的空闲时间
  24.      */
  25.     private static final long ALIVE_TIME = 2000;
  26.     private static ThreadPoolExecutor pool = new ThreadPoolExecutor(SIZE_CORE_POOL, SIZE_MAX_POOL, ALIVE_TIME, TimeUnit.MILLISECONDS, bqueue, new ThreadPoolExecutor.CallerRunsPolicy());
  27.     static {
  28.         pool.prestartAllCoreThreads();
  29.     }
  30.     public static ThreadPoolExecutor getPool() {
  31.         return pool;
  32.     }
  33.     public static void main(String[] args) {
  34.         System.out.println(pool.getPoolSize());
  35.     }
  36. }

即该线程池随类加载时创建,维护5个线程,这5个线程永远存活,如果同时任务数大于5个,多余的将放在队列中,如果队列数超过队列最大大小2000,将开始创建额外的线程,直到线程数超过最大线程数10,调用 CallerRunsPolicy 拒绝策略。

 

使用方法如上切面类中

  1. ThreadPoolUtil.getPool().execute(new SaveSystemLogThread(log, logService));

 

四、完整代码地址

完整代码地址:https://github.com/saysky/sensboot

欢迎讨论,该项目主要用于基本框架和工具整合实例

 

  • 微信
  • 交流学习,有偿服务
  • weinxin
  • 博客/Java交流群
  • 资源分享,问题解决,技术交流。群号:590480292
  • weinxin
言曌

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

目前评论:1   其中:访客  1   博主  0

    • avatar hooqing

      楼主用的是idea么?模板好漂亮,哪一款给个下载链接呗