深入解析 当匿名内部类替换成lambda表达式出现: Unable to determine source type <S> and target type <T>的报错

分析中,涉及的知识点,我都尽可能带了一下,所以内容比较多。建议直接看目录跳转到想了解的部分即可。
涉及知识点:匿名内部类生成,lambda表达式生成,泛型擦拭,泛型桥方法,泛型与Signature Attribute关系,jvm指令invokeDynamic,反射机制等。

请先看目录~

1. 背景说明

在用sonar扫码项目时,有如下的提示:
sonar提示将匿名内部类可替换成lmbda表达式
将匿名内部类替换成lambda后,如下:
匿名表达式替换成lambda
替换后,重新启动项目报错~
替换后项目启动报错
最终结论,不能换,2个机制不一样。可直接跳转 3 总结

2 具体解析

2.1 GenericConversionService的addConverter

GenericConversionService  报错
进入源码,我们很明显看到报错的地方。
是因为 代码A 处的ResolvableType[] typeInfo 为null。

	public void addConverter(Converter<?, ?> converter) {
		
        ResolvableType[] typeInfo = this.getRequiredTypeInfo(converter.getClass(), Converter.class); // 代码B 
        if (typeInfo == null && converter instanceof DecoratingProxy) {
            typeInfo = this.getRequiredTypeInfo(((DecoratingProxy)converter).getDecoratedClass(), Converter.class);
        }
        
        if (typeInfo == null) { // 代码A 
            throw new IllegalArgumentException("Unable to determine source type <S> and target type <T> for your Converter [" + converter.getClass().getName() + "]; does the class parameterize those types?");
        } else {
            this.addConverter((GenericConverter)(new GenericConversionService.ConverterAdapter(converter, typeInfo[0], typeInfo[1])));
        }
    }

通过对 getRequiredTypeInfo 查看,可知该方法通过spring core 的 ResolvableType 获取自定义转换器的泛型类型

	private ResolvableType[] getRequiredTypeInfo(Class<?> converterClass, Class<?> genericIfc) {
		// 代码A
        ResolvableType resolvableType = ResolvableType.forClass(converterClass).as(genericIfc);
        // 代码B 
        ResolvableType[] generics = resolvableType.getGenerics();
        if (generics.length < 2) {
            return null;
        } else {
            Class<?> sourceType = generics[0].resolve();
            Class<?> targetType = generics[1].resolve();
            return sourceType != null && targetType != null ? generics : null;
        }
    }

2.2 ResolveType 解析直观对比

当匿名内部类的时候
入参:converterClasscom.example.jun.WebConfig$1
代码A:resolvableTypeorg.springframework.core.convert.converter.Converter<java.lang.String, java.lang.String>
代码B : 返回解析成功的S,T分别是String 类型
在这里插入图片描述
当lambda的时候
入参:converterClasscom.example.jun.WebConfig$$Lambda$475/749362556
代码A:resolvableTypeorg.springframework.core.convert.converter.Converter<?, ?>
代码B : 返回解析成功的S,T分别是类型。最终解析为null 类型。
在这里插入图片描述

2.3 匿名内部类 和 lambda表达式生成区别

为了聊了其中的机制,我们首先从来分析匿名内部类 和 lambda 表达式的生成区别。
主要是解析字节码用到了以下2种方式。① idea插件-jclasslib(备注:jclasslib 只是为了展示更为直观,没有也不用紧。) ② 使用javap 命令

2.3.1 匿名内部类的生成分析

这个举个粒子~

public class AnonymousInnerClassTest {
    public static void main(String[] args) {
        Comparator<String> comparator = new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.charAt(1) - o1.charAt(1);
            }
        };
    }
}

经过编译之后(javac 或者idea 自行编译)
匿名内部类编译之后 会以 $[数字] 结尾生成一个新的字节码 文件
在这里插入图片描述

2.3.1.1 关于主类 - AnonymousInnerClassTest 字节码

使用jclasslib,可以直观的看到AnonymousInnerClassTest。(如果不使用 使用javap -p -c -v -l AnonymousInnerClassTest.class 亦可)
方法包括 initmain
属性中 包括 内部类。
在这里插入图片描述
main的字节码如下

// 创建类实例AnonymousInnerClassTest$1
0 new #2 <com/example/jun/AnonymousInnerClassTest$1> 
// 复制栈顶入栈
3 dup 
// 实例化 AnonymousInnerClassTest$1 ,这里会是当前对象的引用出栈,然而init没有返回值,为避免后续操作栈没有当前对象的引用,所以在这步之前一般都会dup一份
4 invokespecial #3 <com/example/jun/AnonymousInnerClassTest$1.<init> : ()V> 
// 将栈顶引用型数值存入第二个本地变量
7 astore_1 
// main 返回
8 return

在这里插入图片描述

2.3.1.2 匿名类 - AnonymousInnerClassTest$1 字节码

现在我们使用javap 查看额外生成的字节码

>javap -p -c -v -l AnonymousInnerClassTest$1.class
Classfile /D:/workspace/BugTest/jun/src/main/java/com/example/jun/AnonymousInnerClassTest$1.class
  Last modified 2022-4-7; size 762 bytes
  MD5 checksum 7456e47928bf1bdf52666453ebb91b03
  Compiled from "AnonymousInnerClassTest.java"
class com.example.jun.AnonymousInnerClassTest$1 extends java.lang.Object implements java.util.Comparator<java.lang.String>
  minor version: 0
  major version: 61
  flags: ACC_SUPER
Constant pool:
   #1 = Methodref          #2.#3          // java/lang/Object."<init>":()V
   #2 = Class              #4             // java/lang/Object
   #3 = NameAndType        #5:#6          // "<init>":()V
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Methodref          #8.#9          // java/lang/String.charAt:(I)C
   #8 = Class              #10            // java/lang/String
   #9 = NameAndType        #11:#12        // charAt:(I)C
  #10 = Utf8               java/lang/String
  #11 = Utf8               charAt
  #12 = Utf8               (I)C
  #13 = Methodref          #14.#15        // com/example/jun/AnonymousInnerClassTest$1.compare:(Ljava/lang/String;Ljava/lang/String;)I
  #14 = Class              #16            // com/example/jun/AnonymousInnerClassTest$1
  #15 = NameAndType        #17:#18        // compare:(Ljava/lang/String;Ljava/lang/String;)I
  #16 = Utf8               com/example/jun/AnonymousInnerClassTest$1
  #17 = Utf8               compare
  #18 = Utf8               (Ljava/lang/String;Ljava/lang/String;)I
  #19 = Class              #20            // java/util/Comparator
  #20 = Utf8               java/util/Comparator
  #21 = Utf8               Code
  #22 = Utf8               LineNumberTable
  #23 = Utf8               (Ljava/lang/Object;Ljava/lang/Object;)I
  #24 = Utf8               Signature
  #25 = Utf8               Ljava/lang/Object;Ljava/util/Comparator<Ljava/lang/String;>;
  #26 = Utf8               SourceFile
  #27 = Utf8               AnonymousInnerClassTest.java
  #28 = Utf8               EnclosingMethod
  #29 = Class              #30            // com/example/jun/AnonymousInnerClassTest
  #30 = Utf8               com/example/jun/AnonymousInnerClassTest
  #31 = NameAndType        #32:#33        // main:([Ljava/lang/String;)V
  #32 = Utf8               main
  #33 = Utf8               ([Ljava/lang/String;)V
  #34 = Utf8               NestHost
  #35 = Utf8               InnerClasses
{
  com.example.jun.AnonymousInnerClassTest$1();
    descriptor: ()V
    flags:
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0

  public int compare(java.lang.String, java.lang.String);
    descriptor: (Ljava/lang/String;Ljava/lang/String;)I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=3
         0: aload_2
         1: iconst_1
         2: invokevirtual #7                  // Method java/lang/String.charAt:(I)C
         5: aload_1
         6: iconst_1
         7: invokevirtual #7                  // Method java/lang/String.charAt:(I)C
        10: isub
        11: ireturn
      LineNumberTable:
        line 10: 0

  public int compare(java.lang.Object, java.lang.Object);
    descriptor: (Ljava/lang/Object;Ljava/lang/Object;)I
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=3, locals=3, args_size=3
         0: aload_0
         1: aload_1
         2: checkcast     #8                  // class java/lang/String
         5: aload_2
         6: checkcast     #8                  // class java/lang/String
         9: invokevirtual #13                 // Method compare:(Ljava/lang/String;Ljava/lang/String;)I
        12: ireturn
      LineNumberTable:
        line 7: 0
}
Signature: #25                          // Ljava/lang/Object;Ljava/util/Comparator<Ljava/lang/String;>;
SourceFile: "AnonymousInnerClassTest.java"
EnclosingMethod: #29.#31                // com.example.jun.AnonymousInnerClassTest.main
Error: unknown attribute
  NestHost: length = 0x2
   00 1D
InnerClasses:
     #14; //class com/example/jun/AnonymousInnerClassTest$1

通过分析,可以从反编译后的字节码中看出来AnonymousInnerClassTest$1 中,
有3个方法:
com.example.jun.AnonymousInnerClassTest$1();
public int compare(java.lang.String, java.lang.String);
public int compare(java.lang.Object, java.lang.Object);

2.3.1.3 匿名类 - AnonymousInnerClassTest$1 源码

现在我们来直观的看下生成的内容。这里 Luyten 或者idea,都可以直接解析class文件 。解析完后,分别如下。
在这里插入图片描述
在这里插入图片描述

2.3.1.3.1 匿名类的泛型处理 - 桥方法

在这里,编译器在泛型擦拭过程中生成了一个桥方法。泛型的桥方法

在编译扩展参数化类或实现参数化接口的类或接口时,编译器可能需要创建一个合成方法,称为桥方法,作为类型擦除过程的一部分。
在这里插入图片描述

而桥方法public int compare(java.lang.Object, java.lang.Object); 本质上将参数进行转换checkcase,最后还是invokeVirtual public int compare(java.lang.String, java.lang.String);
在这里插入图片描述

2.3.1.3.2 匿名类的泛型处理 - Signature

同时有个特别之处,在于在这里插入图片描述
而这个属性的定义如下。

最简单的来说,他可以记录泛型。
在这里插入图片描述

2.3.2 【小节】匿名内部类实现总结

① 编译器会根据主类生成 &[数字] 结尾的新的字节码
② 在泛型擦拭过程中,会生成桥方法。
③ 泛型擦拭中,字节码中会有signatura 来记录泛型变量。

这里特别说明一下,② 和 ③是因为我们指定泛型类型,如果不指定的话,泛型则默认是Object。会被认定为普通类和普通方法,编译后字节码是没有Signature Attriture的 。具体说明见 泛型擦拭
在这里插入图片描述

2.3.2 lambda的生成分析

2.3.2.1 关于主类分析
public class LambdaClassTest {
    public static void main(String[] args) {
        Comparator<String> comparator = (o1, o2) -> o2.charAt(1) - o1.charAt(1);
    }
}

通过查看jclasslib,可知生成的字节码有3个方法,分别是 init , main , lambda$main$0

2.3.2.2 主类编译期会生成一个新的方法 - lambda$main$0

首先来看lambda$main$0,关于相关方法签名定义如下:
在这里插入图片描述通过对lambda$main$0 的字节码的解读,可知方法体就是我们定义的 return o2.charAt(1) - o1.charAt(1)。也就是说,编译器会把相关内容编译成一个 私有静态方法。方法名称是 $main$[数字] ,方法体为自定义的实现的内容在这里插入图片描述
相当于: private int lambda$main$0(String o1,String o2){return o2.charAt(1) - o1.charAt(1);}

2.3.2.3 关于lambda的jvm指令 invokedynamic

下来,我们来解读 main 方法
在这里插入图片描述
这里有个新jvm指令:invokedynamic( java 7 引入的一条新的虚拟机指令,java 8 这条指令才第一次在 java 应用,用在 lambda 表达式中)。

// invokedynamic 调用 BSM (引导方法-应用级的代码)
// BSM 返回 CallSite(调用点) 对象
// CallSite 是 MethodHandle(方法句柄) 的holder(持有)
// MethodHandle (java 反射机制)执行一个真正执行方法
invokedynamic [CallSite BSM()]
  • 一句话解释这个指令就是:Invoke a dynamically-computed call site(调用动态计算的调用站点)
  • invokedynamic 与其他 invoke 指令不同的是它允许由应用级的代码来决定方法解析。
  • 应用级的代码其实是一个方法,在这里这个方法被称为引导方法(Bootstrap Method),简称 BSM。
  • BSM 返回一个 CallSite(调用点) 对象,这个对象就和 invokedynamic 链接在一起了。以后再执行这条 invokedynamic 指令都不会创建新的 CallSite 对象。
  • CallSite 就是一个 MethodHandle(方法句柄) 的 holder。方法句柄指向一个调用点真正执行的方法。而MethodHandle可以理解为java提供的一种反射机制。(反射这里就不展开了~)

1、查看 invokedynamic 指向
点击invokedynamic 查看对应的常量池的指定(下面展示我同时放插件和javap的2种效果,对比着看)
在这里插入图片描述

0: invokedynamic #2,  0   // InvokeDynamic #0:compare:()Ljava/util/Comparator;

2、invokedynamic 指向类型

查看常量池#2 ,对应类型是 CONSTANT_InvokeDynamic_info

CONSTANT_InvokeDynamic_info 调用动态指令 (§invokedynamic) 使用该结构来指定引导方法动态调用名称调用的参数返回类型 ,以及可选一系列称为 bootstrap 方法的静态参数的附加常量。)

在这里插入图片描述

 #2 = InvokeDynamic      #0:#35         // #0:compare:()Ljava/util/Comparator;

2.1 invokedynamic 指向类型 - 参数
查看(第一个黄色框)返回值是 #35 ,是 CONSTANT_NameAndType_info

CONSTANT_NameAndType_info 表示方法名和方法类型,这个会作为参数传递给 BSM(应用层面方法,就是后面的metafactory的一些入参,由jvm提供)

在这里插入图片描述

 #35 = NameAndType        #43:#44        // compare:()Ljava/util/Comparator;

2.2 invokedynamic 指向类型 - 调用方法标识
查看(第二个黄色框)bootstrapMethods对应#0

bootstrapMethods 该属性记录调用动态指令引用的引导方法说明符

在这里插入图片描述

BootstrapMethods:
  0: #31 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:
      #32 (Ljava/lang/Object;Ljava/lang/Object;)I
      #33 invokestatic com/example/jun/LambdaClassTest.lambda$main$0:(Ljava/lang/String;Ljava/lang/String;)I
      #34 (Ljava/lang/String;Ljava/lang/String;)I

3、查看对应的BootstrapMethods 具体调用方法
查看#31 ,是 CONSTANT_MethodHandle_info

CONSTANT_MethodHandle_info ,该结构用于表示方法句柄。

而这里的MethodHandle,就是最终调用的应用层面的方法,理解为BSM,是java.lang.invoke.LambdaMetafactory.metafactory
在这里插入图片描述

 #31 = MethodHandle       #6:#40         // 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;
2.3.2.4 引导方法的源码分析 - metafactory

这里直接查看java源码如下

public static CallSite metafactory(MethodHandles.Lookup caller,
                                       String invokedName,
                                       MethodType invokedType,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType)
            throws LambdaConversionException {
        AbstractValidatingLambdaMetafactory mf;
        // 代码一:初始化
        mf = new InnerClassLambdaMetafactory(caller, invokedType,
                                             invokedName, samMethodType,
                                             implMethod, instantiatedMethodType,
                                             false, EMPTY_CLASS_ARRAY, EMPTY_MT_ARRAY);
        // 代码二:检测参数                                    
        mf.validateMetafactoryArgs();
        // 代码三:返回callSite
        return mf.buildCallSite();
    }
2.3.2.4.1 metafactory参数说明
  • 前三个参数是由JVM传入的

    • caller - 表示具有调用者访问权限的查找上下文(调用Lambda表达式的类名)
    • invokedName - 要实现的方法的名称
    • invokedType - 调用站点的预期签名。参数类型代表捕获变量的类型;返回类型是要实现的接口
  • 后三个参数则在字节码 BootstrapMethods->Method arguments域中有体现

    • samMethodType –函数对象要实现的方法的签名和返回类型。
    • implMethod 一个直接方法句柄,描述在调用时应该调用的实现方法。就是通过invokestatic指令调用了上述lambda$main$0()方法,(Lambda表达式的逻辑)
    • InstancedMethodType –应该在调用时动态执行的签名和返回类型。(一般InstancedMethodType 和 samMethodType 一样。但是你懂得,泛型下就不一样了~)

具体参数断点后的效果如下:
在这里插入图片描述

2.3.2.4.2 InnerClassLambdaMetafactory 初始化

查看内部类lambda元工厂初始化。这里会初始化后期生成的匿名类的类名,asm的初始化等。

public InnerClassLambdaMetafactory(MethodHandles.Lookup caller,
                                       MethodType invokedType,
                                       String samMethodName,
                                       MethodType samMethodType,
                                       MethodHandle implMethod,
                                       MethodType instantiatedMethodType,
                                       boolean isSerializable,
                                       Class<?>[] markerInterfaces,
                                       MethodType[] additionalBridges)
            throws LambdaConversionException {
        super(caller, invokedType, samMethodName, samMethodType,
              implMethod, instantiatedMethodType,
              isSerializable, markerInterfaces, additionalBridges);
        implMethodClassName = implDefiningClass.getName().replace('.', '/');
        implMethodName = implInfo.getName();
        implMethodDesc = implMethodType.toMethodDescriptorString();
        implMethodReturnClass = (implKind == MethodHandleInfo.REF_newInvokeSpecial)
                ? implDefiningClass
                : implMethodType.returnType();
        constructorType = invokedType.changeReturnType(Void.TYPE);
        // 代码一:生成lambda类对象名称
        lambdaClassName = targetClass.getName().replace('.', '/') + "$$Lambda$" + counter.incrementAndGet();
        // 代码二:字节码书写器,使用asm动态生成字节码
        cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        // 代码三:获取原匿名内部类名的参数个数
        int parameterCount = invokedType.parameterCount();
        if (parameterCount > 0) {
            argNames = new String[parameterCount];
            argDescs = new String[parameterCount];
            for (int i = 0; i < parameterCount; i++) {
                argNames[i] = "arg$" + (i + 1);
                argDescs[i] = BytecodeDescriptor.unparse(invokedType.parameterType(i));
            }
        } else {
            argNames = argDescs = EMPTY_STRING_ARRAY;
        }
    }
2.3.2.4.3 CallSite 的生成源码分析 - buildCallSite()

查看生成CallSite 的过程

CallSite buildCallSite() throws LambdaConversionException {
		// 代码一: 运行时期 动态生成类,这里断点,可以发现生成一个类名为:com.example.jun.LambdaClassTest$$Lambda$1/64830413
        final Class<?> innerClass = spinInnerClass();
        // 代码二:判断要实现的方法类型的属性个数,如果这个方法类型的属性为0,就放开实例化的权限,直接newInstance()即可
        if (invokedType.parameterCount() == 0) {
            final Constructor<?>[] ctrs = AccessController.doPrivileged(
                    new PrivilegedAction<Constructor<?>[]>() {
                @Override
                public Constructor<?>[] run() {
                    Constructor<?>[] ctrs = innerClass.getDeclaredConstructors();
                    if (ctrs.length == 1) {
                    	// 代码三:因为lambda 去实现内部类的构造器是私有的,之后我们要示例化,所以所以这里把访问权限放开
                        ctrs[0].setAccessible(true);
                    }
                    return ctrs;
                }
                    });
            if (ctrs.length != 1) {
                throw new LambdaConversionException("Expected one lambda constructor for "
                        + innerClass.getCanonicalName() + ", got " + ctrs.length);
            }

            try {
                // 代码四: 实例化
                Object inst = ctrs[0].newInstance();
				// 代码五:
                // MethodHandles.constant方法将这个内部类对象的信息组装并绑定到一个MethodHandle对象,作为ConstantCallSite的构造函数的参数“target”返回。后面对于Lambda表达式的调用,都会通过MethodHandle直接调用,不需再次生成CallSite.
                return new ConstantCallSite(MethodHandles.constant(samBase, inst));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception instantiating lambda object", e);
            }
        }
        // 代码六: 如果这个方法类型的属性不为0,无法直接实例化newInstance,这时候利用unsafe的来实例化。
         else {
            try {
                UNSAFE.ensureClassInitialized(innerClass);
                // 代码七:返回CallSite。
                return new ConstantCallSite(
                        MethodHandles.Lookup.IMPL_LOOKUP
                             .findStatic(innerClass, NAME_FACTORY, invokedType));
            }
            catch (ReflectiveOperationException e) {
                throw new LambdaConversionException("Exception finding constructor", e);
            }
        }
    }
2.3.2.4.4 asm生成匿名类源码分析 - spinInnerClass

解析com.example.jun.LambdaClassTest$$Lambda$1/64830413 类生成

// lambda需要实现的接口
final Class<?>[] markerInterfaces;   
// 返回的实列是否可以序列化
final boolean isSerializable;             
   
private Class<?> spinInnerClass() throws LambdaConversionException {
        String[] interfaces;
        // 代码一:获取实现的接口名字 java/util/Comparator
        String samIntf = samBase.getName().replace('.', '/');
        // isAssignableFrom: 确定此类对象表示的类或接口是否与指定的类参数表示的类或接口相同,或是其超类或超接口。
        // 查看samBase 是需要实现 isSerializable 
        boolean accidentallySerializable = !isSerializable && Serializable.class.isAssignableFrom(samBase);
        if (markerInterfaces.length == 0) {
        	// 代码二:接口集合
            interfaces = new String[]{samIntf};
        }
         else {
            // 代码二:如果又多个,会确保没有重复的接口
            Set<String> itfs = new LinkedHashSet<>(markerInterfaces.length + 1);
            itfs.add(samIntf);
            for (Class<?> markerInterface : markerInterfaces) {
                itfs.add(markerInterface.getName().replace('.', '/'));
                accidentallySerializable |= !isSerializable && Serializable.class.isAssignableFrom(markerInterface);
            }
            interfaces = itfs.toArray(new String[itfs.size()]);
        }
		
		// 代码三:写入基本的类信息字节码
		// 依次是 版本,访问权限,类名,签名(Signtura),父类名称,实现接口
        cw.visit(CLASSFILE_VERSION, ACC_SUPER + ACC_FINAL + ACC_SYNTHETIC,
                 lambdaClassName, null,
                 JAVA_LANG_OBJECT, interfaces);

        // 代码4:生成要由构造函数填写的最终字段。写入字段的字节码
        // 就是你要实现的匿名类,如果有字段的话。考虑场景抽象类
        for (int i = 0; i < argDescs.length; i++) {
            FieldVisitor fv = cw.visitField(ACC_PRIVATE + ACC_FINAL,
                                            argNames[i],
                                            argDescs[i],
                                            null, null);
            fv.visitEnd();
        }

		// 代码5:生成构造器的字节码
        generateConstructor();

		// 代码6:如果存在字段,就生成工厂方法
        if (invokedType.parameterCount() != 0) {
            generateFactory();
        }

        // 代码7 :提取sam方法,就是填充 你用lambda定义的匿名内部类要实现的类的定义的方法 的字节码。这边后面会展开讲解
        // 注: SAM代表单一抽象方法,SAM-type指的是Runnable,Callable等接口Ladda表达式是Java 8中的一个新特性,被认为是SAM类型,可以自由转换为它们
        // 代码7.1 :生成方法:访问权限,方法名,方法类型(参数,返回值),描述,签名(Signature)
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, samMethodName,
                                          samMethodType.toMethodDescriptorString(), null, null);
        // 代码7.2 生成注解                            
        mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
        // 代码7.3 生成最终的方法
        new ForwardingMethodGenerator(mv).generate(samMethodType);

        // 代码8: 生成桥方法(这里java/lang/invoke/LambdaMetafactory.metafactory 最开始的时候,这个值传的是null~)
        if (additionalBridges != null) {
            for (MethodType mt : additionalBridges) {
                mv = cw.visitMethod(ACC_PUBLIC|ACC_BRIDGE, samMethodName,
                                    mt.toMethodDescriptorString(), null, null);
                mv.visitAnnotation("Ljava/lang/invoke/LambdaForm$Hidden;", true);
                new ForwardingMethodGenerator(mv).generate(mt);
            }
        }

	
        if (isSerializable)
        	// 代码9:返回的实例如果支持序列化,就生成支持序列化的writeReplace方法
			// 备注:序列化中 writeReplace 有最高优先权
            generateSerializationFriendlyMethods();
        else if (accidentallySerializable)
       		// 代码10 : 如果要实现的匿名类要求序列化
       		// 生成不利于序列化的readObject/writeObject方法
            generateSerializationHostileMethods();

        cw.visitEnd();

 
		// 代码11:在此VM中定义生成的类
        final byte[] classBytes = cw.toByteArray();
        //代码12: 如果需要,转储到一个文件以进行调试
        // 这里后面会用到
        if (dumper != null) {
            AccessController.doPrivileged(new PrivilegedAction<Void>() {
                @Override
                public Void run() {
                    dumper.dumpClass(lambdaClassName, classBytes);
                    return null;
                }
            }, null,
            new FilePermission("<<ALL FILES>>", "read, write"),
            // createDirectories may need it
            new PropertyPermission("user.dir", "read"));
        }
		
		// 代码13:使用UnSaft 定义一个匿名类
        return UNSAFE.defineAnonymousClass(targetClass, classBytes, null);
    }

现在我们可以得出结论,在运行时期,lambda会通过invokedynamic 调用应用方法BSM,而BSM在生成CallSite中,会用使用asm为主类生成一个匿名类。上面我们过了一下具体生成的流程,现在我们来看下,运行中生成的匿名类是什么样子。

2.3.2.5 获取运行时期的asm生成的类

在上面的 代码12 dumper 处,只要dump 不为null,就可以输出asm生成的字节码文件

	// For dumping generated classes to disk, for debugging purposes
    private static final ProxyClassesDumper dumper;

    static {
    	// 这里可以看出,只要有指定参数 `jdk.internal.lambda.dumpProxyClasses` ,dump 就可以不为null
        final String key = "jdk.internal.lambda.dumpProxyClasses";
        String path = AccessController.doPrivileged(
                new GetPropertyAction(key), null,
                new PropertyPermission(key , "read"));
        dumper = (null == path) ? null : ProxyClassesDumper.getInstance(path);
    }
2.3.2.5.1 获取运行时期的asm生成的类 - idea 设置

idea设置如下(我这里是2021.02):
在这里插入图片描述
在这里插入图片描述

-Djdk.internal.lambda.dumpProxyClasses=C:\Users\zjw\Desktop\test

在这里插入图片描述
然后在运行,就可以发现在指定的路径下有输出啦~
在这里插入图片描述

2.3.2.5.2 获取运行时期的asm生成的类 - java命令行演示

如果不使用idea,用原始的java命令行,就是

// 如果不指定路径,默认是同级。注意包名和路径问题
java -Djdk.internal.lambda.dumpProxyClasses com.example.jun.LambdaClassTest

在这里插入图片描述

2.3.2.6 运行时期匿名类字节码解析

好啦,现在我们在看生成的字节码的方法。这里有2个方法,private com.example.jun.LambdaClassTest$$Lambda$1();public int compare(java.lang.Object, java.lang.Object);

  public int compare(java.lang.Object, java.lang.Object);
  	// 方法描述:(参数类型);(返回类型)
    descriptor: (Ljava/lang/Object;Ljava/lang/Object;)I
    // 访问标志:public
    flags: ACC_PUBLIC
    // 字节码,方法体
    Code:
      stack=2, locals=3, args_size=3
      	// 将一个局部变量a1 加载到操作栈
         0: aload_1
        // 检查类实例类型的指令
         1: checkcast     #15                 // class java/lang/String
        // 将一个局部变量a2 加载到操作栈
         4: aload_2
        // 检查类实例类型的指令
         5: checkcast     #15                 // class java/lang/String
        // 代码一:调用staic方法,这里调用的方法就是主类编译完后,新增的方法。
         8: invokestatic  #21                 // Method com/example/jun/LambdaClassTest.lambda$main$0:(Ljava/lang/String;Ljava/lang/String;)I
         // 返回
        11: ireturn
    RuntimeVisibleAnnotations:
      0: #13()
2.3.2.7 运行时期匿名类源码解析

现在我们来直观的看下生成的内容。这里 Luyten 或者idea,都可以直接解析class文件 。解析完后,分别如下。
在这里插入图片描述
在这里插入图片描述

2.3.2.8 运行时期匿名类的泛型处理

这里我们可以发现,① 字节码中并没有发现 Signature 这个属性 ②而源码中
类定义也是 final class LambdaClassTest$$Lambda$1 implements Comparator 这里并没有指定接口 Comparator<T> 中的T 类型。
在没有指定泛型类型的情况下,T 默认是 Object ,同时将作为普通的类。这也解释了为什么没有Signature Attribute了。类型擦拭
在这里插入图片描述

2.3.2.9 【小节,有图】lambda 表达式实现总结

自此我们可以得出lambda表达式的生成的过程。在编译时期,主类主要会根据lambda的方法体生成一个私有静态方法,在调用lambda表达式的jvm指令是invokedynamic,这个命令会在运行的时候动态调用应用层面的方法(BSM)。而在运行时期,lambda的引导方法,会生成一个匿名内部类,这个内部类的方法里面调用的是之前编译时期主类生成的私有静态方法。

在这里插入图片描述

2.3.3 匿名内部类 和 lambda表达式生成运行的区别

时期区别匿名内部类lambda表达式
编译期文件个数① 主类字节码
② `$[数字]` 结尾的内部类的字节码
① 主类字节码
编译说明① 主类正常编译 ② 匿名内部类如果有泛型会合成桥方法 ③ 匿名内部类如果指定泛型,会在Signature记录泛型信息① 主类会以lambda的方法体生成一个私有静态方法。参数返回类型以实际泛型为准 ②主类调用lambda使用的是invokedynamic指令
运行时期文件个数 ① 会利用asm动态生成一个新的字节码(匿名内部类),指定jvm参数可输出到文件系统 ② 生成的匿名内部类并没有指定泛型,默认使用Object
运行情况 正常调用invoke指令 invokedynamic 调用BSM(引导方法),最后返回CallSite。

2.4 ResolvableType 获取泛型解析

回到一开始 在2.2,我们直观看出是因为 ResolvableType 获取类的泛型类型, 匿名表达式可以获取 ,而lambda的时候无法获取。

  • ResolvableType是对Java类型的封装,提供了对父类(getSuperType())、接口(getInterfaces())、泛型参数(getGeneric(int…))的访问,最底层的实现为resolve()方法。
  • ResolvableType可以通过forField(Field)、forMethodParameter(Method, int)、forMethodReturnType(Method)和forClass(Class)进行构造。

这里我们写个例子,分析下。

public class ResolveTypeTest {

    public static void main(String[] args) {
        Comparator<String> lambdaComparator
                = (o1, o2) -> o2.charAt(1) - o1.charAt(1);

        ResolvableType resolvableType1 = ResolvableType.forClass(lambdaComparator.getClass()).as(Comparator.class);
        // 代码一:java.util.Comparator<?>
        System.out.println(resolvableType1);
        ResolvableType[] generics1 = resolvableType1.getGenerics();
        for (ResolvableType generic : generics1) {
            // 代码二:null
            System.out.println(generic.resolve());
        }

        Comparator<String> anonymousInnerClassComparator = new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.charAt(1) - o1.charAt(1);
            }
        };
        
        ResolvableType resolvableType2 = ResolvableType.forClass(anonymousInnerClassComparator.getClass()).as(Comparator.class);
        // 代码三:java.util.Comparator<java.lang.String> 
        System.out.println(resolvableType2);
        ResolvableType[] generics2 = resolvableType2.getGenerics();
        for (ResolvableType generic : generics2) {
            // 代码四:class java.lang.String
            System.out.println(generic.resolve());
        }

    }
}

在这里我们可以看到 代码一 和 代码三 处对于泛型的解析结果不同。

2.4.1 ResolvableType源码解析

查看ResolveType相关源码解析。

ResolvableType.forClass(Class<?> clazz) - 指定clazz 为要解析的类型
as(Class<?> type) - 将type作为指定类的可解析类型返回(搜索超类型和接口层次结构以查找匹配项)
而在 as(Class<?> type) 中 ,最终是调用 getGenericInterfaces (代码1~代码4)

	//没有可用值时返回的ResolvableType。NONE优先于null使用,因此可以安全地链接多个方法调用。
	public static final ResolvableType NONE = new ResolvableType(EmptyType.INSTANCE, null, null, 0);

	// 类型
	private final Type type;
	// type的Class类型
    private Class<?> resolved;
   
	// 返回指定类的ResolvableType,使用完整的泛型类型信息进行可分配性检查。
	 public static ResolvableType forClass(Class<?> clazz) {
        return new ResolvableType(clazz);
    }
    
	private ResolvableType(Class<?> clazz) {
		// 未指定具体Class,则解析Object
        this.resolved = clazz != null ? clazz : Object.class;
        this.type = this.resolved;
        this.typeProvider = null;
        this.variableResolver = null;
        this.componentType = null;
        this.hash = null;
    }
	
	// 将此类型解析为类,如果无法解析该类型,则返回null。如果直接分解失败,该方法将考虑类型变量和WildcardTypes的边界;然而,Object对象的边界将被忽略。
	public Class<?> resolve() {
        return this.resolved;
	}

	// 代码1:将此类型作为指定类的可解析类型返回。搜索超类型和接口层次结构以查找匹配项,如果此类型未实现或扩展指定的类,则返回NONE。
	public ResolvableType as(Class<?> type) {
		if (this == NONE) {
			return NONE;
		}
		Class<?> resolved = resolve();
		if (resolved == null || resolved == type) {
			return this;
		}
	
	   // 代码2:获取当前要解析的class的直接的超类型和接口
		for (ResolvableType interfaceType : getInterfaces()) {
			// 搜索超类型和接口层次结构以查找匹配项
			ResolvableType interfaceAsType = interfaceType.as(type);
			if (interfaceAsType != NONE) {
			   // 存在则返回
				return interfaceAsType;
			}
		}
		// 代码3:查看父类是否存在相关接口
		return getSuperType().as(type);
	}
    
    // 返回一个ResolvableType表示此类型实现的直接接口的数组。如果此类型未实现任何接口,则返回一个空数组。
    public ResolvableType[] getInterfaces() {
		Class<?> resolved = resolve();
		if (resolved == null) {
			return EMPTY_TYPES_ARRAY;
		}
		ResolvableType[] interfaces = this.interfaces;
		if (interfaces == null) {
			// 代码4:返回表示由该类对象表示的类或接口直接实现的接口的类型。
			Type[] genericIfcs = resolved.getGenericInterfaces();
			interfaces = new ResolvableType[genericIfcs.length];
			for (int i = 0; i < genericIfcs.length; i++) {
				interfaces[i] = forType(genericIfcs[i], this);
			}
			this.interfaces = interfaces;
		}
		return interfaces;
	}

2.4.2 getGenericInterfaces 获取泛型信息

getGenericInterfaces,是其实就是获取存储在class文件的Signature Attribute泛型信息。

getGenericInterfaces

  • 如果superinterface是参数化类型,则为其返回的type对象必须准确反映源代码中使用的实际类型参数。如果以前没有创建过表示每个超级接口的参数化类型,则会创建该类型。有关参数化类型的创建过程的语义,请参见参数化类型的声明。
  • 如果这个类对象代表一个类,那么返回值是一个数组,其中包含代表该类直接实现的所有接口的对象。数组中接口对象的顺序与该类对象所表示的类的声明的implements子句中接口名称的顺序相对应。
  • 如果这个类对象代表一个接口,那么数组包含代表该接口直接扩展的所有接口的对象。数组中接口对象的顺序与该类对象表示的接口声明的extends子句中接口名称的顺序相对应。
  • 如果该类对象表示不实现接口的类或接口,则该方法返回长度为0的数组。
  • 如果该类对象表示基元类型或void,则该方法返回长度为0的数组。
    0 如果这个类对象表示数组类型,那么接口Cloneable和java。伊奥。Serializable按该顺序返回。

2.4.3 小节

由此,我们得出 ① 匿名表达式编译过程中,匿名内部类实现是implement Comparator<String> ,在泛型擦拭过程中,类字节码中 Siganture 是保留了相关的泛型信息 ② 在lambda 表达式的类实现是implement Comparator,未明确具体的泛型。最终生成的只是只包含普通的类、接口和方法。Signature并没有相关的泛型信息。
就是说:lambda生成的类不存储通用签名,所以你不能使用通常的反射方法,让你绕过擦除,所以lambda的此处,并无法获取泛型信息。

3 总结

springMvn 转换器的默认实现 GenericConversionService 中在注册添加转换器的时候,通过ResolveType获取转换器的泛型类型,最后是调用getGenericInterfaces
而lambda表达式在运行期生成的匿名类并不不存储通用签名(Signature Attribute),无所获取泛型类型。
而匿名内部类在编译器的生成的匿名类是包含通用签名(Signature Attribute),可以获取。
所以这里是没办法替换的~

4 扩展

4.1 lambda表达式如何获取泛型

这边提供有2种思路
第一种,写个辅助工具类,然后传入泛型类型。然后通过获取辅助类的泛型类型。
第二种,是我们通过查看lambda生成的字节码,可以看出在调用静态方法的时候,那么我们其实可以通过解析函数式接口的方法签名中使用的类型参数。这里我发现一个工具 typetools
在这里插入图片描述
简单上个demo

// 可点击源码解读,思路就是解析invokestatic methodRef中methodRef的参数类型。
public class TypeToolsTest {
    public static void main(String[] args) {
        Comparator<String> comparator
                = (o1, o2) -> o2.charAt(1) - o1.charAt(1);
        Class<?>[] typeArgs = TypeResolver.resolveRawArguments(Comparator.class, comparator.getClass());
        for (Class<?> typeArg : typeArgs) {
            System.out.println(typeArg);
        }
    }
}

5 附录

5.2 springMvc类型转换Convert

在这里实现功能,就是利用Convert简单的将前端穿过来的string字符串去除前后的空格。

// spring 提供了Convert接口,可定义转换器
// S为源对象 T为目标对象
public interface Converter<S, T> {
	// 将S源对象转为T目标对象
    T convert(S source);
}
// spring 提供了ConversionService 接口,可定义转换器的调用逻辑
public interface ConversionService {
    // 用于判断当前的转化的服务能否进行转换
    boolean canConvert(Class<?> sourceType, Class<?> targetType);
    boolean canConvert(TypeDescriptor sourceType, TypeDescriptor targetType);
    // 转换
    <T> T convert(Object source, Class<T> targetType);
    Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType);
}
// spring 提供了ConversionService 接口,可定义转换器的注册
public interface ConverterRegistry { 
    void addConverter(Converter<?, ?> converter);
    void addConverter(GenericConverter converter);
    void addConverterFactory(ConverterFactory<?, ?> converterFactory);
    void removeConvertible(Class<?> sourceType, Class<?> targetType)
}

有了这3个基础的接口,分别用于转化器的定义,注册,调用。便可以任意自定义。

// spring 提供了一个基本的实现 - GenericConversionService,实现了转换器的注册和调用逻辑

public interface ConfigurableConversionService extends ConversionService, ConverterRegistry {
}

public class GenericConversionService implements ConfigurableConversionService{
// 。。。
}

5.2 泛型之Type 与 ResolvableType

5.2.1 Type

Type 是 java 中用于表示 类型的体系。可用于获取不同类型的泛型。

这里推荐看 Java中的Type类型详解

5.2.1.1 Class

Class(原始/基本类型,也叫raw type):不仅仅包含我们平常所指的类、枚举、数组(String[]、byte[])、注解,还包括基本类型(原始类型(primitive type 虚拟机创建 8种int 、float加void)对应的包装引用类型Integer、Float等等

5.2.1.2 ParameterizedType

ParameterizedType 表示参数化类型,例如 Collection<String>.
简单理解为形式上带<>。(List 不是ParameterizedType,List<String> 是ParameterizedType)

5.2.1.3 GenericArrayType

GenericArrayType: 泛型数组,组件类型是参数化类型或类型变量.比如List[]T[]

5.2.1.4 TypeVariable

TypeVariable:类型变量,各种类型变量的通用超级接口。形式上<>里面的类型。就是List< T>、Map< K,V>中的T,K,V等值

5.2.1.5 WildcardType

WildcardType:泛型表达式,表示的仅仅是类似 ? extends T? super K这样的通配符表达式。 ?—通配符表达式,表示通配符泛型;例如:List< ? extends Number>List< ? super Integer>

5.2.2 ResolvableType

ResolvableType 是spring core 是为所有的java类型提供了统一的数据结构以及API ,通过ResolvableType对象获取类型携带的信息。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值