简介
说明
本文介绍精选的Java编程规范(代码规范)。遵守这些规范,代码的bug数将会大幅减少,代码可维护性、可读性、扩展性会大幅上升。(本文持续更新)
为什么要有编程规范?
编程规范有如下作用:
- 提高代码可读性、维护性、扩展性
- 提高开发速度、减少bug
- 有助于留住人才(接手别人的垃圾代码占离职原因的很大一部分)
好的编程规范是怎样的?
- 规范要精简,否则不利于普及
规范的落实
- 编程规范要配合代码审核,否则都是空话。
- 代码审核必须在代码提测之前进行。(因为如果测试通过了,由于不符合编程规范再改动的话,需要费时间进行回归测试,导致影响上线)
- 代码评审要以平台的方式进行:被评审者将代码提到评审平台,然后评审者在此平台进行批注,被评审者进行代码修改。
- 这种代码评审基本是毫无卵用的:多个人聚在一个会议室里,盯着一个电脑投屏进行评审。
- 代码规范要加入绩效考核(占比在1%~5%之间),否则无法引起重视。
- 好的大公司就是这么做的,代码极好,扩展性极好,技术氛围极好,项目的质量极好、用户的反馈极好...
格式
- 单个方法不能超过60行,如果超过必须拆分
- IDEA大概能显示40行代码,60行对一个方法来说肯定是够用的。
- 反例:所有逻辑堆在一个方法里,单个方法成百上千行
- 单行代码不能超过100个字符(包括空格),如果超过必须换行。换行时遵循如下原则:
- 第二行相对第一行缩进 4 个空格;从第三行开始,不再继续缩进
- 运算符与下文一起换行;方法调用的点符号与下文一起换行
- if/for/while/switch/do 等保留字与括号之间必须加空格。
- 尽早拆分Service,避免一个Service代码行数太多。
命名
- 命名可以很长,不能为了缩短长度进行单词的简写(提高可读性)。(例外:接口实现类以Impl结尾等通用的缩写)
- 正例:interfaceOperationCode
- 反例:intOpCode
- 字段名第一个字母必须小写。
- 重大bug:用lombok,若前两个字母大写,用Jackson序列化时第二个大写会变成小写。
- 类名和包名使用单数形式,但是如果类名有复数含义,类名可以使用复数形式
- 正例:com.abc.user.detail
- 反例:com.abc.users.detail
- 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。如果必须要用两个单词,也都要小写
- 正例 : com.alibaba.open.util、com.abc.userdetail
- 抽象类命名使用Abstract开头;异常类命名用Exception结尾;接口类命名不要添加无用的前缀与后缀(比如:I开头、Interface结尾)
- POJO类中布尔类型变量不要加is前缀,否则部分框架解析会引起序列化错误。
- 反例:将是否删除定义为:Boolean isDeleted
- 正例:将是否删除定义为:Boolean deletedFlag
- Controller的url命名规范:
- 驼峰命名。
- 统一是这种:/表名/增删改查,比如:用户详情的增加接口:/userDetail/add。增删改查对应的英文如下:
- 增加:add
- 编辑:edit
- 删除:delete
- 分页查询:page
- 列表查询:list
常量
- 不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护(提高可维护性)。
- 正例:缓存相关的常量: CacheConstant;配置相关的常量: ConfigConstant。
- 常量命名全部大写,单词间用下划线隔开。
- 正例 : MAX_STOCK_COUNT
- 反例 : Max_Stock_Count
类型
- 禁止使用Map作为方法的入参或返回值,必须使用对象明确列出所有字段。(例外:有些地方只能是Map类型,比如:短信发送接口,参数是不确定的,调用方自定义key和value)
- 反例:给别人提供Feign接口时,返回值是Map类型。
- 正例:定义一个XxxVO,将字段写到XxxVO里,作为接口返回值。
- 正例:查很多用户详情时,为提高接口速度,将单个SQL转成批量SQL(将多个=转化成单个IN),然后将其结果转成key为用户id,value为用户数据的Map,再进行后续操作。
- 如果代码内部用到了Map,必须在map定义处写明key和value分别是什么。
- 这些地方必须使用包装数据类型:所有的 POJO 类属性、RPC 方法的返回值和参数
- 即:要用Integer、Long等,不要用int、long
- 实体类不要implements Serializable
- Serializable是序列化,现在都是使用JSON了,不要再实现Serializable了!(题外话:写代码时要思考,看究竟有什么用,不明白的就去搞明白,不要糊里糊涂地写)。
- 当然,有时候就必须要实现Serializable,比如:Dubbo的实体类,默认用的是Serializable的序列化来传递数据,所以必须实现Serializable。
参数与返回值
- 不能修改入参对象的字段,如果要修改,必须新创建一个对象
- 例外:如果方法就是为了给入参对象赋值的,可以去修改。
- 方法入参的个数必须小于5
- 如果大于等于5,必须将入参封装为一个类。
- 分页接口的请求和响应必须继承公共父类,如下:
分页的请求类
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class PageRequest {
@ApiModelProperty("当前页")
private Long currentPage = 0L;
@ApiModelProperty("每页个数")
private Long pageSize = 10L;
@ApiModelProperty("创建时间开始")
private LocalDateTime createTimeStart;
@ApiModelProperty("创建时间结束")
private LocalDateTime createTimeEnd;
}
分页的响应类
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.List;
@Data
public class PageResponse<T> {
@ApiModelProperty("当前页")
private Long currentPage = 0L;
@ApiModelProperty("每页个数")
private Long pageSize = 10L;
@ApiModelProperty("总个数")
private Long totalSize = 0L;
private List<T> dataList;
}
注释
- 所有接口说明都使用knife4j的,不要重复写注释。
- 方法注释中没用的说明要删掉,比如:参数后如果没加详细说明,就把这行参数注释删掉。
- 文件前不要让Idea自动生成创建时间、创建人。
- 原因:这是多余的操作,git已经记录了此文件的创建人和创建时间
复用
- 逻辑重复的代码要抽取为同一个(提高复用性)
- 正例:页面上的分页查询、Excel导出查询的数据,这两个共用查数据的方法。
单一职责
- 每个接口的参数和返回值必须是独立的。
- 比如:查询用户的入参和返回值是:UserPageQueryBO,UserPageQueryVO;保存用户的入参和返回值是:UserSaveBO,UserSaveVO。虽然UserPageQueryBO和UserSaveBO会有很多重复的字段,但也不能合并为一个BO。如果进行了合并,会有如下致命缺点:
- 看接口入参无法确定用到了哪些字段,必须看代码实现才能确定,极不清晰。
- 接口文档不清晰,当导出接口文档时,前端开发人员也不好分辨。
- 此情况例外:功能极其接近时允许用同一个参数和返回值。
- 比如查询用户需要提供两个接口:分页查询和全量查询,此时可以共用查询入参,将其定义为:UserQueryBO。
- 比如:查询用户的入参和返回值是:UserPageQueryBO,UserPageQueryVO;保存用户的入参和返回值是:UserSaveBO,UserSaveVO。虽然UserPageQueryBO和UserSaveBO会有很多重复的字段,但也不能合并为一个BO。如果进行了合并,会有如下致命缺点:
- 每个类的字段必须是有效的
- 比如:查询用户的入参实体类里,只包含必须的字段,比如:用户名、手机号,不要包含用不到的字段,比如:用户id,用户的密码。
数据库
注意事项
- 状态、类型等字段Java代码使用枚举类型,数据库必须使用字符串(提高可维护性)
- 创建时间、更新时间、是否删除等字段,必须放到列的最后。(提高可读性)
- 后边如果有新加的字段,要加在这些字段之前,永远保持这些字段在最后!
- 主键ID一律使用MyBatis-Plus自带的雪花算法来生成。(有利于数据库迁移等)
- 查数据的方式要最优,如下方式要优先考虑前边的方式。
- lambdaQuery、在代码中用字符串拼MyBatis-Plus条件(用字符串指定字段名)、Mapper中写注解、在XML中写。
- 查数据时尽量不要联表查。(因为数据量大时,联表查会很慢)。
建表的必要字段
说明
为了便于排查问题,建表时需要加一些必要的字段:创建人、更新人、创建时间、更新时间、删除标记。
SQL
ALTER TABLE `库名`.`表名`
ADD COLUMN `id` bigint NOT NULL COMMENT '主键',
ADD COLUMN `create_time` datetime NOT NULL COMMENT '创建时间',
ADD COLUMN `update_time` datetime NOT NULL COMMENT '修改时间',
ADD COLUMN `create_id` varchar(32) COMMENT '创建人ID',
ADD COLUMN `create_name` varchar(32) COMMENT '创建人ID',
ADD COLUMN `update_id` varchar(32) COMMENT '修改人名字',
ADD COLUMN `update_name` varchar(32) COMMENT '修改人名字',
ADD COLUMN `delete_flag` bigint NOT NULL DEFAULT 0 COMMENT '删除标记。0:未删除;其他:已删除';
详解
- 创建时间和更新时间的字段名
- 阿里开发手册的规范:
- 嵩山版、华山版等:表必备三字段:id,create_time,update_time。(推荐)
- 泰山版等:表必备三字段:id,gmt_create,gmt_modified。(不推荐)
- 注意:时间相关的字段名应该有“time”才好。而且有一些Mock工具(例如:ApiFox)会根据字段名自动构造假数据,字段中带“time”工具就会自动生成很接近实际的mock数据,有利于前端自测代码。
- 阿里开发手册的规范:
- 更新时间不要设置为自动更新
- 不要让更新时间自动更新,即:不要这样写:ADD COLUMN `update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
- 原因:
- 数据库服务器很可能没有正确设置时区,如果“更新时间”字段使用了数据库服务器的时区进行了自动更新,那这个时间就是不正确的。
- 以我工作的几家公司来看,数据库服务器的时区基本都是错误的。
- 正确的方法是:“创建时间”和“更新时间”全部通过应用来自动生成。比如MybatisPlus的自动填充:MyBatis-Plus--自动填充的用法_IT利刃出鞘的博客-CSDN博客
- 删除标记不要使用0,1来表示,在删除时应该将id的值赋值给delete_flag
- 如果用0,1表示,会影响唯一索引。见:MyBatis-Plus--解决逻辑删除与唯一索引的问题--方法/实例_IT利刃出鞘的博客-CSDN博客
- 数据库的时间字段对应的Java的DAO的字段类型要用LocalDateTime
- 现在21世纪了,不要再用Date了。
- LocalDateTime的优点:
- 可明确知晓:这是个日期+时间的字段。(Date不能一眼看出是日期还是时间还是两者)
- LocalDateTime的格式化等操作是线程安全的。
- LocalDateTime的方法很丰富。
上边的字段必须有公共的实体类(CommonEntity),使用Xxx extends CommonEntity的方式,不要在自己实体类手写这些字段。
CommonEntity如下:
package com.example.knife.common.entity;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 数据库公共实体类
*/
@Data
public class CommonEntity {
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
private Long id;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 修改时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 创建人ID
*/
private String createId;
/**
* 创建人名字
*/
private String createName;
/**
* 更新人ID
*/
private String updateId;
/**
* 更新人名字
*/
private String updateName;
/**
* 删除标记。0:未删除;其他:已删除
*/
@TableLogic("id")
private Long deleteFlag;
}
API选用
- 时间类型必须使用JDK8的类型,比如:LocalDateTime,LocalDate,LocalTime
- 不要使用Date类型(Date的API难用,Date含义不清晰(不知道是日期还是时间)多线程格式化时要考虑线程安全等)。这老一套的东西该摒弃了。
- 小数数字类型必须使用BigDecimal。
- double和float会失真。
- BigDecimal使用时也要小心,尽量学一下再用,它还是要注意一些问题的,比如:若不指定精度,除不尽的算术操作会抛异常。详见:Java之BigDecimal系列--使用/教程/实例_IT利刃出鞘的博客-CSDN博客
- 题外话:对于IT来说,不是说知道某个技术就行了,而要深入下去,无论是普通的API还是高级技术,无论是哪个技术都有需要注意的地方,都可能有坑。如果对技术浅尝辄止,那么就很难进步,很可能就是一年的经验用十年,难以成长为技术大佬。
- Lombok的使用
- 不要使用@Accessors,可以使用@Builder
- 为了方便给字段赋值,lombok提供了一些注解,但不要用@Accessors,因为它生成的set方法有返回值,会存在问题:有些中间件会判断方法是否有返回值进而进行操作,比如:EasyExcel。(如果使用了@Accessors,会导致导入Excel时取不到值)
- 题外话:lombok等中间件固然很方便,但任何API都要选用,不要全部采用。其中一个重要的原则就是:不要改变正常的逻辑,比如:set方法就不应该有返回值。
- 不要使用@Accessors,可以使用@Builder
- 要使用@Autowired,不要用@Resource。
- Spring的项目,就尽量用Spring的东西。
技术选型
必须使用主流且稳定的技术栈(见:Java后端开发技术选型_IT利刃出鞘的博客-CSDN博客)
不得使用如下技术(如下技术都有稳定、成熟的同类技术可以替代):
不稳定,bug多的技术
- fastjson
- hutool
资源消耗很大的技术
- FileBeat
全局处理
异常
- 必须将错误信息作为异常抛出来,让全局异常处理器去处理。
- 不能自己返回错误信息。(会增加代码的复杂度,如果代码有多个调用,层层传递错误信息会无法维护)
- 不能自己捕获了异常然后不处理信息。(会导致无法排查问题)
包装返回值给前端
- 自己不要去包装返回值给前端,由AOP统一去包装。(减少代码量,便于维护)
feign调用
- 自己不要去包装返回值给调用方,由AOP统一去包装。(减少代码量,便于维护)
- 调用方不要手动去解析包装数据,需要由统一的解码器去处理。
逻辑
- json字符串解析为对象
- 一个json字符串必须有一个完全对应的java对象。(代码可读性高)
- 必须一步到位将json字符串解析为整个java对象,不能解析完外层再解析内层。
- get方法不能有其他逻辑,必须直接返回字段本身。
- 如果必须要手动捕获异常,必须要输出详细堆栈 将堆栈打出来
- 如果有日志组件,必须使用日志组件。
- 如果没有日志组件,用log.error("xxx", e);
- 所有地方必须判断null
- 如果是null,必须抛出异常,信息为:xxx不能为null。
- 禁止将业务对象作为Map的key,禁止覆写equals和hashCode。
- 因为别人也会用这个业务对象,改了这些会导致别人的业务出问题!
其他
- 代码中不得存在被IDEA警告的代码,若存在则必须看IDEA的提示并进行修改。(包括:错的单词拼写,泛型警告,优化的提示等)