1.AOP基本概念
切面(Aspect)
:通知(advice)和切入点(pointcut)共同组成了切面(aspect)
切入点(Pointcut)
:匹配join point的谓词,切面切向哪里,某个类或者某一层包路径
连接点(Joinpoint)
:aop拦截的类或者方法,例如方法被调用时、异常被抛出时。
(在Spring中,所有的方法都可以认为是joinpoint,但是我们不希望所有的方法都添加Advice,而
pointcut的作用就是提供一组规则来匹配joinpoint
,给满足规则的joinpoint添加Advice。)
通知(Advice)
:切面何时使用,即注有@Around、@Before、@After等注解的方法
目标对象(Target Object)
:被通知的对象
AOP代理(AOP Proxy)
: AOP的代理有两种,一种是JDK动态代理,一种是CGLIB代理。默认情况下,TargetObject实现了接口时,则采用JDK动态代理;反之,采用CGLIB代理
Joinpoint和Pointcut区别:
在Spring AOP中,所有的方法执行都是joinpoint,而pointcut是一个描述信息,它修饰的是joinpoint,通过pointcut可以确定哪些jointpoint可以被Advice。
2.通知(Advice)类型说明
@Around
:环绕通知,包围一个连接点的通知,可以在核心方法前后完成自定义的行为。这是
最常用最重要的
。这个注解需要传入参数
ProceedingJoinPoint pjp
,决定是否进入核心方法—-
调用pjp.proceed();
如果不调的话将不进入核心方法!
@Before
:前通知,核心代码执行前通知
@After
:后通知,连接点执行退出时通知(不论正常返回还是异常退出)
@AfterReturning
:返回后通知,正常返回后通知
@AfterThrowing
:抛出异常后通知,抛出异常时通知
注意
:除了@Around传的参数是
ProceedingJoinPoint pjp外,
其它都是传的
JoinPoint jp
,也就是说能控制是否进入核心代码的只有Around,
因为aop走到核心代码就是通过调用ProceedingJoinPoint的proceed()方法
,而JoinPoint没有这个方法。
3.Advice顺序:
情况一:一个方法只被一个Aspect类拦截
添加PointCut类 添加Aspect类
这两个可以放在一起
package com.bob.hello.aspect;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
/**
* Description
*
* @author Bob
* @date 2020/6/2
**/
@Component
@Aspect
public class DemoAspect {
/**
* @description 拦截com.bob.hello包下所有方法
* @author Bob
* @date 2020/6/2
*/
@Pointcut(value = "within(com.bob.hello.controller.*)")
public void demoPointCut() {
}
@Before(value = "demoPointCut()")
public void before(JoinPoint joinPoint) {
System.out.println("[DemoAspect] before advice");
}
@Around(value = "demoPointCut()")
public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
System.out.println("[DemoAspect] around advice 1");
proceedingJoinPoint.proceed();
System.out.println("[DemoAspect] around advice 2");
}
@After(value = "demoPointCut()")
public void after(JoinPoint joinPoint) {
System.out.println("[DemoAspect] after advice");
}
@AfterThrowing(value = "demoPointCut()")
public void afterThrowing(JoinPoint joinPoint) {
System.out.println("[DemoAspect] afterThrowing advice");
}
@AfterReturning(value = "demoPointCut()")
public void afterReturning(JoinPoint joinPoint) {
System.out.println("[DemoAspect] afterReturning advice");
}
}
添加测试用的Controller
package com.bob.hello.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Description
*
* @author Bob
* @date 2020/5/26
**/
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
System.out.println("hello");
return "hello";
}
}
测试 正常情况
浏览器直接输入:
http://localhost:8080/hello
输出的结果:
[DemoAspect] around advice 1
[DemoAspect] before advice
hello
[DemoAspect] around advice 2
[DemoAspect] after advice
[DemoAspect] afterReturning advice
注意:如果用到了Spring Security,会先走登录拦截
结论
在一个方法只被一个aspect类拦截时,aspect类内部的 advice 将按照以下的顺序进行执行:
情况二:同一个方法被多个Aspect类拦截
aspect1和aspect2的执行顺序是未知的,除非设置他们的Order顺序,具体参见:
Spring AOP @Before @Around @After 等 advice 的执行顺序
4.定义Aspect(切面)
当使用@Aspect标注一个bean后,Spring框架会自动收集这些bean,并添加到Spring AOP中,例如:
@Component
@Aspect
public class DemoAspect {
}
注意
:仅仅使用@Aspect 注解并不能将一个Java对象转换为bean,因此还需要类似@Component 之类的注解。
注意
:如果一个类被@Aspect 标注,则这个类就不能是其它aspect的**advised object** 了,因为使用@Aspect后,这个类就会别排除在auto-proxying机制之外。
5.声明Pointcut(切入点)
在@AspectJ 风格的AOP中,这样来描述pointcut,例如:
@Pointcut(value = "within(com.bob.hello.controller.*)")
public void demoPointCut() {
}
value里面的值叫
pointcut(切点)表达式
,它由
标识符
和
操作参数
组成,within就是标识符,()里的就是操作参数。
6.组合切入点表达式
AspectJ使用 且(&&)、或(||)、非(!)来组合切入点表达式
常见的切点表达式
execution:使用”execution(方法表达式)”匹配方法执行
// 任何公共方法的执行
public * *(..)
// 匹配指定包中的所有的方法
execution(* com.xys.service.*(..))
// 匹配当前包中的指定类的所有方法
execution(* UserService.*(..))
// 匹配指定包中的所有 public 方法
execution(public * com.xys.service.*(..))
// 匹配指定包中的所有 public 方法, 并且返回值是 int 类型的方法
execution(public int com.xys.service.*(..))
// 匹配指定包中的所有 public 方法, 并且第一个参数是 String, 返回值是 int 类型的方法
execution(public int com.xys.service.*(String name, ..))
// 匹配所有带@Scheduled 注解的方法
execution(@org.springframework.scheduling.annotation.Scheduled * *(..))
within:使用”within(类型表达式)”匹配指定类型内的方法执行
// 匹配指定包中的所有的方法, 但不包括子包
within(com.xys.service.*)
// 匹配指定包中的所有的方法, 包括子包
within(com.xys.service..*)
// 匹配当前包中的指定类中的方法
within(UserService)
// 匹配一个接口的所有实现类中的实现的方法
within(UserDao+)
@within:使用”@within(注解类型)”匹配所以持有指定注解类型内的方法;注解类型也必须是全限定类型名;
// 匹配注解
@within(org.springframework.web.bind.annotation.RestController) || @within(org.springframework.stereotype.Controller) || @within(org.springframework.stereotype.Service) || @within(org.springframework.stereotype.Component)
//任何目标对象对应的类型持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用
@within(cn.javass.spring.chapter6.Secure)
@annotation:使用”@annotation(注解类型)”匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名;
提示:括号里的”注解类型”不是接口!而是”@interface”!!
// 当前执行方法上持有注解 cn.javass.spring.chapter6.Secure将被匹配
@annotation(cn.javass.spring.chapter6.Secure )
匹配Bean名字
// 匹配以指定名字结尾的 Bean 中的所有方法
bean(*Service)
切点表达式组合
// 匹配以 Service 或 ServiceImpl 结尾的 bean
bean(*Service || *ServiceImpl)
// 匹配名字以 Service 开头, 并且在包 com.xys.service 中的 bean
bean(*Service) && within(com.xys.service.*)
execution通常用与匹配方法,within通常用于匹配特定注解
6.声明Advice(通知)
示例中的@Before @Around等
7.Spring AOP实战
HTTP接口鉴权
核心代码:
@Component
@Aspect
public class HttpAopAdviseDefine {
// 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}
// 定义 advise
@Around("pointcut()")
public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
.getRequest();
// 检查用户所传递的 token 是否合法
String token = getUserToken(request);
if (!token.equalsIgnoreCase("123456")) {
return "错误, 权限不合法!";
}
return joinPoint.proceed();
}
private String getUserToken(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return "";
}
for (Cookie cookie : cookies) {
if (cookie.getName().equalsIgnoreCase("user_token")) {
return cookie.getValue();
}
}
return "";
}
}
方法调用日志
核心代码:
@Component
@Aspect
public class LogAopAdviseDefine {
private Logger logger = LoggerFactory.getLogger(getClass());
// 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
@Pointcut("within(NeedLogService)")
public void pointcut() {
}
// 定义 advise
@Before("pointcut()")
public void logMethodInvokeParam(JoinPoint joinPoint) {
logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
}
@AfterReturning(pointcut = "pointcut()", returning = "retVal")
public void logMethodInvokeResult(JoinPoint joinPoint, Object retVal) {
logger.info("---After method {} invoke, result: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
}
@AfterThrowing(pointcut = "pointcut()", throwing = "exception")
public void logMethodInvokeException(JoinPoint joinPoint, Exception exception) {
logger.info("---method {} invoke exception: {}---", joinPoint.getSignature().toShortString(), exception.getMessage());
}
}
方法耗时统计
核心代码:
@Component
@Aspect
public class ExpiredAopAdviseDefine {
private Logger logger = LoggerFactory.getLogger(getClass());
// 定义一个 Pointcut, 使用 切点表达式函数 来描述对哪些 Join point 使用 advise.
@Pointcut("within(SomeService)")
public void pointcut() {
}
// 定义 advise
// 定义 advise
@Around("pointcut()")
public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 开始
Object retVal = pjp.proceed();
stopWatch.stop();
// 结束
// 上报到公司监控平台
reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());
return retVal;
}
public void reportToMonitorSystem(String methodName, long expiredTime) {
logger.info("---method {} invoked, expired time: {} ms---", methodName, expiredTime);
//
}
}
参考:
Spring AOP @Before @Around @After 等 advice 的执行顺序
spring aop的@Before,@Around,@After,@AfterReturn,@AfterThrowing的理解