Spring AOP


1.AOP的概念

AOP(Aspect Oriented Programming)面向切面编程,与OOP(Object Oriented Programming,面向对象编程)相辅相成,提供了与OOP不同的抽象软件结构的视角。在OOP中,以类为程序的基本单元,而AOP的基本单元是Aspect(切面)。

尽管OOP可以通过封装、继承达到代码的复用,但仍然有同样的代码分散在各个方法中。AOP采取横向抽取机制,将分散在各个方法中的重复代码提取出来,然后在编译或运行阶段应用到需要执行的地方。这种横向抽取是OOP无法实现的,因为OOP实现的父子关系的纵向复用。AOP不是OOP的替代品,而是补充,两者相辅相成。

1.1 切面

Aspect 是指:封装横切到系统功能(如事务处理)的类。

1.2 连接点

Joinpoint :程序运行中的一些时间点,例如:方法的调用、异常的抛出。

1.3 切入点

Pointcut :指需要处理的连接点。Spring AOP中,所有的方法执行都是连接点,而切入点是一个描述信息,修饰连接点。通过切入点确定哪些连接点需要被处理。

1.4 通知

Advice :由切面添加到特定的连接点(满足切入点规则)的一段代码,即在定义好的切入点处所要执行的代码。可以理解为切面开启后切面的方法,通知是切面的具体实现。

根据Spring中通知中目标类方法中的连接点位置,通知可分为6种类型:

1.4.1 环绕通知

​ MethodInterceptor 在目标方法执行前、后实施增强,可用于日志记录、事务处理等功能。@Around

1.4.2 前置通知

​ MethodBeforeAdvice 在目标方法执行前实施增强,可用于权限管理等功能。@Before

1.4.3 后置返回通知

​ AfterReturningAdvice 在目标方法执行成功后实施增强,可用于关闭流、删除临时文件等功能。@AfterReturning

1.4.4 后置(最终)通知

​ AfterAdvice 在目标方法执行后实施增强,与后置返回不同的是,不管是否发生异常都要执行,类似于finally。可用于释放资源。@After

1.4.5 异常通知

​ ThrowsAdvice 在方法抛出异常后实施增强,可用于处理异常、记录日志等功能。@AfterThrowing

1.4.6 引入通知

​ IntroductionInterceptor 在目标类中添加一些新的方法和属性,可用于修改目标类(增强类)。

1.5 引入

Introduction :在不修改代码的前提下,引入可以在运行期为实现类动态的添加自定义的方法和属性

1.6 目标对象

Target Object :指所有被通知的对象。

1.7 代理

Proxy :通知应用到目标对象之后被动态创建的对象。

1.8 织入

Weaving :将切面代码插入到目标对象上,从而生成代理对象的过程。

织入方式:

  • 编译期织入:需要有特殊的Java编译器
  • 类装载期织入:需要有特殊的类装载器
  • 动态代理织入:在运行期为目标类添加通知生成子类的方式。Spring AOP默认采用动态代理织入。

2.动态代理

动态代理

Spring AOP中常用JDK和CGLIB两种动态代理技术。

2.1 JDK动态代理

JDK动态代理必须借助一个接口才能产生代理对象。对于使用业务接口的类,Spring默认使用JDK动态代理实现AOP。

package dynamic.jdk;
public interface TestDao {
    public void save();
    public void modify();
    public void delete();
}

创建接口实现类作为目标类,在代理类中对其方法进行增强处理。

package dynamic.jdk;
public class TestDaoImpl implements TestDao {
    @Override
    public void save() {
        System.out.println("保存");
    }
    @Override
    public void modify() {
        System.out.println("修改");
    }
    @Override
    public void delete() {
        System.out.println("删除");
    }
}

创建切面类,定义多个通知(增强处理的功能方法)。

package aspect;
public class MyAspect {
    public void check() {
        System.out.println("模拟权限控制");
    }
    public void except() {
        System.out.println("模拟异常处理");
    }
    public void log() {
        System.out.println("模拟日志记录");
    }
}

创建代理类,JDK动态代理中代理类必须实现java.lang.reflect.InvocationHandler接口,并编写代理方法。

package dynamic.jdk;
public class JDKDynamicProxy implements InvocationHandler {
    // 声明目标类接口对象(真实对象)
    private TestDao testDao;
    /**
     * 创建代理的方法,建立代理对象和真实对象的代理关系,并返回代理对象
     */
    public Object createProxy(TestDao testDao) {
        this.testDao=testDao;
        // 类加载器
        ClassLoader cld=JDKDynamicProxy.class.getClassLoader();
        // 被代理对象实现的所有接口
        Class[] clazz=testDao.getClass().getInterFaces();
        // 使用代理类进行增强,返回代理后的对象
        return Proxy.newProxyInstance(cld, clazz, this);
    }
    /**
     * 代理的逻辑方法,所有动态代理类的方法调用都交给该方法处理
     * proxy 是被代理对象
     * method 是将要被执行的方法
     * args 是执行方法是需要的参数
     * return 是返回代理结果
     */
     @Override
     public Object invoke(Object proxy, Method method, Object[] args) throws
     Throwable {
         // 创建一个切面
         MyAspect myAspect=new MyAspect();
         // 前置增强
         myAspect.check();
         myAspect.except();
         // 在目标类上调用方法并传入参数,相当于调用testDao中的方法
         Object obj=method.invoke(testDao, args);
         // 后置增强
         myAspect.log();
         myAspect.monitor();
         return obj;
     }
}

创建测试类,在main方法中创建代理对象和目标对象,然后从代理对象中获取对目标对象增强后的对象,最后调用该对象的添加、修改、删除方法。

package dynamic.jdk;
public class JDKDynamicTest {
    public static void main(String[] args) {
        // 创建代理对象
        JDKDynamicProxy jdkProxy=new JDKDynamicProxy();
        // 创建目标对象
        TestDao testDao=new TestDaoImpl();
        // 从代理对象中获取增强后的目标对象。该对象是一个被代理的对象,它会进入代理的逻辑方法invoke中
        TestDao testDaoAdvice=(TestDao)jdkProxy.createProxy(testDao);
        // 执行方法
        testDaoAdvice.save();
        System.out.println("===============");
        testDaoAdvice.modify();
        System.out.println("===============");
        testDaoAdvice.delete();
    }
}

运行结果:
模拟权限控制
模拟异常处理
保存
模拟日志记录
===============
模拟权限控制
模拟异常处理
修改
模拟日志记录
===============
模拟权限控制
模拟异常处理
删除
模拟日志记录

2.2 CGLIB 动态代理

JDK动态代理必须提供接口才能使用,对于没有提供接口的类,只能采用CGLIB动态代理。

CGLIB(Code Generation Library,代码生成库)是一个高性能开源的代码生成库,采用非常底层的字节码技术,对指定的目标类生成一个子类,并对子类进行增强。Spring Core包已经集成了所需的jar包。

创建目标类,不需要实现任何接口。

package dynamic.cglib;
public class TestDao {
    public void save() {
        System.out.println("保存");
    }
    public void modify() {
        System.out.println("修改");
    }
    public void delete() {
        System.out.println("删除");
    }
}

创建代理类,该类实现MethodInterceptor接口

package dynamic.cglib;
public class CglibDynamicProxy implements MethodInterceptor {
    /**
     * 创建代理的方法,生成CGLIB代理对象
     * target 是目标对象,需要增强的对象
     * 返回目标对象的CGLLIB代理对象
     */
    public Object createProxy(Object target) {
        // 创建一个动态类对象,即增强类对象
        Enhancer enhancer=new Enhancer();
        // 确定需要增强的类,设置其父类
        enhancer.setSuperclass(target.getClass());
        // 确定代理逻辑对象为当前对象,要求当前对象实现MethodInterceptor的方法
        enhancer.setCallback(this);
        // 返回创建的代理对象
        return enhancer.create();
    }

    /**
     * intercept 方法会在程序执行目标方法时被调用
     * proxy 是CGLIb根据指定父类生成的代理对象
     * method 是拦截方法
     * args 是拦截方法的参数数组
     * methodProxy 是方法的代理对象,用于执行父类的方法
     * 返回代理结果
     */
    @Override
    public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        MyAspect myAspect=new MyAspect();
        myAspect.check();
        myAspect.except();
        // 目标方法执行,返回代理结果
        Object obj=methodProxy.invokeSuper(proxy, args);
        myAspect.log();
        myAspect.monitor();
        return obj;
    }
}

创建测试类。

package dynamic.cglib;
public class CglibDynamicTest {
    public static void main(String[] args) {
        // 创建代理对象
        CglibDynamicProxy cdp=new CglibDynamicProxy();
        // 创建目标对象
        TestDao testDao=new TestDao();
        // 获取增强后的目标对象
        TestDao testDaoAdvice=(TestDao)cdp.createProxy(testDao);
        // 执行方法
        testDaoAdvice.save();
        System.out.println("==============");
        testDaoAdvice.modify();
        System.out.println("==============");
        testDaoAdvice.delete();
    }
}

运行结果相同

3.基于代理类的AOP实现

纵观AOP编程,程序员只需要参与三个部分:

  1. 定义普通业务组件
  2. 定义切入点,一个切入点可能横切多个业务组件
  3. 定义增强处理,增强处理就是AOP框架为普通业务组件织入的处理动作

所以进行AOP编程的关键就是定义切入点和定义增强处理,一旦定义了合适的切入点和增强处理,AOP框架将自动生成AOP代理,即:代理对象的方法=增强处理+被代理对象的方法。

Spring默认使用JDK动态代理实现AOP编程。使用org.springframework.aop.framework.ProxyFactoryBean 创建代理是Spring AOP实现的最基本方式。

3.1 ProxyFactoryBean

ProxyFactoryBean 是org.springframework.beans.factory.FactoryBean 接口的实现类,FactoryBean负责实例化一个Bean实例,ProxyFactoryBean负责为其他Bean实例创建代理实例。

下面通过一个实现环绕通知的实例演示Spring使用ProxyFactoryBean创建AOP代理的过程。

3.1.1 创建切面类

由于该实例实现环绕通知,切面类需要实现 MethodInterceptor 接口。

package spring.proxyfactorybean;
public class MyAspect implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation arg) throw Throwable {
        check();
        except();
        Object obj=arg.proceed();
        log();
        return obj;
    }
    public void check() {
        System.out.println("模拟权限控制");
    }
    public void except() {
        System.out.println("模拟异常处理");
    }
    public void log() {
        System.out.println("模拟日志记录");
    }
}
3.1.2 配置切面并指定代理

切面类需要配置为Bean实例,这样Spring容器才能识别为切面对象。

applicationContext.xml

<!-- 定义目标对象(使用上一个案例的) -->
<bean id="testDao" class="dynamic.jdk.TestDaoImpl"/>

<!-- 创建一个切面 -->
<bean id="myAspect" class="spring.proxyfactorybean.MyAspect"/>

<!-- 使用Spring代理工厂定义一个名为testDaoProxy的代理对象 -->
<bean id="testDaoProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
    <!-- 指定代理实现的接口 -->
    <property name="proxyInterfaces" value="dynamic.jdk.TestDao"/>
    <!-- 指定目标对象 -->
    <property name="target" ref="testDao"/>
    <!-- 指定切面,织入环绕通知 -->
    <property name="interceptorNames" value="myAspect"/>
    <!-- 指定代理方式,true指定CGLIB动态代理(默认为false,指定JDK动态代理)-->
    <property name="proxyTargetClass" value="true"/>
</bean>
3.1.3 测试
package spring.proxyfactorbean;
public class ProxyFactoryBeanTest {
    public static void main(String[] args) {
        ApplicationContext appCon=new ClassPathXmlApplicationContext("/spring/proxyfactorybean/applicationContext.xml");
        TestDao testDaoAdvice=(TestDao)appCon.getBean("testDaoProxy");
        testDaoAdvice.save();
        System.out.println("===============");
        testDaoAdvice.modify();
        System.out.println("===============");
        testDaoAdvice.delete();
    }
}

运行结果与之前相同

4.基于注解开发AspectJ

AspectJ 是一个基于Java的AOP框架。从Spring 2.0以后引入了AspectJ的支持。建议使用AspectJ实现AOP。

AspectJ实现Spring AOP有两种方式:基于XML、基于注解。基于注解要比基于XML配置开发便捷许多。

注解名称 描述
@Aspect 用于定义一个切面。注解在切面类上。
@Pointcut 用于定义切入点表达式。在使用时需要定义一个切入点方法,该方法是一个返回void且方法体为空的普通方法。
@Before 用于定义前置通知。在使用时通常为其指定value属性值。
@AfterReturning 用于定义后置返回通知。在使用时通常为其指定value属性值。
@Around 用于定义环绕通知。在使用时通常为其指定value属性值。
@AfterThrowing 用于定义异常通知。在使用时通常为其指定value属性值。还有一个throwing属性,用于访问目标方法抛出的异常,该属性值与异常通知方法中同名的形参一致。
@After 用于定义后置(最终)通知。在使用时通常为其指定value属性值。

4.1 创建切面类,并进行注解

首先需要使用@Aspect 定义一个切面类,由于该类在Spring中是作为组件使用的,所以还需要使用@Component 。然后使用@Pointcut 注解切入点表达式,并通过定义方法来表示切入点名称。最后在每个通知方法上添加相应的注解,并将切入点名称作为参数传递给需要执行增强的通知方法。

package aspectj.annotation;
@Aspect
@Component
public class MyAspect {
    /**
     * execution(* dynamic.jdk.*.*(..))定义切入点表达式
     * 意思是:匹配dynamic.jdk包中任意类的任意方法的执行
     * 第一个* 返回类型,使用*代表所有类型。注意第一个*与包名之间有一个空格
     * 第二个* 表示的类名,使用*代表匹配包中的所有类
     * 第三个* 表示的是方法名,使用*表示所有方法
     * (..)表示方法的参数,"..."表示任意参数
     */
    @Pointcut("execution(* dynamic.jdk.*.*(..))")
    private void myPointCut() {}

    /**
     * 前置通知,使用Joinpoint接口作为参数获取目标对象信息
     */
    @Before("myPointCut()")
    public void before(JoinPoint jp) {
        System.out.print("前置通知:模拟权限控制");
        System.out.println(", 目标类对象:" + jp.getTarget() + ", 被增强处理的方法:" + jp.getSignature().getName());
    }

    /**
     * 后置返回通知
     */
    @AfterReturning("myPointCut()")
    public void afterReturning(JoinPoint jp) {
        System.out.print("后置返回通知:模拟删除临时文件");
        System.out.println(", 被增强处理的方法:" + jp.getSignature().getName());
    }

    /**
     * 环绕通知
     * ProceedingJoinPoint 是JoinPoint的子接口,代表可以执行的目标方法
     * 返回值的类型必须是Object
     * 必须一个参数是ProceedingJoinPoint类型
     * 必须 throws Throwable
     */
    @Around("myPointCut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.print("环绕开始:执行目标方法前,模拟开启事务");
        // 执行当前目标方法
        Object obj=pjp,proceed();
        System.out.println(", 目标类对象:" + jp.getTarget() + ", 被增强处理的方法:" + jp.getSignature().getName());
        return obj;
    }

    /**
     * 异常通知
     */
    @AfterThrowing(value="myPointCut()", throwing="e")
    public void except(Throwable e) {
        System.out.println("异常通知:" + "程序执行异常" + e.getMessage());
    }

    /**
     * 后置(最终)通知
     */
    @After("myPointCut()")
    public void after() {
        System.out.println("最终通知:模拟释放资源");
    }
}

4.2 注解目标类

使用@Repository 将目标类 TestDaoImpl 注解为目标对象。

@Repository("testDao")

4.3 创建配置文件

applicationContext.xml

<!-- 指定需要扫描的包,使注解生效 -->
<context:component-scan base-package="aspectj.annotation"/>
<context:component-scan base-package="dynamic.jdk"/>
<!-- 启动基于注解的AspectJ支持 -->
<aop:aspectj-autoproxy />

4.4 测试

public class AnnotationAspectJTest {
    public static void main(String[] args) {
        ApplicationContext appCon=new ClassPathXmlApplicationContext("/aspectj/applicationContext.xml");
        TestDao testDaoAdvice=(TestDao)appCon.getBean("testDao");
        testDaoAdvice.save();
    }
}

运行结果:
前置通知:模拟权限控制,目标类对象:dynamic.jdk.TestDaoImpl@647fd8ce,被增强处理的方法:save
环绕开始:执行目标方法前,模拟开启事务
保存
最终通知:模拟释放资源
环绕结束:执行目标方法后,模拟关闭事务
后置返回通知:模拟删除临时文件,被增强处理的方法:save

异常通知得到执行,需要在TestDaoImpl类的save方法中添加异常代码,例如“ int n = 10/0; ”。

运行结果:
前置通知:模拟权限控制,目标类对象:dynamic.jdk.TestDaoImpl@647fd8ce,被增强处理的方法:save
环绕开始:执行目标方法前,模拟开启事务
最终通知:模拟释放资源
异常通知:程序执行异常/ by zero

  TOC