基于Java注解和模块化生成树形业务文档的实践

一、前言

一个新人快速掌握一个新系统业务逻辑的最好的工具是什么,是看代码?是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){
    }

如果我们能拿到所有类的注解信息,然后根据模块注解与域名注解的关联,就可以生成一个文档,类似如图:

screenshot.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);
                }
            }

        }
    }

打印出如下效果:

screenshot.png

其中,application:onlinecout为树形的根,mouduleName:evidence和mouduleName:trialing是根节点的两个孩子节点。然后mouduleName:evidence下面又有了domainName:proofevidence和domainName:confrontationevidence孩子节点....

每个域服务还都列出了函数签名,参数说明,返回值说明。

五、总结

其实既然已经获得了注解信息,我们可以根据需要比如生成markdown文件,PDF,或者直接把数据扔给前端,前端按照需要格式渲染都可以。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大闲人柴毛毛

轻量级线程池的实现

写在前面 最近因为项目需要,自己写了个单生产者-多消费者的消息队列模型。多线程真的不是等闲之辈能玩儿的,我花了两个小时进行设计与编码,却花了两天的时间调试与运...

5104
来自专栏JadePeng的技术博客

RPC框架原理与实现

RPC,全称 Remote Procedure Call(远程过程调用),即调用远程计算机上的服务,就像调用本地服务一样。那么RPC的原理是什么呢?了解一个技术...

7537
来自专栏安恒网络空间安全讲武堂

从零基础到成功解题之0ctf-ezdoor

2144
来自专栏用户2442861的专栏

Java NIO使用及原理分析 (一)

最近由于工作关系要做一些Java方面的开发,其中最重要的一块就是Java NIO(New I/O),尽管很早以前了解过一些,但并没有认真去看过它的实现原理,...

862
来自专栏大史住在大前端

webpack4.0各个击破(5)—— Module篇

使用webpack对脚本进行合并是非常方便的,因为webpack实现了对各种不同模块规范的兼容处理,对前端开发者来说,理解这种实现方式比学习如何配置webpac...

1282
来自专栏乐百川的学习频道

Vert.x学习笔记(一) Vert.x 核心包

Vert.x是一个事件驱动的JVM上的框架,可以帮助我们构建现代、灵活、可扩展的程序。Vert.x有多种语言的版本,可以用在Java、Kotlin、Scala、...

7189
来自专栏服务端技术杂谈

JAVA NIO内存泄漏

前言 写NIO程序时,经常使用ByteBuffer来读取写入数据,那使用ByteBuffer.allocate()还是ByteBuffer.allocateDi...

3428
来自专栏java 成神之路

NIO 之 Channel

36513
来自专栏分布式系统和大数据处理

C#网络编程(同步传输字符串) - Part.2

在与服务端的连接建立以后,我们就可以通过此连接来发送和接收数据。端口与端口之间以流(Stream)的形式传输数据,因为几乎任何对象都可以保存到流中,所以实际上可...

1233
来自专栏Android机器圈

Java设计模式总汇二(小白也要飞)

PS:上一篇我介绍了适配器设计模式、单例设计模式、静态代理设计模式、简单工厂设计模式,如果没有看过第一篇的小火鸡可以点这个看看http://www.cnblog...

3439

扫码关注云+社区

领取腾讯云代金券