Java规范
# 环境规范
# IDE
- 使用
LF(\n)
换行符,而非CRLF(\r\n)
说明:由于项目通常在 Linux 环境下编译,为了不必要的麻烦统一使用 LF(\n)
换行符,尤其是使用 windows 的同学,windows 默认为 CRLF(\r\n)
,而新版的 macOS 或 Linux 用户通常不需要修改,默认就是 LF(\n)
IDEA 设置默认: File | Settings | Editor | Code Style
,Line separator: Unix and macOS(\n)
- 使用
4个空格
缩进,而非Tab 字符
说明:在使用 IDEA 开发中,我们仍然使用 tab 按键进行缩进,但需要在 IDEA 右下方配置为 4 spaces
,通常默认就是
- 使用
UTF-8
编码,而非GBK
# 目录结构
cn.nipx. 业务线.[子业务线],最多 4 级
包名 | 说明 |
---|---|
controller | 控制器 |
service | 业务服务 |
constant | 常量 |
client | 供其他服务调用的 feign 接口,此包应存在于供其他服务依赖的 api 模块中,后缀 Client |
client | 供其他服务调用的 feign 接口实现,实际为控制器,此包应存在于服务模块中,后缀 ClientImpl |
listener | 消息队列消费者 |
# 编码规范
以教务模块(Academic)下的学生管理(Student)为例
# 通用
# 依赖注入
- 使用构造器注入,而非
@Autowired
说明:Spring (4.x+) 推荐使用构造器注入
正例:
@RestController
@RequiredArgsConstructor
class StudentController{
private final StudentService studentService;
}
反例:
@RestController
class StudentController{
@Autowired
private StudentService studentService;
}
- 集合对象变量名应使用接口后缀,而非实现类后缀
正例:
List<Student> studentList = new ArrayList<>();
Set<String, Student> studentSet = new HashSet<>();
Map<String, Student> studentMap = new HashMap<>();
反例:
ArrayList<Student> studentArrayList = new ArrayList<>();
HashSet<String, Student> studentHashSet = new HashSet<>();
HashMap<String, Student> studentHashMap = new HashMap<>();
- 对象列表变量名使用
List
后缀,而非复数
说明:List 后缀相较于复数有更好的阅读性
正例: List<Student> studentList
反例: List<Student> students
特例: List<Long> userIds
,id 列表使用复数
# 服务发现
- 在本机添加环境变量
SPRING_CLOUD_NACOS_DISCOVERY_WEIGHT
值为0
,避免前端调试调到本机。
说明:而不是使用添加 SPRING_CLOUD_NACOS_DISCOVERY_ENABLED
值为 false
,这会导致本机无法通过服务发现调用其他服务。
# Controller 层(含 HTTP API)
# 职责
- 接收前端请求
- 对前端请求参数做基本的校验(@Validated)
- 调用 Service 层完成前端需求
# 类
- 类名命名时应将模块名前缀去掉,在同一个模块下还怕分不清这个对象是哪个模块的?
正例:StudentController
反例:AcademicStudentController
# HTTP 接口
- 控制器资源使用复数命名
正例: /students/...
反例: /student/...
- 资源、动作由多个单词组成时,使用
-
分割
正例: /professional-emphasis/...
反例: /professionalEmphasis/...
- 使用
/资源/actions/动作
扩展 RESTful 接口,而不是直接在资源后面添加动作
正例: PUT /students/{id}/actions/disable
、 GET /students/actions/code-list
反例: PUT /students/{id}/disable
、 GET /students/code-list
- 幂等性接口使用 PathVariable 接收必填参数
正例: GET /students/clazz-id/{clazzId}
:获取某个班级的学生列表
反例: GET /students?clazz-id={clazzId}
- 请求类型遵循 RESTful 规范
GET
:
- 安全且幂等
- 获取表示
- 变更时获取表示(缓存)
POST
:
- 不安全且不幂等
- 使用服务端管理的(自动产生)的实例号创建资源
- 创建子资源
- 部分更新资源
- 如果没有被修改,则不过更新资源(乐观锁)
PUT
- 不安全但幂等
- 用客户端管理的实例号创建一个资源
- 通过替换的方式更新资源
- 如果未被修改,则更新资源(乐观锁)
DELETE
:
- 不安全但幂等
- 删除资源
参考:
- AIP-136: Custom methods (opens new window)
- “一把梭:REST API 全用 POST” | 酷 壳 - CoolShell (opens new window)
# 方法
- 方法命名应尽量表达业务含义而非数据变化
正例: detail
、 distable
、 enable
反例: getById
、 updateIsDisable
- 前台接口必须使用 Req (DTO) 对象入参,而非 Entity 对象
说明:确保 Req 对象中的属性都是业务必须的,严禁图省事使用 Entity 对象,避免前端传递非法参数(如创建时间等敏感字段)。
正例: create(@RequestBody @Validated StudentReq req)
反例: create(@RequestBody @Validated Student student)
前台结构必须校验入参
不应在 Controller 层编写任何业务逻辑,应全权交由 Service 层处理
正例:
pubilc Result<Void> create(@RequestBody @Validated StudentReq req){
studentService.create(req);
return Result.ok();
}
反例:
pubilc Result<Void> create(@RequestBody @Validated StudentReq req){
Student student = BeanUtil.copyProperties(req, Student.class);
Assert.isTrue(studentService.create(student), "新增失败");
return Result.ok();
}
- Service 层无返回 (void) 时,返回成功
说明:约定 Service 层方法处理过程中出现异常,会抛出 RuntimeException
,再通过全局异常处理返回 500 给前端,而如能够执行到 return 说明执行成功。
正例:
pubilc Result<Void> create(@RequestBody @Validated StudentReq req){
studentService.create(req);
return Result.ok();
}
@RequestMapping("/students") // 资源使用复数
class StudentController{
// 列表
@GetMapping
Result<List<StudentResp>> list(StudentQueryReq query);
// 分页列表
@GetMapping
ResultPage<StudentResp> page(StudentQueryReq query);
// 学生学号列表
@GetMapping("/actions/code-info-list")
Result<List<StudentResp>> listCodeInfo();
// 详情
@GetMapping("/{id}")
Result<StudentResp> detail(@PathVariable("id") Long id);
// 新增
@PostMapping
Result<Void> create(@RequestBody @Validated StudentReq req);
// 修改
@PutMapping
Result<Void> update(@RequestBody @Validated StudentReq req);
// 删除
@DeleteMapping("/{id}")
Result<Void> delete(@PathVariable("id") Long id);
// 禁用
@PutMapping("/{id}/acitons/disable")
Result<Void> disable(@PathVariable("id") Long id);
}
列表(list)与分页列表(page)
这两个接口互斥,只可使用其中一个,如果前端即用到分页又用到全部,那么使用分页列表 (page),在需要返回全部时传递 pageSize=-1
,如果前端只需要返回全部,则使用列表 (list)。
为什么使用delete而不是remove?
在一些规范中约定 remove 是逻辑删除,delete 是物理删除;这里的命名跟这些规范没有任何关系,纯属约定。
而且逻辑还是物理删除这不是 controller 层也不是 service 层考虑的事,这应由持久层决定。
而在有回收站这一概念的业务场景中,应使用另外的字段 (
waitDelete
) 来区分,remove
为删除到回收站,delete
为彻底删除。
# Service 层
# 类
注意
本规范下的 Service 层结构有别于大部分的框架,不使用 接口-实现类
的结构,而是直接使用 实现类
,在笔者有限的项目经验中未能找到任何必须使用 接口-实现类
结构的场景。
详细理由参考:Do I need an interface with Spring boot? | Dimitri's tutorials (opens new window)
- 严禁实现 MyBatis-Plus 的 IService 接口及实现
说明:MyBatis-Plus (mp) 的 IService 接口及实现虽能节省编写通用 CURD 方法的时间,但同时会使得 mp 与 service 耦合,如之后重构需更改持久化框架,需要大量的修改,不能满足开闭原则。
正例: public class StudentService{}
反例: public class StudentService extends ServiceImpl<StudentMapper, Student> implements IService<Student>{}
# 方法
- 命名应与 Controller 层保持一致,即应尽量表达业务含义而非数据变化
说明:在能用简短词汇描述时,优先使用词汇命名,即 detail
优先于 getById
。
- 获取单个对象的方法用 get 做前缀。
getCodeInfoById
- 获取多个对象的方法用 list 做前缀。如:
list
、listCodeInfo
- 获取统计值的方法用 count、sum、min、max 做前缀。
- 插入的方法用 create 做前缀。
- 删除的方法用 remove 做前缀。
remove
- 修改的方法用 update 做前缀。
update
词汇
优于For
优于By
说明:在命名时应优先这个方法能不能用一个词汇概括(如: create
、 update
),再考虑能不能用用于 (For) 来概括(如:listForMe),最后再考虑通过 (By) 概括(如:listByStatus)。
对于调用者抽象,无需了解功能实现的细节,只需知道调用 update 方法就能完成更新操作,只需传递合法入参即可 (控制器 + service 校验),而不需要通过方法名中的 ById 告知是通过 ID 更新的。
正例: changePassword
优于 updatePasswordById
- 获取当前 Service 对应实体时无需在方法名中强调
说明:StudentService 对应的实体类即是 Student,因此在获取 Student 对象的方法名省略 Student,而 StudentResp 不是对应实体,因此需要标明。
正例: getById(id)
、 getStudentResqById(id)
、 list(query)
、 listStudentResq(query)
反例: getStudentById(id)
、 listStudent(query)
- 禁止在本层使用任何 MyBatis-Plus 包下的东西
说明:MyBatis-Plus 作为一个具体的持久化框架,不应该与 Service 有任何直接接触,而是面向持久层接口。
# Repository 层
StudentRepository --> StudentRepositoryImpl
在很多项目中,Service 层下面就到了 Mapper 层,但本规范在这两层之间增加了 Repository 层,原因如下:
- MyBatis-Plus (mp) 与 Service 层耦合:mp 的条件构造器能够通过 Java 代码代替大部分 CURD SQL 编写,而这些代码在没有 Repository 层之前,需要编写在 Service 层,从而导致业务逻辑代码与数据查询逻辑代码耦合在一起,降低可读性
- MyBatis 与 Service 层耦合:即使抛开 mp,直接依赖 Mapper 仍然会导致 Service 与 MyBatis 耦合,Mapper Interface 作为 MyBatis 特有的接口命名,在更换 ORM 框架时,仍然需要大量修改 Service 层的依赖。
因此 Service 层应依赖一个自定义的持久层接口,无论项目更改了什么 ORM 框架或是数据库都不会影响到 Service 层之上,只需将新的持久层实现类注册的 Spring 容器即可以完成切换。
# 类
- Repository 接口禁止继承 MyBatis-Plus 的 IService
说明:在接口继承 IService 跟在 Service 继承没什么区别,但允许在 Repository 的实现类中实现及继承 ServiceImpl,由于 Service 是依赖 Repository 接口,实现类继承任何东西都不会使 Repository 接口暴露无关的方法,而在实现类继承 ServiceImpl 则能基于其内置方法实习大部分的接口方法,免于编写 SQL。
正例: public class StudentRepositoryImpl extends ServiceImpl<StudentMapper, Student> implements IService<Student>{}
反例: public interface StudentRepository extends IService<Student>{}
# 方法
- 命名规范
- 获取单个对象的方法用 find 做前缀。如:
findById
- 获取多个对象的方法用 select 做前缀。如:
selectAll
、selectByQuery
、selectCodeInfo
- 获取分页对象的方法用 page 做前缀。如:
page(query)
- 获取统计值的方法用 count、sum、min、max 做前缀。
countByStatus
- 插入的方法用 insert 做前缀。 如:
insert
- 删除的方法用 delete 做前缀。 如:
deleteById
- 修改的方法用 update 做前缀。如:
updateById
参考 JPA 命名规则 (opens new window)
- 分页查询方法命名使用
page(query)
,列表查询方法使用slecteByQuery(query)
说明:约定认为分页默认携带查询参数,因此无需强调 byQuery
正例: ResultPage<Student> page(StudentQueryReq query)
反例: ResultPage<Student> pageByQuery(StudentQueryReq query)
# Domain
- 在 Java 的世界中,万事万物皆对象,因此在开发中,不可避免会创建很多领域模型,在本规范中浅将其分为实体对象、数据传输对象。
- 约定下面的所有对象存储到
domain
包下,如/doamin/entity/Student
、/damain/dto/req/StudentReq
为什么是domain包下,而不是model、pojo?
无论是 domain、model、pojo 要表达的意思都是相似的,包下的类都是一些只有 private
、 get
、 set
的简单对象,之所以使用 domain 纯属约定。
# 实体对象 (Entity)
包名:
/entiy
后缀:
无
正例: Student
反例: StudentEntity
- 属性:属性与数据库字段完全一致,不应包括其他属性
反例:数据库中只有班级 id ( clazz_id
) 字段,但在 Entity 对象中另外添加了班级名称 ( clazz_name
) 字段。如因 API 返回需要,应在 Resp 对象中添加。
# 数据传输对象 (DTO)
- 包名:
/dto
说明:在本规范中并没有以 DTO 为后缀的对象,因此 dto 包下并没有类文件,只有子包(如下)。
# 查询对象 (Query)
- 包名:
/query
- 后缀:
Query
正例: StudentQuery
说明:多用于接收列表查询条件对象
# 请求对象 (Req)
- 包名:
/req
- 后缀:
Req
正例: StudentAddReq
说明:用于 POST 操作的请求体接收,如创建对象、修改对象。
# 响应对象 (Resp)
- 包名:
/resp
- 后缀:
Resp
正例: StudentDetailResp
说明:用于方法返回对象,如对象列表、对象详情。
# 命名
- 命名规范:
[<服务端名>]<实体名称>[<动作>]<DTO类型>
正例: StudentAddReq
、 StudentEditReq
、 AppStudentAddReq
、 MpStudentResp
- 约定后台服务 DTO 省略 Admin 服务端名前缀
正例: StudentReq
反例: AdminStudentReq
约定新增和编辑请求 Req 属性除 id 以为一致时,共用
StudentReq
,反之分别使用StudentAddReq
、StudentEditReq
通用的 CURD 请求对象均不需要添加动作名,此外的与 Controller 的 URL 中的
acitons/<动作>
一致
正例: HTTP /students/{id}/acitons/audit
=> StudentAuditReq
# Util 工具类
- 使用 lombok 的
@NoArgsConstructor(access = AccessLevel.PRIVATE)
注解禁止实例化。