《Head-First-设计模式》笔记15-代理模式

定义

代理模式(Proxy Pattern) :为另一个对象提供一个替身或占位符以控制对这个对象的访问。

在某些情况下,一个客户不想或者不能直接引用一个对 象,此时可以通过一个称之为“代理”的第三者来实现 间接引用。代理对象可以在客户端和目标对象之间起到 中介的作用,并且可以通过代理对象去掉客户不能看到 的内容和服务或者添加客户需要的额外服务。

静态代理

所谓静态代理,就是代理类是由程序员自己编写的,在编译期就确定好了的

类图

代理模式包含如下角色:

  • Subject: 抽象主题角色
  • Proxy: 代理主题角色
  • RealSubject: 真实主题角色

静态代理模式

例子

抽象主题

public interface HelloSerivice {
    public void say();
}

真实主题

public class HelloSeriviceImpl implements HelloSerivice{

    @Override
    public void say() {
        System.out.println("hello world");
    }
}

代理主题

public class HelloSeriviceProxy implements HelloSerivice {

    private HelloSerivice target;

    public HelloSeriviceProxy(HelloSerivice target) {
        this.target = target;
    }

    @Override
    public void say() {
        System.out.println("记录日志");
        target.say();
        System.out.println("清理数据");
    }
}

测试

public class Test {
    public static void main(String[] args) {
        //目标对象
        HelloSerivice target = new HelloSeriviceImpl();
        //代理对象
        HelloSeriviceProxy proxy = new HelloSeriviceProxy(target);
        proxy.say();
    }
}/*output:
记录日志
hello world
清理数据
*/

静态代理的用途

控制真实对象的访问权限:通过代理对象控制对真实对象的使用权限。

避免创建大对象:通过使用一个代理小对象来代表一个真实的大对象,可以减少系统资源的消耗,对系统进行优化并提高运行速度。

增强真实对象的功能:这个比较简单,通过代理可以在调用真实对象的方法的前后增加额外功能。

动态代理

虽然静态代理模式很好用,但是静态代理还是存在一些局限性的,比如使用静态代理模式需要程序员手写很多代码,这个过程是比较浪费时间和精力的。一旦需要代理的类中方法比较多,或者需要同时代理多个对象的时候,这无疑会增加很大的复杂度。

动态代理中的代理类并不要求在编译期就确定,而是可以在运行期动态生成,从而实现对目标对象的代理功能。

Java中,实现动态代理有两种方式

  • JDK 动态代理:java.lang.reflect 包中的 Proxy 类和 InvocationHandler 接口提供了生成动态代理类的能力。

  • Cglib 动态代理:Cglib (Code Generation Library )是一个第三方代码生成类库,运行时在内存中动态生成一个子类对象从而实现对目标对象功能的扩展。

动态代理和静态代理的区别

  • 静态代理类:由程序员创建或由特定工具自动生成源代码,再对其编译。在程序运行前,代理类的 .class 文件就已经存在了。
  • 动态代理类:在程序运行时,运用反射机制动态创建而成。
  • 静态代理通常只代理一个类,动态代理是代理一个接口下的多个实现类。
  • 静态代理事先知道要代理的是什么,而动态代理不知道要代理什么东西,只有在运行时才知道。
  • 动态代理是实现 JDK 里的 InvocationHandler 接口的 invoke 方法,但注意的是代理的是接口,也就是你的业务类必须要实现接口,通过 Proxy 里的 newProxyInstance 得到代理对象。
  • 还有一种动态代理 CGLIB,代理的是类,不需要业务类继承接口,通过派生的子类来实现代理。通过在运行时,动态修改字节码达到修改类的目的。

JDK 动态代理和 Cglib 动态代理的区别

Cglib 与动态代理最大的区别就是:

  • 使用动态代理的对象必须实现一个或多个接口
  • 使用 cglib 代理的对象则无需实现接口,达到代理类无侵入。

Cglib 是一个强大的高性能的代码生成包,它可以在运行期扩展 Java 类与实现 Java 接口。它广泛的被许多 AOP 的框架使用,例如 Spring AOP 和 dynaop,为他们提供方法的 interception(拦截)。

Cglib 包的底层是通过使用一个小而快的字节码处理框架 ASM,来转换字节码并生成新的类。不鼓励直接使用 ASM,因为它需要你对 JVM 内部结构包括 class 文件的格式和指令集都很熟悉。

动态代理的用途

Java 的动态代理,在日常开发中可能并不经常使用,但是并不代表他不重要。Java 的动态代理的最主要的用途就是应用在各种框架中。因为使用动态代理可以很方便的运行期生成代理类,通过代理类可以做很多事情,比如 AOP,比如过滤器、拦截器等。

在我们平时使用的框架中,像 servlet 的 filter、包括 spring 提供的 aop 以及 struts2 的拦截器都使用了动态代理功能。我们日常看到的 mybatis 分页插件,以及日志拦截、事务拦截、权限拦截这些几乎全部由动态代理的身影。

JDK 动态代理

类图

JDK 动态代理

例子

需要你去实现一个 InvocationHandler 接口,并且调用 Proxy 的静态方法去产生代理类。

代理类

public class HelloInvocationHandler implements InvocationHandler {
    private HelloSerivice target;

    public HelloInvocationHandler(HelloSerivice target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //将代理对象生成字节码到F盘上,方便反编译出java文件查看,实际动态代理是不需要自己生成的
        addClassToDisk(proxy.getClass().getName(), ProxyClassImpl.class,"F:/$Proxy0.class");
        if ("say".equals(method.getName())) {
            System.out.println("记录日志");
            Object result = method.invoke(target, args);
            System.out.println("清理数据");
            return result;
        }
        return null;
    }

    public Object newProxyInstance() {
        return Proxy.newProxyInstance(
                DynamicTest.class.getClassLoader(), // 1. 类加载器
                new Class<?>[]{HelloSerivice.class}, // 2. 代理需要实现的接口,可以有多个
                new HelloInvocationHandler(new HelloSeriviceImpl()));// 3. 方法调用的实际处理者

    }
    /**
     * 用于生产代理对象的字节码,并将其保存到硬盘上
     * @param className
     * @param cl
     * @param path
     */
    private void addClassToDisk(String className, Class<?> cl, String path) {
        //用于生产代理对象的字节码
        byte[] classFile = ProxyGenerator.generateProxyClass(className, cl.getInterfaces());
        FileOutputStream out = null;
        try {
            out = new FileOutputStream(path);
            //将代理对象的class字节码写到硬盘上
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

测试

public class DynamicTest {
    public static void main(String[] args) {
        HelloSeriviceImpl h = new HelloSeriviceImpl();
        HelloInvocationHandler handler = new HelloInvocationHandler(h);
        HelloSerivice helloSerivice = (HelloSerivice) handler.newProxyInstance();
        System.out.println(helloSerivice.getClass().getName());
        helloSerivice.say();
    }
}/*output:
com.sun.proxy.$Proxy0
记录日志
hello world
清理数据
*/

从结果我们可以看到 proxyHandler.newProxyInstance(); 实际返回的是 com.sun.proxy.$Proxy0,我们把生成的 $Proxy0.class 文件,可以使用反编译工具查看,具体可查看JDK动态代理实现原理

  • 代理对象是在程序运行时产生的,而不是编译期;
  • 对代理对象的所有接口方法调用都会转发到 InvocationHandler.invoke() 方法,在 invoke() 方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;之后我们通过某种方式执行真正的方法体,示例中通过反射调用了 Hello 对象的相应方法,还可以通过RPC调用远程方法。

注意:对于从 Object 中继承的方法,JDK Proxy 会把 hashCode()、equals()、toString() 这三个非接口方法转发给 InvocationHandler,其余的 Object 方法则不会转发。

使用 Cglib 实现动态代理

CGLIB(Code Generation Library)是一个基于 ASM 的字节码生成库,它允许我们在运行时对字节码进行修改和动态生成。官方示例

引入 jar 包

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.ow2.asm/asm -->
<dependency>
    <groupId>org.ow2.asm</groupId>
    <artifactId>asm</artifactId>
    <version>6.2</version>
</dependency>

实现 MethodInterceptor 接口

public class CglibProxy implements MethodInterceptor{
    private Enhancer enhancer = new Enhancer();

    /**
     *
     * @param o 是被代理对象
     * @param method 调用方法的Method对象
     * @param args 方法参数
     * @param methodProxy
     * @return cglib生成用来代替Method对象的一个对象,使用MethodProxy比调用JDK自身的Method直接执行方法效率会有提升
     * @throws Throwable
     */
    @Override
    public Object intercept(Object o, Method method, Object[] args,
            MethodProxy methodProxy) throws Throwable {
        System.out.println("before " + methodProxy.getSuperName());
        System.out.println(method.getName());
        Object o1 = methodProxy.invokeSuper(o, args);
        //Object o2 = method.invoke(o, args); 使用这种方式会发生死循环,因为方法会被拦截
        System.out.println("after " + methodProxy.getSuperName());
        return o1;
    }

    public  Object newProxyInstance(Class<?> c) {
        //设置产生的代理对象的父类。
        enhancer.setSuperclass(c);
        //设置CallBack接口的实例
        enhancer.setCallback(this);
        //使用默认无参数的构造函数创建目标对象
        return enhancer.create();
    }
}

被代理对象和测试类

class Do{
    public int doSomething(int num){
        System.out.println("方法执行中。。。。。。");
        return num;
    }
}
public class CglibDemo {
    public static void main(String[] args) {
        CglibProxy cglibProxy = new CglibProxy();
        Do o = (Do)cglibProxy.newProxyInstance(Do.class);
        System.out.println(o.doSomething(5));
    }
}/*output:
before CGLIB$doSomething$0
doSomething
方法执行中。。。。。。
after CGLIB$doSomething$0
5
*/

上述代码中,我们通过 CGLIB 的 Enhancer 来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用 create() 方法得到代理对象,对这个对象所有非 final 方法的调用都会转发给 MethodInterceptor.intercept() 方法,在 intercept() 方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用 MethodProxy.invokeSuper() 方法,我们将调用转发给原始对象,具体到本例,就是 Do 的具体方法。CGLIG 中 MethodInterceptor 的作用跟 JDK 代理中的 InvocationHandler 很类似,都是方法调用的中转站。

注意:对于从 Object 中继承的方法,CGLIB 代理也会进行代理,如 hashCode()、equals()、toString() 等,但是 getClass()、wait() 等方法不会,因为它是 final 方法,CGLIB 无法代理

更多 CGLIB 内容:http://www.cnblogs.com/shijiaqi1066/p/3429691.html

正向代理和反向代理

正向代理

正向代理(forward proxy):是一个位于客户端和目标服务器之间的服务器(代理服务器),为了从目标服务器取得内容,客户端向代理服务器发送一个请求并指定目标,然后代理服务器向目标服务器转交请求并将获得的内容返回给客户端。客户端必须要进行一些特别的配置才能使用正向代理。一般情况下,如果没有特别说明,代理技术默认是指正向代理技术。

正向代理

反向代理

反向代理(reverse proxy):是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

对于常用的场景,就是我们在Web开发中用到的负载均衡服务器,客户端发送请求到负载均衡服务器上,负载均衡服务器再把请求转发给一台真正的服务器来执行,再把执行结果返回给客户端。

反向代理

前面我们说过,通过正向代理服务器访问目标服务器,目标服务器是不知道真正的客户端是谁的,甚至不知道访问自己的是一个代理。而通过反向代理服务器访问目标服务器时,客户端是不知道真正的目标服务器是谁的,甚至不知道自己访问的是一个代理。这也是正向代理和反向代理的区别

反向代理的用途

  • 隐藏服务器真实 IP:使用反向代理,可以对客户端隐藏服务器的IP地址。
  • 负载均衡:反向代理服务器可以做负载均衡,根据所有真实服务器的负载情况,将客户端请求分发到不同的真实服务器上。
  • 提高访问速度:反向代理服务器可以对于静态内容及短时间内有大量访问请求的动态内容提供缓存服务,提高访问速度。
  • 提供安全保障:反向代理服务器可以作为应用层防火墙,为网站提供对基于 Web 的攻击行为(例如DoS/DDoS)的防护,更容易排查恶意软件等。还可以为后端服务器统一提供加密和 SSL 加速(如 SSL 终端代理),提供 HTTP 访问认证等。

参考

http://design-patterns.readthedocs.io/zh_CN/latest/structural_patterns/proxy.html
https://mp.weixin.qq.com/s/kycTUCtgmjtdAA4f_KpFFg
https://www.cnblogs.com/CarpenterLee/p/8241042.html
https://brightloong.github.io/2017/03/17/Java静态代理-动态代理笔记/



转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 bin07280@qq.com

文章标题:《Head-First-设计模式》笔记15-代理模式

文章字数:3.3k

本文作者:Bin

发布时间:2018-07-24, 18:59:16

最后更新:2019-12-04, 14:30:02

原始链接:http://coolview.github.io/2018/07/24/Head-First-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E3%80%8AHead-First-%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E3%80%8B%E7%AC%94%E8%AE%B015-%E4%BB%A3%E7%90%86%E6%A8%A1%E5%BC%8F/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录