文档章节

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

Lienson
 Lienson
发布于 2017/08/10 22:11
字数 2250
阅读 7
收藏 0

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

1.代理模式介绍

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

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

来看一张类结构图:

  • 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 Browser browser;

    public ChromeBrowserProxy(Browser browser) {
        this.browser = browser;
    }

    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

本文转载自:https://my.oschina.net/u/2377110/blog/1504596

Lienson
粉丝 15
博文 107
码字总数 97577
作品 0
福州
程序员
私信 提问
设计模式知识汇总(附github分享)

写在前面 主要内容 为了更系统的学习设计模式,特地开了这样一个基于Java的设计模式【集中营】,都是笔者在实际工作中用到过或者学习过的一些设计模式的一些提炼或者总检。慢慢地初见规模,也...

landy8530
2018/10/10
0
0
面试官:Spring中用了哪些设计模式?

前言 设计模式作为工作学习中的枕边书,却时常处于勤说不用的尴尬境地,也不是我们时常忘记,只是一直没有记忆。Spring作为业界的经典框架,无论是在架构设计方面,还是在代码编写方面,都堪...

Java填坑路
02/15
563
0
这些Spring中的设计模式,你都知道吗?

设计模式作为工作学习中的枕边书,却时常处于勤说不用的尴尬境地,也不是我们时常忘记,只是一直没有记忆。 Spring作为业界的经典框架,无论是在架构设计方面,还是在代码编写方面,都堪称行...

Java填坑之路
2018/08/17
0
0
Spring框架中的设计模式(四)​

Spring框架中的设计模式(四) 本文是Spring框架中使用的设计模式第四篇。本文将在此呈现出新的3种模式。一开始,我们会讨论2种结构模式:适配器和装饰器。在第三部分和最后一部分,我们将讨...

瑞查德-Jack
2018/07/20
85
0
大家自己手写设计模式吗?

希望如实回答,想评论 不会设计模式的都是屌丝的,不会设计模式都是码农的都别评论了。我接受不了。 自负,自傲的前辈也别评论了。我只是想知道,设计模式在工作中用的多不多。单例之类除外。...

韭零后张子游
2013/07/19
2.1K
33

没有更多内容

加载失败,请刷新页面

加载更多

OpenStack 简介和几种安装方式总结

OpenStack :是一个由NASA和Rackspace合作研发并发起的,以Apache许可证授权的自由软件和开放源代码项目。项目目标是提供实施简单、可大规模扩展、丰富、标准统一的云计算管理平台。OpenSta...

小海bug
昨天
6
0
DDD(五)

1、引言 之前学习了解了DDD中实体这一概念,那么接下来需要了解的就是值对象、唯一标识。值对象,值就是数字1、2、3,字符串“1”,“2”,“3”,值时对象的特征,对象是一个事物的具体描述...

MrYuZixian
昨天
6
0
数据库中间件MyCat

什么是MyCat? 查看官网的介绍是这样说的 一个彻底开源的,面向企业应用开发的大数据库集群 支持事务、ACID、可以替代MySQL的加强版数据库 一个可以视为MySQL集群的企业级数据库,用来替代昂贵...

沉浮_
昨天
6
0
解决Mac下VSCode打开zsh乱码

1.乱码问题 iTerm2终端使用Zsh,并且配置Zsh主题,该主题主题需要安装字体来支持箭头效果,在iTerm2中设置这个字体,但是VSCode里这个箭头还是显示乱码。 iTerm2展示如下: VSCode展示如下: 2...

HelloDeveloper
昨天
7
0
常用物流快递单号查询接口种类及对接方法

目前快递查询接口有两种方式可以对接,一是和顺丰、圆通、中通、天天、韵达、德邦这些快递公司一一对接接口,二是和快递鸟这样第三方集成接口一次性对接多家常用快递。第一种耗费时间长,但是...

程序的小猿
昨天
10
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部