赞
踩
各位好:
关于swagger map类型的参数 的问题,本篇文章提供一套解决方案。但是方案不够尽善尽美。比如 迫不得已引入BaseController 来解决顺序问题, BeanPospProcessor 和 MapContext 的额外引入,收集的同时 消耗了内存空间,同时也间接的与swagger内置的收集系统冗余。
还有一个比较严重的问题是,MapApiReader 会重复 创建Class类 对象,也正是因为这个原因,导致 swagger不支持Map类型 解析生成Molde的问题,没有暴露出来。这算是一个大大的巧合吧。
所以,又花了些时间,重构了整个项目,改动量挺大的 。代码地址 还是 https://github.com/SincereJ/swagger-demo.git
但是 是 rebuild 分支 ,rebuild 分支。
重构的整个思想没有变化,还是干的偷梁换柱的勾当。只不过这回“偷”的比较合情合理。
东施效颦也好,嫫母自好也罢。欢迎各位 批评指正。目前 代码 还没有编写注释内容。等整个工程相对稳定了,会及时补充完善注释内容的。
最后 前一阵 有人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>
接下来 对整个过程 稍微缕一下。
本文实现的注解 可以直接作用在方法上,而不是 写在参数列表里,对于强迫症的同学,绝对是福音啊。
接下来是 代码 :
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(){ } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。