• 【笑小枫的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='系统管理-操作日志记录';
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    使用的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;
    
    /**
     * 

    * 系统管理-操作日志记录 *

    * * @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; }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    操作类别OperateTypeEnum枚举类:

    package com.maple.demo.config.enums;
    
    
    /**
     * @author 笑小枫
     * @date 2022/6/27
     */
    public enum OperateTypeEnum {
        // 0网站用户 1后台用户 2小程序 3其他
        BLOG,
        ADMIN,
        APP,
        OTHER
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    定义切点的注解

    定义一个自定义注解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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156

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

    写个测试类吧

    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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

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

    image-20220627172120224

    再模拟一下post异常请求吧:POST http://localhost:6666/example/testPostLog

    image-20220627172936529

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

    image-20220627173315916

    关于笑小枫💕

    本章到这里结束了,喜欢的朋友关注一下我呦😘😘,大伙的支持,就是我坚持写下去的动力。
    老规矩,懂了就点赞收藏;不懂就问,日常在线,我会就会回复哈~🤪
    笑小枫个人博客:https://www.xiaoxiaofeng.com
    本文源码:https://github.com/hack-feng/maple-demo

    本系列其它文章

    本系列的源码已同步在Github:https://github.com/hack-feng/maple-demo

    1. SpringBoot项目创建

    2. SpringBoot配置基于swagger2的knife4j接口文档

    3. SpringBoot集成Mybatis Plus

    4. SpringBoot返回统一结果包装

    5. SpringBoot返回统一异常处理

    6. SpringBoot日志打印Logback详解

    7. SpringBoot控制台自定义banner

    8. SpringBoot集成Redis

    9. SpringBoot用户登录拦截器

    10. SpringBoot处理请求跨域问题

    11. SpringBoot接口日志信息统一记录

    12. SpringBoot导入Excel

    13. SpringBoot导出Excel

    14. SpringBoot发送邮件

    15. SpringBoot根据模板生成Word

    16. SpringBoot生成PDF

    17. SpringBoot文件上传下载

    18. SpringBoot中的Properties配置

  • 相关阅读:
    免费下载word简历模板的网站
    算法与数据结构 --- 队列的表示和操作的实现
    一些动态规划dp简单基础题
    基于探针的分布式追踪工具
    社会工程攻击依然是企业面临的最大威胁
    level2行情接口中的TickRecord有什么作用?
    CSS3 媒体查询
    Vue-2.1scoped样式冲突
    MySQL之存储引擎
    Cesium-动态绘制面
  • 原文地址:https://blog.csdn.net/qq_34988304/article/details/127777792