从代理模式到Spring AOP原理

从代理模式到Spring AOP原理

(一)简介

Spring的核心是IOC与AOP,IOC主要是依赖关系的管理,包括依赖查询和依赖注入,在之前关于bean的文章中已经对bean的生命周期做了相对多的分析,基本了解了IOC的原理。在这里我们来探讨下AOP的实现原理。

AOP(面向方面编程),也被称为面向切面编程。AOP技术利用一种称为“横切”的技术,解剖封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,这样就能减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处都基本相似,比如权限认证、日志、事务处理等功能。

这些概念似乎很难懂,专业术语太多。确实,我第一次学也完全不知道他在讲什么。于是我们还是老规矩,先不看这些概念,自己去实现一个简单的AOP模块来帮助我们理解。实现AOP的技术,主要分为两大类:

采用动态代理技术,编写一个代理类的接口,实现一个真正的代理类,实现该接口,创建一个动态的代理类,实现InvocationHandler接口,并重写invoke()方法,调用Proxy.newProxyInstance()方法生成动态代理对象。

采用静态代理技术,自定义一个代理类(增强类)实现和被代理类(被增强类)相同的接口,在代理类中声明被代理类的对象,在代理类的方法中使用被代理类调用方法。

从代理模式到Spring AOP原理_第1张图片
这张图可以帮助大家大概了解静态代理和动态代理的主要区别,具体的细节我会在下面的实例中讲。

(二)静态代理版AOP

首先,我们需要定义我们的核心业务类的接口。

public interface IHello {
    void sayHello(String str);
}

然后,构建我们的目标类,也就是核心类。

public class Hello implements IHello{
    @Override
    public void sayHello(String str) {
        System.out.println("hello " + str);
    }
}

紧接着是代理类。代理类其实很好理解,就是一个类调用了目标类的方法,相当于一个替目标类做事的类。除了目标类的方法之外,代理类还会做一些别的工作,比如在这里我们可以让他写日志。

public class ProxyHello implements IHello{    
    private Hello hello;
        
    public ProxyHello() {
        super();
        this.hello = new Hello();
    }
    
    @Override
    public void sayHello(String str) {
        Logger.start();  //添加特定的方法
        hello.sayHello(str);  //调用目标类方法
        Logger.end();  //添加特定的方法
    }
}

用来写日志的调用类,将会在代理类中调用。

public class Logger {
    public static void start(){
        System.out.println(new Date()+ " say hello start");
    }
    
    public static void end(){
        System.out.println(new Date()+ " say hello end");
    }
}

测试一下,需要日志功能时,我们就可以使用代理类,不需要日志功能时我们直接使用目标类即可。

public class Test {
    public static void main(String[] args) {
        IHello hello = new ProxyHello();  //需要日志功能,使用代理类
        //IHello hello = new Hello();  //不需要日志功能,直接使用目标类
        hello.sayHello("tomorrow");
    }
}

这样我们就实现了一个最简单的AOP。但是静态代理的实现会存在一个问题:如果我们像Hello这样的类很多,那么我们就要去写很多个HelloProxy这样的类。很明显这是一件麻烦的事,也不利于后期维护。那么我们就会想办法来改进,这也就出现了动态代理的实现方式。

(三)动态代理版AOP

我们先来看一种简单的基于JDK Proxy的动态代理实现方式。首先,我们依旧先把我们的核心业务接口和目标类写好。

public interface IHello {
    void sayHello(String str);
}
public class Hello implements IHello{
    @Override
    public void sayHello(String str) {
        System.out.println("hello " + str);
    }
}

注意这个日志的接口,我们传入的参数是一个方法对象

public interface ILogger {
	void start(Method method);
	void end(Method method);
}

日志实现类也与之前不太一样,这里我们可以手动来获取方法名,这样日志中就可以记录调用的方法的名称。

public class DLogger implements ILogger{
    @Override
    public void start(Method method) {
        System.out.println(new Date() + method.getName() + " say hello start");
    }
    
    @Override
    public void end(Method method) {
        System.out.println(new Date() + method.getName() + " say hello end");
    }
}

下面我们来看看这个DynaProxyHello类,很容易发现他是InvocationHandler接口的实现类。首先我们会注意到两个属性,一个是proxy,一个是target,分别代表了我们的调用对象(这个案例中指日志模块)和目标对象。其中bind方法中调用了newProxyInstance方法,他通过字符串化产生一个新的java类,再动态编译返回对象,注意是动态编译。简单来说就是他通过一些传递进来的参数,自己动态模拟写了一个java类,实例化对象之后返回给用户,这个对象就是我们的代理对象。

invoke方法中就是通过反射来调用日志方法和核心方法,我们可以看见start.invoke、method.invoke、end.invoke,因为用了反射所以看起来可能复杂一些,但实际上功能与前面的静态代理并没有什么本质区别。当我们创建了的代理类对象进行方法调用的时候,都会转化为对invoke方法的调用。

public class DynaProxyHello implements InvocationHandler{
    private Object proxy;  //调用对象
    private Object target;  //目标对象

    public Object bind(Object target,Object proxy){
        this.target = target;
        this.proxy = proxy;
        //生成代理对象
        return Proxy.newProxyInstance(this.target.getClass().getClassLoader(), this.target.getClass().getInterfaces(), this);
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        Object result = null;
        //反射得到调用类
        Class clazz = this.proxy.getClass();
        //反射得到调用类的Start方法
        Method start = clazz.getDeclaredMethod("start", new Class[]{Method.class});
        //反射执行start方法
        start.invoke(this.proxy, new Object[]{method});
        //执行目标类的核心方法
        method.invoke(this.target, args);
        //反射得到调用类的end方法
        Method end = clazz.getDeclaredMethod("end", new Class[]{Method.class});
        //反射执行end方法
        end.invoke(this.proxy, new Object[]{method});
        return result;
    }
}

下面来测试一下。

public class Test {
    public static void main(String[] args) {
        IHello hello = (IHello) new DynaProxyHello().bind(new Hello(), new DLogger());  //需要日志功能,使用代理类
        //IHello hello = new Hello();  //不需要日志功能,直接使用目标类
        hello.sayHello("tomorrow");
    }
}

我们可以发现JDK Proxy版的动态代理有一个问题,就是只能针对接口进行代理(测试类主函数的第一行 IHello hello = (IHello) new DynaProxyHello().bind(new Hello(), new DLogger()); 需要基于IHello接口),如果我们要代理一个普通对象,这个方法是无法使用的。因此我们需要寻找另一种方法。

CGLIB,一个比JDK Proxy的动态代理更加强大的代理工具,底层使用了ASM(一个短小精悍的字节码操作框架)来操作字节码生成新的类,比使用反射效率更高。他可以实现普通对象(无接口)的代理,并且代码更加紧凑易读,下面我们就用CGLIB来实现与上面同样的功能。

首先我们需要导入cglib包,我这里用的是当前的最新版本3.3.0。

<dependency>
      <groupId>cglibgroupId>
      <artifactId>cglibartifactId>
      <version>3.3.0version>
dependency>

然后将之前写过的日志模块的接口和实现类写好。

public interface ILogger {
    void start(Method method);
    void end(Method method);
}
public class DLogger implements ILogger {
    @Override
    public void start(Method method) {
        System.out.println(new Date() + method.getName() + " say hello start");
    }

    @Override
    public void end(Method method) {
        System.out.println(new Date() + method.getName() + " say hello end");
    }
}

然后是我们的核心业务类。

public class Hello {
    public void sayHello(String str) {
        System.out.println("hello " + str);
    }
}

紧接着,是最关键的拦截器,这是与上面JDK的动态代理最不一样的地方。这个类要实现MethodInterceptor接口,实际上实现的功能和上面的invoke方法基本一致,我们这里可以先把他当做invoke方法来理解。

public class DynaProxyHello implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        DLogger dLogger = new DLogger();
        dLogger.start(method);
        Object object = methodProxy.invokeSuper(o, objects);
        dLogger.end(method);
        return object;
    }
}

然后是测试类,这个类明显比较复杂,我们来看看他干了些什么事。首先将目标类Hello设置成父类,然后设置拦截器DynaProxyHello,最后执行enhancer.create()动态生成一个代理类,并从Object强制转型成父类Hello。其中,Enhancer类是CGLib中的一个字节码增强器,它可以方便的对你想要处理的类进行扩展,我们暂时还不需要了解这么细,想弄明白的朋友可以自行Google。

public class Test {
    public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        //继承目标类
        enhancer.setSuperclass(Hello.class);
        //设置回调
        enhancer.setCallback(new DynaProxyHello());
        //生成代理对象
        Hello hello = (Hello) enhancer.create();
        hello.sayHello("tomorrow");
    }
}

这样一个基于CGLib的动态代理就写完了,我个人感觉这个过程比基于JDK的动态代理要简单一点,不过涉及的知识点反而更多了,需要去仔细的思考。我们可以看一下刚刚讲的两种动态代理方式的AOP实现的区别:

从代理模式到Spring AOP原理_第2张图片

不卖关子了,实际上Spring AOP就是基于动态代理的。如果要代理的对象,实现了某个接口,那么Spring AOP会使用JDK Proxy去创建代理对象。而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候Spring AOP会使用CGlib生成一个被代理对象的子类来作为代理。当然,我们也可以设置强制使用CGlib。

2020年7月23日

你可能感兴趣的:(Spring技术分析)