【笑小枫的SpringBoot系列】【十一】SpringBoot接口日志信息统一记录

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【笑小枫的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


40f87244091029fe08311b77b70cff40.png


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


e299218c1496164078715d4068b8758f.png


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


25f5692bfb19c72798c69e998e0c6436.png

关于笑小枫💕


本章到这里结束了,喜欢的朋友关注一下我呦😘😘,大伙的支持,就是我坚持写下去的动力。

老规矩,懂了就点赞收藏;不懂就问,日常在线,我会就会回复哈~🤪

笑小枫个人博客:https://www.xiaoxiaofeng.com

本文源码:https://github.com/hack-feng/maple-demo


相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
目录
打赏
0
0
0
0
27
分享
相关文章
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— logback.xml 配置文件解析
本文解析了 `logback.xml` 配置文件的详细内容,包括日志输出格式、存储路径、控制台输出及日志级别等关键配置。通过定义 `LOG_PATTERN` 和 `FILE_PATH`,设置日志格式与存储路径;利用 `&lt;appender&gt;` 节点配置控制台和文件输出,支持日志滚动策略(如文件大小限制和保存时长);最后通过 `&lt;logger&gt;` 和 `&lt;root&gt;` 定义日志级别与输出方式。此配置适用于精细化管理日志输出,满足不同场景需求。
355 1
告别传统Log追踪!GOAT如何用HTTP接口重塑代码监控
本文介绍了GOAT(Golang Application Tracing)工具的使用方法,通过一个Echo问答服务实例,详细展示了代码埋点与追踪技术的应用。内容涵盖初始化配置、自动埋点、手动调整埋点、数据监控及清理埋点等核心功能。GOAT适用于灰度发布、功能验证、性能分析、Bug排查和代码重构等场景,助力Go项目质量保障与平稳发布。工具以轻量高效的特点,为开发团队提供数据支持,优化决策流程。
301 89
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
本课主要讲解Spring Boot项目中的属性配置方法。在实际开发中,测试与生产环境的配置往往不同,因此不应将配置信息硬编码在代码中,而应使用配置文件管理,如`application.yml`。例如,在微服务架构下,可通过配置文件设置调用其他服务的地址(如订单服务端口8002),并利用`@Value`注解在代码中读取这些配置值。这种方式使项目更灵活,便于后续修改和维护。
66 0
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录——使用Logger在项目中打印日志
本文介绍了如何在项目中使用Logger打印日志。通过SLF4J和Logback,可设置不同日志级别(如DEBUG、INFO、WARN、ERROR)并支持占位符输出动态信息。示例代码展示了日志在控制器中的应用,说明了日志配置对问题排查的重要性。附课程源码下载链接供实践参考。
188 0
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— application.yml 中对日志的配置
在 Spring Boot 项目中,`application.yml` 文件用于配置日志。通过 `logging.config` 指定日志配置文件(如 `logback.xml`),实现日志详细设置。`logging.level` 可定义包的日志输出级别,例如将 `com.itcodai.course03.dao` 包设为 `trace` 级别,便于开发时查看 SQL 操作。日志级别从高到低为 ERROR、WARN、INFO、DEBUG,生产环境建议调整为较高级别以减少日志量。本课程采用 yml 格式,因其层次清晰,但需注意格式要求。
245 0
|
4月前
|
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录——slf4j 介绍
在软件开发中,`System.out.println()`常被用于打印信息,但大量使用会增加资源消耗。实际项目推荐使用slf4j结合logback输出日志,效率更高。Slf4j(Simple Logging Facade for Java)是一个日志门面,允许开发者通过统一方式记录日志,无需关心具体日志系统。它支持灵活切换日志实现(如log4j或logback),且具备简洁占位符和日志级别判断等优势。阿里巴巴《Java开发手册》强制要求使用slf4j,以保证日志处理方式的统一性和维护性。使用时只需通过`LoggerFactory`创建日志实例即可。
112 0
【Azure App Service】分享使用Python Code获取App Service的服务器日志记录管理配置信息
本文介绍了如何通过Python代码获取App Service中“Web服务器日志记录”的配置状态。借助`azure-mgmt-web` SDK,可通过初始化`WebSiteManagementClient`对象、调用`get_configuration`方法来查看`http_logging_enabled`的值,从而判断日志记录是否启用及存储方式(关闭、存储或文件系统)。示例代码详细展示了实现步骤,并附有执行结果与官方文档参考链接,帮助开发者快速定位和解决问题。
120 24
【YashanDB知识库】YashanDB run.log中有slow log queue is full信息
【YashanDB知识库】YashanDB run.log中有slow log queue is full信息
微服务——SpringBoot使用归纳——Spring Boot中的项目属性配置——少量配置信息的情形
在微服务架构中,随着业务复杂度增加,项目可能需要调用多个微服务。为避免使用`@Value`注解逐一引入配置的繁琐,可通过定义配置类(如`MicroServiceUrl`)并结合`@ConfigurationProperties`注解实现批量管理。此方法需在配置文件中设置微服务地址(如订单、用户、购物车服务),并通过`@Component`将配置类纳入Spring容器。最后,在Controller中通过`@Resource`注入配置类即可便捷使用,提升代码可维护性。
64 0
制造业ERP源码,工厂ERP管理系统,前端框架:Vue,后端框架:SpringBoot
这是一套基于SpringBoot+Vue技术栈开发的ERP企业管理系统,采用Java语言与vscode工具。系统涵盖采购/销售、出入库、生产、品质管理等功能,整合客户与供应商数据,支持在线协同和业务全流程管控。同时提供主数据管理、权限控制、工作流审批、报表自定义及打印、在线报表开发和自定义表单功能,助力企业实现高效自动化管理,并通过UniAPP实现移动端支持,满足多场景应用需求。
228 1

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等