通过这一篇博客,可能会加深对AnnotationProcessor、SPI机制等的理解,可能会诱发你对已有的知识产生天马行空的使用想法
背景
你是否在工作中遇到这样的场景:“XX,你还记得我们项目里面有写过助手类,能把字符串格式化成这个样子吗。然后XX一脸懵逼”,你又是否听过这样的吐槽:“什么low biiii中台,写个功能库连个像样的文档都写不出来,难道让我去看他底裤(代码)??”
我们总说优秀的代码不需要文档,看到就明白他的意思。快别搞笑了,不是每个人都敢把底裤漏给别人看的,毕竟程序员之间相互尊重的一大准则就是别轻易去看别人代码😂,毕竟每个人编码的思路不会完全一致,而且在对需求的理解不够深刻的情况下去看别人的代码,总是会看的很不爽的。半开玩笑的讲,程序员一般不写文档(懒😂),写文档就是为了让别人别看代码。
软件开发文档是软件开发使用和维护过程中的必备资料。它能提高软件开发的效率,保证软件的质量,而且在软件的使用过程中有指导,帮助,解惑的作用,尤其在维护工作中,文档是不可或缺的资料
言归正传,开发文档包括:《功能要求》、《投标方案》、《需求分析》、《技术分析》、《系统分析》、《数据库文档》、《功能函数文档》、《界面文档》、《编译手册》、《项目总结》等。不过在敏捷开发背景(或者打着名义压缩工期背景)下,有些文档往往略去了,对于迭代维护中,能够起到较大帮助的往往是《系统分析》、《数据库文档》、《功能函数文档》这三块文档。
《系统分析》 -- 包括功能实现、模块组成、功能流程图、函数接口、数据字典、软件开发需要考虑的各种问题等。以《需求分析》为基础,进行详细的系统分析 ( 产品的开发和实现方法 ) ,估计开发期间需要把什么问题说明白,程序员根据《系统分析》,开始在项目主管的带领下进行编码。
《数据库文档》 -- 包括数据库名称、表名、字段名、字段类型、字段说明、备注、字段数值计算公式等。以《系统分析》为基础,进行详细的数据库设计。必要时可以用图表解说,特别是关系数据库。
7. 《功能函数文档》 -- 包括变量名、变量初值、功能,函数名,参数,如何调用、备注、注意事项等。以《系统分析》为基础,进行详细的说明,列出哪个功能涉及多少个函数,以便以后程序员修改、接手和扩展
在实际的开发工作中,我们可能并没有充足的时间去完成一份详实的文档,在遇到一个较大规模的系统性问题时,才有可能写一份概要的、内容上涵盖:“功能要求”+“技术分析”+“系统分析”+“功能函数、数据库文档”的大杂烩文档,但是这样的机会确实要视各个公司情况而定,而且需要手写文档、人工维护,如果公司给到的时间并不充足,那么平时的软件迭代、维护工作就会遇到一些障碍。
是否可以自动维护文档
我们上面的背景聊得有点多了,可能大家在平时的工作中也接触过服务端输出的接口文档(毕竟是刚需),除去手动维护文档,也可以利用注解生成文档,比如swagger之类的。那么我们是否可以在移动端领域中,使用类似的机制,为我们生成一些有效的文档、或者是文档的前期材料呢?
这里呢先岔开说一段,我们都接触过javadoc,也可以直接将javadoc生成文档,但是这种生成的文档我们使用的可能性是很低,javadoc往往仅作为阅读代码时的辅助。我们不考虑使用这种方式生成独立的文档。
好了,让我们来回忆一下,我们在软件维护工作中,我们最希望获得哪些信息,例如:
- 系统中的角色、角色的功能
- 某些类的实际含义,界面对应的类,界面的路由
- 某些特定功能的函数
- 一段代码的主要逻辑 等等
这里有些信息是方便我们正向阅读代码的,如“系统中的角色、角色的功能”;有些信息是为了方便反向查找代码,如界面对应的类、界面的路由,特定功能的函数等。这些信息我们可以用注解的方式,写入源代码之中,并且可以利用一定的机制、直接整合为文档输出,直接作为手册文档或者某类文档的素材;而一段代码的主要逻辑,以代码注释的形式更有意义。
说到这里,有经验的同学都会想起Annotation Processor技术,本篇博客中的内容也是基于使用APT的注解处理实现的,但是绝不是唯一选择。
注解处理器是(Annotation Processor)是javac的一个工具,在编译时扫描、处理注解(Annotation)
Annotation Processing Tool (APT),一个插件工具,可以让Android的编译过程中使用注解处理器,后被Google官方方案替代,但这个名词一直被沿用
Talk is cheap,here is the code
https://github.com/leobert-lan/ReportPrinter 这是sample项目和库源码。
熟悉套路的同学会直接去寻找AbstractProcessor的实现类,去查看主要逻辑,所幸代码不是太长,我们直接全贴上来
@AutoService(Processor.class) @SupportedOptions({KEY_MODULE_NAME, MODE, ACTIVE, CLZ_WRITER}) public class ReportProcessor extends AbstractProcessor { private Set<ReporterExtension> extensions; private final ClassLoader loaderForExtensions; private Logger logger; private Elements elements; private String module; private Mode _mode; private State _state; private WriterType _writerType; private Filer filer; public ReportProcessor() { this(ReportProcessor.class.getClassLoader()); } private ReportProcessor(ClassLoader loaderForExtensions) { this.loaderForExtensions = loaderForExtensions; this.extensions = null; } @Override public synchronized void init(ProcessingEnvironment env) { super.init(env); elements = env.getElementUtils(); logger = new Logger(env.getMessager()); String mode = ""; String state = ""; String writerType = ""; Map<String, String> options = env.getOptions(); if (MapUtils.isNotEmpty(options)) { module = options.get(KEY_MODULE_NAME); mode = options.get(MODE); state = options.get(ACTIVE); writerType = options.get(CLZ_WRITER); logger.info(">>> module is " + module + " mode is:" + mode + " state is:" + state + " writerType is:" + writerType + " <<<"); } if (module == null || module.equals("")) { module = "default"; } _mode = Mode.customValueOf(mode); _state = State.customValueOf(state); _writerType = WriterType.customValueOf(writerType); filer = env.getFiler(); try { extensions = ImmutableSet.copyOf(ServiceLoader.load(ReporterExtension.class, loaderForExtensions)); StringBuilder tmp = new StringBuilder(); for (ReporterExtension ext : extensions) { tmp.append(ext.getClass().getName()).append(" ; "); } logger.info(">>> check extensions:" + tmp.toString()); // ServiceLoader.load returns a lazily-evaluated Iterable, so evaluate it eagerly now // to discover any exceptions. } catch (Throwable t) { StringBuilder warning = new StringBuilder(); warning.append("An exception occurred while looking for ReporterExtension extensions. " + "No extensions will function."); if (t instanceof ServiceConfigurationError) { warning.append(" This may be due to a corrupt jar file in the compiler's classpath."); } warning.append(" Exception: ") .append(t); logger.warning(warning.toString()); extensions = ImmutableSet.of(); } } @Override public SourceVersion getSupportedSourceVersion() { return SourceVersion.latest(); } @Override public Set<String> getSupportedAnnotationTypes() { Set<String> supportedAnnotations = Sets.newLinkedHashSet(); for (ReporterExtension ext : extensions) { supportedAnnotations.addAll(ext.applicableAnnotations()); } return supportedAnnotations; } @Override public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) { try { internalProcess(set, roundEnvironment); } catch (Exception e) { logger.error(e); } return false; } private boolean internalProcess(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) throws Exception { if (State.Off.equals(_state)) { logger.warning(">>> reporter off"); return false; } if (CollectionUtils.isNotEmpty(set)) { boolean handleByAnyOne = false; for (ReporterExtension ext : extensions) { Set<String> targetAnnotations = ext.applicableAnnotations(); Map<String, List<Model>> previousData = new HashMap<>(); for (String anno : targetAnnotations) { List<Model> modelsForOneAnnotation = new ArrayList<>(); TypeElement annoType = elements.getTypeElement(anno); Set<? extends Element> hitElements = roundEnvironment.getElementsAnnotatedWith(annoType); for (Element element : hitElements) { Model model = Model.newBuilder() .annoType(annoType) .element(element) .elementKind(element.getKind()) .name(findElementName(element)) .build(); modelsForOneAnnotation.add(model); } previousData.put(anno, modelsForOneAnnotation); } Result result = ext.generateReport(previousData); if (result == null) continue; handleByAnyOne = handleByAnyOne | result.isHandled(); if (result.isHandled()) { if (Mode.MODE_FILE.equals(_mode)) generateReport(result); else generateExtReportJavaFile(result); } } // return handleByAnyOne; change to always false } return false; } private String findElementName(Element element) { String name = "unknown element name"; try { if (element.getKind().isClass() || element.getKind().isInterface()) { String path = element.getEnclosingElement().toString(); String simpleName = element.getSimpleName().toString(); name = path + "." + simpleName; } else {//field name = findLocation(element); } } catch (Exception e) { logger.warning(e.getMessage()); } return name; } private String findLocation(Element element) { String path = ""; if (element != null) { Element parent = element.getEnclosingElement(); if (element.getKind().isClass() || element.getKind().isInterface()) { String p = element.getEnclosingElement().toString(); String s = element.getSimpleName().toString(); return p + "." + s; } return findLocation(parent) + "#" + element.toString(); } return path; } private void generateReport(Result result) { String fileName = Utils.generateReportFilePath(module + result.getReportFileNamePrefix(), result.getFileExt()); logger.info("generate " + fileName); if (Utils.createFile(fileName)) { Utils.writeStringToFile(fileName, result.getReportContent(), false); logger.info("generate success"); } else { logger.info("generate failure"); } } private void generateExtReportJavaFile(Result result) { String fileName = Utils.genFileName(module + result.getReportFileNamePrefix(), result.getFileExt()); logger.info("generate " + fileName); Writeable writeable = getWriteable(); Utils.generatePrinterClass(Utils.genReporterClzName(module + result.getReportFileNamePrefix()), fileName, result.getReportContent(), writeable); logger.info("generate success"); } private Writeable getWriteable() { if (WriterType.Custom.equals(_writerType)) { return Writeable.DirectionWriter.of(new File("./" + module + "/ext")); } else { return Writeable.FilerWriter.of(filer); } } }
讯享网
经过简单的代码阅读,我们了解到:
- 在init中获取了用户配置、并且做了这样一件事:
讯享网ServiceLoader.load(ReporterExtension.class, loaderForExtensions)
- 在获取支持的的注解时,我们用了第一步中得到的ReporterExtension
- 在 public abstract boolean process(Set<? extends TypeElement> var1, RoundEnvironment var2); 的实现中,对扫描到的被注解的类或者类成员,并转换为数据模型后按照不同的配置进入处理
- 在处理时,我们使用了第一步中的到的ReporterExtension,并将处理完的结果,输出为文件。
主要逻辑就是上面这些内容,所以我们要去看看第一步到底做了啥。
ReporterExtension何许人也?一个接口而已。
讯享网public interface ReporterExtension { Set<String> applicableAnnotations(); Result generateReport(Map<String,List<Model>> previousData); }
定义了两个方法,可以处理哪些注解,生成文档内容。大家应该都听说过SPI机制,其实AnnotationProcessor也是SPI机制的一种利用。下面我会费点笔墨介绍下SPI机制,熟悉的同学可以略过下一节
SPI机制
Service Provider Interface(SPI):是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
用大白话来说,框架开发人员实现了一套系统,这套系统的主流程都已经实现好了,但是流程中的某些细节部分,是需要交给业务方的。比较理想的情况是框架对业务方没有啥代码边界,框架的初始化过程基本透明,那么可以进行依赖注入,但是这个情况是比较理想的,即使要求每个框架开发团队都自己干一套,也没法实现统一性😂。
于是乎诞生了这么一套服务发现机制,约定了要使用这套机制,必须使用标准服务接口,例如我们上面提到的ReporterExtension,然后业务方实现接口,并在resources/META-INF/services/目录下创建一个和标准服务接口对应的文件,以本文代码为例:
文件名为:osp.leobert.android.reportprinter.spi.ReporterExtension,其内容为实现类的全类名。
以上的内容,都只是“约定”,因为实际运行时,框架层的代码和业务层的代码都会加载到同一个虚拟机中,彼此是“透明”的,就可以使用反射获取对象实例了。而ServiceLoader类可以按照上述的“约定”,寻找到标准服务接口的实现类并且通过反射生成实例。具体的代码就不展示了,有兴趣的同学还请自行查阅。
其实AutoService也是和SPI机制有关的,他可以帮助我们在对应目录下生成此文件。
为什么要使用SPI机制
其实这是一个顺理成章的事情,作为框架层,我并不清楚使用者到底需要为哪些信息生成文档,并不清楚文档内容的组织形式,所以基于约定,框架先询问业务方需要收集哪些注解信息,然后扫描源码寻找信息,将注解和被注解者的信息打包交给业务方,业务方处理后生成文档的实际内容交给框架,框架按照配置信息输出文档。
自定义:如何按照SPI机制扩展自己的文档生成器
先介绍下DEMO中的内容,实际操作时可以对照:
- ':kotlin_sample' :在kotlin项目中的使用演示
- ':sample':在Java项目中的使用演示
- ':ReportNotation':标准服务接口等,老版本中还有一些注解,即默认包含了一些可被生成的文档,后被移除
- ':report-anno-compiler':注解处理器
- ':DemoReporterExt':一个例子,可以为“@Demo”注解生成文档
- ':utils_reporter':一个例子,可以为“@Util”注解生成文档,并利用了net.steppschuh.markdowngenerator:markdowngenerator按MarkDown语法编辑文档
我们可以参考后两个去按需扩展。
我们以DemoReporterExt为例简单阅读一下代码。
首先声明依赖:
implementation project(":ReportNotation") //目前发布到仓库的版本:osp.leobert.android:ReportNotation:1.1.2 annotationProcessor 'com.google.auto.service:auto-service:1.0-rc3' implementation 'com.google.auto.service:auto-service:1.0-rc3'
然后声明你需要的注解:
讯享网package osp.leobert.android.reporter.demoext; public @interface Demo { String foo(); }
target和Retention用默认的也就够了,或者按需定义。
定义服务接口实现
package osp.leobert.android.reporter.demoext; import com.google.auto.service.AutoService; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import osp.leobert.android.reportprinter.spi.Model; import osp.leobert.android.reportprinter.spi.ReporterExtension; import osp.leobert.android.reportprinter.spi.Result; @AutoService(ReporterExtension.class) public class FooReporter implements ReporterExtension { @Override public Set<String> applicableAnnotations() { return Collections.singleton(Demo.class.getName()); } @Override public Result generateReport(Map<String, List<Model>> previousData) { if (previousData == null) return null; List<Model> demoModels = previousData.get(Demo.class.getName()); if (demoModels == null || demoModels.isEmpty()) return Result.newBuilder().handled(false).build(); StringBuilder stringBuilder = new StringBuilder(); for (Model model : demoModels) { Demo demo = model.getElement().getAnnotation(Demo.class); String foo = demo.foo(); stringBuilder.append(model.getElementKind().toString()) .append(" :") .append(model.getName()) .append("\r\n") .append("foo:") .append(foo) .append("\r\n"); } return Result.newBuilder() .handled(true) .reportFileNamePrefix("Demo") .fileExt("md") .reportContent(stringBuilder.toString()) .build(); } }
直接用AutoService帮助生成resource文件。applicableAnnotations 方法中返回的Set只包含Demo注解的全类名,如果关心多个注解,就都加上,注意,一个扩展只能生成一份文档,如果需要对不同的注解生成不同的文档,那就要实现多个扩展,而且必须拆分到不同的Module。
在 Result generateReport(Map<String, List<Model>> previousData) 方法中取出关心的注解,并按照自己的格式生成文档信息。
最终返回一个Result模型。
是不是很简单? 我们看看Sample项目中的使用
讯享网@Demo(foo = "foo of demo notated at clz") public class SampleClz { // @ChangeLog(version = "1.0.0", // changes = { // "f1", // "f2" // }) @Demo(foo = "foo of demo notated at function") private void foo(Object bar) { } @Demo(foo = "foo of demo notated at field") private int i; }
我们得到了下面的内容:
CLASS :osp.leobert.android.reportsample.SampleClz foo:foo of demo notated at clz METHOD :osp.leobert.android.reportsample.SampleClz#foo(java.lang.Object) foo:foo of demo notated at function FIELD :osp.leobert.android.reportsample.SampleClz#i foo:foo of demo notated at field
看起来也很简单,毕竟我们的实现很简单😂
可以做什么
这是一个值得关注的问题,我们可以拿这个来做什么?
- 我们可以输出一些规模庞大的类簇信息,例如某系统中使用解释器模式,类确实会比较多,维护一份文档的价值是比较高的,尤其是业务变动还比较频繁时,可以实时维护;又如应用中自定义的UI、试图组件等
- 用来实现TODO或者在代码Review过程中标记一些改善计划
- ChangeLog
- 工具类、工具方法标记并生成手册
- 收集废弃类使用信息或者Lint抑制信息(SuppressWarning、SuppressLint等)
还有其他方式吗?
这里就不卖关子了,我们除了可以利用AnnotationProcessor去收集注解信息,也可以利用gradle编译时扫描的时机,更有甚者可以利用运行时反射创造扫描时机。
举个例子,我们可以另行创建一个Java项目或者Web-Service项目(例如基于Spring-Boot的项目),将源码的sourceset配置给此项目,配置要扫描的package,在运行时扫描package下的类,继而收集注解信息(Retention必须是Runtime了)并生成文档。如果你还想继续折腾,可以利用模板引擎,将md文件转为Html,这样你就拥有了“线上文档”;合理的采用归档机制,还可以变成一个可持续更新、可版本回溯的文档😂。当然,这都是折腾了,没有实际需求的话,简单了解下就好了,坦率的说,这些方式我在2018年都折腾过,利用Web-Service项目运行时反射生成代码的Demo都找不着了😂,因为实际需求并用不上🤣。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/61349.html