文章目录
前言
本文的java代码开发的通用规范,适用于新手上路和大部分职场
一、命名风格
- 采用英文全称进行命名,保持各层级命名基本统一。
- 命名英文单词用全称,尽量不要使用简称。
- 模块包命名全部使用小写,命名方式:com.项目名称.项目模块.系统分层(如:com.cdcw.modules.controller)。
- 类名使用 UpperCamelCase 风格,遵从大驼峰形式,多个英文单词以大写字母间隔,尽量避免使用缩写,不要使用’_’’-’等特殊符号,以下情形例外:DO / BO / DTO / VO / AO
正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion
反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion - 属性定义位置在类定义开始,按照 public,protected,package,private 顺序定义。
- 方法命名采用“动作+属性” 的方法。并且,动作以小写字母开始,属性以大写字母开始。常用的动作有:is、get、set、save、add、del 等。示例:getName()。
- 方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式。
正例: localValue / getHttpMessage() / inputUserId - 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚。
正例:MAX_STOCK_COUNT 反例:MAX_COUNT - 抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾。
- Model 类中布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。
反例:定义为基本数据类型 Boolean isDeleted;的属性,它的方法也是
isDeleted(),RPC 框架在反向解析的时候,“以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。 - 不要使用 SqlServer 数据库保留字段命名属性。
- 对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别。
正例:CacheManagerImpl 实现 CacheManager 接口。 - 为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。
正例:从远程仓库拉取代码的类命名为 PullCodeFromRemoteRepository
反例:变量 int a;的随意命名方式。 - 接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是 与接口方法相关,并且是整个应用的基础常量。
正例:接口方法签名:void f(); 接口基础常量表示:String COMPANY =“alibaba”;
反例:接口方法定义:public abstract void f();
说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默 认实现。 - 枚举类名建议带上 Enum 后缀,枚举成员名称需要全大写,单词用下划线隔开。
说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。
正例:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKOWN_REASON。 - 各层命名规约:
A) Service/DAO 层方法命名规约
1) 获取单个对象的方法用 get 做前缀。
2) 获取多个对象的方法用 list 做前缀。
3) 获取统计值的方法用 count 做前缀。
4) 插入的方法用 save/insert 做前缀。
5) 删除的方法用 remove/delete 做前缀。
6) 修改的方法用 update 做前缀。
二、变量定义
- 不要使用一个常量类维护所有常量,按常量功能进行归类,分开维护。
说明:大而全的常量类,非得使用查找功能才能定位到修改的常量,不利于理解和维护。
正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类ConfigConsts 下。
三、代码格式
- 废弃的/无用的代码一律直接删除,禁止以注释等方式保留。如需查看历史代码,通过 SVN/Git 的 history 找回(无用的代码会干扰团队成员的阅读/或被误调,越积越多会导致代码维护成本增高)。
补充:master 按照规范,开发分支允许以注释方式保留。 - 接口类中的方法不需添加 public 修饰符。
- 需要序列化的 Bean 类统一实现 Serializable 接口并用 IDE 生成
serialVersionUID。
Eg:
public class MyEntity implements Serializable {
private static final long serialVersionUID = 123456L;
...
}
- 常用字符串统一定义在常量类里,如: “utf-8”, “yyyyMMdd”。
- 避免数字类型比较的坑: 统一采用 equals 进行比较其值,不用==进行比较,避免踩坑。
- 方法单一职责: 单个方法代码行数控制在 100 行以内,超长的需要拆分(拆分成多个方法或类)。
- 遵循: Don’t Repeat Yourself,即 DRY 原则。避免进行简单的复制粘贴修改,当出现重复代码时思考是否封装。
- 可异步执行的耗时操作采用异步处理:使用 Spring @Async 或 MQ,或夜间 Timer 定时。
- 常用数据考虑缓存,存入 Redis,设置缓存过期时间。
- 保证写一致性的逻辑,在外层方法上添加事务。
- 大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果是非空代码块则:
1) 左大括号前不换行。
2) 左大括号后换行。
3) 右大括号前换行。
4) 右大括号后还有 else 等代码则不换行 表示终止的右大括号后必须换行。 - 左小括号和字符之间不能出现空格;同样,右小括号和字符之间也不能出现空格。
反例:if (空格 a == b 空格) - if/for/while/switch/do 等保留字与括号之间都必须加空格。
- 任何二目、三目运算符的左右两边都需要加一个空格。
说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等。 - 注释的双斜线与注释内容之间有且仅有一个空格。
正例:// 注释内容,注意在//和注释内容之间有一个空格。 - 方法参数在定义和传入时,多个参数逗号后边必须加空格。
正例:下例中实参的"a",后边必须要有一个空格。method(“a”, “b”, “c”)。 - IDE 的 text file encoding 设置为 UTF-8; IDE 中文件的换行符
使用 Unix 格式,不要使用 Windows 格式。 - 方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。
说明:没有必要插入多个空行进行隔开。 - Bean 属性拷贝推荐用 Spring BeanCopier 或者 Mapstruct,避免
Apache BeanUtils 或调用 setter。
四、OOP 规则
- 所有的覆写方法,必须加@Override 注解。
- 不能使用过时的类或方法。
- Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
正例:“test”.equals(object);
反例:object.equals(“test”); - 所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。
说明:对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑, 推荐使用 equals 方法进行判断。 - RPC 方法的返回值和参数必须使用包装数据类型。
- 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init方法中。
- 当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读。
- 循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
说明:反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。
反例:
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
- 慎用 Object 的 clone 方法来拷贝对象。
说明:对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。
五、 集合处理
- 使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size()。
说明:使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组地址;如果数组元素大于实际所需,下标为[ list.size() ]的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。
正例:
List<String> list = new ArrayList<String>(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);
反例:
直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误。
2. 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请
使用 Iterato 方式,如果并发操作,需要对 Iterator 对象加锁。
正例:
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
List<String> list = new ArrayList<String>();
list.add("1");
list.add("2");
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
- 使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
六、控制语句
- if/else/for/while 语句后必须使用大括号,即使只有一行代码。(需求总是变化的,一行是暂时的)。
- 嵌套层次过多的代码块利用反向思维缩减层次。
- 禁止在循环中执行耗时的操作,如在循环中执行 SQL 语句/调用外部服务等。
- 在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止。
- 除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
说明:很多 if 语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能明确什么样的条件执行什么样的语句,那么,如果阅读者分析逻辑表达式错误呢?
正例:
// 伪代码如下
final boolean existed = (file.open(fileName, "w") != null) && (...) ||
(...);
if (existed) {
...
}
反例:
if ((file.open(fileName, "w") != null) && (...) || (...)) {
...
}
- 循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。
- 下列情形,需要进行参数校验:
1) 调用频次低的方法。
2) 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。
3) 需要极高稳定性和可用性的方法。
4) 对外提供的开放接口,不管是 RPC/API/HTTP 接口。
七、注释规约
- Java 文件统一添加固定 Header,通过 IDE 统一配置(code
templates),主要标注该类的作用,对类进行简单描述;记录类的作者和类的创建日期。
Eg:
/**
* <Description>
* @author Wings_of_L
* @version 1.0
* @date ${YEAR}/${MONTH}/${DAY}
*/
- 接口和方法统一添加 Java Doc 标准注释,描述该方法的主要意义,对方法的参数进行说明。
Eg:
/**
*缓存 key-value 并设定过期时间
* @param key 缓存对象的 key
* @param valueList 缓存对象
* @return 缓存是否成功
*/
<T> boolean addList(String key, List<T> valueList);
- 属性注释必须说明该属性意义。
Eg:
/**
* 用户姓名
*/
private String name;
- Dao 层、Service 层接口中的方法都必须按照方法的注释要求进行注释。
Eg:
/**
* 获取成员列表
* @author Wings_of_L
* @param page 分页
* * @param orgId ID
* @return 成员列表
*/
List<UserImpl> getList(Page page, Long orgId);
/**
* 列表查询
* @author Wings_of_L
* @param page 分页
* @param content 关键字
* @return
*/
List<UserDetail> getList(Page page, String content);
- 需暂留的弃用类/方法添加 @Deprecated 废弃标记 和 @see 链接指向新接口
Eg:
/* @see com.wjw.common.cache.redis.JedisSentinelPoolUtil*/
@Deprecated
public class JedisUtils {…}
- 类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容 */格式,不得使用// xxx 方式。
- 所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。说明:对子类的实现要求,或者调用注意事项,请一并说明。
- 方法内部单行注释,在被注释语句上方另起一行,使用// 注释。方法内部多行注释 使用/* */注释,注意与代码对齐。
- 所有的枚举类型字段必须要有注释,说明每个数据项的用途。
- 代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。
说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。 - 谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除。
- 对于注释的要求:第一、能够准确反应设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看的,使其能够快速接替自己的工作。
- 好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。
- 特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描, 经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
1) 待办事宜(TODO):( 标记人,标记时间,[预计处理时间]) 表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个Javadoc 标签)。
2) 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。
八、异常处理
- 下层有常异,必须抛向上一层,捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容,controller 禁止向外部抛出系统底层异常及错误码。
- 为了使系统能够更好的跟踪运行情况,必须把底层异常放入新异常中。
- 如果一个层要抛出多个异常,那么所有自定义异常必须统一继承一个父类异常。这样上层可以通过父类异常捕获。
- 异常统一在控制器层处理,控制器层以下的层次在处理异常时,只需要把低层的异常类放到本层约定的异常类中,并抛出,如有需要可以加适当的异常消息。
- 调用外部服务等可能异常的代码块,用 try/catch 代码块捕获并在catch 中记录异常跟踪日志及业务逻辑处理。
- 禁止吞掉异常信息
1)禁止 catch 里不做任何记录和处理,吞掉异常及其堆栈信息;
2)禁止: logger.error(“XXX 操作异常”) 或 logger.error(“XXX 操作异
常”+e) 或 e.printStackTrace(); 3)正确: logger.error(“XXX 操作异常”, e) - 对于非预期的条件,尽量增加 else 记录跟踪日志。
- 禁止通过 System.*.out()打印日志(单元测试例外)。
- 日志记录 logger 需使用 Slf4J/log4j 代理声明,禁止绑死具体日志系统的 API,避免后期更换日志组件导致代码的大量改动。如采用了 lombok,可用 @Slf4j 注解替代以上声明。
Eg:
private static final Logger log =
LoggerFactory.getLogger(OrganizationServiceImpl.class);
- 对 trace/debug/info 级别的日志输出,必须使用占位符形式,避免直接 String 拼接异常信息(即使日志级别不匹配也会执行拼接操作空耗资源)。
Eg:
log.debug("当前用户 id: {} ,操作对象: {}=>{} ", userId, objectType,
objectId);
或条件输出形式如:
if(log.isDebugEnabled()){
log.debug("当前用户 id: “+id+” ,操作对象: “+ objectType +”=>
“+ objectId);
}
- 避免 NPE(NullPointException)
1)equals 比较将非空对象前置如"true".equals(request.getParameter(“isXx”)),即使后者为空也不会导致NPE。
2)数据库字段可空的映射属性使用包装类型定义:如基本数据类型的 int 映射到数据库的 null 值将产生 NPE,而用吧包装类型Integer 则不会。
3)可能为空的变量进行必要判空,并在非预期条件下打印必要的跟踪日志,不但避免 NPE,还非常便于跟踪调试。
4)级联调用 obj.getA().getB().getC() 易产生 NPE,先进行判空或使用 JDK8的 Optional 类包装。
5)调用 Dubbo 接口拿到返回值时,进行判空。
6)封装统一的判空类用于常用类型的判空,代码需要判空时统一调用即可。
如 XX.isEmpty(), XX.isNotEmpty()。
12.如果一个方法业务逻辑比较复杂,在一个方法中进行处理,该方法将会臃肿、不宜阅读和维护,建议每个方法代码行不要大于 50 行,建议多使用方法重构。建议对方法中需要进行说明的地方进行注释,如参数、方法中多个步骤的业务逻辑处理。(备注,代码行数指的是代码语句行数,不是编辑器行数)。
九、编码检查
- 后端服务及其他需要自测的代码,编写对应的单元测试类,统一采用 Junit,禁止直接在原 Java 类中写 main()方法自测。
Eg:
Junit 单元测试类示例:
public class TestApollo {
@Test // 标记为单元测试方法
public void testApolloConfig(){
String appId = Foundation.app().getAppId();
// 预期结果断言
Assert.assertNotNull(appId);
}
}
十、编码安全
- 用户敏感数据禁止列表批量展示/导出,必须进行脱敏,如客户手机号,身份证号。
- 禁止直接写死登录账号密码,给系统造成巨大的安全风险;
- 禁止代码/配置文件中出现生产环境的明文密码(改为加密存放)。
- 禁止使用侵犯他人版权的代码,字体,图片。
- 对用户输入数据必须做有效性检查。
十一、其他约定
- 在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
说明:不要在方法体内定义:
Pattern pattern = Pattern.compile(规则); - 注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够 取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后 取整,直接使用 Random 对象的 nextInt 或 者 nextLong 方法。
- 获取当前毫秒数 System.currentTimeMillis(); 而不是 new Date().getTime();
- 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。
十二、SqlServer 数据库
建表规约
- 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只 出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
说明:SqlServer 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。
正例:feitian_admin,rdc_config,level3_name
反例:Feitian_Admin,rdcConfig,level_3_name - 禁用保留字,如 desc、range、match、delayed 等,请参考 SqlServer官方保留字。
- 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 dx_字段名。
说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。 - 小数类型为 decimal,禁止使用 float 和 double。
- 如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
- varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text。
- 表必备三个字段:id,create_time, update_time, delete_flag
- 对于 Boolean 型的字段,采用 decimal 类型
- 表和字段都需要添加注释信息。
- 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。 - 合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。
索引规约
- 业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。 - 超过三个表禁止 join。需要 join 的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。
说明:即使双表 join 也要注意表索引、SQL 性能。 - 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。 - 创建索引时避免有如下极端误解:
1)宁滥勿缺。认为一个查询就需要建一个索引。
2)宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。
3)抵制惟一索引。认为业务的唯一性一律需要在应用层通过“先查后插”方式解决。
SQL 语句
- 不要使用 count(列名)或 count(常量)来替代 count(),count()是
SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为NULL 值的行。 - count(distinct col)计算该列除 NULL 之外的不重复行数,注意
count(di col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。 - 当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL。
- 不得使用外键与级联,一切外键概念必须在应用层解决。
- 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
- in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。
- 如果有全球化需要,所有的字符存储与表示,均以 utf-8 编码,注意字符的区别。
说明: SELECT LENGTH(" 轻 松 工 作 ") ; 返 回 为 12 SELECT CHARACTER_LENGTH(“轻松工作”);返回为 4 如果需要存储表情,那么选择 utfmb4 来进行存储,注意它与-8 编码的区别。 - 更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。
- @Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。