searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

从代理聊到Lambda表达式

2024-02-21 02:05:08
69
0

从代理概念到代理模式

代理可谓无处不在。它是法律术语,是组织术语,也是商业行为。它是一种服务器架构,也是一种设计模式。

从其概念而言,代理与委托似乎密不可分,在被代理方委托下,代理方进行一定权责的行为。但在技术领域的定义中,对齐这个解释却颇为牵强,代理往往表达为对被代理对象的访问权限控制。

然而在实现代理模式时,被代理方又被进一步淡化,开发人员只关注在代理层插入自定义功能,全然没有委托的含意,也不再局限于访问的控制,只有无感知的侵入还算比较得体。

在Java的编程框架里,依赖有其静态的一面,调用关系在编译期间业已确定,也有其动态的一面,调用对象能够进行指派增强。代理模式是对动态依赖的具体实现,本质是面向接口的契约和面向继承的指派,一般可分为静态代理和动态代理,这些都是老生常谈。


静态代理无非就是如何设计类和实现类,左右不过是面向对象的编码,机制上着实乏善可陈。相反,动态代理花样繁多,耳熟能详的就有JavaProxy、CGLIB、ASM、Javassist、AspectJ和之上的SpringAOP等等。支撑这广泛框架的,是Java的类加载机制,实现最终代理接管,必然要经过代理类生成、代理类加载、代理对象替换三个阶段。

 

从类加载机制到代理对象

某种意义上,编程语言是英语的特殊子集,“.java”文件是Java的人工书写形式,而“.class”文件才是JVM世界中的通行文本。代理类生成主要是指代理类字节码的生成,而字节码也算是一种特殊的编程语言,只要按照既定规范,能够“入乡随俗讲当地话”,工具只是节省人力,最终跳不出“javac”结果的条条框框。

在虚拟机规范中,一个“.class”的生命周期包括如下七个阶段:


  • 加载(Loading):获取类的二进制字节流,生成代表该类的java.lang.Class对象
  • 验证(Verification):校验字节流是否符合虚拟机要求,包括格式、元数据、语义和引用等校验
  • 准备(Preparation):类变量分配内存和初始化
  • 解析(Resolution):常量池符号引用替换成直接引用
  • 初始化(Initialization):执行类构造器方法(<clinit>),从这开始执行类中定义的程序代码

加载是一切的起点,类加载器(ClassLoader)则是需要叩开的大门。曾经看过一家肉制品加工的老板吹嘘自己的机器和生产线有多先进——“猪从这边赶进去,香肠就从另一边出来了”,这么看类加载器也差不多——“字节码从这边赶进去,Class对象就从另一边出来了”。

Java类加载的“双亲委派模型”是另一个老生常谈的话题,实践中却不必拘泥于此,只消抓住“类是由类和加载器共同确定”这一要旨,剩下的就是因势利导了:

  • 双亲委派的层级模型保障基本的稳定性,核心类(如rt.jar)最终都由Bootstrap ClassLoader加载,既防止恶意篡改,又能在任何加载器环境中保持绝对统一
  • 由于类加载的可见性是单向的(即子加载器对父加载器加载的类可见,反之不然),必要时也需要破坏双亲委派模型,例如基础类需要调用用户代码的时候(如SPI机制),由此引入上下文加载器(Context ClassLoader),通过线程传递来达到父加载器请求子加载器去完成类加载动作的效果
  • 用户对程序隔离和动态性的追求,往往只将层级模型作为加载顺序的约束,通过自定义的类加载器和选择策略,来达到设计目的(如SpringBoot/Tomcat/OSGI的类加载架构)

前面长篇累牍也只讲到与代理类加载相关的事情,因为如何生成代理对象并不是语言机制的问题,更多是上层业务的选择。至于是使用JavaProxy实现基于接口的代理,还是使用CGLIB实现基于对象的代理,都没有本质差别,只是框架不同罢了。

如果将静态代理比作逐帧手绘的老式动画,那么动态代理就像包含矢量计算的游戏CG。当它们灌注成盘准备播放的时候,所谓动态也只是少画几张手稿而已,毕竟即使观影途中觉得颜色不够鲜艳,也不能掀开机器再往胶卷上涂抹改进。

考虑两个真正称得上是“动态”的场景——从空间上,被代理对象生命周期不由编程控制;从时间上,在程序运行中织入代理逻辑。SpringAOP是处理第一种场景的特殊方案,因为它管理所有Bean的生命周期,对于更一般性的场景,还需要从JVM本身的支持入手。

 

从Java agent到动态代理

JVM TI(JVM Tool Interface)是一组用于开发和监控工具的官方编程接口,可以将它理解成在JVM中预埋的事件回调或钩子方法(Hook),能够实现各种profile、debug、监控、线程分析、堆栈分析、覆盖分析等工具。顺着代理的场景,针对类加载事件(ClassFileLoadHook),开发者可以使用JVM TI动态地加载、卸载或重新定义类,一般是通过Java agent实现Instrumentation接口来处理的。


简单浏览Instrumentation的接口定义,通过注册的ClassFileTransformer拦截类加载事件,可以对类字节码进行重写,从而实现类的重定义。

public interface Instrumentation {
    /**
     * 注册Transformer,拦截此后的类加载事件
     */
    void addTransformer(ClassFileTransformer transformer);
    
    /**
     * 重新触发已加载的类,由注册的Transformer拦截
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
    /**
     * 获取已加载的所有类对象
     */
    Class[] getAllLoadedClasses();
}

Java agent有两种加载方式,可与前面提到的动态场景相对应:

  • JVM启动时加载,通过启动参数加载agent,可以拦截编程时不直接控制生命周期的对象
public static void premain(String args, Instrumentation inst);
public static void premain(String args);
  • JVM运行时加载,通过Attach API加载agent,可以重定义类实现代理逻辑织入
public static void agentmain(String args, Instrumentation inst);
public static void agentmain(String args);
VirtualMachine vm = VirtualMachine.attach("target_jvm_pid");  
try {
    vm.loadAgent("agent_jar_path");    
} finally {
    vm.detach();
}

到这里似乎有时机有手段可以无侵入地实现任意对象的代理了,毕竟猪是自己亲手赶进去的,出来什么口味的香肠还不是手到擒来。可是偏偏就有漏网之鱼,因为有些香肠,它居然不需要猪。

 

从Lambda表达式到放弃代理

public class TestAgent {

    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("run1");
            }
        };
        Runnable r2 = new Runnable2();
        Runnable r3 = () -> System.out.println("run3");

        System.out.println("r1:" + r1.getClass().getName());
        System.out.println("r2:" + r2.getClass().getName());
        System.out.println("r3:" + r3.getClass().getName());

        r1.run();
        r2.run();
        r3.run();
    }

    public static class Runnable2 implements Runnable {

        @Override
        public void run() {
            System.out.println("run2");
        }
    }

}
public class TestPremain {

    public static void premain(String args, Instrumentation inst) {
        System.out.println("Premain start.");
        addTransformer(inst);
        System.out.println("Premain end.");
    }

    private static void addTransformer(Instrumentation instrumentation) {
        instrumentation.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                System.out.println("Transform " + className);
                return null;
            }
        }, true);
    }

}

上边这个简单的例子尝试查看Transformer拦截类的情况(忽略add之前加载的类,premain在main之前,不影响分析),出现了一个奇怪的类(test.TestAgent$$Lambda$14/0x0000000800099440),它没有经过transform方法,也与匿名内部类(test/TestAgent$1)不同,没有生成任何.class文件。

Transform test/TestAgent
Transform test/TestAgent$1
Transform test/TestAgent$Runnable2
r1:test.TestAgent$1
r2:test.TestAgent$Runnable2
r3:test.TestAgent$$Lambda$14/0x0000000800099440
run1
run2
run3

这就是Lambda表达式生成的“隐藏类”,可以添加调试参数(-Djdk.internal.lambda.dumpProxyClasses)将其保留下来,反编译结果可以看出它是个合成类,从内容可以推断几点有意思的地方:

  • final类加private构造函数,说明要通过反射来间接使用
  • 接口实现引用了主类“不存在”的静态方法,说明Lambda表达式生成类与匿名内部类的差别
  • 生成的类名与class.getName()不同,没有后缀(TestAgent$$Lambda$14/0x0000000800099440),说明“隐藏类”规则的特殊性
import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class TestAgent$$Lambda$14 implements Runnable {
    private TestAgent$$Lambda$14() {
    }

    @Hidden
    public void run() {
        TestAgent.lambda$main$0();
    }
}

使用javap指令解析主类的字节码,查看Lambda表达式生成的关键信息(节选):

  • javap -verbose -private TestAgent.class
16: invokedynamic #6,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;

#6 = InvokeDynamic      #0:#37         // #0:run:()Ljava/lang/Runnable;

BootstrapMethods:
  0: #34 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #35 ()V
      #36 REF_invokeStatic test/TestAgent.lambda$main$0:()V
      #35 ()V

private static void lambda$main$0();
    descriptor: ()V
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #15                 // String run3
         5: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0

可以看到确是生成了一个静态方法(lambda$main$0),最终委托给LambdaMetafactory.metafactory()生成调用点:

public static CallSite metafactory(MethodHandles.Lookup caller, String invokedName, MethodType invokedType, MethodType samMethodType, MethodHandle implMethod, MethodType instantiatedMethodType) throws LambdaConversionException {
    AbstractValidatingLambdaMetafactory mf = new InnerClassLambdaMetafactory(caller, invokedType, invokedName, samMethodType, implMethod, instantiatedMethodType, false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
    mf.validateMetafactoryArgs();
    return mf.buildCallSite();
}

层层追查下去,找到生成Class的地方(省略其他):

private Class<?> spinInnerClass() throws LambdaConversionException {
    ...
    return UNSAFE.defineAnonymousClass(this.targetClass, classBytes, (Object[])null);
}

所以Lambda表达式生成类的特殊性在于它是由Unsafe.defineAnonymousClass方法生成的“VM anonymous class”,这里不展开它的具体细节,暂且简单理解它是一个不被类加载器系统或者系统字典感知的类型:

  • 它“没有名字”,即便使用class.getName()结果进行Class.forName()操作也会抛出ClassNotFoundException异常,构造出来后只能通过Unsafe.defineAnonymousClass()返回的Class对象进行反射操作
  • 它“不显式挂在ClassLoader下面”,与retransform class不相容,不能被重定义

那么,使用Java agent是不是就完全没法拦截和生成Lambda表达式的代理对象了?其实也不是,虽然这种香肠不需要猪,但我们可以代理香肠机——也就是拦截InnerClassLambdaMetafactory类,并对spinInnerClass()方法做手脚。

但这样做过于复杂,也侵入了本不对开发人员可见的机制,稍有不慎影响整个系统。所以,对于解决Lambda表达式生成对象动态代理问题的最好方法或许就是不要有这种想法。

毕竟条条大路通罗马,没必要钻这死胡同。

0条评论
0 / 1000
陈一之
21文章数
2粉丝数
陈一之
21 文章 | 2 粉丝
原创

从代理聊到Lambda表达式

2024-02-21 02:05:08
69
0

从代理概念到代理模式

代理可谓无处不在。它是法律术语,是组织术语,也是商业行为。它是一种服务器架构,也是一种设计模式。

从其概念而言,代理与委托似乎密不可分,在被代理方委托下,代理方进行一定权责的行为。但在技术领域的定义中,对齐这个解释却颇为牵强,代理往往表达为对被代理对象的访问权限控制。

然而在实现代理模式时,被代理方又被进一步淡化,开发人员只关注在代理层插入自定义功能,全然没有委托的含意,也不再局限于访问的控制,只有无感知的侵入还算比较得体。

在Java的编程框架里,依赖有其静态的一面,调用关系在编译期间业已确定,也有其动态的一面,调用对象能够进行指派增强。代理模式是对动态依赖的具体实现,本质是面向接口的契约和面向继承的指派,一般可分为静态代理和动态代理,这些都是老生常谈。


静态代理无非就是如何设计类和实现类,左右不过是面向对象的编码,机制上着实乏善可陈。相反,动态代理花样繁多,耳熟能详的就有JavaProxy、CGLIB、ASM、Javassist、AspectJ和之上的SpringAOP等等。支撑这广泛框架的,是Java的类加载机制,实现最终代理接管,必然要经过代理类生成、代理类加载、代理对象替换三个阶段。

 

从类加载机制到代理对象

某种意义上,编程语言是英语的特殊子集,“.java”文件是Java的人工书写形式,而“.class”文件才是JVM世界中的通行文本。代理类生成主要是指代理类字节码的生成,而字节码也算是一种特殊的编程语言,只要按照既定规范,能够“入乡随俗讲当地话”,工具只是节省人力,最终跳不出“javac”结果的条条框框。

在虚拟机规范中,一个“.class”的生命周期包括如下七个阶段:


  • 加载(Loading):获取类的二进制字节流,生成代表该类的java.lang.Class对象
  • 验证(Verification):校验字节流是否符合虚拟机要求,包括格式、元数据、语义和引用等校验
  • 准备(Preparation):类变量分配内存和初始化
  • 解析(Resolution):常量池符号引用替换成直接引用
  • 初始化(Initialization):执行类构造器方法(<clinit>),从这开始执行类中定义的程序代码

加载是一切的起点,类加载器(ClassLoader)则是需要叩开的大门。曾经看过一家肉制品加工的老板吹嘘自己的机器和生产线有多先进——“猪从这边赶进去,香肠就从另一边出来了”,这么看类加载器也差不多——“字节码从这边赶进去,Class对象就从另一边出来了”。

Java类加载的“双亲委派模型”是另一个老生常谈的话题,实践中却不必拘泥于此,只消抓住“类是由类和加载器共同确定”这一要旨,剩下的就是因势利导了:

  • 双亲委派的层级模型保障基本的稳定性,核心类(如rt.jar)最终都由Bootstrap ClassLoader加载,既防止恶意篡改,又能在任何加载器环境中保持绝对统一
  • 由于类加载的可见性是单向的(即子加载器对父加载器加载的类可见,反之不然),必要时也需要破坏双亲委派模型,例如基础类需要调用用户代码的时候(如SPI机制),由此引入上下文加载器(Context ClassLoader),通过线程传递来达到父加载器请求子加载器去完成类加载动作的效果
  • 用户对程序隔离和动态性的追求,往往只将层级模型作为加载顺序的约束,通过自定义的类加载器和选择策略,来达到设计目的(如SpringBoot/Tomcat/OSGI的类加载架构)

前面长篇累牍也只讲到与代理类加载相关的事情,因为如何生成代理对象并不是语言机制的问题,更多是上层业务的选择。至于是使用JavaProxy实现基于接口的代理,还是使用CGLIB实现基于对象的代理,都没有本质差别,只是框架不同罢了。

如果将静态代理比作逐帧手绘的老式动画,那么动态代理就像包含矢量计算的游戏CG。当它们灌注成盘准备播放的时候,所谓动态也只是少画几张手稿而已,毕竟即使观影途中觉得颜色不够鲜艳,也不能掀开机器再往胶卷上涂抹改进。

考虑两个真正称得上是“动态”的场景——从空间上,被代理对象生命周期不由编程控制;从时间上,在程序运行中织入代理逻辑。SpringAOP是处理第一种场景的特殊方案,因为它管理所有Bean的生命周期,对于更一般性的场景,还需要从JVM本身的支持入手。

 

从Java agent到动态代理

JVM TI(JVM Tool Interface)是一组用于开发和监控工具的官方编程接口,可以将它理解成在JVM中预埋的事件回调或钩子方法(Hook),能够实现各种profile、debug、监控、线程分析、堆栈分析、覆盖分析等工具。顺着代理的场景,针对类加载事件(ClassFileLoadHook),开发者可以使用JVM TI动态地加载、卸载或重新定义类,一般是通过Java agent实现Instrumentation接口来处理的。


简单浏览Instrumentation的接口定义,通过注册的ClassFileTransformer拦截类加载事件,可以对类字节码进行重写,从而实现类的重定义。

public interface Instrumentation {
    /**
     * 注册Transformer,拦截此后的类加载事件
     */
    void addTransformer(ClassFileTransformer transformer);
    
    /**
     * 重新触发已加载的类,由注册的Transformer拦截
     */
    void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    
    /**
     * 获取已加载的所有类对象
     */
    Class[] getAllLoadedClasses();
}

Java agent有两种加载方式,可与前面提到的动态场景相对应:

  • JVM启动时加载,通过启动参数加载agent,可以拦截编程时不直接控制生命周期的对象
public static void premain(String args, Instrumentation inst);
public static void premain(String args);
  • JVM运行时加载,通过Attach API加载agent,可以重定义类实现代理逻辑织入
public static void agentmain(String args, Instrumentation inst);
public static void agentmain(String args);
VirtualMachine vm = VirtualMachine.attach("target_jvm_pid");  
try {
    vm.loadAgent("agent_jar_path");    
} finally {
    vm.detach();
}

到这里似乎有时机有手段可以无侵入地实现任意对象的代理了,毕竟猪是自己亲手赶进去的,出来什么口味的香肠还不是手到擒来。可是偏偏就有漏网之鱼,因为有些香肠,它居然不需要猪。

 

从Lambda表达式到放弃代理

public class TestAgent {

    public static void main(String[] args) {
        Runnable r1 = new Runnable() {
            @Override
            public void run() {
                System.out.println("run1");
            }
        };
        Runnable r2 = new Runnable2();
        Runnable r3 = () -> System.out.println("run3");

        System.out.println("r1:" + r1.getClass().getName());
        System.out.println("r2:" + r2.getClass().getName());
        System.out.println("r3:" + r3.getClass().getName());

        r1.run();
        r2.run();
        r3.run();
    }

    public static class Runnable2 implements Runnable {

        @Override
        public void run() {
            System.out.println("run2");
        }
    }

}
public class TestPremain {

    public static void premain(String args, Instrumentation inst) {
        System.out.println("Premain start.");
        addTransformer(inst);
        System.out.println("Premain end.");
    }

    private static void addTransformer(Instrumentation instrumentation) {
        instrumentation.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
                System.out.println("Transform " + className);
                return null;
            }
        }, true);
    }

}

上边这个简单的例子尝试查看Transformer拦截类的情况(忽略add之前加载的类,premain在main之前,不影响分析),出现了一个奇怪的类(test.TestAgent$$Lambda$14/0x0000000800099440),它没有经过transform方法,也与匿名内部类(test/TestAgent$1)不同,没有生成任何.class文件。

Transform test/TestAgent
Transform test/TestAgent$1
Transform test/TestAgent$Runnable2
r1:test.TestAgent$1
r2:test.TestAgent$Runnable2
r3:test.TestAgent$$Lambda$14/0x0000000800099440
run1
run2
run3

这就是Lambda表达式生成的“隐藏类”,可以添加调试参数(-Djdk.internal.lambda.dumpProxyClasses)将其保留下来,反编译结果可以看出它是个合成类,从内容可以推断几点有意思的地方:

  • final类加private构造函数,说明要通过反射来间接使用
  • 接口实现引用了主类“不存在”的静态方法,说明Lambda表达式生成类与匿名内部类的差别
  • 生成的类名与class.getName()不同,没有后缀(TestAgent$$Lambda$14/0x0000000800099440),说明“隐藏类”规则的特殊性
import java.lang.invoke.LambdaForm.Hidden;

// $FF: synthetic class
final class TestAgent$$Lambda$14 implements Runnable {
    private TestAgent$$Lambda$14() {
    }

    @Hidden
    public void run() {
        TestAgent.lambda$main$0();
    }
}

使用javap指令解析主类的字节码,查看Lambda表达式生成的关键信息(节选):

  • javap -verbose -private TestAgent.class
16: invokedynamic #6,  0              // InvokeDynamic #0:run:()Ljava/lang/Runnable;

#6 = InvokeDynamic      #0:#37         // #0:run:()Ljava/lang/Runnable;

BootstrapMethods:
  0: #34 REF_invokeStatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
    Method arguments:
      #35 ()V
      #36 REF_invokeStatic test/TestAgent.lambda$main$0:()V
      #35 ()V

private static void lambda$main$0();
    descriptor: ()V
    flags: (0x100a) ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=0, args_size=0
         0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #15                 // String run3
         5: invokevirtual #11                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 13: 0

可以看到确是生成了一个静态方法(lambda$main$0),最终委托给LambdaMetafactory.metafactory()生成调用点:

public static CallSite metafactory(MethodHandles.Lookup caller, String invokedName, MethodType invokedType, MethodType samMethodType, MethodHandle implMethod, MethodType instantiatedMethodType) throws LambdaConversionException {
    AbstractValidatingLambdaMetafactory mf = new InnerClassLambdaMetafactory(caller, invokedType, invokedName, samMethodType, implMethod, instantiatedMethodType, false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
    mf.validateMetafactoryArgs();
    return mf.buildCallSite();
}

层层追查下去,找到生成Class的地方(省略其他):

private Class<?> spinInnerClass() throws LambdaConversionException {
    ...
    return UNSAFE.defineAnonymousClass(this.targetClass, classBytes, (Object[])null);
}

所以Lambda表达式生成类的特殊性在于它是由Unsafe.defineAnonymousClass方法生成的“VM anonymous class”,这里不展开它的具体细节,暂且简单理解它是一个不被类加载器系统或者系统字典感知的类型:

  • 它“没有名字”,即便使用class.getName()结果进行Class.forName()操作也会抛出ClassNotFoundException异常,构造出来后只能通过Unsafe.defineAnonymousClass()返回的Class对象进行反射操作
  • 它“不显式挂在ClassLoader下面”,与retransform class不相容,不能被重定义

那么,使用Java agent是不是就完全没法拦截和生成Lambda表达式的代理对象了?其实也不是,虽然这种香肠不需要猪,但我们可以代理香肠机——也就是拦截InnerClassLambdaMetafactory类,并对spinInnerClass()方法做手脚。

但这样做过于复杂,也侵入了本不对开发人员可见的机制,稍有不慎影响整个系统。所以,对于解决Lambda表达式生成对象动态代理问题的最好方法或许就是不要有这种想法。

毕竟条条大路通罗马,没必要钻这死胡同。

文章来自个人专栏
学而时习
21 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
2
2