java项目结构
项目结构
包命名由名公司名+(项目名)组成:vip.infotech.base-platform.common
前端代码目录功能模块划分与后端保持一致。
小项目可以不建common公共工程保持各个子工程独立性。对于微服务还是用的common公共工程比较好,common下面还可以再拆分子模块。
1 | base-platform |
- 使用代码生成器可以对数据库表直接生成对应的实体类与接口代码,controller默认提供
save/update/delete/info/list
- 对于数据库中间表也建立实体类与DAO、Service层代码,controller层可以根据需要删减
- 对于前后端分离的项目如果比较简单可以把admin与api工程合并成一个api工程,对外提供系统管理api与业务api
- 业务代码尽量写在service层,方便controller层复用
划分原则说明:技术划分子工程,再业务划分小模块。或者直接业务划分大模块再技术划分小模块。也可以混合使用。
比如:mall、wx-mall就是按照业务划分子工程,内部在controller、service、dao等。spi、spi-mok、api就是按技术划分子工程,内部还可以通过vip.infotech.base-platform.module.xxx
划分子业务模块,根据具体情况选择。大项目肯定是需要一个业务一个以子工程,可以独立开发部署。spi也可以当成是按功能业务划分,为了拆分业务,对于webservice想新建一个webservice工程也没毛病,会增加一定的维护成本。项目大了还可能再启一个新的大工程,通过接口进行各种交互。模块划分也不是一层不变的,后期可以根据业务或者技术调整进行动态变化。对于一些接口命名,我们要做的是优秀的产品,而不是花大把时间纠结几个技术名词。
其他:
1 | infotechcloud |
方法命名
对于软件开发命名与空想需求是两大难点,一人一种想法。
mybatis-plus 3.x:
通用 Service CRUD 封装IService接口,进一步封装 CRUD 采用 get 查询单行 remove 删除 list 查询集合 page 分页 前缀命名方式区分 Mapper 层避免混淆
泛型 T 为任意实体对象
建议如果存在自定义通用 Service 方法的可能,请创建自己的 IBaseService 继承 Mybatis-Plus 提供的基类
controller层: save/info/list/page(可用list代替)/update/remove/open/close
2.x
service层封装: insert/insertBatch/delete/deleteById/updateById/update/getById/GetOne/selectById/selectByMap/selectList/selectPage/selectMaps/
service层封装扩展:insertXX/insertXXXBatch/deleteXXX/deleteXXXbyId/selectXXXById/selectXXXPage
dao层继承baseMapper:同service层
3.x
service层封装:save/saveBatch/saveOrUpdateBatch/removeById/removeByMap/remove/removeByIds/updateById/update/updateBatchById/saveOrUpdate/getById/listByIds/getOne/getMap/getObj/count/list/page/listMaps/listObjs/query/update
service层封装扩展: saveXX/saveXXBatch/removeXXById/listXXByYY/listXXPage
,XX可以说固定的比如Item(saveItem/listItems,list是否使用复数需要统一风格,item无法提现再用具体User之类替代)或者业务名比如saveUser等。
baseMapper: insert/deleteById/deleteByMap/delete/deleteBatchIds/updateById/update/selectById/selectBatchIds/selectOne/selectCount/selectList/selectMaps/selectObjs/selectPage/selectMapsPage
dao层继承baseMapper:类似baseMapper
service层:save/get/count/update/list/page/remove
dao层:insert/select/count/update/delete
例如:listRolesByUserId/listDeptTrees/getDeptName/removeXXXByEventId
扩展:还可以扩展查询统一service与dao层都用queryXXX,不要重写框架封装好的方法,有需要旧自己扩展。mybatis plus框架注入顺序是先xml,后系统自带
还可以参考:Service/DAO 层方法命名规约
1) 获取单个对象的方法用 get(Service/DAO) 做前缀。
2) 获取多个对象的方法用 list(Service/DAO) 做前缀,或者select。
3) 获取统计值的方法用 count(Service/DAO) 做前缀。
4) 插入的方法用 save(Service)/insert(DAO) 做前缀。
5) 删除的方法用 remove(Service)/delete(DAO) 做前缀。
6) 修改的方法用 update(Service/DAO) 做前缀。
最后总结两种命名方式:
- 【推荐】框架封装的方法不动,后面添加的service与dao层方法统一:
get/list/count/save/remove/update
- 也就service层使用接近业务的命名方式比如
save
,dao层使用接近数据库的命名方式比如insert
综合考量,如果业务简单方便查找方法可以service与dao层名字一样,一个service可以调用一个自己dao也可以调用多个dao别人的dao也调用(最好不要调用别人的dao),service层可以调用其他service层方法,最好不要直接调用其他人的dao层(如果项目就一两个人都是自己做可能问题不大),在多人开发的团队项目中,模块与模块之间都是以service暴露服务的。service是对外提供的业务的操作,它屏蔽了数据库访问。当某个业务依赖其它业务时,反映到程序中就是service调用service,而不是service调用dao。
这样做的好处是只关心service会被外部调用,尽全力把service维护好,至于dao是怎样的,不用对外暴露。如果把dao给其它service访问,那么关注点将会分散给dao,导致两头维护。
对应:即,一个service配一个dao,高内聚,体现了一个类即一个服务的思想,虽然夸了层,放在不同的类,但是从上到下浑然一体,与其他dao没有任何交集,是现在新产生的一种思想,缺点是代码量会大一点。
不对应:即,一个service可以配若干个dao,这是基于传统三层的架构演化而来,dao层的代码复用性高,说白了还是三层思想,不体现高内聚的原则。
框架统一封装了通用save方法后面自己有一个save方法有特殊业务可能涉及到了多个service操作,最好再定义一个方法处理比如saveDept还进行了部门关系表(维护所有部门上下级关系方便查询)操作。
自己扩展的方法命名最好保持一致,风格尽量保持统一即可
分层是为了,每一层的变动都不影响其他层,而且上层可以复用底层能力
根据具体情况增加VO层对页面传输与返回的数据进行包装,对于比较特殊的接口比如接口返回内容动态变化可以考虑使用Map对象。
还可以参考开源lengleng/pig项目的命名规范。
早期有个项目封装的是默认每一层都使用相同方法名。
编码
- 对于页面使用到的下拉框选择涉及多个表的建议入口放在需要获取的原始数据管理模块上。比如需要用户下拉框,可能根据部门查询用户,可能根据区域查询用户信息。建议都放在用户接口上
get: user/area-select
,get: user/dept-select
或者get: user/select?userId=xxx&deptId=xxx
这种比较适合复用性强的接口。当然如果统一放在条件接口上也是可以的,需要保持统一如:get: area/user-select
,get: dept/user-select
。还有就是通过关注点进行选择,如果关注的是用户的各种功能可以放在用户上,如果关注点是其他用户至少附属概念可以考虑放在其他位置。 - 分页:默认列表都做分页、可以添加一个不分页的条件查询所有数据paging=false,如果客户端不传分页条件默认显示100条。
- 对Java内方法的命名尽量使用动词,使用的时候就是名词+动词比如:
cat.eat()
。经常进行重构,抽离复用代码块。
扩展
Service层返回值封装
例子 方式一:
1 |
|
例子 方式二:
1 |
|
第一种如果要实现Result.error(ERROR_MSG.XXX_UNFOUND)
,需要service层抛出异常。第二种在service层返回Result比较奇怪。还有根据职责来划分,如果service层只给controller层用一和二都可以。如果service层还能被其他service调用,第二种不太能满足使用。现在挺多系统都是使用try catch
进行处理。封装好系统全局异常处理,通过切面或者拦截器处理。
结论:直接抛异常 + 全局异常处理比较合理。
异常处理
一班来说DAO鱼Service层可以直接把异常往外层抛,统一在controller层处理异常。少部分不需要外层处理或者反馈给客户端的可以在内部直接处理了,或者根据情况内部处理了,内部无法处理的继续向外层抛出异常。
e.printStackTrace();
记得打印错误堆栈
websocket项目
对于websocket项目,请求的时候通过websocket的path区分不同类型请求。返回的时候要增加一个action
字段标识针对什么类型请求的返回(action可以放在返回最外层、也可以放在data内容层里面,属于通用格式一般是放在最外层统一处理),使用stomp订阅发布模式返回都是同一个websocket接口获取的(或者客户端通过msg中具体内容区分,这个不太好)。{"code":0,"msg":'',"action":"login","data":""}
也可以使用普通普通http请求实现订阅,主要内容:标识订阅、取消订阅、订阅路径。
ElasticsearchTemplate例子
1 | List<Book> bookList = elasticsearchTemplate.queryForList(searchQuery, Book.class); |
代码评审
代码评审非常重要,潜移默化中,对技术提升很有帮助。就跟专修工人有个包工头或者师傅天天监督质量避免工程出现质量问题被老板批斗或者赔偿。
量改进的逻辑清晰、简洁、看方法名可以直观看出整个流程。