我们在Web开发框架推导一文中一步步搭建了开发框架。在当时的情况下,还是可以满足需要的。但是随着项目的逐步完善,需求变更的频率逐渐变得高于新需求的频率,原有框架的弊端也越来越明显,需要对框架进行升级和完善。我们先看看原来框架存在的问题,然后再根据这些问题改进框架。原框架问题代码生成问题参数传递问题服务层问题测试依赖问题Mapper.xml问题代码生成问题在原框架中,我们写了一个基于各种约束的代码生成组件。通过这个组件,我们可以针对选中的Table生成Controller、Service、Model、Mapper等一系列类,也就是说只要建好表,就可以直接生成一套CRUD,并且可以直接启动和测试。这在项目开始的时候看起来很美好,但是当需求改变的时候,还是有很多局限性。首先,固化生成的代码逻辑。如果做一些调整,需要调整生成代码的组件,然后重新打包,上传到jar仓库,修改项目中的组件版本,再生成代码。整个过程太麻烦了。其次,为了方便代码生成,其实也做了很多妥协:为了方便修改表字段后重新生成,很多类都抽象出一个操作Model字段的基类。这些基类不能手动更改,因为它们每一代都会被覆盖。这实际上导致了班级数量的增加。生成的CRUD是固化的,无法手动调整。如果生成的CRUD不符合要求,不能直接在代码上修改。只能复制一份进行修改,因为再次生成时会被覆盖。这导致代码冗余。Param和Result委托给Model,这样当Model发生变化时,编译时就可以知道相应字段的调整。但是它也引入了很多问题,我们将在“参数传递问题”一节中单独讨论。参数传递问题为了方便代码生成,决定Param和Result都继承自Model,这就导致了以下问题:Param和Result都依赖于Model。但是Param和Result是视图层模型,而Model是持久层模型,两者的进化程度并不一致。但是目前的继承关系导致视图层模型的演化默认是和持久层同步的。当然你也可以手动调整Param和Result,但是这样就失去了代码生成的优势。Param和Result通过delegation设置字段,即它们实际上没有字段,通过getters和setters将值设置到Model中。这导致无法使用lombok来简化getter和setter,导致更多的Param和Result代码行。同时,对于swagger来说,有些注解需要基于字段,导致某些功能无法实现(例如:ModelAttribute),只能基于额外的手段来处理(例如:字段文档需要通过ApiImplicitParams实现)。CRUD是基于相同的Param和Result,导致前端界面显示很多无用的字段,导致前端更难理解界面Servicelayerproblemsservice层存在以下问题:service层的职责太多,包括事务处理、参数设置和业务逻辑,导致Service中的代码都是面条代码,不利于业务逻辑的理解。同时直接在类中添加事务注解。Spring默认的事务机制会导致类似如下代码的逻辑调用不会抛出预期的异常这里,会是一个TransactionRollBack异常discussService.save(discuss);}}//discussServicepublicStringsavePost(PostDiscussdiscuss){thrownewRuntimeException("Failedtosave");}测试的核心业务逻辑取决于问题。在Service中,测试还是需要依赖Spring。当项目越来越大的时候,启动项目的时间也越来越长,可能1分钟或者更多。这导致单元测试的效率越来越低。关于Mapper.xml的问题面试的时候经常会问这样的问题:接口在Java中的作用是什么?Service和DAO为什么要写接口然后实现这个接口呢?接口和实现在同一个模块下,反正要重新打包。多写接口不就是多写几行代码吗?与上述问题类似,Mybatis声称将sql单独放到Mapper.xml文件中,这样就可以不用编译直接修改sql。但是Mapper.xml是和Class放在一起的,改了需要重新打包,而且Mybatis不能动态加载Mapper.xml,那么独立sql转XML有什么好处呢?对于最后一个问题,我的回答是,对于大部分项目来说,没有优势!项目是否易于部署和扩展,并不取决于你使用的框架,而是取决于你的设计。以Mapper.xml为例,Mybatis将sql和代码分离,但是如果你在项目中仍然把Mapper.xml和代码放在同一个模块中,那么这个优势就失去了。既然没有这个优势,那我们还需要单独写Mapper.xml文件吗?我的选择是,那就不写了,直接使用Mybatis提供的注解。同时,为了解决Service层对DAO层(这里指Mybatis)的强依赖,对框架做了一些改进,将Service层和DAO层解耦。详情见下方改进计划。框架改进方案为了解决以上问题,对框架做了如下调整:分离Param、Result和Model替换代码生成独立业务逻辑Model层优化分离Param、Result和Model如前所述,它们之间会存在强耦合Param、Result和Model问题比较多,这里把Param、Result和Model分开。每一个都是一个独立的Bean,解决了上面的问题。但它引入了两个新问题:第一,明显增加了手工编码量。当一个表修改一个字段时,需要修改三个或更多的类。其次,增加了数据传输之间的代码。即把Param传给Model,需要给字段赋值。如果一个字段的值一次设置一个字段,就会增加很多枯燥的代码。但是使用反射会对性能产生一些影响,那么这两个问题怎么解决呢?首先,纯手动自慰是肯定不行的。需要提供一些自动化手段。对于赋值,Spring提供了BeanUtils来简化处理。虽然这个值是根据反射来设置的,但是对于当前阶段来说,这个性能损失是没有影响的。但是,BeanUtils不能复制不同类型的属性。假设我有一个域对象Book,里面有一个Author字段。现在我想给BookResult赋值,它有一个AuthorResult字段。这个时候BeanUtils是不能赋值的。于是自己写了一个基于Gson的工具类来处理。BeanUtils复制属性10000次性能测试需要500多毫秒,而基于Gson的工具类只需要300毫秒左右。对于表字段的生成,如果使用IDEA,IDE默认提供了一个脚本,可以将表生成POJO!我们可以使用这个脚本生成Model,然后将字段复制到Param和Result中,简化字段创建的写法。我修改了这个脚本以满足项目需要。主要是增加了对lombok的支持,增加了类注解和字段注解。替换代码生成针对上面代码生成组件的问题,我调整了代码的生成方式。不再基于组件生成,而是基于IDEA自带的FileTemplate、LiveTemplate和ScriptedExtensions。这样虽然不能一次性生成多个文件,但是由于生成逻辑基本是一次性的,所以影响不是很大。首次生成代码时,代码生成组件的效率高于FileTemplate、LiveTemplate和ScriptedExtensions的组合,但后期调整的灵活性明显高于代码生成组件:首先,当文件结构调整后,只需要修改FileTemplate,将配置文件导出给项目组成员即可。同样,在调整LiveTemplate时,只需要修改对应??的LiveTemplate并导出配置文件给项目组成员即可。其次,你要生成哪个文件,只需要为这个文件生成即可。第三,通过FileTemplate生成完整文件后,可以通过LiveTemplate快速进行模块化编码。最后,可以在项目级别设置FileTemplate,即每个项目都可以有独立的FileTemplate的具体操作过程,下面演示。独立的业务逻辑将原来的Controller、Service、Model三层拆分为四层,针对Service和测试的问题:Controller负责接收和返回前端数据,统一异常处理;Service负责事务和Domain层逻辑的组装。这里不会出现事务嵌套的问题,也不会造成无法捕捉到预期异常的问题。Domain负责业务逻辑,Model负责数据持久化。这样一来,Service的职责就减少了,同时也不存在事务嵌套的问题。模型层优化上文提到,框架最终放弃了Mapper.xml,使用Mybatis注解实现持久化操作。改用注解避免了XML代码的编写,但是并没有解决框架对Mybatis的强依赖。因此在Domain中增加了一个Repository接口层,用于定义Domain的持久化操作,而Repository在Model层实现。这里的实现是Mybatis的实现。这样做有两个好处:依赖倒置:原来是Domain依赖了Model层,现在Model层又依赖了Domain层,以至于我要替换Mybatis的时候Domain完全没有察觉。独立测试:因为现在Domain不依赖任何其他层,所以可以在没有数据库和容器的情况下进行测试。这样测试的效率就不会随着项目的发展而越来越低。既然知道了框架改进的细节,那么如何改进框架呢,我们现在就开始改造吧。其实主要是代码生成方式的改造,也就是写FileTemplate、LiveTemplate和ScriptedExtensions。下面从ScriptedExtensions开始简单介绍这三个函数。ScriptedExtensions先来解释一下,什么是ScriptedExtensions。我们都知道现在的IDE都是插件化的,也就是说我们可以通过开发者提供的插件开发包来开发插件来扩展现有的IDE功能。但是,编写插件需要特定的开发环境。如果是一个很简单的功能,那么费心去搭建开发环境就很麻烦了。所以IDEA提供了ScriptedExtensions,可以理解为插件的简化版,即可以通过脚本来扩展IDE的功能。IDEA提供了Database功能,可以连接数据库进行相关操作。连接数据库,在表上右击,可以看到ScriptedExtensions选项,里面有个功能可以根据表生成POJOgroovy脚本。但是功能比较低:包名是硬编码的:com.sample不生成表注释,不基于lombok简化getters和setters。不过幸运的是,我们可以基于这个脚本进行修改。在刚才的ScriptedExtensions菜单中,有一个GotoScriptsDirectory选项,点击之后,就可以进入脚本目录了。直接复制这个groovy文件Ctrl+c,Ctrl-v,重命名,根据这个脚本修改。如何根据自己的需要修改,主要是根据表格信息拼接String。FileTemplateFileTemplate是IDEA提供的生成文件的模板。在菜单中点击File->New...后,出现的各种文件都是基于FileTemplate实现的。我们自定义的Controller、Service、Domain等类可以通过FileTemplate创建来简化。具体使用方法是按Ctrl-Alt-S调出设置菜单,点击Editor->FileAndCodeTemplate,在里面添加一个Template。几点说明:下面的说明列出了一些默认参数及其作用。您还可以自定义变量。如果自定义变量没有赋值,创建时会有输入框提示输入内容。该模板是基于Velocity的,所以如果你熟悉Velocity的话,可以直接上手。EnableLiveTemplate选项是激活FileTemplate中的LiveTemplate变量,但需要使用#[[]]#包。但是对于Java的创建,这个函数有一个bug,无法定位到需要的位置,所以暂时没有使用。创建完成后,在新建菜单中可以看到这个模板。LiveTemplateLiveTemplate其实就是CodeSnippet。创建方法类似于FileTemplate。按Ctrl-Alt-S调出设置菜单,点击Editor->LiveTemplate,在里面添加Template。几点说明:这里的变量用$$包裹起来。每个变量都是一个占位符。使用tab展开后,可以手动输入value右下角的Editvariables给变量赋值。IDEA提供了一些方法。可以在默认值下设置更改链接,可以选择LiveTemplate生效的位置,比如只在Java类的声明处生效。编码过程创建完以上模板后,编码过程如下:在表上右击生成,通过ScriptedExtensionsModel使用FileTemplate快速生成Controller、Service、Domain等类。使用LiveTemplate快速编写代码。总结本文对原有框架存在的问题进行梳理和解决,对框架进行升级,以适应项目的发展和推进。
