SpringBoot AOP插拔式操作日志

设计思想

在实际应用开发中, 时常会有需要记录操作日志的需求, 操作日志可以给开发者带来明确的操作数据信息, 进而更加了解项目分析问题, 操作日志设计五花八门, 故因此我设计了一个 通用的 \color{#FF0000}{通用的} 通用的, 灵活的 \color{#FF0000}{灵活的} 灵活的, 简洁的 \color{#FF0000}{简洁的} 简洁的, 插拔式 \color{#FF0000}{插拔式} 插拔式操作日志切面类

使用需求

  • 数据库连接
  • 需引入spring-boot-starter-jdbc依赖
  • 需引入spring-boot-starter-aop依赖

AOP插拔式操作日志类

只需修改 切入点配置 \color{#FF0000}{切入点配置} 切入点配置 异常匹配包路径 \color{#FF0000}{异常匹配包路径} 异常匹配包路径就可以正常使用
其他配置可根据项目需求灵活变动或修改

/**
 * @ClassName LogAspect
 * @Description 操作日志组件类
 * @Author DLTS
 */

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCallback;
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.annotation.Annotation;
import java.lang.reflect.Method;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Objects;

@Aspect
@Component
public class LogAspect {

    /**
     * 数据库操作对象
     */
    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 切入点配置
     */
    @Pointcut("execution(public * com.dlts.admin.controller.*.*(..))")
    public void logPointCut() {
        // 切入点根据包结构进行配置
    }

    /**
     * 异常匹配包路径
     * 用于筛选异常信息, 过滤无用异常信息
     */
    private static final String packagePath = "com.dlts.admin";
    /**
     * 表前缀
     */
    private static final String tablePrefix = "log_";
    /**
     * 表后缀时间格式(当前按月分表)
     */
    private static final DateTimeFormatter tableSuffixFormatter = DateTimeFormatter.ofPattern("yyyyMM");
    /**
     * 接口排除集
     * 支持?匹配符和*通配符
     */
    private static final HashSet<String> excludeSet = new HashSet<String>() {{
    }};

    /**
     * 获取操作人
     *
     * @return 可返回ID, 姓名或其他相关属性
     */
    public String getOperator(HttpServletRequest request) {
        return null;
    }

    /**
     * 建表语句
     * %s 表示当前表名称
     */
    private static final String createTableSql = "create table %s\n" +
            "(\n" +
            "    id                bigint unsigned primary key auto_increment not null comment '主键id',\n" +
            "    ip                varchar(128)                               not null comment 'IP地址',\n" +
            "    operator          varchar(256)                               null comment '操作人',\n" +
            "    uri               varchar(256)                               not null comment '请求URI',\n" +
            "    class_info        varchar(256)                               not null comment '类信息',\n" +
            "    method_info       varchar(256)                               not null comment '方法信息',\n" +
            "    success           tinyint(1)                                 not null comment '是否成功',\n" +
            "    request_body      text                                       null comment '请求体',\n" +
            "    response_body     text                                       null comment '响应体',\n" +
            "    error_message     text                                       null comment '错误信息',\n" +
            "    create_time       datetime  default CURRENT_TIMESTAMP        not null comment '创建时间',\n" +
            "    update_time       timestamp default CURRENT_TIMESTAMP        not null on update CURRENT_TIMESTAMP comment '修改时间'\n" +
            ")";
    /**
     * 判断表是否存在语句
     */
    private static final String checkTableSql = "select count(1) from information_schema.tables where table_name = ? limit 1";
    /**
     * 插入语句
     */
    private static final String insertSql = "insert into %s (ip, operator, uri, class_info, method_info, success, request_body, response_body, error_message) values (?, ?, ?, ?, ?, ?, ?, ?, ?)";

    public boolean insert(String tableName, Log log) {
        return jdbcTemplate.execute(connection -> {
            PreparedStatement preparedStatement = connection.prepareStatement(String.format(insertSql, tableName));
            preparedStatement.setString(1, log.getIp());
            preparedStatement.setString(2, log.getOperator());
            preparedStatement.setString(3, log.getUri());
            preparedStatement.setString(4, log.getClassInfo());
            preparedStatement.setString(5, log.getMethodInfo());
            preparedStatement.setBoolean(6, log.getSuccess());
            preparedStatement.setString(7, log.getRequestBody());
            preparedStatement.setString(8, log.getResponseBody());
            preparedStatement.setString(9, log.getErrorMessage());
            return preparedStatement;
        }, (PreparedStatementCallback<Boolean>) preparedStatement -> preparedStatement.execute());
    }

    public class Log {
        /**
         * 主键
         */
        private Long id;
        /**
         * 请求来源IP地址
         */
        private String ip;
        /**
         * 操作人
         */
        private String operator;
        /**
         * 请求地址
         */
        private String uri;
        /**
         * 类信息
         */
        private String classInfo;
        /**
         * 方法信息
         */
        private String methodInfo;
        /**
         * 是否成功
         */
        private Boolean success;
        /**
         * 请求体
         */
        private String requestBody;
        /**
         * 响应体
         */
        private String responseBody;
        /**
         * 错误信息
         */
        private String errorMessage;

        /**
         * 创建时间
         */
        private LocalDateTime createTime;
        /**
         * 修改时间
         */
        private LocalDateTime updateTime;

        public Long getId() {
            return id;
        }

        public void setId(Long id) {
            this.id = id;
        }

        public String getIp() {
            return ip;
        }

        public void setIp(String ip) {
            this.ip = ip;
        }

        public String getOperator() {
            return operator;
        }

        public void setOperator(String operator) {
            this.operator = operator;
        }

        public String getUri() {
            return uri;
        }

        public void setUri(String uri) {
            this.uri = uri;
        }

        public String getClassInfo() {
            return classInfo;
        }

        public void setClassInfo(String classInfo) {
            this.classInfo = classInfo;
        }

        public String getMethodInfo() {
            return methodInfo;
        }

        public void setMethodInfo(String methodInfo) {
            this.methodInfo = methodInfo;
        }

        public Boolean getSuccess() {
            return success;
        }

        public void setSuccess(Boolean success) {
            this.success = success;
        }

        public String getRequestBody() {
            return requestBody;
        }

        public void setRequestBody(String requestBody) {
            this.requestBody = requestBody;
        }

        public String getResponseBody() {
            return responseBody;
        }

        public void setResponseBody(String responseBody) {
            this.responseBody = responseBody;
        }

        public String getErrorMessage() {
            return errorMessage;
        }

        public void setErrorMessage(String errorMessage) {
            this.errorMessage = errorMessage;
        }

        public LocalDateTime getCreateTime() {
            return createTime;
        }

        public void setCreateTime(LocalDateTime createTime) {
            this.createTime = createTime;
        }

        public LocalDateTime getUpdateTime() {
            return updateTime;
        }

        public void setUpdateTime(LocalDateTime updateTime) {
            this.updateTime = updateTime;
        }
    }

    /******************************** 以上是配置 以下是逻辑 ********************************/

    /**
     * 日志对象
     */
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
    /**
     * jackson序列化对象
     */
    private static final ObjectMapper objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);

    /**
     * 核心环绕切面方法
     *
     * @param point 连接点对象
     * @return 响应对象
     * @throws Throwable
     * @throws InterruptedException
     */
    @Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable, InterruptedException {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        log.info("------------------------------------------------------------------------------");
        log.info("接口路径: {}", request.getRequestURI());
        log.info("请求方式: {}", request.getMethod());
        log.info("临时令牌: {}", request.getHeader("token"));
        log.info("请求时间: {}", LocalDateTime.now());
        log.info("------------------------------------------------------------------------------");
        // 校验排除
        if (isExclude(excludeSet, request.getRequestURI())) return point.proceed();
        // 封装log对象
        Log log = new Log();
        // 设置请求来源IP
        log.setIp(getIp(request));
        // 设置操作人(自行补充逻辑)
        log.setOperator(getOperator(request));
        // 设置请求URI
        log.setUri(request.getRequestURI());
        // 获取目标类对象
        Class<?> targetClass = point.getTarget().getClass();
        // 设置类信息
        log.setClassInfo(getAnnotationValue(targetClass.getAnnotation((Class<Annotation>) Class.forName("io.swagger.annotations.Api")), "tags", targetClass.getSimpleName()));
        // 获取目标方法对象
        MethodSignature signature = (MethodSignature) point.getSignature();
        Method method = targetClass.getDeclaredMethod(signature.getName(), signature.getParameterTypes());
        method.setAccessible(true);
        // 设置方法信息
        log.setMethodInfo(getAnnotationValue(method.getAnnotation((Class<Annotation>) Class.forName("io.swagger.annotations.ApiOperation")), "value", method.getName()));
        // 设置请求参数
        log.setRequestBody(getJsonStr(point.getArgs()));
        try {
            Object result = point.proceed();
            // 设置是否成功
            log.setSuccess(true);
            // 设置响应体
            log.setResponseBody(getJsonStr(result));
            return result;
        } catch (Throwable throwable) {
            StackTraceElement[] stackTraceElements = throwable.getStackTrace();
            StackTraceElement traceElement = Arrays.stream(stackTraceElements)
                    .filter(element -> element.getClassName().startsWith(packagePath))
                    .findFirst().orElse(stackTraceElements[0]);
            String errorMessage = String.format("%s\n%s", traceElement.toString(), throwable.toString());
            System.err.println(errorMessage);
            // 设置是否成功
            log.setSuccess(false);
            // 设置错误信息
            log.setErrorMessage(errorMessage);
            throw throwable;
        } finally {
            // 校验表
            String tableName = tablePrefix.concat(tableSuffixFormatter.format(LocalDate.now()));
            createTableIfNotExist(tableName);
            insert(tableName, log);
        }
    }

    /**
     * 校验排除
     *
     * @param excludeSet
     * @param str
     * @return
     */
    public boolean isExclude(HashSet<String> excludeSet, String str) {
        if (Objects.isNull(excludeSet) || excludeSet.isEmpty()) return false;
        return excludeSet.stream().anyMatch(p -> isMatch(str, p));
    }

    /**
     * 获取注解的方法值
     *
     * @param annotation   注解对象
     * @param method       注解方法
     * @param defaultValue 默认值
     * @return 注解方法返回值
     */
    public static String getAnnotationValue(Annotation annotation, String method, String defaultValue) {
        try {
            if (Objects.isNull(annotation)) return defaultValue;
            Method api = annotation.getClass().getDeclaredMethod(Objects.isNull(method) || method.trim().isEmpty() ? "value" : method);
            if (Objects.isNull(api)) return defaultValue;
            Object obj = api.invoke(annotation);
            if (obj.getClass().isArray()) return objectMapper.writeValueAsString(obj);
            return obj.toString();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return defaultValue;
    }

    /**
     * Object对象转Json字符串
     *
     * @param objs
     * @return
     */
    public static String getJsonStr(Object... objs) {
        try {
            if (Objects.isNull(objs) || objs.length == 0) return null;
            else if (objs.length == 1) return objectMapper.writeValueAsString(objs[0]);
            else return objectMapper.writeValueAsString(objs);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * existTableName 作为临时存储, 减少数据库访问次数
     */
    private String existTableName;

    /**
     * 查询数据库表存在, 如果不存在则创建表
     *
     * @param tableName 动态生成的表名
     */
    public void createTableIfNotExist(String tableName) {
        // 判断表是否存在
        if (Objects.equals(tableName, existTableName)) return;
        else if (checkTable(tableName)) existTableName = tableName;
        else if (createTable(tableName)) existTableName = tableName;
        else throw new RuntimeException("日志表 '" + tableName + "' 创建失败");
    }

    /**
     * 判断数据库表是否存在
     *
     * @param tableName 表名称
     * @return true表示存在, false表示不存在
     */
    public boolean checkTable(String tableName) {
        return jdbcTemplate.execute(connection -> {
            PreparedStatement preparedStatement = connection.prepareStatement(checkTableSql);
            preparedStatement.setString(1, tableName);
            return preparedStatement;
        }, (PreparedStatementCallback<Integer>) preparedStatement -> {
            preparedStatement.execute();
            ResultSet rs = preparedStatement.getResultSet();
            rs.next();
            return rs.getInt(1);
        }) > 0;
    }


    /**
     * 创建表
     *
     * @param tableName
     * @return
     */
    public boolean createTable(String tableName) {
        return !jdbcTemplate.execute(connection -> connection.prepareStatement(String.format(createTableSql, tableName))
                , (PreparedStatementCallback<Boolean>) preparedStatement -> preparedStatement.execute());
    }

    /**
     * ?单配符和*通配符的校验匹配
     *
     * @param s 待匹配的字符串
     * @param p 匹配规则
     * @return 匹配结果
     */
    public static Boolean isMatch(String s, String p) {
        // s的索引位置
        int i = 0;
        // p的索引位置
        int j = 0;
        // 通配符时回溯的位置
        int ii = -1;
        int jj = -1;
        while (i < s.length()) {
            if (j < p.length() && p.charAt(j) == '*') {
                // 遇到通配符了,记录下位置,规则字符串+1,定位到非通配字符串
                ii = i;
                jj = j;
                j++;
            } else if (j < p.length() && (s.charAt(i) == p.charAt(j) || p.charAt(j) == '?')) {
                // 匹配到了
                i++;
                j++;
            } else {
                // 匹配失败,需要判断s 是否被p的*号匹配着.如果等于-1, 前面没有通配符
                if (jj == -1) return false;
                // 回到之前记录通配符的位置
                j = jj;
                // 带匹配字符串也回到记录的位置,并后移一位
                i = ii + 1;
            }
        }
        // 当s的每一个字段都匹配成功以后,判断p剩下的串,是*则放行
        while (j < p.length() && p.charAt(j) == '*') j++;
        // 检测到最后就匹配成功
        return j == p.length();
    }

    /**
     * 获取访问真实IP
     *
     * @return
     */
    public static String getIp(HttpServletRequest request) {
        if (Objects.isNull(request)) {
            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            request = Objects.requireNonNull(attributes).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.getHeader("X-Real-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
        }
        //对于通过多个代理的情况,第一个IP为客户端真实IP,多个IP按照','分割
        if (ipAddress != null && ipAddress.length() > 15) { //"***.***.***.***".length() = 15
            if (ipAddress.indexOf(",") > 0) {
                ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
            }
        }
        return ipAddress;
    }
}

小结

如有不完善的地方或错误, 请评论点出或联系我及时纠正, 谢谢。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值