您现在的位置是:首页 > 文章详情

利用java反射和java-parser制作可以迭代、分布式、全栈式代码生成器的研究

日期:2019-09-28点击:514

利用java反射和java-parser制作可以迭代、分布式、全栈式代码生成器的研究

作者:niaoge(qq:78493244)

摘要:

全面的代码生成器在减少开发成本,减少代码维护成本,降低运行bug上起着至关重要的作用。然而,通用的代码生成器生成代码是固定式、覆盖式、单次性,不能很好兼容拓展技术和迭代开发,一旦在生成代码上编辑,维护和后期再次生成代码将是灾难性的。另外,虽然服务端工作是整体项目的起点,但是随着nodeJS,native语言兴起,客户端代码事实独立于服务端项目,不再是服务端的MVC的一部分,从服务端到客户端出现代码逻辑上的断层,需要人工根据文档检测逻辑,难以从全栈角度把控代码。本文探讨已经开源的StateGen(https://github.com/stategen/stategen)框架中的代码生成器技术,它采用java反射技术,结合java-parser技术,有效地解决上述代码生成器的缺点,将代码生成器提高到支持迭代开发生成、全栈式生成的高度。本文作者相信,随着StateGen的使用和StateGen自身技术的升级提高,必将使企业的开发、维护成本、线上bug大大降低。

关键词:代码生成器;服务端;客户端;全栈式;迭代;分布式;React; native; flutter

0 引言:

全文将分别从服务端、客户端代码生成,以及两端相配合的方式,从整栈的高度探讨StateGen是如何在节给开发成本,防止前后端开发脱节等一系列问题的原理和实现。除相关使用的技术外,本文中代码生成器使用了模板语言。本文中的系统指的分布式系统中的系统,如:vip系统、trade系统.**

1 服务端代码自动生成

1.1 在已编辑的domain/pojo类上首次、多次生成代码,避免DTO的使用和维护

1.1.1 首先我们明确:失血模式的domain/pojo只是业务代码中自定义数据格式,它与数据库中的表是一对一的关系,由表生成,它并不会暴露业务代码逻辑,也和数据安全没有直接的关系。DTO的存在主要用作用是弥补通用代码生成器在生成domain/pojo上的不足,这主要体现在以下方面:

A. domain不能自定义继承于其它类,特别是分布式系统中不能继承自外部系统的domain.

B. domain不能实现于自定义接口

C. domain不能自定义成员变量

D. domain不能自定义getter/setter方法

E. 基于以上自定义必须的引用类,domain不能自定义import.

1.1.2 因此,当自动生成的domain存在上述不足时,需要在业务代码中额外使用DTO封装数据。但是,维护DTO的成本巨大.DTO往往与domain在代码存在诸多重复,重复和相似部分容易误导开发人员。

重复可能是软件中一切邪恶的根源。—— Robert C.Martin

1.1.3 由于DTO需要与domain相互转换数据,以下两种方式都会在迭代中产生意外的bug。

A. 采用Spring的  BeanUtils.copyProperties方法:

a) 该方法避免字段逐个用代码实现转换,但是要求domain与DTO相应字段类型不能存在差异.否则可能会引起运行时数据转换异常.但是人工很难保证大量的DTO中各字段类型与domain中相应的类型一致和正确。

b) 当字段名细微差别时,开发中,人工很难区别出来时,会导致数据转换时丢失,如username与userName.

B. 由于以上方式的缺定,一些公司要求在业务中维护自定义转换器来对字段逐个转换,但是也存在以下优缺点:

a) 该类转换器优点是在ide中即能反应转换bug,但需要手工改。

b) 在分布式系统中,迭代导致的字段改变,往往只牵涉到上游代码和下游代码,转换器导致不涉及业务的中间代码必须做相应的更改,往往导致意外bug,容易引起一系列的发布工作。

1.1.4 从实际各代码生成器来看,MyBatis Generator,Hibernate Generator以及 dalgen都不能避免上述问题,这种先天不足,导致迭代后线上容易出现bug。因此,DTO不能很好地响应迭代开发,是迭代开发中的绊脚石.

1.1.5 StateGen在传统的代码生成器技术上运用java-parser解决上述问题,方法如下:

A. 生成的代码用java-parser 解析出AST语法树,为dest

B. 目标代码如果存在的话,java-parser 解析出AST语法树,为source

C. 以source的成员,逐个在dest中查找,在查找不到的情况下

a) 将成员变量(field)放置在dest中的 serialVersionUID成员变量的上面,以便开发人员清楚:该field是自定义field还是因迭代导致的多余的

b) 将getter/setter方法放置在toString()方法的下面,至于 getter/setter中引用的成员变量不存在时,ide/编译器会及时发现bug.

c) 将import部分加入到dest的imports部分

d) 将继承类加入到dest的继承类中

e) 将接口加入到dest的接口中

f) 保存dest到目标文件

1.1.6 经过以上操作后,我们的自动代码生成器生成的domain,不但适合新表,也非常适合迭代中domain的迭代,同时避免了使用DTO。由于domain是项目中的基础代码,它的自由决定了上层代码不需要绕弯。

1.2 DAO层/数据持久化的生成规则

1.2.1 DAO代码与真正的持久化工具是调用关系,因为DAO中用到持久化工具采用依赖倒置原则(DIP),自动生成的DAO代码可以相对固定,由于采用java-parser在保存前分析已生成的代码,只需要把原来代码中import或需要的import中加入即可。它避免把无关的包引入导致ide如eclipse出现大量感叹号提示,且不能消除的问题.

1.2.2 表对应的DAO Spring bean显示配置,它简化了人工配置的麻烦

1.2.3 表对应的DAO 对应的 配置文件,如 iBatis/Mybatis 的mapping.同样,简化了人工配置和书写mapping的麻烦.

1.3 Service代码生成规则 :

1.3.1 Service代码生成分为6个部分,2个jar包,如下:

A. 分布式系统中对外Service接口类:它以分布式系统中提供服务的当前系统名为后缀,如本系统名为Trade, UserService即为UserServiceTrade, 解决以下二个问题

a) 让开发人员清楚当前代码中用到的服务来自本地还是来自分布式系统中的某个系统

b) Spring技术中,引用的外部服务中的bean name与本地applicationContext.xml中 bean name可能冲突的问题

B. 本地Service接口:它继承A中自对外Service,如UserService继承自UserServiceTrade

C. 本地Service实现类,它实现B中接口。

D. 分布式系统对外配置文件,

a) 文件名以本地系统名标示,路径为:classpath*:META-INF/facade/facade-trade.xml,

b) 与A一起被编译成jar包供其它系统引用。避免外部系统引用服务时手写配置文件工作量,特别是迭代中,手写配置很难发现兼容问题,易导致bug

E. 本地Service bean显示配置文件**

F. 分布式系统中,本地service对外注册服务的配置文件,为了避免D中的配置2个问题:

a) 被逐个手写引入的问题,不利于快速生成代码和迭代中手工维护引用。

b) 本地系统引用了本地对外的服务导致bean name重名,系统不能启动.

解决方法:只需要在applicationContext.xml中用正则表达式配置排除引入本地系统即可,如本地系统叫trade

<import resource="classpath*:META-INF/facade/facade-:{?!.*(trade)}*.xml"/>

1.3.2 Service接口、特别是Service的实现部分impl是业务代码的核心,既能生成代码,也能保留之前的开发成果,在其上持迭代生成才能减少后端工作量。用java-parser处理已有的代码可以很好地解决这些问题,但是处理方法与domain处理相反,方法如下:

A. 对 1.3.1中的A或B或C对应的已存在的目标代码,用java-parser解析为AST语法树,为dest,

B. 对 1.3.1中的A或B或C对应的生成代码,用java-parser解析为AST语法树,为source

C. 用source中的各成员,逐个在dest中查找,方法名称区分大小写,当不存在的情况下,

a) 向dest中添加成员(field,method)

b) 向dest中添加实现类

c) 向dest中添加实现接口

d) 向dest中添加由上述a)b)c)导致的自定义引用和必须引用.

由a)b)c)d)导致的代码bug,ide/编译阶段即能显示发现.

D. 保存dest到目录文件

1.3.3 经过以上操作后,在分布式系统中,本地服务将直接用本地内存中调用Spring bean,只有远程bean才从分布式中调用,service适合自由伸缩的分布式,也可以非分布式,它经过预先java-parser分析处理,可以保留之前的开发成果,因此非常迭代开发。

2 利用服务端代码生成客户端/前端代码

2.1 充分利用服务端已有的代码,其优势和需要解决以下问题:

A. 一套逻辑,尽量避免服务端和客户端分别开发。往往,代码的最先逻辑开发在服务端,如果能充分利用服务端已有代码的逻辑生成相配套的客户端代码,那么将大大减轻客户端开发的工作量,并且减少客户端bug的产生等相关一系列问题.

B. 利用约定大于配置,做到可以约定也可以自定义配置的开发需求。减少工作量,同时也方便代码生成器生成

C. 避免迭代开发中,客户端不能在ide/编译阶段发现兼容性bug.

D. 避免服务端必须手工制作API文档诸多问题:制作耗时、不准确、客户端开发人员理解偏差导致的开发bug

E. 客户端各种技术层出不穷,生成的代码适应现在和将来技术,如nodeJs/react/vue,android原生代码,iOS原生代码,flutter…,要避免生成器生成代码过时,或不能升级给限制了企业升级技术。

F. 避免同一套代码,多客户端开发的人工成本问题。

G. 避免前端人工手写网络调用、反序列化、与服务端交互代码,耗时,容易出错。

H. 当以上工作解决将,将给大大减少前端开发人员调试,测试人员的工作量,也减少后端支配前端开发的工作量.

2.2 从服务端代码API生成客户端代码的可行性、非脚手架式一次性生成代码的分析:

A. 真正的服务端API不是简单通用的crud的demo,而是有着复杂的业务逻辑,对应前端也是相僦的业务逻辑,因此不适合网页配置生成和脚手架式生成.我们需要的是一个支持任意后端api到前端代码的生成器。

B. 经研究,在响应式技术的前端,对于后端一个任意给定的API,其对应的前端网络调用、状态化、交互代码都是特定的,没有歧义,或者经过适当改造后,没有歧义。如些,没有歧义的前端代码理论上可以用代码生成器制作。

a) 更新api应返回新的状态数据

b) 插入api应返回新的状态数据

c) 删除api应返回被删除的id集合

C. 经过分析,同一作用域中,任何调用的API返回值与之前状态中的数据作用生成新的数据状态化数据时,类型有且只有以下3种:

a) 重新加载

b) 按主键增加或更新

c) 按主键删除

D. 前端数据状态化时,数据隔离分析和解决方法:

a) 后端不同的Spring Controller对应前端用相应的model/provider隔离,保证前端可以相互访问,同时数据不相互污染。

b) 同一个后端Spring  Controller内对应前端的model/provider内, 按后端返回值类型或指定类型隔离,userArea,topicArea, 以User.java为列,以下状态化到前端的数据都是在userArea隔离下

User
List
PageList
@State(area=User.class) public String delete(){}

E. 支持前端最大化地生成代码的同时,也应保证前端代码可自定义,特别是界面上排版、美工部分。因此,生成的代码应尽量采用依赖倒置(DIP)原则。给前端灵活性。

F. 只有利用java反射技术,相比于传统的代码生成器,才能既不增加前、后端工作量,也能保证生成的代码准确和一致性。类似的成功案例:swagger,利用java反射技术生成自动化在线文档。

2.3 生成前端代码前的准备工作:

2.3.1 充分利用版本控制软件,将服务端代码与客户端统一管理,便于服务端到客户端的代码生成,以git版本管理器为例,前端项目应为后端项目的子项目,这样前端可单独开发,后端开发同时,生成前端相应的代码到git子项目中,即前端代码的项目中。这种方式保证迭代中前后端版本一致

2.3.2 扫描后端所有class文件,找出类上标注@Controller的controller,利用java反射解析class文件扫描controller上所有@RequestMaping及@ResponseBody标注的public方法.

2.3.3 解析2.3.2 中方法对应的参数、返回值对应的类,或者泛型中的类/枚举

2.3.4 非原型类或基本类型,解析2.3.3中类各字段/getter方法对应的类/枚举

2.3.5 上面2.3.4中非原型或基本类型再次递归解析

2.3.6 以上数据导入生成模版中。做以下代码生成.

2.4 生成前端代码对应的类/枚举

2.4.1 字段名

2.4.2 类型/泛型类型

2.4.3 字段Label

2.4.4 主键

2.4.5 数据规则

2.4.6 对于flutter ,生成反序列化和序列化代码,多层序列化调用,反序列化的基本类型解析采用依赖倒置(DIP),保证自动代码的兼容性问题。

2.5 生成前端代码对应的网络调用,每个后端controller对用前端相应的apis文件。依次生成method对应的api

2.5.1 域名部分以非固定,以key为区分,可让前端调试,便于前端统一处理,也便于运维做nginx反向代理

2.5.2 以@RequstMapping解析出来的url作为url,

2.5.3 以method解析出来的参数作为入参的参数名和参数型

2.5.4 以method解析出来的返回值作为返回值,

2.5.5 以上生成的代码以依赖倒置(DIP)原则,调用各自真实的网络处理

2.6 生成前端代码对的数据状态化(state)层代码,后端每个controller对应前端model/provider

2.6.1 调用相应api,入参,返回值

2.6.2 根据返回值隔离原则对原前端持有的数据进行,替代增加或更新删除.

2.6.3 前端监听状态中的数据变化更新页面

2.6.4 前端路由生成

2.7 前端字段值渲染生成代码

2.7.1 以后端domain/java bean中字段设置,根据基本类型,采用依赖倒置(DIP)的技术,传入参数,让前端自定义生成,保证前端灵活性.

2.8 生成前端组件编辑框组件

2.8.1 以method中参数对应的基本类型,生成前端代码的组件生成调用函数,采用依赖倒置(DIP),调用客户端自定义代码生成组件

2.8.2 组件的规则对应method中参数的规则

2.8.3 Method中参数规则优先取自自身的java annotation,没有时,查找同级下,复杂类型domain/java bean中的字段,如果有的,取该规则。

3 必要的脚手架代码

3.1 为了让上述自动生成代码的功能生成到相应的目录下,需要提供以下脚手架代码以更生成必要的项目,减少初始化人工成本。

A. 分布式系统初始化脚手架

B. A中 spring-mvc项目,可以有多个并行

C. B 中前端项目初始化脚手架,可以有多个并行

4 总结

4.1 经过以上分析后和实践,StateGen中的代码生成器目前能完成服务端从 domain,dao,service facade ,所有代码, StateGen也行完成Service impl 的大部分代码。以上这些基础代码是规范和健壮的,它保证上层代码的健壮。并且这些代码可以持续生成.因此很好地支持迭代开发。

4.2 StateGen时间完成前端代码从网络调用、数据状态化、交互上的所有代码,对于前端剩下的需要手工制作的工作量为美工和排版。

4.3 在StateGen中,前端能很好和跟据后端api的迭代中的变化自动生成代码,它很好的避免人工调整代代码的有可能导致的遗漏,也减轻了前端的工作量,由于这些生成给前端的代码也是规范性和基础性的,在其之上建立的代码也很更规范。

4.4 StateGen中代码生成器生成的代码,由于采用java-parser分析支持迭代和大量的DIP原则,生成的代码不会阻碍手工代码开发.

原文链接:https://yq.aliyun.com/articles/719675
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章