SpringBoot使用slf4j+logback配合AOP做日志记录

需要大致了解:java日志基础,如核心组件Loggers,Appenders,Layouts的用处、SpringAOP概念

为什么需要日志

当应用程序部署到服务器上运行时,用户在使用过程中可能会出现各种错误。这时应用程序将错误信息生成日志,就方便了开发人员快速定位错误和根源,从而进行有针对的维护。所以,在大型应用程序中,日志记录是必不可少的。

选择日志框架

目前市面上可供选择的日志框架非常多,如JCL、SLF4J、Jboss-logging、jUL、log4j、log4j2、logback等,首先要分清楚 [日志抽象层] 和 [日志实现]。 这两者的关系可以参考设计模式中的“门面模式”。 我们在开发中调用日志记录方法时,不应直接调用日志实现类的方法,而是调用日志抽象层的方法。这样方便解耦,以后想更换别的日志实现时,可以直接改动配置文件的信息,而不用修改一行代码。 那么如何选择日志框架呢?

  • 日志抽象层:JCL(Jakarta Commons Logging), SLF4j(Simple Logging Facade for Java), jboss-logging
  • 日志实现:Log4j, JUL(java.util.logging), Log4j2, Logback

关于如何选择网络上有很多文章分析,在此不赘述。结论就是SLF4J更受开发者青睐,事实上《阿里java开发手册》上也规定:应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架
SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

至于选择日志实现,log4j是很常用的,但其作者又写了log4j的升级版logback,相比log4j有更好的性能。有诸多理由让我们选择logback,使用好logback关键的一点就是配置好logback.xml文件,可参阅logback使用和配置详解

maven引入

1
2
3
4
5
6
7
8
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
</dependency>

SpringBoot已默认使用slf4j和logback 无需引入对应依赖。

如何插入日志记录

使用SpringAOP,目的是让开发者专注于业务逻辑而无需关心在哪里插入日志,并且可以降低日志记录操作对业务代码的侵入性。
这里我们使用 AspectJ 的几个注解来写一个切面类TestAspect.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import com.example.demo.annotation.RequestColor;
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;

@Aspect
@Component("testAspect")
public class TestAspect {
private static final Logger logger = LoggerFactory.getLogger(TestAspect.class);
//controller包切点
@Pointcut("execution(* com.example.demo.controller.*.*(..))")
public void controllerPointCut() {
}

//TestController切点
@Pointcut("execution(* com.example.demo.controller.TestController.*(..))")
public void testControllerPointCut() {
}

//具体方法 ayahiro 切点
@Pointcut("execution(* com.example.demo.controller.TestController.ayahiro()))")
public void ayahiroPointCut() {
}

@Before(value = "testControllerPointCut()")
public void doBefore(JoinPoint joinPoint){
System.out.println("form: TestAspect---->>");
String className=joinPoint.getSignature().getDeclaringTypeName();
String methodName=joinPoint.getSignature().getName();
System.out.println("doBefore拦截了"+className+"."+methodName);
}

@After(value = "@annotation(requestColor)")
public void doAfter(JoinPoint joinPoint, final RequestColor requestColor){
System.out.println("form: TestAspect---->>");
String className=joinPoint.getSignature().getDeclaringTypeName();
String methodName=joinPoint.getSignature().getName();
System.out.println("doAfter拦截了"+className+"."+methodName);
System.out.println("requestType: "+ requestColor.type());
}

@Around(value = "ayahiroPointCut()")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable{
Object re=joinPoint.proceed(); //执行了ayahiro方法 返回了String
System.out.println(re);
System.out.println("form: TestAspect---->>");
String className=joinPoint.getSignature().getDeclaringTypeName();
String methodName=joinPoint.getSignature().getName();
System.out.println("doAround拦截了"+className+"."+methodName);
return re;
}

@AfterReturning(value = "testControllerPointCut()")
public void doAfterReturning(JoinPoint joinPoint){
System.out.println("form: TestAspect---->>");
String className=joinPoint.getSignature().getDeclaringTypeName();
String methodName=joinPoint.getSignature().getName();
System.out.println("doAfterReturning拦截了"+className+"."+methodName);
}

@AfterThrowing(value = "testControllerPointCut()")
public void doAfterThrowing(JoinPoint joinPoint){
System.out.println("form: TestAspect---->>");
String className=joinPoint.getSignature().getDeclaringTypeName();
String methodName=joinPoint.getSignature().getName();
System.out.println("doAfterThrowing拦截了"+className+"."+methodName);
}
}

以及Controller,有两个返回字符串的测试方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import com.example.demo.annotation.RequestColor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

@RequestColor(type = RequestColor.Type.YELLOW)
@RequestMapping(path = {"/ayahiro"},method = {RequestMethod.GET})
public String ayahiro() throws Exception{
//int num=2/0;
return "this ayahiro";
}

@RequestMapping(path = {"/moonKa"},method = {RequestMethod.GET})
public String moonKa(){
return "this moonKa";
}
}

介绍几个常用的注解

  • @Aspect 表明这个类是“切面类”,切面类就是用来定义切点和切点处要增强功能的方法
  • @Pointcut 这个注解包含两部分,PointCut表达式和PointCut签名。表达式是用来确定切入点的位置的,说白了就是通过一些规则来确定,哪些方法是要增强的,也就是要拦截哪些方法。注解括号里的部分就是描述切点的位置,有很多种方法来确定,代码中使用的execution表达式是其中的一种,其语法和其他描述方法可自行百度。 签名就是被注解的方法名,签名没有实际用处,只是用来标记一个Pointcut,可以理解成这个切入点的一个记号。
  • @Before 顾名思义,即在切入点处方法执行前,执行此方法。同下面的@After,@Around,@AfterReturning, @AfterThrowing注解类似,都是规定了在何时(相对于待增强方法)执行被注解的方法。只不过注解属性有所区别
  • JoinPoint 代表着织入增强处理的连接点。注意一点:除了注解@Around的方法外,其他都可以加这个JoinPoint作参数,@Around注解的方法的参数一定要是ProceedingJoinPoint。 JoinPoint包含了几个很有用的参数:
    • Object[] getArgs:返回目标方法的参数
    • Signature getSignature:返回目标方法的签名
    • Object getTarget:返回被织入增强处理的目标对象
    • Object getThis:返回AOP框架为目标对象生成的代理对象

运行效果

理解了几个注解的作用后,通过运行结果,来看看测试方法都被哪些增强方法拦截了
启动后,在浏览器输入http://localhost:8080/ayahiro

可以看到,ayahiro()被所有增强方法拦截了。testControllerPointCut()和ayahiroPointCut()拦截不难理解,都前者是划定了一个范围,后者是直接具体定位到该方法。其中@After(value = “@annotation(requestColor)”) 的拦截方式比较特别,是通过自定义注解拦截的,因为ayahiro()被@RequestColor修饰,而@After拦截所有被@RequestColor修饰的方法。
输入http://localhost:8080/moonKa

可以看到@After就没有拦截moonKa方法,因为该方法没有被@RequestColor修饰。

使用日志

理解了AOP的思想之后,再结合slf4j记录日志就显得非常简单,调用日志方法只需要声明一个 private static final Logger logger = LoggerFactory.getLogger(当前类.class); 再用loger去调用具体的方法:.info() .warn() .debug() .error()即可~

参考资料:

0%