2022-06-07 六.日志实现

前言

一个项目中日志是必不可少的功能,在开发、测试、部署、线上等环境下,日志可以帮助开发人员对程序的执行、问题排除带来帮助。

而日志又分为系统日志和操作日志:

  • 系统日志是为了了解应用程序的执行过程,比如请求日志、在if…else…等进行了哪个分支、在某一行代码对信息进行打印输出,通过日志信息帮助开发人员了解系统的执行
  • 操作日志一般是给用户看的,比如订单的创建中间的步骤,目前进度到了哪里、或者是对一些信息做了新增或修改;这样子对操作日志的可读性有一定要求

那么在系统中如何实现呢?最简单操作日志的就是使用log.info("用户{} 将收货地址‘{}’ 修改为 ‘{}’ ", userId, oldAddr, newAddr);将修改记录进行输出。这里对修改信息进行了收集,但对于代码而言增加了代码可读性的负担,或者说侵入性。到了要进行日志输出时就需要调用log.xxx(xxxx);,有什么办法可以解决呢?

Java项目中日志引入与实现

开启日志

比较常用的日志Log4j、Log4j2、Logbak,在SpringBoot中使用门面模式对不同的日志进行支持

SpringBoot项目中导入starter依赖时,默认使用Logback日志。它使用门面模式Slf4j来做制定了日志功能接口,具体使用的日志只需要实现功能,就可以做到无缝切换

maven依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

springboot默认使用logback,这里也就使用logback进行讲解,首先是配置文件

# 日志默认是info级别, 可以对特定的路径指定日志等级
logging:
  level: 
    com.zsl.service.dao: debug

log配置文件名:

Logging SystemCustomization
Logbacklogback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
Log4j2log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging)logging.properties

自定义配置 ‘logback-spring.xml’:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,比如: 如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文档如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文档是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<configuration  scan="true" scanPeriod="10 seconds">
    <contextName>logback</contextName>
    
<!--    logback-spring.xml 读取顺序晚于 application.yaml 可以使用yaml里的配置-->
    <springProperty scope="context" name="logName" source="log.storage-path" defaultValue="default"/>

    <!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义后,可以使“${}”来使用变量。 -->
    <property name="log.path" value="${logName}"/>

    <!--0. 日志格式和颜色渲染 -->
    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>

    <!--1. 输出到控制台-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 设置字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>

    <!--2. 输出到文档-->
    <!-- 2.1 level为 DEBUG 日志,时间滚动输出  -->
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/debug.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 日志归档 -->
            <fileNamePattern>${log.path}/debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>1000MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录debug级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>debug</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 2.2 level为 INFO 日志,时间滚动输出  -->
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/info.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset>
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!-- 每天日志归档路径以及格式 -->
            <fileNamePattern>${log.path}/info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>1000MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录info级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>info</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 2.3 level为 WARN 日志,时间滚动输出  -->
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/warn.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>1000MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录warn级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>warn</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 2.4 level为 ERROR 日志,时间滚动输出  -->
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/error.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>1</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档只记录ERROR级别的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>ERROR</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
    </appender>

    <!-- 2.5 所有 除了DEBUG级别的其它高于DEBUG的 日志,记录到一个文件  -->
    <appender name="ALL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- 正在记录的日志文档的路径及文档名 -->
        <file>${log.path}/all.log</file>
        <!--日志文档输出格式-->
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            <charset>UTF-8</charset> <!-- 此处设置字符集 -->
        </encoder>
        <!-- 日志记录器的滚动策略,按日期,按大小记录 -->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${log.path}/all-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>1000MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文档保留天数-->
            <maxHistory>15</maxHistory>
        </rollingPolicy>
        <!-- 此日志文档记录除了DEBUG级别的其它高于DEBUG的 -->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>DEBUG</level>
            <onMatch>DENY</onMatch>
            <onMismatch>ACCEPT</onMismatch>
        </filter>
    </appender>

    <!--
        <logger>用来设置某一个包或者具体的某一个类的日志打印级别、
        以及指定<appender>。<logger>仅有一个name属性,
        一个可选的level和一个可选的addtivity属性。
        name:用来指定受此logger约束的某一个包或者具体的某一个类。
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
              还有一个特殊值INHERITED或者同义词NULL,代表强制执行上级的级别。
              如果未设置此属性,那么当前logger将会继承上级的级别。
        addtivity:是否向上级logger传递打印信息。默认是true。
        <logger name="org.springframework.web" level="info"/>
        <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>
    -->

    <!--
        使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
        第一种把<root level="info">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
        第二种就是单独给dao下目录配置debug模式,代码如下,这样配置sql语句会打印,其他还是正常info级别:
        【logging.level.org.mybatis=debug logging.level.dao=debug】
     -->

    <!--
        root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
        level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
        不能设置为INHERITED或者同义词NULL。默认是DEBUG
        可以包含零个或多个元素,标识这个appender将会添加到这个logger。
    -->

    <!-- 4  最终的策略:
                 基本策略(root级) + 根据profile在启动时, logger标签中定制化package日志级别(优先级高于上面的root级)-->
    <springProfile name="dev">
        <root level="info">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
            <appender-ref ref="ALL_FILE" />
        </root>
        <logger name="com.zsl" level="debug"/> <!-- 开发环境, 指定某包日志为debug级 -->
    </springProfile>

    <springProfile name="zsl_test">
        <root level="info">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
            <appender-ref ref="ALL_FILE" />
        </root>
        <logger name="com.zsl" level="debug"/> <!-- 测试环境, 指定某包日志为info级 -->
<!-- 		<logger name="com.zsl" level="info"/>--> <!-- 测试环境, 指定某包日志为info级 -->
    </springProfile>

    <springProfile name="prod">
        <root level="info">
            <!-- 生产环境最好不配置console写文件 -->
            <appender-ref ref="DEBUG_FILE" />
            <appender-ref ref="INFO_FILE" />
            <appender-ref ref="WARN_FILE" />
            <appender-ref ref="ERROR_FILE" />
            <appender-ref ref="ALL_FILE" />
        </root>
        <logger name="com.zsl" level="warn"/> <!-- 生产环境, 指定某包日志为warn级 -->
        <logger name="com.zsl.MyApplication" level="info"/> <!-- 特定某个类打印info日志, 比如application启动成功后的提示语 -->
    </springProfile>

</configuration>

日志实现

系统日志

系统日志,为了了解执行过程,采用Interceptor的形式对请求进行日志收集,对于配置的执行过程,可以使用log.info()进行日志输出。

目前只实现了雏形后续每天会不断完善

包结构:

在这里插入图片描述

这里使用interceptor实现:

package com.zsl.custombox.log.core.interceptor;

import com.zsl.custombox.common.util.SecurityContextHolder;
import com.zsl.custombox.common.util.ServletUtil;
import com.zsl.custombox.common.model.log.SystemLogContext;
import com.zsl.custombox.common.util.SystemLogContextHolder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 访问记录(系统日志)
 *  实现@LogRecord注解实现操作日志
 *
 * @Author zsl
 * @Date 2022/5/22 14:55
 * @Email 249269610@qq.com
 */
public class AccessLogInterceptor implements HandlerInterceptor {
    private final Logger log = LoggerFactory.getLogger(this.getClass());

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 创建全局日志记录上下文 todo 获取数据(实现工具类)

        SystemLogContext systemLogContext = new SystemLogContext()
                .setUserId(SecurityContextHolder.getAuth().getUserId())
                .setRequestNo(0L)// 可以使用雪花算法获取64位唯一id
                .setIp(ServletUtil.getIp())
                .setUri(request.getRequestURI())
                .setMethod(request.getMethod())
                .setStartTime(new Date(System.currentTimeMillis()));
        // 存储全局日志记录上下文
        SystemLogContextHolder.set(systemLogContext);
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        SystemLogContext systemLogContext = SystemLogContextHolder.get();
        systemLogContext.setRespTime(System.currentTimeMillis() - systemLogContext.getStartTime().getTime());

        // format log
        StringBuilder requestStr = new StringBuilder();
        List<Object> requestArgs = new ArrayList<>();
        requestStr.append("\n=========================== AccessLog ===========================\n");
        requestStr.append(String.format("       %-10s: {}\n", "userId"));
        requestArgs.add(systemLogContext.getUserId());
        requestStr.append(String.format("       %-10s: {}\n", "requestNo"));
        requestArgs.add(systemLogContext.getRequestNo());
        requestStr.append(String.format("       %-10s: {}\n", "ip"));
        requestArgs.add(systemLogContext.getIp());
        requestStr.append(String.format("       %-10s: {}\n", "uri"));
        requestArgs.add(systemLogContext.getUri());
        requestStr.append(String.format("       %-10s: {}\n", "method"));
        requestArgs.add(systemLogContext.getMethod());
        requestStr.append(String.format("       %-10s: {}\n", "startTime"));
        requestArgs.add(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(systemLogContext.getStartTime()));
        requestStr.append(String.format("       %-10s: {} ms\n", "respTime"));
        requestArgs.add(systemLogContext.getRespTime());
        requestStr.append(String.format("       %-10s: {}\n", "respCode"));
        requestArgs.add(systemLogContext.getRespCode());
        requestStr.append(String.format("       %-10s: {}\n", "respMsg"));
        requestArgs.add(systemLogContext.getRespMsg());
        requestStr.append("=================================================================\n");

        // todo 日志入库
        log.info(requestStr.toString(), requestArgs.toArray());

        // 清理ThreadLocal
        SystemLogContextHolder.clear();
    }
}

日志记录全局上下文:

package com.zsl.custombox.common.model.log;

import lombok.Data;
import lombok.experimental.Accessors;

import java.util.Date;
import java.util.Map;

/**
 * 日志记录上下文
 *
 * @Author zsl
 * @Date 2022/5/22 16:48
 * @Email 249269610@qq.com
 */
@Data
@Accessors(chain = true)
public class SystemLogContext {
    // =========================== request data ===========================
    // 用户id(匿名为0)
    private Long userId;
    // 请求号码,保证请求调用时请求号不变
    private Long requestNo;
    // ip地址
    private String ip;
    // 统一资源接口uri
    private String uri;
    // 参数
//    private String params;
    // 请求方法(GET、POST...)
    private String method;
    // 请求时间
    private Date startTime;

    // =========================== response data ===========================
    // 响应时间
    private Long respTime;
    // 响应码
    private Integer respCode;
    // 响应信息
    private String respMsg;
    // 响应体
//    private String respBody;
}

日志记录上下文工具类:

package com.zsl.custombox.log.core.util;


import com.zsl.custombox.log.core.model.LogRecordContext;

/**
 * 线程安全日志记录
 *
 * @Author zsl
 * @Date 2022/5/22 16:57
 * @Email 249269610@qq.com
 */
public class LogRecordContextHolder {
    private static final ThreadLocal<LogRecordContext> LOG_RECORD_CONTEXT = new ThreadLocal<>();

    public static LogRecordContext get() {
        return LOG_RECORD_CONTEXT.get();
    }

    public static void set(LogRecordContext logRecordContext) {
        LOG_RECORD_CONTEXT.set(logRecordContext);
    }

    public static void clear() {
        LOG_RECORD_CONTEXT.remove();
    }
}

自动配置类:

package com.zsl.custombox.log.config;

import com.zsl.custombox.log.core.interceptor.AccessLogInterceptor;
import com.zsl.custombox.log.core.model.logrecord.LogRecordOperationSource;
import com.zsl.custombox.log.core.service.record.DefaultLogRecordServiceImpl;
import com.zsl.custombox.log.core.service.record.ILogRecordService;
import com.zsl.custombox.log.core.service.function.DefaultFunctionServiceImpl;
import com.zsl.custombox.log.core.service.function.IFunctionService;
import com.zsl.custombox.log.core.service.function.IParseFunction;
import com.zsl.custombox.log.core.service.function.ParseFunctionFactory;
import com.zsl.custombox.log.core.service.operator.DefaultOperatorGetServiceImpl;
import com.zsl.custombox.log.core.service.operator.IOperatorGetService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

/**
 * 日志自动配置,AccessLogInterceptor(访问日志)、LogRecordAspect(操作日志)
 *
 * @Author zsl
 * @Date 2022/5/22 21:04
 * @Email 249269610@qq.com
 */
@Configuration
public class LogAutoConfiguration implements WebMvcConfigurer {

    // ==========================   拦截器   ==========================
    @Bean
    public AccessLogInterceptor accessLogInterceptor() {
        return new AccessLogInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.accessLogInterceptor());
    }

    // ==========================   操作日志容器   ==========================

    // 处理注解信息操作源
    @Bean
    public LogRecordOperationSource logRecordOperationSource () {
        return new LogRecordOperationSource();
    }

    // 自定义函数
    @Bean
    @ConditionalOnMissingBean(IFunctionService.class)
    public IFunctionService functionService(ParseFunctionFactory parseFunctionFactory) {
        return new DefaultFunctionServiceImpl(parseFunctionFactory);
    }

    @Bean
    public ParseFunctionFactory parseFunctionFactory(@Autowired List<IParseFunction> list) {
        return new ParseFunctionFactory(list);
    }

    @Bean
    @ConditionalOnMissingBean(IParseFunction.class)
    public IParseFunction defaultParseFunction() {
        return new IParseFunction() {
            @Override
            public String functionName() {
                return null;
            }

            @Override
            public String apply(String value) {
                return null;
            }
        };
    }

    // 持久化
    @Bean
    @ConditionalOnMissingBean(ILogRecordService.class)
    public ILogRecordService logRecordService() {
        return new DefaultLogRecordServiceImpl();
    }

    // 操作人
    @Bean
    @ConditionalOnMissingBean(IOperatorGetService.class)
    public IOperatorGetService operatorGetService() {
        return new DefaultOperatorGetServiceImpl();
    }

}

操作日志

使用AOP+注解的方式,进行操作日志:

/**
 * @Author zsl
 * @Date 2022/5/23 23:20
 * @Email 249269610@qq.com
 */
@Aspect
@Component
public class LogRecordAspect {
    private final Logger log = LoggerFactory.getLogger(this.getClass());


    @Autowired
    ILogRecordService logRecordService;

    @Autowired
    IFunctionService functionService;

    LogRecordValueParser logRecordValueParser = new LogRecordValueParser(new LogRecordExpressionEvaluator(), functionService);

    @Around("@annotation(com.zsl.custombox.log.core.annotation.LogRecord)")
    public Object logRecord(ProceedingJoinPoint point) throws Throwable {
        return execute(point, point.getTarget().getClass(), ((MethodSignature) point.getSignature()).getMethod(), point.getArgs());
    }

    /**
     * v0.0.1 版本 简单使用SpEL解析(在后面版本中提供以函数式方式解析变量)
     */
    private Object execute(ProceedingJoinPoint point, Class<?> targetClass, Method method, Object[] args) throws Throwable {
        Object ret = null;

        LogRecordContext.putEmptySpan();

        MethodExceptionResult exceptionResult = new MethodExceptionResult(true, null, "");
        // 拆分注解信息存储为List等待执行

        // 执行List中自定义函数
        String spEL = getAnnotation(method).content();

        try {
            ret = point.proceed();
        } catch (Throwable throwable) {
            exceptionResult = new MethodExceptionResult(false, throwable, throwable.getMessage());
        }
        // 解析List中SpEL
        try {
            // 存储日志
            if (Strings.isNotBlank(spEL)) {
                recordExecute(ret, method, args, spEL, targetClass,
                        exceptionResult.isSuccess(), exceptionResult.getErrorMsg());
            }
        } catch (Throwable t) {
            log.error("记录操作日志失败,不影响业务执行!", t);
        } finally {
            // 清理 Context
            LogRecordContext.clear();
        }
        // 执行失败将异常抛出
        if (!exceptionResult.isSuccess()) {
            throw exceptionResult.getThrowable();
        }
        return ret;
    }

    private void recordExecute(Object ret, Method method, Object[] args, String spEL, Class<?> targetClass, boolean success, String errorMsg) {
        // 创建上下文
        EvaluationContext evaluationContext = logRecordValueParser.createEvaluationContext(method, args, ret, errorMsg);
        // 获取评估后expressionString
        String expression = logRecordValueParser.getExpression(spEL, new AnnotatedElementKey(method, targetClass), evaluationContext);
        // 持久化操作日志
        logRecordService.record(new com.zsl.custombox.log.core.model.logrecord.LogRecord(expression));
    }

    /**
     * 获取方法
     */
    private Method getMethod(ProceedingJoinPoint point) {
        return ((MethodSignature) point.getSignature()).getMethod();
    }

    /**
     * 获取注解
     */
    private LogRecord getAnnotation(Method method) {
        LogRecord annotation = method.getAnnotation(LogRecord.class);
        return annotation;
    }
}

例:

public class XxxxServiceImpl {
    
    @LogRecord(content = "'修改用户名为' + #userParam.username")
    public int updateUser(UserParam userParam) {
        return mapper.updateUser(userParam);
    }
}

最终记录操作日志:修改用户名为xxx

这个只是简易版,后续会完善注解解析,期待的效果 " 用户xxx 修改用户名 ‘aaa’ 为 ‘bbb’ "

后续会将使用SpEL实现日志项目完善提交到Github中,欢迎关注~
https://github.com/zsl0/zsl0-component

相关文章

实战!日志打印的15个好建议

如何优雅地记录操作日志?

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: ams-server-2022-07-21.rar 是一个文件的名称。这个文件是一个压缩包,文件格式为.rar。根据文件名可以推测,这个文件是关于"ams-server"的,日期为2022年7月21日。 "ams-server"代表了一个可能是"Attendance Management System Server"(考勤管理系统服务器)的缩写。这个服务器可能是一个用于管理和处理考勤系统数据的应用程序。 通过这个压缩包,我们可以猜测其中包含了与该考勤管理系统服务器相关的文件和数据。可能会有服务器的配置文件、源代码、数据库备份文件或其他相关的文档和资料。 如果需要使用这个压缩包,首先需要将其解压缩。通常情况下,我们可以使用压缩软件(比如WinRAR、7-Zip等)来解压缩RAR文件。在解压缩之后,我们可以查看其中的内容,并按照需求进行相应的操作。 总而言之, "ams-server-2022-07-21.rar"是一个压缩文件,可能包含了与考勤管理系统服务器相关的文件和数据。不同的应用场景可能需要不同的操作和处理。 ### 回答2: ams-server-2022-07-21.rar是一个文件的名称。根据名称可以猜测它是一个压缩文件,并且可能与ams服务器的某个版本或日期相关。根据“-2022-07-21”的部分,可以猜测这个压缩文件可能是为2022年7月21日的ams服务器版本制作的。 根据常见的命名规则,"ams-server"可能是指ams服务器的名称或缩写。服务器是一种计算机程序,用于提供服务、管理资源和处理请求。ams可能是一个特定的项目、软件或系统。压缩文件通常用于将多个文件或文件夹压缩成一个单独的文件,以便在网络上传输或存储时占用更少的空间。 因此,ams-server-2022-07-21.rar可能是一个存档了该日期的ams服务器相关文件的压缩文件。使用解压缩软件可以将压缩文件解压缩,以获得存档内的所有文件和文件夹。解压后的文件可能包含ams服务器的程序、配置文件、日志文件或其他与服务器操作相关的文件。 需要注意的是,这只是对文件名称的推测,实际内容可能有所不同。要确切了解ams-server-2022-07-21.rar文件的内容和用途,需要进一步查看文件、文档或与文件相关的信息。 ### 回答3: ams-server-2022-07-21.rar 是一个压缩文件,扩展名为.rar。根据名称可以推测,这个文件可能是一个AMS服务器的软件包,版本为2022年7月21日。rar 是一种流行的压缩格式,通常用于将多个文件或文件夹打包成一个单独的文件。通过解压缩该文件,我们可以获得其中的内容。 解压缩过程首先需要一个解压软件,如WinRAR或7-Zip等。我们可以将ams-server-2022-07-21.rar 文件拖放到解压软件的窗口中,或者使用软件的解压缩功能。解压缩后会生成一个或多个文件和文件夹,这些文件和文件夹可能包含了AMS服务器的程序文件、配置文件、文档等。 根据文件名中的日期,可以判断这个软件包是在2022年7月21日创建的。这可能表示该版本的AMS服务器在该日期之前是最新的,提供了一些新功能、修复或改进。用户可以下载和安装这个软件包,以使用其中提供的更新功能,或者修复现有版本的问题。 总之,ams-server-2022-07-21.rar 是一个AMS服务器的压缩软件包,文件名反映了其创建日期。通过解压缩该文件,我们可以访问其中的内容,并使用其中的文件来更新或改进现有的AMS服务器。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值