zoukankan      html  css  js  c++  java
  • 在SpringBoot中用SpringAOP实现日志记录功能

    背景:

    我需要在一个SpringBoot的项目中的每个controller加入一个日志记录,记录关于请求的一些信息。

    代码类似于:

      logger.info(request.getRequestUrl());

    之类的。

    代码不难,但由于Controller的数量不少,干起来也是体力活。所以想到了用Spring AOP来解决这个问题。

    首先,在pom中加入SpringAOP的相关依赖:

    <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>

    上一篇我们说到,如果要直接用@Aspect注解的话,要在spring的配置文件中加入

    <aop:aspectj-autoproxy />

    那么我们这里要不要在程序的主类中增加@EnableAspectJAutoProxy来启用呢?      实际并不需要,可以看下面关于AOP的默认配置属性,其中spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy。

    好的也就是说,只要引入SpringAOP相关的jar包依赖,我们就可以开始相关的Aspet的编程了。

    这里直接上代码,然后再做解释:

    首先是包结构的图:

    这里涉及到接收请求的Controller的包有两个,com.stuPayment.controller还有com.stuPayment.uiController

    然后看我们的切面类WebLogAspect类的代码:

    package com.stuPayment.util;
    
    import java.util.Arrays;
    
    import javax.servlet.http.HttpServletRequest;
    
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.AfterReturning;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestAttributes;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    
    @Aspect
    @Component
    public class WebLogAspect {
        
        private final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);
        
        @Pointcut("execution(public * com.stuPayment.controller..*.*(..))")//切入点描述 这个是controller包的切入点
        public void controllerLog(){}//签名,可以理解成这个切入点的一个名称
        
        @Pointcut("execution(public * com.stuPayment.uiController..*.*(..))")//切入点描述,这个是uiController包的切入点
        public void uiControllerLog(){}
        
        @Before("controllerLog() || uiControllerLog()") //在切入点的方法run之前要干的
        public void logBeforeController(JoinPoint joinPoint) {
            
            
            RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();//这个RequestContextHolder是Springmvc提供来获得请求的东西
            HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
            
             // 记录下请求内容
            logger.info("################URL : " + request.getRequestURL().toString());
            logger.info("################HTTP_METHOD : " + request.getMethod());
            logger.info("################IP : " + request.getRemoteAddr());
            logger.info("################THE ARGS OF THE CONTROLLER : " + Arrays.toString(joinPoint.getArgs()));
            
            //下面这个getSignature().getDeclaringTypeName()是获取包+类名的   然后后面的joinPoint.getSignature.getName()获取了方法名
            logger.info("################CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
            //logger.info("################TARGET: " + joinPoint.getTarget());//返回的是需要加强的目标类的对象
            //logger.info("################THIS: " + joinPoint.getThis());//返回的是经过加强后的代理类的对象
    
        }
       
    
    }

     针对这个切面类,来展开说明@Aspect切面类的编程。

    @Aspect和@Component

    首先,这个@Aspect注释告诉Spring这是个切面类,然后@Compoment将转换成Spring容器中的bean或者是代理bean。 总之要写切面这两个注解一起用就是了。

    既然是切面类,那么肯定是包含PointCut还有Advice两个要素的,下面对几个注解展开讲来看看在@Aspect中是怎么确定切入点(PointCut)和增强通知(Advice)的。

    @PointCut

    这个注解包含两部分,PointCut表达式和PointCut签名。表达式是拿来确定切入点的位置的,说白了就是通过一些规则来确定,哪些方法是要增强的,也就是要拦截哪些方法。

    @PointCut(...........)括号里面那些就是表达式。这里的execution是其中的一种匹配方式,还有:

    execution: 匹配连接点
    
    within: 某个类里面
    
    this: 指定AOP代理类的类型
    
    target:指定目标对象的类型
    
    args: 指定参数的类型
    
    bean:指定特定的bean名称,可以使用通配符(Spring自带的)
    
    @target: 带有指定注解的类型
    
    @args: 指定运行时传的参数带有指定的注解
    
    @within: 匹配使用指定注解的类
    
    @annotation:指定方法所应用的注解

     注意,由于是动态代理的实现方法,所以不是所有的方法都能拦截得下来,对于JDK代理只有public的方法才能拦截得下来,对于CGLIB只有public和protected的方法才能拦截。

    这里我们主要介绍execution的匹配方法,因为大多数时候都会用这个来定义pointcut:

    execution(modifiers-pattern? ret-type-pattern declaring-type-pattern?name-pattern(param-pattern)
                throws-pattern?)
    
    execution(方法修饰符(可选)  返回类型  类路径 方法名  参数  异常模式(可选)) 

    除了返回类型,方法名还有参数之外,其他都是可选的

     ret-type-pattern:可以为*表示任何返回值,全路径的类名等.

    name-pattern:指定方法名,*代表所以,set*,代表以set开头的所有方法.
    parameters pattern:指定方法参数(声明的类型),         ()匹配没有参数;  (..)代表任意多个参数;   (*)代表一个参数,但可以是任意型;    (*,String)代表第一个参数为任何值,第二个为String类型。

    下面给几个例子:

    1)execution(public * *(..))——表示匹配所有public方法
    2)execution(* set*(..))——表示所有以“set”开头的方法
    3)execution(* com.xyz.service.AccountService.*(..))——表示匹配所有AccountService接口的方法
    4)execution(* com.xyz.service.*.*(..))——表示匹配service包下所有的方法
    5)execution(* com.xyz.service..*.*(..))——表示匹配service包和它的子包下的方法

    然后其他的匹配法要用的时候再百度吧~

    然后是@PointCut的第二个部分,签名signature,也就是代码中的

    @Pointcut("execution(public * com.stuPayment.uiController..*.*(..))")//切入点描述,这个是uiController包的切入点
        public void uiControllerLog(){}

    像方法定义的这个Public void uiControllerLog(){}这个看起来像是方法定义的东西,就是签名,签名没有实际用处,只是用来标记一个Pointcut,可以理解成这个切入点的一个记号。 

    @Before

    这个是决定advice在切入点方法的什么地方执行的标签,这个注解的意思是在切入点方法执行之前执行我们定义的advice。

    @Before("controllerLog() || uiControllerLog()") //在切入点的方法run之前要干的
        public void logBeforeController(JoinPoint joinPoint) {

     @Before注解括号里面写的是一个切入点,这里看见切入点表达式可以用逻辑符号&&,||,!来描述。  括号里面也可以内置切点表达式,也就是直接写成:

    @Before("execution(public * com.stuPayment.uiController..*.*(..))")

     跟写成@Before("uiControllerLog()")的效果是一样的。

    然后看到注解下面的方法,就是描述advice的,我们看到有个参数JoinPoint,这个东西代表着织入增强处理的连接点。JoinPoint包含了几个很有用的参数:

    • Object[] getArgs:返回目标方法的参数
    • Signature getSignature:返回目标方法的签名
    • Object getTarget:返回被织入增强处理的目标对象
    • Object getThis:返回AOP框架为目标对象生成的代理对象

    除了注解@Around的方法外,其他都可以加这个JoinPoint作参数。@Around注解的方法的参数一定要是ProceedingJoinPoint,下面会介绍。

    @After

    这个注解就是在切入的方法运行完之后把我们的advice增强加进去。一样方法中可以添加JoinPoint。

    @Around

    这个注解可以简单地看作@Before和@After的结合。这个注解和其他的比比较特别,它的方法的参数一定要是ProceedingJoinPoint,这个对象是JoinPoint的子类。我们可以把这个看作是切入点的那个方法的替身,这个proceedingJoinPoint有个proceed()方法,相当于就是那切入点的那个方法执行,简单地说就是让目标方法执行,然后这个方法会返回一个对象,这个对象就是那个切入点所在位置的方法所返回的对象。

    除了这个Proceed方法(很重要的方法),其他和那几个注解一样。

    @AfterReturning

    顾名思义,这个注解是在目标方法正常完成后把增强处理织入。这个注解可以指定两个属性(之前的三个注解后面的括号只写一个@PointCut表达式,也就是只有一个属性),一个是和其他注解一样的PointCut表达式,也就是描述该advice在哪个接入点被织入;然后还可以有个returning属性,表明可以在Advice的方法中有目标方法返回值的形参。

    @AfterReturning(returning = "returnOb", pointcut = "controllerLog() || uiControllerLog()")
        public void doAfterReturning(JoinPoint joinPoint, Object returnOb) {
            System.out.println("##################### the return of the method is : " + returnOb);
        }

    浏览器发出一个请求后,效果截图:

    (这里是一个请求登录界面的请求,所以uicontroller返回一个String作为视图。)

    @AfterThrowing

    异常抛出增强,在异常抛出后织入的增强。有点像上面的@AfterReturning,这个注解也是有两个属性,pointcut和throwing。

    用法也和刚刚的那个returning差不多:

    @AfterThrowing(pointcut = "controllerLog() || uiControllerLog()", throwing = "ex")
    public void doAfterThrowing(JoinPoint joinPoint, Exception ex) {
            String methodName = point.getSignature().getName();
            List<Object> args = Arrays.asList(point.getArgs());
            System.out.println("连接点方法为:" + methodName + ",参数为:" + args + ",异常为:" + ex);
              
    }    

    好了现在注解都介绍完了,这里还要提到上面用到的一个类:RequestContextHolder

    比如说,有个需求需要在service中获得request和response,我们一般会(我就是)直接在controller那把request或response作为参数传到service,这就很不美观。后来知道,原来SpringMVC提供了个很强大的类ReqeustContextHolder,通过他你就可以获得request和response什么的。

    //下面两个方法在没有使用JSF的项目中是没有区别的
    RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
    //                                            RequestContextHolder.getRequestAttributes();
    //从session里面获取对应的值
    String str = (String) requestAttributes.getAttribute("name",RequestAttributes.SCOPE_SESSION);
     
    HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
    HttpServletResponse response = ((ServletRequestAttributes)requestAttributes).getResponse();

     好了完成了这个切面的编程后,你就成功把日志功能切入到各个controller中了。看个效果图。

    最后,再记录一下各个不同的advice的拦截顺序的问题。

    情况一,只有一个Aspect类:

      无异常:@Around(proceed()之前的部分) → @Before → 方法执行 → @Around(proceed()之后的部分) → @After → @AfterReturning

      有异常:@Around(proceed(之前的部分)) → @Before → 扔异常ing → @After → @AfterThrowing    (大概是因为方法没有跑完抛了异常,没有正确返回所有@Around的proceed()之后的部分和@AfterReturning两个注解的加强没有能够织入)

    情况二,同一个方法有多个@Aspect类拦截:

      单个Aspect肯定是和只有一个Aspect的时候的情况是一样的,但不同的Aspect里面的advice的顺序呢??答案是不一定,像是线程一样,没有谁先谁后,除非你给他们分配优先级,同样地,在这里你也可以为@Aspect分配优先级,这样就可以决定谁先谁后了。

    优先级有两种方式:

    • 实现org.springframework.core.Ordered接口,实现它的getOrder()方法
    • 给aspect添加@Order注解,该注解全称为:org.springframework.core.annotation.Order

    不管是哪种,都是order的值越小越先执行:

    @Order(5)
    @Component
    @Aspect
    public class Aspect1 {
        // ...
    }
    
    @Order(6)
    @Component
    @Aspect
    public class Aspect2 {
        // ...
    }

    这样Aspect1就永远比Aspect2先执行了。

    注意点:

    • 如果在同一个 aspect 类中,针对同一个 pointcut,定义了两个相同的 advice(比如,定义了两个 @Before),那么这两个 advice 的执行顺序是无法确定的,哪怕你给这两个 advice 添加了 @Order 这个注解,也不行。这点切记。
    • 对于@Around这个advice,不管它有没有返回值,但是必须要方法内部,调用一下 pjp.proceed();否则,Controller 中的接口将没有机会被执行,从而也导致了 @Before这个advice不会被触发。

    参考过的博客:

    https://blog.csdn.net/bombsklk/article/details/79143145    SpringBoot用AOP实现日志

    https://blog.csdn.net/loongshawn/article/details/72303040?utm_source=tuicool&utm_medium=referral     execution表达式

    https://blog.csdn.net/caomiao2006/article/details/51287200   advice中获取参数的方法

    https://blog.csdn.net/rainbow702/article/details/52185827   advice的顺序

    https://blog.csdn.net/zzy7075/article/details/53559902    RequestContextHolder类的相关介绍

     

  • 相关阅读:
    Software Solutions CACHE COHERENCE AND THE MESI PROTOCOL
    CACHE COHERENCE AND THE MESI PROTOCOL
    Multiprocessor Operating System Design Considerations SYMMETRIC MULTIPROCESSORS
    Organization SYMMETRIC MULTIPROCESSORS
    PARALLEL PROCESSING
    1分钟内发送差评邮件
    Secure Digital
    SYMMETRIC MULTIPROCESSORS
    A Taxonomy of Parallel Processor Architectures
    parallelism
  • 原文地址:https://www.cnblogs.com/wangshen31/p/9379197.html
Copyright © 2011-2022 走看看