Spring之AOP(面向切面编程)
1、AOP的基本介绍
AOP是Aspect Oriented Programming,即面向切面编程。AOP是OOP(面向对象编程)的延续,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。OOP作为面向对象编程的模式,获得了巨大的成功,OOP的主要功能是数据封装、继承和多态。而AOP是一种新的编程方式,它和OOP不同,OOP把系统看作多个对象的交互,AOP把系统分解为不同的关注点,或者称之为切面(Aspect)。
AOP 要达到的效果是,保证开发者在不修改源代码的前提下,去为系统中的业务组件添加某种通用功能。AOP 的本质是由 AOP 框架修改业务组件的多个方法的源代码。
AOP技术看上去比较神秘,但实际上,它本质就是一个动态代理。
1.1、spring AOP的代理机制
按照 AOP 框架修改源代码的时机,可以将其分为两类:
- 静态 AOP 实现:AOP 框架在编译阶段对程序源代码进行修改,生成了静态的 AOP 代理类。此时生成的 *.class 文件已经被改掉了,需要使用特定的编译器,比如 AspectJ。
- 动态 AOP 实现:AOP 框架在运行阶段对动态生成代理对象,在内存中以 JDK 动态代理,或 CGlib 动态地生成 AOP 代理类,如 SpringAOP。目标对象和切面都是普通Java类,通过JVM的动态代理功能或者第三方库实现运行期动态织入。
最简单的方式就是动态 AOP 实现,Spring的AOP实现就是基于JVM的动态代理。JVM的动态代理要求必须实现接口,所以如果一个普通类并没有实现任何借口,那么就需要通过CGLIB或者Javassist这些第三方库来实现 AOP。
如果要被代理的对象是个实现类,Spring 会自动使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);如果要被代理的对象是个普通类,即不是实现类,那么 Spring 会强制使用 CGLib 来实现动态代理。
通过配置Spring的中<aop:config>标签可以显式地指定使用什么代理机制,proxy-target-class=true 表示使用CGLib代理,如果为 false 就是默认使用JDK动态代理:
1.2、AOP相关术语
AOP 领域中的特性术语:
- 通知(Advice,增强): 通知描述了切面何时执行以及如何执行增强处理。(比如给类中的某个方法进行了添加了一些额外的操作,这些额外操作就是增强)
- 连接点(join point): 应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用,即哪些方法可以被增强,这些方法就可以称之为一个连接点。
- 切入点(PointCut): 可以插入增强处理的连接点。实际被真正增强了的方法,称为切入点。(连接点都可被增强,但实际应用可能只增强了类中的某个方法,则该方法就被称为切入点)
- 切面(Aspect): 切面是通知和切点的结合。把通知(增强)应用到切入点的过程就称为切面。
- 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
- 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。
1.3、通知的五种类型(@Before、@After、@AfterReturning、@AfterThrowing、@Around)
通知(增强)有五种类型:
- 前置通知(@Before):在目标方法运行之前运行。目标代码有异常也会执行,但如果拦截器抛异常,那么目标代码就不执行了;
- 后置通知(@After):在目标方法运行结束之后运行。无论目标代码是否抛异常,拦截器代码都会执行;
- 返回通知(@AfterReturning):在目标方法正常返回之后运行。和@After不同的是,只有当目标代码正常返回时,才执行拦截器代码,并且这个通知执行顺序在 @After 之后
- 异常通知(@AfterThrowing):在目标方法出现异常之后运行。和@After不同的是,只有当目标代码抛出了异常时,才执行拦截器代码;
- 环绕通知(@Around):增强的方法会将目标方法封装起来,能控制是否执行目标代码,并可以在执行前后、抛异常后执行任意拦截代码,可以说是包含了上面所有功能。
2、AOP的基本使用
2.1、切入点表达式
切入点表达式是 spring 用表达式来对指定的方法进行拦截。
示例如下:
execution([权限修饰符] [返回类型(可省略)] [完整类名].[方法名](参数列表)) //示例如下,权限修饰符可用*表示任意权限,参数列表可用 .. 表示方法中的参数 execution(public int com.demo.Test.*(..)) //作用于Test类中的所有方法 execution(* com.demo.Test.add(..)) //作用于Test类中的add方法 execution(* com.demo.*.*(..)) //作用于demo包下的所有类的所有方法
2.2、AOP的使用
spring 框架一般都是基于 AspectJ 实现 AOP 操作,AspectJ 不是 spring 的组成部分,一般把 AspectJ 和 spring 框架一起使用,进行 AOP 操作。
基于 AspectJ 实现 AOP 操作有两种方式:
- 基于XML配置文件实现
- 基于注解方式实现
先导入依赖包,spring 实现AOP不仅仅需要自己的jar包,还需要第三方的jar,将这三个jar包放入项目中就可以spring的aop编程了,如下所示:
使用示例:
先创建一个类(被增强的类):
package webPackage; import org.springframework.stereotype.Component; //需要被增强的类 @Component public class User { public void add() { System.out.println("user add..."); } }
然后创建增强类(切面类,编写增强逻辑):
package webPackage; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Component @Aspect //生成代理对象 public class UserProxy { //前置通知 @Before("execution(* webPackage.User.add(..))") public void beforeHandler() { System.out.println("前置处理。。。"); } }
在 xml 配置文件中引入命名变量,并且开启组件注解扫描和 aspectj 切面支持:
<?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/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <!--开启组件扫描--> <context:component-scan base-package="testMain, service, dao, webPackage"></context:component-scan> <!-- 开启aspectj切面支持 --> <aop:aspectj-autoproxy></aop:aspectj-autoproxy> </beans>
验证代码:
package testMain; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import webPackage.User; public class Test01 { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("bean01.xml"); User user = (User) context.getBean("user"); user.add(); //将输出 前置处理。。。 user add... } }
如果 User 类实现了某个接口,即是实现类,上面的用法可能会报错: com.sun.proxy.$Proxy11 cannot be cast to web.User。报错是因为不能用接口的实现类(Target)来转换Proxy的实现类,它们是同级,应该用共同的接口来转换。将获得Bean的接收类型改成接口类型即可。
此时可以这么用:
package test; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import web.User; import web.UserInter; public class Test { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("bean01.xml"); UserInter user = (UserInter) context.getBean("user"); user.add(); //将输出 前置处理。。。 user add... } }
上面实现的是前置通知,其他类型通知如下:
package webPackage; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Component @Aspect //生成代理对象 public class UserProxy { //前置通知 @Before("execution(* webPackage.User.add(..))") public void beforeHandler() { System.out.println("前置处理。。。"); } //后置通知 @After("execution(* webPackage.User.add(..))") public void afterHandler() { System.out.println("后置处理。。。"); } //返回通知 @AfterReturning("execution(* webPackage.User.add(..))") public void afterReturnHandler() { System.out.println("返回处理。。。"); } //异常通知 @AfterThrowing("execution(* webPackage.User.add(..))") public void afterThrowReturnHandler() { System.out.println("异常处理。。。"); } //环绕通知 @Around("execution(* webPackage.User.add(..))") public void aroundThrowReturnHandler(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { System.out.println("环绕之前。。。"); proceedingJoinPoint.proceed(); //执行目标方法 System.out.println("环绕之后。。。"); } }
执行结果:环绕之前。。。 前置处理。。。 user add... 环绕之后。。。 后置处理。。。 返回处理。。。
异常通知没有执行,因为目标方法并没有抛出异常,如果抛出异常,异常通知才会执行。
2.3、抽取相同切入点(@Pointcut)
使用多个通知时,可能需要重复写切入点表达式,此时我们可以通过 @Pointcut 注解来将切入点抽取出来进行复用:
package webPackage; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component; @Component @Aspect //生成代理对象 public class UserProxy { @Pointcut("execution(* webPackage.User.add(..))") public void pointDemo() {} //前置通知 @Before("pointDemo()") public void beforeHandler() { System.out.println("前置处理。。。"); } //后置通知 @After("pointDemo()") public void afterHandler() { System.out.println("后置处理。。。"); } }
2.4、多个切面(增强类)优先级
如果有多个切面对同一个类的方法都进行了增强,我们可以用 @Order(num) 来定义各个切面的优先级,num越小,优先级越高。
比如 UserProxy 和 UserProxy02 都对 User 的 add 方法进行了增强,我们就可以通过 @Order(num) 来指定哪个增强类先执行:
UserProxy 代码:
package webPackage; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Component @Aspect @Order(2) public class UserProxy { @Pointcut("execution(* webPackage.User.add(..))") public void pointDemo() {} //前置通知 @Before("pointDemo()") public void beforeHandler() { System.out.println("UserProxy 的前置处理。。。"); } }
UserProxy02 代码:
package webPackage; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; @Component @Aspect //生成代理对象 @Order(1) public class UserProxy02 { @Pointcut("execution(* webPackage.User.add(..))") public void pointDemo() {} //前置通知 @Before("pointDemo()") public void beforeHandler() { System.out.println("UserProxy02 的前置处理。。。"); } }
验证代码:
package testMain; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import webPackage.User; public class Test01 { public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("bean01.xml"); User user = (User) context.getBean("user"); user.add(); //将输出:UserProxy02 的前置处理。。。 UserProxy 的前置处理。。。 user add... } }
2.5、完全注解开发(不需要xml配置文件)
我们可以通过配置类来替代 xml 配置文件,实现完全注解开发:
照着上面的例子,先建一个 User 类和一个增强类 UserProxy,然后用配置类替代 xml 配置文件。配置类如下:
package webPackage; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; @Configuration @ComponentScan(basePackages = {"testMain", "service", "dao", "webPackage"}) @EnableAspectJAutoProxy(proxyTargetClass = true) public class AopConfig { }
Spring的 IOC 容器看到 @EnableAspectJAutoProxy 注解,就会自动查找带有@Aspect
的Bean,然后根据每个方法的@Before
、@Around
等注解把AOP注入到特定的Bean中。
使用 bean 跟用配置文件方式不太一样,代码如下:
package testMain; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import webPackage.AopConfig; import webPackage.User; public class Test01 { public static void main(String[] args) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AopConfig.class); User user = (User) context.getBean("user"); user.add(); } }
2.6、直接通过配置文件实现AOP(一般不用)
先创建一个类:
然后创建增强类:
配置前置通知: