来看一个简单的例子,当需要在执行某些方法时留下日志信息,可能会这样写:
import java.util.logging.*; public class HelloSpeaker{ pirvate Logger logger=Logger.getLogger(this.getClass().getName()); public void hello(String name){ logger.log(Level.INFO, "hello method starts…");// 方法开始执行时留下日志 Sytem.out.println("hello, "+name); // 程序的主要功能 Logger.log(Level.INFO, "hello method ends…");// 方法执行完毕时留下日志 } }
在HelloSpeaker类中,当执行hello()方法时,程序员希望该方法执行开始与执行完毕时都留下日志。最简单的做法是用上面的程序设计,在方法执行的前后加上日志动作。
可以使用代理(Proxy)机制来解决这个问题,有两种代理方式:静态代理(static proxy)和动态代理(dynamic proxy)。
在静态代理的实现中,代理类与被代理的类必须实现同一个接口。在代理类中可以实现记录等相关服务,并在需要的时候再呼叫被代理类。这样被代理类就可以仅仅保留业务相关的职责了。
举个简单的例子,首先定义一个IHello接口,IHello.java代码如下:
public interface IHello{ public void hello(String name); }
然后让实现业务逻辑的HelloSpeaker类实现IHello接口,HelloSpeaker.java代码如下:
public class HelloSpeaker implements IHello{ public void hello(String name){ System.out.println("hello,"+name); } }
可以看到,在HelloSpeaker类中没有任何日志的代码插入其中,日志服务的实现将被放到代理类中,代理类同样要实现IHello接口。
HelloProxy.java代码如下:
public class HelloProxy implements IHello{ private Logger logger=Logger.getLogger(this.getClass().getName()); private IHello helloObject; public HelloProxy(IHello helloObject){ this.helloObject=helloObject; } public void hello(String name){ log("hello method starts…");// 日志服务 helloObject.hello(name);// 执行业务逻辑 log("hello method ends…");// 日志服务 } private void log(String msg){ logger.log(Level.INFO,msg); } }
在HelloProxy类的hello()方法中,真正实现业务逻辑前后安排记录服务,可以实际撰写一个测试程序来看看如何使用代理类。
public class ProxyDemo{ public static void main(String[] args){ IHello proxy=new HelloProxy(new HelloSpeaker()); proxy.hello("Justin"); } }
程序运行结果:
hello,Justin
要实现动态代理,同样需要定义所要代理的接口。
IHello.java代码如下:
public interface IHello{ public void hello(String name); }
然后让实现业务逻辑的HelloSpeaker类实现IHello接口。
HelloSpeaker.java代码如下:
public class HelloSpeaker implements IHello{ public void hello(String name){ System.out.println("Hello,"+name); } }
与上例不同的是,这里要实现不同的代理类:
import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; public class LogHandler implements InvocationHandler{ private Object sub; public LogHandler() { } public LogHandler(Object obj){ sub = obj; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{ System.out.println("before you do thing"); method.invoke(sub, args); System.out.println("after you do thing"); return null; } }
写一个测试程序,使用LogHandler来绑定被代理类。
ProxyDemo.java代码如下:
import java.lang.reflect.Proxy; public class ProxyDemo { public static void main(String[] args) { HelloSpeaker helloSpeaker=new HelloSpeaker(); LogHandler logHandler=new LogHandler(helloSpeaker); Class cls=helloSpeaker.getClass(); IHello iHello= (IHello)Proxy.newProxyInstance(cls.getClassLoader(),cls.getInterfaces(),logHandler); iHello.hello("Justin"); } }
程序运行结果:
before you do thing Hello, Justin after you do thing
在DynamicProxyDemo例子中,记录的动作原先被横切(Cross-cutting)到HelloSpeaker本身所负责的业务流程中。类似于日志这类的动作,如安全检查、事务等服务,在一个应用程序中常被安排到各个类的处理流程之中。这些动作在AOP的术语中称为cross-cutting concerns。如图5.7所示,原来的业务流程是很单纯的。
图5.7 原来的业务流程
为了加入日志与安全检查等服务,类的程序代码中被硬生生地写入了相关的Logging、Security程序片段,如图5.8所示。
图5.8 加入各种服务的业务流程
将散落在各个业务类中的cross-cutting concerns收集起来,设计各个独立可重用的类,这种类称为Aspect。例如,在动态代理中将日志的动作设计为LogHandler类,LogHandler类在AOP术语中就是Aspect的一个具体实例。在需要该服务的时候,缝合到应用程序中;不需要服务的时候,也可以马上从应用程序中脱离。应用程序中的可重用组件不用做任何的修改。例如,在动态代理中的HelloSpeaker所代表的角色就是应用程序中可重用的组件,在它需要日志服务时并不用修改本身的程序代码。
Spring提供了5种通知(Advice)类型:Interception Around、Before、After Returning、Throw 和Introduction。它们分别在以下情况被调用:
Interception Around Advice:在目标对象的方法执行前后被调用。
Before Advice:在目标对象的方法执行前被调用。
After Returning Advice:在目标对象的方法执行后被调用。
Throw Advice:在目标对象的方法抛出异常时被调用。
Introduction Advice:一种特殊类型的拦截通知,只有在目标对象的方法调用完毕后执行。
创建一个Before Advice的Web项目,步骤如下。
① 创建一个Web项目,命名为“Spring_Advices”。
② 编写Java类。
Before Advice会在目标对象的方法执行之前被呼叫。这个接口提供了获取目标方法、参数及目标对象。
MethodBeforeAdvice接口的代码如下:
import java.lang.ref.*; import java.lang.reflect.Method; public interface MethodBeforeAdvice{ void before(Method method, Object[] args, Object target) throws Exception; }
用实例来示范如何使用Before Advice。首先要定义目标对象必须实现的接口IHello。
IHello.java代码如下:
public interface IHello{ public void hello(String name); }
接着定义一个HelloSpeaker,实现IHello接口。
HelloSpeaker.java代码如下:
public class HelloSpeaker implements IHello{ public void hello(String name){ System.out.println("Hello,"+name); } }
在对HelloSpeader不进行任何修改的情况下,想要在hello()方法执行之前可以记录一些信息。有一个组件,但没有源代码,可对它增加一些日志的服务。
LogBeforeAdvice.java代码如下:
import java.lang.reflect.*; import java.util.logging.Level; import java.util.logging.Logger; import org.springframework.aop.MethodBeforeAdvice; public class LogBeforeAdvice implements MethodBeforeAdvice{ private Logger logger=Logger.getLogger(this.getClass().getName()); public void before(Method method,Object[] args,Object target) throws Exception{ logger.log(Level.INFO, "method starts…"+method); } }
③ 添加Spring开发能力。
applicationContext.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-2.0.xsd"> <bean id="logBeforeAdvice" class="LogBeforeAdvice" /> <bean id="helloSpeaker" class="HelloSpeaker" /> <bean id="helloProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>IHello</value> </property> <property name="target"> <ref bean="helloSpeaker" /> </property> <property name="interceptorNames"> <list> <value>logBeforeAdvice</value> </list> </property> </bean> </beans>
④ 运行程序,测试结果。
写一个程序测试一下Before Advice的运作。
SpringAOPDemo.java代码如下:
import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class SpringAOPDemo{ public static void main(String[] args){ ApplicationContext context=new FileSystemXmlApplicationContext(" /WebRoot/WEB-INF/classes/applicationContext.xml"); IHello helloProxy=(IHello)context.getBean("helloProxy"); helloProxy.hello("Justin"); } }
程序运行结果:
Hello,Justin
HelloSpeaker与LogBeforeAdvice是两个独立的类。对于HelloSpeaker来说,它不用知道LogBeforeAdvice的存在;而LogBeforeAdvice也可以运行到其他类之上。HelloSpeaker与LogBeforeAdvice都可以重复使用。
创建一个切入点Pointcut项目,步骤如下。
① 创建一个Web项目,命名为“Spring_Pointcut”。
② 编写Java类。
IHello.java代码如下:
public interface IHello{ public void helloNewbie(String name); public void helloMaster(String name); }
HelloSpeaker类实现IHello接口。HelloSpeaker.java代码如下:
public class HelloSpeaker implements IHello{ public void helloNewbie(String name){ System.out.println("Hello, "+name+"newbie! "); } public void helloMaster(String name){ System.out.println("Hello, "+name+"master! "); } }
③ 添加Spring开发能力。
applicationContext.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-2.0.xsd"> <bean id="logBeforeAdvice" class="LogBeforeAdvice" /> <bean id="helloAdvisor" class="org.springframework.aop.support.NameMatchMethodPointcutAdvisor"> <property name="mappedName"> <value>hello*</value> </property> <property name="advice"> <ref bean="logBeforeAdvice" /> </property> </bean> <bean id="helloSpeaker" class="HelloSpeaker" /> <bean id="helloProxy" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>IHello</value> </property> <property name="target"> <ref bean="helloSpeaker" /> </property> <property name="interceptorNames"> <list> <value>helloAdvisor</value> </list> </property> </bean> </beans>
④ 运行程序,测试结果。
SpringAOPDemo.java代码如下:
import org.springframework.context.ApplicationContext; import org.springframework.context.support.FileSystemXmlApplicationContext; public class SpringAOPDemo { public static void main(String[] args) { ApplicationContext context = new FileSystemXmlApplicationContext( "/WebRoot/WEB-INF/classes/applicationContext.xml"); IHello helloProxy = (IHello) context.getBean("helloProxy"); helloProxy.helloNewbie("Justin"); helloProxy.helloMaster("Tom"); } }
程序运行结果:
Hello, Justinnewbie! Hello, Tommaster!
附:目录《JavaEE基础实用教程》笔记说明