Java 项目规范 Java 项目命名规范 全部采用小写方式, 以中划线分隔。
1 2 3 正例:`mall-management-system / order-service-client / user-api` 反例:`mall_management-system / mallManagementSystem / orderServiceClient`
方法参数规范 无论是 controller,service,manager,dao 亦或是其他 class 的 代码,每个方法最多 5 个参数,如果超出 5 个参数的话,要封装成 javabean 对象。
反例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public String signEnvelop (JdRequestParam param, String password, String priCert, String pubCert, String username, String ip, String userAgent) {}
代码目录结构 统一的目录结构是所有项目的基础。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 src 源码目录 |-- common 各个项目的通用类库 |-- config 项目的配置信息 |-- constant 全局公共常量 |-- handler 全局处理器 |-- interceptor 全局连接器 |-- listener 全局监听器 |-- module 各个业务(方便将来拆成微服务) |-- |--- employee 员工模块 |-- |--- role 角色模块 |-- |--- login 登录模块 |-- third 三方服务,比如redis, oss,微信sdk等等 |-- util 全局工具类 |-- Application.java 启动类
common 目录规范 common 目录用于存放各个项目通用的项目,但是又可以依照项目进行特定的修改。
1 2 3 4 5 6 7 8 9 src 源码目录 |-- common 各个项目的通用类库 |-- |--- anno 通用注解,比如权限,登录等等 |-- |--- constant 通用常量,比如 ResponseCodeConst |-- |--- domain 全局的 javabean,比如 BaseEntity,PageParamDTO 等 |-- |--- exception 全局异常,如 BusinessException |-- |--- json json 类库,如 LongJsonDeserializer,LongJsonSerializer |-- |--- swagger swagger 文档 |-- |--- validator 适合各个项目的通用 validator,如 CheckEnum,CheckBigDecimal 等
module 目录规范 module 目录里写项目的各个业务,每个业务一个独立的顶级文件夹,在文件里进行 mvc 的相关划分。 其中,domain 包里存放 entity, dto, vo,bo 等 javabean 对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 src |-- module 所有业务模块 |-- |-- role 角色模块 |-- |-- |--RoleController.java controller |-- |-- |--RoleConst.java role相关的常量 |-- |-- |--RoleService.java service |-- |-- |--RoleDao.java dao |-- |-- |--domain domain |-- |-- |-- |-- RoleEntity.java 表对应实体 |-- |-- |-- |-- RoleForm.java 请求Form对象 |-- |-- |-- |-- RoleVO.java 返回对象 |-- |-- employee 员工模块 |-- |-- login 登录模块 |-- |-- email 邮件模块 |-- |-- .... 其他
MVC 规范 整体分层 controller 层
service 层
manager 层
dao 层
controller 层规范1)只允许在 method 上添加 RequestMapping 注解
只允许在 method 上添加 RequestMapping 注解,不允许加在 class 上(为了方便的查找 url,放到 class 上 url 不能一次性查找出来)
正例:
1 2 3 4 5 6 7 @RestController public class DepartmentController { @GetMapping("/department/list") public ResponseDTO<List<DepartmentVO>> listDepartment () { return departmentService.listDepartment(); }
反例:
1 2 3 4 5 6 7 @RequestMapping ("/department" )public class DepartmentController { @GetMapping("/list") public ResponseDTO<List<DepartmentVO>> listDepartment () { return departmentService.listDepartment(); }
2)不推荐使用 restful 命名 url
不推荐使用 restful 命名 url, 只能使用 get/post 方法。url 命名遵循:/业务模块/子模块/动作 ; 其中 业务模块和子模块 使用 名字, 动作 使用动词; 原因:
虽然 restful 大法好,但是有时并不能一眼根据 url 看出来是什么操作,所以我们选择了后者,这个没有对与错,只有哪个更适合我们的团队
正例:
1 2 3 4 5 6 GET /department/get/{id} 查询某个部门详细信息 POST /department/query 复杂查询 POST /department/add 添加部门 POST /department/update 更新部门 GET /department/delete/{id} 删除部门 GET /department/employee/delete/{id} 删除部门员工
3)swagger 接口注释必须加上后端作者
每个方法必须添加 swagger 文档注解 @ApiOperation ,并填写接口描述信息,描述最后必须加上接口的作者信息,格式如下: @author 卓大
正例:
1 2 3 4 5 @ApiOperation("更新部门信息 @author 卓大") @PostMapping("/department/update") public ResponseDTO<String> updateDepartment (@Valid @RequestBody DepartmentUpdateForm departmentUpdateForm) { return departmentService.updateDepartment(departmentUpdateForm); }
4)controller 每个方法要保持简洁
controller 在mvc中负责协同和委派业务,充当路由的角色,所以要保持代码少量和清晰,要做到如下要求:
正例:
1 2 3 4 5 @ApiOperation("添加部门 @author 卓大") @PostMapping("/department/add") public ResponseDTO<String> addDepartment (@Valid @RequestBody DepartmentAddForm departmentAddForm) { return departmentService.addDepartment(departmentAddForm); }
5)只能在 controller 层获取当前请求用户
只能在 controller 层获取当前请求用户,并传递给 service 层。
因为获取当前请求用户是从 ThreadLocal 里获取取的,在 service、manager、dao 层极有可能是其他非 request 线程调用,会出现 null 的情况,尽量避免
1 2 3 4 5 6 @ApiOperation("添加员工 @author 卓大") @PostMapping("/employee/add") public ResponseDTO<String> addEmployee (@Valid @RequestBody EmployeeAddForm employeeAddForm) { RequestUser requestUser = SmartRequestUtil.getRequestUser(); return employeeService.addEmployee(employeeAddForm, requestUser); }
service 层规范1)合理拆分 service 业务
我们不建议service文件行数太大 ,如果业务较大,请拆分为多个 service;
如订单业务,所有业务都写到 OrderService 中会导致文件过大,故需要进行拆分如下:
OrderQueryService 订单查询业务
OrderCreateService 订单新建业务
OrderDeliverService 订单发货业务
OrderValidatorService 订单验证业务
2)谨慎使用 @Transactional 事务注解
谨慎使用 @Transactional 事务注解的使用,不要简单对 service 的方法添加个 @Transactional 注解就觉得万事大吉了。应当合并对数据库的操作,尽量减少添加了@Transactional方法内的业务逻辑。@Transactional 注解内的 rollbackFor 值必须使用异常 Exception.class
对于@Transactional 注解,当 spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,如果业务并没有进入到最终的 操作数据库环节,那么就没有必要获取连接并开启事务,应该直接将 connection 返回给数据库连接池,供其他使用(比较难以讲解清楚,如果不懂的话就主动去问)。
反例:
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 @Transactional(rollbackFor = Exception.class) public ResponseDTO<String> upOrDown (Long departmentId, Long swapId) { DepartmentEntity departmentEntity = departmentDao.selectById(departmentId); if (departmentEntity == null ) { return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS); } DepartmentEntity swapEntity = departmentDao.selectById(swapId); if (swapEntity == null ) { return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS); } Long count = employeeDao.countByDepartmentId(departmentId) if (count != null && count > 0 ) { return ResponseDTO.wrap(DepartmentResponseCodeConst.EXIST_EMPLOYEE); } Long departmentSort = departmentEntity.getSort(); departmentEntity.setSort(swapEntity.getSort()); departmentDao.updateById(departmentEntity); swapEntity.setSort(departmentSort); departmentDao.updateById(swapEntity); return ResponseDTO.succ(); }
以上代码前三步都是使用 connection 进行验证操作,由于方法上有@Transactional 注解,所以这三个验证都是使用的同一个 connection。
若对于复杂业务、复杂的验证逻辑,会导致整个验证过程始终占用该 connection 连接,占用时间可能会很长,直至方法结束,connection 才会交还给数据库连接池。
对于复杂业务的不可预计的情况,长时间占用同一个 connection 连接不是好的事情,应该尽量缩短占用时间。
正例:
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 DepartmentService.java public ResponseDTO<String> upOrDown (Long departmentId, Long swapId) { DepartmentEntity departmentEntity = departmentDao.selectById(departmentId); if (departmentEntity == null ) { return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS); } DepartmentEntity swapEntity = departmentDao.selectById(swapId); if (swapEntity == null ) { return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS); } Long count = employeeDao.countByDepartmentId(departmentId) if (count != null && count > 0 ) { return ResponseDTO.wrap(DepartmentResponseCodeConst.EXIST_EMPLOYEE); } departmentManager.upOrDown(departmentSort,swapEntity); return ResponseDTO.succ(); } DepartmentManager.java @Transactional(rollbackFor = Throwable.class) public void upOrDown (DepartmentEntity departmentEntity ,DepartmentEntity swapEntity) { Long departmentSort = departmentEntity.getSort(); departmentEntity.setSort(swapEntity.getSort()); departmentDao.updateById(departmentEntity); swapEntity.setSort(departmentSort); departmentDao.updateById(swapEntity); }
将数据在 service 层准备好,然后传递给 manager 层,由 manager 层添加@Transactional 进行数据库操作。
以上是使用 manager 去处理解决的,其实也可以是使用spring的 TransactionTemplate 事务模板解决
3)需要注意的是:注解 @Transactional 事务在类的内部方法调用是不会生效的
反例:如果发生异常,saveData 方法上的事务注解并不会起作用
1 2 3 4 5 6 7 8 9 10 11 12 @Service public class OrderService { public void createOrder (OrderAddForm addForm) { this .saveData(addForm); } @Transactional(rollbackFor = Exception.class) public void saveData (OrderAddForm addForm) { orderDao.insert(addForm); } }
Spring 采用动态代理(AOP)实现对 bean 的管理和切片,它为我们的每个 class 生成一个代理对象。只有在代理对象之间进行调用时,可以触发切面逻辑。而在同一个 class 中,方法 A 调用方法 B,调用的是原对象的方法,而不通过代理对象。所以 Spring 无法拦截到这次调用,也就无法通过注解保证事务了。简单来说,在同一个类中的方法调用,不会被方法拦截器拦截到,因此事务不会起作用。
解决方案:
可以将方法放入另一个类,如新增 manager层,通过 spring 注入,这样符合了在对象之间调用的条件。
启动类添加@EnableAspectJAutoProxy(exposeProxy = true),方法内使用AopContext.currentProxy()获得代理类,使用事务。
1 2 3 4 5 6 7 8 9 10 11 12 SpringBootApplication.java @EnableAspectJAutoProxy(exposeProxy = true) @SpringBootApplication public class SpringBootApplication {}OrderService.java public void createOrder (OrderCreateDTO createDTO) { OrderService orderService = (OrderService)AopContext.currentProxy(); orderService.saveData(createDTO); }
manager 层规范 dao 层规范 1)持久层框架选择
优先使用 mybatis-plus 框架。
2)使用 mybatis-plus 的要求
正例: NoticeDao.java 常量在参数中传入到 xml
1 2 3 4 5 public interface NoticeDao { Integer noticeCount (@Param("sendStatus") Integer sendStatus) ; }
NoticeMapper.xml
1 2 3 4 5 6 7 <select id ="noticeCount" resultType ="integer" > select count(1) from t_notice where send_status = #{sendStatus} </select >
反例:常量直接写死到 xml 中
1 2 3 4 5 public interface NoticeDao { Integer noticeCount () ; }
NoticeMapper.xml
1 2 3 4 5 6 7 <select id ="noticeCount" resultType ="integer" > select count(1) from t_notice where send_status = 0 </select >
3)连接 join 写法
建议在 xml 中的 join 关联写法使用表名的全称,而不是用别名,对于关联表太多的话,在 xml 格式中,其实很难记住 别名是什么意思!
反例: t_notice 别名 tn,t_employee 别名 e, 在 xml 中已经很难区分是什么意思了,索性不如使用全称
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 <select id ="queryPage" resultType ="net.lab1024.sa.admin.module.business.oa.notice.domain.vo.NoticeVO" > SELECT tn.*, e.actual_name AS createUserName FROM t_notice tn LEFT JOIN t_employee e ON tn.create_user_id = e.employee_id <where > tn.deleted_flag = #{queryForm.deletedFlag} <if test ="queryForm.keywords != null and queryForm.keywords != ''" > AND (INSTR(tn.notice_title,#{queryForm.keywords}) OR INSTR(e.actual_name,#{queryForm.keywords})) </if > <if test ="queryForm.noticeType != null" > AND tn.notice_type = #{queryForm.noticeType} </if > <if test ="queryForm.noticeBelongType != null" > AND tn.notice_belong_type = #{queryForm.noticeBelongType} </if > <if test ="queryForm.startTime != null" > AND DATE_FORMAT(tn.publish_time, '%Y-%m-%d') > = #{queryForm.startTime} </if > <if test ="queryForm.endTime != null" > AND DATE_FORMAT(tn.publish_time, '%Y-%m-%d') < = #{queryForm.endTime} </if > <if test ="queryForm.disabledFlag != null" > AND tn.disabled_flag = #{queryForm.disabledFlag} </if > </where > <if test ="queryForm.sortItemList == null or queryForm.sortItemList.size == 0" > ORDER BY tn.top_flag DESC,tn.publish_time DESC </if > </select >
正确:使用全称
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 <select id ="queryPage" resultType ="net.lab1024.sa.admin.module.business.oa.notice.domain.vo.NoticeVO" > SELECT t_notice.*, t_employee.actual_name AS createUserName FROM t_notice LEFT JOIN t_employee ON t_notice.create_user_id = t_employee.employee_id <where > t_notice.deleted_flag = #{queryForm.deletedFlag} <if test ="queryForm.keywords != null and queryForm.keywords != ''" > AND (INSTR(t_notice.notice_title,#{queryForm.keywords}) OR INSTR(t_employee.actual_name,#{queryForm.keywords})) </if > <if test ="queryForm.noticeType != null" > AND t_notice.notice_type = #{queryForm.noticeType} </if > <if test ="queryForm.noticeBelongType != null" > AND t_notice.notice_belong_type = #{queryForm.noticeBelongType} </if > <if test ="queryForm.startTime != null" > AND DATE_FORMAT(t_notice.publish_time, '%Y-%m-%d') > = #{queryForm.startTime} </if > <if test ="queryForm.endTime != null" > AND DATE_FORMAT(t_notice.publish_time, '%Y-%m-%d') < = #{queryForm.endTime} </if > <if test ="queryForm.disabledFlag != null" > AND t_notice.disabled_flag = #{queryForm.disabledFlag} </if > </where > <if test ="queryForm.sortItemList == null or queryForm.sortItemList.size == 0" > ORDER BY t_notice.top_flag DESC,t_notice.publish_time DESC </if > </select >
javabean 命名规范 1) javabean 的整体要求:
不得有任何的业务逻辑或者计算
基本数据类型必须使用包装类型(Integer, Double、Boolean 等)
不允许有任何的默认值
每个属性必须添加注释,并且必须使用多行注释。
必须使用 lombok 简化 getter/setter 方法
建议对象使用 lombok 的 @Builder ,@NoArgsConstructor,同时使用这两个注解,简化对象构造方法以及 set 方法。
2)javabean 名字划分:
XxxEntity 数据库持久对象
XxxVO 返回前端对象
XxxForm 前端请求对象
XxxDTO 数据传输对象
XxxBO 内部处理对象
3)数据对象;XxxxEntity,要求:
4)请求对象;XxxxForm,要求:
5)返回对象;XxxxVO,要求:
6)业务对象 BO,要求:
boolean 类型的属性命名规范 类中布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。反例:定义为基本数据类型 Boolean isDeleted;的属性,它的方法也是 isDeleted(),RPC 在反向解析的时候,“以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
这是阿里巴巴开发手册中的原文,我们团队的规定是:boolean 类型的类属性和数据表字段都统一使用 flag 结尾。虽然使用 isDeleted,is_deleted 从字面语义上更直观,但是比起可能出现的潜在错误,这点牺牲还是值得的。
正例:
1 deletedFlag,deleted_flag,onlineFlag,online_flag