一、日志概述
日志记录了系统行为的时间、地点等很多细节的具体信息,在发生错误或者接近某种危险状态时能够及时提醒开发人员处理,往往在系统产生问题时承担问题定位与诊断和解决的重要角色。
开发阶段我们可以通过开发工具的控制台查看异常信息,但是一旦上线之后,很多问题只能通过进行日志分析才可以解决。所以日志功能在开发环节中是十分重要的。
1.1 常用的日志框架
常用的日志框架一般包括:Commons Logging、Slf4j、Log4j,Log4j2,Logback。
日志框架一般分为日志门面和对应的具体实现两部分,所谓日志门面就是门面模式的一个典型的应用,门面模式也称为外观模式,而日志门面框架就是一套提供了日志相关功能的接口而无具体实现的框架,日志记录是通过调用具体的实现框架来进行的。
典型的日志门面就是 Commons Logging、SLF4J,具体的实现就是Logback、Log4j 。比较常用的组合方式是 Slf4j 和 Logback 的组合使用,Commons Logging 与 Log4j 的组合使用,并且 Logback 必须得配合 Slf4j 使用。
1.2 Logback 简介
Logback 是由 log4j 的创始人编写的,但是性能上的话会比 log4j 更好一些,他主要分为有三个模块:
- logback-core:核心代码模块。
- logback-classic:log4j 的一个改良版本,同时实现了 slf4j 的接口。
- logback-access:这是 logback 的访问模块,它与 Servlet 容器集成并且提供通过 Http 来访问日志的功能。
二、SpringBoot整合Logback框架
2.1 关于依赖
在spring boot项目中,我们不用再单独引入相关依赖,因为在spring boot项目的启动依赖中包含了该日志框架的相关依赖,spring-boot-starter 就已经包含了 spring-boot-starter-logging,而 spring-boot-starter-logging 中引用了 Logback 的依赖。
🛎️ 总结:
- SpringBoot 底层使用slf4j+logback的方式进行日志记录
- SpringBoot 把其他的日志都替换成了slf4j
2.2.添加配置文件
在resources文件夹下面添加logback的配置文件来配置logback信息:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<contextName>community</contextName>
<property name="LOG_PATH" value="./logs"/>
<property name="APPDIR" value="community"/>
<!-- 错误信息保存文件 file -->
<appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APPDIR}/log_error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APPDIR}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>5MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<append>true</append>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>error</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 警告信息文件 file -->
<appender name="FILE_WARN" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APPDIR}/log_warn.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APPDIR}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>5MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<append>true</append>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- info file -->
<appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APPDIR}/log_info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/${APPDIR}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>5MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<maxHistory>30</maxHistory>
</rollingPolicy>
<append>true</append>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>info</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- console -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
<charset>utf-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>debug</level>
</filter>
</appender>
<logger name="com.greate.community" level="debug"/>
<root level="info">
<appender-ref ref="FILE_ERROR"/>
<appender-ref ref="FILE_WARN"/>
<appender-ref ref="FILE_INFO"/>
<appender-ref ref="STDOUT"/>
</root>
</configuration>
2.3.在springboot配置文件中添加日志配置文件路径
spring.profiles.active = develop
# logback
logging.config=classpath:logback-spring-${spring.profiles.active}.xml
这里需要注意的是,我这里日志配置文件的名称为logback-spring-develop.xml ,上面的写法是为了方便在开发环境和生产环境之间切换。
三、SpringBoot项目使用AOP拦截请求日志信息
3.1 关于AOP
Aop的整体内容主要包含以下几点:
Pointcut:切点,主要是决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为 execution 方式和 annotation 方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。
Advice:处理(通知),包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。这也回答了我们之前提到的什么时候切入,切入过后应该要做哪些事情。
Aspect:切面,即 Pointcut 和 Advice,在代码中就是我们定义的这个切面的类,选择在什么地方织入。
Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 AOP 中,一个连接点总是代表着一个方法执行。
Weaving:织入,就是通过动态代理的方式在目标对象方法中执行处理内容的过程。
3.2 添加Aop依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3.3 添加AOP配置
package com.softeem.car.aspect;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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.text.SimpleDateFormat;
import java.util.Date;
import java.util.Objects;
/**
* 统一日志记录
*/
@Component
@Aspect
public class ServiceLogAspect {
private static final String START_TIME = "request-start";
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
/**
* 将service业务层中所有的方法作为切入点
*/
@Pointcut("execution(* com.softeem.car.service.*.*(..))")
public void pointcut() {
}
/**
* 前置操作
* @param joinPoint
*/
@Before("pointcut()")
public void before(JoinPoint joinPoint) {
// 用户[IP 地址], 在某个时间访问了 [com.softeem.car.service.xxx]
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return ;
}
HttpServletRequest request = attributes.getRequest();
String ip = request.getRemoteHost();
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
String target = joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName();
logger.info(String.format("用户[%s], 在[%s], 访问了[%s].", ip, time, target));
}
}
接下来编写控制器,调用service中的任意方法,都会触发记录日志:
同时也会在对应的目录中自动生成日志文件: