AOP(Aspect Oriented Programming)
,面向切面思想,是Spring的三大核心思想之一(其余两个:IOC - 控制反转
、DI - 依赖注入
)。
那么AOP为何那么重要呢?
在我们的程序中,经常存在一些系统性的需求,比如 权限校验
、日志记录
、统计
等,这些代码会散落穿插在各个业务逻辑中,非常冗余且不利于维护,那么面向切面编程往往让我们的开发更加低耦合,也大大减少了代码量,同时呢让我们更专注于业务模块的开发,把那些与业务无关的东西提取出去,便于后期的维护和迭代。
简单地去理解,其实AOP要做三类事:
在哪里切入
,也就是权限校验等非业务操作在哪些业务代码中执行。
在什么时候切入
,是业务代码执行前还是执行后。
切入后做什么事
,比如做权限校验、日志记录等。
AOP的体系图:
一些概念:
概念 | 说明 |
---|---|
Pointcut | 切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。切点分为execution 方式和 annotation 方式。前者可以用路径表达式指定哪些类织入切面,后者可以指定被哪些注解修饰的代码织入切面。 |
Advice | 处理,包括处理时机和处理内容。处理内容就是要做什么事,比如校验权限和记录日志。处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。 |
Aspect | 切面,即 Pointcut 和 Advice 。 |
Joint point | 连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。 |
Weaving | 织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。 |
如何创建详见:IDEA 创建 SpringBoot 项目
<dependency><groupId>org.springframework.bootgroupId><artifactId>spring-boot-starter-aopartifactId>
dependency>
package com.cw.tsb.app.aspect;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;&#64;Component
&#64;Aspect
public class ControllerAspect {&#64;Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法&#xff0c;而不做任何特殊的处理。}&#64;Around("pointCut()")public Object doAround(ProceedingJoinPoint joinPoint) {System.out.println("------------- doAround.");Object obj &#61; null;try {obj &#61; joinPoint.proceed();} catch (Throwable t){t.printStackTrace();}return obj;}&#64;After("pointCut()")public void doAfter(JoinPoint joinPoint){System.out.println("------------- doAfter.");}&#64;Before("pointCut()")public void doBefore(JoinPoint joinPoint){System.out.println("------------- doBefore.");}/*** 后置返回* 如果第一个参数为JoinPoint&#xff0c;则第二个参数为返回值的信息* 如果第一个参数不为JoinPoint&#xff0c;则第一个参数为returning中对应的参数* returning&#xff1a;限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知&#xff0c;否则不执行&#xff0c;* 参数为Object类型将匹配任何目标返回值*/&#64;AfterReturning(value &#61; "pointCut()", returning &#61; "result")public void doAfterReturning(JoinPoint joinPoint, String result){System.out.println("doAfterReturning result &#61; " &#43; result);}&#64;AfterThrowing(value &#61; "pointCut()", throwing &#61; "t")public void doAfterThrowing(JoinPoint joinPoint, Throwable t){System.out.println("------------- doAfterThrowing throwable &#61; " &#43; t.toString());}
}
该注解要添加在类上&#xff0c;声明这是一个切面类&#xff0c;使用时需要与&#64;Component注解一起用&#xff0c;表明同时将该类交给spring管理。
&#64;Component
&#64;Aspect
public class ControllerAspect {
}
用来定义一个切点&#xff0c;即上文中所关注的某件事情的入口&#xff0c;切入点定义了事件触发时机。
该注解需要添加在方法上&#xff0c;该方法签名必须是 public void
类型&#xff0c;可以将&#64;Pointcut
中的方法看作是一个用来引用的助记符&#xff0c;因为表达式不直观&#xff0c;因此我们可以通过方法签名的方式为此表达式命名。因此 &#64;Pointcut 中的方法只需要方法签名&#xff0c;而不需要在方法体内编写实际代码
。
该注解有两个常用的表达式&#xff1a;execution()
和 annotation()
。
&#64;Aspect
&#64;Component
public class ControllerAspect {&#64;Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法&#xff0c;而不做任何特殊的处理。}
}
表达式为&#xff1a;
execution(* com.cw.tsb.app.controller..*.*(..))
第一个 *
&#xff1a;表示返回值类型&#xff0c;*
表示所有类型&#xff1b;
包名
&#xff1a;标识需要拦截的包名&#xff1b;
包名后的 ..
&#xff1a;表示当前包和当前包的所有子包&#xff0c;在本例中指 com.cw.tsb.app.controller
包、子包下所有类&#xff1b;
第二个 *
&#xff1a;表示类名&#xff0c;*
表示所有类&#xff1b;
最后的 *(..)
&#xff1a;星号表示方法名&#xff0c;* 表示所有的方法&#xff0c;后面括弧里面表示方法的参数&#xff0c;两个句点表示任何参数。
annotation()
方式是针对某个注解来定义切点&#xff0c;比如我们对具有 &#64;PostMapping
注解的方法做切面&#xff0c;可以如下定义切面&#xff1a;
&#64;Aspect
&#64;Component
public class ControllerAspect {&#64;Pointcut("&#64;annotation(org.springframework.web.bind.annotation.PostMapping)")public void pointCut() {//该方法仅用于扫描controller包下类中的方法&#xff0c;而不做任何特殊的处理。}
}
然后使用该切面的话&#xff0c;就会切入注解是 &#64;PostMapping
的所有方法。这种方式很适合处理 &#64;GetMapping
、&#64;PostMapping
、&#64;DeleteMapping
不同注解有各种特定处理逻辑的场景。
还有就是如上面案例所示&#xff0c;针对自定义注解来定义切面。
&#64;Aspect
&#64;Component
public class ControllerAspect {&#64;Pointcut("&#64;annotation(com.cw.tsb.app.annotation.PermissionsAnnotation)")private void permissionCheck() {//该方法仅用于扫描controller包下类中的方法&#xff0c;而不做任何特殊的处理。}
}
&#64;Around
注解用于修饰 Around
增强处理&#xff0c;Around
增强处理非常强大&#xff0c;表现在&#xff1a;
&#64;Around
可以自由选择增强动作与目标方法的执行顺序&#xff0c;也就是说可以在增强动作前后&#xff0c;甚至过程中执行目标方法。这个特性的实现在于&#xff0c;调用 ProceedingJoinPoint
参数的 procedd()
方法才会执行目标方法。
&#64;Around
可以改变执行目标方法的参数值&#xff0c;也可以改变执行目标方法之后的返回值。
Around
增强处理有以下特点&#xff1a;
当定义一个 Around
增强处理方法时&#xff0c;该方法的第一个形参必须是 ProceedingJoinPoint
类型&#xff08;至少一个形参&#xff09;。在增强处理方法体内&#xff0c;调用 ProceedingJoinPoint
的 proceed
方法才会执行目标方法&#xff1a;这就是 &#64;Around
增强处理可以完全控制目标方法执行时机、如何执行的关键&#xff1b;如果程序没有调用 ProceedingJoinPoint
的 proceed
方法&#xff0c;则目标方法不会执行。
调用 ProceedingJoinPoint
的 proceed
方法时&#xff0c;还可以传入一个 Object[]
对象&#xff0c;该数组中的值将被传入目标方法作为实参 —— 这就是 Around
增强处理方法可以改变目标方法参数值的关键。这就是如果传入的 Object[]
数组长度与目标方法所需要的参数个数不相等&#xff0c;或者 Object[]
数组元素与目标方法所需参数的类型不匹配&#xff0c;程序就会出现异常。
&#64;Around
功能虽然强大&#xff0c;但通常需要在线程安全的环境下使用。因此&#xff0c;如果使用普通的&#64;Before
、&#64;AfterReturning
就能解决的问题&#xff0c;就没有必要使用 Around
了。如果需要目标方法执行之前和之后共享某种状态数据&#xff0c;则应该考虑使用 Around
。尤其是需要使用增强处理阻止目标的执行&#xff0c;或需要改变目标方法的返回值时&#xff0c;则只能使用 Around
增强处理了。
&#64;Before
注解指定的方法在切面切入目标方法之前执行&#xff0c;可以做一些 Log
处理&#xff0c;也可以做一些信息的统计&#xff0c;比如 获取用户的请求 URL
以及 用户的 IP
地址等等&#xff0c;这个在做个人站点的时候都能用得到&#xff0c;都是常用的方法。例如下面代码&#xff1a;
&#64;Aspect
&#64;Component
public class ControllerAspect {&#64;Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法&#xff0c;而不做任何特殊的处理。}/*** 在上面定义的切面方法之前执行该方法* &#64;param joinPoint jointPoint*/&#64;Before("pointCut()")public void doBefore(JoinPoint joinPoint) {// 获取签名Signature signature &#61; joinPoint.getSignature();// 获取切入的包名String declaringTypeName &#61; signature.getDeclaringTypeName();// 获取即将执行的方法名String funcName &#61; signature.getName();log.info("即将执行方法为: {}&#xff0c;属于{}包", funcName, declaringTypeName);// 也可以用来记录一些信息&#xff0c;比如获取请求的 URL 和 IPServletRequestAttributes attributes &#61; (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request &#61; attributes.getRequest();// 获取请求 URLString url &#61; request.getRequestURL().toString();// 获取请求 IPString ip &#61; request.getRemoteAddr();}
}
JointPoint
对象很有用&#xff0c;可以用它来获取一个签名&#xff0c;利用签名可以获取请求的包名、方法名&#xff0c;包括参数&#xff08;通过 joinPoint.getArgs()
获取&#xff09;等。
&#64;After
注解和 &#64;Before
注解相对应&#xff0c;指定的方法在切面切入目标方法之后执行&#xff0c;也可以做一些完成某方法之后的 Log
处理。
&#64;Aspect
&#64;Component
public class ControllerAspect {&#64;Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法&#xff0c;而不做任何特殊的处理。}/*** 在上面定义的切面方法之后执行该方法* &#64;param joinPoint jointPoint*/&#64;After("pointCut()")public void doAfter(JoinPoint joinPoint) {log.info("&#61;&#61;&#61;&#61; doAfter 方法进入了&#61;&#61;&#61;&#61;");Signature signature &#61; joinPoint.getSignature();String method &#61; signature.getName();log.info("方法{}已经执行完", method);}
}
到这里&#xff0c;我们来写个 Controller
测试一下执行结果&#xff0c;新建一个 AopController
如下&#xff1a;
&#64;RestController
&#64;RequestMapping("/aop")
public class AopController {&#64;GetMapping("/{name}")public String testAop(&#64;PathVariable String name) {return "Hello " &#43; name;}
}
启动项目&#xff0c;在浏览器中输入&#xff1a;http://localhost:8080/aop/csdn&#xff0c;观察一下控制台的输出信息&#xff1a;
&#61;&#61;&#61;&#61;doBefore 方法进入了&#61;&#61;&#61;&#61;
即将执行方法为: testAop&#xff0c;属于com.itcodai.mutest.AopController包
用户请求的 url 为&#xff1a;http://localhost:8080/aop/name&#xff0c;ip地址为&#xff1a;0:0:0:0:0:0:0:1
&#61;&#61;&#61;&#61; doAfter 方法进入了&#61;&#61;&#61;&#61;
方法 testAop 已经执行完
从打印出来的 Log
中可以看出程序执行的逻辑与顺序&#xff0c;可以很直观的掌握 &#64;Before
和 &#64;After
两个注解的实际作用。
&#64;AfterReturning
注解和 &#64;After
有些类似&#xff0c;区别在于 &#64;AfterReturning
注解可以用来捕获切入方法执行完之后的返回值&#xff0c;对返回值进行业务逻辑上的增强处理&#xff0c;例如&#xff1a;
&#64;Aspect
&#64;Component
public class ControllerAspect {&#64;Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法&#xff0c;而不做任何特殊的处理。}/*** 后置返回* 如果第一个参数为JoinPoint&#xff0c;则第二个参数为返回值的信息* 如果第一个参数不为JoinPoint&#xff0c;则第一个参数为returning中对应的参数* returning&#xff1a;限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知&#xff0c;否则不执行&#xff0c;* 参数为Object类型将匹配任何目标返回值*/&#64;AfterReturning(value &#61; "pointCut()", returning &#61; "result")public void doAfterReturning(JoinPoint joinPoint, String result){// 实际项目中可以根据业务做具体的返回值增强}
}
需要注意的是&#xff0c;在 &#64;AfterReturning
注解 中&#xff0c;属性 returning
的值必须要和参数保持一致&#xff0c;否则会检测不到。该方法中的第二个入参就是被切方法的返回值&#xff0c;在 doAfterReturning
方法中可以对返回值进行增强&#xff0c;可以根据业务需要做相应的封装。
当被切方法执行过程中抛出异常时&#xff0c;会进入 &#64;AfterThrowing
注解的方法中执行&#xff0c;在该方法中可以做一些异常的处理逻辑。要注意的是 throwing
属性的值必须要和参数一致&#xff0c;否则会报错。该方法中的第二个入参即为抛出的异常。
&#64;Aspect
&#64;Component
public class ControllerAspect {&#64;Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//该方法仅用于扫描controller包下类中的方法&#xff0c;而不做任何特殊的处理。}&#64;AfterThrowing(value &#61; "pointCut()", throwing &#61; "t")public void doAfterThrowing(JoinPoint joinPoint, Throwable t){System.out.println("------------- doAfterThrowing throwable &#61; " &#43; t.toString());// 处理异常的逻辑}
}