一、日志
1、简介
市面上的日志框架:JUL、JCL、jboss-logging、logback、log4j、log4j2、self4j。。。。
日志门面 | 日志实现 |
JCL( Jakarta Commons Logging) SLF4j( Simple Logging Facade for Java) jboss-logging | Log4j JUL( java.util.logging) Log4j2 Logback |
左边是一个门面(抽象层),右边是一个实现
日志门面:SLF4J
日志实现:Logback
Springboot:底层是Spring框架,Spring框架默认是使用JCL;然而Springboot改选了SLF4J和Logback。
2、SLF4J使用
1)如何在系统中使用 SLF4J ?
以后在开发中,日志记录方法的调用不应该直接使用日志的实现类,而是调用抽象层里面的方法;
给系统中导入 slf4j 的 jar 包和 Logback 的 jar 包
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}
官方文档图示:
每个日志文件实现的框架都有自己的配置文件。使用slf4j以后,配置文件还是做成日志实现框架自己本身的配置文件。
2)问题
一个系统的开发不可以只依赖于一个东西,如A系统中使用了(slf4j+Logback) 的日志记录,但是系统还会使用Spring(commons-logging)、Hibernate(jboss-loggin)、Mybatis
那么怎么统一进行日志记录,即别的框架和我一起使用使用 slf4j 进行日志输出?
官网图示:
如何让系统中所有的日志都统一到slf4j?
- 将系统中的其他日志框架先排除;
- 用中间包来替换原有的日志框架;
- 我们导入slf4j其他实现
3、Springboot日志关系
Springboot使用它做日志功能:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.0.4.RELEASE</version>
<scope>compile</scope>
</dependency>
底层的依赖关系
总结:
- Springboot底层也是使用slf4j+Logback进行日志记录
- Springboot也把其他日志换成了slf4j
- 中间替换包
@SuppressWarnings("rawtypes") public abstract class LogFactory { static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J = "http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j"; static LogFactory logFactory = new SLF4JLogFactory();
- 如果我们要引入其他矿建,一定要把这个框架中的默认日志依赖移除掉
<dependency> <groupId>org.springframework</groupId> <artifactId>spring‐core</artifactId> <exclusions> <exclusion> <groupId>commons‐logging</groupId> <artifactId>commons‐logging</artifactId> </exclusion> </exclusions> </dependency>
Springboot能够自动适配所有的日志,而且底层是使用slf4j+logback的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除即可。
4、日志使用
1)默认配置
Springboot默认帮我们配置好了日志;
//日志记录器
Logger logger = LoggerFactory.getLogger(getClass());
@Test
public void contextLoads() {
/**
* 日志的级别,由地到高,可以调整输出的日志级别,就只会打印这个级别及以上的
* Springboot默认使用的是inform级别的
*/
logger.trace("这是trace跟踪日志。。。。");
logger.debug("这是debug调试日志。。。。");
logger.info("这是通inform通知日志。。。。");
logger.warn("这是warm警告日志。。。。。");
logger.error("这是error错误日志。。。。");
}
可以在application.properties中修改Springboot日志的默认设置
#日志级别
logging.level.com.smart.springboot=trace
#不指定路径:在当前目录下生成springboot.log日志
#也可以指定路劲
#logging.file=springboot.log
#在当前磁盘的根目录下创建spring和log文件夹;使用默认的spring.log日志文件
logging.path=/spring/log
#在控制台输出的日志格式
logging.pattern.console=%d{yyyy‐MM‐dd} [%thread] %‐5level %logger{50} ‐ %msg%n
#指定在文件中输出的格式
logging.pattern.file=%d{yyyy‐MM‐dd}===[%thread]===%‐5level %logger{50} ==== %msg%n
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%‐5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
‐‐>
%d{yyyy‐MM‐dd HH:mm:ss.SSS} [%thread] %‐5level %logger{50} ‐ %msg%n
logging.file | logging.path | Example | Description |
(none) | (none) | 只在控制台输出 | |
指定文件名 | (none) | my.log | 输出日志到my.log文件 |
(none) | 指定目录 | /var/log | 输出到指定目录的 spring.log 文件中 |
2)指定配置
给类路径下放上每个日志框架自己的配置文件,然后Springboot就不使用自己默认的配置了。
Logging System | Customization |
Logback | logback-spring.xml , logback-spring.groovy , logback.xml or logback.groovy |
Log4j2 | log4j2-spring.xml or log4j2.xml |
JDK (Java Util Logging) | logging.properties |
logback.xml:直接就被框架识别了
logback-spring.xml:日志框架就不直接加载日志的配置项,由Springboot解析日志配置,可以使用Springboot的高级Profile功能。
<springProfile name="staging">
<!‐‐ configuration to be enabled when the "staging" profile is active ‐‐>
可以指定某段配置只在某个环境下生效
</springProfile>
如果没有这个名字,但是添加了<springProfile>标签,就会报错:no applicable action for [springProfile]
比如:
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!‐‐
日志输出格式:
%d表示日期时间,
%thread表示线程名,
%‐5level:级别从左显示5个字符宽度
%logger{50} 表示logger名字最长50个字符,否则按照句点分割。
%msg:日志消息,
%n是换行符
‐‐>
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%d{yyyy‐MM‐dd HH:mm:ss.SSS} ‐‐‐‐> [%thread] ‐‐‐> %‐5level
%logger{50} ‐ %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy‐MM‐dd HH:mm:ss.SSS} ==== [%thread] ==== %‐5level
%logger{50} ‐ %msg%n</pattern>
</springProfile>
</layout>
</appender>
5、切换日志框架
可以按照slf4j日志适配图,进行相关切换
slf4j+log4j的方式
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐web</artifactId>
<exclusions>
<exclusion>
<artifactId>logback‐classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
<exclusion>
<artifactId>log4j‐over‐slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j‐log4j12</artifactId>
</dependency>
切换为log4j2
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring‐boot‐starter‐log4j2</artifactId>
</dependency>
二、使用AOP进行统一日志处理请求
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
在完成了依赖之后,一般来说不需要再做任何配置。也许在Spring中使用过注解的方式的人会问是否需要在程序的主类中增加 @EnableAspectJAutoProxy 来启用,实际上不需要。
下面是关于AOP的默认配置
# AOP
spring.aop.auto=true # Add @EnableAspectJAutoProxy.
spring.aop.proxy-target-class=false # Whether subclass-based (CGLIB) proxies are to be created (true) as
opposed to standard Java interface-based proxies (false).
也就是说spring.aop.auto属性默认是开启的,也就是只需要引入了AOP依赖之后,默认增加了 @EnableAspectJAutoProxy。而当我们需要使用CGLIB来实现AOP的时候,需要配置 spring.aop.proxy-target-class=true,不然默认使用的是java的实现。
aop实现日志的切面
- 使用@Aspect注解将一个java类定义为切面
- 使用@Pointcut定义一个切点,可以是一个规则的表达式
- 根据需要在切入点不同位置的切入内容
- 使用@Before在切入点开始位置切入内容
- 使用@After在切入点结尾处切入内容
- 使用@AfterReturning在切入点return内容之后切入内容(可以用来对返回值做一些加工处理)
- 使用@Around在切入点前后切入内容,并自己控制何时执行切入点自身的内容
- 使用@AfterThrowing用来处理当切入内容部分抛出异常之后的处理逻辑
@Aspect
@Component
public class LogAspect {
Logger logger = LoggerFactory.getLogger(getClass());
@Pointcut(value = "execution(* com.smart.springboot.controller.*.*(..))")
public void log(){}
@Before(value = "log()")
public void doBefore(JoinPoint joinPoint){
//接收请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//记录下请求的内容
logger.info("URL:"+request.getRequestURI());
logger.info("HTTP_METHOD:"+request.getMethod());
logger.info("IP:"+request.getRemoteAddr());
logger.info("CLASS_METHOD:"+joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName());
logger.info("ARGS:"+ Arrays.asList(joinPoint.getArgs()));
}
@AfterReturning(pointcut = "log()",returning = "res")
public void doAfterReturning(Object res){
logger.info("Response:"+res);
}
}
注意:切点表达式一定要正确,否则不起作用或者报错。
优化:AOP切面同步问题
在切面中,通过 doBefore 和 doAfterReturning 连个独立函数实现了切点头部和返回内容的处理。如果想要得到消耗时间需要在两个函数中独立获取当前时间,然后计算得出。但是直接使用基本类型会出现线程同步问题,因此我们可以引入ThreadLocal 对象进行处理:
@Aspect
@Component
public class LogAspect {
Logger logger = LoggerFactory.getLogger(getClass());
ThreadLocal<Long> threadLocal = new ThreadLocal<>();
@Pointcut(value = "execution(* com.smart.springboot.controller.*.*(..))")
public void log(){}
@Before(value = "log()")
public void doBefore(JoinPoint joinPoint){
threadLocal.set(System.currentTimeMillis());
//接收请求,记录请求内容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//记录下请求的内容
logger.info("URL:"+request.getRequestURI());
logger.info("HTTP_METHOD:"+request.getMethod());
logger.info("IP:"+request.getRemoteAddr());
logger.info("CLASS_METHOD:"+joinPoint.getSignature().getDeclaringTypeName()+"."+joinPoint.getSignature().getName());
logger.info("ARGS:"+ Arrays.asList(joinPoint.getArgs()));
}
@AfterReturning(pointcut = "log()",returning = "res")
public void doAfterReturning(Object res){
logger.info("Response:"+res);
logger.info("SPEEDTIME:"+(System.currentTimeMillis()-threadLocal.get()));
}
}
如果有多个切面日志可以设置不同的优先级使用 @Order(i) 来标识优先级。i 的值越小,优先级越高。
假设我们还有一个checkAspect的切面类,我们设置 @Order(10),之前的LogAspect类设置 @Order(5),所以LogAspect类有更高的优先级:执行顺序如下:
- 在 @Before 中优先执行 @Order(5) 的内容再执行 @Order(10)的内容;
- 在 @After和@AfterReturning中优先执行 @Order(10)的内容,再执行 @Order(5)的内容
总结: