[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引入

<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

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,有两个返回字符串的测试方法

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()即可~

参考资料:
https://www.cnblogs.com/wangshen31/p/9379197.html
https://blog.csdn.net/caychen/article/details/80112915

发表评论

电子邮件地址不会被公开。 必填项已用*标注