一、编程规范
(一)命名规范
- 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例:_name / _name / $name / name_ / name$ / name_ - 所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式
- 类名使用UpperCamelCase风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID等。(DO等名词解释请见附录)
正例:ForceCode / UserDO / HtmlDTO / XmlService / TcpUdpDeal / TaPromotion 反例:forcecode / UserDo / HTMLDto / XMLService / TCPUDPDeal / TAPromotion - 方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase风格。
正例: localValue / getHttpMessage() / inputUserId - 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME 反例:MAX_COUNT / EXPIRED_TIME - 抽象类命名使用
Abstract 或Base 开头; 异常类命名使用Exception 结尾; 测试类命名以它要测试的类的名称开始,以Test 结尾。 - 类型与中括号紧挨相连来表示数组。
正例:定义整形数组int[] arrayDemo 。 反例:在main参数中,使用String args[] 来定义。 - POJO类中的任何布尔类型的变量,都不要加is前缀,否则部分框架解析会引起序列化错误。
说明:在本文MySQL规约中的建表约定第一条,表达是与否的变量采用is_xxx的命名方式,所以,需要在<resultMap> 设置从is_xxx到xxx的映射关系。 - 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
正例:应用工具类包名为com.alibaba.ei.kunlun.aap.util 、类名为MessageUtils - 避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。
说明:子类、父类成员变量名相同,即使是public类型的变量也能够通过编译,另外,局部变量在同一方法内的不同代码块中同名也是合法的,这些情况都要避免。对于非setter/getter的参数名称也要避免与成员变量名称相同。 - 杜绝完全不规范的缩写,避免望文不知义。
反例:AbstractClass “缩写”成AbsClass ;condition “缩写”成 condi ;Function 缩写”成Fu ,此类随意缩写严重降低了代码的可阅读性。 - 为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达。
- 在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。
正例: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 = "alibaba"; 反例:接口方法定义 public abstract void f(); 说明:JDK8中接口允许有默认实现,那么这个default方法,是对所有实现类都有价值的默认实现。 - 接口和实现类的命名有两套规则:
1)对于Service和DAO类,基于SOA的理念,暴露出来的服务一定是接口,内部的实现类用Impl的后缀与接口区别。 正例:CacheServiceImpl 实现CacheService 接口。 2)如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able的形容词)。 正例:AbstractTranslator 实现 Translatable 接口。 - 各层命名规约:
A) Service/DAO层方法命名规约 ?????1) 获取单个对象的方法用get 做前缀。 ?????2) 获取多个对象的方法用list 做前缀,复数结尾,如:listObjects 。 ?????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) 左大括号前不换行。 ?????2) 左大括号后换行。 ?????3) 右大括号前换行。 ?????4) 右大括号后还有else等代码则不换行;表示终止的右大括号后必须换行。 - 左小括号和右边相邻字符之间不出现空格;
右小括号和左边相邻字符之间也不出现空格; 而左大括号前需要加空格 反例:if ( a == b ) if/for/while/switch/do 等保留字与括号之间都必须加空格- 任何二目、三目运算符的左右两边都需要加一个空格。
说明:包括赋值运算符=、逻辑运算符&&、加减乘除符号等。 - 采用4个空格缩进,禁止使用Tab字符。
正例:
public static void main(String[] args) {
String say = "hello";
int flag = 0;
if (flag == 0) {
System.out.println(say);
}
if (flag == 1) {
System.out.println("world");
} else {
System.out.println("ok");
}
}
- 在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。
正例:
double first = 3.2d;
int second = (int)first + 2;
- 单行字符数限制不超过120个,超出需要换行,换行时遵循如下原则:
1)第二行相对第一行缩进4个空格,从第三行开始,不再继续缩进,参考示例。 2)运算符与下文一起换行。 3)方法调用的点符号与下文一起换行。 4)方法调用中的多个参数需要换行时,在逗号后进行。 5)在括号前不要换行 正例:
StringBuilder sb = new StringBuilder();
sb.append("yang").append("hao")...
.append("chen")...
.append("chen")...
.append("chen");
反例:
StringBuilder sb = new StringBuilder();
sb.append("you").append("are")...append
("lucky");
method(args1, args2, args3, ...
, argsX);
- 方法参数在定义和传入时,多个参数逗号后面必须加空格。
正例:下例中实参的args1,后边必须要有一个空格。
method(args1, args2, args3);
- IDE的text file encoding设置为UTF-8; IDE中文件的换行符使用Unix格式,不要使用Windows格式。
- 不同逻辑、不同语义、不同业务的代码之间插入一个空行分隔开来以提升可读性。
说明:任何情形,没有必要插入多个空行进行隔开。
(三)OOP规约
- 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
- 所有的覆写方法,必须加
@Override 注解。 Object 的equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals。 正例:"test".equals(object); 反例:object.equals("test"); 说明:推荐使用JDK7引入的工具类java.util.Objects#equals(Object a, Object b) - 所有整型包装类对象之间值的比较,全部使用
equals 方法比较 说明:对于Integer var = ? 在-128至127之间的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用equals方法进行判断。 - 浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用
equals 来判断 说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数 反例:
float a = 1.0F - 0.9F;
float b = 0.9F - 0.8F;
if (a == b) {
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) {
}
正例:
(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");
}
- 定义数据对象DO类时,属性类型要与数据库字段类型相匹配。 Java实体类的属性类型与数据库表字段类型对应表
正例:数据库字段的bigint 必须与类属性的Long 类型相对应。 反例:某个案例的数据库表id字段定义类型bigint unsigned ,实际类对象属性为Integer ,随着id越来越大,超过Integer 的表示范围而溢出成为负数。 - 关于基本数据类型与包装数据类型的使用标准如下:(基本数据类型及其包装类)
1)所有的POJO类属性必须使用包装数据类型。 2)RPC方法的返回值和参数必须使用包装数据类型。 3)所有的局部变量使用基本数据类型。 说明:POJO类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题,或者入库检查,都由使用者来保证。 正例:数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。 反例:某业务的交易报表上显示成交总额涨跌情况,即正负x%,x为基本数据类型,调用的RPC服务,调用不成功时,返回的是默认值,页面显示为0%,这是不合理的,应该显示成中划线-。所以包装数据类型的null值,能够表示额外的信息,如:远程调用失败,异常退出。 - 定义DO/DTO/VO等POJO类时,不要设定任何属性默认值。
- 循环体内,字符串的连接方式,使用
StringBuilder 的append 方法进行扩展。 说明:下例中,反编译出的字节码文件显示每次循环都会new出一个StringBuilder 对象,然后进行append 操作,最后通过toString 方法返回String 对象,造成内存资源浪费。 反例:
String str = "start";
for (int i = 0; i < 100; i++) {
str = str + "hello";
}
(四)日期时间
- 日期格式化时,传入pattern中表示年份统一使用小写的y。
说明:日期格式化时,yyyy表示当天所在的年,而大写的YYYY代表是week in which year(JDK7之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的YYYY就是下一年。 正例:表示日期和时间的格式如下所示:
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
- 在日期格式中分清楚大写的M和小写的m,大写的H和小写的h分别指代的意义。
说明:日期格式中的这两对字母表意如下: 1) 表示月份是大写的M; 2) 表示分钟则是小写的m; 3) 24小时制的是大写的H; 4) 12小时制的则是小写的h。 - 获取当前毫秒数:
System.currentTimeMillis() ; 而不是new Date().getTime() 。 说明:如果想获取更加精确的纳秒级时间值,使用System.nanoTime 的方式。在JDK8中,针对统计时间等场景,推荐使用Instant 类。
(五)集合处理
- 判断所有集合内部的元素是否为空,使用
isEmpty() 方法,而不是size()==0 的方式 - 不要在
foreach 循环里进行元素的remove/add 操作。remove 元素请使用Iterator 方式,如果并发操作,需要对Iterator 对象加锁。 正例:
List<String> list = new ArrayList<>();
list.add("1");
list.add("2");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
String item = iterator.next();
if (删除元素的条件) {
iterator.remove();
}
}
反例:
for (String item : list) {
if ("1".equals(item)) {
list.remove(item);
}
}
(六)前后端规约
- 前后端交互的API,需要明确协议、域名、路径、请求方法、请求内容、状态码、响应体。
说明: 1) 协议:生产环境必须使用HTTPS。 2) 路径:每一个API需对应一个路径,表示API具体的请求地址: ?????a) 代表一种资源,只能为名词,推荐使用复数,不能为动词,请求方法已经表达动作意义。 ?????b) URL路径不能使用大写,单词如果需要分隔,统一使用下划线。 ?????c) 路径禁止携带表示请求内容类型的后缀,比如".json",".xml",通过accept头表达即可。 3) 请求方法:对具体操作的定义,常见的请求方法如下: ?????a) GET :从服务器取出资源。 ?????b) POST :在服务器新建一个资源。 ?????c) PUT :在服务器更新资源。 ?????d) DELETE :从服务器删除资源。 4) 请求内容:URL带的参数必须无敏感信息或符合安全要求;body里带参数时必须设置Content-Type 。 5) 响应体:响应体body可放置多种数据类型,由Content-Type 头来确定。 - 前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}。
说明:此条约定有利于数据层面上的协作更加高效,减少前端很多琐碎的null判断。 - 在前后端交互的
JSON 格式数据中,所有的key 必须为小写字母开始的lowerCamelCase 风格,符合英文表达习惯,且表意完整。 正例:errorCode / errorMessage / assetStatus / menuList / orderList / configFlag 反例:ERRORCODE / ERROR_CODE / error_message / error-message / errormessage / ErrorMessage / msg - 对于需要使用超大整数的场景,服务端一律使用
String 字符串类型返回,禁止使用Long 类型。 - HTTP请求通过URL传递参数时,不能超过2048字节。
- HTTP请求通过body传递内容时,必须控制长度,超出最大长度后,后端解析会出错。
说明:nginx默认限制是1MB,tomcat默认限制为2MB,当确实有业务需要传较大内容时,可以通过调大服务器端的限制 - 在翻页场景中,用户输入参数的小于1,则前端返回第一页参数给后端;后端发现用户输入的参数大于总页数,直接返回最后一页。
二、MySQL数据库
(一)建表规约
- 表达是与否概念的字段,必须使用
is_xxx 的方式命名,数据类型是unsigned tinyint (1表示是,0表示否)。 说明:任何字段如果为非负数,必须是unsigned 。 注意:POJO类中的任何布尔类型的变量,都不要加is前缀,所以,需要在<resultMap> 设置从is_xxx到Xxx的映射关系。数据库表示是与否的值,使用tinyint类型,坚持is_xxx的命名方式是为了明确其取值含义与取值范围。 正例:表达逻辑删除的字段名is_deleted ,1表示删除,0表示未删除。 - 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
说明:MySQL在Windows下不区分大小写,但在Linux下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。 正例:aliyun_admin,rdc_config,level3_name 反例:AliyunAdmin,rdcConfig,level_3_name - 表名不使用复数名词
说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于DO类名也是单数形式,符合表达习惯。 - 禁用保留字,如
desc、range、match、delayed 等,请参考MySQL官方保留字。 - 主键索引名为pk_字段名;唯一索引名为uk_字段名;普通索引名则为idx_字段名。
说明:pk_ 即primary key;uk_ 即 unique key;idx_ 即index的简称。 - 小数类型为
decimal ,禁止使用float和double 。 说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。 - 如果存储的字符串长度几乎相等,使用
char 定长字符串类型。 varchar 是可变长字符串,不预先分配存储空间,长度不要超过5000,如果存储长度大于此值,定义字段类型为text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。- 表必备三字段:
id, create_time, update_time 。 说明:其中id必为主键,类型为bigint unsigned 、单表时自增、步长为1。create_time, update_time 的类型均为datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。
(二)索引规约
- 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
说明:不要以为唯一索引影响了insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。 - 超过三个表禁止join。需要join的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。
说明:即使双表join也要注意表索引、SQL性能。 - 在
varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。 说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上,可以使用count(distinct left(列名, 索引长度))/count(*) 的区分度来确定。 - 利用延迟关联或者子查询优化超多分页场景。
说明:MySQL并不是跳过offset 行,而是取offset+N 行,然后返回放弃前offset行,返回N行,那当offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写。 正例:先快速定位需要获取的id段,然后再关联:
SELECT t1.* FROM 表1 as t1, (select id from 表1 where 条件 LIMIT 100000,20 ) as t2 where t1.id=t2.id
(三)SQL语句
- 不要使用
count(列名) 或count(常量) 来替代count(*) ,count(*) 是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关。 说明:count(*) 会统计值为NULL的行,而count(列名) 不会统计此列为NULL值的行。 count(distinct col) 计算该列除NULL之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为NULL,那么即使另一列有不同的值,也返回为0。- 当某一列的值全是NULL时,
count(col) 的返回结果为0,但sum(col) 的返回结果为NULL,因此使用sum()时需注意NPE问题。 正例:可以使用如下方式来避免sum的NPE问题: SELECT IFNULL(SUM(column), 0) FROM table; - 使用
ISNULL() 来判断是否为NULL 值。 说明:NULL 与任何值的直接比较都为NULL 。 1) NULL<>NULL 的返回结果是NULL ,而不是false 。 2) NULL=NULL 的返回结果是NULL ,而不是true 。 3) NULL<>1 的返回结果是NULL ,而不是true 。 反例:在SQL语句中,如果在null前换行,影响可读性。 select * from table where column1 is null and column3 is not null ; 而ISNULL(column) 是一个整体,简洁易懂。从性能数据上分析,ISNULL(column) 执行效率更快一些。 - 代码中写分页查询逻辑时,若count为0应直接返回,避免执行后面的分页语句。
- 不得使用外键与级联,一切外键概念必须在应用层解决。 说明:(概念解释)学生表中的student_id是主键,那么成绩表中的student_id则为外键。如果更新学生表中的student_id,同时触发成绩表中的student_id更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
- 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
- **数据订正(**特别是删除或修改记录操作)时,要先select,避免出现误删除,确认无误才能执行更新语句。
- 对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定。
说明:对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名(或表名),并且操作列在多个表中存在时,就会抛异常。 正例:select t1.name from table_first as t1 , table_second as t2 where t1.id=t2.id; 反例:在某业务中,由于多表关联查询语句没有加表的别名(或表名)的限制,正常运行两年后,最近在某个表中增加一个同名字段,在预发布环境做数据库变更后,线上查询语句出现出1052异常:Column 'name' in field list is ambiguous 。
(四)ORM映射
- 在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
说明: 1)增加查询分析器解析成本。 2)增减字段容易与resultMap配置不一致。 3)无用字段增加网络消耗,尤其是text类型的字段。 - POJO类的布尔属性不能加is,而数据库字段必须加is_,要求在resultMap中进行字段与属性之间的映射。
说明:参见定义POJO类以及数据库字段定义规定,在sql.xml增加映射,是必须的。 - 不要用
resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义<resultMap> ;反过来,每一个表也必然有一个<resultMap> 与之对应。 说明:配置映射关系,使字段与DO类解耦,方便维护。 sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现SQL注入。- iBATIS自带的
queryForList(String statementName,int start,int size) 不推荐使用。 说明:其实现方式是在数据库取到statementName 对应的SQL语句的所有记录,再通过subList 取start,size 的子集合。 正例:
Map<String, Object> map = new HashMap<>(16);
map.put("start", start);
map.put("size", size);
- 不允许直接拿
HashMap 与Hashtable 作为查询结果集的输出。 - 更新数据表记录时,必须同时更新记录对应的update_time字段值为当前时间。
- 不要写一个大而全的数据更新接口。传入为POJO类,不管是不是自己的目标更新字段,都进行
update table set c1=value1,c2=value2,c3=value3 ; 这是不对的。执行SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是增加binlog存储。
附录
注:上述摘选自 阿里巴巴《Java开发手册(嵩山版)》的部分内容 如有疑问请自行点击链接下载。
|