Spring AOP (一)

今天来学习 Spring AOP(Aspect-oriented programming) 的相关知识。主要是概念,一种编程的思想。


什么是AOP

Spring AOP 即 Aspect-oriented programming,面向切面编程,是作为面向对象编程的一种补充,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术,专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题。主要的功能有:日志记录,性能统计,安全控制,事务处理,异常处理等等。简单地说,就是一个拦截器( interceptor )拦截一些处理过程。

对切面的理解

程序中的每一个模块或者说功能,任何一个模块中都要记录它的日志、事务、安全验证等等,给我们带来的工作量非常大。当程序到达某种规模时,尤其是格式调整之类的,这种改动量是非常大的。如果通过切面方式,对开发人员是不可见的,默认地会对每一个子模块记录日志等这些工作。通过预编译或者动态代理的方式来执行这个功能,对开发人员是透明,他不需要知道。切面是和功能垂直的,也就是切面是横切与各个功能之上的。

AOP基本概念及特点

Spring AOP并不是全面的AOP解决方案,AspectJ才是,Spring AOP不是为了提供最完整的AOP实现(尽管它非常强大),而是侧重于提供一种AOP实现和Spring IOC容器之间的整合,用于帮助解决企业应用中的常见问题。Spring AOP不会与AspectJ竞争,从而提供综合全面的AOP解决方案。

Spring AOP架构是一个基于代理的AOP架构,它的基本流程是基于拦截器(Interceptor)链,也就是说,一个目标类方法的调用(Invocation)可以被认为是一个连接点(Joinpoint),Spring 为这个方法的调用设置拦截器(可以有多个),在这个拦截器的链条上,每一个拦截器都具备around通知(Advice),它们逐个执行,最终完成对目标方法的调用。

相关术语

- 切面(Aspect) : 一个关注点的模块化,这个关注点可能会横切多个对象,用Spring的Advisor或拦截器实现。

- 连接点(Joinpoint) : 程序执行过程中的某个特定的点,比如方法的调用或特定的异常抛出。

- 通知 : 在切面的某个特定的点执行的动作,有四种类型。

- 切入点(Pointcut) : 指定一个通知将被引发的一系列连接点的集合,在AOP中通知和一个切入点表达式关联。

- 引入(Introduction) : 在不修改类代码的前提下,为类添加新的方法和属性,Spring 允许引入新的接口到任何被通知的对象。

- 目标对象(Target Object) : 包含连接点的对象,被一个或者多个切面所通知的对象。也被称作被通知或被代理对象。

- AOP代理(AOP Proxy) : AOP框架创建的对象,包含通知,用来实现切面契约(包括通知方法执行等功能),可以是JDK动态代理或者CGLIB代理。

- 组装(Weaving) : 组装ASpect连接到其他的应用程序类型或者对象,并创建一个被通知的对象。分为:编译时织入,执行时织入。

Spring的AOP实现

通过预编译-AspectJ,运行期动态代理(JDK动态代理、CGLib动态代理)-SpringAOP、JbossAOP。纯java实现,无需特殊的编译过程,不需要控制类加载器层次,目前只支持方法执行连接点(通知Spring Bean的方法执行)

有接口和无接口的Spring AOP实现区别

Spring AOP默认使用标准的JAVASE动态代理作为AOP代理,使得任何接口(或者接口集)都可以被代理

Spring AOP中也可以使用CGLIB代理(如果一个业务对象并没有实现一个接口)

Spring AOP 的通知类型(Advice)

- 前置通知(Before advice) - method 执行前通知,在某个连接点(join point)之前执行的通知,但不能阻止连接点前的执行(除非它抛出一个异常)

- 返回后通知(After returning advice) : 在某个连接点(join point)正常完成后执行的通知。

- 抛出异常后通知(After throwing advice) : 在方法抛出异常退出时执行的通知

- 后通知(After(finally) advice) : 在某个连接点退出的时候执行的通知(不论是正常返回还是异常退出)

- 环绕通知(Around Advice) : 包围一个连接点(join point)的通知

简单实现

CustomerService类

package boy.aop.advice;

public class CustomerService {
    private String name;
    private String url;
    
    public void setName(String name) {
        this.name = name;
    }
    
    public void setUrl(String url) {
        this.url = url;
    }
    
    public void printName(){
        System.out.println("Customer name : " + this.name);
    }
    
    public void printUrl(){
        System.out.println("Customer website : " + this.url);
    }
    
    public void printThrowException(){
        throw new IllegalArgumentException();
    }

}

SpringAOPAdvice.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd">
           
          <bean id="customerService" class="boy.aop.advice.CustomerService">
                <property name="name" value="Boy"></property>   
                <property name="url" value="https://www.b521.net"></property>
          </bean>
          
</beans>

Before Advice

package boy.aop.advice;

import java.lang.reflect.Method;

import org.springframework.aop.MethodBeforeAdvice;

public class BeforeMethod implements MethodBeforeAdvice {

    @Override
    public void before(Method arg0, Object[] target, Object arg1) throws Throwable {
        System.out.println("BeforeMethod : Before method succ!");
        
    }

}

配置xml

      <bean id="beforeMethod" class="boy.aop.advice.BeforeMethod"></bean>
      
      <bean id="customerServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="customerService"></property>
        <property name="interceptorNames">
            <list>
                <value>beforeMethod</value>
            </list>
        </property>
      </bean>

测试类

package boy.aop.advice;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class App {
    
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext(
                new String[] {"SpringAOPAdvice.xml"});
        CustomerService cust = (CustomerService) context.getBean("customerServiceProxy");
        System.out.println("-------------");
        cust.printName();
        System.out.println("-------------");
        cust.printUrl();
        try {
            cust.printThrowException();
        } catch (Exception e) {
            
        }
        
    }
    
}

运行结果

-------------
BeforeMethod : Before method succ!
Customer name : Boy
-------------
BeforeMethod : Before method succ!
Customer website : https://www.b521.net
BeforeMethod : Before method succ!

每一个 customerService 的 method 运行前,都将先执行 HijackBeforeMethod 的 before 方法。


After Returning Advice

创建一个实现了接口 AfterReturningAdvice 的 class ,method 运行后,直到返回结果后,才运行下边的代码,如果没有返回结果,将不运行切入的代码。

package boy.aop.advice;

import java.lang.reflect.Method;

import org.springframework.aop.AfterReturningAdvice;

public class AfterMethod implements AfterReturningAdvice {

    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("AfterMethod : succ!");
        
    }
    
}

配置xml

      <bean id="afterMethod" class="boy.aop.advice.AfterMethod"></bean>
      
      <bean id="customerServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="customerService"></property>
        <property name="interceptorNames">
            <list>
                <value>afterMethod</value>
            </list>
        </property>
      </bean>

运行结果

22:13:35.876 [main] DEBUG org.springframework.aop.framework.CglibAopProxy - Method is declared on Advised interface: public abstract java.lang.Class org.springframework.aop.TargetClassAware.getTargetClass()
-------------
Customer name : Boy
AfterMethod : succ!
-------------
Customer website : https://www.b521.net
AfterMethod : succ!

Afetr Throwing Advice

创建一个实现了 ThrowsAdvice 接口的 class ,劫持 IllegalArgumentException 异常,目标 method 运行时,抛出 IllegalArgumentException 异常后,运行切入的方法。

ThowsAdvice类

package boy.aop.advice;

import org.springframework.aop.ThrowsAdvice;

public class ThowsAdvice implements ThrowsAdvice {
    
    public void afterThrowing(IllegalArgumentException e) throws Throwable{
        System.out.println("ThowsAdvice Exception : succ!");
    }
    
}

配置xml

      <bean id="thowsAdvice" class="boy.aop.advice.ThowsAdvice"></bean>
      
      <bean id="customerServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="customerService"></property>
        <property name="interceptorNames">
            <list>
                <value>thowsAdvice</value>
            </list>
        </property>
      </bean>

运行结果

-------------
22:06:41.021 [main] DEBUG org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor - Found exception handler method: public void boy.aop.advice.ThowsAdvice.afterThrowing(java.lang.IllegalArgumentException) throws java.lang.Throwable
Customer name : Boy
-------------
22:06:41.041 [main] DEBUG org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor - Found exception handler method: public void boy.aop.advice.ThowsAdvice.afterThrowing(java.lang.IllegalArgumentException) throws java.lang.Throwable
Customer website : https://www.b521.net
22:06:41.041 [main] DEBUG org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor - Found exception handler method: public void boy.aop.advice.ThowsAdvice.afterThrowing(java.lang.IllegalArgumentException) throws java.lang.Throwable
22:06:41.041 [main] DEBUG org.springframework.aop.framework.adapter.ThrowsAdviceInterceptor - Found handler for exception of type [java.lang.IllegalArgumentException]: public void boy.aop.advice.ThowsAdvice.afterThrowing(java.lang.IllegalArgumentException) throws java.lang.Throwable
ThowsAdvice Exception : succ!

**可以见到,在捕获到异常时执行了,前面的方法正常执行。实现了接口 MethodInterceptor 的 class ,你必须通过 methodInvocation.proceed() 来调用原来的方法,即通过调用 methodInvocation.proceed() 来调用 CustomerService 中的每一个方法,当然也可以不调用原方法 **


Around Advice

结合了以上 3 种形式的 Advice,实现了接口 MethodInterceptor 的 class ,你必须通过 methodInvocation.proceed() 来调用原来的方法,即通过调用 methodInvocation.proceed() 来调用 CustomerService 中的每一个方法,当然也可以不调用原方法。

package boy.aop.advice;

import java.util.Arrays;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

public class AroundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        System.out.println("Method name : " 
                + invocation.getMethod().getName());
        System.out.println("Method arguments : " 
                + Arrays.toString(invocation.getArguments()));
        
        //相当于 Before method
        System.out.println("AroundMethod : Before method");
        
        try {
            
            //调用原方法(CustomerService的方法)
            Object result = invocation.proceed();
            
            //相当于 After method
            System.out.println("AroundMethod : After method");
            
            return result;
            
        } catch (IllegalArgumentException e) {
            //相当于 ThrowAdvice
            System.out.println("AroundMethod : Throw exception");
            throw e;
        }
        
    }

}

配置xml

      <bean id="aroundAdvice" class="boy.aop.advice.AroundAdvice"></bean>
      
      <bean id="customerServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="target" ref="customerService"></property>
        <property name="interceptorNames">
            <list>
                <value>aroundAdvice</value>
            </list>
        </property>
      </bean>

运行结果

22:25:09.625 [main] DEBUG org.springframework.aop.framework.CglibAopProxy - Method is declared on Advised interface: public abstract java.lang.Class org.springframework.aop.TargetClassAware.getTargetClass()
-------------
Method name : printName
Method arguments : []
AroundMethod : Before method
Customer name : Boy
AroundMethod : After method
-------------
Method name : printUrl
Method arguments : []
AroundMethod : Before method
Customer website : https://www.b521.net
AroundMethod : After method
Method name : printThrowException
Method arguments : []
AroundMethod : Before method
AroundMethod : Throw exception

CustomerService 中每一个方法的调用,都会执行 AroundMethod 中的 invoke 方法,可以看到整个切入点将目标 around。一般都用 Around Advice ,因为它能够实现所有类型的 Advice 。不过还是尽量选择合适的 Advice 。

最后由 不一样的少年 编辑于2016年12月11日 22:31