2020年,悲伤!

那天,我突然好想打球。

前言

        上篇文章主要是针对性的日志记录,这篇文章为切面式的日志记录。总体结构为通过自定义注解的方式,通过 aop 切点拦截来实现日志记录。

        项目源码:https://github.com/XGLLHZ/springboot-frame.git

 

正文

        1、首先我们需要一个日志实体类。像主键、操作名(比如删除、新增等等)、类名、方法名、请求 api、请求参数、请求参数

、异常详情(如果是异常记录)等这些属性都得有。

@Data
@Accessors(chain = true)
@TableName("sys_log")
public class LogEntity extends BaseEntityUtil implements Serializable {

    @TableId(type = IdType.AUTO)
    private Integer id;   //主键
    private String userName;   //用户名
    private String operateName;   //操作名
    private String className;   //类名 + 方法名
    private String requestApi;   //请求 api
    private String requestParams;   //请求参数
    private Long requestTime;   //请求耗时
    private Integer logType;   //日志类型:1:正常日志;2:异常日志
    private String requestIp;   //请求 ip
    private String address;   //地址
    private String browser;   //浏览器
    private String exceptionDetail;   //异常详情
    private Integer deleteFlag;   //删除状态:0:未删除;1:已删除
    private Timestamp createTime;   //创建时间
    private Timestamp updateTime;   //修改时间

}

 

        2、其次、我们的日志是存储在数据库中的,所以肯定得有 mapper、service 等这一套来讲他存到数据库中。事务层的主要代码如下:

@Service
@Primary
public class LogServiceImpl extends ServiceImpl<LogMapper, LogEntity> implements LogService {
    @Autowired
    LogMapper logMapper;
    @Autowired
    RedisUtil redisUtil;
    @Autowired
    OnlineUserService onlineUserService;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Integer insertLog(ProceedingJoinPoint joinPoint, LogEntity logEntity) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
        HttpServletRequest request = HttpUtil.getRequest();
        //操作
        if (logEntity != null) {
            logEntity.setOperateName(logAnnotation.value());
        }
        assert logEntity != null;
        //用户名
        String ip = StringUtil.getUserIp(request);
        List<String> list = redisUtil.getListKey(ConstConfig.ONLINE_KEY + "*");
        Collections.reverse(list);
        List<OnlineUserEntity> list1 = new ArrayList<>();
        OnlineUserEntity onlineUserEntity1 = null;
        for (String key : list) {
            onlineUserEntity1 = (OnlineUserEntity) redisUtil.getValue(key);
            if (onlineUserEntity1 != null) {
                if (onlineUserEntity1.toString().contains(ip)) {
                    list1.add(onlineUserEntity1);
                }
            }
        }
        logEntity.setUserName(list1.get(0).getUserName());
        //用户 ip
        logEntity.setRequestIp(ip);
        //类名 + 方法名
        String className = joinPoint.getTarget().getClass().getName() + "." + methodSignature.getName() + "()";
        logEntity.setClassName(className);
        //参数
        StringBuilder stringBuilder = new StringBuilder("{");
        Object[] objects = joinPoint.getArgs();
        String[] strings = ((MethodSignature) joinPoint.getSignature()).getParameterNames();
        if (objects != null) {
            for (int i = 0; i < strings.length; i++) {
                stringBuilder.append(" ").append(strings[i]).append(": ").append(objects[i]);
            }
        }
        logEntity.setRequestParams(stringBuilder + "}");
        //请求 api
        String url = request.getRequestURI();
        logEntity.setRequestApi(url);
        //用户地址
        logEntity.setAddress(StringUtil.getAddressByIp(ip));
        //浏览器
        logEntity.setBrowser(StringUtil.getUserBrowser(request));
        Integer res = logMapper.insert(logEntity);
        return res;
    }

}

        上面的代码主要用来获取日志中的用户、堆栈信息,并将它们存放到数据库中。其中的参数 ProceedingJoinPoint ,通过这个类可以获取方法运行时的堆栈信息、路径、参数等,有兴趣的可以百度哈。我们自定义了一个注解 LogAnnotation,通过 其中的 value() 方法可以获取操作名,比如 @LogAnnotation("删除用户"),则此操作的名称就是删除用户。用户名等信息从 redis 中获取(用户登录时将其信息放入 redis 中)。用户的 ip、api、用户地址、所用浏览器等信息可以通过 request 来获取,笔者这里写了一个工具类,有兴趣的同学可以瞅瞅。然后再通过 ProceedingJoinPoint 对象来获取日志产生的方法路径、参数、文件名等信息。最后再将这些信息放入数据库。

 

        3、然后自定义注解,@LogAnnotation

@Target(ElementType.METHOD)   //日志作用范围(CLASS:类;METHOD:方法 等)
@Retention(RetentionPolicy.RUNTIME)   //日志有效范围(RUNTIME:运行时 等)
public @interface LogAnnotation {

    String value() default "";

}

        其中 @Target 注解意为这个注解的作用范围,比如作用在类、方法、接口上等。@Retention 可以理解为声明时候出发,比如方法运行时,类被实例化时等等. 

 

        4、然后我们需要通过 spring 的 aop 来实现日志的切点拦截。

@Slf4j
@Aspect
@Component
public class LogAspect {

    //spring 4 之后推荐构造方法式的注入
    private final LogService logService;

    ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public LogAspect(LogService logService) {
        this.logService = logService;
    }

    /**
     * 配置切入点
     */
    @Pointcut("@annotation(org.huangzi.main.common.annotation.LogAnnotation)")
    public void logPointCut() {
        //让同类中其他方法使用此切入点
    }

    /**
     * 配置通知(环绕通知,即方法执行完后)
     * 正常通知
     * @param joinPoint
     */
    @Around("logPointCut()")
    public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object object;
        //请求耗时
        threadLocal.set(System.currentTimeMillis());
        LogEntity logEntity = new LogEntity();
        logEntity.setLogType(ConstConfig.ORDINARY_LOG_CODE);
        object = joinPoint.proceed();   //执行方法,object 为方法返回值
        logEntity.setRequestTime(System.currentTimeMillis() - threadLocal.get());
        logEntity.setExceptionDetail("");
        threadLocal.remove();
        logService.insertLog(joinPoint, logEntity);
        return object;
    }

    /**
     * 配置通知
     * 异常通知
     * @param joinPoint
     * @param throwable
     */
    @AfterThrowing(pointcut = "logPointCut()", throwing = "throwable")
    public void logException(JoinPoint joinPoint, Throwable throwable) {
        //请求耗时
        LogEntity logEntity = new LogEntity();
        logEntity.setLogType(ConstConfig.ERROR_LOG_CODE);
        logEntity.setRequestTime(System.currentTimeMillis() - threadLocal.get());
        threadLocal.remove();
        //异常详情
        logEntity.setExceptionDetail(ThrowableUtil.getStackTrace(throwable));
        logService.insertLog((ProceedingJoinPoint) joinPoint, logEntity);
    }

}

        其中 @Aspect、@Pointcut等注解为 spring aop 中的相关注解,其意如下:

@Aspect:作用是把当前类标识为一个切面供容器读取
 
@Pointcut:Pointcut是植入Advice的触发条件。每个Pointcut的定义包括2部分,一是表达式,二是方法签名。方法签名必须是 public及void型。可以将Pointcut中的方法看作是一个被Advice引用的助记符,因为表达式不直观,因此我们可以通过方法签名的方式为 此表达式命名。因此Pointcut中的方法只需要方法签名,而不需要在方法体内编写实际代码。
@Around:环绕增强,相当于MethodInterceptor
@AfterReturning:后置增强,相当于AfterReturningAdvice,方法正常退出时执行
@Before:标识一个前置增强方法,相当于BeforeAdvice的功能,相似功能的还有
@AfterThrowing:异常抛出增强,相当于ThrowsAdvice
@After: final增强,不管是抛出异常或者正常退出都会执行

        在面向切面编程中,切面 = 切点 + 通知。上面的方法中 logPointcut() 方法为切入点,也就是注解作用的地方为切入点,logAround() 和 logException() 为通知,也就是什么时候产生日志。其中第一个为正常日志通知,也就是当 @LogAnnotation 注解作用的地方的方法正常执行完或者类被实例化后进行日志记录;第二个是异常日志,也就是当注解作用的地方代码运行产生异常时记录日志。

 

        5、示例。这里我们将 controller 层作为日志记录面,如下:

@RestController
@RequestMapping("/admin/log")
public class LogController {

    @Autowired
    LogService logService;

    @LogAnnotation("日志列表")
    @RequestMapping("/list")
    public APIResponse getList(@RequestBody LogEntity logEntity) {
        return logService.getList(logEntity);
    }

}

        当我们在接口方法上加入 @LogAnnotation 注解,放访问 /admin/log/list 接口时,就会将相关日志信息存入数据库中。

springboot开启日志记录 springboot 日志记录_springboot开启日志记录

 (请忽略乱码 0.0)

 

                             00000                                           000

                        00            00                               00     00

                                        00                           00         00

                                 00                              000000000000000

                        00                                                        00

                        0000000000                                        00