swagger优雅显示全部状态码
背景: 后端在与前端交流的时候,常常需要用到swagger文档去沟通接口。虽然swagger中有对于枚举类和返回状态码的支持,但是需要在代码中写好返回响应,会带来很多的多余代码。对实际开发并不友好。
引入的依赖
<!-- 使用优先级法排除swagger的jar包,为了让没有设定property的example的也可以不报错-->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.5.21</version>
</dependency>
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.5.21</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.shardingsphere/sharding-jdbc-core -->
<!--Swagger-->
<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>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.6</version>
</dependency>
swagger本身对于状态码的支持
@ApiResponses({
@ApiResponse(code = 200, message = "成功",response = CustomEnumConstant.class),
@ApiResponse(code = 500, message = "失败")
})
ps: 对于此,其实我是真的不想写这么多响应在代码里面,如果真的要写,不如直接去Apifox里面,测一遍接口保存测试用例和响应示例。
在网上找的对于枚举类的支持方法
基本就是改写ModelPropertyBuilderPlugin或者ExpandedParameterBuilderPlugin。
- ModelPropertyBuilderPlugin是控制模型属性的构建
- ExpandedParameterBuilderPlugin是控制参数基本属性的构建,如参数的类型、描述、是否必填等。实现这个接口可以控制参数的基本展示形式
但是都满足不了我的需求,我想把整一个状态码枚举类给前端看。因为单独的在一个模型属性中是显示不了全部后台定义的状态码的。
解决办法
我就想到能不能通过状态码枚举类,动态生成一个类,交给swagger扫描到文档中,这样就可以达到很好的显示效果了.
这个的难点有两个:
- 1.如何动态生成类
- 2.如何交给swagger扫描
解决过程
对于如何动态生成类,我借鉴了GPT的说法.可以使用cglib的api或者java原生的javassist.
对于cglib,api有些学习难度.我借鉴了一篇博客
https://developer.aliyun.com/article/1388546
最终用的javassist.
如何交给swagger扫描我也是在上面的博客中找到的办法.
下面直接贴上显示状态码的代码
import com.fasterxml.classmate.TypeResolver;
import io.swagger.annotations.ApiModelProperty;
import javassist.*;
import javassist.bytecode.AnnotationsAttribute;
import javassist.bytecode.ConstPool;
import javassist.bytecode.annotation.StringMemberValue;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import springfox.documentation.schema.ModelRef;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.ParameterBuilderPlugin;
import springfox.documentation.spi.service.contexts.ParameterContext;
import javassist.bytecode.annotation.Annotation;
import java.lang.reflect.Modifier;
/**
* 用于状态码枚举类的swagger显示
*/
@Configuration
@Order(-19999)
@Slf4j
public class SwaggerModelReader implements ParameterBuilderPlugin {
@Value("${httpCode}")
private String httpCode;
private Integer cnt = 0;
@Value("${name}")
private String name;
@Autowired
private TypeResolver typeResolver;
//根据用户自定义的类型拿到该类型所在的包的class位置
@Override
public void apply(ParameterContext context) {
if (cnt == 1) {
//根据枚举位置获取枚举类
// 使用反射获取枚举类
Class<?> enumClass;
try {
enumClass = Class.forName(httpCode);
} catch (ClassNotFoundException e) {
log.error("找不到枚举类");
throw new RuntimeException(e);
}
//获取枚举类的所有常量
Object[] enumConstants = enumClass.getEnumConstants();
//生成一个新的类
Class newClass = createRefModelIgp(enumConstants);
//向documentContext的Models中添加我们新生成的Class
context.getDocumentationContext()
.getAdditionalModels()
.add(typeResolver.resolve(newClass));
//修改model参数的ModelRef为我们动态生成的class
context.parameterBuilder()
.parameterType("body")
.modelRef(new ModelRef(name))
.name(name);
}
cnt++;
}
/**
* @return
*/
private Class createRefModelIgp(Object[] enumConstants) {
try {
//创建一个类
ClassPool pool = ClassPool.getDefault();
int lastDotIndex = httpCode.lastIndexOf(".");
String prefix = httpCode.substring(0, lastDotIndex);
CtClass ctClass = pool.makeClass(prefix + "." + name);
//创建对象,并把已有的变量添加进去
createCtFileds(enumConstants, ctClass);
//返回最终的class
return ctClass.toClass();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
@Override
public boolean supports(DocumentationType delimiter) {
return true;
}
/**
* 根据propertys中的值动态生成含有Swagger注解的javaBeen
*/
public void createCtFileds(Object[] enumConstants, CtClass ctClass) {
//添加枚举值成为变量
for (Object object : enumConstants) {
try {
// 获取字段名和类型
String fieldName = ((Enum<?>) object).name();
// 遍历枚举常量数组
// 强制转换为 HttpCode 接口类型
HttpCode httpCode = (HttpCode) object;
// 使用接口方法获取枚举常量的值
int code = httpCode.getCode();
String message = httpCode.getMessage();
String fieldsInfo = code + "\t" + message;
// 创建 CtField
CtField ctField = new CtField(ClassPool.getDefault().get("java.lang.String"), fieldName, ctClass);
ctField.setModifiers(Modifier.PUBLIC);
// 设置参数描述
// 使用枚举值的字符串表示作为描述
String apiModelPropertyValue = fieldsInfo;
if (StringUtils.isNotBlank(apiModelPropertyValue)) {//添加model属性说明
ConstPool constPool = ctClass.getClassFile().getConstPool();
AnnotationsAttribute attr = new AnnotationsAttribute(constPool, AnnotationsAttribute.visibleTag);
Annotation ann = new Annotation(ApiModelProperty.class.getName(), constPool);
ann.addMemberValue("value", new StringMemberValue(apiModelPropertyValue, constPool));
attr.addAnnotation(ann);
ctField.getFieldInfo().addAttribute(attr);
}
// 添加字段到类中
ctClass.addField(ctField);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
上面的是解析的方法,状态码想要显示,需要实现下面这个接口
public interface HttpCode {
int getCode();
String getMessage();
}
然后再yml配置中添加以下
# 你实现HttpCode接口的类的位置
httpCode: com.xxxx.xxxx.xxxx.HttpStatus
# 你希望在swagger文档中状态码显示的名字
name: 状态码
遇到的问题
- 1.如何通过反射拿到枚举的常量
- 2.动态生成类的CtClass ctClass = pool.makeClass(prefix + “.” + name);报错frozen class
解决
- 1.写一个接口让用户实现就可以保证反射取得枚举常量通用性
- 2.开头的cnt就是解决报错frozen class的,具体原因未知