天天看点

Spring AOP 动态代理入门(一)

1.经典Spring AOP 类图(主要针对com.springframework.aop包)

Spring AOP 动态代理入门(一)

AOP Alliance(http://aopalliance.sourceforge.net/) 是个联合的开源协作组织,在多个项目间进行协作以期提供一套标准的AOP Java接口

Spring AOP就是基于AOP Alliance标准API实现的,如果你打算使用Spring的AOP或基于AOP的任何特性,只需这个JAR文件

前置通知【BeforeAdvice】、后置通知【AfterAdvice】、异常通知【ThrowAdvice】都继承自标准接口 Advice

而环绕通知则直接使用AOP Alliance提供的标准接口【MethodInterceptor】

2.经典Spring AOP使用方式

本质上是基于JDK的动态代理实现的面向切面编程,按照JDK动态代理的要求:必须基于接口生成目标对象

(1)定义一个接口 ICalculate,包含几个简单方法 

public interface ICalculate {

    int add(int a, int b);

    int sub(int a, int b);

}
           

(2)定义一个实现类CalculateImpl

public class CalculateImpl implements ICalculate {

    /*
     * (non-Javadoc)
     * 
     * @see com.yli.sample.spring.exam.ICalculate#add(int, int)
     */
    @Override
    public int add(int a, int b) {
        return a + b;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.yli.sample.spring.exam.ICalculate#sub(int, int)
     */
    @Override
    public int sub(int a, int b) {
        if (0 == b) {
            throw new IllegalArgumentException("发生除0错误!");
        }
        return a / b;
    }
}
           

为什么要称为经典的Spring SOP使用方式呢,因为和自行实现JDK的动态代理过程是类似的,属于基础编程,不能算AOP框架的高级编程方式

======================================那么先介绍自行实现JDK的动态代理==========================================

再定义一个类实现JDK提供的InvocationHandler接口,为委托类【即CalculateImpl】生成代理对象

public class CalculateInvocationHandler implements InvocationHandler {

    // 委托对象
    private Object target;

    public CalculateInvocationHandler(Object target) {
        this.target = target;
    }

    // 为委托对象生成代理对象
    public Object newProxyInstance() throws IllegalArgumentException {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass()
                .getInterfaces(), this);
    }

    // 执行委托方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("========开始执行方法========");
        System.out.println("方法名称: " + method.getName());
        System.out.println("参数值: " + Arrays.toString(args));
        Object result = method.invoke(target, args);
        System.out.println("========结束方法执行========");
        return result;
    }
}
           

然后使用 main 方法测试

public static void main(String[] args) {
        ICalculate ical = new CalculateImpl();
        CalculateInvocationHandler handler = new CalculateInvocationHandler(ical);
        ICalculate icalProxy  = (ICalculate)handler.newProxyInstance();
        icalProxy.add(5, 3);
        
    }
           

打印执行结果

========开始执行方法========
方法名称: add
参数值: [5, 3]
========结束方法执行========
           

======================================继续介绍经典的 Spring AOP 实现方式==========================================

(3)前置通知、后置通知、异常通知、环绕通知

/**
 * Spring AOP框架-BeforeAdvice-前置通知
 */
public class CalculateLogBeforeAdvice implements MethodBeforeAdvice {

    private static Logger log = LoggerFactory.getLogger(CalculateLogBeforeAdvice.class);

    /**
     * 实现before方法
     * @param method 要执行的方法
     * @param args 方法参数
     * @param target 执行该方法的目标对象
     */
    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        // 打印方法名称
        log.info("the method " + method.getName() + "() begin with");
        // 打印参数列表
        int index = 0;
        for (Object arg : args) {
            log.info("param(" + (index++) + "):" + arg.toString());
        }
    }
}
           
/**
 * 后置通知
 */
public class CalculateLogAfterAdvice implements AfterReturningAdvice {

    private static Logger log = LoggerFactory.getLogger(CalculateLogAfterAdvice.class);

    /**
     * 实现afterReturning方法
     * @param returnValue 方法执行返回值
     * @param method 要执行的方法
     * @param args 方法参数
     * @param target 执行该方法的目标对象
     */
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target)
            throws Throwable {
        log.info("the method " + method.getName() + "() ends with " + returnValue);
    }
}
           

前置通知和后置通知,直接实现目标接口[MethodBeforeAdvice]和[AfterReturningAdvice]定义的方法

异常通知和环绕通知,其目标接口没有定义方法(参考最上面的类图也可以看出来),因此要自行实现方法

/**
 * 异常通知,目标接口最终是继承自AfterAdvice
 * 而AfterAdvice是没有定义方法的,因此目标类要实现自己的方法
 */
public class CalculateLogExceptionAdvice implements ThrowsAdvice {

    private static Logger log = LoggerFactory.getLogger(CalculateLogExceptionAdvice.class);

    /**
     * 方法名称使用afterThrowing
     * 方法参数可以只使用运行时异常
     */
    public void afterThrowing(IllegalArgumentException exp)
            throws Throwable {
        log.error(exp.getMessage());
    }
    /**
     * 方法签名可以扩展,比如要打印其他信息
     * method,args,target要么全部加上,要么一个都不加
     */
    // public void afterThrowing(Method method, Object[] args, Object target,
            // IllegalArgumentException exp) throws Throwable {
        // log.error(exp.getMessage());
    //}
}
           
/**
 * 环绕通知,表现更为直接
 * 目标接口直接来自于AOP Alliance包,并非来自Spring框架
 */
public class CalculateLogAroundAdvice implements MethodInterceptor {

    private static Logger log = LoggerFactory.getLogger(CalculateLogAroundAdvice.class);

    /**
     * 方法参数和[前置、后置、异常通知】不同
     * 因为目标方法来自于AOP标准框架
     */
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        log.info("Around:The method " + invocation.getMethod().getName() + "() begins with "
                + Arrays.toString(invocation.getArguments()));

        try {
            // 执行代理bean的业务方法
            // 相当于 method.invoke()
            // 必须要执行
            Object result = invocation.proceed();
            log.info("Around:The method " + invocation.getMethod().getName() + "() ends with " + result);
            
            // 可以返回任何值,但建议返回执行的实际结果
            return result;
        } catch (IllegalArgumentException exp) {
            log.error("Around:Illegal argument " + Arrays.toString(invocation.getArguments())
                    + " for method " + invocation.getMethod().getName() + "()");
            throw exp;
        }
    }
}
           

(4)配置bean

<?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:cxf="http://cxf.apache.org/core"
	xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
	xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
   	http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
   	http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
   	http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
   	http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
   	http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
   	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
   	http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
   	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
   	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
   	
   	<!-- 业务bean -->
   	<bean id="calculate" class="com.yli.sample.spring.aop.CalculateImpl"></bean>
   	
   	<!-- 前置通知:记录业务日志 -->
   	<bean id="beforeLog" class="com.yli.sample.spring.aop.CalculateLogBeforeAdvice"></bean>
   	
   	<!-- 后置通知:记录业务日志 -->
   	<bean id="afterLog" class="com.yli.sample.spring.aop.CalculateLogAfterAdvice"></bean>
   	
   	<!-- 异常通知:记录业务日志 -->
   	<bean id="exceptionLog" class="com.yli.sample.spring.aop.CalculateLogExceptionAdvice"></bean>
   	
   	<!-- 环绕通知:记录业务日志 -->
   	<bean id="aroundLog" class="com.yli.sample.spring.aop.CalculateLogAroundAdvice"></bean>
   	
   	<!-- 为业务Bean生成代理Bean -->
   	<bean id="calculateProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
   		<!-- JDK的动态代理 需要业务类必须实现一个接口,此处可以指定接口 -->
   		<property name="proxyInterfaces">
   			<list>
   				<value>com.yli.sample.spring.aop.ICalculate</value>
   			</list>
   		</property>
   		<!-- 如果不指定要代理的接口,spring会使用cgliab直接代理目标类 -->
   		<property name="target" ref="calculate"></property>
   		<!-- 定义代理Bean要使用的通知 -->
   		<property name="interceptorNames">
   			<list>
   				<value>beforeLog</value>
   				<value>afterLog</value>
   				<value>exceptionLog</value>
   				<value>aroundLog</value>
   			</list>
   		</property>
   	</bean>
   	
</beans>
           

(5)业务接口、实现类、各种通知和bean配置完成了,可以main方法测试

public static void main(String[] args) {
        String configLocation = "classpath:/conf/spring/aop-bean.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(configLocation);

        // 注意获取的是代理Bean,不是原生的业务Bean
        // 使用经典的Spring AOP,只有代理Bean才能触发通知
        ICalculate calculateProxy = (ICalculate) context.getBean("calculateProxy");
        calculateProxy.sub(15, 5);
        calculateProxy.sub(15, 0);
    }
           

执行结果

11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - the method sub() begin with
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - param(0):15
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - param(1):5
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogAroundAdvice - Around:The method sub() begins with [15, 5]
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogAroundAdvice - Around:The method sub() ends with 3
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogAfterAdvice - the method sub() ends with 3
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - the method sub() begin with
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - param(0):15
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogBeforeAdvice - param(1):0
11:24:17.126 [main] INFO  c.y.s.s.aop.CalculateLogAroundAdvice - Around:The method sub() begins with [15, 0]
Exception in thread "main" 11:24:17.126 [main] ERROR c.y.s.s.aop.CalculateLogAroundAdvice - Around:Illegal argument [15, 0] for method sub()
11:24:17.126 [main] DEBUG o.s.a.f.a.ThrowsAdviceInterceptor - Found handler for exception of type [java.lang.IllegalArgumentException]: public void com.yli.sample.spring.aop.CalculateLogExceptionAdvice.afterThrowing(java.lang.IllegalArgumentException) throws java.lang.Throwable
11:24:17.126 [main] ERROR c.y.s.s.a.CalculateLogExceptionAdvice - 发生除0错误!
           

(6)这种动态代理方式有两个缺点

业务类的所有方法都被代理,不能根据规则匹配方法;只能使用代理Bean来执行方法才能触发通知事件!

经典Spring AOP 提供多种方式满足以上需求,可以【增强部分】,也可以【自动创建代理Bean】

<?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:cxf="http://cxf.apache.org/core"
	xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
	xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
   	http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
   	http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
   	http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
   	http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
   	http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
   	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
   	http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
   	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
   	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
   	   	
   	<!-- 业务Bean -->
   	<bean id="autoCalculate" class="com.yli.sample.spring.aop.CalculateImpl"></bean>
   	
   	<!-- 环绕通知 -->
   	<bean id="aroundLog" class="com.yli.sample.spring.aop.CalculateLogAroundAdvice"></bean>
   	
   	<!-- 增强器 支持【部分增强】  -->
   	<bean id="pointAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor">
   		<!-- 要增强的方法名称 -->
   		<property name="mappedNames">
   			<list>
   				<value>add</value>
   				<value>sub</value>
   			</list>
   		</property>
   		<!-- 增强部分使用的【通知事件】 -->
   		<property name="advice" ref="aroundLog"></property>
   	</bean>
   	
   	<!-- BeanNameAutoProxyCreator 支持【自动代理】 -->
	<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
		<property name="beanNames">
			<list>
				<!-- 通配符匹配哪些Bean需要自动创建代理 -->
				<value>*Calculate*</value>
			</list>
		</property>
		<property name="interceptorNames">
			<list>
				<!-- 指定【增强器】 -->
				<value>pointAdvisor</value>
				<!-- 其实此处可以直接指定【通知】 -->
				<!-- <value>aroundLog</value> -->
			</list>
		</property>
	</bean>
   	
</beans>
           

main方法测试

public static void main(String[] args) {
        
        String location = "classpath:conf/spring/aop2-bean.xml";
        ApplicationContext context = new ClassPathXmlApplicationContext(location);
        
        // 使用Spring提供的自动创建代理
        // 此处获取的Bean就可以直接使用业务Bean了
        ICalculate calculate1 = (ICalculate) context.getBean("autoCalculate");
        calculate1.add(3, 5);
    }
           

除了上面那种方式,Spring还提供一个默认的自动代理,只需要指定一个【增强器】即可

Spring会自动检查IOC容器中每个Bean,自动匹配增强器

<?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:cxf="http://cxf.apache.org/core"
	xmlns:p="http://cxf.apache.org/policy" xmlns:ss="http://www.springframework.org/schema/security"
	xmlns:jaxws="http://cxf.apache.org/jaxws" xmlns:jee="http://www.springframework.org/schema/jee"
	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
   	http://cxf.apache.org/core http://cxf.apache.org/schemas/core.xsd
   	http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd
   	http://cxf.apache.org/policy http://cxf.apache.org/schemas/policy.xsd
   	http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
   	http://cxf.apache.org/bindings/soap http://cxf.apache.org/schemas/configuration/soap.xsd
   	http://www.springframework.org/schema/context  http://www.springframework.org/schema/context/spring-context.xsd
   	http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd
   	http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
   	http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
   	   	
   	<!-- 业务Bean -->
   	<bean id="autoCalculate" class="com.yli.sample.spring.aop.CalculateImpl"></bean>
   	
   	<!-- 环绕通知 -->
   	<bean id="aroundLog" class="com.yli.sample.spring.aop.CalculateLogAroundAdvice"></bean>
	
	<!-- 例如指定一个增强器【正则表达式匹配的增强器】
		前面已经设置过【根据名称匹配的增强器】
	 -->
	<bean id="regAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
   		<property name="patterns">
   			<list>
   				<value>.*sub.*</value>
   			</list>
   		</property>
   		<property name="advice" ref="aroundLog"></property>
   	</bean>
	
	<!-- DefaultAdvisorAutoProxyCreator 也支持【自动代理】
		Spring会检查IOC中每个增强器与Bean,如果曾强器切入点与Bean相匹配,则自动代理
	 -->
	<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" />
   	
</beans>