读取Java源文件中字段的注释当做Swagger的字段描述

本文作者: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;
	}
	...
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值