背景:java
我须要在一个SpringBoot的项目中的每一个controller加入一个日志记录,记录关于请求的一些信息。web
代码相似于:spring
logger.info(request.getRequestUrl());编程
之类的。浏览器
代码不难,但因为Controller的数量很多,干起来也是体力活。因此想到了用Spring AOP来解决这个问题。session
首先,在pom中加入SpringAOP的相关依赖:mvc
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
上一篇咱们说到,若是要直接用@Aspect注解的话,要在spring的配置文件中加入框架
<aop:aspectj-autoproxy />spring-boot
那么咱们这里要不要在程序的主类中增长@EnableAspectJAutoProxy来启用呢? 实际并不须要,能够看下面关于AOP的默认配置属性,其中spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增长了@EnableAspectJAutoProxy。ui
好的也就是说,只要引入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包含了几个颇有用的参数:
除了注解@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分配优先级,这样就能够决定谁先谁后了。
优先级有两种方式:
不论是哪一种,都是order的值越小越先执行:
@Order(5) @Component @Aspect public class Aspect1 { // ... } @Order(6) @Component @Aspect public class Aspect2 { // ... }
这样Aspect1就永远比Aspect2先执行了。
注意点:
参考过的博客:
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类的相关介绍