Spring源码-AOP(一)-代理模式

转自:青离的博客.请去原创作者博客看
在我们的项目中,往往会出现许多业务或功能存在相同或相似的操作,这些操作与具体的业务逻辑相关性不大,比如记录关键的操作日志,或者更新数据库的事务控制等。因为这些操作散落在众多的不相关的业务间,不能通过继承的体系去管理,而通过工具类的方法也会显得代码的繁琐以及一些控制粒度的细分问题,因而就出现了AOP(Aspect-Oriented Programming),即面向切面编程。在这一节中,我不想直接谈AOP的有关内容,而是先聊聊AOP中必须的一个设计模式:代理模式,以及它的一些实现方式。

1.代理模式介绍

什么是代理模式?就是用一个新的对象来伪装原来的对象,从而实现一些“不可告人”的动作。

什么情况下会使用代理模式?简单来说,就是不能或者不想直接引用一个对象。什么是不能?比如我在内网中想访问外网的资源,但是因为网关的控制,访问不了。那什么是不想呢?比如我在网页上要显示一张图片,但是图片太大了,会拉慢页面的加载速度,我想用一张小一点的图片代替。

来看一张类结构图:


Class Proxy
  • Subject:原对象的抽象
  • RealSubject:原对象的实现
  • Proxy: 代理对象

通过代理模式,客户端访问时同原来一样,但访问的前后已经做了额外的操作(可能你的信息和数据就被窃取了)。

好了,来看一个正常点的例子。做IT的一般都需要翻墙,比如去YouTube上看点MV啥的(说好的正常呢),但是正常访问肯定是要被屏蔽的,所以就要通过一些工具去穿过重重防守的GTW。一般的方式就是本地的工具将你的访问信息加密后,交给一个未被屏蔽的国外的服务器,然后服务器解密这些访问信息,去请求原始的访问地址,再将请求得到的资源和信息回传给你自己的本地。我们以浏览器来举例。

浏览器接口:

public interface Browser {
    void visitInternet();
}

Chrome的实现类:

public class ChromeBrowser implements Browser{
    public void visitInternet() {
        System.out.println("visit YouTube");
    }
}

如果直接访问肯定是要挂掉的,我们通过解密和加密的两个方法简单模拟翻墙的过程。

public class ChromeBrowser implements Browser{
    public void visitInternet() {
        encrypt();
        System.out.println("visit YouTube");
        decrypt();
    }
    
    // 加密
    private void encrypt(){
        System.out.println("encrypt ...");
    }
    
    // 解密
    private void decrypt(){
        System.out.println("decrypt ...");
    }
}

虽然这样就可以访问成功了,但直接将加密和解密的方式写死在原对象里,不仅侵入了原有的代码结构,而且会显得很LOW。那怎么办?代理模式啊。

2.静态代理

根据上面的代理模式的类图,最简单的方式就是写一个静态代理,为ChromeBrowser写一个代理类。

public class ChromeBrowserProxy implements Browser{
    private ChromeBrowser browser;
    
    public ChromeBrowserProxy(ChromeBrowser chromeBrowser) {
        this.browser = chromeBrowser;
    }

    public void visitInternet() {
        encrypt();
        browser.visitInternet();
        decrypt();
    }
    
    // 加密
    private void encrypt(){
        System.out.println("encrypt ...");
    }
    
    // 解密
    private void decrypt(){
        System.out.println("decrypt ...");
    }
}

ChromeBrowserProxy同样实现Browser接口,客户端访问时不再直接访问ChromeBrowser,而是通过它的代理类。

public class StaticProxyTest {

    public static void main(String[] args) {
        Browser browser = new ChromeBrowserProxy(new ChromeBrowser());
        browser.visitInternet();
    }
}

这种方式解决了对原对象的代码侵入,但是出现了另一个问题。如果我有好几个浏览器,难道每个浏览器的实现类都要写一个代理类吗?太LOW太LOW。我们需要更牛B的方式:JDK动态代理。

3.JDK动态代理

在JDK中提供了一种代理的实现方式,可以动态地创建代理类,就是java.lang.reflect包中的Proxy类提供的newProxyInstance方法。

Proxy.newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
  • classLoader是创建代理类的类加载器
  • interfaces是原对象实现的接口
  • InvocationHandler是回调方法的接口

真正的代理过程通过InvocationHandler接口中的invoke方法来实现

public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
  • proxy是代理对象
  • method是执行的方法
  • args是执行方法的参数数组

还是Chrome浏览器举例:

public class JdkBrowserProxy implements InvocationHandler{

    private Browser browser;
    
    public JdkBrowserProxy(Browser browser) {
        this.browser = browser;
    }
    
    public Browser getProxy(){
        return (Browser) Proxy.newProxyInstance(browser.getClass().getClassLoader(),
                browser.getClass().getInterfaces(), this);
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        encrypt();
        Object retVal = method.invoke(browser, args);
        decrypt();
        return retVal;
    }
    
    /**
     * 加密
     */
    private void encrypt(){
        System.out.println("encrypt ...");
    }
    
    /**
     * 解密
     */
    private void decrypt(){
        System.out.println("decrypt ...");
    }
}

JdkBrowserProxy实现InvocationHandler接口,并通过构造方法传入被代理的对象,然后在invoke方法中实现代理的过程。

来看测试方法

public class JdkDynamicProxyTest {

    public static void main(String[] args) {
        Browser browser = new JdkBrowserProxy(new ChromeBrowser()).getProxy();
        browser.visitInternet();
    }
}

JDK的动态代理基本能够解决大部分的需求,唯一的缺点就是它只能代理接口中的方法。如果被代理对象没有实现接口,或者想代理没在接口中定义的方法,JDK的动态代理就无能为力了,此时就需要CGLIB动态代理。

4.CGLIB动态代理

cglib是一种强大的,高性能高品质的代码生成库,用来在运行时扩展JAVA的类以及实现指定接口。

通过cglib提供的Enhancer类的create静态方法来创建代理类

Enhancer.create(Class type, Callback callback)
  • type是原对象的Class对象
  • callback是回调方法接口

cglib中的callback通过实现它的MethodInterceptor接口的intercept方法

public Object intercept(Object obj, java.lang.reflect.Method method, Object[] args, MethodProxy proxy) throws Throwable;
  • obj是被代理的对象
  • method是执行的方法
  • args是执行方法的参数数组
  • proxy用来执行未被拦截的原方法

这次的cglib代理类不局限于上面的浏览器的例子,而是通过泛型来实现通用,并且使用单例模式减少代理类的重复创建。

public class CglibBrowserProxy implements MethodInterceptor{

    private static CglibBrowserProxy proxy = new CglibBrowserProxy();
    
    private CglibBrowserProxy(){
        
    }
    
    public static CglibBrowserProxy getInstance(){
        return proxy;
    }

    public <T> T getProxy(Class<T> clazz){
        return (T) Enhancer.create(clazz, this);
    }
    
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        encrypt();
        Object retVal = proxy.invokeSuper(obj, args);
        decrypt();
        return retVal;
    }
    
    /**
     * 加密
     */
    private void encrypt(){
        System.out.println("encrypt ...");
    }
    
    /**
     * 解密
     */
    private void decrypt(){
        System.out.println("decrypt ...");
    }
}

然后在ChromeBrowser添加一个听音乐的方法,它并未在Browser接口定义

public void listenToMusic(){
    System.out.println("listen to Cranberries");
}

来看下客户端测试

public class CglibDynamicProxyTest {

    public static void main(String[] args) {
        ChromeBrowser browser = CglibBrowserProxy.getInstance().getProxy(ChromeBrowser.class);
        browser.visitInternet();
        browser.listenToMusic();
    }
}

可以发现没有使用Browser接口来接受代理对象,而是直接使用ChromeBrowser对象。这样的方式就可以代理ChromeBrowser中未在Chrome接口中的方法。

如果想让一个对象调用它未实现的接口中的方法,即后面AOP里所说的引用增强,原生的cglib怎么实现呢?

5.CGLIB引入增强

引入增强听上去很高大上,其实它的实现原理就以下几步:

  1. 通过CGLIB创建代理对象,并使其实现指定接口
  2. 在MethodIntercept的回调方法中,判断执行方法是否为接口中的方法,如果是,则通过反射调用接口的实现类。

创建一个新接口Game,它定义了开始的方法

public interface Game {
    void start();
}

让代理类实现Game接口,并在intercept方法中判断执行方法是接口方法还是原对象的方法

public class CglibIntroductionBrowserProxy implements MethodInterceptor,Game{

    private static CglibIntroductionBrowserProxy proxy = new CglibIntroductionBrowserProxy();
    
    private CglibIntroductionBrowserProxy(){
        
    }
    
    public static CglibIntroductionBrowserProxy getInstance(){
        return proxy;
    }

    public <T> T getProxy(Class<T> clazz){
        return (T) Enhancer.create(clazz, new Class[]{ Game.class }, this);
    }
    
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        Object retVal;
        if(method.getDeclaringClass().isInterface()){
            method.setAccessible(true);
            retVal = method.invoke(this, args); 
        }else{
            retVal = proxy.invokeSuper(obj, args);
        }
        return retVal;
    }

    public void start() {
        System.out.println("start a game");
    }

}

可以发现执行接口方法时,通过jdk的反射机制来实现的。而调用其自身方法,则是通过cglib来触发的。在上面的intercept方法中也可以在方法执行的前后添加一些操作来扩展或改变原方法。

来看测试类

public class CglibIntroductionDynamicProxyTest {

    public static void main(String[] args) {
        Browser browser = CglibIntroductionBrowserProxy.getInstance().getProxy(ChromeBrowser.class);
        browser.visitInternet();
        
        Game game = (Game) browser;
        game.start();
    }
}

最后补充几点

  1. JDK动态代理的代理对象只能通过接口去接收,如果用原对象接收,会报类型转换异常
  2. cglib不能拦截final修饰的方法,调用时只会执行原有方法
  3. cglib是在运行时通过操作字节码来完成类的扩展和改变,除了代理,还支持很多强大的操作,比如bean的生成和属性copy,动态创建接口以及融合多个对象等,具体见https://github.com/cglib/cglib/wiki/Tutorial

参考文档:

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

推荐阅读更多精彩内容

  • 前言 之前已经完成了IOC的源码分析,接下来分析下AOP相关代码。在分析之前,先记录下aop的概念。 概念:作为面...
    阿亮私语阅读 1,244评论 3 7
  • 参考资料:菜鸟教程之设计模式 设计模式概述 设计模式(Design pattern)代表了最佳的实践,通常被有经验...
    Steven1997阅读 1,176评论 1 12
  • 设计模式概述 在学习面向对象七大设计原则时需要注意以下几点:a) 高内聚、低耦合和单一职能的“冲突”实际上,这两者...
    彦帧阅读 3,752评论 0 14
  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 12,296评论 6 86
  • 2018年第一次推荐书籍《跃迁》通读多遍,发现很多经历与我相识,老话说得好“读万卷书不如行万里路,行万里路不如高人...
    Peter唐明阅读 290评论 0 0