常言笑的家

Spring, Hibernate, Struts, Ajax, RoR

Spring Aop Step-By-Step 学习笔记(上)

最近由于工作需要,要求掌握关于 Spring 方面的东西。所以花了两个星期的时间来学习 Spring 的基本知识,主要包括 Ioc Aop 两方面。
本文为笔者的 Spring Aop 方面的学习笔记,主要结合了 Spring In Action 第三章 Spring-Reference 第五章 为学习向导。根据自己的理解和书中的实例来一步一步完成对于在 Spring Aop 方面的编程。其中基础部分 Ioc 需要读者自己参考资料了解,本文将不做描述。
说明:我将尽量缩短程序长度,在程序部分将减少注释说明,重点要读者自己根据上下文和程序结果理解体会,具体 api 信息请读者自己参考 Spring-api 文档和相关资料。
一.    准备工作:
 
1.  开发环境:
l             适合人群:
                  要了解 Spring Ioc ,对 Spring- Aop 可以不了解或者仅仅熟悉 Aop 概念,未参与 Spring Aop 开发实战的初学者。同时也希望高手对于本文的不足或理解错误之处给予指点,谢谢。
l             开发环境:
JDK 1.4_2
l             开发工具:
Eclipse 3.12 (未采用任何插件,主要是为初学者熟悉和理解 xml 文档的配置)
l             所需组件:
Spring-Framework-1.2.8
下载地址:
 
2.    建立工程:
   首先用 Eclpse 建立一个普通 java 项目,导入 jar 文件到编译环境中,如下:
a)             Spring.jar Spring 的核心 jar 文件,必须;
b)             Commons-loggin.jar 日志文件,必须;
c)             Cglib.jar 动态代理文件,不是必须(本文需要);
d)             Jak-oro.jar 使用 Perl Awk 正则表达式进行文本解析工具,不是必须(本文需要);
建立工程如下:

好了,下来我们开始我们的 Spring-aop 之旅;
 
二.  Spring -Aop 入门
 
AOP 全名 Aspect-oriented programming Spring framework 是很有前途的 AOP 技术。作为一种非侵略性的,轻型的 AOP framework ,你无需使用预编译器或其他的元标签,便可以在 Java 程序中使用它。这意味着开发团队里只需一人要对付 AOP framework ,其他人还是像往常一样编程。
关键性概念:
1)          Advice 是代码的具体实现,例如一个实现日志记录的代码。
2)          Pointcut 是在将 Advice 插入到程序的条件。
3)           advisor 是把 pointcut advice 的组合在一起装配器。
 
图例:
你的程序可能如上,现在要在三个流程上同时加入日志控制和权限控制,如下:
 

 你的程序可能如上,现在要在三个流程上同时加入日志控制和权限控制,如下:

 

 

         其中拿日志为例,日志控制和流程之间的穿插点处叫做连接点( Joinpoint ),而 Advice 就是我们日志处理的具体代码, Pointcut 就是定义一个规则,对三个和业务有关的连接点进行过滤和匹配(例如我们对于业务 1 不做日志处理)。 Advisor 就是将符合的规则的剩下的两个连接点和具体的日志记录代码组合在一起。
 
三.  Spring-Aop 前置通知、后置通知、环绕通知、异常通知实现
 
我以 Spring In Action 提供的例子进行二次改造,完成我们自己的流程。业务流程很简单,顾客到商店买东西,店员根据顾客的需要给于顾客提供服务。实现方法前插入,方法后插入,环绕,异常四种。
 
       代码:
          建立一个用户类;
public   class  Customer  {
     
private  String name  =   " 悠~游! " ;
     
public  String getName()  {
          
return  name;
     }

}

三个产品
public class Cheese {
   
public String toString()
         
return "奶酪!";
   }

}

public class Pepper {
     
public String toString()
          
return "胡椒粉!";
     }

}

public class Squish {
     
public String toString()
          
return "果酱!";
     }

}

          建立一个接口;
public   interface  KwikEMart  {
     Squish buySquish(Customer customer) 
throws  KwikEMartException;
     Pepper buyPepper(Customer customer) 
throws  KwikEMartException;
     Cheese buyCheese(Customer customer) 
throws  KwikEMartException;
}
实现这个接口,我们实现三个方法,买奶酪,买胡椒粉,买果酱;
public   class  ApuKwikEMart  implements  KwikEMart  {
     
private   boolean  cheeseIsEmpty  =   false ;

     
private   boolean  pepperIsEmpty  =   false ;

     
private   boolean  squishIsEmpty  =   false ;

     
public  Cheese buyCheese(Customer customer)  throws  NoMoreCheeseException {

          
if  (cheeseIsEmpty)  {
               
throw   new  NoMoreCheeseException();
          }


          Cheese s 
=   new  Cheese();
          System.out.println(
" --我想买: "   +  s);
          
return  s;

     }


     
public  Pepper buyPepper(Customer customer)  throws  NoMorePepperException {

          
if  (pepperIsEmpty)  {
               
throw   new  NoMorePepperException();
          }


          Pepper s 
=   new  Pepper();
          System.out.println(
" --我想买: "   +  s);
          
return  s;

     }


     
public  Squish buySquish(Customer customer)  throws  NoMoreSquishException {

          
if  (squishIsEmpty)  {
               
throw   new  NoMoreSquishException();
          }


          Squish s 
=   new  Squish();
          System.out.println(
" --我想买: "   +  s);
          
return  s;

     }


     
public   void  setCheeseIsEmpty( boolean  cheeseIsEmpty)  {
          
this .cheeseIsEmpty  =  cheeseIsEmpty;
     }


     
public   void  setPepperIsEmpty( boolean  pepperIsEmpty)  {
          
this .pepperIsEmpty  =  pepperIsEmpty;
     }


     
public   void  setSquishIsEmpty( boolean  squishIsEmpty)  {
          
this .squishIsEmpty  =  squishIsEmpty;
     }


}

环绕通知的实现,必须实现invoke方法,通过调用invoke.proceed()手工调用对象方法:
public   class  OnePerCustomerInterceptor  implements  MethodInterceptor  {
     
     
private  Set customers = new  HashSet();

     
public  Object invoke(MethodInvocation invoke)  throws  Throwable  {
          
          Customer customer
= (Customer)invoke.getArguments()[ 0 ];
          
if (customers.contains(customer))
               
throw   new  KwikEMartException( " One per customer. " );
          }

          
          System.out.println(
" 店员: " + customer.getName()  +   "  ,Can I help you ? " );
          Object squishee
= invoke.proceed();  // 手工调用对象方法;
          System.out.println( " 店员:OK!  "   +  customer.getName()  +   " .give you!  "  );
          
          customers.add(squishee);

          
return  squishee;
     }

}

前置通知的实现;
public class WelcomeAdvice implements MethodBeforeAdvice {

     
public void before(Method method, Object[] args, Object target) throws Throwable {
          
          Customer customer 
= (Customer) args[0];
          System.out.println(
"店员::Hello " + customer.getName() + " . How are you doing?");

     }

}

public class Customer {
     
private String name = "悠~游!";
     
public String getName() {
          
return name;
     }

}
后置通知实现;
public class ThankYouAdvice implements AfterReturningAdvice {

     
public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
          
          Customer customer 
= (Customer) args[0];
          System.out.println(
"店员:Thank you " + customer.getName() + " . Come again! " );

     }

}
系统异常处理通知实现;
public   class  KwikEmartExceptionAdvice  implements  ThrowsAdvice  {

     
public   void  afterThrowing(NoMoreSquishException e)  {
          System.out.println(
" 系统:NoMoreSquisheesException异常截获了:  "   +  e.getMessage());
     }

     
public   void  afterThrowing(NoMoreCheeseException e)  {
          System.out.println(
" 系统:NoMoreCheeseException异常截获了:  "   +  e.getMessage());
     }

     
public   void  afterThrowing(NoMorePepperException e)  {
          System.out.println(
" 系统:NoMorePepperException异常截获了:  "   +  e.getMessage());
     }

}

自定义的异常接口;
public class KwikEMartException extends Exception {

     
private static final long serialVersionUID = -3962577696326432053L;

     String retValue 
= "KwikEMartException 异常!";

     
public KwikEMartException(String name) {
          retValue 
= name;
     }

     
public KwikEMartException() {

     }

     
public String getMessage() {
          
return retValue;
     }


}
没有更多的奶酪异常;
public class NoMoreCheeseException extends KwikEMartException {
     
private static final long serialVersionUID = -3961123496322432053L;

     String retValue 
= "NoMoreCheeseException 异常!";

     
public NoMoreCheeseException() {
          
super();
     }


     
public NoMoreCheeseException(String name) {
          
super(name);
     }


     
public String getMessage() {

          
return retValue;
     }

}
没有更多的胡椒粉异常;
public   class  NoMorePepperException  extends  KwikEMartException  {
     
private   static   final   long  serialVersionUID  =   - 3961234696322432053L ;

     String retValue 
=   " NoMorePepperException 异常! " ;

     
public  NoMorePepperException()  {
          
super ();
     }


     
public  NoMorePepperException(String name)  {
          
super (name);
     }


     
public  String getMessage()  {

          
return  retValue;
     }

}

没有更多的果酱异常;
public   class  NoMoreSquishException  extends  KwikEMartException  {
     
private   static   final   long  serialVersionUID  =   - 3121234696322432053L ;

     String retValue 
=   " NoMoreSquishException 异常! " ;

     
public  NoMoreSquishException()  {
          
super ();
     }


     
public  NoMoreSquishException(String name)  {
          
super (name);
     }


     
public  String getMessage()  {

          
return  retValue;
     }

}
运行实例类;
public class RunDemo {

               
public static void kwikEMart() {

               ClassPathXmlApplicationContext context 
= new ClassPathXmlApplicationContext("demo/kwikemart.xml");

                    
//如果你想通过类来引用这个的话,就要用到CGLIB.jar了,同时在代理工厂里面设置:
                    
//<property name="proxyTargetClass" value="true" />
                    KwikEMart akem = (KwikEMart) context.getBean("kwikEMart");
     
                    
try {
                         akem.buySquish(
new Customer());
                         akem.buyPepper(
new Customer());
                         akem.buyCheese(
new Customer());
                    }
 catch (KwikEMartException e) {
                         
//异常已经被截获了,不信你看控制台!~;
                    }

               }


               
public static void main(String[] args) {
                    kwikEMart();
               }

}

Xml 文件配置:
    
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">


<beans>
     
<bean id="kwikEMartTarget" class="demo.ApuKwikEMart">
          
<!-- 
               把这里注释去掉的话,程序调用的时候测试异常通知;
               <property name="cheeseIsEmpty">
                    <value>true</value>
               </property>
               <property name="pepperIsEmpty">
                    <value>true</value>
               </property>
               <property name="squishIsEmpty">
                    <value>true</value>
               </property>
          
-->
     
</bean>

     
<!-- 方法调用前通知 -->
     
<bean id="welcomeAdvice" class="demo.advice.WelcomeAdvice" />
     
<!-- 方法调用后通知 -->
     
<bean id="thankYouAdvice" class="demo.advice.ThankYouAdvice" />
     
<!-- 环绕调用通知 -->
     
<bean id="onePerCustomerInterceptor" class="demo.advice.OnePerCustomerInterceptor" />
     
<!-- 异常调用通知 -->
     
<bean id="kwikEmartExceptionAdvice" class="demo.advice.KwikEmartExceptionAdvice" />
     
     
<bean id="kwikEMart" class="org.springframework.aop.framework.ProxyFactoryBean">
          
<property name="proxyInterfaces" value="demo.KwikEMart" />
          
<property name="interceptorNames">
               
<list>
                    
                         
<value>welcomeAdvice</value>
                         
<value>thankYouAdvice</value>
                         
<value>onePerCustomerInterceptor</value>
                         
<value>kwikEmartExceptionAdvice</value>
                    
               
</list>
          
</property>
          
<property name="target">
               
<ref bean="kwikEMartTarget" />
          
</property>
     
</bean>

</beans>

这个例子东西很多,不过每个类的代码都不大。如果你对 org.springframework.aop.framework.ProxyFactoryBean 不是很了解的话可以看我下篇尾处的介绍。 读清楚之后,我们运行RunDemo 类,查看控制台结果,如下:
 
店员::Hello 悠~游! . How are you doing?
店员:悠~游! ,Can I help you ?
-- 我想买:果酱!
店员:OK! 悠~游!.give you!
店员:Thank you 悠~游! . Come again!
店员::Hello 悠~游! . How are you doing?
店员:悠~游! ,Can I help you ?
-- 我想买:胡椒粉!
店员:OK! 悠~游!.give you!
店员:Thank you 悠~游! . Come again!
店员::Hello 悠~游! . How are you doing?
店员:悠~游! ,Can I help you ?
-- 我想买:奶酪!
店员:OK! 悠~游!.give you!
店员:Thank you 悠~游! . Come again!
 
    我们将 kwikEMartTarget 里面的注释去掉,测试异常实现,如下:
    
店员::Hello 悠~游! . How are you doing?
店员:悠~游! ,Can I help you ?
系统:NoMoreSquisheesException异常截获了: NoMoreSquishException 异常!
     好好理解一下,我就不废话了,我们进行下一节。
 
 
 
四. Spring-Aop Pointcut+advice+Advisor 实现
 
我们修改我们的xml文档后如下:
 
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd">

<beans>

     
<bean id="kwikEMartTarget" class="demo.ApuKwikEMart"></bean>

     
<!-- 环绕调用通知 -->
     
<bean id="onePerCustomerInterceptor" class="demo.advice.OnePerCustomerInterceptor" />

     
<!-- 使用NameMatchMethodPointcut -->
     
<bean id="nameMatchfilterPointcut" class="org.springframework.aop.support.NameMatchMethodPointcut">
          
<property name="mappedName">
               
<!-- 
                    buy.* 以buy开头的方法;
               
-->
               
<value>buy*</value>
          
</property>
     
</bean>

     
<!-- 使用Perl5RegexpMethodPointcut -->
     
<bean id="regexpFilterPointcut" class="org.springframework.aop.support.Perl5RegexpMethodPointcut">
          
<property name="pattern">
               
<!-- 
                    .*buy.+ 以buy开头的方法;
                    .*buyS.+ 以buyS开头的方法;
               
-->
               
<value>.*suy.+</value>
          
</property>
     
</bean>

     
<bean id="runDemofilterPointcutAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
          
<property name="pointcut">
               
<ref bean="nameMatchfilterPointcut" />
          
</property>
          
<property name="advice">
               
<ref bean="onePerCustomerInterceptor" />
          
</property>
     
</bean>
     
<bean id="kwikEMart" class="org.springframework.aop.framework.ProxyFactoryBean">
          
<property name="proxyInterfaces" value="demo.KwikEMart" />
          
<property name="interceptorNames">
               
<list>
                    
<value>runDemofilterPointcutAdvisor</value>
               
</list>
          
</property>
          
<property name="target">
               
<ref bean="kwikEMartTarget" />
          
</property>
     
</bean>

</beans>
 
运行,结果如下:
店员:悠~游! ,Can I help you ?
--我想买:果酱!
店员:OK! 悠~游!.give you!
店员:悠~游! ,Can I help you ?
--我想买:胡椒粉!
店员:OK! 悠~游!.give you!
店员:悠~游! ,Can I help you ?
--我想买:奶酪!
        店员:OK! 悠~游!.give you!
 
在这里简单说明一下xml文档:nameMatchfilterPointcutregexpFilterPointcut 是我们自己定义好规则的Pointcut。nameMatchfilterPointcut 根据mappedName来设置过滤规则, regexpFilterPointcut则是用pattern来设置过滤规则。runDemofilterPointcutAdvisor则将我们的Pointcutadvice组合在一起。
读者可以自己修改runDemofilterPointcutAdvisorpointcut来切换不同的Pointcut。如果需要RegexpMethodPointcut 的子类的实现,必须要oro包支持。(注:RegexpMethodPointcut有俩个子类的实现,JdkRegexpMethodPointcut和Perl5RegexpMethodPointcut)。
     但是,如果我们想让我们的Advisor同时实现多个Pointcut+advice怎么办呢?利用Spring In Action里面的实例,我们来自己实现我们的Pointcut
 
    代码:
 
 
public class MyUnionPointcut implements Pointcut {
     
     
private Pointcut delegate;

     
public ClassFilter getClassFilter() {
          
return getDelegate().getClassFilter();
     }


     
private Pointcut getDelegate() {
          
if (delegate == null{
               
throw new AopConfigException("No pointcuts have been configured.");
          }

          
return delegate;
     }


     
public MethodMatcher getMethodMatcher() {
          
return getDelegate().getMethodMatcher();
     }


     
public void setPointcuts(List pointcuts) {

          
if (pointcuts == null || pointcuts.size() == 0{
               
throw new AopConfigException("Must have at least one Pointcut.");
          }

          delegate 
= (Pointcut) pointcuts.get(0);

          
for (int i = 1; i < pointcuts.size(); i++{
               Pointcut pointcut 
= (Pointcut) pointcuts.get(i);
               delegate 
= Pointcuts.union(delegate, pointcut);
          }


     }

}

其实就是继承Pointcut类,实现getMethodMatcher()方法即可,接下来看我们把那两个Pointcut组合成一个,当其中一个Pointcut满足的时候就返回true,调用我们的Advice。
 
在xml中,加入下面代码:
<bean id="myUnionPointcut" class="demo.utils.MyUnionPointcut">
          
<property name="pointcuts">
               
<list>
               
<!-- 这里说明一下:动态切入点和静态切入点集合在一起的时候好像有问题? -->
                    
<ref bean="nameMatchfilterPointcut" />
                    
<ref bean="regexpFilterPointcut" />
               
</list> 
          
</property>
     
</bean>
修改runDemofilterPointcutAdvisor的pointcut来加入我们组合好的Pointcut。

     
<bean id="runDemofilterPointcutAdvisor" class="org.springframework.aop.support.DefaultPointcutAdvisor">
          
<property name="pointcut">
               
<ref bean=”myUnionPointcut" />
          
</property>
          
<property name="advice">
               
<ref bean="onePerCustomerInterceptor" />
          
</property>
     
</bean>
 
 
    
同时在运行前,读者可以自己修改两个pointcut的匹配方法,来匹配更多的可选项。同时可以参考的一个类是org.springframework.aop.support.UnionPointcut,它实现两个pointcut的联合。如果读者想实现更加灵活的匹配,需要自己来定义自己的pointcut。(如第一个pointcut交叉第二个pointcut,联合第三个pointcut),交叉的工具类为ComposablePointcut。
运行的结果请读者自己试验。这个时候您可能在想,这些pointcut都是Spring自己的实现,我们能否自己来定义我们自己规则的pointcut呢?当然可以!~
代码:
/**
 * 
 * 自己定义的静态切入点
 * 满足条件是:当传入的数字大于1块钱的时候才可以;
 * 
 * 
@author 悠~游
 * 
@since 2006-06-22
 * 
@link www.uusam.com
 
*/

public class MyPointcut extends StaticMethodMatcherPointcut implements Serializable {

     
private static final long serialVersionUID = -101281038294508751L;

     
private int money = 0;

     
/**
      * 实现方法,业务逻辑为:根据输入的钱数和动作(必须是以buy开头的方法),来确定是否有钱购买商品,买完之后去掉商品的价格;
      
*/

     
public boolean matches(Method method, Class targetClass) {
          
if (method.getName().indexOf("buyCheese"== 0 && money >= 100{
               money 
-= 100;
               
return true;
          }
 else if (method.getName().indexOf("buyPepper"== 0 && money >= 5{
               money 
-= 5;
               
return true;
          }
 else if (method.getName().indexOf("buySquish"== 0 && money >= 10{
               money 
-= 10;
               
return true;
          }

          System.out.println(
"门卫:你要买的东西太贵,你的钱 "+money+" 太少!~ ,取消服务!");
          
return false;
     }


     
public void setMoney(int money) {
          
this.money = money;
     }


}

这个就是我们自己定义的静态Pointcut,主要实现自己的matches方法。看看如何加入到我们的XML文档中:
<!-- 使用自定义的切入点 -->
     
<bean id="myPointcut" class="demo.pointcut.MyPointcut">
          
<property name="money">
               
<value>100</value>
          
</property>
     
</bean>
    
 
很简单不是么?我们定义一个数字,就是用户的money,进入商店时候兜里的钱^_^。同样修改我们的myUnionPointcut里面的pointcuts,加入我们的pointcut。
 
<bean id="myUnionPointcut" class="demo.utils.MyUnionPointcut">
          
<property name="pointcuts">
               
<list>
                    
<!—上面两个要设置成不通过哦,或者索性就去掉先。 -->
                    
<ref bean="nameMatchfilterPointcut" />
                    
<ref bean="regexpFilterPointcut" />
<ref bean="myPointcut" />
               
</list> 
          
</property>
     
</bean>
 
    
当上面两个Pointcut定义的规则不通过的时候,程序开始校验我们的myPointcut。运行,结果如下:
店员:悠~游! ,Can I help you ?
--我想买:果酱!
店员:OK! 悠~游!.give you!
店员:悠~游! ,Can I help you ?
--我想买:胡椒粉!
店员:OK! 悠~游!.give you!
门卫:你要买的东西太贵,你的钱 85 太少!~ , 取消服务!
--我想买:奶酪!//服务员没了...
    
     好了,是不是我们想要的结果呢?呵呵。
     同时,Spring 提供动态Pointcut。关于动态的说明我就不在熬述了,我们这里只关心具体Spring带给我们的具体实现方法,具体应用请读者自己斟酌使用。
    
 
    
<!-- 定制动态接入点,来判断当前线程堆栈中是否有demo.RunDemo这个类; -->
     
<bean id="runDemoPointcut" class="org.springframework.aop.support.ControlFlowPointcut">
          
<constructor-arg>
               
<value>demo.RunDemo</value>
          
</constructor-arg>
     
</bean>
     修改下面的引用我们的动态Pointcut;
<bean id="myUnionPointcut" class="demo.utils.MyUnionPointcut">
          
<property name="pointcuts">
<list>
     
<ref bean=" runDemoPointcut " />
         
</list> 
    
</property>
</bean>
 
     运行,结果如下:
店员:悠~游! ,Can I help you ?
--我想买:果酱!
店员:OK! 悠~游!.give you!
店员:悠~游! ,Can I help you ?
--我想买:胡椒粉!
店员:OK! 悠~游!.give you!
店员:悠~游! ,Can I help you ?
--我想买:奶酪!
店员:OK! 悠~游!.give you!
    
     动态切入点是根据当前堆栈信息进行方法匹配的一种规则,读者可以自己修改demo.RunDemo,如java.lang.Integer,来看看结果。
 
--我想买:果酱!
--我想买:胡椒粉!
--我想买:奶酪!
    
     到这里能够读下来已经很不容易了,呵呵。还是站起来走动一下吧,接下来我们将搞定其他的一些东东。

posted on 2006-12-19 13:12 常言笑 阅读(382) 评论(0)  编辑  收藏 所属分类: JAVA/J2EE


只有注册用户登录后才能发表评论。


网站导航:
 

My Links

Blog Stats

常用链接

留言簿(5)

随笔分类

随笔档案

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜