Spring基础知识(8)- Spring AOP
面向切面编程(AOP)、Spring AOP
1. 面向切面编程(AOP)
AOP (Aspect Oriented Programming)译为 “面向切面编程”,和 OOP(面向对象编程)类似,它也是一种编程思想。
多数情况下,我们会使用 OOP(面向对象)思想,将应用划分为不同的业务模块,每个模块的核心功能都只为特定的业务提供服务。
存在一些非业务的通用功能,例如日志管理、权限管理、事务管理、异常管理等通用功能,几乎所有的业务模块都会使用到它们,因此这些通用功能代码就只能横向散布式地嵌入到多个不同的业务模块之中。这无疑会产生大量重复性代码,不利于各个模块的复用。
可以将这些重复性代码封装成为公共函数,然后在业务模块中显式的调用,这样做的确能一定程度上减少重复性代码,但这样也增加了业务代码与公共函数的耦合性,任何对于公共函数的修改都会对所有与之相关的业务代码造成影响。
1) AOP 的特点
与 OOP 中纵向的父子继承关系不同,AOP 是通过横向的抽取机制实现的。它将应用中的一些非业务的通用功能抽取出来单独维护,并通过声明的方式(例如配置文件、注解等)定义这些功能要以何种方式作用在那个应用中,而不是在业务模块的代码中直接调用。
这虽然设计公共函数有几分类似,但传统的公共函数除了在代码直接硬调用之外并没有其他手段。AOP 则为这一问题提供了一套灵活多样的实现方法(例如 Proxy 代理、拦截器、字节码翻译技术等),可以在无须修改任何业务代码的基础上完成对这些通用功能的调用和修改。
AOP 编程和 OOP 编程的目标是一致的,都是为了减少程序中的重复性代码,让开发人员有更多的精力专注于业务逻辑的开发,只不过两者的实现方式大不相同。
OOP 就像是一根“绣花针”,是一种婉约派的选择,它使用继承和组合方式,仔细地为所有涉及通用功能的模块编制成一套类和对象的体系,以达到减少重复性代码的目标。而 AOP 则更像是一把“砍柴刀”,是一种豪放派的选择,大刀阔斧的规定,凡是某包某类下的某方法都一并进行处理。
AOP 不是用来替换 OOP 的,而是 OOP 的一种延伸,用来解决 OOP 编程中遇到的问题。
在 Spring 框架中使用 AOP 主要有以下优势。
(1) 提供声明式企业服务,特别是作为 EJB 声明式服务的替代品,最重要的是,这种服务是声明式事务管理。
(2) 允许用户实现自定义切面。在某些不适合用 OOP 编程的场景中,采用 AOP 来补充。
(3) 可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时也提高了开发效率。
2) AOP 联盟
为了更好的应用 AOP 技术,技术专家们成立了 AOP 联盟(AOP Alliance)。
AOP 联盟定义了一套用于规范 AOP 实现的底层 API,通过这些统一的底层 API,使得各个AOP 框架及工具产品之间可以相互移植。这些 API 主要以标准接口的形式提供,是 AOP 编程思想所要解决各种问题的最高抽象。
所有的 AOP 框架都应该是对这些 AOP 接口规范的具体实现,因此通常我们也将 AOP 框架称作 AOP 实现。
目前最流行的 AOP 实现(框架)主要有两个,分别为 Spring AOP 和 AspectJ。
AOP 框架 | 说明 |
Spring AOP |
是一款基于 AOP 编程的框架,它能够有效的减少系统间的重复代码,达到松耦合的目的。 Spring AOP 使用纯 Java 实现,不需要专门的编译过程和类加载器,在运行期间通过代理方式向目标类植入增强的代码。 Spring AOP 支持 2 种代理方式,分别是基于接口的 JDK 动态代理和基于继承的 CGLIB 动态代理。 |
AspectJ |
是一个基于 Java 语言的 AOP 框架,从 Spring 2.0 开始,Spring AOP 引入了对 AspectJ 的支持。 AspectJ 扩展了 Java 语言,提供了一个专门的编译器,在编译时提供横向代码的植入。 |
3) AOP 术语
名称 | 说明 |
Joinpoint(连接点) | AOP 的核心概念,指的是程序执行期间明确定义的一个点,例如方法的调用、类初始化、对象实例化等。在 Spring 中,连接点则指可以被动态代理拦截目标类的方法。 |
Pointcut(切入点) | 又称切点,指要对哪些 Joinpoint 进行拦截,即被拦截的连接点。 |
Advice(通知) | 指拦截到 Joinpoint 之后要执行的代码,即对切入点增强的内容。 |
Target(目标) | 指代理的目标对象,通常也被称为被通知(advised)对象。 |
Weaving(织入) | 指把增强代码应用到目标对象上,生成代理对象的过程。 |
Proxy(代理) | 指生成的代理对象。 |
Aspect(切面) | 切面是切入点(Pointcut)和通知(Advice)的结合。 |
Advice 直译为通知,也有人将其翻译为“增强处理”,共有 5 种类型,如下表所示。
通知 | 说明 |
before(前置通知) | 通知方法在目标方法调用之前执行 |
after(后置通知) | 通知方法在目标方法返回或异常后调用 |
after-returning(返回后通知) | 通知方法会在目标方法返回后调用 |
after-throwing(抛出异常通知) | 通知方法会在目标方法抛出异常后调用 |
around(环绕通知) | 通知方法会将目标方法封装起来 |
4) AOP 的类型
(1) 动态 AOP
动态 AOP 的织入过程是在运行时动态执行的。其中最具代表性的动态 AOP 实现就是 Spring AOP,它会为所有被通知的对象创建代理对象,并通过代理对象对被原对象进行增强。
相较于静态 AOP 而言,动态 AOP 的性能通常较差,但随着技术的不断发展,它的性能也在不断的稳步提升。
动态 AOP 的优点是它可以轻松地对应用程序的所有切面进行修改,而无须对主程序代码进行重新编译。
(2) 静态 AOP
静态 AOP 是通过修改应用程序的实际 Java 字节码,根据需要修改和扩展程序代码来实现织入过程的。最具代表性的静态 AOP 实现是 AspectJ。
相较于动态 AOP 来说,性能较好。但它也有一个明显的缺点,那就是对切面的任何修改都需要重新编译整个应用程序。
2. Spring AOP
1) Spring AOP 的代理机制
Spring 在运行期会为目标对象生成一个动态代理对象,并在代理对象中实现对目标对象的增强。
Spring AOP 的底层是通过以下 2 种动态代理机制,为目标对象(Target Bean)执行横向织入的。
代理技术 | 描述 |
JDK 动态代理 | Spring AOP 默认的动态代理方式,若目标对象实现了若干接口,Spring 使用 JDK 的 java.lang.reflect.Proxy 类进行代理。 |
CGLIB 动态代理 | 若目标对象没有实现任何接口,Spring 则使用 CGLIB 库生成目标对象的子类,以实现对目标对象的代理。 |
注意:由于被标记为 final 的方法是无法进行覆盖的,因此这类方法不管是通过 JDK 动态代理机制还是 CGLIB 动态代理机制都是无法完成代理的。
Spring 能够基于 org.springframework.aop.framework.ProxyFactoryBean 类,根据目标对象的类型(是否实现了接口)自动选择使用 JDK 动态代理或 CGLIB 动态代理机制,为目标对象(Target Bean)生成对应的代理对象(Proxy Bean)。
ProxyFactoryBean 的常用属性如下表所示。
属性 | 描述 |
target | 需要代理的目标对象(Bean) |
proxyInterfaces | 代理需要实现的接口,如果需要实现多个接口,可以通过
|
proxyTargetClass | 针对类的代理,该属性默认取值为 false(可省略), 表示使用 JDK 动态代理;取值为 true,表示使用 CGlib 动态代理 |
interceptorNames | 拦截器的名字,该属性的取值既可以是拦截器、也可以是 Advice(通知)类型的 Bean,还可以是切面(Advisor)的 Bean。 |
singleton | 返回的代理对象是否为单例模式,默认值为 true。 |
optimize | 是否对创建的代理进行优化(只适用于CGLIB)。 |
2) Spring AOP 连接点
Spring AOP 并没有像其他 AOP 框架(例如 AspectJ)一样提供了完成的 AOP 功能,它是 Spring 提供的一种简化版的 AOP 组件。其中最明显的简化就是,Spring AOP 只支持一种连接点类型:方法调用。您可能会认为这是一个严重的限制,但实际上 Spring AOP 这样设计的原因是为了让 Spring 更易于访问。
方法调用连接点是迄今为止最有用的连接点,通过它可以实现日常编程中绝大多数与 AOP 相关的有用的功能。如果需要使用其他类型的连接点(例如成员变量连接点),我们可以将 Spring AOP 与其他的 AOP 实现一起使用,最常见的组合就是 Spring AOP + AspectJ。
3) Spring AOP 通知类型
AOP 联盟为通知(Advice)定义了一个 org.aopalliance.aop.Interface.Advice 接口。
Spring AOP 按照通知(Advice)织入到目标类方法的连接点位置,为 Advice 接口在 org.springframework.aop 包里提供了 5 个子接口,如下表。
通知/接口 | 描述 |
前置通知(MethodBeforeAdvice) | 在目标方法执行前实施增强。 |
后置通知(AfterReturningAdvice) | 在目标方法执行后实施增强。 |
环绕通知(MethodInterceptor) | 在目标方法执行前后实施增强。 |
异常通知(ThrowsAdvice) | 在方法抛出异常后实施增强。 |
引入通知(IntroductionInterceptor) | 在目标类中添加一些新的方法和属性。 |
4) Spring AOP 切面类型
Spring 使用 Advisor 接口表示切面的概念,实现对通知(Adivce)和连接点(Joinpoint)的管理,在 org.springframework.aop 包,下同。
在 Spring AOP 中,切面可以分为三类:一般切面、切点切面和引介切面。
切面/接口 | 描述 |
一般切面(Advisor) | Spring AOP 默认的切面类型。由于 Advisor 接口仅包含一个 Advice(通知)类型的属性,而没有定义 PointCut(切入点),因此它表示一个不带切点的简单切面。 |
切点切面(PointcutAdvisor) | Advisor 的子接口,用来表示带切点的切面,该接口在 Advisor 的基础上还维护了一个 PointCut(切点)类型的属性。 |
引介切面(IntroductionAdvisor) | Advisor 的子接口,用来代表引介切面,引介切面是对应引介增强的特殊的切面,它应用于类层面上,所以引介切面适用 ClassFilter 进行定义。 |
本文只对 Advisor 和 PointcutAdvisor 进行讲解,后面再介绍 IntroductionAdvisor。
5) 一般切面(Advisor)的开发
当我们在使用 Spring AOP 开发时,若没有对切面进行具体定义,Spring AOP 会通过 Advisor 为我们定义一个一般切面(不带切点的切面),然后对目标对象(Target)中的所有方法连接点进行拦截,并织入增强代码。
Spring 配置文件(spring-beans.xml)配置格式如下:
1 <?xml version="1.0" encoding="UTF-8"?> 23 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 7 http://www.springframework.org/schema/context 8 http://www.springframework.org/schema/context/spring-context-4.0.xsd"> 9 10 11 class="com.example.SportsImpl"> 12 13class="com.example.SportsBeforeAdvice"> 14class="com.example.SportsAfterAdvice"> 15 16class="org.springframework.aop.framework.ProxyFactoryBean"> 17 25 2618 19 2420
23sportsBeforeAdvice 21sportsAfterAdvice 22
示例:
1 package com.example; 2 3 import java.lang.reflect.Method; 4 import org.springframework.context.support.ClassPathXmlApplicationContext; 5 import org.springframework.aop.MethodBeforeAdvice; 6 import org.springframework.aop.AfterReturningAdvice; 7 8 public class App { 9 public static void main( String[] args ) { 10 11 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-beans.xml"); 12 13 // 一般切面(Advisor) 14 Sports sports = context.getBean("sportsProxy", Sports.class); 15 sports.swimming(); 16 sports.test(); 17 } 18 } 19 20 interface Sports { 21 public String swimming(); 22 public void test(); 23 } 24 25 class SportsImpl implements Sports { 26 27 @Override 28 public String swimming() { 29 System.out.println("Sports -> swimming()"); 30 return "OK"; 31 } 32 33 @Override 34 public void test() { 35 System.out.println("Sports -> test()"); 36 } 37 38 } 39 40 class SportsBeforeAdvice implements MethodBeforeAdvice { 41 @Override 42 public void before(Method method, Object[] args, Object target) throws Throwable { 43 System.out.println("SportsBeforeAdvice -> before()"); 44 } 45 } 46 47 class SportsAfterAdvice implements AfterReturningAdvice { 48 @Override 49 public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable { 50 System.out.println("SportsAfterAdvice -> afterReturning(): returnValue = " + returnValue); 51 } 52 }
输出:
SportsBeforeAdvice -> before()
Sports -> swimming()
SportsAfterAdvice -> afterReturning(): returnValue = OK
SportsBeforeAdvice -> before()
Sports -> test()
SportsAfterAdvice -> afterReturning(): returnValue = null
6) 切点切面(PointcutAdvisor)的开发
PointCutAdvisor 是 Adivsor 接口的子接口,用来表示带切点的切面。使用它,我们可以通过包名、类名、方法名等信息更加灵活的定义切面中的切入点,提供更具有适用性的切面。
Spring 提供了多个 PointCutAdvisor 的实现,其中常用实现类如如下。
NameMatchMethodPointcutAdvisor:指定 Advice 所要应用到的目标方法名称,例如 hello* 代表所有以 hello 开头的所有方法。
RegExpMethodPointcutAdvisor:使用正则表达式来定义切点(PointCut),RegExpMethodPointcutAdvisor 包含一个 pattern 属性,该属性使用正则表达式描述需要拦截的方法。
Spring 配置文件(spring-beans.xml)配置格式如下:
1 <?xml version="1.0" encoding="UTF-8"?> 23 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 7 http://www.springframework.org/schema/context 8 http://www.springframework.org/schema/context/spring-context-4.0.xsd"> 9 10 11 class="com.example.Food"> 12 13class="com.example.FoodAroundAdvice"> 14 15class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor"> 16 19 2017 18 class="org.springframework.aop.framework.ProxyFactoryBean"> 21 25 2622 23 24
示例:
1 package com.example; 2 3 import java.lang.reflect.Method; 4 import org.springframework.context.support.ClassPathXmlApplicationContext; 5 import org.aopalliance.intercept.MethodInterceptor; 6 import org.aopalliance.intercept.MethodInvocation; 7 8 public class App { 9 public static void main( String[] args ) { 10 11 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-beans.xml"); 12 13 // 切点切面(PointcutAdvisor) 14 Food food = context.getBean("foodProxy", Food.class); 15 food.test1(); 16 food.test2(); 17 food.eating(); 18 } 19 } 20 21 class Food { 22 public String eating() { 23 System.out.println("Food -> eating()"); 24 return "GOOD"; 25 } 26 27 public void test1() { 28 System.out.println("Food -> test1()"); 29 } 30 31 public void test2() { 32 System.out.println("Food -> test2()"); 33 } 34 } 35 36 class FoodAroundAdvice implements MethodInterceptor { 37 @Override 38 public Object invoke(MethodInvocation methodInvocation) throws Throwable { 39 System.out.println("FoodAroundAdvice -> invoke(): before"); 40 // 执行被代理对象中的逻辑 41 Object result = methodInvocation.proceed(); 42 43 System.out.println("FoodAroundAdvice -> invoke(): after"); 44 return result; 45 } 46 }
7) 自动代理
在前面的案例中,所有目标对象(Target Bean)的代理对象(Proxy Bean)都是在 XML 配置中通过 ProxyFactoryBean 创建的。但在实际开发中,一个项目中往往包含很多 Bean, 如果每个 Bean 都通过 ProxyFactoryBean 创建,那么开发和维护成本会十分巨大。
Spring 提供的自动代理方案,都是基于后处理 Bean 实现的,即在 Bean 创建的过程中完成增强,并将目标对象替换为自动生成的代理对象。
Spring 为我们提供了 3 种自动代理方案:
(1) BeanNameAutoProxyCreator:根据 Bean 名称创建代理对象。
(2) DefaultAdvisorAutoProxyCreator:根据 Advisor 本身包含信息创建代理对象。
(3) AnnotationAwareAspectJAutoProxyCreator:基于 Bean 中的 AspectJ 注解进行自动代理对象。
本文只对 BeanNameAutoProxyCreator 和 DefaultAdvisorAutoProxyCreator 进行讲解,把AnnotationAwareAspectJAutoProxyCreator 放到后面 AspectJ 部分。
(1) 使用 BeanNameAutoProxyCreator 创建代理对象
Spring 配置文件(spring-beans.xml)配置格式如下:
1 <?xml version="1.0" encoding="UTF-8"?> 23 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 7 http://www.springframework.org/schema/context 8 http://www.springframework.org/schema/context/spring-context-4.0.xsd"> 9 10 11 class="com.example.SportsImpl"> 12class="com.example.Food"> 13 14 15class="com.example.SportsBeforeAdvice"> 16class="com.example.SportsAfterAdvice"> 17class="com.example.FoodAroundAdvice"> 18 19 20class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator"> 21 24 2522 23
示例:
1 package com.example; 2 3 import java.lang.reflect.Method; 4 import org.springframework.context.support.ClassPathXmlApplicationContext; 5 import org.springframework.aop.MethodBeforeAdvice; 6 import org.springframework.aop.AfterReturningAdvice; 7 8 import org.aopalliance.intercept.MethodInterceptor; 9 import org.aopalliance.intercept.MethodInvocation; 10 11 public class App { 12 public static void main( String[] args ) { 13 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-beans.xml"); 14 15 Sports sports = context.getBean("sportsImpl", Sports.class); 16 sports.swimming(); 17 18 Food food = context.getBean("food", Food.class); 19 food.eating(); 20 } 21 } 22 23 // 使用本文前面示例的 class 24 interface Sports { 25 ... 26 } 27 28 class SportsImpl implements Sports { 29 ... 30 } 31 32 class SportsBeforeAdvice implements MethodBeforeAdvice { 33 ... 34 } 35 36 class SportsAfterAdvice implements AfterReturningAdvice { 37 ... 38 } 39 40 class Food { 41 ... 42 } 43 44 class FoodAroundAdvice implements MethodInterceptor { 45 ... 46 }
输出:
SportsBeforeAdvice -> before()
FoodAroundAdvice -> invoke(): before
Sports -> swimming()
FoodAroundAdvice -> invoke(): after
SportsAfterAdvice -> afterReturning(): returnValue = OK
SportsBeforeAdvice -> before()
FoodAroundAdvice -> invoke(): before
Food -> eating()
FoodAroundAdvice -> invoke(): after
SportsAfterAdvice -> afterReturning(): returnValue = GOOD
(2) 使用 DefaultAdvisorAutoProxyCreator 创建代理对象
Spring 配置文件(spring-beans.xml)配置格式如下:
1 <?xml version="1.0" encoding="UTF-8"?> 23 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xmlns:context="http://www.springframework.org/schema/context" 5 xsi:schemaLocation="http://www.springframework.org/schema/beans 6 http://www.springframework.org/schema/beans/spring-beans-4.0.xsd 7 http://www.springframework.org/schema/context 8 http://www.springframework.org/schema/context/spring-context-4.0.xsd"> 9 10 11 class="com.example.SportsImpl"> 12class="com.example.Food"> 13 14 15class="com.example.SportsBeforeAdvice"> 16class="com.example.SportsAfterAdvice"> 17class="com.example.FoodAroundAdvice"> 18 19 20class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor"> 21 24 25 2622 23 class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"> 27 28
示例:
1 package com.example; 2 3 import java.lang.reflect.Method; 4 import org.springframework.context.support.ClassPathXmlApplicationContext; 5 import org.springframework.aop.MethodBeforeAdvice; 6 import org.springframework.aop.AfterReturningAdvice; 7 8 import org.aopalliance.intercept.MethodInterceptor; 9 import org.aopalliance.intercept.MethodInvocation; 10 11 public class App { 12 public static void main( String[] args ) { 13 ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-beans.xml"); 14 15 Sports sports = context.getBean("sportsImpl", Sports.class); 16 sports.swimming(); 17 sports.test(); 18 19 Food food = context.getBean("food", Food.class); 20 food.test1(); 21 food.test2(); 22 food.eating(); 23 } 24 } 25 26 // 使用本文前面示例的 class 27 28 ...
输出:
Sports -> swimming()
FoodAroundAdvice -> invoke(): before
Sports -> test()
FoodAroundAdvice -> invoke(): after
FoodAroundAdvice -> invoke(): before
Food -> test1()
FoodAroundAdvice -> invoke(): after
FoodAroundAdvice -> invoke(): before
Food -> test2()
FoodAroundAdvice -> invoke(): after
Food -> eating()