介绍一种基于spring框架的增改业务代码架构,架构主要改善了参数转换和验证两部分的代码结构,目的在于减少代码量,增强代码简洁性和可维护性,特别是可维护性。
传统架构方式
众所周知,编写增改业务逻辑是很枯燥的,业务代码大多类似以下代码:
controller接收页面提交的参数的封装对象stu
。
1 |
|
controller接收到参数之后进行验证,一般会将验证代码整理到外部,使controller更加简洁明确。验证方法validateParam(StudentVo stu)执行stu对象的检测,如果发现错误,会返回字符串提示,如果检测不通过,那么controller返回错误对象到前端。
1 | String res = validateParam(stu); |
在validateParam(StudentVo stu)检测方法体内部进行各项参数的检查,通常这一步的编码是非常繁琐和枯燥的,validateParam方法会引用很多错综复杂的具体检测内容的外部方法,如判空、判重、判有效性、判相关性等方法,不仅编码繁琐,还容易漏掉检测项,更痛苦的是后期极难维护,还可能出现重复代码。
1 | private String validateParam(StudentVo stu) { |
为了便于阅读和后面的内容介绍,将上述的StudentVo属性代码展示如下:
1 | public class StudentVo { |
数据检测通过后,需要进行数据转换,类似学院
和专业
这类数据一般是码表,数据库里面不会直接存储学院名称
这类字符串,所以collegeName
要转换为collegeId
。为了转换为数据库存储对象,需要编写StudentPo
类,再将所有属性转换并存入StudentPo对象(类似collegeName等这类数据在页面一般使用下拉菜单选择,因此传到后台的一般是对应的id值,为了方便介绍后面的内容,所以假设传到后台的数据是字符串类型的学院名称)。
1 | public class StudentPo { |
对于字段比较少的po类,可以逐个属性进行转换和设置,如果比较多,就比较繁琐了,比较优雅的办法是写一个转换类用于类之间的转换,另一种方法是指定一个公共静态方法返回需要的po对象。前者一般会用到反射,需要配置字段映射,代码比较复杂,但一劳永逸,比较适用于字段多而且需要转换的类多的情况,也可以使用第三方的转换工具类,比如apache的PropertyUtils的copyProperties方法,而后者写起来繁琐冗长,每个转换都需要重复写,但代码清晰,适用于字段较少的情况。以后者为例,如在StudentVo中添加StudentPo to(StudentVo vo)方法:
1 | public static StudentPo to(StudentVo vo) { |
转换方法设置在vo中更好,因为po是数据库的对应关系,属于底层代码,不建议引入上层业务代码vo类,所以我认为转换代码写在vo类中更合理。
。
上述就是常规的增改业务代码,多写几次会越来越熟练,但再多写几次就会彻底崩溃。我一直在思考有没有好的方法或模式,能将这些类似的代码(如验证和转换代码)分离出来封装一套通用的业务方案,或者至少能将这部分代码逻辑分离出来,不与系统业务纠缠在一起。最终我想到了解决方案,下面介绍数据逻辑和系统业务分离的代码架构。
逻辑业务分离架构方式
架构更该包含两部分:数据检验和数据转换。
1、数据检验部分基于spring集成的hibernate的validation框架,该框架符合JSR-303和JSR-349规范,重点关注一下JSR-303的注解。
2、数据转换部分是基于spring集成的jackson框架。
先梳理一下后台数据处理流程,spring接收到json参数后会通过jackson对参数进行解析,然后封装为对象,也就是上述使用的stu
,如果在controller中对对象参数标注了@Valid注解,那么spring还会调用validation对参数进行检测,并将验证失败的结果封装到Error对象中,与参数对象一并传到controller中。(这里只讨论json参数,即@RequestBody标注的参数,其他类型的参数解析器不一样)
下面先介绍验证代码,首先需要编写vo类,按照JSR-303规范和hibernate自定义的注解进行验证注解的标注。
1 | public class StudentInsertVo { |
spring集成了validation的验证,在controller中配置@Valid注解和Errors对象即可获得验证结果,Errors对象必须跟在@Valid注解参数后面。
1 | public Object insert( StudentInsertVo stu, Errors errors){ |
值得注意的是,JSR-303的注解可以标注在属性和getter方法上,验证时的值就是属性或者getter返回的值,换句话说,如果修改过getter方法致使返回的值与属性的值不同,那么用于验证的值也会不同。如果属性和方法均标注了注解,那么都会做检测。
这样,验证逻辑就独立出来,在vo中进行维护,剩下的工作仅需要将vo转换为po即可。
既然spring利用框架对参数进行封装和验证,那么框架一定留有接口让我们扩展,实际也是这样的,在程序设计过程中,对参数的转换需要留出接口方便以后扩展,我也是利用了这些接口对数据进行转换。
前面说过spring将json参数封装为对象是通过jackson进行转换,而jackson优先使用setter方法对对象设值,那么更改vo的setter方法可以对值进行转换。当然jackson也提供了专门的注解@com.fasterxml.jackson.databind.annotation.JsonDeSerialize和抽象类com.fasterxml.jackson.databind.JsonDeSerializer,但是使用起来比较复杂,不赘述。
下面代码是修改过的StudentInsertVo,将collegeName和majorName替换为collegeId和majorId,后者正是我们需要的,然后更改setCollegeName方法
和setMajorName方法
将String类型的参数转换为Integer类型的参数。
当页面传来参数名为collegeName的参数时,会调用setCollegeName方法进行设值,而方法中真正设值的属性是collegeId,这样就达到collegeName转换为collegeId的目的了。majorName和majorId也是同样的道理。
1 | public class StudentInsertVo { |
参数转换要考虑异常的问题,如果转换过程中抛出异常,那么前端会收到400 Bad Request,这是就需要将错误信息传递到controller中与Errors对象一并处理,可以使用ThreadLocal传递异常。
1 | public static final ThreadLocal<List<Exception>> |
vo相应的更改,以setCollegeId方法为例。对方法体进行异常捕获,并将异常存入ThreadLocal中。
1 | public void setCollegeId(String collegeName) { |
controller中添加转换异常的处理。
1 | public Object insert( StudentInsertVo stu, Errors errors){ |
所有的非默认转换方法均需要捕获异常,会有很多重复代码,因此将异常传递的方法和异常处理的方法进行封装。封装为ParamErrorHandler类,并提供spreadException方法用于传递异常(将异常存入ThreadLocal),以及getExceptionList方法用于获取异常(将转换和验证的异常合并)。
1 | public class ParamErrorHandler { |
vo的setter方法相应的更改:
1 | public void setCollegeId(String collegeName) { |
controller更改:
1 | public Object insert( StudentInsertVo stu, Errors errors){ |
按照上述代码架构,数据逻辑与系统业务就分离开了,数据逻辑独立在vo中,更易于维护,也使得系统业务更明确更精简。代码中一些细节未详细说明,无大碍,依然可以按照思路进行架构。
但一定要注意,ParamErrorHandler.getExceptionList()在finally中对context进行了清理,这一步非常重要。应用大多会使用线程池,就可能出现多次请求使用的是同一个线程,如果不清理ThreadLocal就会造成获取到同一个ThreadLocal和其存储的检测结果,就会导致报出错误的参数错误信息。
最后,至于这样的架构是否合理我并没有太多的意见,个人是比较喜欢的,即先将参数转换后封装为对象,再将对象传到controller,因为这样更符合流程逻辑。当然也许有人更喜欢先对参数代表进行检测后,再进行转换。这里没有定论,待后期的经验验证。
附表:JSR-303和hibernate自定义验证注解
注解不一定全
JSR-303
Constraint | 详细信息 |
---|---|
@Null | 被注释的元素必须为 null |
@NotNull | 被注释的元素必须不为 null |
@AssertTrue | 被注释的元素必须为 true |
@AssertFalse | 被注释的元素必须为 false |
@Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 |
@DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 |
@Size(max, min) | 被注释的元素的大小必须在指定的范围内 |
@Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 |
@Past | 被注释的元素必须是一个过去的日期 |
@Future | 被注释的元素必须是一个将来的日期 |
@Pattern(value) | 被注释的元素必须符合指定的正则表达式 |
hibernate自定义注解
Constraint | 详细信息 |
---|---|
被注释的元素必须是电子邮箱地址 | |
@Length | 被注释的字符串的大小必须在指定的范围内 |
@NotEmpty | 被注释的字符串的必须非空 |
@Range | 被注释的元素必须在合适的范围内 |