一、基本概念
- AOP,即面向切面编程,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高开发的效率
- 可以明确的定义这个功能应用在哪里,以什么方式应用,并且不必修改受影响的类。这样一来横切关注点就被模块化到特殊的类里——这样的类我们通常称之为“切面”
- 即:不通过修改源代码的方式,在主干功能里面添加新功能
- AOP的好处:
- 每个事物逻辑位于一个位置,代码不分散,便于维护和升级
- 业务模块更简洁,只包含核心业务代码
二、底层原理
- AOP底层使用动态代理,在有接口的情况下,使用JDK动态代理;在没有接口的情况下,使用CGLIB代理。具体的设计模式,详见代理模式
三、AOP术语
- 横切关注点:从每个方法中抽取出来的同一类非核心业务。
- 切面(Aspect):封装横切关注点信息的类,每个关注点体现为一个通知方法。
- 通知(Advice):切面必须要完成的各个具体工作
- 目标(Target):被通知的对象
- 代理(Proxy):向目标对象应用通知之后创建的代理对象
- 连接点(Joinpoint):横切关注点在程序代码中的具体体现,对应程序执行的某个特定位置。例如:类某个方法调用前、调用后、方法捕获到异常后等。
在应用程序中可以使用横纵两个坐标来定位一个具体的连接点:
- 切入点(pointcut): 定位连接点的方式。每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物。如果把连接点看作数据库中的记录,那么切入点就是查询条件——AOP可以通过切入点定位到特定的连接点。切点通过
org.springframework.aop.Pointcut
接口进行描述,它使用类和方法作为连接点的查询条件。 - 简单来说,横切关注点就是被抽取出来的业务,切面就是封装业务方法的类,通知就是具体的方法。每个目标都有相对应的横切关注点,在程序中的具体位置就是连接点,而连接点可以选择是否切入通知,选择被切入通知的位置叫做切入点。
四、AOP依赖
- spring框架一般都是基于AspectJ实现AOP操作
- 相关依赖:
- 或通过maven导入
<dependencies>
<!--spring AOP的包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!--springIOC的包-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
</dependencies>
复制代码
五、基于注解
- 在spring配置文件中,开启注解扫描、开启aop
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> 复制代码
<!-- 开启注解扫描 --> <context:component-scan base-package="com.du.spring5"/> 复制代码
- 开启生成代理对象
<!-- 开启生成代理对象 --> <aop:aspectj-autoproxy/> 复制代码
- 通过注解
@Component
创建UserProxy
对象,在增强类上面添加@Aspect
,说明这是一个切面对象@Component @Aspect public class UserProxy2 { ... } 复制代码
- 配置不同类型的通知。在作为通知方法上面添加通知类型注解,使用切入点表达式配置。
- AspectJ支持5种类型的通知注解:
@Before
:前置通知,在方法执行之前执行@After
:后置通知,在方法执行之后执行@AfterRunning
:返回通知,在方法返回结果之后执行@AfterThrowing
:异常通知,在方法抛出异常之后执行@Around
:环绕通知,围绕着方法执行
- 在切面上再添加一个
@Order
注解,可以设置多个切面的优先级,值越小优先级越大,越先运行@Component //通过注解配置 @Aspect // 生成代理对象 @Order(10) public class UserProxy { } 复制代码
六、切入点表达式
- 知道对哪个类里面的哪个方法进行增强
- 语法结构:
execution([权限修饰符] [返回类型] [类全路径] [方法名称]([参数列表]) )
6.1 例子
- 对
com.du.spring5.bean.User
类里面的add
进行增强:execution(* com.du.spring5.bean.User.add(..))
- 对
com.du.spring5.bean.User
类里面的所有方法进行增强:execution(* com.du.spring5.bean.User.*(..))
- 对
com.du.spring5.bean
包内的所有类的所有方法进行增强:execution(* com.du.spring5.bean.*.*(..))
..
匹配任意数量、任意类型的参数
6.2 例子二
- 在AspectJ中,切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。
- 任意类中第一个参数为int类型的add方法或sub方法:
execution (* *.add(int,..)) || execution(* *.sub(int,..))
七、通知(Advice)
- 在具体的连接点上要执行的操作
- 一个切面可以包括一个或者多个通知。
- 通知所使用的注解的值往往是切入点表达式。
7.1 前置通知
- 在方法执行之前执行的通知
- 使用
@Before
注解
7.2 后置通知
- 后置通知:后置通知是在连接点完成之后执行的,即连接点返回结果或者抛出异常的之后。可以类比成finally,无论是否正常结束,都会执行。
- 使用
@After
注解
7.3 返回通知
- 只有正常结束了,才会执行
- 使用
@AfterReturning
注解 - 若想获取方法执行完之后的返回结果,可以在参数列表中添加一个接收
result
的参数,示例如下:
@AfterReturning(value = "execution(* com.du.spring5.bean.*.*(..))", returning = "result")
public void afterReturning(Object result) {
System.out.println("方法执行结束,返回值为" + result);
}
复制代码
returning
指定 返回值赋给哪个参数,将参数名result
赋给它
7.4 异常通知
- 只在连接点抛出异常时才执行异常通知
- 使用
@AfterThrowing
注解 - 若想捕获异常的信息,就可以像上面一样,在入参中添加一个接受
Exception
的参数,并在注解中说明
@AfterThrowing(value = "execution(* com.du.spring5.bean.*.*(..))", throwing = "exception")
public void afterThrowing(Exception exception) {
System.out.println("发生了" + exception + "异常");
}
复制代码
7.5 JoinPoint
- 若想在通知中接收目标方法的信息,就可以在参数列表中添加一个
JoinPoint
类型的参数,Spring 会自动将该对象注入到方法中,无需在注解中说明。
@Before(value = "execution(* com.du.spring5.bean.*.*(..))")
public void before(JoinPoint joinPoint) {
// 获取方法的所有输入参数
Object[] args = joinPoint.getArgs();
// 获取签名
Signature signature = joinPoint.getSignature();
System.out.println("正在执行[" + signature.getName() + "]方法, 参数为" + Arrays.toString(args));
}
复制代码
更多细节可参考
JoinPoint
接口
7.6 @Pointcut
- 为了避免切入点表达式重复写,可以通过注解
@Pointcut
,统一配置,统一使用。@Pointcut("execution(* com.du.spring5.bean.*.*(..))") public void pointcut() { } @Before(value = "pointcut()") public void before(JoinPoint joinPoint) { } 复制代码
7.7 环绕通知
- 环绕通知是所有通知类型中功能最为强大的,能够全面地控制连接点,甚至可以控制是否执行连接点。
- 很类似于反射的方法,相当于在参数中接收一个包含需要执行的方法及其参数的对象
ProceedingJoinPoint
,然后通过反射的方法执行该方法。而在执行方法的周围,可以通过添加一些try-catch-finally
,实现与上面四种通知类型的效果。@Around(value="pointcut()") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { try { System.out.println("【环绕前置通知】"); Object[] args = joinPoint.getArgs(); // 获取参数 Object res = joinPoint.proceed(args); // 执行方法,获取结果 System.out.println("【环绕返回通知】,结果为" + res); return res; } catch (Throwable throwable) { System.out.println("【环绕异常通知】"); throw new RuntimeException(throwable); } finally { System.out.println("【环绕后置通知】"); } } 复制代码
记得将接收到的结果返回出去,否则之前注解的
@AfterReturning
的方法、真正调用方法的地方,没办法获取到执行的结果。拦截到的
Exception
,记得也抛出去,否则其他地方也接收不到。
7.8 执行顺序
- 当前版本是
5.2.10
,执行顺序如下 - 无异常:
@Before
-> 真正的方法 ->@AfterReturning
->@After
- 存在异常:
@Before
-> 真正的方法 ->@AfterThrowing
->@After
- 包含上述的环绕通知之后:
环绕前置通知
->@Before
-> 真正的方法 ->@AfterReturning
->@After
->环绕返回通知
->环绕后置通知
八、完全注解开发
- 不使用xml文件,直接使用一个config类
@Configuration // 表示这是个配置类
@ComponentScan(basePackages = {"com.du.spring5"}) // 开启组件扫描
@EnableAspectJAutoProxy(proxyTargetClass = true) // 开启aop
public class ConfigAop {
}
复制代码
- 调用
@Test
public void test3() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConfigAop.class); // 配置类
User2 user2 = context.getBean("user2", User2.class);
user2.add();
}
复制代码
九、基于xml文件
- 除了使用AspectJ注解声明切面,Spring也支持在bean配置文件中声明切面。这种声明是通过aop名称空间中的XML元素完成的。
- 正常情况下,基于注解的声明要优先于基于XML的声明。通过AspectJ注解,切面可以与AspectJ兼容,而基于XML的配置则是Spring专有的。由于AspectJ得到越来越多的 AOP框架支持,所以以注解风格编写的切面将会有更多重用的机会。
- 比较:较重要的配置,通过
xml
配置;其他配置通过注解配置。
9.1 配置案例
- xml文件,名称空间添加aop相关内容
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd" > 复制代码
- 在xml上配置aop
- 在bean配置文件中,所有的Spring AOP配置都必须定义在
<aop:config>
元素内部。对于每个切面而言,都要创建一个<aop:aspect>
元素来为具体的切面实现引用后端bean实例。 - 切面bean必须有一个标识符,供
<aop:aspect>
元素引用。
<aop:config> <!-- 设置切入点 --> <aop:pointcut id="user_add_pointcut" expression="execution(* com.du.spring5.bean.User.add(..))"/> <!--设置切面--> <aop:aspect ref="userProxy"> <!-- 设置切面的位置,以及使用切面插入的方法,切入点--> <!-- 标签头说明了切入的位置,method为指定的方法,pointcut-ref为指定的切入点 --> <aop:before method="before" pointcut-ref="user_add_pointcut"/> <aop:after method="after" pointcut-ref="user_add_pointcut"/> <aop:after-returning method="afterReturn" pointcut-ref="user_add_pointcut"/> <aop:after-throwing method="afterThrowing" pointcut-ref="user_add_pointcut"/> <aop:around method="around" pointcut-ref="user_add_pointcut"/> </aop:aspect> </aop:config> 复制代码
- 在bean配置文件中,所有的Spring AOP配置都必须定义在
- 原始类
public class User { private final static Logger logger = Logger.getLogger(User.class); public void add() { logger.info("add......."); } } 复制代码
- 代理类
public class UserProxy { private final static Logger logger = Logger.getLogger(UserProxy.class); /** * 前置通知 */ public void before() { logger.info("before"); } /** * 最终通知 */ public void after() { logger.info("after"); } /** * 后置通知(返回通知) */ public void afterReturn() { logger.info("afterReturn"); } /** * 异常通知 */ public void afterThrowing() { logger.info("afterThrowing"); } /** * 环绕通知 * @param proceedingJoinPoint * @throws Throwable */ public void around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { logger.info("around before"); proceedingJoinPoint.proceed(); logger.info("around ater"); } } 复制代码
9.2 配置
- 所有的Spring AOP配置都必须定义在
<aop:config>
元素内部。在这个config里面,把需要的切面类在里面通过<aop:aspect>
配置。
9.3 切面
<aop:aspect>
切面里面需要指定切面类是哪个(已经被注入到IOC容器内了),通过ref
参数指定对象id;里面还可以指定优先级order
9.3 切入点
- 切入点使用
<aop:pointcut>
元素声明 - 这个标签可以放进
<aop:config>
里面,给所有切面使用,也可以放进<aop:aspect>
只给当前切面使用。 - 该标签需要通过
expression
指定切入点表达式,设置id
以供别的标签调用。
9.4 声明通知
before
通知可以通过<aop:before>
标签指定,其中,method
属性指定方法,pointcut-ref
指定切入点。- 具体配置
<aop:config>
<!-- 设置切入点 -->
<aop:pointcut id="user_add_pointcut" expression="execution(* com.du.spring5.bean.User.add(..))"/>
<!--设置切面-->
<aop:aspect ref="userProxy">
<!-- 设置切面的位置,以及使用切面插入的方法,切入点-->
<!-- method为切入的位置,pointcut-ref为代理类的指定的方法 -->
<aop:before method="before" pointcut-ref="user_add_pointcut"/>
<aop:after method="after" pointcut-ref="user_add_pointcut"/>
<aop:after-returning method="afterReturn" pointcut-ref="user_add_pointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="user_add_pointcut"/>
<aop:around method="around" pointcut-ref="user_add_pointcut"/>
</aop:aspect>
</aop:config>
复制代码
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END