前面一篇文章简单介绍了常见的自定义注解:spring boot自定义注解(0)—常见类型

这篇文章介绍一下spring boot如何通过自定义注解实现记录操作日志过程。

0.准备工作

首先创建一个srping boot项目,如果不会可以参考这篇文章:Spring Boot(1)—创建并运行项目

需要引入AOP依赖和fastjson依赖

  <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.7</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>site.longkui</groupId>
    <artifactId>app</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>app</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- mysql  -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.34</version>
        </dependency>
        <!--mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <!-- aop -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjtools</artifactId>
            <version>1.8.9</version>
        </dependency>
        <!-- 引入 fastjson 依赖 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.79</version>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

编写sql,参考如下

DROP TABLE IF EXISTS `log_record`;
CREATE TABLE `log_record`  (
  `id` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'id主键',
  `module` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '所属模块',
  `describe` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '操作描述内容',
  `path` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求路径',
  `method` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '请求方法',
  `IP` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'IP地址',
  `qualifiedName` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求名',
  `inputParam` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请求参数',
  `outputParam` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '请出参数',
  `errorMsg` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '异常信息',
  `requestTime` datetime NULL DEFAULT NULL COMMENT '请求开始时间',
  `responseTime` datetime NULL DEFAULT NULL COMMENT '请求响应时间',
  `costTime` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '接口耗时',
  `status` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '请求是否成功',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

1.创建实体类
package site.longkui.app.entity.logrecord;

import java.io.Serializable;
import java.util.Date;

public class LogRecordEntity implements Serializable {
    //主键id
    private String id;
    //所属模块
    private String module;
    //操作内容描述
    private String describe;
    //请求路径
    private String path;
    //请求方法
    private String method;
    //IP地址
    private String IP;
    //请求名
    private String qualifiedName;
    //请求参数
    private String inputParam;
    //请出参数
    private String outputParam;
    //异常信息
    private String errorMsg;
    //请求开始时间
    private Date requestTime;
    // 请求响应时间
    private Date responseTime;
    // 接口耗时,单位:ms
    private String costTime;
    // 请求是否成功
    private String status;

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getModule() {
        return module;
    }
    public void setModule(String module) {
        this.module = module;
    }
    public String getDescribe() {
        return describe;
    }
    public void setDescribe(String describe) {
        this.describe = describe;
    }
    public String getPath() {
        return path;
    }
    public void setPath(String path) {
        this.path = path;
    }
    public String getMethod() {
        return method;
    }
    public void setMethod(String method) {
        this.method = method;
    }
    public String getIP() {
        return IP;
    }
    public void setIP(String IP) {
        this.IP = IP;
    }
    public String getQualifiedName() {
        return qualifiedName;
    }
    public void setQualifiedName(String qualifiedName) {
        this.qualifiedName = qualifiedName;
    }
    public String getInputParam() {
        return inputParam;
    }
    public void setInputParam(String inputParam) {
        this.inputParam = inputParam;
    }
    public String getOutputParam() {
        return outputParam;
    }
    public void setOutputParam(String outputParam) {
        this.outputParam = outputParam;
    }
    public String getErrorMsg() {
        return errorMsg;
    }
    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
    public Date getRequestTime() {
        return requestTime;
    }
    public void setRequestTime(Date requestTime) {
        this.requestTime = requestTime;
    }
    public Date getResponseTime() {
        return responseTime;
    }
    public void setResponseTime(String String) {
        this.responseTime = responseTime;
    }
    public String getCostTime() {
        return costTime;
    }
    public void setCostTime(String costTime) {
        this.costTime = costTime;
    }
    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
    @Override
    public String toString() {
        return "LogRecordEntity{" +
                "id='" + id + '\'' +
                ", module='" + module + '\'' +
                ", describe='" + describe + '\'' +
                ", path='" + path + '\'' +
                ", method='" + method + '\'' +
                ", IP='" + IP + '\'' +
                ", qualifiedName='" + qualifiedName + '\'' +
                ", inputParam='" + inputParam + '\'' +
                ", outputParam='" + outputParam + '\'' +
                ", errorMsg='" + errorMsg + '\'' +
                ", requestTime='" + requestTime + '\'' +
                ", responseTime='" + responseTime + '\'' +
                ", costTime='" + costTime + '\'' +
                ", status='" + status + '\'' +
                '}';
    }
}
2.定义自定义注解
package site.longkui.app.annotate;

import java.lang.annotation.*;

@Target( {ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface OperateLog {
    //操作描述
    String describe() default "";
    //操作模块
    String module() default "";

}

这个地方我们自定义了一个注解,这个注解使用在方法上面,用于标记AOP切面会拦截这个方法,并且记录请求日志信息。

3.定义AOP切面
package site.longkui.app.aop;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
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 site.longkui.app.annotate.OperateLog;
import site.longkui.app.entity.logrecord.LogRecordEntity;
import site.longkui.app.mapper.LogRecordMapper;

import javax.servlet.http.HttpServletRequest;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Date;


// 标记当前类是一个切面类
@Aspect
// 将当前类放入IOC容器
@Component
public class LogRecordAspect {


    @Autowired
    LogRecordMapper logRecordMapper;

    /**
     * 创建线程局部变量
     */
    private ThreadLocal<LogRecordEntity> threadLocal = new ThreadLocal<>();

    /**
     * 定义切入点,这里我们使用AOP切入自定义【@OperateLog】注解的方法
     */
    @Pointcut("@annotation(site.longkui.app.annotate.OperateLog)")
    public void methodPointCut() {
    }

    /**
     * 前置通知,【执行Controller方法之前】执行该通知方法
     */
    @Before("methodPointCut()")
    public void beforeAdvice() {
        System.out.println("前置通知......");
    }

    /**
     * 后置通知,【Controller方法执行完成,返回方法的返回值之前】执行该通知方法
     */
    @After("methodPointCut()")
    public void afterAdvice() {
        System.out.println("后置通知......");
    }

    /**
     * 环绕通知,执行Controller方法的前后执行
     *
     * @param point 连接点
     */
    @Around("methodPointCut()")
    public Object Around(ProceedingJoinPoint point) throws Throwable {
        System.out.println("环绕通知之前.....");
        // 获取当前请求对象
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                .getRequestAttributes()).getRequest();
        if (request == null) {
            return null;
        }
        //获取当前请求相关信息
        LogRecordEntity logRecordEntity = new LogRecordEntity();  //实体类
        logRecordEntity.setPath(request.getRequestURI());      //获取请求地址
        logRecordEntity.setMethod(request.getMethod());        //获取请求方式
//        logRecordEntity.setId(request.getRemoteHost());      //弃用,改用自定义方法
        logRecordEntity.setIP(getIRealIPAddr(request));        //获取ip
        logRecordEntity.setRequestTime(new Date(System.currentTimeMillis())); //获取系统时间作为请求时间
        // 反射获取调用方法
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = signature.getMethod();
        if (method.isAnnotationPresent(OperateLog.class)) {
            // 获取注解信息
            OperateLog annotation = method.getAnnotation(OperateLog.class);
            logRecordEntity.setModule(annotation.module());       //注解信息中的 module的内容
            logRecordEntity.setDescribe(annotation.describe());   //注解信息中的 describe 的内容
        }
        //获取全限定类名称
        String name = method.getName();
        logRecordEntity.setQualifiedName(name);
        // 获取请求参数
        String inputParam = JSONObject.toJSONString(point.getArgs());
        logRecordEntity.setInputParam(inputParam);


        // 设置局部变量
        threadLocal.set(logRecordEntity);
        //此处打印获取的具体内容
        System.out.println(logRecordEntity.toString());
        //也可以调用既定方法往数据库里写入数据
        //logRecordMapper.insertLogRecord(logRecordEntity);
        Object ret = point.proceed();
        return ret;
    }

    /**
     * 返回值通知,Controller执行完成之后,返回方法的返回值时候执行
     *
     * @param ret 返回值的名称
     */
    @AfterReturning(pointcut = "methodPointCut()", returning = "ret")
    public Object afterReturning(Object ret) {
        System.out.println("返回值通知......ret=" + ret);
        // 获取日志实体对象
        LogRecordEntity entity = this.getEntity();
        String outputParam = JSON.toJSONString(ret);
        entity.setOutputParam(outputParam); // 保存响应参数
        entity.setStatus("成功"); // 设置成功标识
        //保存到数据库中,在这里调用可以把返回值一起调用
        threadLocal.remove();
        System.out.println(entity);
        try {
            logRecordMapper.insertLogRecord(entity);
        } catch (Exception e) {
            e.toString();
        }
        return ret;
    }

    /**
     * 异常通知,当Controller方法执行过程中出现异常时候,执行该通知
     *
     * @param ex 异常名称
     */
    @AfterThrowing(pointcut = "methodPointCut()", throwing = "ex")
    public void throwingAdvice(Throwable ex) {
        System.out.println("异常通知......");
        // 获取日志实体对象
        LogRecordEntity entity = this.getEntity();
        StringWriter errorMsg = new StringWriter();
        ex.printStackTrace(new PrintWriter(errorMsg, true));
        entity.setErrorMsg(errorMsg.toString()); // 保存响应参数
        entity.setStatus("error"); // 设置成功标识
        //可以插入到数据库中
        threadLocal.remove();
        System.out.println(entity);
    }

    //获取真实IP
    private String getIRealIPAddr(HttpServletRequest request) {
        String ipAddress;
        ipAddress = request.getHeader("x-forwarded-for");
        if (ipAddress == null || ipAddress.length() == 0
                || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0
                || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0
                || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if (ipAddress.equals("127.0.0.1")
                    || ipAddress.equals("0:0:0:0:0:0:0:1")) {
                // 根据网卡取本机配置的IP
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
                ipAddress = inet.getHostAddress();
            }

        }

        // 对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.length() > 15) { // "***.***.***.***".length()
            // = 15
            if (ipAddress.indexOf(",") > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
            }
        }

        return ipAddress;
    }

    private LogRecordEntity getEntity() {
        // 获取局部变量
        LogRecordEntity entity = threadLocal.get();
        long start = entity.getRequestTime().getTime();
        long end = System.currentTimeMillis();
        // 获取响应时间、耗时
        entity.setCostTime((end - start) + "ms");
        entity.setResponseTime(String.valueOf(end));
        return entity;
    }

}

这里我们写了切面,并且通过自定义方法logRecordMapper.insertLogRecord往数据库里写入数据。

4.编写测试类并进行测试
    //根据id查询一个学生
    @GetMapping("/getStudentById/{id}")
    @OperateLog(describe = "根据id查询一个学生",module = "学生模块")
    public JSONObject getStudentById(@PathVariable("id") String id){
        try {
            JSONObject jsonObject=studentsService.getStudentById(id);
            return jsonObject;
        }catch (Exception e){
            e.toString();
            logger.error(e.toString());
            return null;
        }
    }

访问接口:localhost:8082/api/students/getList/1003

查看数据库的情况:

可以看到已经成功插入到数据库中了