各位好:
关于swagger map类型的参数 的问题,本篇文章提供一套解决方案。但是方案不够尽善尽美。比如 迫不得已引入BaseController 来解决顺序问题, BeanPospProcessor 和 MapContext 的额外引入,收集的同时 消耗了内存空间,同时也间接的与swagger内置的收集系统冗余。
还有一个比较严重的问题是,MapApiReader 会重复 创建Class类 对象,也正是因为这个原因,导致 swagger不支持Map类型 解析生成Molde的问题,没有暴露出来。这算是一个大大的巧合吧。
所以,又花了些时间,重构了整个项目,改动量挺大的 。代码地址 还是 https://github.com/SincereJ/swagger-demo.git
但是 是 rebuild 分支 ,rebuild 分支。
重构的整个思想没有变化,还是干的偷梁换柱的勾当。只不过这回“偷”的比较合情合理。
- 优化掉了 BaseController
- 去掉 BeanPospProcessor 和 MapContext 的额外引入
- 修复 MapApiReader 重复创建Class类 对象
- 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>
接下来 对整个过程 稍微缕一下。
- 在spring初始化的时候 通过 MyBeanPostProcessor 收集 注解信息 放到 SwaggerMapContext里
- MapApiReader 实现了 ParameterBuilderPlugin ,在 apply 里 ,循环遍历 收集到的注解信息 使用 ASM ,动态生成类。也就是 将Map类型参数里的 key 转换成一个类中的 字段。
- 生成类之后,加载这个类,替换 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(){
}
}