【开源项目】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<Person> queryPersonById(@BridgeModelProperty(value = "用户id", required = true) Long id){
        ... //此处省略
    }

    其中CommonResponse和Person的定义:

@Data
public class CommonResponse<T> {
    
    @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<Person>做特殊处理,用method.getReturnType()作为新的代理接口方法的返回值,那么显然,返回类型中无法附带Person的信息,相应的界面如下:

image.png

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

1.2 javassist不支持泛型的问题

    然而,如果尝试创建上述接口的代理并设置代理方法的返回值类型为CommonResponse<String>也是不可行的,可以看看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<String>:

Vector<String> v = new Vector<String>();
  :
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<List<Person>>

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

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

    private BB name;

    private CC desc;

    private CommonResponseSub<BB,List<TT>,String> subData;
}

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

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

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

2.1.1 递归模型

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

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

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

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

    public class CommonResponseSub<A, B, C> extends CommonResponse<B> {
        ...//省略
    }
    

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

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

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

        此时针对TestGeneric的属性subData的CommonResponseSub类型存在泛型传递,如果指定TestGeneric<String, Boolean, Integer>,那么TestGeneric的泛型映射为 “TT:String, BB:Boolean, CC:Integer”, 而属性subData的类型CommonResponseSub的泛型映射为"A:Boolean, B:List<String>, C:String",CommonResponseSub的父类CommonResponse存在泛型映射“T:List<String>”, 而List<String>又存在泛型映射“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<Person> ;

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

  3. java.lang.reflect.TypeVariable : 代表返回类型为一个泛型的定义,如CommonResponse<T>的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>中的T)
      1. Class:代表类 (如CommonResponse<String>中的String.class)
      1. GenericInfo代表嵌套泛型类 (如TestGeneric<CommonResponse<String>, Boolean, Integer>中用CommonResponse<String>生成GenericInfo对象)
      1. GenericArrayInfo代表嵌套泛型类数组(如TestGeneric<CommonResponse<String>[], Boolean, Integer>中用CommonResponse<String>[]生成GenericArrayInfo对象)。

        如果类有多个泛型参数,将定义的泛型参数映射依次转换到features数组中,如TestGeneric<CommonResponse<String>, CommonResponse<String>[], 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<String, Class>),可以解析生成GenericInfo对应的clazz的genericClassMap,以此进行递归处理。

    我们来看部分源码:

       /**
        * 生成GenericInfo对象所属class的代理类
       **/
        public static Class buildGenericClass(GenericInfo genericInfo, Map<String, Class> genericClassMap) {
    
        Class oldReturnClass = genericInfo.getClazz();
    
        if(ClassUtils.isAssignable(oldReturnClass, Map.class)) {
            return Map.class;
        }
    
        Map<String, Class> 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<String, Class> 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<String, Tuple3<Class, ApiModelProperty, Method>> 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<String, Tuple3<Class, ApiModelProperty, Method>> fieldTuples,
                              Map<String, Class> genericClassMap, Class oldReturnClass) {

    if (!CollectionUtils.isEmpty(fieldTuples)) {
        for (String fieldName : fieldTuples.keySet()) {
            Tuple3<Class, ApiModelProperty, Method> 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<String, Class> superGenericClassMap = getSuperGenericClassMap(oldReturnClass, genericClassMap);
        buildfieldTuples(fieldTuples, superGenericClassMap, superClass);
    }

    List<Tuple2<Class, List<SimpleClassTypeSignature>>> tuple2s = getSimpleClassTypeSignatureTuplesForInterfaces(oldReturnClass);
    if (!CollectionUtils.isEmpty(tuple2s)) {
        for (Tuple2<Class, List<SimpleClassTypeSignature>> tuple2 : tuple2s) {
           Class interfaze = tuple2.getFst();
            List<SimpleClassTypeSignature> simpleClassTypeSignatures = tuple2.getSnd();
            Map<String, Class> 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<Person>和Set<Person>需要将其转换成Person[]数组处理,具体可参考源码。

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

2.2 示例效果

2.2.1 简单泛型情况

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

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

    CommonResponse的定义为:

@Data
public class CommonResponse<T> {
    
    private Boolean success;

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

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

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界面如下:

image.png

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

2.2.2 泛型嵌套情况

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

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

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

    再定义一个接口方法queryResultById:

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

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

image.png

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

三、总结与展望

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,264评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,549评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,389评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,616评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,461评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,351评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,776评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,414评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,722评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,760评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,537评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,381评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,787评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,030评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,304评论 1 252
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,734评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,943评论 2 336

推荐阅读更多精彩内容