最近启动一个新项目需要定义api接口,总结一下使用过程中的一些实现方案和想法.
项目是前后端分离的,前端使用vue技术栈,服务端是springboot工程,用到spring cloud的一些组件.
个人倾向于rpc请求统一使用HTTP接口或者HTTPs接口.所以项目里面没有引入dubbo.spring cloud的注册中心使用的是nacos不是erueka.因为nacos也可以作为配置中心,没有使用Apollo config.这里不详细说具体的架构了.有时间再写一个详细说明技术选型的文章.
服务提供HTTP请求,前后端的参数传递比较简单,get请求放到header里面,k-v形式传递.post请求放到body里面,json格式传递. 理论上还有一种是放到url里面参数.实际并没使用.使用header代替了.
url域名定义遵循名词/动词的方式. 例如:
- 服务器域名 serverUrl = a.com
- 操作对象是用户:user
- 请求地址:
- serverUrl/user/get 查询用户详情
- serverUrl/user/page 查询用户列表分页
- serverUrl/user/create 创建用户
- serverUrl/user/updata 更新用户
注意:这里并没有使用rest请求的规范.rest请求倾向于通过http的请求类型作为动词.例如:put、delete等.我个人不太喜欢这种方式,我喜欢操作对象名词放到域名里面,动作放到域名末尾.在使用过程中没什么好坏之分.看个人爱好.rest请求本身也不是http强约束的.
查询类型的通常get请求,更新(创建算更新)类型的通常是post请求.
还有些复杂的请求,例如详细查询里面关联很多其他的实体数据.例如查询用户可需要角色、企业、部门等信息.这个时候使用那个名词需要细致的分析一下,通常选择主要数据.用户最直接想要操作的对象.
例如我们在用户操作页面名词用户.企业操作页面名词是企业.
例如:查询用户详细使用的名词是user.但是需要查询企业的信息.查询企业详细信息使用的名词是company.但是需要查询企业的用户.
以上说的比较理想.实际设计需求的时候尽量减少一个请求查询多个对象的情况.尽量让用户增加点击次数.避免一次查询过多数据.
例如:用户详情页面可以查询用户的基本信息和企业名称.如果需要企业的信息则增加一个查询企业信息的跳转按钮.用户数据表可以冗余一些名称字段.冗余名称字段是有风险的.如果名称变更需要做数据同步.否则会导致数据不一致.所以在冗余字段的时候一定要谨慎.不建议在数据库里面做外键关联,数据库模型越简单越好.时间越长好处越明显.有些字段不允许变更可以大胆冗余.
说服务端参数之前先简单介绍一下分层结构:
横向切分有:controller、service、manager、dao层. controller层对应http的api接口.service对应业务需求接口.通常也是rpc调用api接口的实现层.所以serice一般继承rpc接口的api.manager层主要是执行业务逻辑和进行模型封装.Dao层主要就是操作数据库.
纵向切主要是面向业务需求.通常和api接口对应.例如用户管理、角色管理、权限管理等都可以作为纵向切的维度.
以上的分层主要是个人的理解.可能每个人对分层都有自己的理解.不同的分层可能对入参返回值的定义有区别.但是总体的思想还是横向和纵向的划分.还有各层对应的业务开发的实际作用.
下面说一下各层的参数定义
无论前端使用get还是post服务端的参数建议都转成对象形式.就是controller方法的入参不要使用基本类型,尽量使用对象类型.
有些特殊情况,例如根据id查询详情.入参可能是一个字符串id.但是在实际使用过程中也会需要一些权限相关的信息.所以除了id显示传递之外,还需要其他的参数.所以controller入参也可以定义成一个对象.总之一个最佳实践就是入参统一使用对象.只有在个别情况下入参只有一个值的时候使用基本类型或者开发一些运维接口的时候可以简化入参.
congroller层入参对象一般使用VO结尾.当然这看个人喜好.什么结尾不重要.但是最好结尾要统一.因为后期可能会有大量的层次之间对象转换.例如controller层对象转service层.service层转dao层.例如controller层VO结尾.service层DTO结尾.那么转换函数可能是VO2DTO或者DTO2VO等.看起来比较规整
controller层之后通常是service层.service层同样尽量使用对象入参.对象通常使用DTO结尾.DTO通常比VO会多一些登录用户信息.因为用户访问服务通常需要先登录.但是前端请求接口的时候没必要所有请求都增加用户信息.例如用户名、角色等.一是麻烦,二是不安全.用户信息可以通过cookie里面的token传递.服务端拦截器或者过滤器统一解析.或者通过session得到用户信息.统一放入本地缓存.VO转DTO时候写入.也可以在service层通过本地缓存得到.我倾向于DTO里面增加用户信息.不是在service层读取本地缓存.因为service可能继承RPC接口.RPC调用的用户信息可能是显示传入的.当然也可以通过rpc的拦截器隐式传入,然后在service里面解析.方案没有绝对的好与坏,也看个人意愿.
service之后可能是manager层或者dao层.增加manager层主要为了做一些公共能力的切面使用.例如:统一事务管理.打印参数.参数封装等.事务管理通常使用注解.使用切面统一增加注解可以简化开发.但是也限制灵活性.我个人不建议统一的在切面里做事务.但是有时候也要考虑开发人员的技术能力.有些开发人员不知道如何处理事务.只是写简单的业务代码.这样增加统一注解会简化开发.但是有些复杂的业务可能涉及到异步调用或者请求三方接口.这时候不适合在外层方法增加事务,因为长时间的事务会影响性能.需要更加细粒度的事务处理.当然也可以通过代码结构解决统一切面事务的问题.但是都会增加开发的复杂度.再说打印参数.因为大多数情况controller层到service参数是透传的.所以service层和controller层可以打印一层的入参返回值就可以.manager可能涉及参数的封装.所以最好manager层增加一层统一的参数打印.
在转回参数的说明,manager通常使用service的DTO或者Dao层的MO(模型).我通常使用DTO.service透传到manager.manager内部涉及多个manager调用.manager封装Dao层.Dao入参使用MO.有的人可能习惯采用spring的命名方式,喜欢以bean结尾.例如UserBean等.这个也看个人喜好.没有什么优劣.
以上通过横向切割说一下各层的入参:VO、DTO、MO等.下面通过纵向切割说一下入参的划分.
纵向切割针对业务需求层面的.例如:展示用户列表信息.controller层的VO可以针对接口需求定义.例如QueryUserReqVO,主要包括请求参数.service层有一个QueryUserReqDTO.
因为service通常会有一套接口定义.方便非http的远程调用接口扩展.当然controller定义一套接口也行.之前这么做过.但是实际开发过程中作用不大.不建议controller层定义接口.
在使用spring cloud的注册中心的时候可以直接注册域名请求地址.使用restTemplate请求的时候可以直接使用域名.使用FeignClient的时候可以注册一套api接口.使得HTTP请求和其他远程调用类似.使用那种方案也没有好坏之分,看个人喜好.
继续说纵向切割的参数入参.到了manager层主要是两点考虑,一是承接service层入参,二是转化Dao层入参.所以manager层入参定义通常面向业务的.例如查询用户列表.包括企业信息、角色信息、用户信息等入参.
Dao层通常对应模型的入参.通常Dao入参就是MO.例如manager入参QuerUserReqDTO.转换成多个MO.UserMO.RoleMO等,然后通过MO查询对应模型信息.仅仅从简单的业务需求考虑,这里的模型就是数据库的表.对应一些大数据层面的模型可能与表不是一一对应.例如大数据宽表可能对应多个模型.还有些缓存的模型.可能一个模型对应多个缓存.实际业务比较复杂一些.这里只是说最基本的情况.对于Dao层不建议多表联合查询.尽量单表查询,有关联的则使用主键二次查询.这样Dao的入参可以与模型对应.也可以减少每次查询的时间.虽然总时间可能会增加.但是有利于后期的性能优化.例如增加缓存.
以上就是项目定义api接口和入参返回值的一些说明.上面的介绍只是项目初期设计工程的时候想到的一些问题.在实际开发过程中要复杂的多.在遇到复杂需求的时候尽量不要打破上面的接口定义的方式,尽量适配.尤其在toB项目中.可能需求逻辑比较复杂,功能非常定制化.这个时候要尽量思考功能的纵向切分.尽量简化内部接口的入参和返回值.通过切割成多个简单实现封装成复杂需求.