SpringBoot使用Slf4j+Log4j完成项目的日志记录
前言
本示例采用SpringBoot项目使用SpringAOP记录日志,Slf4j作为日志门面,Log4j2作为日志实现实,实现开发中的日志记录.
部分效果展示 :
日志文件 :
日志信息 :
代码具体实现如下 :
- 因为SpringBoot自动集成了Slf4j日志门面并且同样集成了logback等日志实现,Log4j2和Logback并不能共存,所以我们要先排除依赖,并添加Log4j2的依赖与SpringAOP的依赖。
避坑 : 在网上有很多人说是在 spring-boot-starter-web 这个启动器里面进行依赖排除,但是经过我的测试这种方法有时候并不是有效的,所以复制上面的依赖就好。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><exclusions><!-- 排除springboot自带的logback框架 --><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><!-- 引入log4j2依赖 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency><!-- SpringAOP启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId><version>2.4.5</version></dependency>
二、编写log4j2-spring.xml : log4j2的配置文件
代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<!--status = "warn" 日志框架本身的输入日志级别monitorInterval = "5" 自动加载配置文件的时间间隔不低于5s-->
<Configuration status="warn" monitorInterval="5"><!-- 日志级别以及优先级排序 :在log4j2中, 共有8个级别,按照从低到高为:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF。All:最低等级的,用于打开所有日志记录.Trace:是追踪,就是程序推进一下.Debug:指出细粒度信息事件对调试应用程序是非常有帮助的.Info:消息在粗粒度级别上突出强调应用程序的运行过程.Warn:输出警告及warn以下级别的日志.Error:输出错误信息日志.Fatal:输出每个严重的错误事件将会导致应用程序的退出的日志.OFF:最高等级的,用于关闭所有日志记录.程序会打印高于或等于所设置级别的日志,设置的日志等级越高,打印出来的日志就越少。--><!--集中配置属性进行管理--><Properties><!--定义格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符%logger{36} 表示 Logger 名字最长36个字符--><property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level}[%thread] %style{%logger{36}}{cyan} : %msg%n" /><!-- 定义日志存储的路径,绝对路径 --><property name="FILE_PATH" value="存储的绝对路径" /><property name="FILE_NAME" value="项目名称" /></Properties><!--日志处理--><Appenders><!--*********************控制台日志***********************--><!--target: SYSTEM_OUT 或 SYSTEM_ERR,一般只设置默认:SYSTEM_OUT.--><console name="Console" target="SYSTEM_OUT"><!--输出日志的格式和颜色--><PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false" noConsoleNoAnsi="false"/><!--控制台只输出level及其以上级别的信息(onMatch)放行,其他的直接拒绝(onMismatch)--><ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/></console><!--*********************文件日志***********************--><!--按照一定规则查分日志文件的appender /logs/$${date:yyyy-MM-dd}/myrollog-%d{yyyy-MM-dd-HH-mm}-%i.log/logs:放在logs这个目录下,/$${date:yyyy-MM-dd}:以天为单位生成文件夹myrollog-%d{yyyy-MM-dd-HH-mm}-%d: 以分钟为单位到达了指定大小在进行拆分.gz 进行压缩归档--><!--error 运行时异常日志信息--><RollingFile name = "errorRollingFile" fileName = "${FILE_NAME}/error日志.log"filePattern = "${FILE_PATH}/$${date:yyyy-MM-dd}/error-%d{yyyy-MM-dd-HH-mm}-%i.log.gz"><!--日志级别过滤器,文件只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--><ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/><!--日志的消息格式--><PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%-5level] %l %c{36} - %msg%n"/><!--在系统每次启动时,触发拆分规则,生产一个新的日志文件--><OnStartupTriggeringPolicy/><!--按照文件大小进行拆分--><SizeBasedTriggeringPolicy size = "10 MB"/><!--按照时间节点进行拆分--><TimeBasedTriggeringPolicy/><!--在同一个目录下,文件的个数限定为30个,超过按照实际进行覆盖--><DefaultRolloverStrategy max="30"/></RollingFile><!--fatal 正常运行时日志--><RollingFile name = "fatalRollingFile" fileName = "${FILE_NAME}/fatal日志.log"filePattern = "${FILE_PATH}/$${date:yyyy-MM-dd}/fatal-%d{yyyy-MM-dd-HH-mm}-%i.log.gz"><!--日志级别过滤器,文件只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--><ThresholdFilter level="fatal" onMatch="ACCEPT" onMismatch="DENY"/><!--日志的消息格式--><PatternLayout pattern="${LOG_PATTERN}"/><!--在系统每次启动时,触发拆分规则,生产一个新的日志文件--><OnStartupTriggeringPolicy/><!--按照文件大小进行拆分--><SizeBasedTriggeringPolicy size = "10 MB"/><!--按照时间节点进行拆分--><TimeBasedTriggeringPolicy/><!--在同一个目录下,文件的个数限定为30个,超过按照实际进行覆盖--><DefaultRolloverStrategy max="30"/></RollingFile><!--info 操作日志--><RollingFile name = "infoRollingFile" fileName = "${FILE_NAME}/info日志.log"filePattern = "${FILE_PATH}/$${date:yyyy-MM-dd}/info-%d{yyyy-MM-dd-HH-mm}-%i.log.gz"><!--日志级别过滤器,文件只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--><ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/><!--日志的消息格式--><PatternLayout pattern="${LOG_PATTERN}"/><!--在系统每次启动时,触发拆分规则,生产一个新的日志文件--><OnStartupTriggeringPolicy/><!--按照文件大小进行拆分--><SizeBasedTriggeringPolicy size = "10 MB"/><!--按照时间节点进行拆分--><TimeBasedTriggeringPolicy/><!--在同一个目录下,文件的个数限定为30个,超过按照实际进行覆盖--><DefaultRolloverStrategy max="30"/></RollingFile></Appenders><!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。--><!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效--><loggers><!--过滤掉spring和mybatis的一些无用的信息--><logger name="org.mybatis" level="info" additivity="false"><AppenderRef ref="Console"/></logger><!--监控系统信息--><!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。--><Logger name="org.springframework" level="info" additivity="false"><AppenderRef ref="Console"/></Logger><root level="info"><!--控制台--><appender-ref ref="Console"/><!--用户操作文件--><appender-ref ref="infoRollingFile"/><!--调试错误文件--><appender-ref ref="errorRollingFile"/><!--正常运行文件--><appender-ref ref="fatalRollingFile"/></root></loggers></Configuration>
三、添加全局异步日志 : log4j2.component.properties
Log4j2的最大优点就是它的异步Logger,主要就是性能更好,这个这里不做过多解释。
#全局异步日志开启,提高日志性能
Log4jContextSelector=org.apache.logging.log4j.core.async.AsyncLoggerContextSelector
四、声明log4j2的配置文件路径 :
在application.properties的SpringBoot主配置文件中添加如下配置 :
我这里是给配置文件添加了个config的包目录,你要改成你自己的路径。
五、自定义注解类 :
自定义注解 : 主要是作用就是为了自定义方法操作,用于向AOP中添加方法操作的日志信息,既然已经到项目日志阶段那么我相信你也已经不是一个小白了,注解就是一个标注这里也不做过多解释。
//作用在方法上
@Target(ElementType.METHOD)
//运行时
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotationMethod {//模块名称String module() default "";//操作名称String operator() default "";//扩展属性String value() default "";
}
六、创建HttpContextUtil工具类用于在IP工具类中获取IP使用 :
public class HttpContextUtil {public static HttpServletRequest getHttpServletRequest(){return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();}}
七、创建IP获取工具类 :
用于获取访问的用户IP,并记录到日志中。
@Slf4j
public class IpUtils {/*** 获取IP地址* <p>* 使用Nginx等反向代理软件, 则不能通过request.getRemoteAddr()获取IP地址* 如果使用了多级反向代理的话,X-Forwarded-For的值并不止一个,而是一串IP地址,X-Forwarded-For中第一个非unknown的有效IP字符串,则为真实IP地址*/public static String getIpAddr(HttpServletRequest request) {String ip = null, unknown = "unknown", seperator = ",";int maxLength = 15;try {ip = request.getHeader("x-forwarded-for");if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {ip = request.getHeader("Proxy-Client-IP");}if (StringUtils.isEmpty(ip) || ip.length() == 0 || unknown.equalsIgnoreCase(ip)) {ip = request.getHeader("WL-Proxy-Client-IP");}if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_CLIENT_IP");}if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {ip = request.getHeader("HTTP_X_FORWARDED_FOR");}if (StringUtils.isEmpty(ip) || unknown.equalsIgnoreCase(ip)) {ip = request.getRemoteAddr();}} catch (Exception e) {log.error("IpUtils ERROR ", e);}// 使用代理,则获取第一个IP地址if (StringUtils.isEmpty(ip) && ip.length() > maxLength) {int idx = ip.indexOf(seperator);if (idx > 0) {ip = ip.substring(0, idx);}}return ip;}/*** 获取ip地址** @return*/public static String getIpAddr() {HttpServletRequest request = HttpContextUtil.getHttpServletRequest();return getIpAddr(request);}
}
八、 编写AOP日志切面类
此类用于保存和执行具体的日志信息。
/*** @author 码不多* @version 1.0* @description: 此类采用SpringAOP,用于记录日志*///声明AOP类
@Aspect
//声明组件
@Component
//使用Slf4j日志门面
@Slf4j
public class AopLogUtil {//定义切入点方法 标注这个自定义注解@Pointcut("@annotation(com.shouzhong.epidemicprevention.annotation.LogAnnotationMethod)")public void pt(){}/*** 功能描述: 执行通知日志的方法* @author 码不多* @date 2021/8/16* @param point* @return java.lang.Object*///拦截所有的Controller或者RestController或者自定义注解标识的方法@Around("@within(org.springframework.stereotype.Controller) ||"+"@within(org.springframework.web.bind.annotation.RestController) ||"+"pt()")private Object runAndSaveLog(ProceedingJoinPoint point){//获取获取当前时间long beginTime = System.currentTimeMillis();//执行原始方法Object result = null;try {result = point.proceed();} catch (Throwable throwable) {//出现异常打印error日志log.error("Aop中方法出现异常",throwable);}//获取方法执行时间long runtime = System.currentTimeMillis() - beginTime;//调用保存日志的方法recordLog(point,runtime);//将结果返回return result;}/*** 功能描述: 保存日志的方法* @author 码不多* @date 2021/8/16* @param joinPoint time* @return void*/private void recordLog(ProceedingJoinPoint joinPoint,long time){//获取类名Object target = joinPoint.getTarget();String canonicalName = target.getClass().getCanonicalName();//获取模块名MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();Method method = methodSignature.getMethod();//获取方法名Signature signature = joinPoint.getSignature();String methodName = signature.getName();//获取请求参数Object[] args = joinPoint.getArgs();String params = JSON.toJSONString(args[0]);//获取request,设置ip地址HttpServletRequest request = HttpContextUtil.getHttpServletRequest();//获取IP地址String ipAddr = IpUtils.getIpAddr(request);//获取注解对象LogAnnotationMethod logAnnotation = method.getAnnotation(LogAnnotationMethod.class);//添加类名到日志,采用占位符赋值log.info("类名: {}",canonicalName);//添加模块日志信息,采用占位符赋值,通过注解对象获取注解中的值log.info("模块名: {}",logAnnotation.module());//添加方法名到日志,采用占位符赋值log.info("方法名: {}",methodName);//添加操作到日志,采用占位符赋值,通过注解对象获取注解中的值log.info("操作: {}",logAnnotation.operator());//添加请求参数信息到日志,采用占位符赋值log.info("请求参数: {}:",params);//添加ip地址到日志,采用占位符赋值log.info("ip地址: {}",ipAddr);//添加执行时间到日志,采用占位符赋值log.info("执行时间: {} ms",time);//日志结束log.info("#####################log End####################");补充001 ://不使用IPUtils获取ip和一些其他请求头日志信息的方法: logger.info不在这里定义。还是在上面的代码中/* // 接收请求,记录请求中的内容ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 记录下请求内容//请求的urllogger.info("URL : " + request.getRequestURL().toString());//请求的方法类型 : GET、POST...logger.info("HTTP_METHOD : " + request.getMethod());//IP地址logger.info("IP : " + request.getRemoteAddr());//类方法logger.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());//参数数组logger.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));//获取所有参数方法 ://获取请求参数们Enumeration<String> enu=request.getParameterNames();//遍历while(enu.hasMoreElements()){//取出请求参数名String paraName=(String)enu.nextElement();//请求的具体参数值System.out.println(paraName+": "+request.getParameter(paraName));}}*/补充002:/*//如果有文件上传的参数MultipartFile类型,为了避免冲突可以给它们添加进集合展示// 接收请求,记录请求中的内容ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();//创建集合存储请求参数ArrayList paramsList = new ArrayList();//获取所有参数方法 :Enumeration<String> enu=request.getParameterNames();while(enu.hasMoreElements()){String paraName=(String)enu.nextElement();//将参数添加到集合paramsList.add(paraName+": "+request.getParameter(paraName));}log.info("请求参数 :{}",paramsList)*/}
}
九、控制器日志添加
在你需要记录操作日志的Controller层的请求映射的方法上添加你的自定义注解,并将你的方法日志信息添加到你自定义的属性值中。
//自定义注解,声明方法日志@LogAnnotationMethod(module = "获得信息列表",operator = "分页查询或多条件查询信息需求列表")@RequestMapping("/InforDS")public Pagination<InforDemandSide> inforDemandSide(Pagination pagination){xxxxxxxxxxxxxxxxx;}
十、开启控制台Mybatis的sql输出 :
在application.properties的主配置文件中添加如下配置