在SpringBoot中使用Aop功能实现日志功能

实现背景

主要是为了熟悉Aop的主要注解及功能,给项目的Controller层加上日志

首先在pom文件里面加入aop依赖

<dependency>
        <groupId>org.springframework.bootgroupId>
        <artifactId>spring-boot-starter-aopartifactId>
dependency>

相比入ssm,springboot简化了很多的注解
直接看切面类

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
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;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * 为controller层编写了一个切面aop日志文件
 *
 * @Aspect表示这是一个切面
 * @Component
 */
@Aspect
@Component
public class WebLogAspect {
    private final Logger logger = LoggerFactory.getLogger(WebLogAspect.class);

    /**
     * 定义一个controller包的切入点,包含表达式和表达式签名
     * execution(方法修饰符(可选)  返回类型  类路径 方法名  参数  异常模式(可选))
     */
    @Pointcut("execution(public * com.springboot.richttms.controller..*.*(..))")
    public void controllerLog(){ }//这个方法其实就是签名,没有实际用处,用来标记一个pointcut

    //在切入点的run方法之前执行这个方法
    @Before("controllerLog()")
    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());//返回的是经过加强后的代理类的对象
    }
	//返回参数的返回值
    @AfterReturning(pointcut = "controllerLog()",returning = "returnOb")
    public void doAfterReturning(JoinPoint joinPoint, Object returnOb) {
        System.out.println("##################### the return of the method is : " + returnOb.toString());
    }
}

打印出来的日志

2019-07-27 16:08:16.455  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################URL : http://localhost:8080/scheduleController/schedule/8813
2019-07-27 16:08:16.455  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################HTTP_METHOD : GET
2019-07-27 16:08:16.456  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################IP : 0:0:0:0:0:0:0:1
2019-07-27 16:08:16.456  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################THE ARGS OF THE CONTROLLER : [8813]
2019-07-27 16:08:16.459  INFO 13112 --- [nio-8080-exec-2] c.s.richttms.utils.WebLogAspect          : ################CLASS_METHOD : com.springboot.richttms.controller.schedule.scheduleController.queryBycId
2019-07-27 16:08:16.826  INFO 13112 --- [nio-8080-exec-2] io.lettuce.core.EpollProvider            : Starting without optional epoll library
2019-07-27 16:08:16.829  INFO 13112 --- [nio-8080-exec-2] io.lettuce.core.KqueueProvider           : Starting without optional kqueue library
##################### the return of the method is : Data{data={movie=[Movie{movieId=346559, movieName='黑衣人:全球追缉', movieType='动作', movieActor='克里斯·海姆斯沃斯,泰莎·汤普森,丽贝卡·弗格森', movieAlltime='110', sc='0.0', img='http://p0.meituan.net/w.h/moviemachine/262f95bad79b6ae45b978593157cb68550938.jpg'}, Movie{movieId=1204589, movieName='绝杀慕尼黑', movieType='动作', movieActor='弗拉基米尔·马什科夫,约翰·萨维奇,伊万·科列斯尼科夫', movieAlltime='110', sc='0.0', img='http://p0.meituan.net/w.h/movie/67044d5479f075a18adba35571cadc4f978021.jpg'}, Movie{movieId=1207185, movieName='秦明·生死语者', movieType='悬疑', movieActor='严屹宽,代斯,耿乐', movieAlltime='110', sc='0.0', img='http://p0.meituan.net/w.h/movie/313ed0afcdb4d1cc37cd12b402e9e4421137394.jpg'}]}}

@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,

public void controllerLog(){ }//这个方法其实就是签名,没有实际用处,用来标记一个pointcut

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

@Before("controllerLog()")
    public void logBeforeController(JoinPoint joinPoint){} 

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

@Before("execution(public * com.springboot.richttms.controller..*.*(..))")

然后看到注解下面的方法,就是描述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(pointcut = "controllerLog()",returning = "returnOb")
    public void doAfterReturning(JoinPoint joinPoint, Object returnOb) {
        System.out.println("##################### the return of the method is : " + returnOb.toString());
    }

再记录一下各个不同的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 {
    // ...
}

定义一个自定义的注解类

import java.lang.annotation.*;

/**
 * 自定义一个注解类(连接点)
 */
@Target(ElementType.METHOD)//指明了修饰的这个注解的使用范围,具体根据ElementType来定
@Retention(RetentionPolicy.RUNTIME)//指明修饰注解的生存周期
@Documented//指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。
@Inherited//y允许子类继承父类
public @interface Log {

    /**
     * 方法名
     */
    public String name() default "";

    /**
     * 描述
     */
    public String description() default "no description";

}

@Target 注解
功能:指明了修饰的这个注解的使用范围,即被描述的注解可以用在哪里。

ElementType的取值包含以下几种:

TYPE:类,接口或者枚举

FIELD:域,包含枚举常量

METHOD:方法

PARAMETER:参数

CONSTRUCTOR:构造方法

LOCAL_VARIABLE:局部变量

ANNOTATION_TYPE:注解类型

PACKAGE:包

@Retention 注解
功能:指明修饰的注解的生存周期,即会保留到哪个阶段。

RetentionPolicy的取值包含以下三种:

SOURCE:源码级别保留,编译后即丢弃。

CLASS:编译级别保留,编译后的class文件中存在,在jvm运行时丢弃,这是默认值。

RUNTIME: 运行级别保留,编译后的class文件中存在,在jvm运行时保留,可以被反射调用。

@Documented 注解
功能:指明修饰的注解,可以被例如javadoc此类的工具文档化,只负责标记,没有成员取值。

@Inherited注解
功能:允许子类继承父类中的注解。
使用

@Log(name = "getValue")

大部分内容都是来自这篇博客;
https://www.cnblogs.com/wangshen31/p/9379197.html

你可能感兴趣的:(Java,springboot)