JAVA规范

-
2025-06-24

1 概述

1.1 编写目的

本规范旨在建立统一的编程和项目管理标准,提升软件项目的可维护性、可扩展性、团队协作效率以及代码质量。具体而言,其主要编写目的包括但不限于以下几个方面:

确保代码一致性:通过制定统一的编程风格和命名规则,保证不同开发者编写的代码在风格上保持一致,便于团队成员之间相互理解和维护。

提高代码质量:规范中包含的编码最佳实践、性能优化建议和异常处理指导等,有助于减少错误和漏洞,提升软件的稳定性和可靠性。

促进高效协作:明确的项目结构、版本控制流程和代码审查标准,能够简化团队协作流程,加速代码集成和问题解决,提升整体开发效率。

简化维护工作:良好的文档注释规范和模块化设计原则使得后续的代码维护和升级工作更加容易进行,降低长期维护成本。

增强可读性和可理解性:遵循规范编写的代码逻辑清晰、易于阅读,新加入的团队成员能更快地熟悉项目,降低学习曲线。

支持持续集成与自动化:规范化的代码结构和测试实践有利于实现持续集成和自动化测试,加快软件交付速度并确保软件质量。

保障安全合规:安全性规范的制定和执行能有效预防常见的安全威胁,如注入攻击、跨站脚本等,保护用户数据和系统安全。

提升技术品牌形象:遵循高标准的开发规范体现了团队的专业性和对质量的承诺,对于提升客户信任度和公司技术品牌价值具有积极作用。

综上所述,Java开发规范是构建高质量软件产品的基石,它不仅关注技术细节,还着眼于团队合作、项目管理和长期发展,是软件开发过程中的重要指导文件。

1.2 适用范围

本规范适用于Java开发的项目。

1.3 研发要求

优先使用集团统一研发基础支撑平台及共性技术组件进行开发,使用平台代码生成工具构建基础代码。

 

1.4 内容说明

本规范以Java开发者为中心视角,划分为源文件基础、格式规范、命名约定、注释规范、编程实践和单元测试六个维度,再根据内容特征,细分成若干二级子目录。另外,依据实用程度和优先级,依次分为【高】、【中】、【低】三大类。在延伸信息中,“说明”对规约做了适当扩展和解释;“正例”提倡什么样的编码和实现方式;“反例”说明需要提防的雷区,以及真实的错误案例。

1.5 引用规范

  • 阿里Java开发手册(嵩山版)
  • Google Java编程风格指南
  • GIT开源Effective Java(第三版)

2 源文件基础

2.1 文件名

【高】源文件以其最顶层的类名来命名,大小写敏感,首字母大写,文件扩展名为.java。

2.2 文件编码

【高】源文件编码格式为UTF-8。

2.3 文件结构

【高】一个源文件包含(按顺序地):

①许可证或版权信息(如有需要);

②package声明语句;

③import语句;

④一个顶级类(只有一个);

以上每个部分之间用一个空行隔开。

【高】如果一个文件包含许可证或版权信息,那么它应当被放在文件最前面。

【高】package声明语句不换行,不受长度限制。

【中】import尽量避免使用通配符,import语句不换行,不受长度限制。

【中】当一个类有多个构造函数,或是多个同名方法,这些函数/方法应该按顺序出现在一起,中间不要放进其它函数/方法。

【低】每个类应该以某种逻辑去排序它的成员,类内方法推荐定义的顺序依次是:公有方法或保护方法私有方法getter/setter方法。

3 格式规范

3.1 大括号

【高】大括号与if,else,for,do,while,try,catch等语句一起使用,即使只有一条语句(或是空),也应该把大括号写上;如果是大括号内为空,则简洁地写成{}即可,大括号中间无需换行和空格;如果是非空代码块则:

①左大括号前不换行,后换行。

②右大括号前换行,右大括号后还有else等代码则不换行;表示终止的右大括号后必须换行。

3.2 缩进

【高】每当开始一个新的块,缩进增加4个空格,当块结束时,缩进返回先前的缩进级别,缩进级别适用于代码和注释。

3.3 空格

【高】采用4个空格缩进,禁止使用Tab字符。

说明:如果使用Tab缩进,必须设置1个Tab为4个空格。IDEA设置Tab为4个空格时,请勿勾选Use tab character。

①空格分隔任何保留字与紧随其后的左括号(()(如if, for catch等)。

②空格分隔任何保留字与其前面的右大括号(})(如else, catch)。

③左小括号和右边相邻字符之间不出现空格;右小括号和左边相邻字符之间也不出现空格;而左大括号前需要加空格。

④任何二目、三目运算符的左右两边都需要加一个空格,包括赋值运算符=、逻辑运算符&&、加减乘除符号等。

⑤注释的双斜线与注释内容之间有且仅有一个空格。

⑥在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。

正例:

Double first = 3.2d;

Int second = (int)first + 2;
⑦方法参数在定义和传入时,多个参数逗号后面必须加空格。

正例:

// 下例中实参的args1,args2,后边必须要有一个空格。
method(args1, args2, arg3);

3.4 空行

【中】不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。

说明:任何情形,没有必要插入多个空行进行隔开,或者在代码中出现多个无意义的空行。

3.5 行宽和换行

【高】单行字符数限制不超过120个,超出需要换行,换行时遵循如下原则:

①第二行相对第一行缩进4个空格,从第三行开始,不再继续缩进,参考示例。

②运算符(冒号、逻辑运算符等)与下文一起换行。

③方法调用的点符号与下文一起换行。

④方法调用中的多个参数需要换行时,在逗号后进行。

⑤在括号前不要换行,见反例。

正例:

StringBuilder sb = new StringBuilder();
// 超过120个字符的情况下,换行缩进4个空格,并且方法前的点号一起换行
sb.append("yang").append("hao")
   .append("chen")
   .append("chen")
   .append("chen");

反例:

StringBuilder sb = new StringBuilder();
// 超过120个字符的情况下,不要在括号前换行
sb.append("you").append("are").append
("lucky");
// 参数很多的方法调用可能超过120个字符,逗号后才是换行处
method(args1, args2, args3
, argsX);
【中】单个方法的总行数不超过80行。

说明:除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过80行。

4 命名约定

4.1 通用

【高】代码中的命名均不能以下划线或美元符号开始或结束。

反例:name、__name、$name、name、name$、name__

【高】所有编程相关的命名禁止使用中文的方式。

【高】所有编程相关的命名禁止使用拼音的方式。

说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。

正例:

sgit、sgcc、taobao、cainiao、aliyun、youku、Beijing等国际通用的名称,可视同英文

反例:

DaZhePromotion [打折]、getPingfenByName() [评分] 、String fw[福娃]【中】杜绝完全不规范的缩写。

正例:

longitude 经度 缩写成 lon

latitude  纬度 缩写成 lat

反例:

AbstractClass 缩写成 AbsClass;

Condition 缩写成 condi;

Function 缩写成 Fu,此类随意缩写严重降低了代码的可阅读性。
 

【中】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达。在使用单词时,要结合实际业务和单词的实际含义确定。

正例:

对某个对象引用的volatile字段进行原子更新的类名为AtomicReferenceFieldUpdater

公司简称单位,字段定义使用company

性别,字段定义为gender
反例:

常见的方法内变量为 int a;的定义方式。

公司简称单位,字段定义为unit,此单词属于计量单位,与实际业务中的单位不符合

性别,字段定义为sex,不符合要求,sex强调生物性而非社会属性

4.2 包命名

【高】包(package)的命名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。

正例:

应用工具类包名为com.sgcc.ei.kunlun.aap.util。

类名为MessageUtils(此规则参考Spring的框架结构)。

4.3 类和接口命名

【高】类(Class)和接口(Interface)的命名使用UpperCamelCase风格,但以下情形例外:DO/BO/DTO/VO/AO/PO等。

正例:

```
ForceCode、UserDO、HtmlDTO、XmlService、TcpUdpDeal、TaPromotion
```

反例:

```
Forcecode、UserDo、HTMLDto、XMLService、TCPUDPDeal、APromotion 
```

【高】抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾;测试类命名以它要测试的类的名称开始,以Test结尾。

【中】如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。

说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。

正例:

Public class OrderFactory;
Public class LoginProxy;
Public class ResourceObserver;

4.4 方法、变量、常量命名

【高】方法名、参数名、成员变量、局部变量都统一使首字母小写的lowerCamelCase风格。

正例:

localValue、getHttpMessage()、inputUserId

反例:

localvalue、gethttpMessage()、inputUserid

【高】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚。

正例:

MAX_STOCK_COUNT、CACHE_EXPIRED_TIME

反例:

MAX_COUNT、EXPIRED_TIME

【中】避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。

说明:子类、父类成员变量名相同,即使是public类型的变量也能够通过编译,另外局部变量在同一方法内的不同代码块中同名也是合法的,然而出于代码可理解性的考虑,这些情况都要避免。对于非setter/getter的参数名称也要避免与成员变量名称相同。

5 注释规范

5.1 类注释

【高】类、类属性、类方法的注释必须使用Javadoc规范,使用/*内容/格式,不得使用// xxx方式。所有的类都必须添加创建者和创建日期。

正例:

/**
- 这是一个示例类,展示了Java中的标准注释。
- 
- @author 张三
- @data 2024.10.31
*/

5.2 方法注释

【高】所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。

正例:

/**
- 用来计算两个整数之和的方法。
- 
- @param a 第一个加数
- @param b 第二个加数
- @return 两个加数的和
*/

说明:对子类的实现要求,或者调用注意事项,请一并说明。

5.3 块注释

【高】方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/** */注释,注意与代码对齐。

5.4 变量注释

【高】所有变量都必须添加注释,说明每个变量的用途,注释放在变量的后面,通过空格,使每行的//保持在同一列,如果变量占用较长,在变量上另起一行添加注释。

正例:

public class UserController { 
   private String userId; // 用户ID 
   // 用户线程变量 
   private static final ThreadLocal<User> LOCAL_USER = new ThreadLocal<>();
} 

【高】所有的枚举类型字段必须要有注释,说明每个数据项的用途,注释放在数据项的后面,通过空格,使每行的//保持在同一列,如果数据项占用较长,在数据项上另起一行添加注释。数据库动态维护场景变量注释应描述为码表的类别编码。枚举项必须所有是英文字母大写,由多个单词组成的,使用下划线连接。

正例:

public enum ApiServiceExecuteResultEnum { 
   SUCCESS;   // 成功 
   ERROR;    // 错误 
   OTHER_ERROR; // 其他错误 
}

5.5 其它

【高】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。

说明:代码与注释更新需要始终保持同步。

【高】谨慎注释代码,在上方详细说明,而不是简单地注释掉。如果无用,则删除。

说明:代码被注释掉有两种可能性:

①后续会恢复此段代码逻辑。

②永久不用。

前者如果没有备注信息,难以知晓注释动机;后者建议直接删掉即可,假如需要查阅历史代码,登录代码仓库即可。

【中】与其用不明确的英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。

反例: // “TCP连接超时” 解释成 “传输控制协议连接超时”,反而增加理解难度。

【中】在类中删除未使用的任何字段、方法、内部类,在方法中删除未使用的任何参数声明与内部变量。

【中】合理的命名、代码结构是自解释的,注释力求精简准确、表达到位,将技术和业务结合,从业务和技术的角度对逻辑进行解释。注释以解释为主,避免无意义的注释。

6 编程实践

6.1 OOP规范

【高】避免通过一个类的对象引用访问此类的静态变量或静态方法,直接使用类名来访问即可。

【高】所有的覆写方法,必须加@Override注解。

【高】外部依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated注解,并清晰地说明采用的新接口或者新服务是什么。

【高】不能使用过时的类或方法。

说明:java.net.URLDecoder中的方法decode(String encodeStr)已经过时,应该使用双参数decode(String source, String encode)。

【高】Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals。

正例:

"test".equals(object);

反例:

object.equals("test");

说明:也可以使用 JDK7 引入的工具类 java.util.Objects#equals(Object a, Object b)或者其他第三方提供的equals方法。

【高】所有整型包装类对象之间值的比较,全部使用equals方法比较。

说明:对于 Integer var = ? 在-128至127之间的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,导致判断不一致,推荐使用equals方法进行判断。

【高】浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来判断。

说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数。

正例:


// 1. 指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。
float a = 1.0F - 0.9F;
float b = 0.9F - 0.8F;
float diff = 1e-6F;
if (Math.abs(a - b) < diff) {
  System.out.println("true");
}
// 2.使用BigDecimal来定义值,再进行浮点数的运算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.compareTo(y) == 0) {
  System.out.println("true");
}

反例:


Float a = 1.0F - 0.9F;
Float b = 0.9F - 0.8F
If (a == b) {
// 预期进入此代码块,执行其它业务逻辑
// 但事实上a==b的结果为false
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) {
// 预期进入此代码块,执行其它业务逻辑
// 但事实上equals的结果为false
}

【高】BigDecimal的等值比较应使用compareTo()方法,而不是equals()方法。

说明:equals()方法会比较值和精度(1.0与1.00返回结果为false),而compareTo()则会忽略精度。

【高】定义数据对象DO类时,属性类型要与数据库字段类型相匹配。

正例:

数据库字段的bigint必须与类属性的Long类型相对应。

反例:

某个案例的数据库表id字段定义类型bigint unsigned,实际类对象属性为Integer,随着id越来越大,超过Integer的表示范围而溢出成为负数。

【高】禁止使用构造方法BigDecimal(double)的方式把double值转化为BigDecimal对象。

说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:BigDecimal g = new BigDecimal(0.1F);实际的存储值为:0.10000000149。

正例:

// 优先推荐入参为String的构造方法,或使用BigDecimal的valueOf方法,此方法内部其实执行了Double的toString,而Double的toString按double的实际能表达的精度对尾数进行了截断。
BigDecimal recommend1 = new BigDecimal("0.1");
BigDecimal recommend2 = BigDecimal.valueOf(0.1);

【高】定义DO/DTO/VO等POJO类时,不要设定任何属性默认值。

反例:

POJO类的createTime默认值为new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。

【高】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在init方法中。

【高】setter方法中,参数名称与类成员变量名称一致,this.成员名 = 参数名。在getter/setter方法中,不要增加业务逻辑,增加排查问题的难度。

反例:

public Integer getData () {
   if (condition) {
       return this.data + 100;
   } else {
       return this.data - 100;
   }
} 

【高】循环体内存在超过3个字符串拼接,非多线程情况下,必须使用StringBuilder的append方法进行扩展,多线程情况下,必须使用StringBuffer的append方法进行推展,避免内存资源浪费,保证线程安全。

【高】类成员与方法访问控制从严:

①如果不允许外部直接通过new来创建对象,那么构造方法必须是private。

②工具类不允许有public或default构造方法。

③类非static成员变量并且与子类共享,必须是protected。

④类非static成员变量并且仅在本类使用,必须是private。

⑤类static成员变量如果仅在本类使用,必须是private。

⑥若是static成员变量,考虑是否为final。

⑦类成员方法只供类内部调用,必须是private。

⑧类成员方法只对继承类公开,那么限制为protected。

说明:任何类、方法、参数、变量,严控访问范围,过于宽泛的访问范围,不利于模块解耦。思考:如果是一个private的方法,想删除就删除,可是一个public的service成员方法或成员变量,删除则可能为外部使用者带来找不到方法或成员变量的风险。

【高】在使用正则表达式时,必须利用预编译功能,加快正则匹配速度。

说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);

【高】禁止使用复杂的SQL代替复杂的业务处理逻辑,采用化繁为简的方式,对SQL进行拆分,将业务处理逻辑交由代码层面实现。

【高】在开发过程中,有常用到的字符或者单词,需要以常量的形式统一定义在常量类中进行管理,不要硬编码到业务代码中,便于维护和保持代码的整洁性。

正例:

public class Constant {   
   public final static String COMMA = “,”;   
}
// 使用逗号   
String ids = “id1,id2,id3”;   
String[] arr = ids.split(Constant.COMMA);  

反例:

// 使用逗号 
String ids = “id1,id2,id3”; 
String[] arr = ids.split(“,”); 

【中】依赖数据库设计规范,任何货币金额,均以最小货币单位且整型类型来进行存储。

【中】相同参数类型,相同业务含义,才可以使用Java的可变参数,避免使用Object。

说明:可变参数必须放置在参数列表的最后 (建议开发者尽量不用可变参数编程) 。

正例:

public List<User> listUsers(String type, Long... ids) {...}  

【中】包装数据类型的使用:

①所有的POJO类属性必须使用包装数据类型。

②RPC方法的返回值和参数必须使用包装数据类型。

【中】POJO类必须写toString方法。使用IDE中的工具:source generate toString时,如果继承了另一个POJO类,注意在前面加一下super.toString。

说明:在方法执行抛出异常时,可以直接调用POJO的toString()方法打印其属性值,便于排查问题。

【中】final可以声明类、成员变量、方法、以及本地变量,下列情况使用final关键字:

①不允许被继承的类,如String类。

②不允许修改引用的域对象,如POJO类的域变量。

③不允许被覆写的方法,如POJO类的setter方法。

④不允许运行过程中重新赋值的局部变量。

⑤避免上下文重复使用一个变量,使用final关键字可以强制重新定义一个变量,方便更好地进行重构。

6.2 控制语句

【高】在一个switch块内,每个case要么通过continue/break/return等来终止,要么注释说明程序将继续执行到哪一个case为止;在一个switch块内,都必须包含一个default语句并且放在最后,即使什么代码也没有。

说明:注意break是退出switch语句块,而return是退出方法体。

【高】当switch括号内的变量类型为String并且此变量为外部参数时,必须先进行null判断。

反例:

// 如下的代码输出是什么?
public class SwitchString {
   public static void main(String[] args) {
       method(null);
   }
   public static void method(String param) {
       switch (param) {
           // 肯定不是进入这里
           case "sth":System.out.println("it's sth");break;
           // 也不是进入这里
           case "null":System.out.println("it's null");break; 
           // 也不是进入这里
           default:System.out.println("default");
       }
   }
}

【高】公开接口需要进行入参保护,尤其是批量操作的接口。

反例:

某业务系统,提供一个用户批量查询的接口,API文档上有说最多查多少个,但接口实现上没做任何保护,导致调用方传了一个1000的用户id数组过来后,查询信息后,内存出现溢出问题。
【中】使用if-else判断语句实现逻辑表达时,为避免后续维护困难,嵌套原则上不超过三层。

【中】三目运算符condition?表达式1:表达式2中,高度注意表达式1和2在类型对齐时,可能抛出因自动拆箱导致的空指针异常。

说明:以下两种场景会触发类型对齐的拆箱操作:

①表达式1或表达式2的值只要有一个是原始类型。

②表达式1或表达式2的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。

反例:

Integer a = 1;
Integer b = 2;
Integer c = null;
Boolean flag = false;
// a*b的结果是int类型,那么c会强制拆箱成int类型,抛出空指针异常
Integer result=(flag ? a * b : c);

【中】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、 获取数据库连接,以及不必要的 try-catch 操作(这个try-catch是否可以移至循环体外)。

6.3 集合处理

【高】关于hashCode和equals的处理,遵循如下规则:

①只要覆写equals,就必须覆写hashCode。

②因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须覆写这两种方法。

③如果自定义对象作为Map的键,那么必须覆写hashCode和equals。

说明:String因为覆写了hashCode和equals方法,所以可以将String对象作为key来使用。

【高】判断集合内部的元素是否为空或者是否为null,不要直接使用集合类的isEmpty()和size() == 0的方式,需要使用工具类的isEmpty()方法,此方法对空指针进行了规避。

说明:在某些涉及集合的逻辑中,可能出现集合为null或者空集合,如果直接使用集合类的isEmpty()或者size() == 0方法,就有可能出现空指针异常问题,在工具类的isEmpty()方法中,优先判断是否为null,如为null,则直接给出返回结果,避免空指针异常出现。

【高】在定义带有泛型的集合时,需要明确指定泛型类型,不可使用Object类型。

说明:泛型不指定明确的类型,在遍历集合的时候,容易出现类转换异常(ClassCastException)。

【高】在DAO层中Mapper接口中,接收数据查询结果时,禁止使用List<Map<String,Object>>、Map<String,Object>接收。必须定义对应的实体类进行接收。

说明:首先在对结果进行处理的时候,需要使用Map的get方法,指定字段名,此时字段名会出现硬编码问题,另外Map的value部分使用Object,容易出现类转换(ClassCastException)。

正例:

public interface UserMapper { 
​    List<User> userList(); 
} 

反例:

public interface UserMapper { 
​    List<Map<String,Object>> userList(); 
}

6.4 日期与时间API

【高】日期格式化时,传入pattern中表示年份统一使用小写的y。

说明:日期格式化时,yyyy表示当天所在的年,而大写的YYYY代表是week in which year(JDK7之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的YYYY就是下一年。

正例:

// 表示日期和时间的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"

【高】在日期格式中分清楚大写的M和小写的m,大写的H和小写的h分别指代的意义。

说明:日期格式中的这两对字母表意如下:

①表示月份是大写的 M;

②表示分钟则是小写的m;

③24小时制的是大写的 H;

④12小时制的则是小写的h。

【高】获取当前毫秒数:System.currentTimeMillis();而不是new Date().getTime()。

说明:如果想获取更加精确的纳秒级时间值,使用System.nanoTime的方式。在JDK8中,针对统计时间等场景,推荐使用Instant类。

6.5 异常处理

【高】优先考虑通过开发框架统一封装异常处理。

【高】针对异常的情况才使用异常。

说明:基于异常的控制流模糊了代码的意图,降低了它的性能。异常应该用于解决程序运行中的各种意外情况,而不应该成为正常控制流的一部分,这样有助于代码的可读性和维护性。

【高】Java类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch的方式来处理,如:NullPointerException,IndexOutOfBoundsException等。

说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException来实现。

正例:

if (obj != null) {...}

反例:

try { obj.method(); } catch (NullPointerException e) {...}

【高】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。

说明:对大段代码进行try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题。

【高】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。如果选择忽略异常,catch块应该包含一个注释,解释为什么这样做是合适的,并且该变量应该被命名为ignore 。

正例:

catch (TimeoutException | ExecutionException ignored) {
   // Use default: minimal coloring is desirable, not required 
}

【高】事务场景中,抛出异常被catch后,如果需要回滚,一定要注意手动回滚事务。

【高】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。

说明:如果JDK7及以上,优先使用try-with-resources方式(必须确定流对象是否实现了AutoCloseable接口或其子接口Closeable,否则会出现无法关闭问题)。

【高】不要在finally块中使用return。

说明:try块中的return语句执行成功后,并不马上返回,而是继续执行finally块中的语句,如果此处存在return语句,则在此直接返回,无情丢弃掉try块中的返回点。

反例:

private int x = 0public int checkReturn() {
   try {
       // x等于1,此处不返回
       return ++x;
   } finally {
       // 返回的结果是2
       return ++x;
   }
}

【高】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。

【高】在调用RPC、三方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。

说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException。三方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError。

【中】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。

说明:本规范明确防止空指针异常是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。

6.6 并发编程

【高】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。

说明:资源驱动类、工具类、单例工厂类都需要注意。

【高】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

【高】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors返回的线程池对象的弊端如下:

① FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致内存溢出。

②CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致内存溢出。

【高】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally块进行回收。

正例:

objectThreadLocal.set(userInfo);
try {
   //
} finally {
   objectThreadLocal.remove();
}

【高】高并发时,同步调用应该去考量锁的性能损耗,尽可能使加锁的代码块工作量尽可能的小。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁,避免在锁代码块中调用RPC方法。

【高】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,避免造成死锁。

说明:如果线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。

【高】在使用阻塞等待获取锁的方式中,必须保证能够正常加锁、解锁,以及能够在finally中正常解锁。

【高】资金相关的金融敏感信息,使用悲观锁策略。

说明:涉及资金相关的金融敏感信息,对数据的安全性、准确性要求很高,悲观锁遵循一锁、二判、三更新、四释放的原则,使用悲观锁策略可以有效避免问题。

【中】线程资源必须通过线程池提供,原则上不允许在应用中自行显式创建线程。

说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。

6.7 安全编程

【高】隶属于用户个人的页面或者功能必须进行权限控制校验。

说明:防止没有做权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容。

【高】用户敏感数据禁止直接展示,必须对展示数据进行脱敏。

说明:以中国大陆个人手机号码显示为例:139****1219,隐藏中间4位,防止隐私泄露。涉及其他隐私信息,如住址、身份证号、姓名等,都需要做相应的脱敏操作。

【高】用户输入的SQL参数必须满足防止SQL注入,禁止字符串拼接SQL访问数据库。在Mybatis中的Mapper.xml文件中禁止使用${}引入参数值,必须使用#{},防止SQL注入问题。

【高】用户请求传入的任何参数必须做有效性验证。

【高】微服务接口方法采用GET或POST请求方式。

【高】在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。

说明:如注册时发送验证码到手机,如果没有限制次数和频率,那么攻击者可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。

6.8 日志规范

【高】应用中必须存储输入日志,所有日志文件至少保存30天,日志文件名必须明确标注日志日期,便于日志回溯。

【高】生产环境禁止直接使用System.out或System.err输出日志或使用e.printStackTrace()打印异常堆栈。

【中】谨慎地记录日志。生产环境避免输出debug日志;有选择地输出info日志;如果使用warn来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,评估日志量和磁盘容量,及时监控和处理日志。

说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。

6.9 前后端规范

【高】前后端交互的API需要明确协议、域名、路径、请求方法、请求内容、状态码、响应体。

【高】后端返回的响应内容中,必须使用统一的返回结构,结构中包含响应码、说明信息、响应内容三个部分,不得直接返回字符串、集合等数据,也不可直接响应异常栈信息,分页数据需要通过分页类封装统一返回。

说明:减少前后端的沟通成本,避免不同结构不同返回结构导致的前端判断成本,以及降低前端出错的概率。

正例:

public class ResponseResult<T> { 
   private Integer code; 
   private String message; 
   private T data; 
   …… 
}
public class PageData<T> { 
   private Integer total; 
   private List<T> data; 
}
// Controller层接口 
@GetMapping(“userList”) 
public ResponseResult<PageData<User>> userList(){…} 

反例:

public class PageData<T> { 
   private Integer total; 
   private List<T> data; 
}
// Controller层接口 
@GetMapping("userList"public PageData<User> userList(){…}
@GetMapping("getUserName"public String getUserName(){……} 

【高】前后端数据列表相关的接口返回,如果为空列表,返回[],如果是空对象,返回null。

说明:此条约定有利于数据层面上的协作更加高效,减少前端琐碎的null判断。

【高】在前后端交互的JSON格式数据中,所有的key必须为小写字母开始的lowerCamelCase风格,符合英文表达习惯,且表意完整。

正例:

errorCode、errorMessage、assetStatus、menuList、orderList、configFlag

反例:

ERRORCODE、ERROR_CODE、error_message、error-message、errormessage、ErrorMessage、msg

【高】前后端的时间格式统一为"yyyy-MM-dd"或"yyyy-MM-dd HH:mm:ss",统一采用字符串形式传输,由后端完成字符串日期的转换。

说明:此格式转换满足多种请求方式,如直接采用日期或者是时间戳,可能会在接收参数时出现转换异常。如都做特殊处理,会出现不同请求方式,处理的方式不同,增加后端逻辑处理复杂度,同时也增加前端传入参数的转换复杂度。

【高】服务端的Controller层禁止写业务代码、不可引入DAO层相关的信息或者代码,只做简单的字段格式和非空检验,涉及业务逻辑相关的判断,必须统一书写在Service层。

【高】对于需要使用超大整数的场景,服务端一律使用String字符串类型返回,禁止使用long类型。

说明:Java服务端如果直接返回Long整型数据给前端,JS会自动转换为Number类型(注:此类型为双精度浮点数,表示原理与取值范围等同于Java中的Double)。Long类型能表示的最大值是2的63次方-1,在取值范围之内,超过2的53次方(9007199254740992)的数值转化为JS的Number时,有些数值会有精度损失。
 


目录