架构探险笔记4-使框架具备AOP特性(上)

时间:2021-10-19 13:13:01

对方法进行性能监控,在方法调用时统计出方法执行时间。

原始做法:在内个方法的开头获取系统时间,然后在方法的结尾获取时间,最后把前后台两次分别获取的系统时间做一个减法,即可获取方法执行所消耗的总时间。

项目中大量的方法,如果对每个方法开头结尾都加上这些代码,工作量会很大。现在不用修改现有代码,在另一个地方做性能监控,AOP(Aspect Oriented Programming,面向方面编程)就是我们寻找的解决方案。

在AOP中,我们需要定义一个Aspect(切面)类来编写需要横切业务逻辑的代码,也就是性能监控代码。此外,我们需要通过一个条件来匹配想要拦截的类,这个条件在AOP中称为Pointcut(切点)。

案例思路,统计出执行每个Controller类的各个方法所消耗的时间。每个Controller类都有Controller注解,也就是说,我们只需要拦截所有带有Controller注解的类就行了,切点很容易就能确定下来,剩下的就是做一个切面了。

代理技术

代理,或称为Proxy,意思就是你不用去做,别人替你去处理。比如说:赚钱方面,我就是我老婆的Proxy;带小孩方面,我老婆就是我的Proxy;家务事方面,没有Proxy。

它在程序中开发起到了非常重要的作用,比如AOP,就是针对代理的一种应用。此外,在设计模式中,还有一个“代理模式”,在公司要上网,要在浏览器中设置一个Http代理。

Hello World例子

//接口
public interface Hello{
  void say(String name);  
}

//实现类
public class HelloImpl implements Hello{
  @Override
  public void say(String name){
     System.out.println("Hello!"+name);      
  }    
}

如果要在println方法前面和后面分别需要处理一些逻辑,怎么做呢?把这些逻辑写死在say方法里面吗?这么做肯定不够优雅,“菜鸟”一般这样干,作为一名资深的程序员,我们坚决不能这么做!

我们要用代理模式,写一个HelloProxy类,让它去调用HelloImpl的say方法,在调用的前后分别进行逻辑处理。

public class HelloProxy implements Hello{
  private Hello hello;

  private HelloProxy(){
    hello=new HelloImpl();
  }  
    
   private void say(String name){
       before();
       hello.say(name);
       after();
   }

   private void before(){
       System.out.println("Before");
   }

   private void after(){
       System.out.println("After");
   }
}

用HelloProxy类实现了Hello接口(和HelloImpl实现相同的接口),并且在构造方法中new出一个HelloImpl类的实例。这样一来,我们就可以在HelloProxy的say方法里面去调用HelloImpl的say方法了。更重要的是,我们还可以在调用的前后分别加上before和after两个方法,在这两个方法里去实现那些前后逻辑。

main方法测试

public static void main(String[] args){
    Hello helloProxy = new HelloProxy();
    helloProxy.say("Jack");
}


//打印结果
Before
Hello! Jack
After

JDK动态代理

于是疯狂使用代理模式,项目中到处都是XXXProxy的身影,直到有一天,架构师看到了我的代码,他惊呆了,对我说“你怎么这么喜欢静态代理呢?你就不会用动态代理吗?全部重构!”

研究了一下,原来一直用的是静态代理(上面的例子),到处都是XXXProxy类。一定要将这些垃圾Proxy都重构为“动态代理”。

/**
 * 动态代理
 */
public class DynamicProxy implements InvocationHandler {
    private Object target;
    public DynamicProxy(Object target) {
       this.target = target;
    }
    /* 
     * 用时代理
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        before();
        Object result method.invoke(target, args);
        after();
return result;
    }

}

    //运行
    public static void main(String[] args) {
        Hello hello = new HelloImpl();//用时代理
        DynamicProxy dynamicProxy = new DynamicProxy(hello);
Hello helloProxy
= (Hello)Proxy.newProxyInstance(
      hello.getClass().getClassLoader(),
      hello.getClass().getInterfaces(),
      dynamicProxy
     );
//运行run方法 helloProxy.say("Jack"); }

在这个例子中,DynamicProxy定义了一个Object类型的Object变量,它就是被代理的目标对象,通过构造函数来初始化(“注入”,构造方法初始化叫“正着射”,所以反射初始化叫“反着射”,简称“反射”)。

通过DynamicProxy类去包装Car实例,然后再调用JDK给我们的提供的Proxy类的工厂方法newProxyInstance去动态的创建一个Hello接口的代理类,最后调用这个代理类的run方法。

Proxy.newProxyInstance方法的参数

参数1:ClassLoader

参数2:该实现类的所有接口

参数3:动态代理对象

调用完了用强制类型转换下

这一块想办法封装一下,避免再次出现到处都是Proxy.newProxyInstance方法的情况。于是将这个DynamicProxy重构一下:

public class DynamicProxy implements InvocationHandler{    
private
Object target; public DynamicProxy(Object target) { this.target = target; }
   @SupressWarnings("unchecked")
public <T> T getProxyInstance() { return (T)Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(),this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { before(); Object result = method.invoke(target, args); after(); return result; } //请求前的操作 public void before(){ //预处理 System.out.println(target.getClass()+"被动态代理了,在它执行之前要执行动态代理加入的预处理方法"); } //请求后的操作 public void after(){ //善后处理 System.out.println(target.getClass()+"被动态代理了,在它执行之后要执行动态代理加入的善后方法"); } } public class DynamicProxyDemo { public static void main(String[] args) {
    DynamicProxy dynamicProxy = new DynamicProxy(new HelloImpl());
    Hello helloProxy = dynamicProxy.getProxy();
    helloProxy.say("Jack");
} }

在DynamicProxy里添加了一个getProxy方法,无需传入任何参数,将刚才所说的那块代码放在这个方法中,并且该方法返回一个泛型类型,就不会强制转换类型了。方法头上@SupressWarnings(“unchecked”)注解表示忽略编译时的警告(因为Proxy.newProxyInstance方法返回的是一个Object,这里强制转换为T了,这是向下转型,IDE中就会有警告,编译时也会出现提示)。

调用时就简单了,用2行代理去掉了前面的7行代码(省了5行)。

CGlib动态代理

用了DynamicProxy以后,好处是接口变了,这个动态代理类不用动。而静态代理就不一样了,接口变了,实现类还要动,代理类也要动。但是动态代理并不是万能的,它也有搞不定的时候,比如要代理一个没有任何接口的类,它就没有用武之地了。

CGlib是一个能代理没有接口的类,虽然看起来不起眼,但Spring、Hibernate这样高端的开源框架都用到了它,它是一个在运行期间动态生成字节码的工具,也就是动态生成代理类了。

public class CGLibProxy implements MethodInterceptor{
    
    public <T> T getProxy(Class<T> cls){
        return (T) Enhancer.create(cls,this);
    }

    public Object intercept(Object obj,Method method,Object[] args,MethodProxy proxy) throws Throwable{
        before();
        Object result = proxy.invokeSuper(obj,args);
        after();
        return result;
    }

    ......
}

需要实现CGLib给我们提供的MethodInterceptor实现类,并填充intercept方法。方法中最后一个MethodProxy类型的参数proxy值得注意。CGLib给我们提供的是方法级别的代理,也可以理解为堆方法拦截(也就是“方法拦截器”)。这个功能对于我们程序员来说,如同雪中送炭。我们直接调用proxy的invokeSuper方法,将被代理的对象obj以及方法参数args传入其中即可。

与DynamicProxy类似,在CGLibProxy中也添加了一个泛型的getProxy方法,便于我们可以快速地获取自动生成的代理对象。

public static void main(){
    CGLibProxy cgLibProxy = new CGLibProxy();
    Hello helloProxy = cgLibProxy.getProxy(HelloImpl.class);
    helloProxy.say(Jack);
}

仍然通过2行代码就可以返回代理对象,与JDK动态代理不同的是,这里不需要任何的接口信息,对谁都可以生成动态代理对象

用2行代码返回代理对象还是有些多余的,不想总是去new这个CGLibProxy对象,最好new一次,以后随时拿随时用,于是想到了“单例模式”:

public class CGLibProxy implements MethodInterceptor{
    private static CGLibProxy instance = new CGLibProxy();

    private CGLibProxy(){
        
    }
    private static CGLibProxy getInstance(){
        return instance;
    }
...
getProxy...
intercept... }

加上以上几行代码问题就解决了。需要说明的是,这里有一个private的构造方法,就是为了限制外界不能再去new它了,换句话说,这个类被阉割了。

public static void main(String[] args){
  Hello helloProxy = CGLibProxy.getInstance().getProxy(HelloImpl.class);
  helloProxy.say("Jack");    
}

这里只需要一行代码就可以获取代理对象了.

AOP技术

什么是AOP

AOP(Aspect-Oriented Programming),名字与OOP仅仅差一个字母,其实它是对OOP编程方式的一种补充,并非是取而代之。翻译过来就是“面向切面编程”或“面向方面编程”。最重要的工作就是写这个“切面”,那么什么事“切面”呢?

切面是AOP中的一个术语,表示从业务逻辑中分离出来的横切逻辑,比如性能监控、日志记录、权限控制等,这些功能都可以从业务逻辑代码中抽离出去。也就是说,通过AOP可以解决代码耦合问题,让职责更加单一。

需要澄清的是,其实很早以前就出现了AOP这个概念。最知名强大的Java开源项目就是AspectJ了。它的前身是AspectWerkz(AOP真正的的老祖宗)。Rod Johnson写了一个Spring框架,称为Spring之父。他在Spring的IOC框架基础上又实现了一套AOP框架,后来掉进了深渊,在无法自拔的时候*使用了AspectJ。所以我们现在用的最多的就是Spring+AspectJ这种AOP框架了。

写死代码

public interface Greeting {
    void sayHello(String name);
}

public class GreetingImpl implements Greeting {
    @Override
    public void sayHello(String name) {
        before();
        System.out.println("Hello! "+name);
        after();
    }

    private void before(){
        System.out.println("Before");
    }

    private void after(){
        System.out.println("After");
    }
}

before与after方法写死在sayHello方法体中了,这样的代码非常不好。如果我们要统计每一个方法的执行时间,以对性能进行评估,那是不是每个方法的一头一尾都做点手脚呢?

再比如我们要写一个JDBC程序,那是不是也要在方法的开头去连接数据库,方法的末尾去关闭数据库连接呢?

这样写代码只会把程序员累死,把架构师气死!

一定要想办法对上面的代码进行重构,首先给出三个解决方案:

静态代理

JDK动态代理

CGLib动态代理

静态代理

最简单的解决方案就是使用静态代理模式了,我们单独为GreetingImpl这个类写一个代理类:

public class GreetingProxy implements Greeting {
    private GreetingImpl greetingImpl;

    public GreetingProxy(GreetingImpl greetingImpl) {
        this.greetingImpl = greetingImpl;
    }

    @Override
    public void sayHello(String name) {
        before();
        System.out.println("Hello! "+name);
        after();
    }

    private void before(){
        System.out.println("Before");
    }
    private void after(){
        System.out.println("After");
    }
}

就用这个GreetingProxy去代理GreetingImpl,看看客户端如何来调用:

public class Client {
    public static void main(String[] args) {
        Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
        greetingProxy.sayHello("Jack");
    }
}

这个写的没错,但是有个问题。XxxProxy这样的类会越来越多(这里构造函数参数为GreetingImpl,所以换一个子类就要再次写一个代理类,如果构造函数参数改为接口Greet,这样再使用Greet的子类时可以使用这个类,当换一个接口时又要去写一个代理类),如何才能将这些代理类尽可能减少呢?最好只有一个代理类。

这时我们需要使用JDK的动态代理了。

JDK动态代理

public class JDKDynamicProxy implements InvocationHandler {
    private Object target;

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

    @SuppressWarnings("unchecked")
    public <T> T getProxy(){
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(),target.getClass().getInterfaces(),this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target,args);
        after();
        return result;
    }

    private void before(){
        System.out.println("Before");
    }

    private void after(){
        System.out.println("After");
    }
}

这样所有的代理类都合并到动态代理类中了,但这样做仍然存在一个问题:JDK给我们提供的动态代理只能代理接口,而不能代理没有接口的类

public class Client {
    public static void main(String[] args) {
        //静态代理
        /*Greeting greetingProxy = new GreetingProxy(new GreetingImpl());
        greetingProxy.sayHello("Jack");*/
        //JDK动态代理
        Greeting greeting = new JDKDynamicProxy(new GreetingImpl())
                .getProxy();
        greeting.sayHello("Jack");
    }
}

CGLib动态代理

我们使用开源的CGLib类库可以代理没有接口的类,这样就弥补了JDK的不足。CGLib动态代理类是这样的:

public class CGLibDynamicProxy implements MethodInterceptor{
    //单例模式
    private static CGLibDynamicProxy instance = new CGLibDynamicProxy();
    //私有化构造函数,防止new
    private CGLibDynamicProxy(){}
    //提供给外界获取单一实例的方法
    public static CGLibDynamicProxy getInstance(){
        return instance;
    }

    @SuppressWarnings("unchecked")
    public <T> T getProxy(Class<T> cls){
        return (T) Enhancer.create(cls,this);
    }

    @Override
    public Object intercept(Object target, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        before();
        Object result = methodProxy.invokeSuper(target,args);
        after();
        return result;
    }

    private void before(){
        System.out.println("Before");
    }
    private void after(){
        System.out.println("After");
    }
}

注意这里的坐标

        <!--不能超过3.0版本,这里用2.2-->
        <dependency>
            <groupId>cglib</groupId>
            <artifactId>cglib</artifactId>
            <version>2.2</version>
        </dependency>
        <!--CGLib依赖此包-->
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>6.2</version>
        </dependency>

到此为止,能做的都做了,问题似乎全部都解决了。但事情总不会那么完美,而我们一定要追求完美。

Spring AOP

Rod Johnson搞出了一个AOP框架,Spring AOP:前置增强、后置增强、环绕增强(编程式)

上面例子中提到的before方法,在Spring AOP里就叫Before Advice(前置增强)。有些人将Advice直译为“通知”,这里是不太合适的,因为它没有“通知”的含义,而是对原有代码功能的一种“增强”。再者,CGLib中也有一个Enhancer类,它就是一个增强类。

此外,像after这样的方法就叫After Advice(后置增强),因为它放在后面来增强代码的功能。

如果能把before与after结合在一起,那就叫Around Advice(环绕增强),就像汉堡一样。

前置增强类代码(这个类实现了org.spring.framework.aop.MethodBeforeAdvice):

import org.springframework.aop.MethodBeforeAdvice;

public class GreetingBeforeAdvice implements MethodBeforeAdvice {
    @Override
    public void before(Method method, Object[] objects, Object o) throws Throwable {
        System.out.println("Before");
    }
}

后置增强类:

import org.springframework.aop.AfterReturningAdvice;

public class GreetingAfterAdvice implements AfterReturningAdvice {

    @Override
    public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
        System.out.println("After");
    }
}

类似的这里实现了org.springframework.aop.afterReturningAdvice接口。

调用

public class Client {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();  //创建代理工厂
        proxyFactory.setTarget(new GreetingImpl());    //摄入目标类对象
        proxyFactory.addAdvice(new GreetingBeforeAdvice());   //添加前置增强
        proxyFactory.addAdvice(new GreetingAfterAdvice());   //添加后置增强
        Greeting greeting = (Greeting) proxyFactory.getProxy();
        greeting.sayHello("Jack");

    }
}

当然我们完全可以用一个增强类,让它同时实现MethodBeforeAdvice和AfterReturningAdvice这两个接口,代码:

public class GreetingBeforeAndAfterAdvice implements MethodBeforeAdvice,AfterReturningAdvice {
    @Override
    public void afterReturning(Object returnValue, Method method, Object[] args, Object target) throws Throwable {
        System.out.println("before");
    }

    @Override
    public void before(Method method, Object[] args, Object target) throws Throwable {
        System.out.println("after");
    }
}

这样我们只需要使用一行代码,就可以同时添加前置与后置增强

proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());

刚才有提到过“环绕增强”,其实它可以把“前置增强”与“后置增强”的功能合并起来,无须让我们同时实现两个接口。

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import java.lang.reflect.Method;


public class GreetingAroundAdvice implements MethodInterceptor {

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        before();
        Object result = methodInvocation.proceed();
        after();
        return result;
    }

    private void before(){
        System.out.println("Before");
    }

    private void after(){
        System.out.println("After");
    }
}

环绕增强需要实现org.aopalliance.intercept.MethodInterceptor接口。注意,这个接口不是Spring提供的,它是AOP联盟(一个很高大上的技术联盟)写的,Spring只是借用了它,在客户端汇总同样也需要将该增强类的对象添加到代理工厂中。

public class Client {
    public static void main(String[] args) {
        ProxyFactory proxyFactory = new ProxyFactory();  //创建代理工厂
        proxyFactory.setTarget(new GreetingImpl());    //摄入目标类对象
        //proxyFactory.addAdvice(new GreetingBeforeAdvice());   //添加前置增强
        //proxyFactory.addAdvice(new GreetingAfterAdvice());   //添加后置增强
        //proxyFactory.addAdvice(new GreetingBeforeAndAfterAdvice());   //实现两个接口
        proxyFactory.addAdvice(new GreetingAroundAdvice());  //实现一个Around环绕式接口
        Greeting greeting = (Greeting) proxyFactory.getProxy();
        greeting.sayHello("Jack");
    }
}

以上就是SpringAOP的基本用法,单这只是“编程式”而已。Spring AOP如果只是这样,那就太弱了,它曾经也一度宣传用Spring配置文件的方式来定义Bean对象,把代码中的new操作全部解脱出来。

SpringAOP:前置增强、后置增强、环绕增强(声明式)

Spring配置文件篇日志

    <!--扫描指定包(将带有Component注解的类自动定义为SpringBean)-->
    <context:component-scan base-package="com.smart4j.framework"/>

    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interfaces" value="com.smart4j.framework.Greeting"/>   <!--需要代理的接口-->
        <property name="target" ref="greetingImpl"/>   <!--实现接口类-->
        <property name="interceptorNames">   <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
            <list>
                <value>greetingAroundAdvice</value>
            </list>
        </property>
    </bean>

使用ProxyFactoryFactoryBean就可以取代前面的ProxyFactory,其实他们是一回事。interceptorNames改名为adviceNames或许更容易让人理解。就是网这个属性里添加增强类。

此外,如果只有一个增强类,可以使用下面这个方法来简化

    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="interfaces" value="com.smart4j.framework.Greeting"/>   <!--需要代理的接口  name也可以为proxyInterfaces-->
        <property name="target" ref="greetingImpl"/>   <!--实现接口类   name也可以为targetName-->
        <property name="interceptorNames" value="greetingAroundAdvice">   <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
        </property>
    </bean>

需要注意的是,这里使用了Spring2.5+的"Bean扫描"特性,这样我们就无需再Spring配置问加你了不断的定义<bean id="xxx" class="xxx"/>了,从而解脱了我们的双手。

去掉原本的

    <bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"></bean>

    <bean id="greetingAroundAdvice" class="com.smart4j.framework.aop.GreetingAroundAdvice"></bean>

改为使用@Compoent

@Component
public class GreetingImpl implements Greeting{
。。。
}

@Component
public class GreetingAroundAdvice implements MethodInterceptor {
。。。
}

代码量确实少了,我们将配置性的代码放入配置文件,这样也有助于后期维护。更重要的是,代码值关注于业务逻辑,而将配置放入文件中,这是一条最佳实践!

除了上面提到的那三个增强意外,其实还有两个增强也需要了解一下,关键的时候要能想到它们才行。

Spring AOP:抛出增强

程序报错,抛出异常了,一般的做法是打印控制台到日志文件中,这样很多地方都得去处理,有没有一个一劳永逸的方法呢?那就是Throws Advice(抛出增强)

@Component
public class GreetingThrowAdvice implements ThrowsAdvice {

    public void afterThrowing(Method method, Object[] args, Object target,Exception e){
        System.out.println("-------------Throw Exception-----------------");
        System.out.println("Target class: "+target.getClass().getName());
        System.out.println("Method Name: "+method.getName());
        System.out.println("Exception Message: "+e.getMessage());
        System.out.println("---------------------------------------------");
    }
}

配置spring.xml

    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="proxyInterfaces" value="com.smart4j.framework.Greeting"/>   <!--需要代理的接口-->
        <property name="targetName" value="greetingImpl"/>   <!--实现接口类-->
        <property name="interceptorNames">   <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
            <list>
               <!-- <value>greetingAroundAdvice</value>-->
                <value>greetingThrowAdvice</value>
            </list>
        </property>
    </bean>

结果

架构探险笔记4-使框架具备AOP特性(上)

 抛出增强需要实现org.springframework.aop.ThrowsAdvice接口,在接口方法中可获取方法、参数、目标对象、异常对象等信息。我们可以把这些信息统一写入到日志中,当然也可以持久化到数据库中。

Spring AOP:引入增强

以上提到的都是对方法的增强,那能否对类进行增强呢?用AOP的行话来讲,对方法的增强叫Weaving(织入),而对类的增强叫Introduction(引入),Introduction Advice(引入增强)就是对类的功能增强,它也是Spring AOP提供的最后一种增强。

定义接口

public interface Apology {
    void saySorry(String name);
}

但是我们不想在代码中让GreetingImpl直接去实现这个接口,而想在程序运行的时候动态地实现它。因为加入实现了这个接口,那么久一定要改写GreetingImpl这个类,关键是我们不想改它,或许在真实场景中,这个类有一万行代码。于是,我们需要借助Spring的引入增强。

@Component
public class GreetingIntroAdvice extends DelegatingIntroductionInterceptor implements Apology{

    @Override
    public Object invoke(MethodInvocation mi) throws Throwable {
        return super.invoke(mi);
    }

    @Override
    public void saySorry(String name) {
        System.out.println("Sorry "+name);
    }
}

以上一个引入增强类,扩展了org.springframework.aop.support.DelegatingIntroductionInterceptor类,同时也实现了新定义的Apology接口。在类中首先覆盖了父类的invoke()方法,然后实现了Apology接口的方法。我们相拥这个增强类去丰富GreetingImpl类的功能,那么这个GreetingImpl类无须直接实现Apology接口,就可以直接在程序运行的时候调用Apology接口的方法了。

配置Spring.xml

    <!--引入增强-->
    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="proxyInterfaces" value="com.smart4j.framework.aop.Apology"/>   <!--需要动态实现的接口-->
        <property name="targetName" value="greetingImpl"/>   <!--目标类-->
        <property name="interceptorNames" value="greetingIntroAdvice"/>   <!--拦截器名称(也就是增强类名称,SpringBean的Id)-->
        <property name="proxyTargetClass" value="true"/>   <!--代理目标类,(默认为false,代理接口)-->
    </bean>

需要注意proxyTargetClass属性,它表明是否代理目标类,默认为false,也就是代理接口,此时Spring就用JDK动态代理;如果为TRUE,那么Spring就用CGLib动态代理。

调用

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
        Greeting greeting = (Greeting) context.getBean("greetingProxy");  //从Context中根据id获取Bean对象(其实也就是一个代理)
        greeting.sayHello("jack");   //调用代理方法

        Apology apology = (Apology) greeting;   //将目标类增强向上转型为Apology接口(这是引入增强给我们带来的特性,也是"接口动态实现"功能)
        apology.saySorry("jack");

sarySorry方法原来是可以被greetingImpl对象来直接调用的,只需将其强制转换为该接口即可。

SpringAOP:切面

之前谈到的AOP框架其实可以将它理解为一个拦截器框架,但这个拦截器似乎非常武断。比如说,如果它拦截了一个类,那么它就拦截这个类中所有的方法。类似的,当我们在使用动态代理的时候,其实也遇到了这个问题。需要在代码中对所拦截的方法名加以判断,才能过滤出我们需要拦截的方法,这种做法确实不太优雅。在大量的真实项目中,似乎我们只需要拦截特定的方法就行了,没必要拦截所有的方法。于是,Spring借助很重要的工具---Advisor(切面),来解决这个问题。它也是AOP中的核心,是我们关注的重点。

也就是说,我们可以通过切面,将增强类与拦截匹配条件组合在一起,然后将这个切面配置到ProxyFactory中,从而生成代理。

这里提到这个“拦截匹配条件”在AOP中就叫作Pointcut(切点),其实说白了就是一个基于表达式的拦截条件。Advisor(切面)封装了Advice(增强)与Pointcut(切点)。

@Component
public class GreetingImpl implements Greeting {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello! "+name);
    }

    /*切面新增方法*/
    public void goodMorning(String name){
        System.out.println("Good Morning!"+name);
    }

    public void goodNight(String name){
        System.out.println("Good Night!"+name);
    }
}

在Spring AOP中,最好用的是基于正则表达式的切面类。

配置

    <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="advice" ref="greetingAroundAdvice"/>  <!--增强-->
        <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/>    <!--切点(正则表达式)-->
    </bean>
    <!--配置一个代理类-->
    <bean id="greetingProxy" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="targetName" value="greetingImpl"/>   <!--目标类-->
        <property name="interceptorNames" value="greetingAdvisor"/>   <!--切面(替换之前的拦截器名称)-->
        <property name="proxyTargetClass" value="true"/>   <!--代理目标类,(默认为false,代理接口)-->
    </bean>

调用

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
        GreetingImpl greeting = (GreetingImpl) context.getBean("greetingProxy");  //从Context中根据id获取Bean对象(其实也就是一个代理)
        greeting.sayHello("jack");   //调用未被代理方法
        greeting.goodMorning("Jhon");
        greeting.goodNight("Sawer");

结果

架构探险笔记4-使框架具备AOP特性(上)

注意以上代理对象中的配置的interceptorNames,它不再是一个增强,而是一个切面,因为已经将增强封装到该切面中了。此外,切面还定义了一个切点(正则表达式),其目的是为了只对满足切点匹配条件的方法进行拦截。

这里的切点表达式是基于正则表达式的。其中.*代表匹配所有字符,翻译过来就是匹配GreetingImpl类中以good开头的方法。

除了RegexpMethodPointcutAdvisor以外,在Spring AOP中还提供了几个切面类,比如:

  • DefaultPointcutAdvisor - 默认切面(可扩展它来自定义切面)
  • NameMatchMethodPointcutAdvisor - 根据方法名称进行匹配的切面
  • StaticMethodMatcherPointcutAdvisor - 用于匹配静态方法的切面

总的来说,让用户去配置一个或少数几个代理,似乎还可以接受,但随着项目的扩大,代理配置就会越来越多,配置的重复劳动就多了,麻烦不说,还很容易出错。。能否让Spring框架为我们自动生成代理呢?

Spring AOP:自动代理(扫描Bean名称)

Spring AOP提供了一个可以根据Bean名称来自动生成代理的工具,它就是BeanNameAutoProxyCreator。配置如下:

    <!--自动代理-->
    <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
        <property name="beanNames" value="*Impl"/>   <!--为后缀是Impl的Bean生成代理-->
        <property name="interceptorNames" value="greetingAroundAdvice"/>   <!--增强(这里没用切面)-->
        <property name="optimize" value="true"/>   <!--是否对代理生成策略进行优化-->
    </bean>

调用

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
        GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl");  //从Context中根据id获取Bean对象(自动扫描的id为首字母小写的类名)
        greeting.sayHello("jack");

 

以上使用BeanNameAutoProxyCreator只为后缀为"Impl"的Bean生成代理。需要注意的是,这个地方我们不能定义代理接口,也及时interfaces属性,因为我们根本就不知道这些Bean到底实现了多少接口。此时不能代理接口,而只能代理类。所以这里提供了一个新的配置项,它就是optimize。若为true时,则可对代理生成策略进行优化(默认是false)。也就是说,如果该类有接口,就代理接口(JDK动态代理);如果没有接口,就代理类(使用CGLib动态代理)。并非像之前使用的proxyTargetClass属性那样,强制代理类,而不考虑代理接口的方式。

既然CGLib可以代理任何类,那为什么还要用JDK的动态代理呢?

根据实际项目经验得知,CGLib创建代理的速度比较慢,但创建代理后运行的速度却非常快,而JDK动态代理正好相反。如果在运行的时候不断地用CGLib去创建代理,系统的性能会大打折扣,所以建议一般在系统初始化的时候用CGLib去创建代理,并放入Spring的ApplicationContext中以备后用。

这个例子只能匹配目标类,而不能进一步匹配其中指定的方法,要匹配方法,就要考虑使用切面与切点了。Spring AOP基于切面也提供了一个自动代理生成器:DefaultAdvisorAutoProxyCreator。

Spring AOP:自动代理(扫描切面配置)

为了匹配目标类中的指定方法,我们让然需要在Spring中配置切面与切点:

    <!--自动代理 - 扫描切面配置-->
    <bean id="greetingAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
        <property name="pattern" value="com.smart4j.framework.GreetingImpl.good.*"/>
        <property name="advice" ref="greetingAroundAdvice"/>
    </bean>

    <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator">
        <property name="optimize" value="true" />
    </bean>

这里无须再配置代理,因为代理将由DefaultAdvisorAutoProxyCreator自动生成。也就是说,这个类可以扫描所有的切面类,并为其自动生成代理。

看来不论怎么简化,Rod始终解决不了切面的配置这件繁重的手工劳动。在Spring配置文件中,仍然会存在大量的切面配置。然而在很多情况下,Spring AOP所提供的切面类真的不太够用了,比如像拦截指定注解的方法,我们就必须扩展DefaultPointcutAdvisor类,自定义一个切面类,然后在Spring配置文件中进行切面配置。Rod的解决方案似乎已经掉进了切面类的深渊,最重要的是切面,最麻烦的也是切面。所以要把切面配置给简化掉。

Spring+AspectJ

神一样的rod总算认识到了这一点,接受了网友们的建议,集成了AspectJ,同时也保留了以上提到的切面与代理配置方式(为了兼容老项目,更为了维护自己的面子)。将Spring与AspectJ集成与直接使用AspectJ是不同的,我们不需要定义AspectJ类(它扩展了Java语法的一种新的语言,还需要特定的编译器),只需要使用AspectJ切点表达式即可(它是比正则表达式更加友好的表现形式)。

1.Spring+AspectJ(基于注解:通过AspectJ execution表达式拦截方法)

定义一个Aspect切面类

@Aspect    /*切面*/
@Component
public class GreetingAspect {

    @Around("execution(* com.smart4j.framework.GreetingImpl.*(..))")   /*切点*/
    public Object around(ProceedingJoinPoint pjp) throws Throwable {    /*增强*/
        before();
        Object result = pjp.proceed();
        after();
        return result;
    }

    private void before(){
        System.out.println("Before");
    }
    private void after(){
        System.out.println("After");
    }
}

注意:类上面标注的Aspect注解表明该类是一个Aspect(其实就是Advisor)。该类无须实现任何的接口,只需定义一个方法(方法叫什么名字都无所谓),在方法上标注Around注解,在注解中使用AspectJ切点表达式。方法的参数中包括一个ProceedingJoinPoint对象,它在AOP中称为Joinpoint(连接点),可以通过该对象获取方法的任何信息,例如,方法名、参数等。

解析下切点表达式execution(* com.smart4j.framework.GreetingImpl.*(..))

  • execution表示拦截方法,括号中可定义需要匹配的规则。
  • 第一个"*"表示方法的返回值是任意的;
  • 第二个"*"表示匹配该类中的所有方法;
  • (..)表示方法的参数是任意的。

是不是比正则表达式可读性更强呢?如果想匹配指定的方法,只需将第二个“*”改为指定的方法名即可。

配置如下

    <!--扫描指定包(将带有Component注解的类自动定义为SpringBean)-->
    <context:component-scan base-package="com.smart4j.framework"/>
    <aop:aspectj-autoproxy proxy-target-class="true"/>

两行配置就行了,不需要配置大量的代理,更不需要配置大量的切面!proxy-target-class属性,它的值默认是false,默认只能代理接口(使用JDK动态代理),当为true时,才能代理目标类(使用CGLib动态代理)。

Spring与AspectJ结合功能远远不止这些,我们还可以拦截指定注解的方法。

2.Spring+AspectJ(基于注解:通过AspectJ @annotation表达式拦截方法)

注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Tag {
}

以上定义一个Tag注解,此注解可标注在方法上,在运行时生效。

@Aspect    /*切面*/
@Component
public class GreetingAspect {

    @Around("@annotation(com.smart4j.framework.aspectj.Tag)")   /*切点 - 有Tag标记的Method*/
    public Object around(ProceedingJoinPoint pjp) throws Throwable {    /*增强*/
        before();
        Object result = pjp.proceed();
        after();
        return result;
    }

    private void before(){
        System.out.println("Before");
    }
    private void after(){
        System.out.println("After");
    }
}

直接将Tag注解定义在想要拦截的方法上

@Component
public class GreetingImpl implements Greeting {
    @Tag   /*AspectJ 注解*/
    @Override
    public void sayHello(String name) {
        System.out.println("Hello! "+name);
    }
}

在以上实例中只有一个方法,如果有多个方法,我们只想拦截其中的某一些时,这种解决方案会更加有价值

除了Around注解外,其实还有几个相关的注解,稍微归纳一下:

  • Before - 前置增强
  • After - 后置增强
  • Around - 环绕增强
  • AfterThrowing - 抛出增强
  • DeclareParents - 引入增强

此外还有一个AfterReturning(返回后增强),也可理解为Finally增强,相当于finally语句,它是在方法结束后执行的,也就是说,它比After晚一些。

3.Spring+AspectJ(引入增强)

为了实现基于AspectJ的引入增强,我们同样需要定义一个Aspect类:

@Aspect    /*切面*/
@Component
public class GreetingAspect {
    /*引入增强*/
    @DeclareParents(value = "com.smart4j.framework.GreetingImpl",defaultImpl = ApologyImpl.class)
    private Apology apology;
}

在Aspect类中定义一个需要引入增强的接口,它也就是运行时需要动态实现的接口。在这个接口上标注了DeclareParents注解,该注解有两个属性:

  • Value - 目标类;
  • defaultImpl - 引入接口的默认实现类。

我们只需要对引入的接口提供一个默认实现类即可完成增强:

public class ApologyImpl implements Apology {
    @Override
    public void saySorry(String name) {
        System.out.println("Sorry! " + name);
    }
}

运行

        ApplicationContext context = new ClassPathXmlApplicationContext("/spring.xml");  //获取Spring Context
        GreetingImpl greeting = (GreetingImpl) context.getBean("greetingImpl");  //从Context中根据id获取Bean对象(自动扫描的id为首字母小写的类名)
        greeting.sayHello("jack");

        Apology apology = (Apology) greeting;   //将目标类增强向上转型为Apology接口(这是引入增强给我们带来的特性,也是"接口动态实现"功能)
        apology.saySorry("jack");

从SpringApplicationContext中获取greetingImpl对象(其实是个代理对象),可转型为自己静态实现的接口Greeting,也可转型为自己动态实现的接口Apology,切换起来非常方便。

使用AspectJ的引入增强比原来的SpringAOP的引入增强更加方便了,而且还可面向接口编程(以前只能面向实现类)。

这一切已经非常强大并且非常灵活了,但仍然还是由用户不能尝试这些特性,因为他们还在使用JDK1.4(根本就没有注解这个东西),怎么办呢?SpringAOP为那些遗留系统也考虑到了。

3.Spring+AspectJ(基于配置)

除了使用Aspect注解来定义切面之外,SpringAOP也提供了基于配置的方式来定义切面类:

    <!--AspectJ - 基于配置-->
    <bean id="greetingImpl" class="com.smart4j.framework.GreetingImpl"/>
    <bean id="greetingAspect" class="com.smart4j.framework.aspectj.GreetingAspect"/>

    <aop:config>
        <aop:aspect ref="greetingAspect">
            <aop:around method="around" pointcut="execution(* com.smart4j.framework.GreetingImpl.*(..))"/>
        </aop:aspect>
    </aop:config>

使用<aop:config>元素来进行AOP配置,在其子元素中配置切面,包括增强类型、目标方法、切点等信息。

无论用户是不能使用注解,还是不愿意使用注解,SpringAOP都能提供全方位的服务。

AOP思维导图

架构探险笔记4-使框架具备AOP特性(上)

 

各类增强类型所对应的解决方案

架构探险笔记4-使框架具备AOP特性(上)

SpringAOP整体架构UML类图

架构探险笔记4-使框架具备AOP特性(上)

源码