1. 参考AOP入门(一)中的例子,外面来实现用Spring AOP加入各种统计的东东。
     
    注:
    这篇文章中,我用的是完全传统的Spring AOP,不带有任何AspectJ的东西。
     
  2. 在前面AOP实现一文中提到,Spring AOP要求被代理类必须由Spring容器来管理,即是一个SpringBean。所以,我们要做的第一步,就是配置引入Spring容器管理。
  1. 在src/main/resources/下添加applicationContext.xml (名字可任意)如下

<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xsi:schemaLocation="
http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop  http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
 
 
    <bean id="zoo" class="com.edi.poc.Zoo"/>
    <bean id="dinoHall" class="com.edi.poc.DinoHall"/>
    <bean id="jack" class="com.edi.poc.Tourer"/>
 
</beans>

  1. 修改main代码如下

public static void main(String[] args) {
 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
 Zoo zoo = (Zoo) ctx.getBean("zoo");
 Hall dinoHall = (Hall)ctx.getBean("dinoHall");
 Tourer jack = (Tourer)ctx.getBean("jack");
 zoo.open();
 jack.visit(zoo, HALL_NAME.DINOSAUR);
 zoo.close();
}

这里传入的applicationContext.xml就是Spring配置文件的名字。定的是什么,这里就传什么。所以说,配置名任意。

  1. 运行下试试

十月 15, 2013 11:07:56 上午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@1320a41: startup date [Tue Oct 15 11:07:56 CST 2013]; root of context hierarchy
十月 15, 2013 11:07:57 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [applicationContext.xml]
十月 15, 2013 11:07:57 上午 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
信息: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@12ebf9a: defining beans [zoo,dinoHall,jack]; root of factory hierarchy
十月 15, 2013 11:07:57 上午 org.springframework.beans.factory.support.DefaultSingletonBeanRegistry destroySingletons
信息: Destroying singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@12ebf9a: defining beans [zoo,dinoHall,jack]; root of factory hierarchy
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'zoo' defined in class path resource [applicationContext.xml]: Instantiation of bean failed; nested exception is org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [com.edi.poc.Zoo]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.edi.poc.Zoo.<init>()
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1007)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:953)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:487)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:458)
 at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:295)
 at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:292)
 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
 at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:628)
 at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:932)
 at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:479)
 at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:139)
 at org.springframework.context.support.ClassPathXmlApplicationContext.<init>(ClassPathXmlApplicationContext.java:83)
 at com.edi.poc.Main.main(Main.java:12)
Caused by: org.springframework.beans.BeanInstantiationException: Could not instantiate bean class [com.edi.poc.Zoo]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.edi.poc.Zoo.<init>()
 at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:83)
 at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateBean(AbstractAutowireCapableBeanFactory.java:1000)
 ... 13 more
Caused by: java.lang.NoSuchMethodException: com.edi.poc.Zoo.<init>()
 at java.lang.Class.getConstructor0(Unknown Source)
 at java.lang.Class.getDeclaredConstructor(Unknown Source)
 at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:78)
 ... 14 more

报错了,为何? [com.edi.poc.Zoo]: No default constructor found; nested exception is java.lang.NoSuchMethodException:

因为我们的zoo没有无参构造函数,Spring的BeanFactory默认会去调无参构造函数。修改如下:

<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xsi:schemaLocation="
http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop  http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
 
 
    <bean id="zoo" class="com.edi.poc.Zoo">
constructor-arg
    </bean>
    <bean id="dinoHall" class="com.edi.poc.DinoHall"/>
    <bean id="jack" class="com.edi.poc.Tourer">
constructor-arg
    </bean>
 
</beans>

得到结果:

Dinosaur hall is opened.
The People Zoo is opened.
Charge Jack $1.00 for ticket.
Jack needs to be charged first.
Dianosaur hall charges Jack $2.00
Jack visited diano hall.
Dinosaur hall is closed.
The People Zoo is closed.

  1. 好,现在加入整个公园的客流量,显然是在Zoo的enter方法出+1,即before enter +1
  1. 创建统计类

package com.edi.poc.statistic;
 
public class Statistic {
 
 private static int totalTraffic = 0;
 
 public static void increaseTotalTraffic()
 {
 totalTraffic ++;
 }
  
 public static int getTotalTraffic()
 {
 return totalTraffic;
 }
}

  1. 创建一个简单的beforeadvice

package com.edi.poc.aop;
 
import java.lang.reflect.Method;
 
import org.springframework.aop.MethodBeforeAdvice;
 
public class MyBeforeMethod implements MethodBeforeAdvice {
 
 public void before(Method arg0, Object[] arg1, Object arg2)
 throws Throwable {
 System.out.println("Before method...");
 }
 
}

  1. 修改配置文件

<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xsi:schemaLocation="
http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop  http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
 
 
    <bean id="zoo" class="com.edi.poc.Zoo">
        <constructor-arg value="People"/>
    </bean>
    <bean id="dinoHall" class="com.edi.poc.DinoHall"/>
    <bean id="jack" class="com.edi.poc.Tourer">
        <constructor-arg value="Jack"/>
    </bean>
 
    <bean id="myBeforeAdvice" class="com.edi.poc.aop.MyBeforeMethod"/>
 
    <bean id="customServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="target" ref="zoo"/>
            
            <property name="interceptorNames">
                <list>
                    <value>myBeforeAdvice</value>
                </list>
            </property>
            <!--
<property name="proxyTargetClass">
                 <value>true</value>
             </property>
            -->
    </bean>
</beans>

注:

Spring的ProxyFactory在创建Bean的时候,动态的去选择JDK或CGLIB来做反射生成目标类。可以通过上面蓝色部分强制声明为CGLIB。

  1. 运行下试试

十月 15, 2013 11:45:36 上午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@e14d81: startup date [Tue Oct 15 11:45:36 CST 2013]; root of context hierarchy
十月 15, 2013 11:45:36 上午 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
信息: Loading XML bean definitions from class path resource [applicationContext.xml]
十月 15, 2013 11:45:36 上午 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
信息: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@14c428f: defining beans [zoo,dinoHall,jack,myBeforeAdvice,customServiceProxy]; root of factory hierarchy
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'customServiceProxy': FactoryBean threw exception on object creation; nested exception is org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class com.edi.poc.Zoo]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
 at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:149)
 at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.getObjectFromFactoryBean(FactoryBeanRegistrySupport.java:102)
 at org.springframework.beans.factory.support.AbstractBeanFactory.getObjectForBeanInstance(AbstractBeanFactory.java:1454)
 at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:249)
 at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:194)
 at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1117)
 at com.edi.poc.Main.main(Main.java:13)
Caused by: org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class [class com.edi.poc.Zoo]: Common causes of this problem include using a final class or a non-visible class; nested exception is java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
 at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:217)
 at org.springframework.aop.framework.ProxyFactoryBean.getProxy(ProxyFactoryBean.java:363)
 at org.springframework.aop.framework.ProxyFactoryBean.getSingletonInstance(ProxyFactoryBean.java:317)
 at org.springframework.aop.framework.ProxyFactoryBean.getObject(ProxyFactoryBean.java:243)
 at org.springframework.beans.factory.support.FactoryBeanRegistrySupport.doGetObjectFromFactoryBean(FactoryBeanRegistrySupport.java:142)
 ... 6 more
Caused by: java.lang.IllegalArgumentException: Superclass has no null constructors but no arguments were given
 at org.springframework.cglib.proxy.Enhancer.emitConstructors(Enhancer.java:721)
 at org.springframework.cglib.proxy.Enhancer.generateClass(Enhancer.java:499)
 at org.springframework.cglib.transform.TransformingClassGenerator.generateClass(TransformingClassGenerator.java:33)
 at org.springframework.cglib.core.DefaultGeneratorStrategy.generate(DefaultGeneratorStrategy.java:25)
 at org.springframework.cglib.core.AbstractClassGenerator.create(AbstractClassGenerator.java:216)
 at org.springframework.cglib.proxy.Enhancer.createHelper(Enhancer.java:377)
 at org.springframework.cglib.proxy.Enhancer.create(Enhancer.java:285)
 at org.springframework.aop.framework.CglibAopProxy.getProxy(CglibAopProxy.java:205)
 ... 10 more

这里原因就是我实现的Zoo类有一个非空参的构造函数,那么空参构造函数就必须自己声明,CGLIB要求必须提供一个空参构造器。

加上空参构造器后再执行,打印如下:

Before method...
Before method...
Dinosaur hall is opened.
The People Zoo is opened.
Before method...
Charge Jack $1.00 for ticket.
Jack needs to be charged first.
Dianosaur hall charges Jack $2.00
Jack visited diano hall.
Before method...
Dinosaur hall is closed.
The People Zoo is closed.

这里看到 "Before method" 打印了四次,因为zoo总共被调用了四次

Before .open()

Before .addHall()

Before.enter()

Before .close()

  1. 其实我们只要before.enter(),所以要指定改BeforeAdvice到enter()这个joinpoint,这个绑定就需要用到advisor了。advisor是Spring自己创造的一个专门做这种绑定的东东。修改配置如下:

<beans xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xmlns:aop="http://www.springframework.org/schema/aop"
      xsi:schemaLocation="
http://www.springframework.org/schema/beans  http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop  http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">
 
    <!-- Configure Target Objects -->
    <bean id="zoo" class="com.edi.poc.Zoo">
        <constructor-arg value="People"/>
    </bean>
    <bean id="dinoHall" class="com.edi.poc.DinoHall"/>
    <bean id="jack" class="com.edi.poc.Tourer">
        <constructor-arg value="Jack"/>
    </bean>
 
    <!-- Configure pointcut -->
    <bean id="customPointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
        <property name="mappedName" value="enter"/>
    </bean>
    
    <!-- Configure Advice -->
    <bean id="myBeforeAdvice" class="com.edi.poc.aop.MyBeforeMethod"/>
    
    <!-- Configure Advisor -->
    <bean id="customAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="pointcut" ref="customPointcut"/>
        <property name="advice" ref="myBeforeAdvice"/>
    </bean>
    
    <bean id="customServiceProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="target" ref="zoo"/>
            
            <property name="interceptorNames">
                <list>
                    <value>customAdvisor</value>
                </list>
            </property>
            <!--
            <property name="proxyTargetClass">
                <value>true</value>
            </property>
            -->
    </bean>
</beans>

这里定义了一个pointcut选取名字为“enter”的方法(joinpoint),定义了一个customadvisor把这个pointcut和advice关联起来。

执行结果:

Dinosaur hall is opened.

The People Zoo is opened.

Before method...

Charge Jack $1.00 for ticket.

Jack needs to be charged first.

Dianosaur hall charges Jack $2.00

Jack visited diano hall.

Dinosaur hall is closed.

The People Zoo is closed.

  1. 修改advice为我们真正的义务逻辑

public void before(Method arg0, Object[] arg1, Object arg2)
 throws Throwable {
 System.out.println("Before method...");
 Statistic.increaseTotalTraffic();
 }

再修改下main,把traffic打出来

public static void main(String[] args) {
 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
 Zoo zoo = (Zoo) ctx.getBean("customServiceProxy");
 Hall dinoHall = (Hall)ctx.getBean("dinoHall");
 zoo.addHall(HALL_NAME.DINOSAUR, dinoHall);
 Tourer jack = (Tourer)ctx.getBean("jack");
 zoo.open();
 jack.visit(zoo, HALL_NAME.DINOSAUR);
 System.out.println("Current traffic: " + Statistic.getTotalTraffic());
 zoo.close();
 }

  1. 运行,结果如下:

Dinosaur hall is opened.
The People Zoo is opened.
Before method...
Charge Jack $1.00 for ticket.
Jack needs to be charged first.
Dianosaur hall charges Jack $2.00
Jack visited diano hall.
Current traffic: 1
Dinosaur hall is closed.
The People Zoo is closed.

  1. 我们再来实现统计场馆收入的记录,即after visit
  1. 修改统计类,加入income记录如下:

public class Statistic {
 
 private static int totalTraffic = 0;
  
 private static Map<HALL_NAME, BigDecimal> incomeOfHalls = new HashMap<HALL_NAME, BigDecimal>();
  
 public static void increaseTotalTraffic()
 {
 totalTraffic ++;
 }
  
 public static int getTotalTraffic()
 {
 return totalTraffic;
 }
  
 public static BigDecimal getIncome(HALL_NAME hallName)
 {
 BigDecimal currentAmt = incomeOfHalls.get(hallName);
 if(currentAmt!=null)
 return currentAmt;
 return BigDecimal.valueOf(0);
 }
  
 public static void increaseIncome(HALL_NAME hallName, BigDecimal amount)
 {
 BigDecimal currentAmount = incomeOfHalls.get(hallName);
 if(currentAmount==null)
 currentAmount = amount;
 else
 currentAmount = currentAmount.add(amount);
 incomeOfHalls.put(hallName, currentAmount);
 }
}

好吧,饶过我吧……我偷懒了……这只是个例子,请无视多线程问题和其他问题……只要能跑就够了。

  1. 创建after advice,并修改配置文件

public class MyAfterMethod implements AfterReturningAdvice{
 
 public void afterReturning(Object returnValue, Method method,
 Object[] args, Object target) throws Throwable {
 System.out.println("After return...");
  
 Statistic.increaseIncome(HALL_NAME.DINOSAUR, BigDecimal.valueOf(2l));
 }
 
}

配置:

<bean id="customPointcut2" class="org.springframework.aop.support.NameMatchMethodPointcut">
        <property name="mappedName" value="visit"/>
    </bean>
<bean id="myAfterAdvice" class="com.edi.poc.aop.MyAfterMethod"/>
 
<bean id="customAdvisor2" class="org.springframework.aop.support.DefaultPointcutAdvisor">
        <property name="pointcut" ref="customPointcut2"/>
        <property name="advice" ref="myAfterAdvice"/>
    </bean>
 
<bean id="customServiceProxy2" class="org.springframework.aop.framework.ProxyFactoryBean">
            <property name="target" ref="dinoHall"/>
            
            <property name="interceptorNames">
                <list>
                    <value>customAdvisor2</value>
                </list>
            </property>
</bean>

  1. 修改main,并运行

public static void main(String[] args) {
 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
 Zoo zoo = (Zoo) ctx.getBean("customServiceProxy");
 Hall dinoHall = (Hall)ctx.getBean("customServiceProxy2");
 zoo.addHall(HALL_NAME.DINOSAUR, dinoHall);
 Tourer jack = (Tourer)ctx.getBean("jack");
 zoo.open();
 jack.visit(zoo, HALL_NAME.DINOSAUR);
 System.out.println("Current traffic: " + Statistic.getTotalTraffic());
 NumberFormat currency = NumberFormat.getCurrencyInstance();
 System.out.println("Income of " + HALL_NAME.DINOSAUR + ": "+ currency.format(Statistic.getIncome(HALL_NAME.DINOSAUR)));
 zoo.close();
}

输出:

Dinosaur hall is opened.

The People Zoo is opened.

Before method...

Charge Jack $1.00 for ticket.

Jack needs to be charged first.

Dianosaur hall charges Jack $2.00

Jack visited diano hall.

After return...

Current traffic: 1

Income of DINOSAUR: $2.00

Dinosaur hall is closed.

The People Zoo is closed.

  1. 这里看到有个问题,每个Target都得声明一个ProxyFactoryBean,这样非常麻烦,后期开发维护量巨大。为了相当程度的减轻这个问题,Spring提供了自动代理。 自动代理提供了AbstractAdivosrAutoProxyCreator,可以方便继承,原生提供了两个Creator:
    BeanNameAutoProxyCreator 根据Bean名称创建代理(针对Bean所有方法)
    DefaultAdvisorAutoProxyCreator 根据advisor本身包含信息创建代理(针对特定方法)
  1. 修改使用BeanNamesAutoProxyCreator

<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <!-- Configure the name filter -->
        <property name="beanNames" value="*zoo"></property>
        <property name="interceptorNames" value="customAdvisor"></property>
 </bean>

  1. 修改使用DefaultAdvisorAutoProxyCreator

<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"/>

这种方式感觉有点过于强大了,以至于很容易出错。本例用这个Creator就报错了。但是考虑到现在这种技术很少用了,就没有深究。这里只是介绍下有这种方式存在。如果以后遇到类似用法,再补坑。

  1. 总结 总的来说,传统的Spring AOP实现方式就是动态代理,但是有各种各样的限制和麻烦:
  1. 被代理类以及其父类(如果有)必须有无参构造函数;(有点像早期的序列化要求)
  2. pointcut, advice,advisor都是成套出现,为实现代理一个方法,可能就得实现这三个的一套,显得重复而臃肿;
  3. 如果不使用自动代理,每个Target都有一个ProxyFactoryBean,及其臃肿;
  4. 如果使用动态代理,advice,advisor定义不是非常强大,aspect实现绑定比较麻烦。

本文源码:

https://github.com/EdisonXu/POC/tree/master/intro-aop