天天看点

java开发规范(持续更新)一、编程规范二、项目规范三、MVC 规范四、数据库 规范五、其他

该规范基于阿里开发规范和1024创新实验室团队的开发规范整合自己公司的规范整理而成

文章目录

  • 一、编程规范
    • 1、项目命名规范
    • 2、`TODO/FIXME` 规范
    • 3、方法参数规范
    • 4、注释
      • 4.1、方法内逻辑注释
      • 4.2、方法注释
    • 5、状态型取值应使用枚举规范
    • 6、固定常量使用常量类
    • 7、所有mapper接口和service接口需要加上注释
    • 8、ThreadLocal在set()后需要在正确的时间remove()
  • 二、项目规范
    • 1、目录结构
    • 2、domain中的javabean规范
      • 1)javabean整体要求:
      • 2)model包下javabean要求:
      • 3)传输对象;`XxxxDTO`,要求:
      • 4)视图对象;`XxxxVO`,要求:
      • 5)业务对象; `BO`,要求:
  • 三、MVC 规范
    • 1、整体分层
    • 2、 `controller` 层规范
    • 3、 `service` 层规范
    • 4、 `manager` 层规范
    • 5、 `mapper` 层规范
    • 6、`boolean`类型的属性命名规范
  • 四、数据库 规范
    • 1、建表规范
    • 2、枚举类表字段注释需要将所有枚举含义进行注
    • 3、数据库编码
    • 4、合理结合业务给表字段添加索引和唯一索引具体索引规范请参照《阿里巴巴 Java 开发手册》索引规约
  • 五、其他
    • 1、代码提交规范
    • 3、保持项目整洁

一、编程规范

1、项目命名规范

全部采用小写方式, 以中划线分隔。

正例:

mall-management-system / order-service-client / user-api

反例:

mall_management-system / mallManagementSystem / orderServiceClient

2、

TODO/FIXME

规范

TODO/TBD(to be determined)

注释一般用来描述已知待改进、待补充的修改点,并且加上作者名称。

FIXME

注释一般用来描述已知缺陷,它们都应该有统一风格,方便文本搜索统一处理。如:

// TODO <author-name>: 补充XX处理
// FIXME <author-name>: XX缺陷
           

3、方法参数规范

无论是

controller,service,manager,dao

亦或是其他的代码,每个方法最多

3

个参数,如果超出

3

个参数的话,要封装成

javabean

对象。

  1. 方便他人调用,降低出错几率。尤其是当参数是同一种类型,仅仅依靠顺序区分,稍有不慎便是灾难性后果,而且排查起来也极其恶心。
  2. 保持代码整洁、清晰度。当一个个方法里充斥着一堆堆参数的时候,再坚强的人,也会身心疲惫。

反例:

/**
* 使用证书加密数据工具方法
*
* @param param
* @param password 加密密码
* @param priCert 私钥
* @param pubCert 公钥
* @return 返回加密后的字符串
*/
public String signEnvelop(JdRequestParam param, String password, String priCert, String pubCert){}
           

4、注释

4.1、方法内逻辑注释

注释除了说明作用、逻辑之外。还有一个很重要的原因:当业务逻辑过于复杂,代码过于庞大的时候,注释就变成了一道道美化环境、分离与整理逻辑思路的路标。这是很重要的一点,它能有效得帮助我们免于陷入代码与业务逻辑的泥沼之中。

/**
* 开始抽奖方法
* 保存中奖信息、奖励用户积分等
* @param luckDrawDTO
* @return ResponseDTO 返回中奖信息
*/
public ResponseDTO<String> startLuckDraw(LuckDrawDTO luckDrawDTO) {

    // -------------- 1、校验抽奖活动基本信息 ------------------------
    xxx伪代码一顿操作

    // -------------- 2、新增抽奖记录 -------------------------------
    xxx伪代码一顿操作

    // -------------- 3、如果需要消耗积分,则扣除钢镚积分 -------------
    xxx伪代码一顿操作

    // -------------- 4、获取奖品信息,开始翻滚吧 --------------------
    xxx伪代码一顿操作

    return ResponseDTO.succ(luckDrawPrizeVO);
}
           

4.2、方法注释

方法要尽量通过方法名自解释,不要写无用、信息冗余的方法头,不要写空有格式的方法头注释。

方法头注释内容可选,但不限于:功能说明、返回值,用法、算法实现等等。尤其是对外的方法接口声明,其注释,应当将重要、有用的信息表达清楚。

正例:

/**
 * 解析转换时间字符串为 LocalDate 时间类
 * 调用前必须校验字符串格式 否则可能造成解析失败的错误异常
 *
 * @param dateStr 必须是 yyyy-MM-dd 格式的字符串
 * @return LocalDate
 */
public static LocalDate parseYMD(String dateStr){}
           

反例:

/**
 * 校验对象
 *
 * @param t
 * @return String
 */
public static <T> String checkObj(T t);
           

反例中出现的问题:

  • 方法注释没有说明具体的作用、使用事项。
  • 参数、返回值,空有格式没内容。这是非常重要一点,任何人调用任何方法之前都需要知道方法对参数的要求,以及返回值是什么。

5、状态型取值应使用枚举规范

举例:

@Getter
public enum BasketStatusEnum {

    FREE(1, "空闲"),
    BOUND(2, "已绑定"),
    DELIVERING(3, "配送中(已上车)"),
    IN_SCHOOL(4, "在学校(已下车)"),
    BREAK(7, "损坏");

    private int code;
    private String name;

    BasketStatusEnum(int code, String name) {
        this.code = code;
        this.name = name;
    }
}
           

6、固定常量使用常量类

例如无加工的加工方式code固定为

WJG

,应该使用常量类收纳该常量

常量类应该私有化构造方法,且使用

final

修饰类,防止常量类被继承后,常量类中的常量影响子类中的字段

/**
 * @author zhangxing
 * @date 2020/04/20
 */
public final class ApplicationConstant {

    private ApplicationConstant() {
    }

    public static final String PROFILE_DEVELOPMENT = "dev";
    public static final String PROFILE_PRODUCTION = "prod";

    /**
     * 无加工加工方式
     */
    public static final String NO_MANUFACTURE_HANDLE_METHOD = "WJG";

}
           

7、所有mapper接口和service接口需要加上注释

8、ThreadLocal在set()后需要在正确的时间remove()

二、项目规范

1、目录结构

top.sclf.jyhpv.模块名              源码目录
|-- domain                            javabean(下方有domain的专门说明)
|-- |--- bo                               业务对象(Business Object)
|-- |--- dto                         			传输层对象(Data Transfer Object)
|-- |--- vo                         			视图对象(View Object)
|-- |--- model                         		数据对象(和数据库一一对应的对象)
|-- converter                         用于但不限于domain下的对象转换

|-- controller                        控制器
|-- service                           业务层
|-- |--- impl                         业务实现层
|-- manager                           manager层,下方会详细说明
|-- mapper                            mapper接口和xml

|-- enums                             枚举
|-- constant                          常量(使用class,且class上添加final,请勿使用interface和enum)
|-- annotations                       注解
|-- validator                         自定义校验类
|-- commons                           通用对象
|-- config                            配置类,如swagger配置等..
|-- listener                          监听器(包含接口与实现)
|-- factory                           工厂类
|-- server                            服务相关,如WebSocket服务器
|-- aspect                            切面
|-- database                          数据库/数据源相关
|-- exception                         异常相关
|-- util                              工具类
|-- task                              定时任务
|-- export                            数据导出业务
|-- interceptor                       拦截器

|-- Application.java                  启动类
           

2、domain中的javabean规范

1)javabean整体要求:

  • 不得有任何的业务逻辑或者计算
  • 基本数据类型必须使用包装类型

    (Integer, Double、Boolean 等)

  • 不允许有任何的默认值
  • 每个属性必须添加注释,并且必须使用多行注释。
  • 必须使用

    lombok

    简化

    getter/setter

    方法
  • 建议对象使用

    lombok

    @Builder ,@NoArgsConstructor

    ,同时使用这两个注解,简化对象构造方法以及set方法。

2)model包下javabean要求:

  • Xxxx 与数据库表名保持一致,驼峰规则
  • 类中字段要与数据库字段保持一致,不能缺失或者多余
  • 类中的每个字段添加注释,并与数据库注释保持一致
  • 不允许有组合
  • 项目内的日期类型必须统一,使用

    java.time.LocalDateTime

3)传输对象;

XxxxDTO

,要求:

  • 不可继承自

    Entity

  • VO

    可以继承、组合其他

    DTO,VO,BO

    等对象
  • VO

    只能用于返回前端、rpc 的业务数据封装对象

4)视图对象;

XxxxVO

,要求:

  • 不可继承自

    Entity

  • VO

    可以继承、组合其他

    DTO,VO,BO

    等对象
  • VO

    只能用于返回前端、rpc 的业务数据封装对象

5)业务对象;

BO

,要求:

  • 不可以继承自

    Entity

  • BO

    对象只能用于

    service,manager,dao

    层,不得用于

    controller

三、MVC 规范

1、整体分层

  • controller 层
  • service 层
  • manager 层
  • dao 层

2、

controller

层规范

1)RESTful的URL设计参考https://cizixs.com/2016/12/12/restful-api-design-guide/

2)每个方法必须添加

swagger

文档注解

@ApiOperation

,并填写接口描述信息,描述最后必须加上作者信息

@author zhangxing

正例:

@ApiOperation("更新部门信息 @author zhangxing")
    @PostMapping("/department/update")
    public ResponseDTO<String> updateDepartment(@Valid @RequestBody DeptUpdateDTO deptUpdateDTO) {
        return departmentService.updateDepartment(deptUpdateDTO);
    }
           

3)controller 负责协同和委派业务,充当路由的角色,每个方法要保持简洁:

  • 不做任何的业务逻辑操作
  • 不做任何的参数、业务校验,参数校验只允许使用@Valid 注解做简单的校验
  • 不做任何的数据组合、拼装、赋值等操作
  • 允许设置默认排序,并且需要判断排序规则为空时可以设置默认排序

正例:

@ApiOperation("分页查询订单明细变更记录 @author zhangxing")
    @GetMapping("/pages")
    public PageInfo<DemandHisOrderDetailChange> getOrderDetailChangePage(Pagination pagination, DemandHisOrderDetailChangeQueryDTO queryDTO) {
        if (StringUtils.isBlank(pagination.getSort())) {
            pagination.setSort("delivery_date, use_date");
            pagination.setOrder("ASC");
        }
        return demandHisOrderDetailChangeService.getPage(pagination, queryDTO);
    }
           

3、

service

层规范

1)BaseService的使用,BaseService中注入了相应的mapper,不强制要求使用

XxxService接口继承

BaseService<T, Q>

接口

T,Q分别为对应的数据库model和查询条件对象。举例:

/**
 * <p>
 * 系统日志表 服务类
 * </p>
 */
public interface SysLogService extends BaseService<SysLog, SysLogQueryDTO> {

}
           

XxxServiceImpl实现XxxService接口,继承

BaseServiceImpl<M, T, Q>

M,T,Q分别为Mapper层和model层和查询条件,需要实现

getList

方法。举例:

/**
 * <p>
 * 系统日志表 服务实现类
 * </p>
 */
  @Service
  public class SysLogServiceImpl extends BaseServiceImpl<SysLogMapper, SysLog, SysLogQueryDTO> implements SysLogService {
 
    @Override
    public List<SysLog> getList(SysLogQueryDTO queryDTO) {
        return mapper.selectList(queryDTO);
    }

}

           

2)合理拆分 service 文件,如果业务较大,请拆分为多个 service。

如订单业务,所有业务都写到 OrderService 中会导致文件过大,故需要进行拆分如下:

  • OrderQueryService

    订单查询业务
  • OrderCreateService

    订单新建业务
  • OrderDeliverService

    订单发货业务
  • OrderValidatorService

    订单验证业务

3)谨慎处理

@Transactional

事务注解的使用,不要简单对

service

的方法添加个

@Transactional

注解就觉得万事大吉了。应当合并对数据库的操作,尽量减少添加了

@Transactional

方法内的业务逻辑。

@Transactional

注解内的

rollbackFor

值必须使用异常的基类

Throwable.class

对于@Transactional 注解,当 spring 遇到该注解时,会自动从数据库连接池中获取 connection,并开启事务然后绑定到 ThreadLocal 上,如果业务并没有进入到最终的 操作数据库环节,那么就没有必要获取连接并开启事务,应该直接将 connection 返回给数据库连接池,供其他使用(比较难以讲解清楚,如果不懂的话就主动去问)。

反例:

@Transactional(rollbackFor = Throwable.class)
  public ResponseDTO<String> 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<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 进行数据库操作。

4)需要注意的是:注解

@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无法拦截到这次调用,也就无法通过注解保证事务了。简单来说,在同一个类中的方法调用,不会被方法拦截器拦截到,因此事务不会起作用。

解决方案:

  1. 可以将方法放入另一个类,如新增

    manager层

    ,通过spring注入,这样符合了在对象之间调用的条件。
  2. 启动类添加

    @EnableAspectJAutoProxy(exposeProxy = true)

    ,方法内使用

    AopContext.currentProxy()

    获得代理类,使用事务。
SpringBootApplication.java

@EnableAspectJAutoProxy(exposeProxy = true)
@SpringBootApplication
public class SpringBootApplication {}

OrderService.java

public void createOrder(OrderCreateDTO createDTO){
    OrderService orderService = (OrderService)AopContext.currentProxy();
    orderService.saveData(createDTO);
}
           

5)service是具体的业务处理逻辑服务层,尽量避免将web层某些参数传递到service中。

反例:

public ResponseDTO<String> 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);
    // 示例结束
}
           

反例中出现的问题:

  • 反例中把

    HttpServletRequest

    传递到service中,是为了获取Request流中的字符信息,然后才是真正的业务处理。按照分层的初衷:将代码、业务逻辑解耦,正确的做法应该是

    handlePinganRequest

    方法将

    String

    字符作为参数直接处理业务,将从

    Request

    中获取字符的操作放入

    controller

    中。
  • 另一个坏处是不方便做单元测试,还得一个

    new

    一个

    HttpServletRequest

    并制造一个

    InputStream

    ,然而这样做并不能模拟到真实的业务情景及数据。

4、

manager

层规范

manager 层的作用(引自《阿里 java 手册》):

  • 对第三方平台封装的层,预处理返回结果及转化异常信息;
  • 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;

    缓存示例

    XxxManager.java

    public class XxxManager {
        
        @Autowired
        private XxxMapper mapper;
        
        @AutoWired
        private XxxCacheService cacheService;
    
      	public Xxx getById(id) {
          Xxx x = cacheService.getById(id)
          if(x == null){
            x = mapper.getById(id);
            cacheService.setById(x);
          }
          return x;
        }
        
      }
               
  • 与 mapper 层交互,对多个 mapper 的组合复用。

5、

mapper

层规范

优先使用 tk的通用mapper框架。

1)所有 mapper接口 继承自

BaseMapper<T>

2)禁止使用 tk通用mapper的

Example

条件构建器

3)尽量不直接在 mybatis xml 中写死常量,应从 mapper 中传入到 xml 中

3)建议不要使用星号

*

代替所有字段

正例:

NoticeDao.java
    
    Integer noticeCount(@Param("sendStatus") Integer sendStatus);
---------------------------------------------
    NoticeMapper.xml
    
    <select id="noticeCount" resultType="integer">
        select
        count(1)
        from t_notice
        where
        send_status = #{sendStatus}
    </select>
           

反例:

NoticeDao.java
    
    Integer noticeCount();
---------------------------------------------
    NoticeMapper.xml
    
    <select id="noticeCount" resultType="integer">
        select
        count(1)
        from t_notice
        where
        send_status = 0
    </select>
           

3)mapper层方法命名规范

  • 获取单个对象的方法用

    find

    做前缀。
  • 获取多个对象的方法用

    list

    做前缀。
  • 获取统计值的方法用

    count

    做前缀。
  • 插入的方法用

    insert

    做前缀。
  • 删除的方法用

    delete

    做前缀。
  • 修改的方法用

    update

    做前缀。

建议:mapper层方法命名尽量以sql语义命名,避免与业务关联。

正例:

反例:

反例中出现的不规范操作:

  • find代表单个查询,批量查询的应该 list 开头。
  • 命名与业务关联,局限了mapper方法的使用场景和范围,降低了方法的复用性,造成他人困惑以及重复造轮子。

6、

boolean

类型的属性命名规范

类中布尔类型的变量,都不要加is,否则部分框架解析会引起序列化错误。反例:定义为基本数据类型 Boolean isDeleted;的属性,它的方法也是 isDeleted(),RPC在反向解析的时候,“以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。

这是阿里巴巴开发手册中的原文,我们团队的规定是:

boolean

类型的类属性和数据表字段都统一使用

flag

结尾。虽然使用

isDeleted,is_deleted

从字面语义上更直观,但是比起可能出现的潜在错误,这点牺牲还是值得的。

正例:

deletedFlag,deleted_flag,onlineFlag,online_flag
           

四、数据库 规范

1、建表规范

非中间关系表必备三字段:id, creator_id, creator_name, modifier_id, modifier_name, create_time, modify_time

  • id 字段 Long 类型,单表自增,自增长度为 1
  • create_time 字段 datetime 类型,默认值 CURRENT_TIMESTAMP
  • update_time 字段 datetime 类型,默认值 CURRENT_TIMESTAMP

2、枚举类表字段注释需要将所有枚举含义进行注

修改或增加字段的状态描述,必须要及时同步更新注释。

如下表的

sync_status

字段

同步状态 0 未开始 1同步中 2同步成功 3失败

正例:

CREATE TABLE `t_change_data` (
	`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
	`sync_status` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '同步状态 0 未开始 1同步中 2同步成功 3失败',
	`sync_time` DATETIME NULL DEFAULT NULL COMMENT '同步时间',
	`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
	`update_time` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
	PRIMARY KEY (`id`)
)
           

反例:

CREATE TABLE `t_change_data` (
	`id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
	`sync_status` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0' COMMENT '同步状态 ',
	`sync_time` DATETIME NULL DEFAULT NULL COMMENT '同步时间',
	`create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
	`update_time` DATETIME NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
	PRIMARY KEY (`id`)
)
           

3、数据库编码

字符集:

utf8mb4

排序规则:

utf8mb4_general_ci

4、合理结合业务给表字段添加索引和唯一索引具体索引规范请参照《阿里巴巴 Java 开发手册》索引规约

五、其他

1、代码提交规范

  • 如果改动影响代码非常多,必须新增一个分支提交
  • 提交前应该冷静、仔细检查一下,确保没有忘记加入版本控制或不应该提交的文件。
  • 提交前应该先编译一次(idea里ctrl+F9),防止出现编译都报错的情况。
  • 提交前先更新pull一次代码,提交前发生冲突要比提交后发生冲突容易解决的多。
  • 提交前检查代码是否格式化,是否符合代码规范,无用的包引入、变量是否清除等等。
  • 提交时检查注释是否准确简洁的表达出了本次提交的内容。
  • 提交代码时必须填写详细备注,如完成功能,注释为“新增XX功能”;
  • 若此次提交代码对应禅道中的任务或者bug,格式如下:
task#[任务id] [任务标题] [具体事项]
bug#[bug id] [bug标题] [具体事项]
           
  • 例子如下:
task#1101 开发smartreload功能 完成线程池的编码
bug#1102 smartreload时间不正确 线程池的大小问题
           

3、保持项目整洁

使用git,必须添加 .gitignore 忽略配置文件。

不要提交与项目无关的内容文件:idea配置、target包等。