Java后台代码编写规范前言
本文以 Java 开发者为中心视角,划分为:编程规约、异常日志、单元测试、安全规约、工程结构五个维度,再根据内容特征,细分成若干二级子目录。另外,依据约束力强弱及故障敏感性,规约依次分为强制、推荐、参考三大类。在延伸信息中,“说明”对规约做了适当扩展和解释;“正例”提倡什么样的编码和实现方式;“反例”说明需要提防的雷区,以及真实的错误案例,本文主要参考阿里巴巴编码规范
- 编程规约
- 命名风格
- 【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束
反例: _name/__name/$Object/name_/name$/Object$
- 【强制】代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。
注意,即使纯拼音命名方式也要避免采用。
正例:szkingdom/youku/shenzhen/chengdu 等国际通用的名称,可视同英文。
反例:DaZhePromotion[打折]/getPingfenByName()[评分]/int 某变量 = 3
- 【强制】类名使用UpperCamelCase风格,必须遵从驼峰形式,但以下情形例外:(领域模型的相关命名):DO/BO/DTO/VO/AO/PO/UID等
正例:JavaServerlessPlatform/UserDO/XmlService/TcpUdpDeal/TaPromotion
反例:javaserverlessplatform/UserDo/XMLService/TCPUDPDeal/TAPromotion
- 【强制】方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格,必须遵从驼峰形式。
正例: localValue / getHttpMessage() / inputUserId
- 【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例:localValue/getHttpMessage()/inputUserId
- 【强制】抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾;测试类命名以它要测试的类的名称开始,单元测试类以Test结尾,集成测试类以IT结尾。
正例:MAX_STOCK_COUNT 反例:MAX_COUNT
- 【强制】中括号是数组类型的一部分。
正例:定义整形数组int[] arrayDemo;
反例:在main参数中,使用String args[]来定义。
说明:String[] args是Java风格的数组定义,String args[]是C风格的数组定义。
- 【强制】POJO类中布尔类型变量都不要加is前缀,否则部分框架解析会引起序列化错误。
说明:在本文MySQL规约中的建表约定第一条,表达是与否的值采用is_xxx的命名方式,所以,需要在<resultMap>设置从is_xxx到xxx的映射关系。
反例:定义为基本数据类型Boolean isDeleted的属性,它的方法也是isDeleted(),RPC框架在反向解析的时候,“误以为”对应的属性名称是deleted,导致属性获取不到,进而抛出异常。
- 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
正例:应用工具类包名为com.szkingdom.open.util、类名为MessageUtils(此规则参考spring的框架结构)
- 【强制】避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可读性降低。
说明:子类、父类成员变量名相同,即使是public类型的变量也是能够通过编译,而局部变量在同一方法内的不同代码块中同名也是合法的,但是要避免使用。对于非setter/getter的参数名称也要避免与成员变量名称相同。
- 【强制】杜绝完全不规范的缩写,避免望文不知义。
反例:AbstractClass“缩写”命名成AbsClass;condition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读性。
- 【推荐】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。
正例:在JDK中,表达原子更新的类名为:AtomicReferenceFieldUpdater。
反例:int a的随意命名方式。
- 【推荐】在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。
正例:startTime/workQueue/nameList/TERMINATED_THREAD_COUNT
反例:startedAt/QueueOfWork/listName/COUNT_TERMINATED_THREAD
- 【推荐】如果使用到了设计模式,建议在类名中体现出具体模式。
说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计思想。
正例:public class OrderFactory;
public class LoginProxy;
public class ResourceObserver;
- 【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。
正例:接口方法签名:void commit();
接口基础常量表示:String COMPANY = "szkingdom";
反例:接口方法定义:public abstract void f();
说明:JDK8中接口允许有默认实现,那么这个default方法,是对所有实现类都有价值的默认实现。
- 接口和实现类的命名有两套规则:
- 【强制】对于Service和DAO类,基于SOA的理念,暴露出来的服务一定是接口,内部的实现类用Impl的后缀与接口区别。
- 【推荐】 如果是形容能力的接口名称,取对应的形容词做接口名(通常是–able的形式)。
- 【参考】枚举类名建议带上Enum后缀,枚举成员名称需要全大写,单词间用下划线隔开。
说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。
正例:枚举名字:DealStatusEnum,成员名称:SUCCESS/UNKOWN_REASON。
- 【参考】各层命名规约:
A) Service/DAO层方法命名规约
1) 获取单个对象的方法用get做前缀。
2) 获取多个对象的方法用list做前缀。
3) 获取统计值的方法用count做前缀。
4) 插入的方法用save(推荐)或insert做前缀。
5) 删除的方法用remove(推荐)或delete做前缀。
6) 修改的方法用update做前缀。
B) 领域模型命名规约
1) 数据对象:xxxDO,xxx即为数据表名。
2) 数据传输对象:xxxDTO,xxx为业务领域相关的名称。
3) 展示对象:xxxVO,xxx一般为网页名称。
4) POJO是DO/DTO/BO/VO的统称,禁止命名成xxxPOJO。
- 常量定义
1、【强制】不允许出现任何魔法值(即未经定义的常量)直接出现在代码中。
反例:String key = "Id#szkingdom_" + tradeId;
cache.put(key, value);
2、【强制】long或者Long初始赋值时,必须使用大写的L,不能是小写的l,小写容易跟数字1混淆,造成误解。
说明:Long a = 2l; 写的是数字的21,还是Long型的2?
3、【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
说明:大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解和维护。
正例:缓存相关常量放在类CacheConsts下;系统配置相关常量放在类ConfigConsts下。
4、【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。
1)跨应用共享常量:放置在二方库中,通常是client.jar中的constant目录下。
2)应用内共享常量:放置在一方库的modules中的constant目录下。
反例:易懂变量也要统一定义成应用内共享常量,两位攻城师在两个类中分别定义了表示“是”的变量:
类A中:public static final String YES = "yes";
类B中:public static final String YES = "y";
A.YES.equals(B.YES),预期是true,但实际返回为false,导致产生线上问题。
3)子工程内部共享常量:即在当前子工程的constant目录下。
4)包内共享常量:即在当前包下单独的constant目录下。
5)类内共享常量:直接在类内部private static final定义。
5、【推荐】如果变量值仅在一个范围内变化用Enum类。
说明:如果存在名称之外的延伸属性应使用enum类型,下面正例中的数字就是延伸信息,表示一年中的第几个季节。
正例:
public enum SeasonEnum {
SPRING(1), SUMMER(2), AUTUMN(3), WINTER(4);
private int seq;
SeasonEnum(int seq) {
this.seq = seq;
}
public int getSeq() {
return seq;
}
}。
- 代码格式
- 【强制】如果是大括号内为空,则简洁地写成{}即可,大括号中间无需换行和空格;如果是非空代码块则:
1)左大括号前不换行。
2)左大括号后换行。
3)右大括号前换行。
4)右大括号后还有else等代码则不换行;表示终止的右大括号后必须换行。
- 【强制】左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格;而左大括号前需要空格。详见第5条下方正例提示。
反例:if (空格a == b空格)
- 【强制】if/for/while/switch/do等保留字与括号之间都必须加空格。
- 【强制】任何二目、三目运算符的左右两边都需要加一个空格。
说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等。
- 【强制】采用4个空格缩进,禁止使用tab字符。
如果使用tab缩进,必须设置1个tab为4个空格。IDEA设置tab为4个空格时,请勿勾选Use tab character;而在eclipse中,必须勾选insert spaces for tabs。
- 【强制】注释的双斜线与注释内容之间有且仅有一个空格,禁止行尾注释 。
- 【强制】在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。
- 【强制】单行字符数限制不超过120个,超出需要换行,换行时遵循如下原则:
1)第二行相对第一行缩进4个空格,从第三行开始,不再继续缩进,参考示例。
2)运算符与下文一起换行。
3)方法调用的点符号与下文一起换行。
4)在多个参数超长,逗号后进行换行。
5)在括号前不要换行,见反例。
- 【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格。
正例:下例中实参的"a",后边必须要有一个空格。
method("a", "b", "c");
- 【强制】IDE的text file encoding设置为UTF-8;IDE中文件的换行符使用Unix格式,不要使用windows格式。
- 【强制】单个方法的总行数不超过80行。
说明:除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过80行。
正例:代码逻辑分清红花和绿叶,个性和共性,绿叶逻辑单独出来成为额外方法,使主干代码更加清晰;共性逻辑抽取成为共性方法,便于复用和维护。
- 【推荐】没有必要增加若干空格来使某一行的字符与上一行的相应字符对齐。
正例:int a = 3;
long b = 4L;
float c = 5F;
StringBuffer sb = new StringBuffer();
说明:增加sb这个变量,如果需要对齐,则给a、b、c都要增加几个空格,在变量比较多的情况下,是一种累赘的事情。
- 【推荐】不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。
说明:任何情形,没有必要插入多个空行进行隔开。
- 【强制】注解样式使用compact模式。关于格式请参考:
https://checkstyle.sourceforge.io/config_annotation.html#AnnotationUseStyle
- 【强制】每个注解应单独一行。
反例:if (a == b) { //判断a b是否相等
- 【强制】空的代码块应添加注释说明。
- 【强制】避免使用嵌套的块语句。
- 【强制】嵌套(内部)类/接口声明在类的底部(在所有方法和字段声明之后)。
- 【强制】避免使用静态导入,导入应按JDK/JAVA EE/三方类/当前项目类的顺序导入。
20、【推荐】关于静态导入,使用标准如下:
1) 避免静态导入静态变量。
2) 允许静态导入静态方法简化代码,如静态方法有重名,禁止静态导入。
- OOP约束
- 【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
- 【强制】所有的覆写方法,必须加@Override注解。
反例:getObject()与get0bject()的问题。一个是字母的O,一个是数字的0,加@Override可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。
- 【强制】相同参数类型,相同业务含义,才可以使用Java的可变参数,避免使用Object。
说明:可变参数必须放置在参数列表的最后。(提倡同学们尽量不用可变参数编程)。
正例:public List<User> listUsers(String type, Long... ids) {...}
- 【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须加@Deprecated注解,并清晰地说明采用的新接口或者新服务是什么。
- 【强制】不能使用过时的类或方法。
说明:java.net.URLDecoder中的方法decode(String encodeStr)这个方法已经过时,应该使用双参数decode(String source, String encode)。接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么。
- 【强制】Object的equals方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals。
正例:"test".equals(object);
反例:object.equals("test");
说明:推荐使用java.util.Objects#equals (JDK7引入的工具类)
- 【强制】所有整型包装类对象之间值的比较,全部使用equals方法比较。
说明:对于Integer var = ?在-128至127范围内的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断。
- 【强制】任何货币金额,均以最小货币单位且整型类型来进行存储。
- 【强制】浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来判断。
说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数,具体原理参考《码出高效》
- 【强制】定义数据对象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);
- 关于基本数据类型与包装数据类型的使用标准如下:
1)【强制】所有的POJO类属性必须使用包装数据类型。
2)【强制】RPC方法的返回值和参数必须使用包装数据类型。
3)【推荐】所有的局部变量使用基本数据类型。
说明:POJO类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题,或者入库检查,都由使用者来保证。
正例:数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。
反例:比如显示成交总额涨跌情况,即正负x%,x为基本数据类型,调用的RPC服务,调用不成功时,返回的是默认值,页面显示为0%,这是不合理的,应该显示成中划线。所以包装数据类型的null值,能够表示额外的信息,如:远程调用失败,异常退出。
- 【强制】定义DO/DTO/VO等POJO类时,不要设定任何属性默认值。
反例:POJO类的createTime默认值为new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。
- 【强制】序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID值。
说明:注意serialVersionUID不一致会抛出序列化运行时异常。
- 【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在init方法中。
- 【强制】POJO类必须写toString方法。使用IDE中的工具:source > generate toString时,如果继承了另一个POJO类,注意在前面加一下super.toString。
说明:在方法执行抛出异常时,可以直接调用POJO的toString()方法打印其属性值,便于排查问题。
- 【强制】禁止在POJO类中,同时存在对应属性xxx的isXxx()和getXxx()方法。
说明:框架在调用属性xxx的提取方法时,并不能确定哪个方法一定是被优先调用到。
- 【推荐】使用索引访问用String的split方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛IndexOutOfBoundsException的风险。
说明:
String str = "a,b,c,,";
String[] ary = str.split(",");
//预期大于3,结果是3
System.out.println(ary.length);
- 【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读,此条规则优先于下一条。
- 【推荐】 类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter/setter方法。
说明:公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现;因为方法信息价值较低,所有Service和DAO的getter/setter方法放在类体最后。
- 【推荐】setter方法中,参数名称与类成员变量名称一致,this.成员名 = 参数名。在getter/setter方法中,不要增加业务逻辑,增加排查问题的难度。
反例:
public Integer getData() {
if (condition) {
return this.data + 100;
} else {
return this.data - 100;
}
}
- 【推荐】循环体内,字符串的联接方式,使用StringBuilder的append方法进行扩展。
反例:
String str = "start";
for(int i = 0; i < 100; i++){
str = str + "hello";
}
说明:反编译出的字节码文件显示每次循环都会new出一个StringBuilder对象,然后进行append操作,最后通过toString方法返回String对象,造成内存资源浪费。
- 【推荐】final可以声明类、成员变量、方法、以及本地变量,下列情况使用final关键字:
1)不允许被继承的类,如:String类。
2)不允许修改引用的域对象。
3)不允许被覆写的方法,如:POJO类的setter方法。
4)不允许运行过程中重新赋值的局部变量。
5)避免上下文重复使用一个变量,使用final可以强制重新定义一个变量,方便更好地进行重构。
- 【推荐】慎用Object的clone方法来拷贝对象。
说明:对象的clone方法默认是浅拷贝,若想实现深拷贝需要重写clone方法实现属性对象的拷贝。
- 【推荐】类成员与方法访问控制从严:
1)如果不允许外部直接通过new来创建对象,那么构造方法必须是private。
2)工具类不允许有public或default构造方法。
3)类非static成员变量并且与子类共享,必须是protected。
4)类非static成员变量并且仅在本类使用,必须是private。
5)类static成员变量如果仅在本类使用,必须是private。
6)若是static成员变量,必须考虑是否为final。
7)类成员方法只供类内部调用,必须是private。
8)类成员方法只对继承类公开,那么限制为protected。
说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。思考:如果是一个private的方法,想删除就删除,可是一个public的service成员方法或成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,无限制的到处跑,那么你会担心的。
- 【强制】只有私有构造器的类应该声明为final。
- 【强制】工具类应隐藏public构造器。
- 【强制】确保异常类是不可变的,也就是说,它们仅具有final字段。
- 【强制】成员变量应定义为 private 的,并配置访问方法。
- 【推荐】根据Java编程语言的代码约定,类或接口声明的各部分应按以下顺序出现:
1)类(静态)变量。首先是公共类变量,然后是受保护的,然后是程序包级别(无访问修饰符),然后是私有。
2)实例变量。首先是公共类变量,然后是受保护的,然后是程序包级别(无访问修饰符),然后是私有。
3)构造方法
4)成员方法
5)内部类
- 日期与时间
- 【强制】日期格式化时,传入pattern中表示年份统一使用小写的y。
- 【强制】在日期格式中分清楚大写的M和小写的m,大写的H和小写的h分别指代的意义。
- 【强制】获取当前毫秒数:System.currentTimeMillis();而不是new Date().getTime()
- 【强制】不允许在程序任何地方中使用1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp。
- 【强制】不要在程序中写死一年为365天,避免在公历闰年时出现日期转换错误或程序逻辑错误。
- 【推荐】避免公历闰年2月问题。闰年的2月份有29天,一年后的那一天不可能是2月29 日
- 【推荐】使用枚举值来指代月份。如果使用数字,注意Date,Calendar等日期相关类的月份month取值在0-11 之间。
- 集合处理
- 【强制】关于hashCode和equals的处理,遵循如下规则:
1) 只要覆写equals,就必须覆写hashCode。
2) 因为Set存储的是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须覆写这两个方法。
3) 如果自定义对象作为Map的键,那么必须覆写hashCode和equals。
说明:String已覆写hashCode和equals方法,所以我们可以愉快地使用String对象作为key来使用。
- 【强制】判断所有集合内部的元素是否为空,使用isEmpty()方法,而不是size()==0的方式
- 【强制】在使用java.util.stream.Collectors类的toMap()方法转为Map集合时,一定要使用含有参数类型为BinaryOperator,参数名为mergeFunction的方法,否则当出现相同key值时会抛出IllegalStateException异常
- 【强制】在使用java.util.stream.Collectors类的toMap()方法转为Map集合时,一定要注意当 value 为null时会抛NPE异常。
- 【强制】ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList;
说明:subList返回的是ArrayList的内部类SubList,并不是 ArrayList,而是ArrayList的一个视图,对于SubList子列表的所有操作最终会反映到原列表上。
- 【强制】使用Map的方法keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出UnsupportedOperationException异常
- 【强制】Collections类返回的对象,如:emptyList()/singletonList()等都是immutable list,不可对其进行添加或者删除元素的操作
反例:如果查询无结果,返回Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发UnsupportedOperationException异常。
- 【强制】 在subList场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生ConcurrentModificationException 异常。
- 【强制】使用集合转数组的方法,必须使用集合的toArray(T[] array),传入的是类型完全一致、长度为0的空数组。
- 【强制】在使用Collection接口任何实现类的addAll()方法时,都要对输入的集合参数进行NPE判断。
说明:在ArrayList#addAll方法的第一行代码即Object[] a = c.toArray(); 其中c为输入集合参数,如果为null,则直接抛出异常。
- 【强制】使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。
- 【强制】泛型通配符<? extends T>来接收返回的数据,此写法的泛型集合不能使用add方法,而<? super T>不能使用get方法,作为接口调用赋值时易出错。
- 【强制】在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof判断,避免抛出ClassCastException异常。
- 【强制】不要在foreach循环里进行元素的remove/add操作。remove元素请使用Iterator方式,如果并发操作,需要对Iterator对象加锁。
- 【强制】在JDK7版本及以上,Comparator实现类要满足如下三个条件,不然Arrays.sort,Collections.sort会抛IllegalArgumentException异常。
- 【推荐】集合泛型定义时,在JDK7及以上,使用diamond语法或全省略。
- 【推荐】集合初始化时,指定集合初始值大小。
- 【推荐】使用entrySet遍历Map类集合KV,而不是keySet方式进行遍历。
- 【推荐】高度注意Map类集合K/V能不能存储null值的情况,如下表格:
- 【参考】合理利用好集合的有序性(sort)和稳定性(order),避免集合的无序性(unsort)和不稳定性(unorder)带来的负面影响。
- 【参考】利用Set元素唯一的特性,可以快速对一个集合进行去重操作,避免使用List的contains方法进行遍历、对比、去重操作。
- 控制语句
- 【强制】在一个switch块内,每个case要么通过break/return等来终止,要么注释说明程序将继续执行到哪一个case为止;在一个switch块内,都必须包含一个default语句并且放在最后,即使它什么代码也没有。
说明:注意break是退出switch语句块,而return是退出方法体。
- 【强制】当switch括号内的变量类型为String并且此变量为外部参数时,必须先进行null判断。
- 【强制】在if/else/for/while/do语句中必须使用大括号。
- 【强制】三目运算符“condition?表达式1:表达式2中”,高度注意表达式1和2在类型对齐时,可能抛出因自动拆箱导致的NPE异常。
- 【推荐】当某个方法的代码行数超过10行时,return/throw 等中断逻辑的右大括号后加一个空行。
- 【推荐】推荐尽量少用else, if-else的方式可以改写成:
public void findBoyfriend(Man man) {
if (man.isUgly()) {
System.out.println("本姑娘是外貌协会的资深会员");
return;
}
if (man.isPoor()) {
System.out.println("贫贱夫妻百事哀");
return;
}
if (man.isBadTemper()) {
System.out.println("银河有多远,你就给我滚多远");
return;
}
System.out.println("可以先交往一段时间看看");
}
- 【推荐】除常用方法(如getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
- 【推荐】不要在其它表达式(尤其是条件表达式)中,插入赋值语句。
说明:赋值点类似于人体的穴位,对于代码的理解至关重要,所以赋值语句需要清晰地单独成为一行。
反例:
public Lock getLock(boolean fair) {
// 算术表达式中出现赋值操作,容易忽略count值已经被改变
threshold = (count = Integer.MAX_VALUE) - 1;
// 条件表达式中出现赋值操作,容易误认为是sync==fair
return (sync = fair) ? new FairSync() : new NonfairSync();
}
- 【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的try-catch操作(这个try-catch是否可以移至循环体外)
- 【推荐】避免采用取反逻辑运算符。
说明:取反逻辑不利于快速理解,并且取反逻辑写法必然存在对应的正向逻辑写法。
正例:使用if (x < 628) 来表达 x 小于628。
反例:使用if (!(x >= 628)) 来表达 x 小于628。
- 【推荐】接口入参保护,这种场景常见的是用作批量操作的接口
- 注释约束
- 【强制】类、类属性、类方法的注释必须使用Javadoc规范,使用/*内容/格式,不得使用//xxx方式。
说明:在IDE编辑窗口中,Javadoc方式会提示相关注释,生成Javadoc可以正确输出相应注释;在IDE中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
- 【强制】所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
- 【强制】所有的类都必须添加创建者信息。
- 【强制】方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/* */注释,注意与代码对齐。
- 【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途。
- 【推荐】与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。
- 【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。
- 【推荐】在类中删除未使用的任何字段和方法;在方法中删除未使用的任何参数声明与内部变量。
- 【参考】谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除。
- 【参考】好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。
- 【强制】java源文件必须添加统一的文件头信息。
- 【强制】类、接口、枚举的注释标签出现顺序:@param, @author, @since, @see, @version, @serial, @deprecated。方法、构造器、变量的注释标签出现顺序:@param, @return, @throws, @since, @deprecated, @see。
- 【强制】public修改的变量必须添加注释。
- 异常日志
- 错误码
- 【强制】错误码的定制原则:快速回溯、简单易记、沟通标准化。
说明:错误码的回答问题是错误对象和错误位置,错误码必须能够快速的知晓错误的来源,可以快速判断是谁的问题;错误码要易于记忆与对比;错误码能够脱离文档和系统平台达到线下轻量化底自由沟通的目的。
- 【强制】错误码不体现版本号和错误等级信息
- 【强制】全部都正常,但不得不填充错误码时统一返回特定错误码,例如:00000或0。
- 【强制】错误码使用者避免随意定义新的错误码。
说明:尽可能在原有的错误码中找到语义相近的错误码在代码中使用。
- 【强制】错误码不能直接输出给用户作为提示信息使用。
- 【推荐】错误码之外的业务独特信息由error_message来承载,而不是让错误码本身涵盖过多具体业务属性。
- 异常处理
- 【强制】Java 类库中定义的可以通过预检查方式规避的RuntimeException异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException等等。
- 【强制】异常不要用来做流程控制,条件控制。
- 【强制】catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。
- 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,当然,不影响继续运行的异常除外。如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
- 【强制】有try块放到了事务代码中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
- 【强制】finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。
- 【强制】不要在finally块中使用return。
- 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
- 【强制】在调用RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用Throwable类来进行拦截。
- 【推荐】方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值
- 【推荐】防止NPE,是程序员的基本修养,注意NPE产生的场景。
- 【推荐】定义时区分unchecked/checked 异常,避免直接抛出new RuntimeException(),更不允许抛出Exception或者Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException/ServiceException等。
- 日志约束
- 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架SLF4J中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
- 【强制】所有日志文件至少保存15天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以“应用名.log”来保存,保存在/home/admin/应用名/logs/目录下, 过往日志格式为:{logname}.log.{保存日期},日期格式:yyyy-MM-dd。
- 【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:appName_logType_logName.log。logType:日志类型,如stats/monitor/access等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
- 【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。
- 【强制】对于trace/debug/info级别的日志输出,必须进行日志级别的开关判断。
- 【强制】避免重复打印日志,浪费磁盘空间,务必在logger中设置additivity=false
- 【强制】生产环境禁止直接使用System.out或System.err输出日志或使用e.printStackTrace()打印异常堆栈。
- 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字throws往上抛出。
- 【强制】日志打印时禁止直接用JSON工具将对象转换成String。
- 【推荐】尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。
- 单元测试
- 【强制】好的单元测试必须遵守AIR原则。 说明:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
- 【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用System.out来进行人肉验证,必须使用assert来验证。
- 【强制】保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
- 【强制】单元测试是可以重复执行的,不能受到外界环境的影响。
- 【强制】对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。
- 【强制】核心业务、核心应用、核心模块的增量代码确保单元测试通过。
- 【强制】单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。
- 安全约束
1、【强制】隶属于用户个人的页面或者功能必须进行权限控制校验。
说明:防止没有做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容、修改他人的订单。
2、【强制】用户敏感数据禁止直接展示,必须对展示数据进行脱敏。
说明:中国大陆个人手机号码显示为:137****0969,隐藏中间4位,防止隐私泄露。
3、【强制】用户输入的SQL参数严格使用参数绑定或者METADATA字段值限定,防止SQL注入,禁止字符串拼接SQL访问数据库。
说明:mybatis 中无非必要,均使用#符号,如需使用$符号,需严格限制并过滤用户输入参数,避免sql注入产生。
正例:
select user_name from user_info where user_code = #{code}
反例:在没有进严格输入限制的情况下,使用$绑定参数,如:
select user_name from user_info where user_code = ${code}。
4、【强制】用户请求传入的任何参数必须做有效性验证。
说明:忽略参数校验可能导致:
page size过大导致内存溢出
恶意order by导致数据库慢查询
任意重定向
SQL注入
反序列化注入
正则输入源串拒绝服务ReDoS
说明:Java代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的结果。
- 【强制】禁止向HTML页面输出未经安全过滤或未正确转义的用户数据。
6、【强制】表单、AJAX提交必须执行CSRF安全验证。
说明:CSRF(Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在CSRF漏洞的应用/网站,攻击者可以事先构造好URL,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。
7、【强制】URL外部重定向传入的目标地址必须执行白名单过滤。
说明:可以参考以下策略:
当用户访问需要生成跳转URL的页面时,首先生成随机token,并放入cookie。
在显示连接的页面上生成URL,在URL参数中加入token。
应用程序在跳转前,判断token是否和cookie中的token一致,如果不一致,就判定为URL跳转攻击,并记录日志。
如果在中做页面跳转,需要判断域名白名单后,才能跳转。
8、【强制】在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。
说明:如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。
9、【强制】对敏感信息进行加密传输。
正例:用户登录请求密码输入明文abc123EFG,请求传输过程中密码为密文fkGXT5DGmq8mm9ap7sLA5A==
10、【强制】禁止在数据库或文件系统中明文存储用户密码。
正例:使用摘要+盐,对密码进行加密,例如:
pwd: 396348c2ee1181633969373deae657ee15adf9b64c0c1c7b89458819cf10f0a6
反例:pwd: 123456abc
11、【强制】禁止使用GET方法传递敏感参数(会话标识、身份证号等)。
反例:curl -i http://127.0.0.1:8080/user/info?id_card=42xxxxxxxxxxx1922
1
12、【强制】对未能成功通过验证的请求,应当返回模糊的提示信息如“用户名或密码错误”。
13、【推荐】禁止长时间持续的登录状态,建议设置会话超时时间,超时后强制销毁会话。
14、【强制】文件操作应遵循以下规则:
上传操作应设计身份验证机制
必须在服务端使用白名单方式限制可上传文件类型,如果需求单一,可以不考虑上传文件的格式,直接在服务端根据时间生成(如jpg)文件名和目录名
限制允许上传的文件大小
引用上传的文件时,使用文件id(hash随机生成)进行调用,不要显示文件地址
禁止把上传的文件保存在Web环境中。如果有特殊需求,必须关闭在文件上传目录的执行限
权限检查:文件下载和引用时,应该检查该用户是否具有此文件的相关权限。
禁止使用直接传递文件路径的引用方式,如有特殊需要可设置路径白名单引用
- 工程架构
- 应用分层
- 【推荐】应用分层如下配置
开放接口层:可直接封装Service方法暴露成RPC接口;通过Web封装成http接口;进行网关安全控制、流量控制等。
终端显示层:各个端的模板渲染并执行显示的层。当前主要是velocity渲染,JS渲染,JSP渲染,移动端展示等。
Web层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等。
Service层:相对具体的业务逻辑服务层。
Manager层:通用业务处理层,它有如下特征:
1)对第三方平台封装的层,预处理返回结果及转化异常信息。
2)对Service层通用能力的下沉,如缓存方案、中间件通用处理。
3)与DAO层交互,对多个DAO的组合复用。
DAO层:数据访问层,与底层MySQL、Oracle、Hbase等进行数据交互。
外部接口或第三方平台:包括其它部门RPC开放接口,基础平台,其它公司的HTTP接口。
- 【参考】分层领域模型规约:
DO(Data Object):此对象与数据库表结构一一对应,通过DAO层向上传输数据源对象。
DTO(Data Transfer Object):数据传输对象,Service或Manager向外传输的对象。
BO(Business Object):业务对象,由Service层输出的封装业务逻辑的对象。
AO(Application Object):应用对象,在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高。
VO(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象。
Query:数据查询对象,各层接收上层的查询请求。注意超过2个参数的查询封装,禁止使用Map类来传输。
- 服务器
- 【推荐】高并发服务器建议调小TCP协议的time_wait超时时间。
说明:操作系统默认240秒后,才会关闭处于time_wait状态的连接,在高并发访问下,服务器端会因为处于time_wait的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值。
正例:在linux服务器上请通过变更/etc/sysctl.conf文件去修改该缺省值(秒): net.ipv4.tcp_fin_timeout = 30
- 【推荐】调大服务器所支持的最大文件句柄数(File Descriptor,简写为fd)。
说明:主流操作系统的设计是将TCP/UDP连接采用与文件一样的方式去管理,即一个连接对应于一个fd。主流的linux服务器默认所支持最大fd数量为1024,当并发连接数很大时很容易因为fd不足而出现“open too many files”错误,导致新的连接无法建立。建议将linux服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关)。
- 【推荐】给JVM环境参数设置-XX:+HeapDumpOnOutOfMemoryError参数,让JVM碰到OOM场景时输出dump信息。
说明:OOM的发生是有概率的,甚至相隔数月才出现一例,出错时的堆内信息对解决问题非常有帮助。
- 【推荐】在线上生产环境,JVM的Xms和Xmx设置一样大小的内存容量,避免在GC 后调整堆大小带来的压力。
5、【参考】服务器内部重定向使用forward;外部重定向地址使用URL拼装工具类来生成,否则会带来URL维护不一致的问题和潜在的安全风险。