spring 泛型处理

java 泛型基础

  • 泛型类型:泛型类型是在类型上参数化的泛型类或接口

  • 泛型使用场景

    • 编译时前类型检查。定义为 Collection 类型的集合,add int时会有编译错误。
    • 避免类型强转。如果只是定义 Collection,我们是不知道集合保存的是什么类型的,即便知道,在get之后也需要强制转换成需要的类型。
    • 实现通用算法。如通用的二分查找,排序。
  • 泛型类型擦写

    • java引入java,一遍在编译时提供更严格的类型检查并支持泛型变成。类型擦除是为了不会为参数化类型创建新的类。因此,泛型不会产生运行时开销。而CGLIB这种动态代理技术,会在运行时修改字节码,会产生一些运行时的消耗。为了实现泛型,编译器将类型擦除应用于:
      • 将泛型类型中的所有类型参数替换为其边界,如果类型参数无边界,则将其替换为 Object。因此生成的字节码只包含普通类、接口和方法
      • 必要时插入类型转换以保持类型安全
      • 生成桥接方法以保留泛型类型中的多态性。

以最常用的Collection为例看下泛型的例子

Collection list = new ArrayList<>();
list.add("hello");
// 编译错误
//    list.add(1);
// 泛型擦除
Collection temp = list;
// 编译通过
temp.add(1);
System.out.println(list);
// ClassCastException
list.forEach(System.out::println);

从字节码看泛型

Java 泛型是在编译时实现的,jvm是没有泛型的概念的,也就是说泛型是java语言的特性。既然 jvm 没有泛型的特性,要让编译后的 .class 支持 Java 语言的泛型,字节码就是二者之间的桥梁。

public class Generic {

    private T data;

    public T get() {
        return data;
    }

    public void set(T data) {
        this.data = data;
    }
}

从生成的字节码中可以看到,泛型 T 已经被擦除了:

()V
getData()Ljava/lang/Object;
setData(Ljava/lang/Object;)V

类型擦除与多态

GenericB 继承 Generic 并重写 getData、setData,从 Generic 的字节码知道参数类型是 Object,而 GenericB 定义的是 Number,正常应该是不能重写的,但是编译和运行时没有错误的。jvm为了解决这个问题,引入了桥接方法

public class GenericB extends Generic {

  private Number n;

  @Override
  public Number getData() {
    return this.n;
  }

  @Override
  public void setData(Number data) {
    this.n = data;
  }
}

字节码如下,我只举例 getData 方法。编译器自动生成了“java.lang.Object getData()”方法,在方法里调用“java.lang.Number getData()”方法。

byte code
  public java.lang.Number getData();
    descriptor: ()Ljava/lang/Number;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field n:Ljava/lang/Number;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lgeneric/GenericB;

  public java.lang.Object getData();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #5                  // Method getData:()Ljava/lang/Number;
         4: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lgeneric/GenericB;

再来看下没有写泛型的继承是如何实现的,GenericC继承自 Generic ,但是没有指定泛型类型。

public class GenericC extends Generic {

    private Number n;

    @Override
    public Number getData() {
        return this.n;
    }

    public void setData(Number data) {
        this.n = data;
    }
}

生成的字节码如下。父类中的方法为 getData()Object 和 setData(Object),GenericC 类中的 setData(Number) 与父类 set(Object) 方法参数不同,所以是是重载。我们知道,只有返回值不同不满足重载条件,所以对 GenericC 类的 getData()Number 方法来说,应该算是对父类方法 getData()T 的重写。
编译期自动生成了 getDate() Object 桥接方法来重写父类方法。我们发现字节码里存在了两个只有返回值类型不同的同名方法,不符合java语法。这是因为java和jvm方法签名定义不同:

  • Java 方法签名 = 方法名 + 参数类型 + 参数顺序
  • JVM 方法签名 = 方法名 + 参数类型 + 参数顺序 + 返回值类型 + 可能抛出的异常

在java反射的api里,Method有一个方法是 isBridge 就是用来判断是否桥接方法的。

{
  private java.lang.Number n;
    descriptor: Ljava/lang/Number;
    flags: ACC_PRIVATE

  public generic.GenericC();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method generic/Generic."":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lgeneric/GenericC;

  public java.lang.Number getData();
    descriptor: ()Ljava/lang/Number;
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield      #2                  // Field n:Ljava/lang/Number;
         4: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lgeneric/GenericC;

  public void setData(java.lang.Number);
    descriptor: (Ljava/lang/Number;)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: aload_1
         2: putfield      #2                  // Field n:Ljava/lang/Number;
         5: return
      LineNumberTable:
        line 13: 0
        line 14: 5
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       6     0  this   Lgeneric/GenericC;
            0       6     1  data   Ljava/lang/Number;

  public java.lang.Object getData();
    descriptor: ()Ljava/lang/Object;
    flags: ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokevirtual #3                  // Method getData:()Ljava/lang/Number;
         4: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lgeneric/GenericC;
}

java 5 类型接口

  • java 5 类型接口 - java.lang.reflect.Type
类或接口 说明
java.lang.Class Java 类API,如java.lang.Integer
java.lang.reflect.GenericArrayType 泛型数组类型,比如 T[]
java.lang.reflect.ParameterizedType 泛型参数类型
java.lang.reflect.TypeVariable 泛型类型变量,如Collection 中的E
java.lang.reflect.WildcardType 泛型通配类型
  • java 泛型反射API
类型 API
泛型信息 Generics Info java.lang.Class#getGenericInfo()
泛型参数 Parameters java.lang.reflect.ParameterizedType
泛型父类 super classes java.lang.Class#getGenericSuperclasses()
泛型接口 interfaces java.lang.Class#getGenericInterfaces()
泛型生命 generics Declaration java.lang.reflect.GenericDeclaration
// 原生类型 primitive types : int long float
Class intClass = int.class;
System.out.println("intClass = " + intClass);

// 数组类型 array types : int[],Object[]
Class objectArrayClass = Object[].class;
System.out.println("objectArrayClass = " + objectArrayClass);

// 原始类型 raw types : java.lang.Integer
Class IntegerRawClass = Integer.class;
System.out.println("IntegerRawClass = " + IntegerRawClass);

System.out.println("ArrayList.class = " + ArrayList.class);
// 泛型参数类型 parameterized type
ParameterizedType arrayListParameterizedType = (ParameterizedType) ArrayList.class
    .getGenericSuperclass();
System.out.println("ArrayListParameterizedType = " + arrayListParameterizedType);
System.out.println(
    "arrayListParameterizedType.getRawType() = " + arrayListParameterizedType.getRawType());

System.out.println("泛型类型变量 Type Variable");
// 
Type[] typeVariables = arrayListParameterizedType.getActualTypeArguments();
Stream.of(typeVariables)
    .map(TypeVariable.class::cast) // Type -> TypeVariable
    .forEach(System.out::println);

spring 4.2 泛型优化实现与局限性- ResolvableType

  • 核心 API org.springframework.core.ResolvableType
    • 工厂方法:forXX 方法
    • 转换方法:asXX 方法
    • 处理方法:resolveXX 方法
    • GenericTypeResolver, GenericCollectionTypeResolver 替代者

ResolvableType 是一个 immutable 设计,通过forXX, asXX 返回一个新的 ResolvableType 实例,然后通过 resolveXX,getXX 等方法获取类型信息。

ResolvableType 设计的优点

  • 简化Type API 开发;
  • 不变设计,线程安全
  • Fluent API 设计,builder模式,链式编程。

demo:

private HashMap> myMap;

public static void main(String[] args) throws NoSuchFieldException {

    ResolvableType t = ResolvableType.forField(ResolvableTypeDemo.class.getDeclaredField("myMap"));
    t.getSuperType(); // AbstractMap>
    t.asMap(); // Map>
    t.getGeneric(0).resolve(); // Integer
    t.getGeneric(1).resolve(); // List
    t.getGeneric(1); // List
    t.resolveGeneric(1, 0); // String
    t.resolveGeneric(1, 1); // null
}    

一个案例

举一个例子,我们在定义一个支持泛型接口时,可以用不同的泛型类实现,当注入的时候指定具体的泛型类,spring 就会把具体类型的实现注入到 bean 中,这个过程就涉及到泛型的处理。代码如下:

spring 版本 5.2.7。


/**
 * 泛型注入 demo
 */
public class GenericIocDemo {
  @Bean
  public UserMonitor userMonitor() {
    return new UserMonitor();
  }

  @Bean
  public PersonMonitor personMonitor() {
    return new PersonMonitor();
  }

  private List> users;

  @Autowired
  public void setUsers(List> users) {
    this.users = users;
  }

  @PostConstruct
  public void init() {
    // 注入的对象是 userMonitor
    users.forEach(System.out::println);
  }

  public static void main(String[] args) {

    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    context.register(GenericIocDemo.class);
    context.refresh();
    context.close();
  }

  public static class UserMonitor implements Consumer {
    @Override
    public void accept(User user) {
    }
  }

  public static class PersonMonitor implements Consumer {
    @Override
    public void accept(Person user) {
    }
  }
}

demo 中注入 private List> users,只会注入 UserMonitor 类型的 bean,这个过程涉及到一定涉及到泛型的判断,判断都是通过 ResolvableType 实现的,可以一窥 spring 的使用场景。下面是 spring 处理的主要流程。

image

通过 descriptorResolvableType 只获取了集合泛型,并没有获取泛型的泛型。这是为了在下一步获取候选 bean 做准备。因为 spring 不能通过泛型类型获取 bean,所以这里获取到 Consumer即可

image

因为 requiredType 是 Consumer 接口,所以获取到两个候选 bean。

image

查看这部分代码的时候参照调用栈。第一行获取 实际的泛型类型,方法的最后会用来判断候选对象是否匹配(包括泛型)。

image

在比较之前,会获取候选 bean 的类型。descriptor.getDependencyType() 获取没有泛型的类型,然后和实际类型比较是否匹配,如果匹配则返回。

image
  1. 比较类型是否匹配,包括泛型
  2. 因为泛型不匹配。所以结果是false,不会添加注入的集合中。

总结

简单介绍了java语言和jvm对泛型的支持方式。

java 的泛型擦写是在运行时。其实从字节码的角度看并不存在擦写,因为编译后全部都是Object,擦写描述的其实是java代码编写的不规范,欺骗了编译器,从而导致运行时错误。

spring框架封装的 ResolvableType 实现原理。ResolvableType 可以在我们的项目中直接使用,api设计的也很友好。

使用框架,了解原理,事半功倍。

参考

  • The Java™ Tutorials: generics
  • Java Generics - Classes

你可能感兴趣的:(spring 泛型处理)