30分钟学透设计模式5-从代理模式到AOP

设计模式系列:
30分钟学透设计模式1-单例模式的前世今生
30分钟学透设计模式2-随处可见的Builder模式
30分钟学透设计模式3-使用最多的Iterator模式
30分钟学透设计模式4-最简单的面向接口编程-简单工厂模式
30分钟学透设计模式5-从代理模式到AOP

一、概述

代理(Proxy),顾名思义,你不用去做,交给别人去做。这其中暗含的意思是,你去做的话,需要花费额外的精力;或者说别人具有做这件事的功能。
代理模式:一个类代表另一个类的功能。我们创建包含对象的另外一个对象,用以对外提供强化性的接口。
如果仅仅是,用一个对象包裹另一个对象,这种代理其实没有什么含义。其主要是对其功能性的增强。
怎么理解,看下文。

二、静态代理

有下面这样一个接口。

public interface Caculator {
    int add(int x, int y);
}

其实现类为:

public class CaculatorImpl implements Caculator {
    @Override
    public int add(int x, int y) {
        return x + y;
    }
}

问题:现在有一些调用者要统计这个接口的耗时,要怎么处理呢?

方法1:修改实现类

public class CaculatorImpl implements Caculator {
    @Override
    public int add(int x, int y) {
        long start = System.currentTimeMillis();
        int ret =  x + y;
        System.out.println("total cost:" + (System.currentTimeMillis() - start));
        return ret;
    }
}

这样做不好,因为并不是所有的调用者都需要这个耗时数据,一刀切显然不合理。而且对原有代码进行了侵入。那还有其他方式吗?

方法2:调用者自己处理

调用者在调用add()方法前后,自己处理耗时信息。这种显然也不合理。

除了这两种方法外,还有其他方法吗?老司机都是怎么做的呢?

方法3:静态代理

正如概述中说的,我们可以用一个对象来包裹原有的对象,并增强其功能。怎么理解,具体看代码。

public class CaculatorProxy implements Caculator {
    private Caculator caculator;

    public CaculatorProxy() {
        caculator = new CaculatorImpl();
    }

    public int add(int x, int y) {
        long start = System.currentTimeMillis();
        int ret = caculator.add(x, y);
        System.out.println("total cost:" + (System.currentTimeMillis() - start));
        return ret;
    }

    public static void main(String[] args) {
        Caculator caculator = new CaculatorProxy();
        caculator.add(1, 2);
    }
}

不难看出,原来的CaculatorImpl对象,被包裹进了CaculatorProxy中,并实现了一个加强版的add()方法。

静态代理的特点

上面的方法3其实就是静态代理,是不是很简单。就是用Proxy代理,实现原有类的接口,并包裹下原有类,增强下功能。
优点:扩展原来的功能,不侵入原有代码。
缺点:不是功能性代理,水平扩展性差。
如果我们有另外一个接口,也需要对其方法进行耗时统计:

public interface HelloWorld {
    void sayHello();
}

那我们还得像之前的CaculatorProxy()那样重新写一个Proxy。之前的那个Proxy虽然也是对耗时统计,但却不能重复利用。
那怎么解决,功能性重用呢?

二、动态代理

1、使用JDK动态代理实现一个统计耗时代理

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class CostDynamicProxy implements InvocationHandler {
    private Object target; // 被代理的目标对象

    public CostDynamicProxy(Object target) {
        this.target = target; // 注入
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Exception {
        long start = System.currentTimeMillis();
        Object result = method.invoke(target, args);
        System.out.println("total cost:" + (System.currentTimeMillis() - start));
        return result;
    }

    public <T> T getProxy() {
        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                this
        );
    }
    public static void main(String[] args) {
        Caculator caculator = new CaculatorImpl();
        CostDynamicProxy caculatorProxy = new CostDynamicProxy(caculator);

        Caculator proxy = caculatorProxy.getProxy();
        proxy.add(1, 2);

        HelloWorld helloWorld = new HelloWorldImpl();
        CostDynamicProxy helloWorldProxy = new CostDynamicProxy(helloWorld);
        HelloWorld proxy1 = helloWorldProxy.getProxy();
        proxy1.sayHello();
    }
}

其实并不复杂,只不过把对某一具体的方法的代理,变成了对Java方法的代理。需要动态运行时替换进行代理。
这里面有两个重要的类,分别为:InvocationHandlerProxy,感兴趣的小伙伴可以多了解了解。

重点:从这个JDK动态代理可以看出,其实现了一个功能性代理(统计耗时的代理),无论哪个类,哪个方法都可以通过其得出耗时,不需要单独为其创建一个代理。也就是解决了静态代理的功能性重用问题。

2、CGLib动态代理

既然JDK动态代理,都已经如此完美了。那这节还需要介绍什么呢?
上面需要代理的方法,其都是实现了一个接口。如果一个类并没有实现任何接口,那我们怎么去做?

public class Cal {
    public int add(int x, int y) {
        return x + y;
    }
}

如果我们强行使用JDK动态代理,肯定会报错的。

所以说,我们可以使用CGLib来做动态代理。先看代码。

import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

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

    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy)
            throws Throwable {
        long start = System.currentTimeMillis();
        Object result = methodProxy.invokeSuper(o, objects);
        System.out.println("total cost:" + (System.currentTimeMillis() - start));
        return result;
    }

    public static void main(String[] args) {
        CglibProxy cglibProxy = new CglibProxy();
        Cal cal = cglibProxy.getProxy(Cal.class);
        cal.add(1, 2);
    }
}

需要实现MethodInterceptor()并重写方法 intercept()

三、Spring AOP

AOP即面向切面编程。切面即表示从业务逻辑中脱离出来的横截面,比如上面提到的统计耗时,还有性能解控、日志记录、权限控制等等。
总体来讲,通过AOP,实现切面可以解决代码以及功能的耦合问题,让职责更加单一。逻辑更加清晰。

1、特点

  • 让关注点代码与业务代码分离,可以动态地添加和删除在切面上的逻辑而不影响原来的执行代码。
  • 模块之间的耦合度低;容易扩展;代码复用;

2、前置增强、后置增强、环绕增强

上面的例子中,如果我们仅仅实现long start = xxxx,并且在调用add()方法之前,那这就是一个前置增强
同样地,如果我们仅仅实现System.out.println("total cost xxxx"),并且在调用add()方法之后,那这就是一个后置增强
如果既有前置、也有后置,那就是环绕增强

3、其他概念

  • 横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称之为横切关注点
  • 切面(aspect):类是对物体特征的抽象,切面就是对横切关注点的抽象。面向切面编程,就是指对很多功能都有的重复的代码抽取,再在运行的时候往业务方法上动态植入“切面类代码”。
  • 连接点(joinpoint):被拦截到的点,因为Spring只支持方法类型的连接点,所以在Spring中连接点指的就是被拦截到的方法,实际上连接点还可以是字段或者构造器
  • 切入点(pointcut):切入点在AOP中的通知和切入点表达式关联,指定拦截哪些类的哪些方法, 给指定的类在运行的时候动态的植入切面类代码。
  • 通知(advice):所谓通知指的就是指拦截到连接点之后要执行的代码,通知分为前置、后置、异常、最终、环绕通知五类
  • 目标对象:被一个或者多个切面所通知的对象。
  • 织入(weave):将切面应用到目标对象并导致代理对象创建的过程
  • 引入(introduction):在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
  • AOP代理(AOP Proxy):在Spring AOP中有两种代理方式,JDK动态代理和CGLIB代理。

4、AOP的实现原理

  • Spring AOP使用的为动态代理,即AOP框架不会去修改字节码,而是在内存中临时为方法生成一个AOP对象,这个AOP对象包含了目标对象的全部方法,并且在特定的切点做了增强处理,并回调原对象的方法。
  • Spring AOP中的动态代理主要有两种方式,JDK动态代理和CGLIB动态代理。JDK动态代理通过反射来接收被代理的类,并且要求被代理的类必须实现一个接口。JDK动态代理的核心是InvocationHandler接口和Proxy类。
  • 如果目标类没有实现接口,那么Spring AOP会选择使用CGLIB来动态代理目标类。CGLIB(Code Generation Library),是一个代码生成的类库,可以在运行时动态的生成某个类的子类,注意,CGLIB是通过继承的方式做的动态代理,因此如果某个类被标记为final,那么它是无法使用CGLIB做动态代理的。

猜你喜欢

转载自blog.csdn.net/f59130/article/details/80858680