关于springboot:笑小枫的SpringBoot系列十一SpringBoot接口日志信息统一记录

为什么要记录接口日志?

至于为什么,具体看到这里的小伙伴心里都有一个答案吧,我这里简略列一下罕用的场景吧🙈

  • 用户登录记录统计
  • 重要增删改操作留痕
  • 须要统计用户的拜访次数
  • 接口调用状况统计
  • 线上问题排查
  • 等等等…

既然有这么多应用场景,那咱们该怎么解决,总不能一条一条的去记录吧🥶

面试是不是老是被问Spring的Aop的应用场景,那这个典型的场景就来了,咱们能够应用Spring的Aop,完满的实现这个性能,接下来上代码😁

先定义一下日志存储的对象吧

本文波及到依赖:

  • lombok
  • swagger
  • mybatisplus

简略如下,能够依据本人的需要进行批改

贴一下建表sql吧

CREATE TABLE `sys_operate_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
  `title` varchar(50) DEFAULT '' COMMENT '模块题目',
  `business_type` int(2) DEFAULT '4' COMMENT '业务类型(0查问 1新增 2批改 3删除 4其余)',
  `method` varchar(100) DEFAULT '' COMMENT '办法名称',
  `resp_time` bigint(20) DEFAULT NULL COMMENT '响应工夫',
  `request_method` varchar(10) DEFAULT '' COMMENT '申请形式',
  `browser` varchar(255) DEFAULT NULL COMMENT '浏览器类型',
  `operate_type` int(1) DEFAULT '3' COMMENT '操作类别(0网站用户 1后盾用户 2小程序 3其余)',
  `operate_url` varchar(255) DEFAULT '' COMMENT '申请URL',
  `operate_ip` varchar(128) DEFAULT '' COMMENT '主机地址',
  `operate_location` varchar(255) DEFAULT '' COMMENT '操作地点',
  `operate_param` text COMMENT '申请参数',
  `json_result` text COMMENT '返回参数',
  `status` int(1) DEFAULT '0' COMMENT '操作状态(0失常 1异样)',
  `error_msg` text COMMENT '谬误音讯',
  `create_id` bigint(20) DEFAULT NULL COMMENT '操作人id',
  `create_name` varchar(50) DEFAULT '' COMMENT '操作人员',
  `create_time` datetime DEFAULT NULL COMMENT '操作工夫',
  `update_id` bigint(20) NULL DEFAULT NULL COMMENT '更新人id',
  `update_name` varchar(64) NULL DEFAULT '' COMMENT '更新者',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新工夫',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统管理-操作日志记录';

应用的mybatis plus的主动生成代码性能生成的对象,详情参考[SpringBoot集成Mybatis Plus](),真香🤪

package com.maple.demo.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.maple.demo.config.bean.BaseEntity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

/**
 * <p>
 * 系统管理-操作日志记录
 * </p>
 *
 * @author 笑小枫
 * @since 2022-07-21
 */
@Getter
@Setter
@TableName("sys_operate_log")
@ApiModel(value = "OperateLog对象", description = "系统管理-操作日志记录")
public class OperateLog extends BaseEntity {

    private static final long serialVersionUID = 1L;

    @ApiModelProperty("模块题目")
    private String title;

    @ApiModelProperty("业务类型(0查问 1新增 2批改 3删除 4其余)")
    private Integer businessType;

    @ApiModelProperty("办法名称")
    private String method;

    @ApiModelProperty("响应工夫")
    private Long respTime;

    @ApiModelProperty("申请形式")
    private String requestMethod;

    @ApiModelProperty("浏览器类型")
    private String browser;

    @ApiModelProperty("操作类别(0网站用户 1后盾用户 2小程序 3其余)")
    private Integer operateType;

    @ApiModelProperty("申请URL")
    private String operateUrl;

    @ApiModelProperty("主机地址")
    private String operateIp;

    @ApiModelProperty("操作地点")
    private String operateLocation;

    @ApiModelProperty("申请参数")
    private String operateParam;

    @ApiModelProperty("返回参数")
    private String jsonResult;

    @ApiModelProperty("操作状态(0失常 1异样)")
    private Integer status;

    @ApiModelProperty("谬误音讯")
    private String errorMsg;
}

mapper代码就不贴了,都是生成的,只用到了mybatis plus的insert办法,上面别再问我为什么少个类了😂

定义切点、Aop实现性能

定义波及到枚举类

在config包下创立一个专门寄存枚举的包enums吧(父包名称不应该叫vo的,是我格局小了,一误再误吧🙈)

业务类型BusinessTypeEnum枚举类:

package com.maple.demo.config.enums;

/**
 * @author 笑小枫
 * @date 2022/7/21
 */
public enum BusinessTypeEnum {
    // 0查问 1新增 2批改 3删除 4其余
    SELECT,
    INSERT,
    UPDATE,
    DELETE,
    OTHER
}

操作类别OperateTypeEnum枚举类:

package com.maple.demo.config.enums;


/**
 * @author 笑小枫
 * @date 2022/6/27
 */
public enum OperateTypeEnum {
    // 0网站用户 1后盾用户 2小程序 3其余
    BLOG,
    ADMIN,
    APP,
    OTHER
}

定义切点的注解

定义一个自定义注解MapleLog.java,哪些接口须要记录日志就靠它了,命名依据本人的调整哈,我的maple,谁叫我是笑小枫呢,不要好奇的点这个链接,不然你会发现惊喜😎

package com.maple.common.model;

import com.maple.common.enums.BusinessTypeEnum;
import com.maple.common.enums.OperateTypeEnum;

import java.lang.annotation.*;

/**
 * @author 笑小枫
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MapleLog {

    // 0网站用户 1后盾用户 2小程序 3其余
    OperateTypeEnum operateType() default OperateTypeEnum.OTHER;

    // 0查问 1新增 2批改 3删除 4其余
    BusinessTypeEnum businessType() default BusinessTypeEnum.SELECT;
    
    // 返回保留后果是否落库,没用的大后果能够不记录,比方分页查问等等,设为false即可
    boolean saveResult() default true;
}

Aop实现性能

应用了Aop的盘绕告诉,其中JwtUtil是零碎中存储登录用户用的,能够参考[SpringBoot集成Redis]()依据本人的零碎来,没有去掉就OK

OperateLogMapper是mybatis plus生成的保留到数据的,依据本人的业务来,不须要入库,能够间接打印log,疏忽它🙈

参数和返回后果的值,数据库类型是text,长度不能超过65535,这里截取了65000

形容取的Swagger的@ApiOperation注解的值,如果我的项目没有应用Swagger,能够在自定义注解增加一个desc形容😅

package com.maple.demo.config.aop;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.maple.demo.config.annotation.MapleLog;
import com.maple.demo.config.bean.GlobalConfig;
import com.maple.demo.entity.OperateLog;
import com.maple.demo.mapper.OperateLogMapper;
import com.maple.demo.util.JwtUtil;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
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.Date;
import java.util.Objects;


/**
 * @author 笑小枫
 * 配置切面类,@Component 注解把切面类放入Ioc容器中
 */
@Aspect
@Component
@Slf4j
@AllArgsConstructor
public class SystemLogAspect {

    private final OperateLogMapper operateLogMapper;

    @Pointcut(value = "@annotation(com.maple.demo.config.annotation.MapleLog)")
    public void systemLog() {
        // nothing
    }

    @Around(value = "systemLog()")
    public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
        int maxTextLength = 65000;
        Object obj;
        // 定义执行开始工夫
        long startTime;
        // 定义执行完结工夫
        long endTime;
        HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        // 取swagger的形容信息
        ApiOperation apiOperation = method.getAnnotation(ApiOperation.class);
        MapleLog mapleLog = method.getAnnotation(MapleLog.class);
        OperateLog operateLog = new OperateLog();

        try {
            operateLog.setBrowser(request.getHeader("USER-AGENT"));
            operateLog.setOperateUrl(request.getRequestURI());
            operateLog.setRequestMethod(request.getMethod());
            operateLog.setMethod(String.valueOf(joinPoint.getSignature()));
            operateLog.setCreateTime(new Date());
            operateLog.setOperateIp(getIpAddress(request));
            // 取JWT的登录信息,无需登录能够疏忽
            if (request.getHeader(GlobalConfig.TOKEN_NAME) != null) {
                operateLog.setCreateName(JwtUtil.getAccount());
                operateLog.setCreateId(JwtUtil.getUserId());
            }
            String operateParam = JSON.toJSONStringWithDateFormat(joinPoint.getArgs(), "yyyy-MM-dd HH:mm:ss", SerializerFeature.WriteMapNullValue);
            if (operateParam.length() > maxTextLength) {
                operateParam = operateParam.substring(0, maxTextLength);
            }
            operateLog.setOperateParam(operateParam);

            if (apiOperation != null) {
                operateLog.setTitle(apiOperation.value() + "");
            }

            if (mapleLog != null) {
                operateLog.setBusinessType(mapleLog.businessType().ordinal());
                operateLog.setOperateType(mapleLog.operateType().ordinal());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        startTime = System.currentTimeMillis();
        try {
            obj = joinPoint.proceed();
            endTime = System.currentTimeMillis();
            operateLog.setRespTime(endTime - startTime);
            operateLog.setStatus(0);
            // 判断是否保留返回后果,列表页能够设为false
            if (Objects.nonNull(mapleLog) && mapleLog.saveResult()) {
                String result = JSON.toJSONString(obj);
                if (result.length() > maxTextLength) {
                    result = result.substring(0, maxTextLength);
                }
                operateLog.setJsonResult(result);
            }
        } catch (Exception e) {
            // 记录异样信息
            operateLog.setStatus(1);
            operateLog.setErrorMsg(e.toString());
            throw e;
        } finally {
            endTime = System.currentTimeMillis();
            operateLog.setRespTime(endTime - startTime);
            operateLogMapper.insert(operateLog);
        }
        return obj;
    }

    /**
     * 获取Ip地址
     */
    private static String getIpAddress(HttpServletRequest request) {
        String xip = request.getHeader("X-Real-IP");
        String xFor = request.getHeader("X-Forwarded-For");
        String unknown = "unknown";
        if (StringUtils.isNotEmpty(xFor) && !unknown.equalsIgnoreCase(xFor)) {
            //屡次反向代理后会有多个ip值,第一个ip才是实在ip
            int index = xFor.indexOf(",");
            if (index != -1) {
                return xFor.substring(0, index);
            } else {
                return xFor;
            }
        }
        xFor = xip;
        if (StringUtils.isNotEmpty(xFor) && !unknown.equalsIgnoreCase(xFor)) {
            return xFor;
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {
            xFor = request.getHeader("Proxy-Client-IP");
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {
            xFor = request.getHeader("WL-Proxy-Client-IP");
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {
            xFor = request.getHeader("HTTP_CLIENT_IP");
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {
            xFor = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (StringUtils.isBlank(xFor) || unknown.equalsIgnoreCase(xFor)) {
            xFor = request.getRemoteAddr();
        }
        return xFor;
    }
}

就这样,简略吧,拿去用吧

写个测试类吧

package com.maple.demo.controller;

import com.maple.demo.config.annotation.MapleLog;
import com.maple.demo.config.bean.ErrorCode;
import com.maple.demo.config.enums.BusinessTypeEnum;
import com.maple.demo.config.enums.OperateTypeEnum;
import com.maple.demo.config.exception.MapleCommonException;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.Data;
import org.springframework.web.bind.annotation.*;

/**
 * @author 笑小枫
 * @date 2022/7/21
 */
@RestController
@RequestMapping("/example")
@Api(tags = "实例演示-日志记录演示接口")
public class TestSystemLogController {

    @ApiOperation(value = "测试带参数、有返回后果的get申请")
    @GetMapping("/testGetLog/{id}")
    @MapleLog(businessType = BusinessTypeEnum.OTHER, operateType = OperateTypeEnum.OTHER)
    public Test testGetLog(@PathVariable Integer id) {
        Test test = new Test();
        test.setName("笑小枫");
        test.setAge(18);
        test.setRemark("大家好,我是笑小枫,喜爱我的小伙伴点个赞呗");
        return test;
    }

    @ApiOperation(value = "测试json参数、抛出异样的post申请")
    @PostMapping("/testPostLog")
    @MapleLog(businessType = BusinessTypeEnum.OTHER, operateType = OperateTypeEnum.OTHER, saveResult = false)
    public Test testPostLog(@RequestBody Test param) {
        Test test = new Test();
        test.setName("笑小枫");
        if (test.getAge() == null) {
            // 这里应用了自定义异样,测试能够间接抛出RuntimeException
            throw new MapleCommonException(ErrorCode.COMMON_ERROR);
        }
        test.setRemark("大家好,我是笑小枫,喜爱我的小伙伴点个赞呗");
        return test;
    }

    @Data
    static class Test {
        private String name;

        private Integer age;

        private String remark;
    }
}

浏览器申请http://localhost:6666/example/testGetLog/1

再模仿一下post异样申请吧:POST http://localhost:6666/example/testPostLog

看一下数据落库的后果吧,emmm… operate_location没采集,疏忽吧🤣

对于笑小枫💕

本章到这里完结了,喜爱的敌人关注一下我呦😘😘,大伙的反对,就是我保持写下去的能源。
老规矩,懂了就点赞珍藏;不懂就问,日常在线,我会就会回复哈~🤪
微信公众号:笑小枫
笑小枫集体博客:https://www.xiaoxiaofeng.com
CSDN:https://zhangfz.blog.csdn.net
本文源码:https://github.com/hack-feng/maple-demo

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理