AOP 的全称是“Aspect Oriented Programming”,译为“面向切面编程”,和 OOP(面向对象编程)类似,它也是一种编程思想。
通常情况下,我们会根据业务使用 OOP(面向对象)思想,将应用划分为多个不同的业务模块,每个模块的核心功能都只为特定的业务领域提供服务,例如电商系统中的订单模块、商品模块、库存模块就分别是为维护电商系统的订单信息、商品信息以及库存信息而服务的。
但除此之外,应用中往往还存在一些非业务的通用功能,例如日志管理、权限管理、事务管理、异常管理等。这些通用功能虽然与应用的业务无关,但几乎所有的业务模块都会使用到它们,因此这些通用功能代码就只能横向散布式地嵌入到多个不同的业务模块之中。这无疑会产生大量重复性代码,不利于各个模块的复用。
与 OOP 中纵向的父子继承关系不同,AOP 是通过横向的抽取机制实现的。它将应用中的一些非业务的通用功能抽取出来单独维护,并通过声明的方式(例如配置文件、注解等)定义这些功能要以何种方式作用在那个应用中,而不是在业务模块的代码中直接调用。
AOP 编程和 OOP 编程的目标是一致的,都是为了减少程序中的重复性代码,让开发人员有更多的精力专注于业务逻辑的开发,只不过两者的实现方式大不相同。
AOP 不是用来替换 OOP 的,而是 OOP 的一种延伸,用来解决 OOP 编程中遇到的问题。
Advice 直译为通知,也有人将其翻译为“增强处理”,共有 5 种类型,如下表所示。
AOP 可以被分为以下 2 个不同的类型。
动态 AOP
动态 AOP 的织入过程是在运行时动态执行的。其中最具代表性的动态 AOP 实现就是 Spring AOP,它会为所有被通知的对象创建代理对象,并通过代理对象对被原对象进行增强。
相较于静态 AOP 而言,动态 AOP 的性能通常较差,但随着技术的不断发展,它的性能也在不断的稳步提升。
动态 AOP 的优点是它可以轻松地对应用程序的所有切面进行修改,而无须对主程序代码进行重新编译。
静态 AOP
静态 AOP 是通过修改应用程序的实际 Java 字节码,根据需要修改和扩展程序代码来实现织入过程的。最具代表性的静态 AOP 实现是 AspectJ。
相较于动态 AOP 来说,性能较好。但它也有一个明显的缺点,那就是对切面的任何修改都需要重新编译整个应用程序。
AOP 是 Spring 的核心之一,在 Spring 中经常会使用 AOP 来简化编程。
在 Spring 框架中使用 AOP 主要有以下优势。
Spring 在运行期会为目标对象生成一个动态代理对象,并在代理对象中实现对目标对象的增强。
Spring AOP 的底层是通过以下 2 种动态代理机制,为目标对象(Target Bean)执行横向织入的。
注意:由于被标记为 final 的方法是无法进行覆盖的,因此这类方法不管是通过 JDK 动态代理机制还是 CGLIB 动态代理机制都是无法完成代理的。
Spring AOP 并没有像其他 AOP 框架(例如 AspectJ)一样提供了完成的 AOP 功能,它是 Spring 提供的一种简化版的 AOP 组件。其中最明显的简化就是,Spring AOP 只支持一种连接点类型:方法调用。您可能会认为这是一个严重的限制,但实际上 Spring AOP 这样设计的原因是为了让 Spring 更易于访问。
方法调用连接点是迄今为止最有用的连接点,通过它可以实现日常编程中绝大多数与 AOP 相关的有用的功能。如果需要使用其他类型的连接点(例如成员变量连接点),我们可以将 Spring AOP 与其他的 AOP 实现一起使用,最常见的组合就是 Spring AOP + ApectJ。
Spring AOP 按照通知(Advice)织入到目标类方法的连接点位置,为 Advice 接口提供了 6 个子接口,如下表。
Spring AOP 切面类型
Spring 使用 org.springframework.aop.Advisor 接口表示切面的概念,实现对通知(Adivce)和连接点(Joinpoint)的管理。
在 Spring AOP 中,切面可以分为三类:一般切面、切点切面和引介切面。
一般切面的 AOP 开发
当我们在使用 Spring AOP 开发时,若没有对切面进行具体定义,Spring AOP 会通过 Advisor 为我们定义一个一般切面(不带切点的切面),然后对目标对象(Target)中的所有方法连接点进行拦截,并织入增强代码。
/**
* 增强代码
* MethodBeforeAdvice 前置增强
*/
public class UserDaoBeforeAdvice implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] args, Object target) throws Throwable {
System.out.println("正在执行前置增强操作…………");
}
}
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="userDao" class="net.biancheng.c.dao.impl.UserDaoImpl">bean>
<bean id="beforeAdvice" class="net.biancheng.c.advice.UserDaoBeforeAdvice">bean>
<bean id="userDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="userDao"/>
<property name="proxyInterfaces" value="net.biancheng.c.dao.UserDao"/>
<property name="interceptorNames" value="beforeAdvice"/>
bean>
beans>
Spring 能够基于 org.springframework.aop.framework.ProxyFactoryBean 类,根据目标对象的类型(是否实现了接口)自动选择使用 JDK 动态代理或 CGLIB 动态代理机制,为目标对象(Target Bean)生成对应的代理对象(Proxy Bean)。
PointCutAdvisor 是 Adivsor 接口的子接口,用来表示带切点的切面。使用它,我们可以通过包名、类名、方法名等信息更加灵活的定义切面中的切入点,提供更具有适用性的切面。
Spring 提供了多个 PointCutAdvisor 的实现,其中常用实现类如如下。
创建一个名为 OrderDaoAroundAdvice 的环绕增强类
/**
* 增强代码
* 环绕增强
*/
public class OrderDaoAroundAdvice implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
System.out.println("环绕增强前********");
//执行被代理对象中的逻辑
Object result = methodInvocation.proceed();
System.out.println("环绕增强后********");
return result;
}
}
<bean id="orderDao" class="com.xawl.dao.OrderDao">bean>
<bean id="aroundAdvice" class="com.xawl.advice.OrderDaoAroundAdvice">bean>
<bean id="myPointCutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="patterns" value="com.xawl.dao.OrderDao.add.*,com.xawl.dao.OrderDao.delete.*">property>
<property name="advice" ref="aroundAdvice">property>
bean>
<bean id="orderDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="target" ref="orderDao">property>
<property name="proxyTargetClass" value="true">property>
<property name="interceptorNames" value="myPointCutAdvisor">property>
bean>
所有目标对象(Target Bean)的代理对象(Proxy Bean)都是在 XML 配置中通过 ProxyFactoryBean 创建的。但在实际开发中,一个项目中往往包含非常多的 Bean, 如果每个 Bean 都通过 ProxyFactoryBean 创建,那么开发和维护成本会十分巨大。为了解决这个问题,Spring 为我们提供了自动代理机制。
Spring 提供的自动代理方案,都是基于后处理 Bean 实现的,即在 Bean 创建的过程中完成增强,并将目标对象替换为自动生成的代理对象。通过 Spring 的自动代理,我们在程序中直接拿到的 Bean 就已经是 Spring 自动生成的代理对象了。
Spring 为我们提供了 3 种自动代理方案:
根据 Bean 名称创建代理对象
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="userDao" class="com.xawl.dao.impl.UserDaoImpl">bean>
<bean id="orderDao" class="com.xawldao.OrderDao">bean>
<bean id="beforeAdvice" class="com.xawl.advice.UserDaoBeforeAdvice">bean>
<bean id="aroundAdvice" class="com.xawl.advice.OrderDaoAroundAdvice">bean>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
<property name="beanNames" value="*Dao">property>
<property name="interceptorNames" value="beforeAdvice,aroundAdvice">property>
bean>
beans>
根据切面中信息创建代理对象
<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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd">
<bean id="userDao" class="com.xawl.dao.impl.UserDaoImpl">bean>
<bean id="orderDao" class="com.xawl.dao.OrderDao">bean>
<bean id="beforeAdvice" class="com.xawl.advice.UserDaoBeforeAdvice">bean>
<bean id="aroundAdvice" class="com.xawl.advice.OrderDaoAroundAdvice">bean>
<bean id="myPointCutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
<property name="patterns"
value="com.xawl.dao.OrderDao.add.*,net.biancheng.c.dao.OrderDao.delete.*">property>
<property name="advice" ref="aroundAdvice">property>
bean>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">bean>
beans>
在 Spring 中,虽然我们可以使用 XML 配置文件可以实现 AOP 开发,但如果所有的配置都集中在 XML 配置文件中,就势必会造成 XML 配置文件过于臃肿,从而给维护和升级带来一定困难。
为此,AspectJ 框架为 AOP 开发提供了一套 @AspectJ 注解。它允许我们直接在 Java 类中通过注解的方式对切面(Aspect)、切入点(Pointcut)和增强(Advice)进行定义,Spring 框架可以根据这些注解生成 AOP 代理。
在使用 @AspectJ 注解进行 AOP 开发前,首先我们要先启用 @AspectJ 注解支持。
1)使用 Java 配置类启用
我们可以在 Java 配置类(标注了 @Configuration 注解的类)中,使用 @EnableAspectJAutoProxy 和 @ComponentScan 注解启用 @AspectJ 注解支持。
@Configuration
@ComponentScan(basePackages = "net.biancheng.c") //注解扫描
@EnableAspectJAutoProxy //开启 AspectJ 的自动代理
public class AppConfig {
}
2)基于 XML 配置启用
<context:component-scan base-package="net.biancheng.c">context:component-scan>
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
在启用了 @AspectJ 注解支持的情况下,Spring 会自动将 IoC 容器(ApplicationContext)中的所有使用了 @Aspect 注解的 Bean 识别为一个切面。
在定义完 Bean 后,我们只需要在Bean 对应的 Java 类中使用一个 @Aspect 注解,将这个 Bean 定义为一个切面,代码如下。
import org.aspectj.lang.annotation.*;
@Aspect //定义为切面
public class MyAspect {
}
全注解方式定义切面
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Component // 定义成 Bean
@Aspect //定义为切面
public class MyAspect {
}
在以上代码中共使用两个注解:
在 AspectJ 中,我们可以使用 @Pointcut 注解用来定义一个切点。需要注意的是,定义为切点的方法,它的返回值类型必须为 void,代码如下
// 要求:方法必须是private,返回值类型为 void,名称自定义,没有参数
@Pointcut("execution(*net.biancheng..*.*(..))")
private void myPointCut() {
}
/**
* 将 com.xawl.dao包下 UserDao 类中的 get() 方法定义为一个切点
*/
@Pointcut(value ="execution(* com.xawl.dao.UserDao.get(..))")
public void pointCut1(){
}
/**
* 将 net.biancheng.c.dao包下 UserDao 类中的 delete() 方法定义为一个切点
*/
@Pointcut(value ="execution(* com.xawl.dao.UserDao.delete(..))")
public void pointCut2(){
}
以上这些通知注解中都有一个 value 属性,这个 value 属性的取值就是这些通知(Advice)作用的切点(PointCut),它既可以是切入点表达式,也可以是切入点的引用(切入点对应的方法名称),示例代码如下。
@Pointcut(value ="execution(* com.xawl.dao.UserDao.get(..))")
public void pointCut1(){
}
@Pointcut(value ="execution(* com.xawl.dao.UserDao.delete(..))")
public void pointCut2(){
}