本文作者:suxingrui
本文链接:https://blog.csdn.net/suxingrui/article/details/103788530
版权声明:本文为原创文章,转载请注明出处。
回顾2019年碰到的问题及解决方式
问题:读取Java源文件中字段的注释当做Swagger的字段描述
问题发现:
正常来说,Swagger通过使用@ApiModelProperty来标注Bean对象的字段的相关信息
然后,我们在一个老服务中引进Swagger,如果需要Swagger UI中显示即存Bean对象的字段的信息的话,
那就需要给这几十上百个Bean对象的每个字段都加上@ApiModelProperty
这是一个艰难的工作。。。
所以,考虑着通过读取源文件中字段的注释的方式来实现:不添加@ApiModelProperty也能显示字段描述的目标
调查分析:
又是一系列的源码跟踪分析,继承Swagger的ModelMapperImpl覆盖mapModels方法,并在里面补存对象的属性即可
读取源码中的注释,可以使用工具类:
<dependency>
<groupId>org.jboss.forge.roaster</groupId>
<artifactId>roaster-api</artifactId>
<version>2.21.0.Final</version>
</dependency>
<dependency>
<groupId>org.jboss.forge.roaster</groupId>
<artifactId>roaster-jdt</artifactId>
<version>2.21.0.Final</version>
</dependency>
支持读取【/** 描述*/】,不支持【// 描述】
解决方法:
1、实现一个内部的字段描述类:FieldDesc
private static class FieldDesc {
String name; // 字段
String type; // 类型
String desc; // 描述
private FieldDesc(String name, String type, String desc) {
this.name = name;
this.type = type;
this.desc = desc;
}
}
2、实现根据类对象读取对应源码的字段描述的方法
PS1:因为使用的是spring-boot-maven-plugin打包的,所以这里获取类对应jar包名的方式不一定适用所有同学
PS2:同时,刚好Jenkins与测试机器在同一台机器,所以这里获取源码的方式就是简单的配置路径即可(笑)
不然,可以考虑打测试包的同时把源码打包,或者编译打包之后同时推送源码包到测试环境的指定目录下
private static final Pattern FIND_JAR_PATTERN = Pattern.compile("/WEB-INF/lib/(\\S+)\\.jar!");
private static final Pattern FIND_NAME_PATTERN = Pattern.compile("(\\S+)-[\\d]+\\.");
@Value("${swagger.api.workspace:}")
private String apiWorkspace;
@Value("#{'${swagger.lib.workspaces:}'.split(',')}")
private String[] libWorkspaces;
private Map<String, FieldDesc> getFieldDescMap(Class<?> clz) {
Map<String, FieldDesc> fieldDescMap = Maps.newLinkedHashMap();
try {
URL url = clz.getResource("/" + clz.getName().replace('.', '/') + ".class");
String classPath = url.getFile();
// 获取开发环境,本地的源码
String sourcePath = classPath.replaceAll("target/classes", "src/main/java");
sourcePath = sourcePath.substring(0, sourcePath.length() - ".class".length()) + ".java";
File sourceFile = new File(sourcePath);
String source;
if (!sourceFile.exists()) {
// 打包之后的,对象即可能在classes下面也可能在lib包里,获取类对应的jar包名称
if (classPath.contains("/WEB-INF/classes!/")) {
sourcePath = apiWorkspace;
} else if (classPath.contains("/WEB-INF/lib/")) {
Matcher matcher = FIND_JAR_PATTERN.matcher(classPath);
if (matcher.find()) {
sourcePath = matcher.group(1);
matcher = FIND_NAME_PATTERN.matcher(sourcePath);
if (matcher.find()) {
sourcePath = matcher.group(1);
}
}
}
sourcePath = sourcePath + "/src/main/java/" + clz.getName().replace('.', '/') + ".java";
for (String w : libWorkspaces) {
sourceFile = new File(w + sourcePath);
if (sourceFile.exists()) {
log.debug("sourcePath:{}", sourcePath);
break;
}
}
}
if (sourceFile.exists()) {
source = Files.asCharSource(sourceFile, Charset.forName("UTF-8")).read();// 读取源码
JavaType<?> javaType = Roaster.parse(source); // 解析源码
if (javaType != null && javaType.isClass()) {
JavaClassSource javaClassSource = (JavaClassSource) javaType;
List<FieldSource<JavaClassSource>> fields = javaClassSource.getFields();
if (fields != null) {
for (FieldSource<JavaClassSource> field : fields) {
String type = field.getType().getName().toLowerCase();
fieldDescMap.put(field.getName(),
new FieldDesc(field.getName(), type, field.getJavaDoc().getText()));
}
}
}
}
} catch (IOException e) {
log.info("【SWAGGER】读取源文件异常:{}", e.getMessage());
}
Class<?> superClass = clz.getSuperclass();
if (superClass != null && superClass != Object.class) {
fieldDescMap.putAll(getFieldDescMap(superClass));
}
return fieldDescMap;
}
3、使用从源码中获取到的类字段的描述,补充到Swagger中
/**
* 覆写swagger的ModelMapper,实现:读取文件源码字段的注释当做swagger的字段描述
*
* @author suxingrui
* @time Jun 29, 2019 11:28:41 AM
*/
@Slf4j
@Primary
@Configuration
public class ModelMapperImplEx extends ModelMapperImpl {
@Override
public Map<String, Model> mapModels(Map<String, springfox.documentation.schema.Model> from) {
Map<String, Model> map = super.mapModels(from);
if (map != null) {
Set<String> modelKeys = from.keySet();
// 遍历所有的Model
for (String key : modelKeys) {
Model tm = map.get(key);
// Model的属性
Map<String, Property> properties = tm.getProperties();
if (properties == null) {
continue;
}
ResolvedType resolvedType = from.get(key).getType();
List<RawField> memberFields = resolvedType.getMemberFields();
Map<String, FieldDesc> fieldDescMap = getFieldDescMap(resolvedType.getErasedType());
// 新的属性
Map<String, Property> newProperties = Maps.newLinkedHashMap();
for (RawField rawField : memberFields) {
String name = rawField.getName();
Property property = properties.remove(name);
if (property == null) {
continue;
}
if (property instanceof StringProperty) {
StringProperty sp = (StringProperty) property;
// 枚举使用int,这边的规范,可以按实际删除
if (sp.getEnum() != null && sp.getEnum().size() > 0) {
property = new IntegerProperty();
property.setName(name);
property.setDescription(sp.getDescription());
}
}
newProperties.put(name, property);
if (StringUtils.isNoneBlank(property.getDescription())) {
continue;
}
// 没有描述时,使用从源码获取的
FieldDesc fieldDesc = fieldDescMap.remove(name);
if (fieldDesc != null) {
property.setDescription(fieldDesc.desc);
}
}
for (FieldDesc fieldDesc : fieldDescMap.values()) {
Property property;
if ("integer".equals(fieldDesc.type) || "int".equals(fieldDesc.type)) {
property = new IntegerProperty();
} else if ("boolean".equals(fieldDesc.type)) {
property = new BooleanProperty();
} else if (fieldDesc.type.endsWith("enum")) {
property = new IntegerProperty();
} else {
property = new StringProperty();
}
property.setName(fieldDesc.name);
property.setDescription(fieldDesc.desc);
newProperties.put(fieldDesc.name, property);
}
try {
// 新的属性列表替换旧的
Field propertiesField = FieldUtils.getDeclaredField(tm.getClass(), "properties", true);
if (propertiesField != null) {
propertiesField.set(tm, newProperties);
}
} catch (Exception e) {
log.info("【SWAGGER】设置properties异常:{}", e.getMessage());
}
}
}
return map;
}
...
}