背景
使用Swagger的时候有一种痛苦,侵入性太强了。我个人又喜欢写注释,我理解注释写得越好,越能减少沟通的交流,节省人力,提高工作效率。所以想着使用Controller的注释和实体的注释,就能替换Swagger的注解。全网找下来,有实现这个功能的: github ,但是我使用之后发现一些bug联系不上作者,而且我不会 Kotlin , 所以还是需要自己研究一下。设计
研究swagger的源码
通过研究swagger的源码发现,发现swagger是使用Plugin方式注册bean,使用 DocumentationPluginsManager 来管理plugins。允许一种类型的插件多种实现方式,这样就很方便了。 我们只需要拓展plugin就行。 我的目标是替换 @Api 、 @ApiOperation 、 @ApiModel 、 @ApiModelProperties ,我基本上用到的就只有这些。寻找读取注释的方法
运行时获取注释
这个是我最想要的方法,也确实有这种方法,可以参考下面的代码:public class JavadocReader { /** * 为了方便gc */ private static WeakReferencerootRef; /** * 操作cache需要获取synchronized锁,线程安全 */ private static HashMap<< span="">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 * date 2020/11/6 * 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 1. 在pom文件中绑定complie阶段。<build> <plugins> <plugin> <groupId>self.donghao.extension.mavengroupId> <artifactId>self-swagger-extension-mavenartifactId> <version>1.0-SNAPSHOTversion> <executions> <execution> <id>javadocid> <phase>compilephase> <goals> <goal>javadocgoal> goals> <configuration> <packages> <p>self.donghao.demos.swagger.ctrlp> <p>self.donghao.demos.swagger.dtop> packages> configuration> execution> executions> plugin> ...
2
.
运行maven compile(注意这个时候使用IDEA直接运行spring-boot是没办法获取注释的)
3.
运行项目。
最后看看效果图
其实跳出项目来看,这种实现方式和原生的Swagger是有取有舍,这种消耗了 部分存储空间和内存来获取注释。 如果从注释角度,这种消耗肯定更加小的,看大众的接受哪种方式吧。 究竟哪种方式更好,这个是没有定论的。这部分开源的代码没有做一些缓存优化(笔者没有时间)。如果同学用的话,这个坑需要注意一下。
参考项目
1. springfox-plus