Java 代理模式介绍

admin 2025年2月22日00:01:03评论11 views字数 14518阅读48分23秒阅读模式
  • 静态代理

  • 动态代理

    • JDK 原生动态代理

    • CGLIB 动态代理

  • Ending......

Java 代理模式的主要作用是为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个对象不想或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用。

代理模式的思想是为了提供额外的处理或者不同的操作而在实际对象与调用者之间插入一个代理对象。这些额外的操作通常需要与实际对象进行通信。

举个例子,假设有一组对象都实现了同一个接口,实现同样的方法,但这组对象中有一部分对象需要有单独的方法,传统的笨办法是在每一个应用端都加上这个单独的方法,但是代码重用性低,耦合性高。如果用代理的方法则很好的解决了这个问题。

通过 Java 代理机制,可以在目标对象实现的基础上,增强额外的功能操作,扩展目标对象的功能。这里使用到编程中的一个思想:不要随意去修改别人已经写好的代码逻辑或者方法,如果需改修改,可以通过代理的方式来扩展该方法。

在 Java 代理模式中,可以有 3 种角色:

  • Subject:抽象主题角色,定义代理类和委派类的公共对外方法,也是代理类代理委派类的方法。
  • RealSubject:真实主题角色,这是真正实现业务逻辑的类,也可以将其称为委派类。
  • Proxy:代理主题角色,用来代理和封装委派类,也可以将其称为代理类。

根据字节码的创建时机,Java 对象代理模式可以分为静态代理和动态代理:

  • 静态代理:在程序运行前就已经存在代理类的字节码文件,代理类和委派类的关系在运行前就确定了。
  • 动态代理:动态代理的源码在程序运行期间由 JVM 根据反射等机制来动态生成,所以在运行前并不存在代理类的字节码文件。

静态代理

下面,我们编写几个简单的 Demo 来说明 Java 的静态代理模式。

  • 首先编写一个接口 userInterface.class,该接口声明了至少一个方法。
public interface userInterface {
    public void select();
    public void update();
}
  • 编写一个 userInterface 接口的实现类 userImpl.class,该类的角色为 RealSubject,作为后续代理的委派类。
public class userImpl implements userInterface {
    public void select() {
        System.out.println("User select action");
    }
    public void update() {
        System.out.println("User update action");
    }
}

下面,我们将通过 Java 静态代理,对 userImpl 类进行功能增强,在调用 select()update() 方法前执行一些日志记录操作。

  • 编写一个代理类,并且代理类也需要实现 userInterface 接口。该代理类在后续操作中受到委派类的代理委托。
import java.util.Date;

public class userImplProxy implements userInterface {
    private userImpl target;

    public userImplProxy(userImpl target) {
        this.target = target;
    }
    public void select() {
        before();
        target.select();
        after();
    }
    public void update() {
        before();
        target.update();
        after();
    }
    private void before() {
        System.out.println(String.format("Log start time for action [%s] "new Date()));
    }
    private void after() {
        System.out.println(String.format("Log end time for action [%s] "new Date()));
    }
}
  • 最后,我们可以编写一个客户端测试类 appClient.class,对上述代理过程进行测试。
public class appClient {
    public static void main(String[] args) {
        userImpl targetObj = new userImpl();
        userImplProxy proxyObj = new userImplProxy(targetObj);

        proxyObj.select();
        proxyObj.update();
    }
}

运行 appClient.class 后将输出以下内容:

Log start time for action [Sat Apr 02 14:38:27 CST 2022
User select action
Log end time for action [Sat Apr 02 14:38:27 CST 2022
Log start time for action [Sat Apr 02 14:38:27 CST 2022
User update action
Log end time for action [Sat Apr 02 14:38:27 CST 2022

可见,通过 Java 静态代理,我们达到了增强对象功能的目的,并且没有修改实际目标对象的代码,这是静态代理的一个优点。但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来。

当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,因此有两种解决方式。一种是只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大;另一种是新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类。此外,当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。

既然编写代理类会有很多问题,那么是否有方法不编写代理类,而是让程序在执行时自动生成代理类呢?这就引出了 Java 动态代理。

动态代理

《Java 类加载机制》 这篇文章详细地说明了 Java 类从 “被加载到 JVM 内存” 到 “从 JVM 内存中卸载” 所经历的 7 个阶段。

Java 代理模式介绍
img

其中 Java 类的加载过程包括了前 5 个阶段,Java 类的动态生成与第1个阶段 “加载” 有关。在加载阶段,Java 虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取其定义的二进制字节流。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  3. 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入口。

由于 JVM 规范对这3点要求并不具体,所以实际的实现是非常灵活的。例如关于第1点,获取类的二进制字节流(Class 字节码)就有很多途径:

  • 从 ZIP 包获取,这是 JAR、EAR、WAR 等格式的基础。
  • 从网络中获取,典型的应用是 Applet。
  • 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass() 来为特定接口生成形式为 *$Proxy 的代理类的二进制字节流。
  • 由其它文件生成,典型应用是 JSP,即由 JSP 文件生成对应的 Class 类。
  • 从数据库中获取。
  • ......

因此,Java 动态代理就是想办法根据接口或目标对象,计算出代理类的字节码,然后再加载到 JVM 中使用。为了让生成的代理类与目标对象保持一致性,一般会通过 “实现接口” 和 “继承类” 两种来生成代理类的 Class 字节码:

  1. 通过实现接口的方式:JDK 原生动态代理
  2. 通过继承类的方式:CGLIB 动态代理

JDK 原生动态代理

JDK 原生动态代理利用拦截器和反射机制来实现,只需要 JDK 环境就可以实现对下个代理,无需第三方库的支持。其主要设计两个核心类:

  • java.lang.reflect.Proxy
  • java.lang.reflect.InvocationHandler

java.lang.reflect.Proxy 类为 JDK 原生动态代理提供了四个静态方法,从而为一组接口动态地生成的代理类并返回代理类的实例对象。

// 返回与指定类加载器和接口相关联的的动态代理类的实例对象
public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces)
// 为指定的类加载器, 接口, 调用处理器来构建代理类的一个新实例对象, 该代理类由指定的类加载器定义并实现指定的接口
public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
// 判断指定类是否是一个动态代理类
public static boolean isProxyClass(Class<?> cl)
// 返回与指定代理类实例相关联的调用处理器实例
public static InvocationHandler getInvocationHandler(Object proxy)

java.lang.reflect.InvocationHandler 为调用处理器接口,该接口定义了一个 invoke() 方法,用于集中处理在代理类对象上的方法调用。当程序通过代理对象调用某一个方法时,该方法调用会被自动转发到 InvocationHandler.invoke() 方法来进行调用。

下面,我们编写几个简单的 Demo 来说明 JDK 原生动态代理模式。

  • 首先编写一个接口 userInterface.class,该接口声明了至少一个方法。
public interface userInterface {
    public void select();
    public void update();
}
  • 编写一个 userInterface 接口的实现类 userImpl.class,该类的角色为 RealSubject,作为后续代理的委派类。
public class userImpl implements userInterface {
    public void select() {
        System.out.println("User select action");
    }
    public void update() {
        System.out.println("User update action");
    }
}

下面,我们将通过 JDK 原生动态代理,对 userImpl 类进行功能增强,在调用 select()update() 方法前执行一些日志记录操作。

  • 编写一个调用处理器 logIHandler.class,提供日志记录的增强功能,并实现 InvocationHandler 接口。在 logIHandler 中维护一个目标对象,这个对象是上面的委派类 userImpl。此外,还需要实现 InvocationHandler 接口的 invoke() 方法,并在其中编写方法调用的处理逻辑。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Date;

public class logIHandler implements InvocationHandler {
    private Object target;

    public logIHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        before();
        Object result = method.invoke(target, args);
        after();
        return result;
    }
    private void before() {
        System.out.println(String.format("Log start time for action [%s] "new Date()));
    }
    private void after() {
        System.out.println(String.format("Log end time for action [%s] "new Date()));
    }
}
  • 最后,我们可以编写一个客户端测试类 appClient.class,对上述代理过程进行测试。测试类获取动态生成的代理类的对象须借助 java.lang.reflect.Proxy 类的 newProxyInstance() 方法。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class appClient {
    public static void main(String[] args) throws Exception {
        // 创建被代理的对象, userInterface 接口的实现类
        userImpl targetObj = new userImpl();
        // 获取对应的类加载器 ClassLoader
        ClassLoader classLoader = targetObj.getClass().getClassLoader();
        // 获取 targetObj 所实现的所有接口
        Class[] interfaces = targetObj.getClass().getInterfaces();
        // 创建一个调用处理器,处理所有的代理类对象上的方法调用, 需要传入实际的目标对象 targetObj
        InvocationHandler logHandler = new logIHandler(targetObj);
        /*
         *    根据上述提供的数据创建一个代理类对象, 在这个过程中, 将执行以下操作:
         *    1. JDK 会通过根据传入的参数信息动态地在内存中创建 Class 字节码
         *    2. 根据相应的字节码转换成对应的类
         *    3. 调用 newInstance() 方法创建代理类的实例对象
         */

        userInterface proxyObj = (userInterface) Proxy.newProxyInstance(classLoader, interfaces, logHandler);
        // 调用代理的方法
        proxyObj.select();
        proxyObj.update();
    }
}

运行 appClient.class 后将输出以下内容:

Log start time for action [Sat Apr 02 15:53:48 CST 2022
User select action
Log end time for action [Sat Apr 02 15:53:48 CST 2022
Log start time for action [Sat Apr 02 15:53:48 CST 2022
User update action
Log end time for action [Sat Apr 02 15:53:48 CST 2022

那么程序运行时自动生成的代理类是什么样的呢?我们可以通过设置系统属性来查看代理类,即在生成代理对象前添加以下代码:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles""true");
// 新版本: System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
Java 代理模式介绍
image-20220402163655460

再次运行 appClient.class 后,会在项目根目录中生成完全限定名称为 “com.sun.proxy.$Proxy0.class” 的文件,这就是动态代理类的字节码文件。双击后,IDEA 会自动反编译该文件,得到代理类的源码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.sun.proxy;

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

public final class $Proxy0 extends Proxy implements userInterface {
    private static Method m1;
    private static Method m2;
    private static Method m4;
    private static Method m0;
    private static Method m3;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void select() throws  {
        try {
            super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final void update() throws  {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            // 反射 Object.equals() 方法
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            // 反射 Object.toString() 方法
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            // 反射 userInterface 接口中的 select() 方法
            m4 = Class.forName("userInterface").getMethod("select");
            // 反射 Object.hashCode() 方法
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
            // 反射 userInterface 接口中的 update() 方法
            m3 = Class.forName("userInterface").getMethod("update");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

审计 Proxy0继承了 Proxy 类和 userInterface 接口。当进行类初始化操作(与实例化不同)时,执行静态代码块static{}中的一组反射调用,从而获取一些方法,并分别赋给m1m2m3m4m5`。

此外,类和所有方法都被 public final 修饰,所以代理类只可被使用,不可以被继承。每个方法都有一个 Method 对象来描述,这里的 Method 对象会在代理类被初始化是在 static{} 静态块中创建。

当调用代理类的方法时(例如前面调用的 select()update() 方法),代理类将通过 super.h.invoke(this, m4, (Object[])null) 的格式来调用。其中 super.h 实际上就是在创建代理类的时候传递给 Proxy.newProxyInstance() 方法的 logIHandler 类的对象,它继承了 InvocationHandler 类,负责实际的调用处理逻辑。

当 logIHandler 的 invoke() 方法接收到 proxymethodargs 等参数后,将进行一些处理,然后通过反射让被代理的对象 target 执行方法。

下面,我们在 appClient.class 的 Proxy.newProxyInstance() 处下断点进行动态调试:

Java 代理模式介绍
image-20220402160241878

可以看到,程序分别获取到所需的参数,然后创建了动态代理对象。所需的参数有:

  • targetObj:委派类的实例对象
  • classLoader:委派类的类加载器
  • interfaces:需要被代理的接口
  • logHandler:调用处理器

生成动态代理对象后,将继续往下运行,调用代理对象的 select() 方法:

Java 代理模式介绍
image-20220402160734812

步入后将调用代理对象的 invoke() 方法,然后从 invoke() 方法中调用了 before()

Java 代理模式介绍
image-20220402161039513

继续调试,将调用 method.invoke(target, args),这里的 method 为代理类中通过反射获取到的 userImpl.select() 方法,并通过 invoke() 执行这个方法:

Java 代理模式介绍
image-20220402161251513

也就是说,最终仍会调用委派类中的 select() 方法,结束后将调用我们定义的 after() 方法:

Java 代理模式介绍
image-20220402161912978

CGLIB 动态代理

CGLIB(Code Generation Library)是一个强大的、高性能的、高质量的 Code 生成类库,它可以在程序运行期扩展 Java 类与实现 Java 接口。CGLIB 动态代理时会生成一个委派类的子类,这个子类将重写委派类中所有非 final 修饰的方法,并采用方法拦截的技术拦截所有对父类(委派类)的方法调用,从而在其调用基础上添加自定义的代码逻辑。

CGLIB 动态代理涉及一个名为  “net.sf.cglib.proxy.Enhancer” 的字节码增强类,它是 CGLIB 库中最常用的一个,类似 JDK 原生动态代理中的 Proxy 类,只不过它不仅能代理普通的 Java 类,还能够代理 Java 接口。

Enhander 类的核心功能就是给委派类创建一个子类,并拦截所有对委派类中非 final 修饰的方法调用。该类中有几个比较重要的方,如下所示:

// 为生成的代理类指定父类
public void setSuperclass(java.lang.Class superclass)
// 为生成的代理类指定回调对象
public void setCallback(Callback callback)
// 生成代理类对象
public Object create()

上述的 “回调对象”(Callback )实际上是一个空接口,其中未定义任何方法,该接口的唯一作用就是在代理类的方法被调用时进行回调,也就是说当生成的代理类中的方法被调用时,会调用 Callback 实例对象中的代码逻辑。

CGLIB 库中提供了以下几个可用的 Callback 实例对象:

  • NoOP:该类不进行任何操作,只把对被代理方法请求转发到委派类的原方法中。
  • FixedValue:该类提供了一个 loadObject() 方法,拦截所有的方法调用,并调用 loadObject() 方法来返回一个固定值。该类不会调用委派类任何方法。
  • InvocationHandler:该类与 JDK 原生动态代理类似,当程序想要调用某一个委派类的方法时,该调用会被转发到 InvocationHandler.invoke() 方法中。
  • MethodInterceptor:方法拦截器接口,该类提供了一个 intercept() 方法,当继承了该接口后,所有对代理类的方法调用都会被转发到该接口的 intercept() 方法中。
  • Dispatcher:分发器,提供了一个 loadObject() 方法,该方法会返回一个代理对象来拦截每次对原方法的调用。
  • LazyLoader:懒加载器,提供了一个 loadObject() 方法,该方法会在第一次调用委派类方法时触发,返回一个代理对象。这个代理对象会被存储起来,用于处理后续所有对委派类方法的调用。

下面,我们编写几个简单的 Demo 来说明 CGLIB 动态代理模式,操作前需要自行通过 Maven 引入所需的 CGLIB 库。

  • 直接编写一个委派类 userImpl.class,并提供了两个方法。因为通过 CGLIB 实现动态代理时不需要继承接口,所以不再需要实现一个接口类。
public class userImpl {
    public void select() {
        System.out.println("User select action");
    }
    public void update() {
        System.out.println("User update action");
    }
}
  • 编写一个 logInterceptor.class 类,作为中介类。该类继承了 MethodInterceptor,用于方法的拦截回调。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.proxy.MethodInterceptor;

import java.lang.reflect.Method;
import java.util.Date;

public class logInterceptor implements MethodInterceptor {
    private Object target;

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

    public Object getProxyInstance() {
        // 创建 Enhancer 类的实例对象
        Enhancer enhancer = new Enhancer();
        // 调用 setSuperclass() 方法指定父类
        enhancer.setSuperclass(this.target.getClass());
        // 调用 setCallback() 方法设置回调对象
        enhancer.setCallback(this);
        // 创建并返回代理类的实例对象
        return enhancer.create();
    }

    @Override
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws java.lang.Throwable {
        before();
        Object result = proxy.invokeSuper(obj, args);
        after();
        return result;
    }

    private void before() {
        System.out.println(String.format("Log start time for action [%s] "new Date()));
    }
    private void after() {
        System.out.println(String.format("Log end time for action [%s] "new Date()));
    }
}
  • 最后,我们可以编写一个客户端测试类 appClient.class,对上述代理过程进行测试。
public class appClient {
    public static void main(String[] args) throws Exception {
        userImpl targetObj = new userImpl();
        userImpl proxyObj = (userImpl) new logInterceptor(targetObj).getProxyInstance();

        proxyObj.select();
        proxyObj.update();
    }
}

运行 appClient.class 后将输出以下内容:

Log start time for action [Sat Apr 02 17:42:11 CST 2022
User select action
Log end time for action [Sat Apr 02 17:42:11 CST 2022
Log start time for action [Sat Apr 02 17:42:11 CST 2022
User update action
Log end time for action [Sat Apr 02 17:42:11 CST 2022

就这样吧。。。。。。

Ending......

参考文献:

《Java 反序列化漏洞(5) – 解密 YSoSerial : Java动态代理机制》

Java 动态代理详解

Java 类加载机制

原文始发于微信公众号(山警网络空间安全实验室):Java 代理模式介绍

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月22日00:01:03
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java 代理模式介绍https://cn-sec.com/archives/865078.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息