SpringBoot使用AOP统一处理日志

  • 应用(记录用户操作日志): 有时候我们需要处理一些请求日志,或者对某些方法进行一些监控,如果出现例外情况应该进行怎么样的处理,现在,我们从spring boot中引入AOP

1、开发准备

环境:idea、jdk 1.8、springboot、mysql

1.1 目录结构

└─src
    └─main
        ├─java
        │  └─com
        │      └─example
        │          └─log
        │              │  LogApplication.java
        │              ├─annotation
        │              │      Log.java
        │              ├─aspect
        │              │      LogAspect.java
        │              ├─common
        │              │  ├─context
        │              │  │      BaseContext.java
        │              │  │      CallBack.java
        │              │  │      SpringContextHolder.java
        │              │  ├─enums
        │              │  │      Action.java
        │              │  └─utils
        │              │          CloseUtil.java
        │              │          ExceptionUtil.java
        │              │          FileUtil.java
        │              │          ServletUtil.java
        │              ├─controller
        │              │      LogController.java
        │              ├─domain
        │              │      SysLog.java
        │              ├─repository
        │              │      LogRepository.java
        │              └─service
        │                  │  LogService.java
        │                  └─impl
        │                          LogServiceImpl.java
        └─resources
            │  application.yml
            └─ip2region
                    ip2region.db (注意这个文件,作用:转换IP地址来源)
resources/ip2region/ip2region.db 文件 下载:https://files.cnblogs.com/files/mmdz/ip2region.db.rar
 

1.2 日志表
准备sys_log日志表(mysql)

DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log` (
  `id` char(20) NOT NULL COMMENT '编号',
  `operator` varchar(255) DEFAULT NULL COMMENT '操作人',
  `operation_time` datetime DEFAULT NULL COMMENT '操作时间',
  `title` varchar(255) DEFAULT NULL COMMENT '编号',
  `method` varchar(255) DEFAULT NULL COMMENT '方法',
  `type` varchar(255) DEFAULT NULL COMMENT '请求方式',
  `params` varchar(255) DEFAULT NULL COMMENT '参数',
  `state` bit(1) DEFAULT NULL COMMENT '状态',
  `action` varchar(255) DEFAULT NULL COMMENT '操作',
  `request_ip` varchar(255) DEFAULT NULL COMMENT '请求ip',
  `address` varchar(255) DEFAULT NULL COMMENT 'ip来源',
  `browser` varchar(255) DEFAULT NULL COMMENT '浏览器',
  `time` bigint(10) DEFAULT NULL COMMENT '请求耗时',
  `error` text COMMENT '异常信息',
  `system` varchar(255) DEFAULT NULL COMMENT '操作系统',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

1.3 pom文件

<?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.5.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>log</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>log</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
        <datasource.version>3.3.1</datasource.version>
        <mysql.version>8.0.22</mysql.version>
        <mybatis.plus.version>3.4.3</mybatis.plus.version>
        <swagger.version>2.9.2</swagger.version>
        <hutool.version>5.5.7</hutool.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-aop</artifactId>
        </dependency>
        <!-- 数 据 库 操 作 框 架 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis.plus.version}</version>
        </dependency>
        <!-- 数 据 库 连 接 工 具 -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.version}</version>
            <scope>runtime</scope>
        </dependency>
        <!-- 常 用 工 具 类 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>${hutool.version}</version>
        </dependency>
        <!-- Swagger UI 相关 -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger.version}</version>
            <exclusions>
                <exclusion>
                    <groupId>io.swagger</groupId>
                    <artifactId>swagger-annotations</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.swagger</groupId>
                    <artifactId>swagger-models</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>1.5.21</version>
        </dependency>
        <dependency>
            <groupId>io.swagger</groupId>
            <artifactId>swagger-models</artifactId>
            <version>1.5.21</version>
        </dependency>

        <!-- 解析客户端操作系统、浏览器信息 -->
        <dependency>
            <groupId>nl.basjes.parse.useragent</groupId>
            <artifactId>yauaa</artifactId>
            <version>5.23</version>
        </dependency>
        <!-- 根据IP获取城市-Java调用“ip2region” -->
        <dependency>
            <groupId>org.lionsoul</groupId>
            <artifactId>ip2region</artifactId>
            <version>1.7.2</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

1.4 application.yml

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/log?useUnicode=true&characterEncoding=utf8&useSSL=true&serverTimezone=GMT
    username: root
    password: 123456

2、编码
2.1 domain包
SysLog(日志模型)


package com.example.log.domain;

import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.example.log.common.enums.Action;
import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/**
 * 日志模型
 * */
@Data
@TableName("sys_log")
public class SysLog implements Serializable {
    /** 编号 */
    @TableId("id")
    private String id;
    /** 操作人 */
    @TableField("operator")
    private String operator;
    /** 操作时间 */
    @TableField("operation_time")
    private LocalDateTime operationTime;
    /** 标题 */
    @TableField("title")
    private String title;
    /** 请求方法 */
    @TableField("method")
    private String method;
    /** 请求方式 */
    @TableField("type")
    private String type;
    /** 请求参数 */
    @TableField("params")
    private String params;
    /** 状态(是否成功) */
    @TableField("state")
    private Boolean state;
    /** 操作类型 */
    @TableField("action")
    private Action action;
    /** 请求ip */
    @TableField("request_ip")
    private String requestIp;
    /** ip来源 */
    @TableField("address")
    private String address;
    /** 浏览器 */
    @TableField("browser")
    private String browser;
    /** 请求耗时 */
    @TableField("time")
    private Long time;
    /** 异常信息 */
    @TableField("error")
    private byte[] error;
    @TableField(exist = false)
    private String exceptionDetailStr;
    /** 系统 */
    @TableField("`system`")
    private String system;

    public String getExceptionDetailStr() {
        return new String(ObjectUtil.isNotNull(error) ? error : "".getBytes());
    }
}

2.2 annotation包

package com.example.log.annotation;

import com.example.log.common.enums.Action;
import java.lang.annotation.*;

/**
 * 日志 注解
 * */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Log {

    /**
     * Title 默认输入
     * */
    String title() default "暂无标题";

    /**
     * Describe 默认输入
     * */
    String describe() default "暂无描述";

    /**
     * Action 操作类型
     * */
    Action action() default Action.QUERY;
}

2.3 common包
2.3.1 enums
Action 日 志 分 类

package com.example.log.common.enums;

/**
 * 日 志 分 类
 */
public enum Action {
    /** 认证 */
    AUTH,
    /** 增 */
    ADD,
    /** 删 */
    REMOVE,
    /** 改 */
    EDIT,
    /** 查 */
    QUERY,
    /** 导入 */
    IMPORT,
    /** 导出 */
    REPORT,
    /** 上传 */
    UPLOAD
}

2.3.2 context
CallBack

package com.example.log.common.context;

/**
 * @Desc: TODO
 *          针对某些初始化方法,在SpringContextHolder 初始化前时,<br>
 *          提交一个 提交回调任务。<br>
 *          在SpringContextHolder 初始化后,进行回调使用
 */
public interface CallBack {

    /**
     * 回调执行方法
     */
    void executor();

    /**
     * 本回调任务名称
     * @return /
     */
    default String getCallBackName() {
        return Thread.currentThread().getId() + ":" + this.getClass().getName();
    }

}

SpringContextHolder

package com.example.log.common.context;

import org.springframework.context.ApplicationContext;
import org.springframework.core.env.Environment;
import java.util.ArrayList;
import java.util.List;

public class SpringContextHolder {

    private static ApplicationContext applicationContext = null;
    private static final List<CallBack> CALL_BACKS = new ArrayList<>();
    private static boolean addCallback = true;

    /**
     * 针对 某些初始化方法,在SpringContextHolder 未初始化时 提交回调方法。
     * 在SpringContextHolder 初始化后,进行回调使用
     *
     * @param callBack 回调函数
     */
    public synchronized static void addCallBacks(CallBack callBack) {
        if (addCallback) {
            SpringContextHolder.CALL_BACKS.add(callBack);
        } else {
            System.out.println("CallBack:" + callBack.getCallBackName() +" 已无法添加!立即执行");
            callBack.executor();
        }
    }

    /**
     * 获取SpringBoot 配置信息
     *
     * @param property     属性key
     * @param defaultValue 默认值
     * @param requiredType 返回类型
     * @return /
     */
    public static <T> T getProperties(String property, T defaultValue, Class<T> requiredType) {
        T result = defaultValue;
        try {
            result = getBean(Environment.class).getProperty(property, requiredType);
        } catch (Exception ignored) {}
        return result;
    }

    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    public static <T> T getBean(Class<T> requiredType) {
        assertContextInjected();
        return applicationContext.getBean(requiredType);
    }

    /**
     * 检查ApplicationContext不为空.
     */
    private static void assertContextInjected() {
        if (applicationContext == null) {
            throw new IllegalStateException("applicaitonContext属性未注入, 请在applicationContext" +
                    ".xml中定义SpringContextHolder或在SpringBoot启动类中注册SpringContextHolder.");
        }
    }

}

 

BaseContext

package com.example.log.common.context;

import com.example.log.common.utils.ServletUtil;
import com.example.log.domain.SysLog;
import com.example.log.common.enums.Action;
import com.example.log.service.LogService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
 * Base Context
 */
@Component
public class BaseContext {

    /**
     * 日 志 服 务
     */
    @Resource
    private LogService sysLogService;

    /**
     * 新增日志
     *
     * @param title  标题
     * @param methodName 请求方法
     * @param parameter 参数
     * @param action 动作
     * @param state  状态
     * @param time   请求耗时
     * @param error  异常
     */
    @Async
    @Transactional(rollbackFor = Exception.class)
    public void record(String title,
                       String methodName,
                       String parameter,
                       Action action,
                       Boolean state,
                       Long time,
                       byte[] error) {
        SysLog sysLog = new SysLog();
        sysLog.setOperator("");// 操作人
        sysLog.setOperationTime(LocalDateTime.now());// 操作时间
        sysLog.setTitle(title);//标题
        sysLog.setMethod(methodName);// 请求方法
        sysLog.setType(ServletUtil.getMethod());// 请求方式
        sysLog.setParams(parameter);// 参数
        sysLog.setState(state);// 状态(是否成功)
        sysLog.setAction(action);// 操作类型
        String ip = ServletUtil.getIp();
        sysLog.setRequestIp(ServletUtil.getIp());// 请求ip
        sysLog.setAddress(ServletUtil.getCityInfo(ip));// ip来源
        sysLog.setBrowser(ServletUtil.getBrowser());// 浏览器
        sysLog.setTime(time);// 请求耗时
        sysLog.setError(error);// 异常信息
        sysLog.setSystem(ServletUtil.getSystem());// 操作系统
        sysLogService.save(sysLog);
    }

}

2.3.3 utils
CloseUtil

package com.example.log.common.utils;

import java.io.Closeable;

/**
 * @Desc: TODO 用于关闭各种连接,缺啥补啥
 */
public class CloseUtil {

    public static void close(Closeable closeable) {
        if (null != closeable) {
            try {
                closeable.close();
            } catch (Exception e) {
                // 静默关闭
            }
        }
    }

    public static void close(AutoCloseable closeable) {
        if (null != closeable) {
            try {
                closeable.close();
            } catch (Exception e) {
                // 静默关闭
            }
        }
    }
}

ExceptionUtil

package com.example.log.common.utils;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;

/**
 * @Desc: TODO 将日志堆栈信息输出到文件
 */
public class ExceptionUtil {

    public static String getMessage(Exception e) {
        StringWriter sw = null;
        PrintWriter pw = null;
        try {
            sw = new StringWriter();
            pw = new PrintWriter(sw);
            // 将出错的栈信息输出到printWriter中
            e.printStackTrace(pw);
            pw.flush();
            sw.flush();
        } finally {
            if (sw != null) {
                try {
                    sw.close();
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            if (pw != null) {
                pw.close();
            }
        }
        return sw.toString();
    }

    /**
     * 获取堆栈信息
     */
    public static String getStackTrace(Throwable throwable){
        StringWriter sw = new StringWriter();
        try (PrintWriter pw = new PrintWriter(sw)) {
            throwable.printStackTrace(pw);
            return sw.toString();
        }
    }
}

 

FileUtil

package com.example.log.common.utils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * @Desc: TODO File工具类,扩展 hutool 工具包
 */
public class FileUtil extends cn.hutool.core.io.FileUtil {

    /**
     * 系统临时目录
     * <br>
     * windows 包含路径分割符,但Linux 不包含,
     * 在windows \\==\ 前提下,
     * 为安全起见 同意拼装 路径分割符,
     * <pre>
     *       java.io.tmpdir
     *       windows : C:\Users/xxx\AppData\Local\Temp\
     *       linux: /temp
     * </pre>
     */
    public static final String SYS_TEM_DIR = System.getProperty("java.io.tmpdir") + File.separator;

    /**
     * inputStream 转 File
     */
    public static File inputStreamToFile(InputStream ins, String name){
        File file = new File(SYS_TEM_DIR + name);
        if (file.exists()) {
            return file;
        }
        OutputStream os = null;
        try {
            os = new FileOutputStream(file);
            int bytesRead;
            int len = 8192;
            byte[] buffer = new byte[len];
            while ((bytesRead = ins.read(buffer, 0, len)) != -1) {
                os.write(buffer, 0, bytesRead);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            CloseUtil.close(os);
            CloseUtil.close(ins);
        }
        return file;
    }

}

 

ServletUtil

package com.example.log.common.utils;

import cn.hutool.http.HttpUtil;
import cn.hutool.json.JSONUtil;
import com.example.log.common.context.SpringContextHolder;
import nl.basjes.parse.useragent.UserAgent;
import nl.basjes.parse.useragent.UserAgentAnalyzer;
import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbSearcher;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.net.InetAddress;
import java.net.UnknownHostException;

/**
 * Servlet 工具类
 * */
public class ServletUtil {

    /**
     * IP归属地查询
     */
    private static final String IP_URL = "http://whois.pconline.com.cn/ipJson.jsp?ip=%s&json=true";
    /**
     * 用于IP定位转换
     */
    private static final String REGION = "内网IP|内网IP";

    private static boolean ipLocal = false;
    private static File file = null;
    private static DbConfig config;
    private static final UserAgentAnalyzer userAgentAnalyzer = UserAgentAnalyzer
            .newBuilder()
            .hideMatcherLoadStats()
            .withCache(10000)
            .withField(UserAgent.AGENT_NAME_VERSION)
            .build();

    static {
        SpringContextHolder.addCallBacks(() -> {
            ipLocal = SpringContextHolder.getProperties("ip.local-parsing", false, Boolean.class);
            if (ipLocal) {
                /*
                 * 此文件为独享 ,不必关闭
                 */
                String path = "ip2region/ip2region.db";
                String name = "ip2region.db";
                try {
                    config = new DbConfig();
                    file = FileUtil.inputStreamToFile(new ClassPathResource(path).getInputStream(), name);
                } catch (Exception e) {
                    e.printStackTrace();
//                    log.error(e.getMessage(), e);
                }
            }
        });
    }

    /**
     * Describe: Request 客户端地址(获取ip地址)
     *
     * @return {@link String}
     * */
    public static String getIp() {
        HttpServletRequest request = getRequest();
        String 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 ("127.0.0.1".equals(ipAddress) || "0:0:0:0:0:0:0:1".equals(ipAddress)) {
                // 根据网卡取本机配置的IP
                try {
                    ipAddress = InetAddress.getLocalHost().getHostAddress();
                } catch (UnknownHostException e) {
                    e.printStackTrace();
                }
            }
        }
        //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
            if (ipAddress.indexOf(",") > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }

    /**
     * 根据ip获取详细地址
     */
    public static String getCityInfo(String ip) {
        if (ipLocal) {
            return getLocalCityInfo(ip);
        } else {
            return getHttpCityInfo(ip);
        }
    }

    /**
     * 根据ip获取详细地址
     */
    public static String getHttpCityInfo(String ip) {
        String api = String.format(IP_URL, ip);
        cn.hutool.json.JSONObject object = JSONUtil.parseObj(HttpUtil.get(api));
        return object.get("addr", String.class);
    }

    /**
     * 根据ip获取详细地址
     */
    public static String getLocalCityInfo(String ip) {
        try {
            DataBlock dataBlock = new DbSearcher(config, file.getPath())
                    .binarySearch(ip);
            String region = dataBlock.getRegion();
            String address = region.replace("0|", "");
            char symbol = '|';
            if (address.charAt(address.length() - 1) == symbol) {
                address = address.substring(0, address.length() - 1);
            }
            return address.equals(REGION) ? "内网IP" : address;
        } catch (Exception e) {
//            log.error(e.getMessage(), e);
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 获取 HttpServletRequest 对象
     *
     * @return {@link HttpServletRequest}
     * */
    private static HttpServletRequest getRequest(){
        ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return servletRequestAttributes.getRequest();
    }

    /**
     * Request 请求方法(类型)
     *
     * @return {@link String}
     * */
    public static String getMethod(){
        return getRequest().getMethod();
    }

    /**
     * Request 请求头
     *
     * @param name 名称
     * @return {@link String}
     * */
    public static String getHeader(String name){
        return getRequest().getHeader(name);
    }

    /**
     * Request Agent
     *
     * @return {@link String}
     * */
    private static String getAgent(){
        return getHeader("User-Agent");
    }

    /**
     * Request 浏览器类型
     *
     * @return {@link String}
     * */
    public static String getBrowser(){
        String browser = "";
        String userAgent = getAgent();
        if (userAgent.contains("Firefox")) browser = "火狐浏览器";
        else if (userAgent.contains("Chrome")) browser = "谷歌浏览器";
        else if (userAgent.contains("Trident")) browser = "IE 浏览器";
        else browser = "你用啥浏览器";
        UserAgent.ImmutableUserAgent parse = userAgentAnalyzer.parse(userAgent);
        String value = parse.get(UserAgent.AGENT_NAME_VERSION).getValue();
        return browser + "(" + value + ")";
    }

    /**
     * Request 访问来源 ( 客户端类型 )
     *
     * @return {@link String}
     * */
    public static String getSystem(){
        String userAgent = getAgent();
        if (getAgent().toLowerCase().contains("windows" )) return "Windows";
        else if (userAgent.toLowerCase().contains("mac" )) return "Mac";
        else if (userAgent.toLowerCase().contains("x11" )) return "Unix";
        else if (userAgent.toLowerCase().contains("android" )) return "Android";
        else if (userAgent.toLowerCase().contains("iphone" )) return "IPhone";
        else return "UnKnown, More-Info: " + userAgent;
    }

}

2.4 aspect包(核心)
Log 实现 Aop 切面类

package com.example.log.aspect;

import cn.hutool.json.JSONUtil;
import com.example.log.annotation.Log;
import com.example.log.common.context.BaseContext;
import com.example.log.common.enums.Action;
import com.example.log.common.utils.ExceptionUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
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 javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Log 实现 Aop 切面类
 */
@Aspect
@Component
public class LogAspect {

    ThreadLocal<Long> currentTime = new ThreadLocal<>();

    /**
     * 基 础 上 下 文
     */
    @Resource
    private BaseContext context;

    /**
     * 配置切入点(切 面 编 程)
     */
    @Pointcut("@annotation(com.example.log.annotation.Log) || @within(com.example.log.annotation.Log)")
    public void logPointcut() {
        // 该方法无方法体,主要为了让同类中其他方法使用此切入点
    }

    /**
     * 处 理 系 统 日 志(配置环绕通知,使用在方法logPointcut()上注册的切入点)
     *
     * @param joinPoint join point for advice
     */
    @Around("logPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        // 记录方法的执行时间
        currentTime.set(System.currentTimeMillis());
        // 执 行 方 法
        result = joinPoint.proceed();
        // 注解解析
        Log annotation = getAnnotation(joinPoint);
        String title = annotation.title();
        Action action = annotation.action();
        String describe = annotation.describe();
        // 获取方法名
        String methodName = getMethodName(joinPoint);
        // 获取参数
        String parameter = getParameterToJson((ProceedingJoinPoint) joinPoint);
        // 请求耗时
        Long time = System.currentTimeMillis() - currentTime.get();
        currentTime.remove();

        // 记 录 日 志
        context.record(title, methodName, parameter, action, true, time, null);

        return result;
    }

    /**
     * 配置异常通知
     *
     * @param joinPoint join point for advice
     * @param e         exception
     */
    @AfterThrowing(pointcut = "logPointcut()", throwing = "e")
    public void logAfterThrowing(JoinPoint joinPoint, Throwable e) {
        // 注解解析
        Log annotation = getAnnotation((ProceedingJoinPoint) joinPoint);
        String title = annotation.title();
        Action action = annotation.action();
        String describe = annotation.describe();
        // 获取方法名
        String methodName = getMethodName((ProceedingJoinPoint) joinPoint);
        // 获取参数
        String parameter = getParameterToJson((ProceedingJoinPoint) joinPoint);
        // 请求耗时
        Long time = System.currentTimeMillis() - currentTime.get();
        currentTime.remove();
        // 异常详细
        byte[] exceptionDetail = ExceptionUtil.getStackTrace(e).getBytes();

        // 记 录 日 志
        context.record(title, methodName, parameter, action, false, time, exceptionDetail);
    }

    /**
     * 获 取 注 解
     */
    public Log getAnnotation(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        Class<? extends Object> targetClass = point.getTarget().getClass();
        Log targetLog = targetClass.getAnnotation(Log.class);
        if (targetLog != null) {
            return targetLog;
        } else {
            Method method = signature.getMethod();
            Log log = method.getAnnotation(Log.class);
            return log;
        }
    }

    /**
     * 获 取 方法名
     */
    public String getMethodName(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        // 方法路径
        String methodName = point.getTarget().getClass().getName()+"."+signature.getName()+"()";
        return methodName;
    }

    /**
     * 获 取 参数(转换json格式)
     */
    public String getParameterToJson(ProceedingJoinPoint point) {
        List<Object> argList = new ArrayList<>();
        //参数值
        Object[] argValues = point.getArgs();
        //参数名称
        String[] argNames = ((MethodSignature)point.getSignature()).getParameterNames();
        if(argValues != null){
            for (int i = 0; i < argValues.length; i++) {
                Map<String, Object> map = new HashMap<>();
                String key = argNames[i];
                map.put(key, argValues[i]);
                argList.add(map);
                map = null;
            }
        }
        if (argList.size() == 0) {
            return "";
        }
        return argList.size() == 1 ? JSONUtil.toJsonStr(argList.get(0)) : JSONUtil.toJsonStr(argList);
    }
}

2.5 三层架构
2.5.1 controller

package com.example.log.controller;

import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.example.log.annotation.Log;
import com.example.log.service.LogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/logs")
@Api(tags = "日志管理")
public class LogController {

    /** 使用@RequiredArgsConstructor 基于构造函数注入*/
    private final LogService logService;

    /**
     * 查询日志列表
     */
    @GetMapping("list")
    @ApiOperation(value = "查询日志")
    public String list(){
        return JSONUtil.parse(logService.list()).toString();
    }

    /**
     * 测试
     */
    @GetMapping("demo")
    @Log(title = "测试")
    @ApiOperation(value = "测试")
    public String demo(@RequestParam String param){
        return param;
    }

}

 

2.5.2 service

package com.example.log.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.example.log.domain.SysLog;

public interface LogService extends IService<SysLog> {

}
package com.example.log.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.log.domain.SysLog;
import com.example.log.repository.LogRepository;
import com.example.log.service.LogService;
import org.springframework.stereotype.Service;

@Service
public class LogServiceImpl extends ServiceImpl<LogRepository, SysLog> implements LogService {
}

2.5.2 repository

package com.example.log.repository;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.log.domain.SysLog;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface LogRepository extends BaseMapper<SysLog> {
}

3、测试

新增测试 http://127.0.0.1:8080//api/logs/demo?param=123

在这里插入图片描述

查看数据 http://127.0.0.1:8080//api/logs/list

在这里插入图片描述