# swagger优雅显示全部状态码

背景: 后端在与前端交流的时候,常常需要用到swagger文档去沟通接口。虽然swagger中有对于枚举类和返回状态码的支持,但是需要在代码中写好返回响应,会带来很多的多余代码。对实际开发并不友好。

引入的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<!--		使用优先级法排除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本身对于状态码的支持

1
2
3
4
@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扫描我也是在上面的博客中找到的办法.

下面直接贴上显示状态码的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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();
}
}
}
}

上面的是解析的方法,状态码想要显示,需要实现下面这个接口

1
2
3
4
public interface HttpCode {
int getCode();
String getMessage();
}

然后再yml配置中添加以下

1
2
3
4
# 你实现HttpCode接口的类的位置
httpCode: com.xxxx.xxxx.xxxx.HttpStatus
# 你希望在swagger文档中状态码显示的名字
name: 状态码

显示效果如下:

遇到的问题

  • 1.如何通过反射拿到枚举的常量
  • 2.动态生成类的CtClass ctClass = pool.makeClass(prefix + “.” + name);报错frozen class

解决

  • 1.写一个接口让用户实现就可以保证反射取得枚举常量通用性
  • 2.开头的cnt就是解决报错frozen class的,具体原因未知