【开源项目】springfox-bridge采用多维递归结合javassist生成泛型代理类(完美解决泛型擦除问题)

springfox-bridge项目中,关于泛型问题,采用3个递归模型,动态生成泛型类的代理类,解决泛型擦除问题。

springfox-bridge项目地址: https://github.com/double-bin/springfox-bridge

一、引言

1.1 问题引出

    在springfox-bridge:随心所欲地为非restful接口生成API文档一文中,介绍了springfox-bridge的特性和使用示例,springfox-bridge可以高效快速的为非restful接口创建接口文档,大大简化开发者编写接口文档的时间。

    在springfox-bridge项目中,底层采用了javassist进行了类的“代理”,将非restful接口结合springfox-bridge提供的注解进行动态解析,并生成符合restful规范的“代理接口类”。

    然而我们来看这样形式的接口方法queryPersonById:

    @BridgeOperation(value = "查询用户信息", notes = "根据id查询用户信息")
    public CommonResponse queryPersonById(@BridgeModelProperty(value = "用户id", required = true) Long id){
        ... //此处省略
    }

    其中CommonResponse和Person的定义:

@Data
public class CommonResponse {
    
    @ApiModelProperty("是否成功")
    private Boolean success;

    @ApiModelProperty("结果码")
    private Integer code;
    
    @ApiModelProperty("描述信息")
    private String message;
    
    @ApiModelProperty("响应数据")
    private T data;
}

@Data
public class Person {
    
    @ApiModelProperty("用户id")
    private Long id;

    @ApiModelProperty("姓名")
    private String name;

}

    如果不对返回类型CommonResponse做特殊处理,用method.getReturnType()作为新的代理接口方法的返回值,那么显然,返回类型中无法附带Person的信息,相应的界面如下:

【开源项目】springfox-bridge采用多维递归结合javassist生成泛型代理类(完美解决泛型擦除问题)_第1张图片
image.png

显而易见,data类型被“翻译”成Object而非Person。

1.2 javassist不支持泛型的问题

    然而,如果尝试创建上述接口的代理并设置代理方法的返回值类型为CommonResponse也是不可行的,可以看看javassist documentation关于泛型的说明:

The generics of Java is implemented by the erasure technique. After compilation, all type parameters are dropped off. For example, suppose that your source code declares a parameterized type Vector:

Vector v = new Vector();
  :
String s = v.get(0);
The compiled bytecode is equivalent to the following code:

Vector v = new Vector();
  :
String s = (String)v.get(0);
So when you write a bytecode transformer, you can just drop off all type parameters. Because the compiler embedded in Javassist does not support generics, you must insert an explicit type cast at the caller site if the source code is compiled by Javassist, for example, through CtMethod.make(). No type cast is necessary if the source code is compiled by a normal Java compiler such as javac.

    javassit对泛型的支持不甚友好,即使指定了泛型,javassit还是会将泛型擦除,而且在运行中也可能会抛出各种异常。

    更复杂的情况还有:

  • 接口方法返回值泛型嵌套:CommonResponse>

  • 泛型类继承泛型类型或实现泛型接口:

public class CommonResponseSub extends CommonResponse 
  • 泛型类内部属性/getter方法返回值的泛型嵌套,如:
@Data
public class TestGeneric {
    private TT data;

    private BB name;

    private CC desc;

    private CommonResponseSub,String> subData;
}

    以上列举的复杂泛型类型在springfox-bridge中生成代理时是一个技术难点,涉及:继承、泛型嵌套等,需要妥善的设计来生成代理类,并完成原有接口到代理restful接口的桥接。

二、多维递归生成复杂泛型的代理

2.1 springfox-bridge针对泛型采用的多维递归方法

2.1.1 递归模型

    实际可能出现的情况可能如1.2中描述的比较复杂:接口方法返回的泛型类型中存在泛型嵌套、泛型类型存在继承泛型类型或实现泛型接口、泛型类型的属性或getter返回值为泛型类型,针对这些场景,springfox-bridge抽象出对应的递归模型来解决这一问题:

  1. 指定泛型映射时,映射的类型也是泛型的递归

        如:CommonResponse>, CommonResponse的泛型映射T:List, List的泛型映射:E:Person, 由于Person没有泛型映射了,即达到了递归终止条件

  2. 泛型类继承泛型类、实现泛型接口的递归
        如:

    public class CommonResponseSub extends CommonResponse {
        ...//省略
    }
    

    此时存在泛型的传递,CommonResponseSub的B -> CommonResponse的T, 如果指定类型CommonResponseSub, 那么, CommonResponseSub的泛型映射为 “A:String, B:Boolean, C:Integer”, 由于泛型传递,父类CommonResponse的泛型映射为“T:Boolean”。由于父类CommonResponse没有继承其它泛型类或实现泛型接口,则递归终止。

  3. 泛型类内部属性/getter方法返回值的泛型递归
        如:

    @Data
    public class TestGeneric {
        private TT data;
    
        private BB name;
    
        private CC desc;
    
        private CommonResponseSub,String> subData;
    }
    

        此时针对TestGeneric的属性subData的CommonResponseSub类型存在泛型传递,如果指定TestGeneric,那么TestGeneric的泛型映射为 “TT:String, BB:Boolean, CC:Integer”, 而属性subData的类型CommonResponseSub的泛型映射为"A:Boolean, B:List, C:String",CommonResponseSub的父类CommonResponse存在泛型映射“T:List”, 而List又存在泛型映射“E:String”,总结得出:当泛型映射从类传递到属性或getter方法时,继续从模型1和模型2的维度递归映射。

2.1.2 springfox-bridge泛型代理源码分析

    获取接口方法的java.lang.Method对象,通过getGenericReturnType()可以返回方法返回类型的java.lang.reflect.Type对象,Type对象有几个关键的子类:

  1. java.lang.reflect.ParameterizedType : 代表返回类型具有泛型映射,如CommonResponse

  2. java.lang.reflect.GenericArrayType : 代表返回类型为具有泛型映射的数组,如CommonResponse[] ;

  3. java.lang.reflect.TypeVariable : 代表返回类型为一个泛型的定义,如CommonResponse的getData的返回类型为T ;

  4. java.lang.Class : 代表返回类型为一个具体的类(类的数组),不存在泛型传递。

    上述类型中,1、2、3由于存在递归,需要巧妙地处理来递归生成代理类。springfox-bridge底层定义两个model用来存储泛型信息:

  1. GenericInfo

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class GenericInfo {
    
        private Class clazz;
    
        /**
         *
         * four types:1: string,generic name; 2,Class: concrete class (including array class); 3,GenericInfo: GenericInfo object; 4,GenericArrayInfo: GenericArrayInfo object
         */
        private List features;
    }
    

        GenericInfo用来对标泛型类,其属性clazz代表泛型类,features代表类泛型映射的特性,features的元素存在4中类型:

      1. String字符串:代表泛型定义 (如CommonResponse中的T)
      1. Class:代表类 (如CommonResponse中的String.class)
      1. GenericInfo代表嵌套泛型类 (如TestGeneric, Boolean, Integer>中用CommonResponse生成GenericInfo对象)
      1. GenericArrayInfo代表嵌套泛型类数组(如TestGeneric[], Boolean, Integer>中用CommonResponse[]生成GenericArrayInfo对象)。

        如果类有多个泛型参数,将定义的泛型参数映射依次转换到features数组中,如TestGeneric, CommonResponse[], Integer>生成的GenericInfo对象的clazz属性为TestGeneric.class, 属性features数组依次为:GenericInfo对象、GenericArrayInfo对象、Class对象。

  2. GenericArrayInfo

    @Getter
    @Setter
    @NoArgsConstructor
    @AllArgsConstructor
    public class GenericArrayInfo {
        /**
         *
         * four types:1: string,generic name; 2,Class: concrete class; 3,GenericInfo: GenericInfo object; 4,GenericArrayInfo: GenericArrayInfo object
         */
        private Object info;
    }
    

    GenericArrayInfo用来对标泛型数组,其只有一个属性info,可能存在值的类型跟GenericInfo的features数组元素类型相同。

    通过GenericInfo和GenericArrayInfo可以用来解析存储前述递归模型,特别的,对每个GenericInfo,通过前置的genericClassMap(Map),可以解析生成GenericInfo对应的clazz的genericClassMap,以此进行递归处理。

    我们来看部分源码:

       /**
        * 生成GenericInfo对象所属class的代理类
       **/
        public static Class buildGenericClass(GenericInfo genericInfo, Map genericClassMap) {
    
        Class oldReturnClass = genericInfo.getClazz();
    
        if(ClassUtils.isAssignable(oldReturnClass, Map.class)) {
            return Map.class;
        }
    
        Map newGenericClassMap = getGenericClassMap(genericClassMap, genericInfo);
    
        if (ClassUtils.isAssignable(oldReturnClass, Collection.class)) {
            if (1 == newGenericClassMap.size()) {
    
                Class clazz = null;
                for (String key : newGenericClassMap.keySet()) {
                    clazz = newGenericClassMap.get(key);
                    break;
                }
                return ReflectUtil.getArrayClass(clazz);
            }
        }
        return buildClass(oldReturnClass, newGenericClassMap);
    }
    

    上述buildGenericClass方法生成GenericInfo对象所属class的代理类,方法最后调用的buildClass方法:

```
public static Class buildClass(Class genericClass, Map genericClassMap) {

    try {
        String featureName = getFeatureName(genericClass, genericClassMap);
        if (classMap.containsKey(featureName)) {
            return classMap.get(featureName);
        }

        String newReplaceClassName = BridgeClassNameBuilder.buildNewReplaceClassName(genericClass.getSimpleName());
        CtClass newReturnCtClass = pool.makeClass(newReplaceClassName);

        Map> fieldTuples = buildNewPropertyInfos(genericClass);

        buildfieldTuples(fieldTuples, genericClassMap, genericClass);

        for (String fieldName : fieldTuples.keySet()) {
            Class fieldClass = fieldTuples.get(fieldName).getFst();
            ApiModelProperty apiModelProperty = fieldTuples.get(fieldName).getSnd();
            buildFieldInfo(fieldName, fieldClass, apiModelProperty, newReturnCtClass);
        }

        ConstPool constpool = newReturnCtClass.getClassFile().getConstPool();
        Annotation apiModelAnnotation = new Annotation(ApiModel.class.getName(), constpool);

        apiModelAnnotation.addMemberValue("value", new StringMemberValue(newReplaceClassName, constpool));
        JavassistUtil.addAnnotationForCtClass(newReturnCtClass, apiModelAnnotation);
        newReturnCtClass.writeFile(SpringfoxBridge.getBridgeClassFilePath());

        Class newReplaceClass = newReturnCtClass.toClass();

        classMap.put(featureName, newReplaceClass);

        return newReplaceClass;
    } catch (Exception e) {
        log.error("Build class failed for generic class : {}.", genericClass.getName(), e);
        throw new BridgeException("Build class failed for " + genericClass.getName(), e);
    }

}
```

    buildClass方法为genericClass泛型类生成代理类的方法,其第二个参数genericClassMap为genericClass的泛型定义与实际类(如果存在泛型嵌套的,其值为已经递归解析出代理类,对应前述的递归模型1)的映射, 方法中调用的buildfieldTuples为该类的所有java bean(符合java bean规范的)在代理类中生成同样的java bean:

```
    private static void buildfieldTuples(Map> fieldTuples,
                              Map genericClassMap, Class oldReturnClass) {

    if (!CollectionUtils.isEmpty(fieldTuples)) {
        for (String fieldName : fieldTuples.keySet()) {
            Tuple3 tuple3 = fieldTuples.get(fieldName);
            Method readMethod = ReflectUtil.getDeclaredMethod(oldReturnClass, tuple3.getTrd().getName());
            Field field = ReflectUtil.getDeclaredField(oldReturnClass, fieldName);
            if (null == readMethod && null == field) {
                continue;
            }

            if (null == tuple3.getFst() && null != readMethod) {
                Type genericReturnType = readMethod.getGenericReturnType();
                if (genericReturnType instanceof ParameterizedType) {
                    ParameterizedType tempParameterizedType = (ParameterizedType)genericReturnType;
                    GenericInfo genericInfo = getGenericInfo(tempParameterizedType);
                    tuple3.setFst(buildGenericClass(genericInfo, genericClassMap));
                } else if (genericReturnType instanceof GenericArrayType) {
                    GenericArrayInfo genericArrayInfo = getGenericArrayInfo((GenericArrayType)genericReturnType);
                    tuple3.setFst(buildGenericArrayClass(genericArrayInfo, genericClassMap));
                } else if (genericReturnType instanceof TypeVariable) {
                    String name = ((TypeVariable)genericReturnType).getName();
                    Class clazz = null == genericClassMap ? Object.class : genericClassMap.get(name);
                    clazz = null == clazz ? Object.class : clazz;
                    tuple3.setFst(clazz);
                } else if (genericReturnType instanceof Class) {
                    tuple3.setFst((Class)genericReturnType);
                }
            }
            if (tuple3.getSnd() == null) {
                ApiModelProperty apiModelProperty = null == readMethod? null : ReflectUtil.getAnnotation(readMethod, ApiModelProperty.class);
                if (null == apiModelProperty) {
                    apiModelProperty = null == field? null : ReflectUtil.getAnnotation(field, ApiModelProperty.class);
                }
                tuple3.setSnd(apiModelProperty);
            }
        }
    }

    Class superClass = oldReturnClass.getSuperclass();
    if (null!= superClass && !superClass.equals(Object.class)) {
        Map superGenericClassMap = getSuperGenericClassMap(oldReturnClass, genericClassMap);
        buildfieldTuples(fieldTuples, superGenericClassMap, superClass);
    }

    List>> tuple2s = getSimpleClassTypeSignatureTuplesForInterfaces(oldReturnClass);
    if (!CollectionUtils.isEmpty(tuple2s)) {
        for (Tuple2> tuple2 : tuple2s) {
           Class interfaze = tuple2.getFst();
            List simpleClassTypeSignatures = tuple2.getSnd();
            Map interfaceGenericClassMap = getInterfaceGenericClassMap(interfaze, simpleClassTypeSignatures, genericClassMap);
            buildfieldTuples(fieldTuples, interfaceGenericClassMap, interfaze);
        }
    }
}
```

    上述buildfieldTuples方法中,首先通过Type genericReturnType = readMethod.getGenericReturnType();获取getter方法的返回值类型并进行解析,如果getter返回类型存在泛型、泛型数组的,递归解析生成代理类,对应前述递归模型3

    其次,获取泛型类的父类和实现的接口,递归调用buildfieldTuples方法对java bean进行解析生成,对应前述递归模型2.

    当然,对于java集合类型,如List、Set、Map等,还需要做特殊处理,如List和Set需要将其转换成Person[]数组处理,具体可参考源码。

泛型代理可参考源码:com.github.doublebin.springfox.bridge.core.builder.BridgeGenericReplaceBuilder

2.2 示例效果

2.2.1 简单泛型情况

    我们先看下最终要达到的效果,对于接口返回类型有泛型的情况,springfox-bridge底层生成了泛型类型的代理类,在代理类中利用原来的泛型映射替换泛型类型的定义。例如第一章中的queryPersonById方法:

  @BridgeOperation(value = "查询用户信息", notes = "根据id查询用户信息")
    public CommonResponse queryPersonById(@BridgeModelProperty(value = "用户id", required = true) Long id){
        ... //此处省略
    }

    CommonResponse的定义为:

@Data
public class CommonResponse {
    
    private Boolean success;

    private Integer code;
    
    private String message;
    
    private T data;
}

    那么springfox-bridge针对CommonResponse生成一个新的代理类型:

package bridge.model.replace;

import com.github.doublebin.springfox.bridge.demo.model.Person;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;

@ApiModel("bridge.model.replace.CommonResponse1")
public class CommonResponse1 {
    @ApiModelProperty("结果码")
    private Integer code;
    @ApiModelProperty("响应数据")
    private Person data;
    @ApiModelProperty("是否成功")
    private Boolean success;
    @ApiModelProperty("描述信息")
    private String message;

    ...
    ...//此处省略掉getter、setter方法
    ...
}

    利用springfox-bridge在界面对接口测试时,对实际调用的结果,利用json进行转换,将原响应对象转换成新类型的响应对象,这样便可以正常界面进行调用,且可以正常看到具体泛型映射类Person的@ApiModelProperty定义,生成的swagger界面如下:

【开源项目】springfox-bridge采用多维递归结合javassist生成泛型代理类(完美解决泛型擦除问题)_第2张图片
image.png

    如上图所示,返回类型中显示指明了data属性的类型为Person,Person的@ApiModelProperty也在swagger界面中也一目了然。

2.2.2 泛型嵌套情况

    此处我们定义另外一个泛型类TestResult:

@Data
public class TestResult {
    @ApiModelProperty("数据model")
    private TT model;

    @ApiModelProperty("结果码")
    private int code;
}

    再定义一个接口方法queryResultById:

@BridgeOperation(value = "查询用户Result信息", notes = "根据id查询result信息")
    public TestResult> queryResultById(@BridgeModelProperty(value = "用户id", required = true) Long id){
        ...//此处省略
    }

     显而易见,此处存在泛型嵌套,实际生成的界面如下:

【开源项目】springfox-bridge采用多维递归结合javassist生成泛型代理类(完美解决泛型擦除问题)_第3张图片
image.png

    可以看到,对于泛型嵌套,springfox-bridge递归生成了所有嵌套泛型的代理类,并成功展示了所有model的接口文档说明()。

三、总结与展望

     springfox-bridge关于泛型代理的部分在1.0.3版本中用BridgeGenericReplaceBuilder实现,如有不足或疏漏欢迎与作者联系。

你可能感兴趣的:(【开源项目】springfox-bridge采用多维递归结合javassist生成泛型代理类(完美解决泛型擦除问题))