什么是AOP
AOP(Aspect Oriented Programming),即面向切面编程
众所周知,OOP(面向对象编程)通过的是继承、封装和多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。OOP从纵向上区分出一个个的类来,而AOP则从横向上向对象中加入特定的代码
AOP采用"横切"的技术,剖解开封装的对象内部,将影响了多个类的公共行为封装到一个可重用模块。将那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性
简单来说讲,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程
应用场景举例
1、日志模块
日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此。
2、事务管理
调用方法前开启事务, 调用方法后提交关闭事务
AOP的术语整理
1) 切面(Aspect)
切面是通知和切点的结合。通知和切点共同定义了切面的全部内容——它是什么,在何时和何处完成其功能。比如事务管理是一个切面,权限管理也是一个切面
2) 通知(Advice)
通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。
Spring切面可以应用5种类型的通知:
- **前置通知(Before):**在目标方法被调用之前调用通知功能
- **后置通知(After):**在目标方法完成之后调用通知,不关心方法的输出是什么。是“返回通知”和“异常通知”的并集
- **返回通知(After-returning):**在目标方法成功执行之后调用通知
- **异常通知(After-throwing):**在目标方法抛出异常后调用通知
- **环绕通知(Around):**通知包裹了被通知的方法,可同时定义前置通知和后置通知
3) 切点(Pointcut)
切点定义了在何处工作,也就是真正被切入的地方,也就是在哪个方法应用通知。切点的定义会匹配通知所有要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点
4)连接点(Join point)
连接点是在应用执行过程中能够插入切面的一个点。这个点可以是调用方法时,抛出异常时,甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为
5) 引入(Introduction)
引入让一个切面可以声明被通知的对象实现了任何他们没有真正实现的额外接口,而且为这些对象提供接口的实现。
引入允许我们向现有的类添加新方法或属性。这个新方法和实例变量就可以被引入到现有的类中,从而可以再无需修改这些现有的类的情况下,让它们具有新的行为和状态
5) 织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。在目标对象的生命周期里有多个点可以织入。
- 编译器:切面在目标类编译时被织入。这种方式需要特殊的编译器
- 类加载期:切面在目标类被引入应用之前增强该目标类的字节码
- 运行期:切面在应用运行的某个时刻被织入
实例
maven依赖添加如下
<!--引入AOP依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
或
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
注意:在完成了引入AOP依赖包后,不需要去做其他配置。AOP的默认配置属性中,spring.aop.auto属性默认是开启的,也就是说只要引入了AOP依赖后,默认已经增加了@EnableAspectJAutoProxy,不需要在程序主类中增加@EnableAspectJAutoProxy来启用
web请求入口
对应系统纵向的核心业务模块
package com.lluozh.fagent.controller;
@Slf4j
@RestController
@RequestMapping("/fagent")
public class aopController {
@ExceptionHandler(RRException.class)
@PostMapping(value="/lluozh")
public ApiResult runLocal(@RequestBody ExecParamDto execParamDto) {
System.out.println("lluozh");
}
定义切面类
在类上添加@Aspect 和@Component 注解即可将一个类定义为切面类。
@Aspect 注解 使之成为切面类
@Component 注解 把切面类加入到IOC容器中
@Aspect
@Slf4j
@Component
public class LogAop {
/**
* 定义切入点,切入点为com.lluozh.fagent.controller中的所有函数
*通过@Pointcut注解声明频繁使用的切点表达式
*/
@Pointcut("execution(public * com.lluozh.fagent.controller..*(..))")
public void allController() {
}
/**
* @description 在连接点执行之前执行的通知
*/
@Before("allController()")
public void doBeforeGame(){
System.out.println("before");
}
/**
* @description 在连接点执行之后执行的通知(返回通知和异常通知的异常)
*/
@After("allController()")
public void doAfterGame(){
System.out.println("after!");
}
/**
* @description 在连接点执行之后执行的通知(返回通知)
*/
@AfterReturning("allController()")
public void doAfterReturningGame(){
System.out.println("after");
}
/**
* @description 在连接点执行之后执行的通知(异常通知)
*/
@AfterThrowing("allController()")
public void doAfterThrowingGame(){
System.out.println("after");
}
/**
* @description 参数必须是ProceedingJoinPoint,通过该对象的proceed()方法来执行目标函数
* proceed()的返回值就是环绕通知的返回值,proceedingJoinPoint是个接口
* implement JoinPoint,所以也可以获得目标函数的类名,方法名等参数
*/
@Around("allController()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String requestArgs = Arrays.toString(pjp.getArgs());
String requestURI = request.getRequestURI();
log.info("URI:{}, 参数:{}", requestURI, requestArgs);
Object result = pjp.proceed();
log.info("URI:{}, 返回: {}", requestURI, result);
return result;
}
}
当调用服务时,直接打印请求的日志信息
切点表达式
用于描述切点的位置信息,在此简单描述文中切点表达式的含义
环绕通知
单独讲解一下功能强大的环绕通知,环绕通知可以将你所编写的逻辑将被通知的目标方法完全包装起来
可以使用一个环绕通知来代替之前多个不同的前置通知和后置通知,如下所示,前置通知和后置通知位于同一个方法中,不像之前那样分散在不同的通知方法里面
/**
* @description 使用环绕通知
*/
@Around("allController()")
public void doAroundGame(ProceedingJoinPoint pjp) throws Throwable {
try{
System.out.println("start");
pjp.proceed();
System.out.println("end");
}
catch(Throwable e){
System.out.println("异常通知:err!");
}
}
环绕通知接受ProceedingJoinPoint作为参数,它来调用被通知的方法。通知方法中可以做任何的事情,当要将控制权交给被通知的方法时,需要调用ProceedingJoinPoint的proceed()方法。当你不调用proceed()方法时,将会阻塞被通知方法的访问
参数
当通知方法需要传入参数我们又该怎样处理呢?
和之前创建的切面一样,这里的不同点在于切点还声明了要提供给通知方法的参数。
切点表达式args(point)表明传递给GameDataAspect()方法中的int类型参数也会传递到通知中去,参数名point和缺点方法签名中的参数相匹配
@Aspect
@Slf4j
@Component
public class LogAop {
/**
* 定义切入点,切入点为com.lluozh.fagent.controller中的所有函数
*通过@Pointcut注解声明频繁使用的切点表达式
*/
@Pointcut("execution(public * com.lluozh.fagent.controller.Durant(int)) && args(point))")
public void allController(int point) {
}
@Around("allController(point)")
public void doAroundGame(ProceedingJoinPoint pjp,int point) throws Throwable {
try{
System.out.println("start" + point);
pjp.proceed();
System.out.println("end" + point);
}
catch(Throwable e){
System.out.println("异常通知:err!");
}
}
}