swagger map类型的参数(一)

各位好:
关于swagger map类型的参数 的问题,本篇文章提供一套解决方案。但是方案不够尽善尽美。比如 迫不得已引入BaseController 来解决顺序问题, BeanPospProcessor 和 MapContext 的额外引入,收集的同时 消耗了内存空间,同时也间接的与swagger内置的收集系统冗余。

还有一个比较严重的问题是,MapApiReader 会重复 创建Class类 对象,也正是因为这个原因,导致 swagger不支持Map类型 解析生成Molde的问题,没有暴露出来。这算是一个大大的巧合吧。

所以,又花了些时间,重构了整个项目,改动量挺大的 。代码地址 还是 https://github.com/SincereJ/swagger-demo.git
但是 是 rebuild 分支 ,rebuild 分支。

重构的整个思想没有变化,还是干的偷梁换柱的勾当。只不过这回“偷”的比较合情合理。

  1. 优化掉了 BaseController
  2. 去掉 BeanPospProcessor 和 MapContext 的额外引入
  3. 修复 MapApiReader 重复创建Class类 对象
  4. swagger不支持Map类型 解析生成Molde的问题 也间接的绕过了

东施效颦也好,嫫母自好也罢。欢迎各位 批评指正。目前 代码 还没有编写注释内容。等整个工程相对稳定了,会及时补充完善注释内容的。

最后 前一阵 有人qq 跟我说 项目打包后,执行java -jar xx.jar,注解里example和description 属性都不生效了。确实是有这个问题,本来这次重构也想着修复这个问题,奈何一直也没找到原因。等稍微空闲下来,再仔细研究一下。

关于 执行java -jar xx.jar,注解里example和description 属性都不生效 的问题已经找到问题原因了。是类加载器的问题。java -jar 使用的是 LaunchedURLClassLoader ,它继承于 URLClassLoader。java -jar xx.jar 启动后, jvm 的classpath 会自动变成 xx.jar,LaunchedURLClassLoader 也只会加载 xx.jar 里边的类。自然包括ApiModelProperty 这个注解类的信息。
对应我们通过asm生成的类呢(这里我们叫临时类),他的加载器 属于我们自定义的ClassLoader,代码里叫 ApiJsonClassLoader。在收集 临时类 的注解信息(也就是ApiModelProperty注解)的时候,会使用该临时类的加载器(临时类 对应的ClassLoader),再通过Class.ForName() 方法反射获取 “io.swagger.annotations.ApiModelProperty”的Class信息 。那么问题就出现了。临时类的ClassLoader,是我们自定义的ApiJsonClassLoader,它并没有加载过 ApiModelProperty 类信息,所有就会报“Type io.swagger.annotations.ApiModelProperty not present” 这个异常。有人会说,不是双亲委派嘛,应该向上查找并加载的嘛,遗憾的是 LaunchedURLClassLoader 和我们自定义的 ApiJsonClassLoader,并没有继承关系。
还有为什么这个异常没有被打印并且也没有终止程序呢,因为 再解析类 注解的时候 catch 住了该异常但是没有throw 该异常,同时跳过当前注解的解析,直接return null。这就是是为什么注解 不生效的原因了。

至于是怎么解决这个问题的呢。想了一个不太优雅的办法。 我们生成的临时类,不直接通过defineClass加载到jvm内存。而是生成一个.class 字节码文件,放到一个固定磁盘区域,代码里配置的是 “D:/temp/” ,你自己随便。再通过LaunchedURLClassLoader 去加载 这个路径 下的类信息,这样 我们的临时类 和 ApiModelProperty 类都在一个类加载器下,自然就都能加载到了。

关于 执行java -jar xx.jar,注解里example和description 属性都不生效 的问题。处理的方式不太优雅。这个也被优化了。去掉了 .class 字节码文件临时存放的中转方案。直接使用 Proxy 的一个加载类的方法替换的。

下边截图还能看看,其他的内容可以不用读了,直接github 拉代码操作起来。

本文主要解决的问题是 Swagger2 (SpringFox)关于Map参数生成的API文档中 没有 详细Json结构说明。问题如下图这样:
代码 github 地址:https://github.com/SincereJ/swagger-demo.git
在这里插入图片描述
在这里插入图片描述
由于Map 参数类型 不能 详细的展示给前端,造成很多沟通上的麻烦。本文综合网上 几位 大神的文章,加上自己总结发挥 ,终于 实现了 Map参数生成的API文档中详细内容 的生成。效果图如下:

在这里插入图片描述
首先 swagger 的 版本如下:

<dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-spring-web</artifactId>
            <version>2.9.2</version>
        </dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>2.9.2</version>
		</dependency>
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>2.9.2</version>
		</dependency>

接下来 对整个过程 稍微缕一下。

  1. 在spring初始化的时候 通过 MyBeanPostProcessor 收集 注解信息 放到 SwaggerMapContext里
  2. MapApiReader 实现了 ParameterBuilderPlugin ,在 apply 里 ,循环遍历 收集到的注解信息 使用 ASM ,动态生成类。也就是 将Map类型参数里的 key 转换成一个类中的 字段。
  3. 生成类之后,加载这个类,替换 modelRef 。modelRef 是啥? 看代码 就知道了。

本文实现的注解 可以直接作用在方法上,而不是 写在参数列表里,对于强迫症的同学,绝对是福音啊。

接下来是 代码 :
MyBeanPostProcessor.java

package com.example.postprocessor;


import com.example.swagger.annos.ApiJsonObject;
import com.example.swagger.annos.ApiJsonProperty;
import com.example.swagger.config.SwaggerMapContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.boot.autoconfigure.AutoConfigurationPackages;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

@Slf4j
public class MyBeanPostProcessor implements BeanPostProcessor {

    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class clazz =  bean.getClass();
        Package packageStr = clazz.getPackage();
        String packAgeName = packageStr.getName();
        if(!packAgeName.contains(SwaggerMapContext.filterPackage)){
            return bean;
        }
        if(clazz.getAnnotation(RestController.class) == null && clazz.getAnnotation(Controller.class) == null){
            return bean;
        }
        RequestMapping controllerRequestMapping = (RequestMapping) clazz.getAnnotation(RequestMapping.class);
        String classRequestUrl =  Arrays.toString(controllerRequestMapping.value());

        List methods = Arrays.asList(clazz.getDeclaredMethods());
        Iterator<Method> iterator = methods.iterator();
        while(iterator.hasNext()){
            Method method = iterator.next();
            String methodRequest = getRequestUrl(method);
            String key = classRequestUrl + methodRequest ;
            key = key.replaceAll("\\[","").replaceAll("\\]","");
            ApiJsonObject annotation = method.getAnnotation(ApiJsonObject.class);
            if(annotation != null){
                ApiJsonProperty[] values = annotation.value();
                SwaggerMapContext.getMap().put(key,values);
            }
        }
        return bean;
    }
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        // 这边只做简单打印   原样返回bean
        //if(AutoConfigurationPackages.class.getName().equals(beanName)){
        //    System.out.println("postProcessBeforeInitialization===="+beanName);
        //}
        return bean;
    }

    private String getRequestUrl(Method method){
        String methodRequest = "";
        if(method.getAnnotation(RequestMapping.class) != null) {
            methodRequest = Arrays.toString(method.getAnnotation(RequestMapping.class).value());
        }
        if(method.getAnnotation(PutMapping.class) != null) {
            methodRequest = Arrays.toString(method.getAnnotation(PutMapping.class).value());
        }
        if(method.getAnnotation(DeleteMapping.class) != null) {
            methodRequest = Arrays.toString(method.getAnnotation(DeleteMapping.class).value());
        }
        if(method.getAnnotation(GetMapping.class) != null) {
            methodRequest = Arrays.toString(method.getAnnotation(GetMapping.class).value());
        }
        if(method.getAnnotation(PatchMapping.class) != null) {
            methodRequest = Arrays.toString(method.getAnnotation(PatchMapping.class).value());
        }
        if(method.getAnnotation(PostMapping.class) != null) {
            methodRequest = Arrays.toString(method.getAnnotation(PostMapping.class).value());
        }
        return methodRequest;
    }


}

ApiJsonObject.java

package com.example.swagger.annos;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiJsonObject {
 
    ApiJsonProperty[] value(); //对象属性值
    String name();  //对象名称


 
}


ApiJsonProperty.java

package com.example.swagger.annos;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiJsonProperty {

    String key();  //key

    String example() default "";

    Class type() default String.class;  //支持string 和 int

    String description() default "";
 
}

MapApiReader.java 是核心类

package com.example.swagger.config;

import com.example.swagger.annos.ApiJsonProperty;
import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.service.ResolvedMethodParameter;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.spi.service.contexts.OperationContext;
import springfox.documentation.spi.service.contexts.ParameterContext;

import java.util.Map;

@Component
@Order   //plugin加载顺序,默认是最后加载
public class MapApiReader extends ClassLoader implements ParameterBuilderPlugin {
    @Autowired
    private TypeResolver typeResolver;

    //@Override
    public void apply(ParameterContext parameterContext) {
        Map<String, Object> maps = SwaggerMapContext.getMap();

        ResolvedMethodParameter methodParameter = parameterContext.resolvedMethodParameter();
        OperationContext operationContext = parameterContext.getOperationContext();
        String requestMappingPatternName = operationContext.requestMappingPattern();

        Optional<String> parameterNameOptional = methodParameter.defaultName();
        String parameterName = parameterNameOptional.get();

        if (methodParameter.getParameterType().canCreateSubtype(Map.class) || methodParameter.getParameterType().canCreateSubtype(String.class)) {

            String name = "H" + parameterName;
            name = SwaggerASMUtil.returnClassName(requestMappingPatternName,name);

            ApiJsonProperty[] properties = (ApiJsonProperty[]) maps.get(requestMappingPatternName);

            byte[] cs = SwaggerASMUtil.createRefModel(properties,name);
            Class hw = this.defineClass(name, cs, 0, cs.length);

            parameterContext.getDocumentationContext().getAdditionalModels().add(typeResolver.resolve(hw));
            parameterContext.parameterBuilder().parameterType("body").modelRef(new ModelRef(name)).name(name);
        }
    }

    @Override
    public boolean supports(DocumentationType delimiter) {
        return true;
    }
}

SwaggerASMUtil.java ASM生成类的

package com.example.swagger.config;

import com.example.swagger.annos.ApiJsonProperty;
import jdk.internal.org.objectweb.asm.*;
import org.apache.commons.lang3.StringUtils;

public class SwaggerASMUtil implements Opcodes {

    private static void createClazz(ClassWriter cw,String className){
        cw.visit(V1_8, ACC_PUBLIC, className, null, "java/lang/Object", null);
    }

    private static void createConstructor(ClassWriter cw){
        MethodVisitor methodVisitor=cw.visitMethod(Opcodes.ACC_PUBLIC,"<init>", "()V", null, null);
        methodVisitor.visitCode();

        methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);//0 表示当前对象
        methodVisitor.visitMethodInsn(Opcodes.INVOKESPECIAL,"java/lang/Object", "<init>","()V",false);
        methodVisitor.visitInsn(Opcodes.RETURN);
        methodVisitor.visitMaxs(0, 0);
        methodVisitor.visitEnd();
    }

    private static void doParseFieldAndMethod(ClassWriter cw, ApiJsonProperty[] propertys,String className){
        for (ApiJsonProperty property : propertys) {

            String typeof = "";
            if(property.type() != null){
                typeof = Type.getType(property.type()).getDescriptor();
            }
            int[] loadAndReturnOf = loadAndReturnOf(typeof);

            // 创建 字段 和 注释
            createFieldAndAnno(cw,property,typeof);

            // 创建字段getter 方法
            createFieldGetterMethod(cw,property,className,typeof,getOrSetOffer(typeof,true),loadAndReturnOf);

            // 创建字段setter 方法
            createFieldSetterMethod(cw,property,className,typeof,getOrSetOffer(typeof,false),loadAndReturnOf);

        }
    }

    private static void createFieldGetterMethod(ClassWriter cw,ApiJsonProperty property,String className,String typeof, String typeoffer, int[] loadAndReturnOf){
        String getterName = getterAndSetterName(property.key(),true);
        MethodVisitor m_getName=cw.visitMethod(ACC_PUBLIC, getterName, typeoffer, null, null);
        m_getName.visitVarInsn(ALOAD, 0);
        m_getName.visitFieldInsn(GETFIELD, className, property.key(), typeof);
        m_getName.visitInsn(loadAndReturnOf[1]);
        m_getName.visitMaxs(2, 1);
        m_getName.visitEnd();
    }

    private static void createFieldSetterMethod(ClassWriter cw,ApiJsonProperty property,String className,String typeof, String typeoffer, int[] loadAndReturnOf){
        String setterName = getterAndSetterName(property.key(),false);
        MethodVisitor m_setName=cw.visitMethod(ACC_PUBLIC, setterName, typeoffer, null, null);
        m_setName.visitVarInsn(ALOAD, 0);
        m_setName.visitVarInsn(loadAndReturnOf[0], 1);
        m_setName.visitFieldInsn(PUTFIELD, className, property.key(), typeof);
        m_setName.visitInsn(RETURN);
        m_setName.visitMaxs(3,3);
        m_setName.visitEnd();
    }

    private static void createFieldAndAnno(ClassWriter cw, ApiJsonProperty property,String typeof){
        FieldVisitor fv = cw.visitField(ACC_PUBLIC, property.key(), typeof, null, new String(property.example().toString()));
        fv.visitEnd();

        AnnotationVisitor av = fv.visitAnnotation("Lio/swagger/annotations/ApiModelProperty;", true);
        //注释参数
        av.visit("value", property.key());
        av.visit("example", property.example());
        av.visitEnd();
    }

    public static byte[] createRefModel(ApiJsonProperty[] propertys, String className) {
        try {
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            //创建类
            createClazz(cw,className);

            //创建构造方法
            createConstructor(cw);

            //循环处理 getter 和 setter 方法 创建字段和注解
            doParseFieldAndMethod(cw,propertys,className);

            cw.visitEnd();

            byte[] code = cw.toByteArray();

            return code;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    private static String getterAndSetterName(String name, Boolean isGetter){
        if(name.length() > 1){
            name = StringUtils.capitalize(name);
            if(isGetter) {
                return "get" + name;
            }else{
                return "set" + name;
            }
        }
        return name;
    }

    private static String getOrSetOffer(String typeof, boolean isGet){
        if(isGet){
            return "()" + typeof;
        }
        return "(" + typeof + ")V";
    }

    private static int[] loadAndReturnOf(String typeof) {
        if (typeof.equals("I") || typeof.equals("Z")) {
            return new int[]{ILOAD,IRETURN};
        } else if (typeof.equals("J")) {
            return new int[]{LLOAD,LRETURN};
        } else if (typeof.equals("D")) {
            return new int[]{DLOAD,DRETURN};
        } else if (typeof.equals("F")) {
            return new int[]{FLOAD,FRETURN};
        } else {
            return new int[]{ALOAD,ARETURN};
        }
    }

    public static String returnClassName(String requestMappingPatternName, String name){
        requestMappingPatternName = ("Class"+requestMappingPatternName).replaceAll("/","_");
        return requestMappingPatternName+"_"+name;
    }

}

SwaggerMapContext.java 存放收集的注解信息 主要

package com.example.swagger.config;

import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
public class SwaggerMapContext {

    public static final String filterPackage = "com.example";

    private static Map<String, Object> map = new ConcurrentHashMap<>();

    public static Map<String, Object> getMap(){
        return map;
    }

}

SwaggerMapController.java 测试controller

package com.example.swagger;

import com.example.swagger.annos.ApiJsonObject;
import com.example.swagger.annos.ApiJsonProperty;
import io.swagger.annotations.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RequestMapping("/swagger")
@RestController
@Slf4j
public class SwaggerMapController extends BaseController{

    @PostMapping("/selectIndentNumberByPrimaryIdAndName")
    @ApiJsonObject(name = "params", value = {
            @ApiJsonProperty(type = Integer.class,key = "mobile", example = "18614242538", description = "user mobile"),
            @ApiJsonProperty(type = Integer.class,key = "password", example = "123456", description = "user password"),
            @ApiJsonProperty(type = String.class,key = "name", example = "", description = "user 姓名"),
            @ApiJsonProperty(type = Integer.class,key = "page", example = "", description = "当前页"),
            @ApiJsonProperty(type = Integer.class,key = "rows", example = "15", description = "行数")
    })
    @ApiOperation(value = "视频回放", notes = "courseLessonId 课时编号 不能为空")
    public String selectIndentNumberByPrimaryIdAndName(@RequestBody Map<String,Object> params){
        log.info("ssssssssssssss---index");
        return "ssssssssss";
    }

    /*@GetMapping("/dd")
    //@ApiOperation(value = "视频回放", notes = "courseLessonId 课时编号 不能为空")
    public String dd (@RequestBody  Map<String, Object> params){
        return "doc";
    }*/
}

细心的同学能发现,代码跟网上的 有很多相同。的确是这样,借鉴了一部分,自己发挥了一部分。下边列出 参照大神的 网贴地址:
https://blog.csdn.net/iteye_11019/article/details/82240309
https://blog.csdn.net/hellopeng1/article/details/82227942

最后啰嗦一句,确实存在
在这里插入图片描述
图上说的问题,我是通过下图 得到的灵感:
在这里插入图片描述

弄了个 baseController.java ,里边加了一个 固定的方法。

package com.example.swagger;


import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
 * 基础控制器抽象类
 * 
 * <ul>
 * <li>统一异常处理
 * <li>统一数据返回格式
 * </ul>
 *
 */
@Controller
public abstract class BaseController {

	@GetMapping("/zzzzzzzzzzz")
	public void zzzzzzzzzzz(){

	}

}
  • 10
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 26
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值