Spring AOP 与 动态代理

    前端时间,去参加网易的实习生招聘,面试官问了一个Spring AOP相关的问题:如果有一个没被 aspect 织入的函数A去调用被 aspect 织入的函数B,那么函数A在执行时会有函数B的织入效果吗?

    当时是这个问题没有回答上来,确实没有试过这种情况。现在就来试试这种情况,以及分析一下Spring AOP的相关原理。我们都知道Spring AOP中使用的是动态代理的技术,其中包括了JDK动态代理和CGLIB动态代理,这两种代理分别对应对接口和类进行动态代理类的生成。下面我们通过具体的例子来看这两种方式的区别。

1. JDK动态代理

    首先我们先看看Java中最常使用的动态代理方式。JDK动态代理使用的代理设计模式来进行设计的,针对接口生成代理类。具体例子:

public interface Person {
    String sayHello(String name);
    void eat(String food);
}
public class Chinese implements Person {
    @Override
    public String sayHello(String name) {
        System.out.println("-- sayHello() --");
        return name + " hello";
    }

    @Override
    public void eat(String food) {
        System.out.println("我正在吃:" + food);
    }
}

首先是一个 Person 接口,对应的具体实现类是 Chinese 类。JDK动态代理需要定义一个 InvocationHandler 实现类,在类中传入一个 Chinese 实例,通过 Chinese 的实例对方法进行修饰。

public class ChineseInvocation implements InvocationHandler {

    private Chinese chinese;

    public ChineseInvocation(Chinese chinese) {
        this.chinese = chinese;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if (method.getName().equals("sayHello")) {
            System.out.println("事务开始");
            method.invoke(chinese, args);
            System.out.println("事务结束");
        } else {
            method.invoke(chinese, args);
        }

        return null;
    }
}
public class ChineseProxy {

    public static void main(String args[]) {
        Chinese chinese = new Chinese();

        ChineseInvocation chineseInvocation = new ChineseInvocation(chinese);

        Person chineseProxy = (Person) Proxy.newProxyInstance(
                chinese.getClass().getClassLoader(),
                chinese.getClass().getInterfaces(),
                chineseInvocation
        );

        chineseProxy.sayHello("hello");
        chineseProxy.eat("Apple");
    }
}

最后执行的代码如上,新建一个 Chinese 实例,传入 ChineseInvocation 中,使用 Proxy.newProxyInstance() 生成代理实例 chineseProxy,执行结果:


由于在 ChineseInvocation 对方法进行了判断,只有 sayHello 方法才会在前后加上“事务”标识。这是一个标准的代理模式实现,把具体实现类 Chinese 封装到代理类中。其实没有 Chinese 具体实现类,也是可以产生代理类的,此时 invoke 方法就是代理类中的方法。

public class ChineseInvocation implements InvocationHandler {

//    private Chinese chinese;

//    public ChineseInvocation(Chinese chinese) {
//        this.chinese = chinese;
//    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if (method.getName().equals("sayHello")) {
            System.out.println("事务开始");
//            method.invoke(chinese, args);
            System.out.println("事务结束");
        } else {
//            method.invoke(chinese, args);
            System.out.println("do nothing!");
        }

        return null;
    }
}

比如,把 ChineseInvocation 改为上面代码,执行结果就变为:


    我们需要特别注意一个参数,那就是 InvocationHandler 中 invoke 方法中的第一个参数 proxy,这个参数有什么意义?

    我们可以看到 invoke 的返回值类型是 Object ,所以执行完代理类方法以后我们可以把这个 proxy 返回以进行连续调用,还有一个作用是可以使用反射获取到代理类的相关信息

2.Spring AOP

    关于Spring AOP的实现以及如何在JDK动态代理和CGLIB动态代理中选择参考:Spring AOP的实现原理

3.Spring AOP中的动态代理

    我们需要知道的是String AOP中的JDK动态代理是根据 PointCut 定义找到 Aspect 需要织入的方法,然后根据动态代理生成新的代理类,新的代理类中调用具体实现类的方法,比如上面 Chinese 中的方法,并且通过 Aspect 的定义在具体实现类的前后加上切片代码,最常见的比如日志操作和事务管理。

    所以就回到了最开始的问题。Spring的依赖注入功能在注入有切片的实例时,会根据动态代理生成代理对象,代理对象中根据具体实现类的方法织入切片代码。(是否织入代码和哪些方法织入哪些代码根据pointcut和aspect来确定。)加上代码这一原理与我们平时写Java动态代理代码时一致的,都需要生成一个具体实例,然后再调用这个实例的方法前后加上切片代码。

    所以,这其中涉及到了两个主要实例,动态代理生成的实例和我们自己定义的具体实现实例。而代理实例执行时会调用具体实现实例中的方法,切片代码会在代理实例中加入,所以一个A方法调用B方法,其实只是实例内部调用,不涉及代理实例中的B方法,因此不会出现切片效果。

展开阅读全文

没有更多推荐了,返回首页