背景
使用Swagger的时候有一种痛苦,侵入性太强了。我个人又喜欢写注释,我理解注释写得越好,越能减少沟通的交流,节省人力,提高工作效率。所以想着使用Controller的注释和实体的注释,就能替换Swagger的***注解***。全网找下来,有实现这个功能的:github,但是我使用之后发现一些bug联系不上作者,而且我不会Kotlin, 所以还是需要自己研究一下。
设计
pom文件依赖
因为考虑想开源给大家使用,这里没有去依赖顶层的pom文件。
研究swagger的源码
通过研究swagger的源码发现,发现swagger是使用Plugin方式注册bean,使用DocumentationPluginsManager
来管理plugins。
允许一种类型的插件多种实现方式,这样就很方便了。
我们只需要拓展plugin就行。
我的目标是替换@Api
、@ApiOperation
、@ApiModel
、@ApiModelProperties
,我基本上用到的就只有这些。
寻找读取注释的方法
运行时获取注释
这个是我最想要的方法,也确实有这种方法,可以参考下面的代码:
public class JavadocReader {
/**
* 为了方便gc
*/
private static WeakReference<RootDoc> rootRef;
/**
* 操作cache需要获取synchronized锁,线程安全
*/
private static HashMap<String, SwaggerJavadoc> cache = new HashMap<>();
public static synchronized SwaggerJavadoc readJavaDoc(String fileFullPath, String clsName, boolean isField) {
SwaggerJavadoc result = null;
if (cache.get(clsName) != null) {
return cache.get(clsName);
}
executeJavaDoc(fileFullPath);
RootDoc root = rootRef.get();
assert root != null;
ClassDoc[] classes = root.classes();
for (ClassDoc cls : classes) {
//获取注解 class的注解
result = new SwaggerJavadoc();
result.setClassComment(cls.commentText());
//获取filed的注解
if (isField) {
if (result.getFiledCommentMap() == null) {
result.setFiledCommentMap(new HashMap<>());
}
result.getFiledCommentMap().putAll(Arrays.stream(cls.fields(false)).collect(Collectors.toMap(Doc::name, FieldDoc::commentText)));
} else { //获取方法的注解
if (result.getMethodCommentMap() == null) {
result.setMethodCommentMap(new HashMap<>());
}
result.getMethodCommentMap().putAll(Arrays.stream(cls.methods()).collect(
Collectors.toMap(methodDoc -> String.format("%s_%s", methodDoc.name(), Arrays.toString(methodDoc.parameters())),
MethodDoc::commentText)));
}
cache.put(cls.qualifiedTypeName(), result);
}
rootRef.clear(); //help gc
rootRef = null;
return cache.get(clsName);
}
public static void executeJavaDoc(String targetPath, String fileFullPath) {
// 参考了 https://blog.csdn.net/baiihcy/article/details/53861267
com.sun.tools.javadoc.Main.execute(new String[] {"-doclet", SwaggerDoclet.class.getName(),
// 因为自定义的Doclet类并不在外部jar中,就在当前类中,所以这里不需要指定-docletpath 参数,
//"-docletpath",
//Doclet.class.getResource("/").getPath(),
"-encoding", "utf-8", "-classpath", targetPath,
// 获取单个代码文件FaceLogDefinition.java的javadoc
fileFullPath});
}
public static void executeJavaDoc(String fileFullPath) {
// 参考了 https://blog.csdn.net/baiihcy/article/details/53861267
com.sun.tools.javadoc.Main.execute(new String[] {"-doclet", SwaggerDoclet.class.getName(),
// 因为自定义的Doclet类并不在外部jar中,就在当前类中,所以这里不需要指定-docletpath 参数,
//"-docletpath",
//Doclet.class.getResource("/").getPath(),
"-encoding", String.valueOf(StandardCharsets.UTF_8),
"-classpath",
".",
fileFullPath});
}
/**
* 用来映射注释适合Swagger<br/>
* date 2020/11/6 <br/>
* email yuan.donghao@qq.com
*
* @author 袁小黑
* @version 1.0.0
**/
public static class SwaggerDoclet extends Doclet {
public static boolean start(RootDoc root) {
JavadocReader.rootRef = new WeakReference<>(root);
return true;
}
}
}
测试文件
public static final String RequestFullPath = JavadocReaderTest.class.getResource("/").getPath()+"../../src/test/java/self/donghao/swagger/extension/dto/Request.java";
public static final String MethodFullPath = JavadocReaderTest.class.getResource("/").getPath()+"../../src/test/java/self/donghao/swagger/extension/method/DubboSpringCloudClientBootstrap.java";
public static void main(String[] args) {
System.out.println(JavadocReaderTest.class.getResource("/").getPath());
}
@Test
public void readJavaDocTestRequest() {
System.out.println(JavadocReader.readJavaDoc(RequestFullPath, Request.class.getName(), true));
System.out.println(JavadocReader.readJavaDoc(RequestFullPath, Request.InnerRequest.class.getName().replace("$", "."), true));
}
...
上面的代码是我编写的参考网上的资料编写的,可以运行时获取java注释,也经过了本人实验。
但是有个很大的问题:Java编译器编译Java文件之后会去除这些注释。
上面的代码也是从Java文件中动态读取注释的。这时我也开始理解SpringFox团队为什么不提供注释的plugin而是使用侵入性比较强的注解了。注解的获取非常简单,使用反射即可,注释的获取非常难,这不仅仅是我们上面提到的问题,还有问题就是jdk8、jdk9~jdk14之间的javadoc运行的Main方法不是在同一个package下,这样就更加困难了。我不得不参考springfox-plus,将这个运行时获取注释拆分成两步:
1)生成注释
2)读取注释并将注释内容放入Swagger的Plugin
生成注释
生成注释主要思路是在maven的compile阶段绑定一个maven-plugin(maven的生命周期),运行javadoc生成类描述json文件,并且写到META-INF下。
具体感兴趣的小伙伴可以看这里:javadoc-maven-plugin
读取注解并更改Swagger的Plugin
其实就是读取META-INF下的文件,更新到Swagger的Plugin。
写和读文件都是使用Jackson
的ObjectMappler,Swagger的每个注解都有对应的Plugin,读者感兴趣可以去研究一下。这里给出我使用到的Plugin:ApiListingBuilderPlugin
, ModelBuilderPlugin
、OperationBuilderPlugin
、ModelPropertyBuilderPlugin
。
@Component
@Order(AFTER_SWAGGER)
public class CustomApiBuilder implements ApiListingBuilderPlugin {
@Override
public void apply(ApiListingContext apiListingContext) {
ClassDescription classDescription = ClasspathDocStore
.read(
apiListingContext
.getResourceGroup()
.getControllerClass()
.get()
.getName()); //读取META-INF下的文件,反序列化成ClassDescription
if (classDescription == null || !StringUtils.hasText(classDescription.getDescription())) {
return;
}
//设置
apiListingContext
.apiListingBuilder()
.description( classDescription.getDescription());
}
@Override
public boolean supports(DocumentationType documentationType) {
return true;
}
}
上面是一个替换Swagger的@Api的例子,内容比较简单。
更加具体的内容可以看这里:Swagger-Javadoc。
使用
要求jdk 1.8
- 在pom文件中绑定complie阶段。
<build>
<plugins>
<plugin>
<groupId>self.donghao.extension.maven</groupId>
<artifactId>self-swagger-extension-maven</artifactId>
<version>1.0-SNAPSHOT</version>
<executions>
<execution>
<id>javadoc</id>
<phase>compile</phase>
<goals>
<goal>javadoc</goal>
</goals>
<configuration>
<packages>
<p>self.donghao.demos.swagger.ctrl</p>
<p>self.donghao.demos.swagger.dto</p> <!--扫描的包-->
</packages>
</configuration>
</execution>
</executions>
</plugin>
...
-
运行maven compile(注意这个时候使用IDEA直接运行spring-boot是没办法获取注释的)
-
运行项目。
最后看看效果图
其实跳出项目来看,这种实现方式和原生的Swagger是有取有舍,这种消耗了 部分存储空间和内存来获取注释。如果从注释角度,这种消耗肯定更加小的,看大众的接受哪种方式吧。究竟哪种方式更好,这个是没有定论的。
这部分开源的代码没有做一些缓存优化(笔者没有时间)。如果同学用的话,这个坑需要注意一下。