Spring_AOP_02——实现原理

本文主要讲实现AOP的 代理模式原理,以及静态代理,动态代理的区别和具体实现。

对SpringAOP的概念和使用,可以参考以下文章:
Spring_AOP_01——概念讲解
SpringAOP基础和使用

AOP的实现思路是利用了代理模式,关键在于AOP框架对目标对象创建的AOP代理,实现了对目标对象的增强。

AOP的代理方式主要分为两种,静态代理 和 动态代理。
静态代理的代表为AspectJ
Spring使用动态代理,主要有JDK实现和cglib实现。

Spring创建代理的规则为:
默认使用JDK动态代理来创建AOP代理
当需要代理的类没有实现接口的时候,Spring会切换为使用cglib创建AOP代理
可以在配置文件制定,强制使用cglib代理

本篇主要内容:

  • 代理模式
  • AOP实现方式
  • 静态代理
  • AspectJ静态代理原理
  • 动态代理
  • JDK和cglib实现动态代理原理

关于JDK 代理 和 cglib 代理的 源码层原理,本篇不细讲。



代理模式

说到AOP实现原理,那么就首先需要明白什么是代理模式。这里简单讲解一下代理模式的原理。

代理模式使用代理对象完成用户的请求,控制对元对象的访问,屏蔽用户对真实对象的访问。代理模式是一种弄对象结构型模式。

UML结构图如下:

代理模式


代理模式的结构
代理模式主要包含三个角色:

  • Subject(抽象主题角色)
    它声明了真是主题和代理主题的共同接口,这样一来在任何使用真实主题的地方都可以使用代理主题,客户端通常需要针对抽象主题角色进行编程。

反映在实际环境中,这就是我们平时编程时定义的接口和超类。(分别代表JDK代理和cglib代理)

  • Proxy(代理主题角色)
    它包含了对真实主题的引用,从而可以在任何时候操作真实主题对象。在代理主题角色中,提供一个与真实主题角色相同的即可欧,以便在任何时候可以替代真实主题。

代理主题角色还可以控制对真实主题的使用,负责在需要的时候创建和删除真实主题对象,并对真实主题对象的使用加以约束。

在代理主题角色中,主要任务是定义在代理主题的操作,例如日志打印,权限控制等等,在调用真实操作之前or之后执行的操作。

  • RealSubject(真实主题角色)
    它定义了代理角色所代表的真实对象,在真实主题角色中实现了实际的业务操作。客户端可以通过代理角色间接的调用真实角色中定义的操作。



AOP的实现方式

AOP的实现使用的是代理模式。其中又分为静态代理和动态代理。
静态代理的代表为AspectJ
Spring使用动态代理,主要有JDK实现和cglib实现。

静态代理的缺点很明显
一个代理类只能对一个业务接口的实现类进行包装,如果有多个业务接口的话,就要定义很多实现类进行代理才行。

而且如果代理类对业务方法的预处理,调用后处理都是一样的(例如调用前输出提示,调用后关闭连接),则多个代理类就会有很多重复的代码。

这时我们可以定义一个这样的类,它能代理所有实现类的方法调用:根据传进来的业务实现类和方法名进行具体调用——那就是动态代理

接下来详细介绍一下各自的实现方式以及区别:

静态代理

静态代理就是在编译期生成代理类的方式。
其实上面的代理模式图,实现的就是静态代理。静态代理通常用于对业务的扩充。通过对真实对象的封装,来实现扩展性。

代码简单实现如下:

// 共同接口
public interface Action {
    public void doSomething();
}

//真实对象
public class RealObject implements Action{
    public void doSomething() {
        System.out.println("do something");
    }
}

//代理对象
public class Proxy implements Action {
    private Action realObject;

    public Proxy(Action realObject) {
        this.realObject = realObject;
    }
    public void doSomething() {
        System.out.println("proxy before do");
        realObject.doSomething();  //调用真实对象方法
        System.out.println("proxy after do");
    }
}

//运行代码
Proxy proxy = new Proxy(new RealObject());
proxy.doSomething();

这是一个以接口为抽象主题角色实现的静态代理。可以看到,代理对象和真实对象都实现了共同的接口。在代理对象中,保存了一个真实对象的对象引用。并且在调用对真实对象的操作前后,自己可以做一些其他操作。

AspectJ

AspectJ是一个静态代理的增强。需要明确的是AspectJ并不是Spring框架的一部分,而是一套独立的AOP解决方案。

AspectJ是一个java实现的AOP框架,它能够对Java代码进行AOP编译,让Java代码具有AspectJ的AOP功能(一般在编译器执行,需要aspectJ提供的编译器)

使用AspectJ需要依赖额外的第三方包和aspect的编译器。


AspectJ织入方式
AspectJ主要采用的是静态织入,在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。

AspectJ除了编译期织入,还存在链接期(编译后)织入,即将aspect类和java目标类同时编译成字节码后,再进行织入处理,这种方式比较有助于已编译好的第三方jar和class文件进行织入操作。(这不是本篇重点,详细的aspectJ原理可以自行查找资料)

当然,不管是编译期织入还是编译后织入,AspectJ都是静态代理的方式织入的。即先构建好代理类的class文件,再运行。

关于ajc编译器,是一种弄能够识别aspect研发的编译器,它采用java语言编写,由于javac并不鞥识别aspect语法,便有了ajc编译器。注意ajc编译器也可以编译java文件。

AspectJ织入

我们可以反编译一下ajc织入后的java文件,可以很直观的看到ajc是如何将代码织入的。

//编译后织入aspect类的HelloWord字节码反编译类
public class HelloWord {
    public HelloWord() {
    }

    public void sayHello() {
        System.out.println("hello world !");
    }

    public static void main(String[] args) {
        HelloWord helloWord = new HelloWord();
        HelloWord var10000 = helloWord;

   try {
        //MyAspectJDemo 切面类的前置通知织入
        MyAspectJDemo.aspectOf().ajc$before$com_zejian_demo_MyAspectJDemo$1$22c5541();
        //目标类函数的调用
           var10000.sayHello();
        } catch (Throwable var3) {
        MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574();
            throw var3;
        }

        //MyAspectJDemo 切面类的后置通知织入 
        MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574();
    }
}



动态代理

与静态代理相对的,动态代理的代理类,是在运行时才被动态的创建,所以叫做动态代理

动态代理的好处是:对代理类的函数进行统一管理。解决了解决静态代理中存在的功能重复和代码重复的问题。SpringAOP提供的就是动态代理支持。

与AspectJ一样,目的都是为了统一处理横切业务,但是与AspectJ不同的是,Spring AOP 并不尝试提供完成的AOP功能,而是更注重与Spring IOC 容器的结合,并利用该优势来解决横切业务的问题。

所以在AOP功能完善方面,AspectJ具有的优势更大。(Spring AOP只能在方法层面做横切,AspectJ可以对属性做横切)

同时,Spring注意到AspectJ在AOP的实现上,依赖于特殊的编译器(ajc编译器),因此Spring回避了这一点,Spring采用动态代理技术的实现原理来构建Spring AOP的内部机制(动态织入)。这是与AspectJ(静态织入)最根本的区别。

在AspectJ 1.5 之后,引入了 @Aspect 形式的注解风格开发,Spring也非常快地跟进了这种方式,在Spring 2.0之后便使用了与Aspect 1.5 一样的注解。

注意:Spring只是使用了AspectJ的注解,而没有使用AspectJ的编译器,低层还是使用动态代理技术实现。(很重要!)

Spring的 动态代理主要有两种方式

  • 使用了JDK Proxy的动态代理
  • 使用了cglib Ehancer 的动态代理。


JDK实现动态代理

JDK动态代理的原理,是利用反射机制,生成一个和目标类继承了一样接口的匿名代理类。在调用具体方法前使用InvocationHandler来处理。

JDK动态代理的对象在创建时,需要使用业务实现类的接口作为参数(因为在后面代理方法时需要根据接口内的方法名进行调用)。如果业务实现类没有实现接口,就无法使用JDK动态代理了。并且

关键接口:java.lang.reflect.InvocationHandler
关键类:java.lang.reflect.Proxyjava.lang.reflect.Method

代码示例如下:

// JDK代理类
public class JDKProxy implements InvocationHandler {

    private Object proxyObject;

    public JDKProxy(Object proxyObject) {
        this.proxyObject = proxyObject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // do something
        Object ret = null;  //方法返回值

        System.out.println("JDK Proxy --- 调用前 --- 调用方法是:"+method.getName() + "---参数是:"+ GsonUtil.toJson(args));

        ret = method.invoke(proxyObject, args); //调用invoke方法

        System.out.println("JDK Proxy --- 调用后");
        return ret;
    }
}

// 获取代理对象工厂方法
public class ProxyFactory {

    /**
     * 工厂方法,获取JDKProxy对象
     * @param proxyObject
     * @return
     */
    public static Object createJDKProxyInstance(Object proxyObject){
        JDKProxy jdkProxy = new JDKProxy(proxyObject);
        return Proxy.newProxyInstance(proxyObject.getClass().getClassLoader(), proxyObject.getClass().getInterfaces(), jdkProxy);
    }
}

// 测试代码
DemoManager jdkDemo = (DemoManager) ProxyFactory.createJDKProxyInstance(new DemoManagerImpl());
jdkDemo.add(1,"Antony");
jdkDemo.delete(2);

// 运行结果
JDK Proxy --- 调用前 --- 调用方法是:add---参数是:[1,"Antony"]
DemoManager add--- 调用啦---id=1name=Antony
JDK Proxy --- 调用后


cglib实现动态代理

cglib动态代理的原理是继承需要代理的类,生成的代理类是目标类的子类。用cglib生成的代理类重写了父类的各个方法,拦截器中的intercept方法内容正好就是代理类中的方法体。

cglib是一个代码生成的类库,低层是使用了ASM提供的字节码操控框架。通过在运行时动态地生成某个类的子类。cglib是采用继承的方式实现的代理,所以被声明为final的类无法代理。

关键接口:org.springframework.cglib.proxy.MethodInterceptor
关键类:org.springframework.cglib.proxy.Enhancerorg.springframework.cglib.proxy.MethodProxy

代码示例如下:

// cglib 代理对象的类
public class CGlibProxy implements MethodInterceptor {

    private Object proxyObject;

    public CGlibProxy(Object proxyObject) {
        this.proxyObject = proxyObject;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLib Proxy --- 调用前 --- 调用方法是:"+method.getName() + "---参数是:"+ GsonUtil.toJson(objects));

        Object ret = method.invoke(proxyObject, objects);

        System.out.println("CGLib Proxy --- 调用后");

        return ret;
    }
}

// 工厂方法,获取代理对象
public class ProxyFactory {
    /**
     * 工厂方法,获取cglibProxy对象
     * @param proxyObject
     * @return
     */
    public static Object createCGlibProxyInstance(Object proxyObject){
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(proxyObject.getClass());
        enhancer.setCallback(new CGlibProxy(proxyObject));
        return enhancer.create();
    }
}

// 测试代码
DemoManager cgDemo = (DemoManager) ProxyFactory.createCGlibProxyInstance(new DemoManagerImpl());
cgDemo.add(1,"Antony");
cgDemo.delete(2);

// 运行结果
CGLib Proxy --- 调用前 --- 调用方法是:add---参数是:[1,"Antony"]
DemoManager add--- 调用啦---id=1name=Antony
CGLib Proxy --- 调用后


两者的区别
JDK 使用继承接口的方式生成代理类。(实现接口,管理代理实例)
cglib 使用继承目标类的方式生成代理类。(生成目标类的子类,重写方法)

JDK只能对继承了接口的类代理。
cglib 除了 final 类都可以代理。

JDK代理类生成快,但是运行效率较cglib代理差。
cglib代理类生成慢,但是运行效率较JDK代理快。

Spring如何选择这两种代理

  • 目标对象实现了接口,默认使用JDK代理。
  • 目标对象实现了接口,可以选择强制使用 cglib 代理。
  • 目标对象没有实现接口,只能选择使用 cglib 代理。


为什么不全部使用cglib

cglib创建代理类的速度较慢,但是创建后运行的速度则很快。而JDK代理正好相反。如果在运行时全部使用cglib代理,则系统性能会显著下降。所以一般建议在系统初始化的时候使用cglib方式创建代理,放入Spring的ApplicationContext中以备后用。

SpringAOP 和 AspectJ的区别

Spring AOP AspectJ
在纯 Java 中实现 使用 Java 编程语言的扩展实现
不需要单独的编译过程 除非设置 LTW,否则需要 AspectJ 编译器 (ajc)
只能使用运行时织入 运行时织入不可用。支持编译时、编译后和加载时织入
功能不强-仅支持方法级编织 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等......。
只能在由 Spring 容器管理的 bean 上实现 可以在所有域对象上实现
仅支持方法执行切入点 支持所有切入点
代理是由目标对象创建的, 并且切面应用在这些代理上 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入
比 AspectJ 慢多了 更好的性能
易于学习和应用 相对于 Spring AOP 来说更复杂



(如果有什么错误或者建议,欢迎留言指出)
(本文内容是对各个知识点的转载整理,用于个人技术沉淀,以及大家学习交流用)


参考资料:
关于SpringAOP你该知晓的一切
AOP低层实现——JDK和CGLIB的动态代理

基于SpringAOP的 JDK动态代理和CGLIB动态代理
SpringAOP的两种代理方式:JDK动态代理和CGLIB动态代理

设计模式——代理模式
静态代理和动态代理的理解(附有源码分析)
AspectJ与SpringAOP的比较

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349