SpringBoot通过自定义注解与异步来管理日志流程

Java
234
0
0
2023-09-08
标签   SpringBoot
目录
  • 一、前言
  • 二、基础环境
  • 1. 导入依赖
  • 2. 编写yml配置
  • 三、数据库设计
  • 四、主要功能
  • 1. 编写注解
  • 2. 业务类型枚举
  • 3. 编写切片
  • 4. ip工具类
  • 5. 事件发布
  • 6. 监听者
  • 五、测试
  • 1. controller
  • 2. service
  • 3. dao
  • 4. 测试
  • 5. 数据库
  • 六、总结

一、前言

我们在企业级的开发中,必不可少的是对日志的记录,实现有很多种方式,常见的就是基于AOP+注解进行保存,同时考虑到程序的流畅和效率,我们可以使用异步进行保存!

二、基础环境

1. 导入依赖

我这里的springboot版本是:2.7.4

<?xml version=".0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>.7.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springboot-log</artifactId>
    <version>.0.1-SNAPSHOT</version>
    <name>springboot-log</name>
    <description>springboot-log 日志 Demo</description>
    <properties>
        <java.version>.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>.18.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- Druid -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>.1.16</version>
        </dependency>
        <!--jdbc-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!-- mybatis-plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>.5.1</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 编写yml配置

server:
 port: 8080
spring:
 datasource:
  driver-class-name: com.mysql.cj.jdbc.Driver
  url: jdbc:mysql://localhost:3306/admin?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
  username: root
  password: 123456
  druid:
   initial-size: 5
   max-active: 100
   min-idle: 5
   max-wait: 60000
   time-between-eviction-runs-millis: 60000
   min-evictable-idle-time-millis: 30000
   validation-query: SELECT 1 FROM DUAL
   test-while-idle: true
   test-on-borrow: true
   test-on-return: false

三、数据库设计

数据库保存日志表的设计,这里一切从简,一般日志多的后期会进行分库分表,或者搭配ELK进行分析,分库分表一般采用根据方法类型!

DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log`  (
  `id` bigint() NOT NULL AUTO_INCREMENT COMMENT '日志主键',
  `title` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '模块标题',
  `business_type` int() NULL DEFAULT 0 COMMENT '业务类型(0其它 1新增 2修改 3删除)',
  `method` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '方法名称',
  `request_method` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求方式',
  `oper_name` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '操作人员',
  `oper_url` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '请求URL',
  `oper_ip` varchar() CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT '' COMMENT '主机地址',
  `oper_time` datetime() NULL DEFAULT NULL COMMENT '操作时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '操作日志记录' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS =;

实体类:

package com.example.springbootlog.entity;
import java.util.Date;
import java.io.Serializable;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.ToString;
/**
 * 操作日志记录(SysLog)实体类
 *
 * @author qrxm
 * @since-03-26 02:09:54
 */
@Data
@ToString
@TableName("sys_log")
public class SysLog implements Serializable {
    private static final long serialVersionUID =L;
    /**
    * 日志主键
    */
    @TableId
    private Long id;
    /**
    * 模块标题
    */
    private String title;
    /**
    * 业务类型(其它 1新增 2修改 3删除)
    */
    private Integer businessType;
    /**
    * 方法名称
    */
    private String method;
    /**
    * 请求方式
    */
    private String requestMethod;
    /**
    * 操作人员
    */
    private String operName;
    /**
    * 请求URL
    */
    private String operUrl;
    /**
    * 主机地址
    */
    private String operIp;
    /**
    * 操作时间
    */
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date operTime;
}

四、主要功能

大体思路:

  • 先手写一个注解
  • 切面来进行获取要保存的数据
  • 一个发布者来发布要保存的数据
  • 一个监听者监听后保存(异步)

完整项目架构图如下:

1. 编写注解

package com.example.springbootlog.annotation;
import com.example.springbootlog.constant.BusinessTypeEnum;
import java.lang.annotation.*;
/**
 * 自定义操作日志记录注解
 * @author qrxm
 */
@Target(ElementType.METHOD) // 注解只能用于方法
@Retention(RetentionPolicy.RUNTIME) // 修饰注解的生命周期
@Documented
public @interface Log {
    String value() default "";
    /**
     * 模块
     */
    String title() default "测试模块";
    /**
     * 方法名称
     */
    String method() default "测试方法";
    /**
     * 功能
     */
    BusinessTypeEnum businessType() default BusinessTypeEnum.OTHER;
}

2. 业务类型枚举

package com.example.springbootlog.constant;
public enum BusinessTypeEnum {
    /**
     * 其它
     */
    OTHER(,"其它"),
    /**
     * 新增
     */
    INSERT(,"新增"),
    /**
     * 修改
     */
    UPDATE(,"修改"),
    /**
     * 删除
     */
    DELETE(,"删除");
    private Integer code;
    private String message;
    BusinessTypeEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
    public Integer getCode() {
        return code;
    }
    public String getMessage() {
        return message;
    }
}

3. 编写切片

这里是以切片后进行发起的,当然规范流程是要加异常后的切片,这里以最简单的进行测试哈,大家按需进行添加!!

package com.example.springbootlog.aspect;
import com.example.springbootlog.annotation.Log;
import com.example.springbootlog.entity.SysLog;
import com.example.springbootlog.listener.EventPubListener;
import com.example.springbootlog.utils.IpUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slfj.Logger;
import org.slfj.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
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.util.Date;
@Aspect
@Component
public class SysLogAspect {
    private final Logger logger = LoggerFactory.getLogger(SysLogAspect.class);
    @Autowired
    private EventPubListener eventPubListener;
    /**
     * 以注解所标注的方法作为切入点
     */
    @Pointcut("@annotation(com.example.springbootlog.annotation.Log)")
    public void sysLog() {}
    /**
     * 在切点之后织入
     * @throws Throwable
     */
    @After("sysLog()")
    public void doAfter(JoinPoint joinPoint) {
        Log log = ((MethodSignature) joinPoint.getSignature()).getMethod()
                .getAnnotation(Log.class);
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String method = request.getMethod();
        String url = request.getRequestURL().toString();
        String ip = IpUtils.getIpAddr(request);
        SysLog sysLog = new SysLog();
        sysLog.setBusinessType(log.businessType().getCode());
        sysLog.setTitle(log.title());
        sysLog.setMethod(log.method());
        sysLog.setRequestMethod(method);
        sysLog.setOperIp(ip);
        sysLog.setOperUrl(url);
        // 从登录中token获取登录人员信息即可
        sysLog.setOperName("我是测试人员");
        sysLog.setOperTime(new Date());
        // 发布消息
        eventPubListener.pushListener(sysLog);
        logger.info("=======日志发送成功,内容:{}",sysLog);
    }
}

4. ip工具类

package com.example.springbootlog.utils;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import javax.servlet.http.HttpServletRequest;
public class IpUtils {
    /**
     * 获取客户端IP
     *
     * @param request 请求对象
     * @return IP地址
     */
    public static String getIpAddr(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        String ip = request.getHeader("x-forwarded-for");
        if (ip == null || ip.length() == || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Forwarded-For");
        }
        if (ip == null || ip.length() == || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.length() == || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ":0:0:0:0:0:0:1".equals(ip) ? "127.0.0.1" : getMultistageReverseProxyIp(ip);
    }
    /**
     * 从多级反向代理中获得第一个非unknown IP地址
     *
     * @param ip 获得的IP地址
     * @return 第一个非unknown IP地址
     */
    public static String getMultistageReverseProxyIp(String ip) {
        // 多级反向代理检测
        if (ip != null && ip.indexOf(",") >) {
            final String[] ips = ip.trim().split(",");
            for (String subIp : ips) {
                if (false == isUnknown(subIp)) {
                    ip = subIp;
                    break;
                }
            }
        }
        return ip;
    }
    /**
     * 检测给定字符串是否为未知,多用于检测HTTP请求相关
     *
     * @param checkString 被检测的字符串
     * @return 是否未知
     */
    public static boolean isUnknown(String checkString) {
        return StringUtils.isBlank(checkString) || "unknown".equalsIgnoreCase(checkString);
    }
}

5. 事件发布

事件发布是由ApplicationContext对象进行发布的,直接注入使用即可!

使用观察者模式的目的:为了业务逻辑之间的解耦,提高可扩展性。

这种模式在spring和springboot底层是经常出现的,大家可以去看看。

发布者只需要关注发布消息,监听者只需要监听自己需要的,不管谁发的,符合自己监听条件即可。

package com.example.springbootlog.listener;
import com.example.springbootlog.entity.SysLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
@Component
public class EventPubListener {
    @Autowired
    private ApplicationContext applicationContext;
    /**
     * 事件发布方法
     * @param sysLogEvent
     */
    public void pushListener(SysLog sysLogEvent) {
        applicationContext.publishEvent(sysLogEvent);
    }
}

6. 监听者

@Async:单独开启一个新线程去保存,提高效率!

@EventListener:监听

package com.example.springbootlog.listener;
import com.example.springbootlog.entity.SysLog;
import com.example.springbootlog.service.SysLogService;
import lombok.extern.slfj.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
@Slfj
@Component
public class MyEventListener {
    @Autowired
    private SysLogService sysLogService;
    // 开启异步
    @Async
    // 开启监听
    @EventListener(SysLog.class)
    public void saveSysLog(SysLog event) {
        log.info("=====即将异步保存到数据库======");
        sysLogService.saveLog(event);
    }
}

五、测试

1. controller

package com.example.springbootlog.controller;
import com.example.springbootlog.annotation.Log;
import com.example.springbootlog.constant.BusinessTypeEnum;
import lombok.extern.slfj.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
 * 操作日志记录(SysLog)表控制层
 *
 */
@Slfj
@RestController
@RequestMapping("sysLog")
public class SysLogController {
    @Log(method = "测试添加方法", title = "测试呢", businessType = BusinessTypeEnum.INSERT)
    @GetMapping("/saveLog")
    public void saveLog() {
        log.info("我就是来测试一下是否成功!");
    }
}

2. service

package com.example.springbootlog.service;
import com.example.springbootlog.entity.SysLog;
import com.baomidou.mybatisplus.extension.service.IService;
/**
 * 操作日志记录(SysLog)表服务接口
 */
public interface SysLogService  extends IService<SysLog> {
    Integer saveLog(SysLog sysLog);
}
package com.example.springbootlog.service.impl;
import com.example.springbootlog.entity.SysLog;
import com.example.springbootlog.service.SysLogService;
import com.example.springbootlog.dao.SysLogDao;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
 * 操作日志记录(SysLog)表服务实现类
 */
@Service("sysLogService")
public class SysLogServiceImpl  extends ServiceImpl<SysLogDao, SysLog>  implements SysLogService {
    @Autowired
    private SysLogDao sysLogDao;
    @Override
    public Integer saveLog(SysLog sysLog) {
        return sysLogDao.insert(sysLog);
    }
}

3. dao

这里使用mybatis-plus进行保存

package com.example.springbootlog.dao;
import com.example.springbootlog.entity.SysLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
/**
 * 操作日志记录(SysLog)表数据库访问层
 */
public interface SysLogDao extends BaseMapper<SysLog> {
}

4. 测试

访问地址:http://localhost:8080/sysLog/saveLog

5. 数据库

六、总结

这个实战在企业级必不可少的,每个项目搭建人不同,但是结果都是一样的,保存日志到数据,这样可以进行按钮的点击进行统计,分析那个功能是否经常使用,那些东西需要优化。只要是有数据的东西,分析一下总会有收获的!后面日志多了就行分库分表,ELK搭建。

代码已上传到 Gitee 上面了,地址:https://gitee.com/afoams/springboot-log