基于Java注解和模块化生成树形业务文档的实践
阿里巴巴长期招聘Java研发工程师p6,p7,p8等上不封顶级别,有意向的可以发简历给我,注明想去的部门和工作地点:1064454834@qq.com
欢迎关注微信公众号:技术原始积累 获取更多技术干货
一、前言
一个新人快速掌握一个新系统业务逻辑的最好的工具是什么,是看代码?是debug?是看uc?是看demo?答案应该都不是,因为看代码和debug一来太耗时,二来系统大了业务逻辑错综复杂,很多业务模块耦合在一起,很难通过debug来理清所有业务,而uc和需求demo又都是零散在confluence不同的地方,并没有一个完整的业务介绍流图,即使有也是很早之前的,随着小需求的不断迭代,业务逻辑早就不是这样了。
所以为了不然代码那么混乱,耦合那么严重,可以采取模块化思想,每个功能模块只对外提供一个service,其他模块不能调用该模块的bo,这个可以通过微服务来实现,但是微服务太重,比如我一个应用有10个模块,总不能搞10个应用吧,基于webx的应用可以通过的子容器实现ioc级别隔离,也可以使用classloader实现cl级别的隔离,这就是模块化。然后如果采用了领域模型,则一个模块内有会有多个域服务。
有了模块化后,那么就要解决如何在小需求不断迭代的情况下维护一个全局的业务文档,这个文档是一个树形结构,树的根是应用名称,树的第二次是应用的模块,第三次则是每个模块中的域服务.....
二、基于注解生成树形业务文档思路
基于上面介绍一个应用划分为若干个模块,每个模块含有若干个域服务,每个域服务内又有可能有若各子域,设计三类注解:
//模块类上面加的注解
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ModuleAnnotation {
String moduleName() default "";
String moduleDesc() default "";
}
其中moduleName是模块的名字要保证应用唯一,moduleDesc是当前模块的描述。
//域服务类或者方法上面添加的注解
@Target({ ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DomainAnnotation {
String moduleName() default "";
String rootDomainName() default "";
String rootDomainDesc() default "";
String subDomainName() default "";
String subDomainDesc() default "";
String returnDesc() default "void";
}
//在域服务接口的参数上,为了获取参数名字和描述使用
@Target({ElementType.METHOD,})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Param {
String paramName() default "";
String paramType() default "";
String paramDesc() default "";
}
其中moduleName说明当前域服务属于哪个模块,rootDomainName,rootDomainDesc是跟域服务名称和描述,当前模块下域服务名称要唯一,subDomainName,subDomainDesc为子域,可以为多个中间用英文逗号分隔,returnDesc是返回值说明后面会知道。
使用时候模块注解加载类上:
@ModuleAnnotation(moduleName="trialing",moduleDesc="庭审模块")
public class moduleclass{}
域服务加载方法上(没有子域):
@DomainAnnotation(moduleName="trialing",rootDomainName="seaDomain",rootDomainDesc="纯语音庭审服务")
public void m2New() {
}
域服务加载方法上(有子域):
@DomainAnnotation(moduleName="trialing",rootDomainName="videoDoamin",rootDomainDesc="视频庭审服务",subDomainName="speechDoamin,speechVideoDoamin")
public String hello(@Param(paramName="type",paramDesc="案件类型")String type,@Param(paramName="num",paramDesc="案件个数")String num){
}
@DomainAnnotation(rootDomainName="speechDoamin",rootDomainDesc="语音识别服务")
public String hello2(@Param(paramName="caseId",paramDesc="案号")Long caseId){
}
@DomainAnnotation(rootDomainName="speechVideoDoamin",rootDomainDesc="视频+语音识别服务")
public String hello3(@Param(paramName="name",paramDesc="姓名")String name,@Param(paramName="address",paramDesc="地址")String address){
}
如果我们能拿到所有类的注解信息,然后根据模块注解与域名注解的关联,就可以生成一个文档,类似如图:
![](https://img.haomeiwen.com/i5879294/ac9df89d4f632d91.png)
三、如何收集注解信息
本文选择了Spring bean实例化生命周期的InstantiationAwareBeanPostProcessor扩展,该扩展留出的回调函数:
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
}
springbean创建过程中有那么多扩展,为何偏偏选这个那。第一,一般都会对bo做事务拦截增强,所以如果在bean实例化后的扩展接口,那么拿到的是代理后的bean,要从代理后的bean获取注解必须先使用aop工具类AopUtils.getTargetClass(Object)获取target,然后从target才能获取注解信息,而本文选的扩展接口是在bean实例化前,拿到的是大Class。
获取注解信息思路:
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
//是否开启收集注解
if (!isOpenAnnotation) {
return null;
}
//第一步收集模块类注解信息到moudleAnnotationList
//第二步收集域服务类上面的注解到domainAnnotationList
//第三步收集域服务方法上面的注解到domainAnnotationList。
//第四步收集method上的Param注解获取参数名称和参数描述,拼接函数签名,把信息放入domainMethodMap。
}
其中第四步,一开始我想要使用LocalVariableTableParameterNameDiscoverer获取参数名字,但是咨询@千臂,后知道这个方法不一定能获取的到,因为这个方法是从字节码获取的,而有时候把这些原信息放入到class中会增加class的大写,一些场景下会被优化掉。所以选择了参数注解获取参数,再次谢谢千臂,不然后面采坑怎么死的都不知道^^
其中domainMethodMap构造为:
Map<DomainAnnotation, AnnotationInfo> domainMethodMap = new HashMap<>();
public class AnnotationInfo {
private String methodSign;//存放函数签名
private Map<String,String> paramsDesc = new HashMap<>();//存放参数名和参数描述
}
isOpenAnnotation是在注入InstantiationAwareBeanPostProcessor实例时候配置的属性变量,用来控制是否开启注解收集,日常环境可以开启来收集注解,生成文档,线上则可以选择关闭。
四、如何分析打印注解信息
上节已经收集到了模块注解信息,域服务注解信息,和方法签名和方法参数的信息,下面看如何分析这些注解并打印,理论上拿到了这些信息后,只要有一个建树算法,就可以生成一个树形文档,本文则是简单的递归打印:
private void printTree() {
List<DomainAnnotation> domainList = AnnotationInstantiationAwareBeanPostProcessor.getDomainAnnotationList();
List<ModuleAnnotation> moudleList = AnnotationInstantiationAwareBeanPostProcessor.getMoudleAnnotationList();
//打印树根
System.out.println("application:onlinecout");
for (ModuleAnnotation ma : moudleList) {
String moudleName = ma.moduleName();
String moudleDesc = ma.moduleDesc();
此处打印模块信息
for (DomainAnnotation da : domainList) {
if (da.moduleName().equals(moudleName)) {
// 打印当前域服务
printMethodInfo(da, 2, '-');
// 打印子域服务
generateSubDoamin(domainList, da.subDomainName(), 3);
}
}
System.out.println();
}
}
private void generateSubDoamin(List<DomainAnnotation> domainList, String domainName, int n) {
String subDomains[] = domainName.split(",");
for (DomainAnnotation da : domainList) {
for (String domain : subDomains) {
if (domain.equals(da.rootDomainName())) {
// 打印方法
printMethodInfo(da, n, ' ');
// 打印子域服务
generateSubDoamin(domainList, da.subDomainName(), n + 1);
}
}
}
}
打印出如下效果:
![](https://img.haomeiwen.com/i5879294/65dd953fe26329e2.png)
其中,application:onlinecout为树形的根,mouduleName:evidence和mouduleName:trialing是根节点的两个孩子节点。然后mouduleName:evidence下面又有了domainName:proofevidence和domainName:confrontationevidence孩子节点....
每个域服务还都列出了函数签名,参数说明,返回值说明。
五、总结
其实既然已经获得了注解信息,我们可以根据需要比如生成markdown文件,PDF,或者直接把数据扔给前端,前端按照需要格式渲染都可以。
欢迎关注微信公众号:技术原始积累 获取更多技术干货
![](https://img.haomeiwen.com/i5879294/2c414099893f4cbe.png)