我们参考 Kent Beck 的简单设计四原则来指导我们的如何写出优秀的代码,如何有效地判断我们的代码是优秀的。
以上四个原则的重要程度依次降低, 这组定义被称做简单设计原则。
全部采用小写方式, 项目名称:下划线分隔,子模块名称:中划线分隔。
项目名称:xxxxx_xxxx_xxx_v1 模块名称:xxxx-xxxxxx
TODO/TBD(to be determined) 注释一般用来描述已知待改进、待补充的修改点。
// TODO : 补充XX处理
无论是 controller,service,dao 亦或是其他的代码,每个方法最多 3 个参数,如果超出 3 个参数的话,要封装成 javabean 对象。
反例:
/**
* 使用证书加密数据工具方法
*
* @param param
* @param password 加密密码
* @param priCert 私钥
* @param pubCert 公钥
* @return 返回加密后的字符串
*/
public String signEnvelop(JdRequestParam param, String password, String priCert, String pubCert){}
注释除了说明作用、逻辑之外。还有一个很重要的原因:当业务逻辑过于复杂,代码过于庞大的时候,注释就变成了一道道美化环境、分离与整理逻辑思路的路标。这是很重要的一点,它能有效得帮助我们免于陷入代码与业务逻辑的泥沼之中。
正例:
public Boolean stopCharge(ChargeControlDTO dto, Long userId) {
final Device device = iDeviceService.getById(dto.getDeviceId());
Assert.isTrue(!ObjectUtils.isEmpty(device), ResultCode.INVALID_ARGUMENT, AlertMessage.DATA_IS_NOT_EXIST);
Assert.isTrue(device.getOnlineStatus(),ResultCode.SENTINEL,AlertMessage.OFFLINE);
Connector connector = iConnectorService.getOne(dto.getDeviceId(),dto.getConnectorSerialNumber());
Assert.isTrue(!ObjectUtils.isEmpty(connector), ResultCode.INVALID_ARGUMENT, AlertMessage.DATA_IS_NOT_EXIST);
注释并不是越多越好,当注释过多,维护代码的同时,还需要维护注释,不仅变成了一种负担,也与我们当初添加注释的初衷背道而驰。
首先:大家应该通过清晰的逻辑架构,好的变量命名来提高代码可读性;需要的时候,才辅以注释说明。注释是为了帮助阅读者快速读懂代码,所以要从读者的角度出发,按需注释。注释内容要简洁、明了、无二义性,信息全面且不冗余。
其次:无论是修改、复制代码时,都要仔细核对注释内容是否正确。只改代码,不改注释是一种不文明行为,破坏了代码与注释的一致性,会让其他人迷惑、费解,甚至误解。
反例:
// 查询部门
EmployeeDTO employee = employeeDao.listByDeptId(deptId);
方法要尽量通过方法名自解释,不要写无用、信息冗余的方法头,不要写空有格式的方法头注释。
方法头注释内容可选,但不限于:功能说明、返回值,用法、算法实现等等。尤其是对外的方法接口声明,其注释,应当将重要、有用的信息表达清楚。
反例:
/**
*
* @param phone
* @return
*/
List getByPhone(String phone);
反例中出现的问题:
方法注释没有说明具体的作用、使用事项。
参数、返回值,空有格式没内容。这是非常重要一点,任何人调用任何方法之前都需要知道方法对参数的要求,以及返回值是什么。
统一的目录结构是所有项目的基础。
xxxx_xxxx 源码目录
|-- module-base 子模块层级
|-- |-- base-api 接口api
|-- |-- base-biz 业务层
|-- elec-server 项目启动模块(只负责启动项目)
|-- xxx-framework 结构模块
|-- |-- xxx-common 通用工具类
|-- |-- xxx-mybatis mybatis配置
|-- |-- xxxx-web web端配置
common 目录用于存放各个项目通用的项目,但是又可以依照项目进行特定的修改。
src 源码目录
|-- common 各个项目的通用类库
|-- |--- anno 通用注解,比如权限,登录等等
|-- |--- constant 通用常量,比如 ResponseCodeConst
|-- |--- domain 全局的 javabean,比如 BaseEntity,PageParamDTO 等
|-- |--- exception 全局异常,如 BusinessException
|-- |--- json json 类库,如 LongJsonDeserializer,LongJsonSerializer
|-- |--- swagger swagger 文档
|-- |--- validator 适合各个项目的通用 validator,如 CheckEnum,CheckBigDecimal 等
config 目录用于存放各个项目通用的项目,但是又可以依照项目进行特定的修改。
src 源码目录
|-- config 项目的所有配置信息
|-- |--- MinIoConfiguration minio文件上传的相关配置
|-- |--- RedisConfiguration Redis数据库的配置
|-- |--- SwaggerConfig Swagger文档的配置信息
|-- |--- .... 其他
module 目录里写项目的各个业务,每个业务一个独立的顶级文件夹,在文件里进行 mvc 的相关划分。 其中,domain 包里存放 entity, dto, vo,bo 等 javabean 对象
src
|-- module 所有业务模块
|-- |-- role 角色模块
|-- |-- |--RoleController.java controller
|-- |-- |--RoleConst.java role相关的常量
|-- |-- |--RoleService.java service
|-- |-- |--RoleDao.java dao
|-- |-- |--domain domain
|-- |-- |-- |-- RoleEntity.java 表对应实体
|-- |-- |-- |-- RoleDTO.java dto对象
|-- |-- |-- |-- RoleVO.java 返回对象
|-- |-- employee 员工模块
|-- |-- login 登录模块
|-- |-- email 邮件模块
|-- |-- .... 其他
1) javabean 的整体要求:
- 不得有任何的业务逻辑或者计算
- 基本数据类型必须使用包装类型(Integer, Double、Boolean 等)
- 不允许有任何的默认值
- 必须使用 lombok 简化 getter/setter 方法
- 建议对象使用 lombok 的 @Builder ,@NoArgsConstructor,同时使用这两个注解,简化对象构造方法以及set方法。
正例:
@Builder
@NoArgsConstructor
@Data
public class DemoDTO {
private String name;
private Integer age;
}
// 使用示例:
DemoDTO demo = DemoDTO.builder()
.name("yeqiu")
.age(66)
.build();
2)数据对象;model,要求:
- Xxxx 与数据库表名保持一致
- 类中字段要与数据库字段保持一致,不能缺失或者多余
- 不允许有组合
- 项目内的日期类型必须统一,采用时间戳的形式返回给前端。
3)传输对象;XxxxDTO,XxxxQUERY,XxxxUPDATE 要求:
- 不可以继承自 MODEL
- DTO 可以继承、组合其他 DTO,VO等对象
- DTO 只能用于前端、RPC 的请求参数
- QUERY 见名知意,用于查询操作
- UPDATE 见名知意,用于更新操作
3)视图对象;XxxxVO,要求:
- 不可继承自 MODEL
- VO 可以继承、组合其他 DTO,basePage 等对象
- VO 只能用于返回前端、rpc 的业务数据封装对象
1) 只允许在 method 上添加 RequestMapping 注解,不允许加在 class 上(为了方便的查找 url,放到 url 不能一次性查找出来)
正例:
@RestController
public class DepartmentController {
@GetMapping("/department/list")
public ResponseDTO> listDepartment() {
return departmentService.listDepartment();
}
反例:
@RequestMapping ("/department")
public class DepartmentController {
@GetMapping("/list")
public ResponseDTO> listDepartment() {
return departmentService.listDepartment();
}
2)不推荐使用 rest 命名 url, 只能使用 get/post 方法。url 命名上规范如下:
虽然 Rest 大法好,但是有时并不能一眼根据 url 看出来是什么操作,所以我们选择了后者,这个没有对与错,只有哪个更适合我们的团队。
正例:
GET /department/get/{id} 查询某个部门详细信息
POST /department/query 复杂查询
PUT /department/update 更新部门
DELETE /department/delete 删除部门
3)每个接口必须添加 swagger 文档注解 @ApiOperation ,并填写接口描述信息。
@ApiOperation("更新部门信息")
@PostMapping("/department/update")
public ResponseDTO updateDepartment(@Valid @RequestBody DeptUpdateDTO deptUpdateDTO) {
return departmentService.updateDepartment(deptUpdateDTO);
}
4)controller 负责协同和委派业务,充当路由的角色,每个方法要保持简洁:
正例:
@ApiOperation("添加部门 ")
@PostMapping("/department/add")
public ResponseDTO addDepartment(@Valid @RequestBody DepartmentCreateDTO departmentCreateDTO) {
return departmentService.addDepartment(departmentCreateDTO);
}
5)只能在 controller 层获取当前请求用户,并传递给 service 层。
因为获取当前请求用户是从 ThreadLocal 里获取取的,在 service、manager、dao 层极有可能是其他非 request 线程调用,会出现 null 的情况,尽量避免
@ApiOperation("添加员工")
@PostMapping("/employee/add")
public ResponseDTO addEmployee(@Valid @RequestBody EmployeeAddDTO employeeAddDTO) {
LoginTokenBO requestToken = SmartRequestTokenUtil.getRequestUser();
return employeeService.addEmployee(employeeAddDTO, requestToken);
}
1)合理拆分 service 文件,如果业务较大,请拆分为多个 service。
如订单业务,所有业务都写到 OrderService 中会导致文件过大,故需要进行拆分如下:
2)谨慎处理 @Transactional 事务注解的使用,不要简单对 service 的方法添加个 @Transactional 注解就觉得万事大吉了。应当合并对数据库的操作,尽量减少添加了@Transactional方法内的业务逻辑。
@Transactional 注解内的 rollbackFor 值必须使用异常的基类 Throwable.class
对于@Transactional 注解,当 spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,如果业务并没有进入到最终的 操作数据库环节,那么就没有必要获取连接并开启事务,应该直接将 connection 返回给数据库连接池,供其他使用(比较难以讲解清楚,如果不懂的话就主动去问)。
反例:
@Transactional(rollbackFor = Throwable.class)
public ResponseDTO upOrDown(Long departmentId, Long swapId) {
// 验证 1
DepartmentEntity departmentEntity = departmentDao.selectById(departmentId);
if (departmentEntity == null) {
return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS);
}
// 验证 2
DepartmentEntity swapEntity = departmentDao.selectById(swapId);
if (swapEntity == null) {
return ResponseDTO.wrap(DepartmentResponseCodeConst.NOT_EXISTS);
}
// 验证 3
Long count = employeeDao.countByDepartmentId(departmentId)
if (count != null && count > 0) {
return ResponseDTO.wrap(DepartmentResponseCodeConst.EXIST_EMPLOYEE);
}
// 操作数据库 4
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 连接不是好的事情,应该尽量缩短占用时间。
正例:
DepartmentService.java
public ResponseDTO 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 进行数据库操作。
3)需要注意的是:注解 @Transactional 事务在类的内部方法调用是不会生效的
反例:如果发生异常,saveData方法上的事务注解并不会起作用
@Service
public class OrderService{
public void createOrder(OrderCreateDTO createDTO){
this.saveData(createDTO);
}
@Transactional(rollbackFor = Throwable.class)
public void saveData(OrderCreateDTO createDTO){
orderDao.insert(createDTO);
}
}
Spring采用动态代理(AOP)实现对bean的管理和切片,它为我们的每个class生成一个代理对象。只有在代理对象之间进行调用时,可以触发切面逻辑。而在同一个class中,方法A调用方法B,调用的是原对象的方法,而不通过代理对象。所以Spring无法拦截到这次调用,也就无法通过注解保证事务了。简单来说,在同一个类中的方法调用,不会被方法拦截器拦截到,因此事务不会起作用。
解决方案:
SpringBootApplication.java
@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}
OrderService.java
public void createOrder(OrderCreateDTO createDTO){
OrderService orderService = (OrderService)AopContext.currentProxy();
orderService.saveData(createDTO);
}
4)service是具体的业务处理逻辑服务层,尽量避免将web层某些参数传递到service中。
反例:
public ResponseDTO handlePinganRequest(HttpServletRequest request){
InputStreamReader inputStreamReader = new InputStreamReader(request.getInputStream(), "GBK");
BufferedReader reader = new BufferedReader(inputStreamReader);
StringBuilder sb = new StringBuilder();
String str;
while ((str = reader.readLine()) != null) {
sb.append(str);
}
if(!JSON.isValid(msg)){
return ResponseDTO.wrap(ResponseCodeConst.ERROR_PARAM);
}
PinganMsgDTO PinganMsgDTO = JSON.parseObject(msg,PinganMsgDTO.class);
// 示例结束
}
反例中出现的问题:
manager 层的作用(引自《阿里 java 手册》):
- 对第三方平台封 装的层,预处理返回结果及转化异常信息;
- 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;
- 与 DAO 层交互,对多个 DAO 的组合复用。
使用 mybatis-plus 框架。
1)所有 Dao 继承自 BaseMapper
2)Dao层只针对单表数据进行CRUD,不涉及多表,在service进行拼接
3)尽量采用 lambdaQuery()表达式进行处理,不建议new 条件构造器
正例:
@Override
public Page queryUsers(NameQuery query, Long enterpriseId) {
return lambdaQuery()
.eq(ContactUser::getEnterpriseId, enterpriseId)
.like(C.isNotBlank(query.getName()), ContactUser::getName, query.getName())
.page(new Page<>(query.getPageNumber(), query.getPageSize()));
}
3)dao层方法命名规范
建议:dao层方法命名尽量以sql语义命名,避免与业务关联。
正例:
List listByMonthAndItemId(@Param("month") String month, @Param("itemId") Integer itemId);
反例:
List getInternalData(@Param("month") String month, @Param("itemId") Integer itemId);
反例中出现的不规范操作:
类中布尔类型的变量,都不要加is,否则部分框架解析会引起序列化错误。反例:定义为基本数据类型 Boolean isDeleted;的属性,它的方法也是 isDeleted(),RPC在反向解析的时候,“以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
这是阿里巴巴开发手册中的原文,我们团队的规定是:boolean 类型的类属性和数据表字段都统一使用 flag 结尾。虽然使用 isDeleted,is_deleted 从字面语义上更直观,但是比起可能出现的潜在错误,这点牺牲还是值得的。
正例:
deletedFlag,deleted_flag,onlineFlag,online_flag