静态代理和动态代理

272 阅读7分钟

1、代理模式

代理模式是常用的结构性设计模式之一,当无法直接访问某个对象或者访问某个对象存在困难时,可以通过一个代理对象来间接访问。代理模式中通过一个代理对象作为客户端对象和目标对象的中介,它去掉客户端不能看到的内容和服务或者增添客户需要的额外的新服务。

代理模式的结构:

image.png 包含三个角色:

  • (1)Subject 抽象主题角色 作为真实主题和代理主题的共同接口
  • (2)Proxy 代理主题角色 它包含了对真实主题的应用,从而可以在任何时候操作真实主题对象。它和真实主题实现了相同的接口,可以在任何时候替代真实主题。
  • (3)RealSubject 真实主题角色 它包含了真实的业务操作。

代理模式的优点

将调用者和被调用者解耦; 客户端可以通过增加和更换代理类来增加新功能而无需修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性;

代理模式的缺点

实现代理模式需要额外的操作,而且有些代理模式的实现过程较为复杂,例如远程代理; 由于客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理变慢,例如保护代理。

2、静态代理

程序运行前就已经存在代理类的字节码文件,代理类和目标对象的关系在运行前就确定了。(编译期确定)

静态代理的缺点

  • 代理对象的一个接口只能代理一个目标对象,如果要代理的对象很多,势必要为每个对象创建代理,在程序规模大时静态代理类过多造成代码混乱;
  • 如果多个代理类之间实现的功能差不多,就会导致大量代码重复,难以维护
  • 如果公共接口新增一个方法,那除了目标对象要实现这个方法外,代理对象也需要实现这个方法,增加了代码维护的复杂度。

3、动态代理

动态代理是在程序运行时,通过反射获取被代理类的字节码内容来创建代理类。(运行期确定)

Java中实现动态代理的方法有两种:JDK Proxy和CGLIB

JDK Proxy

JDK Proxy是JDK提供的动态代理功能,是通过反射实现的。

JDK Proxy的实现依赖两个东西:Proxy对象和InvocationHandler接口。Proxy用来动态创建字节码,InvocationHandler接口是代理逻辑需要实现的接口,使用方法如下:

(1)实现代理逻辑

public class MyHandler implements InvocationHandler{
    // 标识被代理类的实例对象
    private Object delegate;   
    // 构造器注入被代理对象
    public MyHandler(Object delegate){
       this.delegate = delegate;
    }
    
    // 重写invoke方法
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("被代理方法调用前的附加代码执行~ ");
        // 真实的被代理方法调用
        method.invoke(delegate, args);
        System.out.println("被代理方法调用后的附加代码执行~ ");
    } 
}

(2)使用代理对象

public class MainTest{
    public static void main(String[] args) {    
        // 创建被代理对象
        ImplA A = new ClassA();
        // 创建处理器类实现
        InvocationHandler myHandler = new MyHandler(A);
        // 重点! 生成代理类, 其中proxyA就是A的代理类了
        ImplA proxyA = (ImplA)Proxy.newProxyInstance(A.getClass().getClassLoader(), A.getClass().getInterfaces(), myHandler);
        // 调用代理类的代理的methoda方法, 在此处就会去调用上述myHandler的invoke方法区执行,至于为什么,先留着疑问,下面会说清楚~
        proxyA.methoda();
    }
}

原理

创建代理对象的几个步骤:

  • 生成代理类的字节码文件;
  • 加载字节码文件到jvm中,生成Class对象;
  • 通过反射机制调用代理对象的构造方法,并创建代理类对象。
ImplA proxyA = (ImplA)Proxy.newProxyInstance(A.getClass().getClassLoader(), A.getClass().getInterfaces(), myHandler);

Proxy.newProxyInstance就是完成了上面的三个步骤,最后返回的proxyA就是代理对象。这个方法有三个入参,分别是目标对象的类加载器、目标对象的接口、代理工作的具体实现。

代理类使用目标类的类加载器进行加载。

自动生成的代理类结构如下:继承了Proxy类,实现了目标类一样的接口。因为Java是单继承的语言,所以JDK Proxy只能代理接口,不能代理类。 Proxy类包含了InvocationHandler实现类的引用,在通过代理类调方法时,调用的是invocationHandler的invoke()方法:super.h.invoke(this, m3, (Object[])null);

final class $Proxy0 extends Proxy implements ImplA {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
    
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.test.ImplA").getMethod("methoda");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
           // ..
        }
    }
   
    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }
    // 需要被加强的方法methoda
    public final void methoda() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final boolean equals(Object var1) throws  {
        // 省略部分代码。。。
        return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
    }
    public final String toString() throws  {
        // 省略部分代码。。。
        return (String)super.h.invoke(this, m2, (Object[])null);
    }
    public final int hashCode() throws  {
        // 省略部分代码。。。
        return (Integer)super.h.invoke(this, m0, (Object[])null);
    }
}

使用场景

  • Spring AOP
  • RPC 框架实现 远程过程调用,RPC 使得调用远程方法和调用本地方法一样,这是怎么搞的呢?服务方对外放出服务的接口 api,调用方拿到接口 api,通过动态代理的方式生成一个代理类,代理类的处理类的 invoke 方法可以通过 websocket 连接远程服务器调用对应的远程接口; 这样我们再用代理对象进行调用对应方法时时,就像调用本地方法一样了
  • mybatis

JDK Proxy的优点

灵活、解耦、更关注于业务

JDK Proxy的缺点

只能代理接口,不能代理类。代理类可以选择CGLIB

CGLIB

CGLIB是依靠ASM操作字节码实现的,通过继承方式实现动态代理。

CGLIB实现动态代理依赖两个东西:Enhancer类和MethodInterceptor接口。

Enhancer类是CGLIB的字节码增强器,用来生成代理类的字节码并创建代理类的对象。

MethodInterceptor接口是代理内容的具体实现,覆写interceptor()实现。

(1)实现代理逻辑

// CGLIB动态代理
// 1. 首先实现一个MethodInterceptor,方法调用会被转发到该类的intercept()方法。
class MyMethodInterceptor implements MethodInterceptor{
  ...
	@Override
	public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
		logger.info("You said: " + Arrays.toString(args));
		return proxy.invokeSuper(obj, args);
	}
}

(2)使用代理对象

// 2. 然后在需要使用HelloConcrete的时候,通过CGLIB动态代理获取代理对象。
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HelloConcrete.class);
enhancer.setCallback(new MyMethodInterceptor());

HelloConcrete hello = (HelloConcrete)enhancer.create();
System.out.println(hello.sayHello("I love you!"));

原理

先通过enhancer设置父类为目标类,设置callback为实现的methodInterceptor,然后生成字节码,并将字节码加载到jvm,并在方法区创建Class对象,并通过反射调用构造器创建代理类的实例对象。然后通过调用代理类对象的方法即可。

代理类的方法里,发现如果代理了目标类的方法,就把方法转发给methodInterceptor.intercept()方法,intercept方法里会调用目标对象的方法;如果发现没有代理目标类的方法,则直接执行目标类的方法。

自动生成的代理类如下:

// CGLIB代理类具体实现
public class HelloConcrete$$EnhancerByCGLIB$$e3734e52
  extends HelloConcrete
  implements Factory
{
  ...
  private MethodInterceptor CGLIB$CALLBACK_0; // ~~
  ...
  
  public final String sayHello(String paramString)
  {
    ...
    MethodInterceptor tmp17_14 = CGLIB$CALLBACK_0;
    if (tmp17_14 != null) {
	  // 将请求转发给MethodInterceptor.intercept()方法。
      return (String)tmp17_14.intercept(this, 
              CGLIB$sayHello$0$Method, 
              new Object[] { paramString }, 
              CGLIB$sayHello$0$Proxy);
    }
    return super.sayHello(paramString);
  }
  ...
}

4、字节码增强

常用的字节码增强技术有ASM和Javassist。这两种方法都可以创建和修改字节码。

ASM是在字节码层面操作字节码。Javassist是在Java源码层面进行操作,更友好。

这两种方法只能在字节码加载到虚拟机之前进行创建和修改,对于已经加载到虚拟机之后的字节码进行重写就不支持了。这种情况下,可以使用Instrument技术。 主要通过JVMTI(JVM Tool Interface)实现。JVMTI在jvm各个生命周期过程中设了钩子,当某个事件发生时,可以调用钩子方法进行特定的处理。比如GC事件、类加载事件、对象创建销毁事件、方法调用返回事件等等。 Agent就是利用JVMTI进行字节码增强的。

参考:xie.infoq.cn/article/811… www.cnblogs.com/carpenterle…