​Springboot​​之日志处理

一、使用​​log4j2​​进行日志管理

1. ​​log4j2​​日志管理框架的简介

​Log4j2​​​是​​log4j 1.x​​​和​​logback​​​的改进版,据说采用了一些新技术(无锁异步、等等),使得日志的吞吐量、性能比​​log4j 1.x​​​提高10倍,并解决了一些死锁的​​bug​​,而且配置更加简单灵活,官网地址: http://logging.apache.org/log4j/2.x/manual/configuration.html

2. 替换​​Springboot​​​默认的​​logback​​日志框架

先去掉​​Springboot​​自带的日志框架

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>

添加​​log4j2​​日志框架

<!--log4j2异步输出的依赖包-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>${com.lamx.version}</version>
</dependency>
<!-- Add Log4j2 Dependency -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>

二、配置日志处理的注解

为了可以灵活的输出日志,我们配置的日志注解来帮助我们。

import java.lang.annotation.*;

/**
* @author 墨龙吟
* @version 1.0.0
* @ClassName SysLog.java
* @Email 2354713722@qq.com
* @Description TODO 系统日志注解
* @createTime 2019年08月31日 - 10:51
*/
@Target(value = {ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SystemLog {

/**
* 方法描述
* @return
*/
String description() default "";

/**
* 是否打印入参,默认打印
*/
boolean inputParam() default true;

/**
* 是否打印入参,默认打印
* @return
*/
boolean outputParam() default true;

/**
* 错误提示
* @return
*/
String error() default "";
}

三、日志配置文件

这项目的​​resources​​​目录下,增加一个​​log4j2.xml​​文件,项目会自动识别为log4j2的配置文件的。

<?xml version="1.0" encoding="UTF-8"?>
<configuration status="OFF">
<Properties>
<Property name="LOG_HOME">${sys:user.dir}/logs</Property>
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%xEx</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_DATEFORMAT_PATTERN">yyyy-MM-dd HH:mm:ss.SSS</Property>
<Property name="CONSOLE_LOG_PATTERN">%clr{%d{${LOG_DATEFORMAT_PATTERN}}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
<Property name="FILE_LOG_PATTERN">%d{${LOG_DATEFORMAT_PATTERN}} ${LOG_LEVEL_PATTERN} %-5pid --- [%t] %-40.40c{1.} L%-4line : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
</Properties>
<appenders>
<Console name="Console" target="SYSTEM_OUT">
<!--<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>-->
<PatternLayout pattern="${sys:CONSOLE_LOG_PATTERN}"/>
</Console>
<!--处理DEBUG级别的日志,并把该日志放到logs/debug.log文件中-->
<!--打印出DEBUG级别日志,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
<RollingFile name="RollingFileDebug" fileName="${LOG_HOME}/debug.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/debug-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<ThresholdFilter level="debug"/>
<ThresholdFilter level="info" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="50 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>
<!--处理INFO级别的日志,并把该日志放到logs/info.log文件中-->
<RollingFile name="RollingFileInfo" fileName="${LOG_HOME}/info.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<!--只接受INFO级别的日志,其余的全部拒绝处理-->
<ThresholdFilter level="info"/>
<ThresholdFilter level="warn" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="10 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>
<!--处理WARN级别的日志,并把该日志放到logs/warn.log文件中-->
<RollingFile name="RollingFileWarn" fileName="${LOG_HOME}/warn.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log.gz">
<Filters>
<ThresholdFilter level="warn"/>
<ThresholdFilter level="error" onMatch="DENY" onMismatch="NEUTRAL"/>
</Filters>
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="10 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>
<!--处理error级别的日志,并把该日志放到logs/error.log文件中-->
<RollingFile name="RollingFileError" fileName="${LOG_HOME}/error.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/error-%d{yyyy-MM-dd}-%i.log.gz">
<ThresholdFilter level="error"/>
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="10 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>
<RollingRandomAccessFile name="ThreadFile" fileName="${LOG_HOME}/thread.log" filePattern="${LOG_HOME}/$${date:yyyy-MM}/fatal-%d{yyyy-MM-dd}-%i.log">
<Filters>
<ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level} [%t] %highlight{%c{1.}.%M(%L)} - %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="1024 MB" />
</Policies>
<DefaultRolloverStrategy max="20" />
</RollingRandomAccessFile>
<RollingRandomAccessFile name="TimeGaps" fileName="${LOG_HOME}/timeGaps.log" filePattern="${LOG_HOME}/$${date:yyyy-MM}/fatal-%d{yyyy-MM-dd}-%i.log">
<Filters>
<ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY" />
</Filters>
<PatternLayout pattern="%date{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level} [%t] %highlight{%c{1.}.%M(%L)} - %msg%n" />
<Policies>
<TimeBasedTriggeringPolicy />
<SizeBasedTriggeringPolicy size="1024 MB" />
</Policies>
<DefaultRolloverStrategy max="20" />
</RollingRandomAccessFile>
<!--druid的日志记录追加器-->
<RollingFile name="druidSqlRollingFile" fileName="${LOG_HOME}/druid-sql.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/druid-sql-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="50 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>
<!--慢SQL的日志记录追加器-->
<!-- dataSource 配置
<property name="filters" value="stat"/>
<property name="connectionProperties" value="druid.stat.logSlowSql=true;druid.stat.slowSqlMillis=5000"/>
-->
<RollingFile name="slowSqlRollingFile" fileName="${LOG_HOME}/slow-sql.log"
filePattern="${LOG_HOME}/$${date:yyyy-MM}/slow-sql-%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout pattern="${sys:FILE_LOG_PATTERN}"/>
<Policies>
<SizeBasedTriggeringPolicy size="50 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>
</appenders>
<loggers>
<root level="info">
<appender-ref ref="Console"/>
<appender-ref ref="RollingFileDebug"/>
<appender-ref ref="RollingFileInfo"/>
<appender-ref ref="RollingFileWarn"/>
<appender-ref ref="RollingFileError"/>
</root>
<!-- 线程池监控日志单独打印到一个文件中 -->
<Logger name="threadMonitorLog" level="info" additivity="false">
<AppenderRef ref="ThreadFile" />
</Logger>
<Logger name="timeGapsLog" level="debug" additivity="false">
<AppenderRef ref="TimeGaps" />
<AppenderRef ref="Console" />
</Logger>
<!--记录druid-sql的记录-->
<logger name="druid.sql" level="debug" additivity="false">
<appender-ref ref="druidSqlRollingFile"/>
<appender-ref ref="Console"/>
</logger>
<!--记录慢sql的记录-->
<logger name="com.alibaba.druid.filter.stat" level="warn" additivity="false">
<appender-ref ref="slowSqlRollingFile"/>
<appender-ref ref="Console"/>
</logger>
<!-- 自定义日志输出控制:开始 -->
<logger name="com.github.busi" level="info"/>
<logger name="org.springframework" level="info"/>
<logger name="org.springframework.context.annotation" level="warn"/>
<logger name="org.springframework.beans.factory.support" level="warn"/>
<logger name="org.springframework.core.env.PropertySourcesPropertyResolver" level="warn"/>
<logger name="org.springframework.beans.factory.annotation" level="warn"/>
<logger name="org.springframework.core.LocalVariableTableParameterNameDiscoverer" level="warn"/>
<logger name="org.springframework.core.annotation.AnnotationUtils" level="warn"/>
<logger name="org.springframework.web.servlet.handler" level="info"/>
<logger name="org.springframework.aop.framework" level="info"/>
<logger name="org.hibernate.validator.internal" level="info"/>
<logger name="org.springframework.jmx.export" level="info"/>
<logger name="org.springframework.core.env" level="info"/>
<logger name="springfox.documentation" level="warn"/>
<logger name="org.apache.ibatis.logging.jdbc" level="off"/>
<!-- 如果需要输出mybatis日志,开启以下配置(主要是dao包) -->
<!--
<logger name="org.apache.ibatis.logging.jdbc" level="debug"/>
<logger name="com.github.busi.mapper" level="debug"/>
-->
<!--log4j2 自带过滤日志-->
<Logger name="org.apache.catalina.startup.DigesterFactory" level="error"/>
<Logger name="org.apache.catalina.util.LifecycleBase" level="error"/>
<Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn"/>
<logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
<Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn"/>
<Logger name="org.crsh.plugin" level="warn"/>
<logger name="org.crsh.ssh" level="warn"/>
<Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error"/>
<Logger name="org.hibernate.validator.internal.util.Version" level="warn"/>
<logger name="org.thymeleaf" level="warn"/>
<logger name="org.springframework.core.env" level="warn"/>
<logger name="org.springframework.core.io.support" level="warn"/>
<logger name="org.springframework.web.filter" level="warn"/>
</loggers>
</configuration>

三、 使用​​AspectJ​​对方法进行切面处理

import com.alibaba.fastjson.JSON;
import com.cloud.app.config.annotation.SystemLog;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import org.slf4j.Logger;

import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Map;

/**
* @author 墨龙吟
* @version 1.0.0
* @ClassName SystemLogConfig.java
* @Email 2354713722@qq.com
* @Description TODO 日志处理AOP配置
* @createTime 2019年09月24日 - 09:25
*/
@Aspect
@Component
public class SystemLogConfig {

/** 将日志输出到日志记录器 */
private static final Logger logger = LoggerFactory.getLogger("timeGapsLog");

private final static String ERROR_INFO = " [ 异常信息: {} ] ";

private final static String UNKNOWN = "unknown";

private final static String DEFAULT_ADDR = "0:0:0:0:0:0:0:1";

private final static String DEFAULT_IP = "127.0.0.1";

/** 配置时间 */
private ThreadLocal<Long> timeThreadLocal;

/** 是否进行日志打印 */
private boolean debug;

/**
* 获取IP真实地址
* @param request
* @return
*/
private String getAddrIp(HttpServletRequest request) {
if (request != null) {
// 如果通过了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP值
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy - Client - IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || UNKNOWN.equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
if (StringUtils.isNoneBlank(ip) && !UNKNOWN.equalsIgnoreCase(ip)) {
ip = ip.split(",")[0].trim();
}
if (DEFAULT_ADDR.equalsIgnoreCase(ip)) {
ip = DEFAULT_IP;
}
return ip;
}
return "获取IP失败";
}

/**
* 获取request请求的信息
* @param joinPoint
*/
private void getRequestObj(JoinPoint joinPoint) {
Object[] args = joinPoint.getArgs();
HttpServletRequest request = null;
for (Object arg : args) {
if (arg instanceof HttpServletRequest) {
request = (HttpServletRequest) arg;
String url = request.getRequestURL().toString();
String ip = getAddrIp(request);
logger.info(" [ ip 地址: {} ]", ip);
logger.info(" [ 请求地址: {} ]", url);
break;
}
}
}

/**
* 获取文件描述
* @param joinPoint
* @return
*/
private void getMethodDescription(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
String description = method.getAnnotation(SystemLog.class).description();
if (StringUtils.isNoneEmpty(description)) {
logger.info(" [ 方法描述: {} ]", description);
} else {
logger.info(" [ 方法描述: {} ]", "无描述");
}
}

/**
* 获取方法请求参数
* @param joinPoint
*/
private void getMethodParam(ProceedingJoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
boolean inputParam = method.getAnnotation(SystemLog.class).inputParam();
if (inputParam) {
Object[] args = joinPoint.getArgs();
if (args != null && args.length > 0) {
for (Object arg : args) {
if (arg instanceof HttpServletRequest) {
HttpServletRequest request = (HttpServletRequest) arg;
String contentType = request.getContentType();
if (contentType != null && contentType.contains("application/json")) {
continue;
} else {
Map<String, String[]> parameterMap = request.getParameterMap();
logger.info(" [ 请求参数: {} ]", parameterMap);
break;
}
}
}
}
}
}

/**
* 在切点方法之前执行
* 前置通知 用于拦截Controller层记录用户的操作
* @param joinPoint 切点
*/
@Before(value = "@annotation(com.cloud.app.config.annotation.SystemLog)")
public void deBefore(JoinPoint joinPoint) {
timeThreadLocal = new ThreadLocal<>();
timeThreadLocal.set(System.currentTimeMillis());
try {
logger.info(" [ 执行方法: {}.{}() ]", joinPoint.getTarget().getClass().getName(), joinPoint.getSignature().getName());
// 获取请求信息
getRequestObj(joinPoint);
// 获取方法描述
getMethodDescription(joinPoint);
} catch (Exception e) {
logger.error(" [ @Before : 发生了异常 ] ");
logger.error(ERROR_INFO, e.getMessage());
}
}

/**
* 环绕通知:
* 注意:Spring AOP的环绕通知会影响到AfterThrowing通知的运行,不要同时使用
*
* 环绕通知非常强大,可以决定目标方法是否执行,什么时候执行,执行时是否需要替换方法参数,执行完毕是否需要替换返回值。
* 环绕通知第一个参数必须是org.aspectj.lang.ProceedingJoinPoint类型
*/
@Around(value = "@annotation(com.cloud.app.config.annotation.SystemLog)")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
// 获取方法参数
getMethodParam(proceedingJoinPoint);
return proceedingJoinPoint.proceed();
}

/**
* 切点方法返回后执行(后置增强), 方法正常退出时执行, 输出执行的输出结果
* @param joinPoint
* @param target
*/
@AfterReturning(value = "@annotation(com.cloud.app.config.annotation.SystemLog)", returning = "target")
public void doAfter(JoinPoint joinPoint,Object target) {
try {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
boolean outputParam = method.getAnnotation(SystemLog.class).outputParam();
if (outputParam) {
logger.info(" [ 返回结果: {} ]", JSON.toJSONString(target));
}
} catch (Exception e) {
logger.error(" [ @AfterReturning : 发生了异常 ] ");
logger.error(ERROR_INFO, e.getMessage());
}
}

/**
* 切点方法抛异常执行 (异常抛出增强), 对于方法发生异常之后, 执行异常输出
* @param joinPoint
* @param e
*/
@AfterThrowing(pointcut = "@annotation(com.cloud.app.config.annotation.SystemLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Throwable e) {
try {
logger.info("@AfterThrowing获得的信息:");
logger.error(ERROR_INFO, ExceptionUtils.getStackTrace(e));
} catch (Exception ex) {
//记录本地异常日志
logger.error(" [ @AfterThrowing: 发生了异常 ] ");
logger.error(ERROR_INFO ,ex.getMessage());
}
}

/**
* 在切点方法之后执行(不管是抛出异常或者正常退出都会执行), 打印执行方法的时间
* @param joinPoint
*/
@After(value = "@annotation(com.cloud.app.config.annotation.SystemLog)")
public void after(JoinPoint joinPoint) {
try {
logger.info(" [ 共耗时长: {} ms ]", System.currentTimeMillis() - timeThreadLocal.get());
} finally {
timeThreadLocal.remove();
}
}

}

四、对于​​AspectJ​​切面编程过程分析

1. 常用注解
@Before           在切点方法之前执行
@After 在切点方法之后执行
@AfterReturning 切点方法返回后执行
@AfterThrowing 切点方法抛异常执行
@Around 属于环绕增强,能控制切点执行前,执行后,用这个注解后,程序抛异常,会影响@AfterThrowing这个注解
2. 正常执行流程

Springboot之日志处理_log4j2

3. 异常执行流程

Springboot之日志处理_Aspect_02

4. 多个Aspect执行的流程

多个Aspect拦截一个方法,哪个Aspect先执行是随机的,如果需要定义顺序,可以使用@Order注解修饰Aspect类。值越小,优先级越高。

Springboot之日志处理_AOP_03

五、​​SpringAOP​​​和​​AspectJ​​的区别

1. ​​AOP​​介绍

​AOP(Aspect Orient Programming)​​​,作为面向对象编程的一种补充,广泛应用于处理一些具有横切性质的系统级服务,如日志收集、事务管理、安全检查、缓存、对象池管理等。​​AOP​​​实现的关键就在于AOP框架自动创建的​​AOP​​​代理,​​AOP​​​代理则可分为静态代理和动态代理两大类,其中静态代理是指使用​​AOP​​​框架提供的命令进行编译,从而在编译阶段就可生成 ​​AOP​​​ 代理类,因此也称为编译时增强;而动态代理则在运行时借助于​​JDK动态代理​​​、​​CGLIB​​​等在内存中“临时”生成​​AOP​​动态代理类,因此也被称为运行时增强。

面向切面的编程(​​AOP​​​) 是一种编程范式,旨在通过允许横切关注点的分离,提高模块化。​​AOP​​​提供切面来将跨越对象关注点模块化。虽然现在可以获得许多​​AOP​​​框架,但在这里我们要区分的只有两个流行的框架:​​Spring AOP​​​和​​AspectJ​​。

2. ​​Aspectj​​介绍

​AspectJ​​​是一个面向切面的框架,他定义了​​AOP​​​的一些语法,有一个专门的字节码生成器来生成遵守​​java​​​规范的 ​​class​​文件。

​AspectJ​​​的通知类型不仅包括我们之前了解过的三种通知:前置通知、后置通知、环绕通知,在​​Aspect​​中还有异常通知以及一种最终通知即无论程序是否正常执行,最终通知的代码会得到执行。

​AspectJ​​​提供了一套自己的表达式语言即切点表达式,切入点表达式可以标识切面织入到哪些类的哪些方法当中。只要把切面的实现配置好,再把这个切入点表达式写好就可以了,不需要一些额外的​​xml​​配置。

3. ​​SpringAOP​​介绍

​Spring AOP​​​也是对目标类增强,生成代理类。但是与​​AspectJ​​​的最大区别在于——​​Spring AOP​​​的运行时增强,而​​AspectJ​​是编译时增强。

六、 效果展示

1. 使用
/**
* @author 墨龙吟
* @version 1.0.0
* @ClassName HomeController.java
* @Email 2354713722@qq.com
* @Description TODO
* @createTime 2019年08月31日 - 10:49
*/
@RestController
@RequestMapping("/api")
public class HomeController {

@Autowired
private UserService userService;

@GetMapping("/all")
@SystemLog(description = "获取所有数据")
public ResultUtil all(HttpServletRequest request) {
List<UserEntity> list = userService.all();
return ResultUtil.ok.setMsg("成功").setData(list).setCode(HttpStatus.OK.value());
}
}
2. 结果:

Springboot之日志处理_log4j2_04

六、关注下公众号,感谢

Springboot之日志处理_spring_05