java WEB项目结构

java项目结构

项目结构

包命名由名公司名+(项目名)组成:vip.infotech.base-platform.common
前端代码目录功能模块划分与后端保持一致。
小项目可以不建common公共工程保持各个子工程独立性。对于微服务还是用的common公共工程比较好,common下面还可以再拆分子模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
base-platform
|--platform-admin 后台管理
|--java
|--common 公共模块
|--config 配置
|--properties 配置实体
|--interceptor 拦截器
|--module 功能模块(这一级也是非必须)
|--sys (后台管理的业务如果仅仅是系统管理,这个层级可以去掉,通过工程名划分业务)
|--controller
|--dao
|--entity
|--service
AdminApplication.java 要在最外层
|--resources
|--default
|--statics
|--templates
|--mapper
|--platform-api api接口,对页面与外部提供接口(api与后台管理可以根据情况选择是否合并)
|--java
|--common 公共模块
|--module 功能模块(这一级也是非必须,具体子模块包划分可以参考API接口文档或者一级菜单)
|--basedata 基础模块(各种基础数据模型维护、厂家、设备型号等业务模型维护)
|--location 定位业务模块
|--webservice webservice接口模块(偏技术,对于webservice想新建一个webservice工程也没毛病,会增加一定的维护成本)
|--monitor 设备监控与管理模块,事件监控、定时配置与策略等
|--alarm 告警模块
|--report 原始记录指标分析模块(可以跟stats模块合并)
|--stats 统计模块
|--manage/business/core 核心业务模块(业务比较单一使用)
|--system 系统管理模块
|--maintain 系统备份、各个模块数据内容备份还原等(非必须,可以放入system模块)
|--platform-spi spi接口,对接设备等南向接口,比如定位接口
|--java
|--common 公共模块
|--module 功能模块(这一级也是非必须)
|--manage/business/core 核心业务模块(业务单一,这个层级可以去掉,通过工程名划分业务)
|--platform-spi-mock spi接口,模拟器(偏技术划分)
|--platform-gen 代码生成
|--platform-schedule 定时任务(偏技术划分,没用到很复杂的功能,可以直接在admin或者api中使用spring自带的定时任务)
|--platform-shop 商城后台管理 (通过工程划分业务例子)
|--platform-ui 前端页面(前端可以单独抽出跟后端平级)
|--mall 商城 (通过工程划分业务例子)
|--wx-mall 微信商城 (通过工程划分业务例子)
  • 使用代码生成器可以对数据库表直接生成对应的实体类与接口代码,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
infotechcloud
|--infotech-core #通用核心
|--infotech-starter-auth # 权限
|--infotech-starter-common # 通用封装
|--infotech-starter-database # 数据库封装
|--infotech-starter-activemq # MQ封装
|--infotech-gateway # 网关
|--infotech-platform # 平台主模块
|--infotech-system-api # 接口
|--infotech-system # 系统管理模块
|--infotech-business-api # 业务接口或者命名core
|--infotech-business # 业务模块或者命名core
|--infotech-mq # MQ
|--infotech-support # 其他功能
|--infotech-code

方法命名

对于软件开发命名与空想需求是两大难点,一人一种想法。

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) 做前缀。

最后总结两种命名方式:

  1. 【推荐】框架封装的方法不动,后面添加的service与dao层方法统一:get/list/count/save/remove/update
  2. 也就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
2
3
4
5
6
@GetMapping("/{id}")
public Result info(@PathVariable("id") Integer id) {
GroupInfoEntity groupInfo = groupInfoService.selectById(id);

return Result.ok().put(Constant.DATA, groupInfo);
}

例子 方式二:

1
2
3
4
@GetMapping("/{id}")
public Result info(@PathVariable("id") Integer id) {
return groupInfoService.selectById(id);
}

第一种如果要实现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
2
List<Book> bookList = elasticsearchTemplate.queryForList(searchQuery, Book.class);
Page<Book> books = elasticsearchTemplate.queryForPage(searchQuery, Book.class);

代码评审

代码评审非常重要,潜移默化中,对技术提升很有帮助。就跟专修工人有个包工头或者师傅天天监督质量避免工程出现质量问题被老板批斗或者赔偿。
量改进的逻辑清晰、简洁、看方法名可以直观看出整个流程。