Java代理

摩森特沃 2021年04月25日 423次浏览

转载声明:本文引用自【从头捋了一遍 Java 代理机制,收获颇丰】,如有侵权,请联系删除

概述

什么是代理

代理,又称代理模式,是设计模式中非常重要的一种,也是对程序设计OCP原则(开闭原则,即对扩展开放,对修改关闭)的重要体现,下面以一个通俗的例子来说明代理模式。

在网上购票还没有那么发达的年代,如果想要买火车票,往往需要到火车站去排队购票,但是一个城市的火车站往往只有那么一两个,而城市却非常大,这样的话那些距离火车站的非常远的人势必就很不方便,而且大量的人聚集到一个地方去买票,给火车站也造成了很大的压力,甚至出现了有人为了买一张票需要彻夜排队的情况,可谓是费时又费力。于是聪明的人们发明了代售点这么个东西,现在大家再也不用去火车站排队买票了,直接在代售点买就行了(当然现实往往比理想的情况要复杂)。这就是代理的思想,在这个例子中,火车票代售点和火车站售票点对大家来所其作用是一样的,但是代售点并没有运营铁路运输,他只是火车站售票点的一个代理,本质上来说是它完成了帮助旅客从火车站买票的任务。

从上面的示例中可以看出,代理的关键之处在于:使用代理对象来代替对真实对象的访问,这样就可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。通俗来说,代理模式的主要作用是扩展目标对象的功能,比如说在目标对象的某个方法执行前后你可以增加一些额外的操作,并且不用修改这个方法的原有代码

代理模式中的角色

  • Real Subject:真实类,也就是被代理类、委托类。用来真正完成业务服务功能
  • Proxy:代理类。将自身的请求用 Real Subject 对应的功能来实现,代理类对象并不真正的去实现其业务功能
  • Subject:定义 RealSubject 和 Proxy 角色都应该实现的接口

xJDcne

结合上面的举例,现在应该能理解了为什么委托类和代理类都需要实现相同的接口?
那是为了保持行为的一致性,在访问者看来两者之间就没有区别。这样,通过代理类这个中间层,很好地隐藏和保护了委托类对象,能有效屏蔽外界对委托类对象的直接访问。同时,也可以在代理类上加上额外的操作,这就实现了委托类的功能增强。

代理的分类

根据被代理类加载的时机的不同,可以将代理分为静态代理和动态代理。如果我们在代码编译时就确定了被代理的类是哪一个,那么就可以直接使用静态代理;如果不能确定,那么可以使用类的动态加载机制,在代码运行期间加载被代理的类这就是动态代理,比如RPC框架和Spring AOP机制

  • 静态代理:由程序员创建或特定工具自动生成源代码,也就是在编译时就已经将接口,被代理类,代理类等确定下来,在程序运行之前,代理类的.class文件就已经生成
  • 动态代理: 代理类在程序运行时创建的代理方式被成为动态代理

常规的编码方式

在介绍完代理模式的理论之后,我们先回顾一下常规编码方式,用来和代理模式形成对比。在常规的编码方式中,所有 interface 类型的变量总是通过向上转型并指向某个实例的,举例说明:

  • 首先,定义一个接口
public interface SmsService {
    String send(String message);
}
  • 然后编写其实现类
public class SmsServicseImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
  • 最后创建该实现类的实例,转型为接口并调用
SmsService s = new SmsServicseImpl();
s.send("Java");

上述这种方式就是我们通常编写代码的方式。而代理模式和这种方式有很大的区别,且看下文。

静态代理

静态代理的实现步骤

从 JVM 层面来说,静态代理在编译时就将接口、委托类、代理类这些都变成了一个个实际的 .class 文件。

实现步骤及代码示例

  • 定义发送短信的接口
public interface SmsService {
    String send(String message);
}
  • 创建一个委托类(Real Subject)实现这个接口
public class SmsServiceImpl implements SmsService {
    public String send(String message) {
        System.out.println("send message:" + message);
        return message;
    }
}
  • 创建一个代理类(Proxy)同样实现这个接口
  • 将委托类 Real Subject 注入进代理类 Proxy,在代理类的方法中调用 Real Subject 中的对应方法。这样的话,我们就可以通过代理类屏蔽对目标对象的访问,并且可以在目标方法执行前后做一些自己想做的事情。
public class SmsProxy implements SmsService {
 
    // 将委托类注入进代理类
    private final SmsService smsService;

    public SmsProxy(SmsService smsService) {
        this.smsService = smsService;
    }

    @Override
    public String send(String message) {
        // 调用委托类方法之前,我们可以添加自己的操作
        System.out.println("before method send()");
        // 调用委托类方法
        smsService.send(message); 
        // 调用委托类方法之后,我们同样可以添加自己的操作
        System.out.println("after method send()");
        return null;
    }
}

那么,如何使用这个被增强的 send 方法呢?

public class Main {
    public static void main(String[] args) {
        SmsService smsService = new SmsServiceImpl();
        SmsProxy smsProxy = new SmsProxy(smsService);
        smsProxy.send("Java");
    }
}

运行上述代码之后,控制台打印出:

before method send()
send message:java
after method send()

从输出结果可以看出,我们已经增强了委托类 SmsServiceImpl 的 send() 方法。

从上述代码我们能看出来,静态代理存在一定的弊端。假如说我们现在新增了一个委托类实现了 SmsService 接口,如果我们想要对这个委托类进行增强,就需要重新写一个代理类,然后注入这个新的委托类,非常不灵活。实际应用场景非常非常少,日常开发几乎看不到使用静态代理的场景。那么能不能想办法将代理类做成通用的呢?为此,动态代理应用而生。

动态代理的基础

Java 字节码生成框架

Java 字节码生成框架与动态代理机制是息息相关的,在上文反射中我们提到,一个 Class 类对应一个 .class 字节码文件,也就说字节码文件中存储了一个类的全部信息。字节码其实是二进制文件,内容是只有 JVM 能够识别的机器码。

解析过程这样的:JVM 读取 .class 字节码文件,取出二进制数据,加载到内存中,解析字节码文件内的信息,生成对应的 Class 类对象:

kwF33H

显然,上述这个过程是在编译期就发生的。

那么,由于JVM 是通过 .class 字节码文件(也就是二进制信息)加载类的,如果我们在运行期遵循 Java 编译系统组织 .class 字节码文件的格式和结构,生成相应的二进制数据,然后再把这个二进制数据加载转换成对应的类。这样,我们就完成了在运行时动态的创建一个类。这个思想其实也就是动态代理的思想。

5isJgL

在运行时期按照 JVM 规范对 .class 字节码文件的组织规则,生成对应的二进制数据。当前有很多开源框架可以完成这个功能,如

  • ASM
  • CGLIB
  • Javassist
  • ......

需要注意的是,「CGLIB 是基于 ASM 的」。这里简单对比一下 ASM 和 Javassist:

  • Javassist 源代码级 API 比 ASM 中实际的字节码操作更容易使用
  • Javassist 在复杂的字节码级操作上提供了更高级别的抽象层。Javassist 源代码级 API 只需要很少的字节码知识,甚至不需要任何实际字节码知识,因此实现起来更容易、更快。
  • Javassist 使用反射机制,这使得它比 ASM 慢

总的来说 ASM 比 Javassist 快得多,并且提供了更好的性能,但是 Javassist 相对来说更容易使用,两者各有千秋。

以 Javassist 为例,我们来看看这些框架在运行时生成 .class 字节码文件的强大能力。正常来说,我们创建一个类的代码是这样的:

package com.most;

public class Programmer {
    public void code() {
        System.out.println("I'm a Java Programmer!");
    }
}

下面通过 Javassist 创建和上面一模一样的 NewProgrammer 类的字节码:

package com.most;

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;

public class MyGenerator {
    public static void main(String[] args) throws Exception {
        ClassPool pool = ClassPool.getDefault();
        // 创建NewProgrammer类
        CtClass aClass = pool.makeClass("com.most.NewProgrammer");
        // 定义方法
        CtMethod method = CtNewMethod.make("public void code(){}", aClass);
        // 插入方法代码
        method.insertBefore("System.out.println(\"I'm a Java NewProgrammer!\");");
        aClass.addMethod(method);
        aClass.writeFile();
    }

通过反编译工具可以看到以下代码

yyI4wF

动态代理

了解了 Java 字节码生成框架,可以开始学习动态代理(Dynamic Proxy)了。回顾一下静态代理,我们把静态代理的执行过程抽象为下图:

ixQSxB

可以看见,代理类无非是在调用委托类方法的前后增加了一些操作。委托类的不同,也就导致代理类的不同。

那么为了做一个通用性的代理类出来,我们把调用委托类方法的这个动作抽取出来,把它封装成一个通用性的处理类,于是就有了动态代理中的 InvocationHandler 角色(处理类)。

于是,在代理类和委托类之间就多了一个处理类的角色,这个角色主要是「对代理类调用委托类方法的这个动作进行统一的调用」,也就是由 InvocationHandler 来统一处理代理类调用委托类方法这个操作。看下图:

OAEYv3

从 JVM 角度来说,动态代理是在运行时动态生成 .class 字节码文件 ,并加载到 JVM 中的。这个我们在 Java 字节码生成框架中已经提到过。

虽然动态代理在我们日常开发中使用的相对较少,但是在框架中的几乎是必用的一门技术。学会了动态代理之后,对于我们理解和学习各种框架的原理也非常有帮助,Spring AOP、RPC 等框架的实现都依赖了动态代理。

就 Java 来说,动态代理的实现方式有很多种,比如:

  • JDK 动态代理
  • CGLIB 动态代理
  • Javassit 动态代理
  • ......
    下面详细讲解这三种动态代理机制

JDK 动态代理

使用步骤

先来看下 JDK 动态代理机制的使用步骤:

  1. 定义一个接口(Subject)
  2. 创建一个委托类(Real Subject)实现这个接口
  3. 创建一个处理类并实现 InvocationHandler 接口,重写其 invoke 方法(在 invoke 方法中利用反射机制调用委托类的方法,并自定义一些处理逻辑),并将委托类注入处理类
public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

该方法有下面三个参数:

  • Object proxy:代理类对象(见第 4 步)
  • Method method:还记得我们在上篇文章反射中讲到的 Method.invoke 吗?就是这个,我们可以通过它来调用委托类的方法(反射)
  • Object[] args:传给被委托类方法的参数列表
  1. 创建代理对象(Proxy):通过 Proxy.newProxyInstance() 创建委托类对象的代理对象
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)

该方法需要三个参数:

  • ClassLoader loader:类加载器 ClassLoader
  • Class<?>[] interfaces:委托类实现的接口数组,至少需要传入一个接口进去
  • InvocationHandler h:调用的 InvocationHandler 实例处理接口方法(也就是第 3 步中创建的类的实例)

也就是说:我们在通过 Proxy 类的 newProxyInstance() 创建的代理对象在调用方法的时候,实际会调用到实现了 InvocationHandler 接口的处理类的 invoke()方法,可以在 invoke() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

代码示例

  1. 定义一个接口(Subject)
package com.most.proxy.jdk;

public interface SmsService {
    String send(String message);
}
  1. 创建一个委托类(Real Subject)实现这个接口
package com.most.proxy.jdk;

public class SmsServiceImpl implements SmsService {
    @Override
    public String send(String message) {
        System.out.println("send message: " + message);
        return message;
    }
}
  1. 创建一个处理类并实现 InvocationHandler 接口,重写其 invoke 方法,并将委托类注入处理类
package com.most.proxy.jdk;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler {

    // 将委托类注入处理类(这里我们用 Object 代替,方便扩展)
    private final Object target;

    public MyInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        Object result = method.invoke(target, args);
        // 调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        // 返回方法调用的结果
        return result;
    }
}
  1. 定义一个创建代理对象(Proxy)的工厂类:通过 Proxy.newProxyInstance() 创建委托类对象的代理对象
package com.most.proxy.jdk;

import java.lang.reflect.Proxy;

public class JDKProxyFactory {
    @SuppressWarnings({"all"})
    public static <T> T getProxyInstance(SmsService target, Class<T> tClass) {
        return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new MyInvocationHandler(target));
    }
}
  1. 实际使用
package com.most.proxy.jdk;

public class Main {
    public static void main(String[] args) {
	// 输出生成的代理类的class文件,输出的默认路径为:项目根路径/com/sun/proxy/
	System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
        SmsService smsService = new SmsServiceImpl();
        SmsService instance = JDKProxyFactory.getProxyInstance(smsService, SmsService.class);
        instance.send("Java");
    }
}
  1. 运行结果
before method send
send message: Java
after method send

CGLIB 动态代理

使用步骤

JDK 动态代理有一个最致命的问题是它只能代理实现了某个接口的实现类,并且代理类也只能代理接口中实现的方法,要是实现类中有自己私有的方法,而接口中没有的话,该方法不能进行代理调用。

为了解决这个问题,我们可以用 CGLIB 动态代理机制。

上文也提到过,CGLIB(Code Generation Library)是一个基于 ASM 的 Java 字节码生成框架,它允许我们在运行时对字节码进行修改和动态生成。原理就是通过字节码技术生成一个子类,并在子类中拦截父类方法的调用,织入额外的业务逻辑。关键词大家注意到没有,拦截!CGLIB 引入一个新的角色就是方法拦截器 MethodInterceptor。和 JDK 中的处理类 InvocationHandler 差不多,也是用来实现方法的统一调用的。看下图:

0D41IW

另外由于 CGLIB 采用继承的方式,所以被代理的类不能被 final 修饰。

很多知名的开源框架都使用到了 CGLIB, 例如:Spring 中的 AOP 模块中:如果目标对象实现了接口,则默认采用 JDK 动态代理,否则采用 CGLIB 动态代理。

来看 CGLIB 动态代理的使用步骤:

  1. 首先创建一个委托类(Real Subject)

  2. 创建一个方法拦截器实现接口 MethodInterceptor,并重写 intercept 方法。intercept 用于拦截并增强委托类的方法(和 JDK 动态代理 InvocationHandler 中的 invoke 方法类似)

Object intercept(Object var1, Method var2, Object[] var3, MethodProxy var4) throws Throwable;

该方法拥有四个参数:

  • Object var1:委托类对象
  • Method var2:被拦截的方法(委托类中需要增强的方法)
  • Object[] var3:方法入参
  • MethodProxy var4:用于调用委托类的原始方法(底层也是通过反射机制,不过不是 Method.invoke 了,而是使用 MethodProxy.invokeSuper 方法)
  1. 创建代理对象(Proxy):通过 Enhancer.create() 创建委托类对象的代理对象

我们在通过 Enhancer 类的 create() 创建的代理对象在调用方法的时候,实际会调用到实现了 MethodInterceptor 接口的处理类的 intercept()方法,可以在 intercept() 方法中自定义处理逻辑,比如在方法执行前后做什么事情。

可以发现,CGLIB 动态代理机制和 JDK 动态代理机制的步骤差不多,CGLIB 动态代理的核心是方法拦截器 MethodInterceptor 和 Enhancer,而 JDK 动态代理的核心是处理类 InvocationHandler 和 Proxy

代码示例

不同于 JDK 动态代理不需要额外的依赖。CGLIB 是一个开源项目,如果你要使用它的话,需要手动添加相关依赖

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

除了使用cglib的依赖之外,spring框架也有一套自己的实现,因此还可以直接使用spring框架中的cglib

  1. 首先创建一个委托类(Real Subject)
package com.most.proxy.cglib;

public class SmsServiceImpl {
    public String send(String message) {
        System.out.println("send message: " + message);
        return message;
    }
}
  1. 创建一个方法拦截器实现接口 MethodInterceptor,并重写 intercept 方法
package com.most.proxy.cglib;

import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        // 调用方法之前,我们可以添加自己的操作
        System.out.println("before method " + method.getName());
        // 通过反射调用委托类的方法
        Object object = methodProxy.invokeSuper(o, args);
        // 调用方法之后,我们同样可以添加自己的操作
        System.out.println("after method " + method.getName());
        // 返回执行结果
        return object;
    }
}
  1. 创建代理对象(Proxy):通过 Enhancer.create() 创建委托类对象的代理对象
package com.most.proxy.cglib;

import org.springframework.cglib.proxy.Enhancer;

public class CGLIBProxyFactory {

    @SuppressWarnings("unchecked")
    public static <T> T getProxy(Class<T> tClass) {
        Enhancer enhancer = new Enhancer();
        enhancer.setClassLoader(tClass.getClassLoader());
        // 此处可以看出CGLIB的代理实现是基于继承关系的
        enhancer.setSuperclass(tClass);
        enhancer.setCallback(new MyMethodInterceptor());
        return (T) enhancer.create();
    }
}
  1. 实际使用
package com.most.proxy.cglib;

public class Main {
    public static void main(String[] args) {
	// 输出生成的代理类的class文件,输出的路径为:指定路径
	System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\Project\\Idea\\most\\com");
        SmsServiceImpl proxy = CGLIBProxyFactory.getProxy(SmsServiceImpl.class);
        proxy.send("Java");
    }
}

  1. 输出结果
before method send
send message: Java
after method send

JDK 动态代理和 CGLIB 动态代理对比

  • JDK 动态代理是基于实现了接口的委托类,通过接口实现代理;而 CGLIB 动态代理是基于继承了委托类的子类,通过子类实现代理

  • JDK 动态代理只能代理实现了接口的类,且只能增强接口中现有的方法;而 CGLIB 可以代理未实现任何接口的类

  • 就二者的效率来说,大部分情况都是 JDK 动态代理的效率更高,随着 JDK 版本的升级,这个优势更加明显

Javassist 动态代理

  • Javassist 和 CGLIB 一样,作为一个 Java 字节码生成框架,Javassist 天生就拥有在运行时动态创建一个类的能力,实现动态代理自然不在话下。Dubbo 就是默认使用 Javassit 来进行动态代理的

动态代理和静态代理的对比

  • 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的
  • JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 .class 字节码文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的

总结

总结一下静态代理和动态代理中的角色

  • 静态代理
  • Subject:公共接口
  • Real Subject:委托类
  • Proxy:代理类
  • JDK 动态代理
  • Subject:公共接口
  • Real Subject:委托类
  • Proxy:代理类
  • InvocationHandler:处理类,统一调用方法

可使用以下语句让生成的代理类class文件输出
System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");,输出路径为:项目根路径/com/sun/proxy/

  • CGLIB 动态代理
  • Subject:公共接口
  • Real Subject:委托类
  • Proxy:代理类
  • MethodInterceptor:方法拦截器,统一调用方法

可使用以下语句让生成的代理类class文件输出
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "E:\\Project\\Idea\\most\\com");,输出路径为:指定路径

引用参考