java代码规范01
笔记前言
本笔记旨在记录Java开发手册的所有重点内容。不积硅步无以至千里。
编程规约
day 1学习的是代码规范中的编程基本规约。
官网有代码规范插件: [阿里巴巴开发规约ide插件](Versions: Alibaba Java Coding Guidelines - IntelliJ IDEs Plugin | Marketplace (jetbrains.com))
1. 命名风格
-
不能以下划线或美元符号开始结尾。
-
严禁使用拼音与英文混合,中文更不行。需使用国际通用名称。
-
代码注释要友好,尊重人。
-
类名使用UpperCamelCase风格,例外: DO/PO/DTO/BO/VO/UID等。
就是ForceCode/UserDo/HtmlDTO/XmlService这类的。
-
方法、参数、成员变量、局部变量统一使用lowerCamelCase风格.
localValue/getHttpMessage()/inputUserId 这类的
-
常量要大写,单词间下划线隔开。
-
抽象类用Abstract或Base开头。异常类用Exception结尾。测试类要用测试类名称开始,以Test结尾。
-
类型与中括号紧挨相连定义数组。除了main参数不一样
-
POJO类中任何布尔类型变量,都不要+ is前缀,否则部分框架解析会引起序列化错误。
-
package包统一使用小写,点分隔。
-
避免子父类成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。
说明: 子类、父类成员变量名相同,即使public也能通过编译,而局部变量在同一方法内的不同代码块中命名也是合法的,但是避免使用。除了getter setter,其他参数名称也要避免和成员变量名称相同。
-
不可以随意使用缩写,必须规范命名。
-
任何自定义编程元素在命名时,使用完整的单词组合来表达。
-
常量与变量命名时,表示类型的名词放在词尾,以提升辨识度。
比如: startTime/workQueue/nameList
-
如果在模块、接口、类、方法使用设计模式,在命名时要体现出具体模式。
阅读者可快速理解架构设计思想。
例如: public class OrderFactory;
public class LoginProxy;
public class ResourceObserver;
-
接口类方法、属性不要加任何修饰符号(public 也不要加),保持代码的简介性,并加上有效的Javadoc注解。尽量不要在接口定义常量。必须定义的话,最好确定该常量与接口的方法相关,并且是整个应用的基础常量。
正例: void commit();
String COMPANY = “alibaba”.
JDK8 中允许接口有默认实现,default方法是对所有实现类都有价值的默认实现。
-
接口与实现类命名有两套规则:
(1) 对于Service 和 DAO类, 基于SOA的理念,暴露出来服务一定是接口,内部的实现类用Impl的后缀与接口区别。
例如: CacheServiceImpl 实现 CacheService接口。
(2) 如果是形容能力的接口名称,取对应的形容词为接口名(通常是 -able结尾的形容词).
例如: Abstract Translator 实现 Translatable.
-
枚举类名带上Enum后缀,枚举成员名称需要全大写,单词间用下划线隔开。
枚举就是特殊的常量类,且构造方法被默认强制是私有。
比如: ProcessStatusEnum成员名称: SUCCESS/UNKNOWN_REASON
-
命名规约:
Service / DAO 层方法命名规约:
- 获取单个对象方法get 做前缀。
- 获取多个对象方法用list 做前缀,复数结尾,如: listObjects
- 获取统计值方法用count 做前缀。
- 插入的方法用save / insert做前缀。
- 删除方法用 remove / delete做前缀。
- 修改方法用update 做前缀。
领域模型命名规约:
- 数据对象: xxxDO, xxx即为数据表名。
- 数据传输对象: xxxDTO, xxx为业务领域相关的名称。
- 展示对象: xxxVO, xxx一般为网页名称。
- POJO是 DO/DTO/BO/VO的统称,禁止命名成 xxxPOJO。
2. 常量定义
-
不允许任何魔法值(未经预先定义的常量)直接出现在代码中。需要在常量块里定义
-
long 或 Long赋值时,数值后使用大写L,不能是小写l,小写容易与数字混淆,造成误解。
-
浮点类数据后缀统一为大写的D或F。
-
不要使用一个常量类维护所有常量,要按照常量功能进行归类,分开维护。
比如缓存相关常量放在CacheConsts下,系统配置相关常量放在类SystemConfigConsts下。
-
常量的复用层次有5层: 跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。
-
跨应用共享常量: 放置在二方库中,通常是client.jar 中的constant目录下
-
应用内共享常量: 防止在一方库中,通常是子模块中的constant目录下。
-
子工程内部共享常量: 即在当前子工程的constant目录下。
-
包内共享常量: 即在当前包下单独的constant
-
类内共享常量: 直接在类内部private static final 定义
-
-
如果变量值仅在一个固定范围内变化用enum类型来定义。
如果存在名称之外的延伸属性应使用enum类型
3. 代码格式
-
如果大括号内为空,简洁地写成{}即可,大括号中间无需换行和空格;如果是非空代码快,则:
-
左大括号前不换行。
-
左大括号后换行。
-
右大括号前换行。
-
右大括号后还有else等代码不换行;表示终止的右大括号后必须换行。
-
-
左小括号和右边相邻字符之间不需要空格;右小括号和左边相邻字符之间也不需要空格;而左大括号前需要加空格。
-
if / for / while / switch / do 等保留字与左右括号之间都必须加空格。
-
任何二目,三目运算符的左右两边都需要加一个空格
包括 =、&& 、 + - * / 都需要!
-
采用4个空格缩进,禁止使用Tab字符!!!
如必须使用Tab缩进,必须设置1个Tab为4个空格。IDEA设置时请勿勾选Use tab character!!
-
注解的双斜线与注释内容之间有且仅有一个空格。
// 这个形式哦!
-
在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开!
-
单行字符限制不超过120个,超出需要换行,换行有以下原则:
- 第二行相对第一行缩进4个空格,第三行开始不再继续缩进
- 运算符与下文一起换行。
- 方法调用的点符号与下文一起换行。
- 方法调用中的多个参数需要换行时,在逗号后进行。
- 括号前不要换行!!!
-
方法参数在定义和传入时,多个参数逗号后面必须加空格。
-
IDE text file encoding必须设置为UTF-8, IDE文件换行符用Unix格式,不要使用Windows格式。
-
单个方法的总行数不超过80行!!!
-
等号不需要完全与上文对齐。
-
任何情形,没有必要插入多个空行进行分隔。
4. OOP规约 (26条需熟记!)
-
避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名访问即可。
In other words, for class level methods or variables, use class itself would suffice.
-
所有覆写方法,必须加@Override进行注解。
说明: 因为加@Override可以准确判断是否覆写成功,并且如果在abstract类中对方法签名修改,实现类会立刻compile error.
-
相同参数类型,相同业务含义,才可以使用的可变参数,类型应避免定义为Object。
可变参数必须放置在参数列表的最后。(不建议开发中使用可变参数编程)
-
外部正在调用的接口或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。接口过时必须添加**@Deprecated**注解,并清晰地说明采用的新接口或者新服务是什么。
-
不能使用过时的类或者方法。(必须查找到新的实现。)
-
Object的equals方法很容易抛nullpointer exception,应使用常量或者确定有值的对象来调用equals方法。
example: “test”.equals(param);
wrong: param.equals(“test”);
推荐使用JDK7 引入的工具类java.util.Objects#equals(Object a, Object b)
-
所有整型包装类对象之间值的比较,全部用equals方法比较。
在这里有一个大坑,对于Integer var = ? 在 -128 至 127之间的赋值,Integer对象是直接在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是区间之外的所有数据都会在heap上产生,并不会复用已有对象,因此== 在这个时候即使值相同也会return false,因此只有用equals方法判断是不会出错的。
-
任何货币金额,均以最小货币单位且为整型进行存储。
-
浮点数之间的等值判断,基本数据类型不能用==进行比较,包装数据类型不能用equals进行判断。
浮点数采用"尾数 + 阶码"编码方式,类似于科学计数法的"有效数字 + 指数"的表示方式。二进制无法精确表示大部分的十进制小数。
wrong example: 在这里都会return false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15float a = ...;
float b = ...;
if (a == b) {
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) {
}correct example:
(1) 指定误差范围,浮点数差值在此范围之内,则认为是相等的。
1
2
3
4
5
6
7
8
9float a = ...
float b = ...
float diff = ...
if (Math.abs(a - b) < diff) {
}(2) 使用BigDecimal来定义值,再进行浮点数的运算操作。
1
2
3
4
5
6
7
8
9
10
11
12
13BigDecimal 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) {
...} -
BigDecimal的等值比较应使用compareTo()方法,而不是equals()方法。
因为equals()方法会比较值与精度(1.0 与 1.00 返回结果为false) 而compareTo()则会忽略精度。
-
定义数据对象DO类时,属性类型要与数据库字段类型相匹配。
比如bigint 必须与Long对应。
-
禁止使用构造方法把BigDecimal(double)的方式把double值转化为BigDecimal对象。
因为这么做存在精度损失危险,再精确计算或值比较的场景中可能会导致业务逻辑异常。
-
关于基本数据类型与包装数据类型使用标准如下:
-
所有的POJO类属性必须使用包装数据类型。
-
RPC方法的返回值与参数必须使用包装数据类型。
-
所有的局部变量使用基本数据类型。
POJO类属性没有初始值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题或者入库检查,都由使用者来保证。包装数据类型的null可以表示额外的信息,而固定的初始化可能会导致错误被忽略。
-
-
定义DO / PO / DTO / VO 等 POJO类时, 不要设定任何默认值
-
序列化类新增属性时,不要修改serialVersionUID 字段,避免反序列失败,如果完全不兼容升级,避免反序列化混乱,那么修改serialVersionUID。
serialVersionUID不一致会抛出序列化运行时异常!!
-
构造方法内禁止加入任何业务逻辑,如果有初始化逻辑,请放在init中。
-
POJO类必须写toString方法。使用IDE工具 source>generate toString时,如果继承了另一个POJO类,注意在前面加一下super.toString().
-
禁止在POJO类中,同时存在对应属性xxx的isXxx()和getXxx()方法。
-
使用索引访问用String的split方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有IndexOutOfBoundsException的危险。
-
当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起, 便于阅读,此条规则优先于下一条。
-
类内方法定义的顺序依次是: 公有方法或保护方法>私有方法>getter>setter方法
-
setter方法中,参数名称与类成员变量名称要一致,this.成员名=参数名。在getter/setter方法中,不要增加业务逻辑,增加排查问题的难度。
-
循环体内,字符串的连接方式,使用StringBuilder的append方法进行扩展。不要使用 str += 方法!
-
final可以声明类、成员变量、方法、以及本地变量,下列情况使用final关键字:
-
不允许被继承的类,如:String 类
-
不允许被修改引用的域对象,如: POJO类的域变量。
-
不允许被覆写的方法,如: POJO类的setter方法。
-
不允许运行过程中重新赋值的局部变量
-
避免上下问重复使用一个变量,使用final关键字可以强制重新定义一个变量,方便更好地进行重构。
-
-
慎用Object的clone方法来拷贝对象。
对象clone方法默认是浅拷贝,若想实现深拷贝需要覆写clone方法是仙女域对象的深度遍历式拷贝。
-
类成员与方法访问控制从严:
- 如果不允许外部直接通过new来创建对象,那么构造方法必须是private.
2)工具类不允许有public 或 default构造方法。
3)类非static成员变量并且与子类共享,必须是protected。
4)类非static成员变量并且仅在本类使用,必须是private。
5)类static成员变量如果仅在本类使用,必须是private。
6)若是static成员变量,考虑是否为final。
7)类成员方法只供类内部调用,必须是private。
8)类成员方法只对继承类公开,那么限制为protected。
如果变量作用域太大,无限制到处跑,不利于模块解耦。
5. 日期时间
-
日期格式化时,传入pattern中表示年份统一使用小写的y。
说明: 日期格式化时,yyyy表示当天所在的年,而大写的YYYY代表是week in which year。意思就是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的YYYY就是下一年。
new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”)
一个很恐怖的例子就是: 如果我们使用 YYYY/MM/dd进行日期格式化,比如2017/12/31的执行结果就变成了2018/12/31,导致故障了。
-
在日期格式中必须分清楚大写的M和小写的m,大写的H和小写的h分别代表什么.
1)表示月份是大写的M
2)表示分钟是小写的m
3)24小时制是大写的H
4)12小时制是消息的h
-
获取当前毫秒数: System.currentTimeMillis();而不是 new Date.getTime().
获取纳秒级,则使用System.nanoTime方式。在JDK8中,针对统计时间等场景,推荐使用Instant类。
-
不允许在程序中任何地方使用 1) java.sql.Date 2) java.sql.Time 3) java.sql.Timestamp.
-
禁止在程序中写死一年为365天,避免在公历闰年时出现日期转换错误或程序逻辑错误。
int dateOfThisYear = LocalDate.now().lengthOfYear();
LocalDate.of(2011, 1, 1).lengthOfYear();
-
避免公历闰年2月问题,闰年的2月份有29天,一年后的那一天不可能是2月29日。
-
使用枚举值来指代月份。如果使用数字,注意Date,Calendar等日期相关类的月份month取值范围从0到11之间。
比如: Calendar.JANUARY Calendar.FEBRUARY Calendar.MARCH来指代相应月份来进行传参或者比较。
6. 集合处理
-
关于hashCode和equals的处理,遵循如下规则:
-
只要覆写equals, 就必须覆写hashCode。
-
因为Set储存是不重复的对象,依据hashCode和equals进行判断,所以Set存储的对象必须覆写这两种方法。
-
如果自定义对象作为Map的键,那么必须覆写hashCode和equals
String因为重写了hashCode和equals方法,所以可以直接使用。
-
-
判断所有集合内部元素是否为空,使用isEmpty()方法。因为时间复杂度为O(1).
-
在使用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()空集合对象,调用方一旦在返回的集合中进行了添加元素的操作,就会触发异常。
-
在subList场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生ConcurrentModificationException异常。
-
使用集合转数组的方法,必须使用集合的toArray(T[] array), 传入的是类型完全一致,长度为0的空数组。
直接使用toArray无参方法存在问题,此方法返回类只能是Object[]类。
原因:
- 等于0,动态创建与size相同的数组,性能最好
- 大于0但小于size,重新创建大小等于size的数组,增加GC负担。
- 等于size,高并发情况下,数组创建完成后,size正在变大的情况下,负面影响与2相同
- 大于size,空间浪费,且在size处插入null值,存在NPE隐患
-
使用Collection接口实现任何类的addAll()方法时,要对输入的集合参数进行NPE判断。
因为addAll方法的第一行代码即Object[] a = c.toArray(),如果c是null,直接抛出异常。
-
使用Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add / remove/ clear方法会抛出 UnsupportedOperationException异常。
-
泛型通配符<? extends T> 来接受返回的数据,此写法的泛型集合不能使用add方法。而<? super T> 不能使用get方法,两者在接口调用赋值的场景中容易出错。
CS2030S中的PECS(producer extends consumer super),即频繁往外读取内容的,适合用<? extends T>,经常往里插入的,适合用<? super T>
-
在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof判断,避免抛出ClassCastException异常。
JDK5后才有泛型,因为向前兼容,编译器本身允许非泛型集合与泛型集合互相赋值。
-
不要再foreach的循环里进行元素的remove / add操作。remove元素使用iterator,如果并发操作,需要对iterator对象加锁。
-
在JDK7版本及以上,Comparator实现类要满足如下3个条件,否则Arrays.sort, Collections.sort会抛出IllegalArgumentException异常。
3个条件:
-
x, y 的比较结果和 y, x的比较结果相反。
-
x > y, y > z, 则 x > z
-
x = y, 则x, z 比较结果和y, z比较结果相同。
如果不满足这些要求,实际使用中可能会出现异常。
-
-
泛型集合使用时,JDK7及以上,使用dimond语法或者全省略
diamond: <>, 省略: 啥都不写。
-
集合初始化时,指定集合初始化大小。
HashMap默认值16,如果没有设置容量初始大小,随着元素增加而被迫不断扩容,resize()方法总共会调用8此,反复重建哈希表和数据迁移。当放置的集合元素个数达千万级时会严重影响程序性能。
-
使用entrySet 遍历Map类集合KV,而不是keySet方式。
entrySet仅遍历一次就把key,value放到了entry中,更高效。如果JDK9,使用Map.forEach方法。
-
高度注意Map类集合K/V能不能存储null值的情况,如下表格:
ConcurrentHashMap key value 都不可以为null!!
-
集合具有有序性(sort), 稳定性(order), 避免无序性(unsort),和不稳定性(unorder)的负面影响。
有序性是指遍历的结果按某种比较规则依次排列的,稳定性指集合每次遍历的元素次序是一定的。
几个例子: ArrayList-> order/unsort, HashMap->unorder/unsort; TreeSet->order/sort.
-
利用Set元素唯一的特性,可以快速对一个集合进行去重操作, 避免使用List的contains()进行遍历去重或者判断包含操作。
7.并发处理
-
获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
资源驱动类,工具类,单例工厂类都需要注意。
-
创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
自定义线程工厂,并且根据外部特征进行分组,比如这个例子:
-
线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
线程池好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销, 解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
-
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor方式,这样的处理方式能让人更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors返回的线程池对象弊端如下:
- FixedThreadPool和SingleThreadPool:
允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM错误。
- CachedThreadPool:
允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM.
- ScheduledThreadPool
允许的请求队列长度为Integer.MAX_VALUE, 可能会堆积大量的请求,从而导致OOM.
-
SimpleDateFormat是非线程安全的类,一般不要定义为static变量,如果要这么做,必须加锁或者使用DateUtils工具类。
如果是JDK8的应用,可以用Instant代替Date, LocalDateTime代替Calendar, DateTimeFormatter代替SimpleDateFormat,因为这是:simple beautiful strong immutable thread-safe
-
必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally进行回收.
1
2
3
4
5
6objectThreadLocal.set(userInfo);
try{
//...
} finally {
objectThreadLocal.remove();
} -
高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁; 能锁block 就不要锁整个method;, 能用对象锁,就不要用类锁。
尽可能使加锁的代码块工作量尽可能小,避免在锁代码中调用RPC方法。
-
对多个资源、数据库表、对象同时加锁时,需要保持一直的加锁顺序,否则可能会造成deadlock.
比如thread 1 对表A,B,C一次全部加锁才可以更新,那么thread 2 也得保持这个顺序,否则很可能会deadlock。
-
在使用阻塞等待获取锁方式中,必须在try代码块之外,并且在加锁方法与try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally中无法解锁
-
在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
因为unlock在执行前,会调用AQS的tryRelease方法,如果未持有锁,则抛出IllegalMonitorStateException异常。
-
并发修改同意记录时,避免信息丢失,需要加锁,要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version作为更新依据。
如果每次访问冲突概率小于20%,推荐使用乐观锁,否则使用悲观锁。乐观锁重试次数不得小于3次。
-
多线程并行处理定时任务时,Timer运行多个TimeTask时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService则没有这个问题。
-
资金相关敏感信息,使用悲观锁策略。
-
使用CountDownLatch进行异步转同步操作,每个线程推出前必须调用countDown方法,线程执行代码注意catch异常,确保countDown方法被执行到,避免主线程无法执行至await方法,直到超时才返回结果。
说明: 注意,子线程抛出异常stack,不能 在主线程try-catch到。
-
避免Random实例被多线程使用,虽然共享实例是线程安全的,但会因为竞争同一seed导致性能下降。
Random实例包括java.util.Random的实例或者Math.random()的方式。
-
通过double-checked locking, 实现延迟初始化需要将目标属性声明为volatile型,
-
volatile解决多线程内存不可见问题对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。
如果是: count++操作,使用如下类实现:
1
2AtomicInteger count = new AtomicInteger();
count.addAndGet(1);如果是JDK8, 推荐使用LongAdder对象,比AtomicLong性能好(减少乐观锁的重试次数)
-
HashMap 在容量不够进行resize时由于高并发可能出现死链,导致CPU飙升,在开发过程中注意规避此风险。
-
ThreadLocal对象使用static修饰,ThreadLocal无法解决共享对象的更新问题。
说明: 这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象都可以操控这个变量。
8.控制语句
-
在一个switch块内,每个case 要么通过continue / break/ return等来终止,要么注释说明程序将继续执行哪一个case为止; 在一个switch块内,都必须包含一个default语句并且放在最后,即使他什么代码也没有。
注意break是推出switch 语句块,而return 是退出方法体。
-
当switch括号内变量类型为String并且此变量为外部参数时,必须先进行null判断.
-
if/ else / for/ while/ do语句中必须使用大括号。
即使有一行代码,也要采用大括号编码方式
-
三目运算符condition尽量避免使用,会降低性能以及减少代码的可读性。
-
高并发场景中,避免使用"等于"判断作为中断或推出的条件。
如果并发控制没有处理好,容易产生等值判断被"击穿"的情况,使用大于或小于的区间判断条件来代替。
常见的场景有,判断剩余奖品数量 = 0时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。
-
当方法的代码行数超过10行时,return / throw等中断逻辑的右大括号需要加一个空行。
这样做逻辑清晰,有利于代码阅读时重点关注。
-
表达异常分支时,减少使用if-else, 这种方式可以改写成:
if (condition) {
…
return obj;
}
可以用defensive code来实现。
-
除常用方法(getXxx / isXxx)等外不要在条吉件判断中执行其他复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的bool 变量,以提高可读性。
-
不要在其他表达式(尤其是条件表达式)插入赋值语句。
-
循环体中语句要考量性能,以下操作:
定义对象、变量。获取数据库链接,进行不必要的try-catch操作都可以移到循环体外处理
-
避免采用 "!"逻辑运算符。
-
公开接口需要入参保护,尤其是批量操作的接口。比如最多查询的数量限制,必须保护。否则会出现各种问题,如内存爆炸。
-
参数校验的情形:
1)调用频次低的方法
2)执行开销很大的方法,参数校验时间复杂度很低,如果参数错误roll back的话会得不偿失
3)需要极高稳定性和可用性的方法
4)对外提供的开放接口,不管是RPC/API/HTTP接口
5)敏感权限入口
-
不需要进行参数校验:
-
极有可能被循环调用的方法。但必须注明外部参数检查
-
底层调用频率比较高的方法。比如DAO层的,可以省略。
-
被声明成private 只会被自己代码调用的方法,如果能确定调用方法的代码传入参数已经做过检查或者肯定不会出问题,此时可以不校验参数。
-
9. 注释规约
-
类、类属性、类方法注释必须遵循Javadoc规范,使用 /** 内容 **/格式,其他的一律不允许。
-
所有abstract methods包括 interface的必须要用Javadoc注释,除了返回值、参数异常说明外,还必须指出该方法做了什么事情,实现了什么功能。
-
所有的类必须添加创建者和创建日期。
-
方法内部单行注释,在被注释语句上方另起一行,使用 // 注释,方法内部多行注释使用 /* */注释,注意和代码对齐。
-
所有枚举类型字段必须要有注释,说明每个数据项的用途
-
注解要表达清楚
-
代码修改,注释也要跟着修改。
-
在类中删除未使用的任何字段和方法、内部类; 在方法中删除未使用的参数声明和内部变量。
-
谨慎注释代码,如果没有详细的用途,请删除。
-
对于注释的要求: 1.能准确反应设计思想和代码逻辑; 2.能描述业务含义,使其他人能立刻了解到代码背后的信息。
-
好的命名、代码结构是自解释的,注释力求精简准确,表达到位。避免出现注释的另一个极端: 过多过滥的注释,代码的逻辑一旦修改,修改注释又是相当大的负担。
-
特殊注释标记,请注明标记人和标记时间。注意及时处理标记,通过标记扫描,经常清理此类标记。线上故障很可能就来源于这些标记。
10. 前后端规约
-
前后端交互的API, 需要明确协议、域名、路径、请求方法、请求内容、状态码、响应体。
-
协议: 生产环境必须用HTTPS
-
路径: 每一个API需对应一个路径,表示API具体的请求地址:
a) 代表一种资源,只能为名词,推荐使用复数,不能为动词,请求方法已经表达动作意义。
b) URL路径不能大写,单词如果需要分割,统一使用下划线。
c) 路径禁止携带表示请求内容类型的后缀,比如".json", “.xml”,通过accept头表达即可。
- 请求方法: 对具体操作的定义,常见的请求方法如下:
a) GET: 从服务器中去除资源
b) POST: 在服务器中新建一个资源
c) PUT: 在服务器中更新资源
d) DELETE:在服务器中删除资源
-
请求内容: URL带的参数必须无敏感信息或符合安全要求,body带参数时必须设置Content-Type.
-
响应体: 响应体body可放置多种数据类型,由Content-Type头来确定。
-
-
前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}.
这样可以有利于数据层面上协作更高效,减少前端很多琐碎的null判断。
-
服务端产生错误时,返回给前端的相应信息必须包含HTTP状态, errorCode, errorMessage、用户提示信息4个部分、
涉众对象分别是: 浏览器、前端开发、错误排查人员、用户。其中输出给用户的提示要求: 尖端清晰,友好,引导用户进行下一步操作或解释错误原因,提示信息可以包括错误原因、上下文环境、推荐操作等。
errorCode: 错误码,有专门的后端错误码表。errorMessage: 简要描述后端出错原因、便于错误排查人员快速定位问题、不能包含敏感数据信息。
-
前后端交互JSON格式数据中,所有key必须为小写字母开始的lowerCamelCase风格,符合英文表达习惯,且表意完整。
-
errorMessage时前后端错误追踪机制的体现,可以在前端输出到type="hidden"文字类控制文件中,或者用户端的日志中,帮助我们快速定位出问题。
-
对于需要使用超大整数的场景,服务端一律使用String字符串类型返回,禁止使用Long类型。
说明:Java服务端如果直接返回Long整型数据给前端,Javascript会自动转换为Number类型(注:此类型为双精度浮点数,表示原理与取值范围等同于Java中的Double)。Long类型能表示的最大值是263-1,在取值范围之内,超过253(9007199254740992)的数值转化为Javascript的Number时,有些数值会产生精度损失。扩展说明,在Long取值范围内,任何2的指数次的整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,双精度浮点数的尾数位只有52位。
反例:通常在订单号或交易号大于等于16位,大概率会出现前后端订单数据不一致的情况。比如,后端传输的"orderId":362909601374617692,前端拿到的值却是:362909601374617660
-
HTTP通过URL传递参数时,不能超过2048字节。
-
HTTP请求通过body传递内容时,必须控制长度,超出最大长度,后端解析会出错。
nginx默认限制是1MB,tomcat默认限制2MB,当确实有业务需要传较大内容时,可以调大服务器端的限制。
-
在翻页场景中,用户输入参数小于1,则前端返回第一页参数给后端; 后端发现用户输入参数大于总页数,直接返回最后一页。
-
服务器内部重定向必须forward; 外部重定向地址必须URL统一代理模块生产,否则会因线上采用HTTPS协议而导致浏览器"不安全",并且会拿来URL维护不一致的问题。
-
服务器返回信息必须被标记成是否可以缓存,如果缓存,客户端可能会重用之前的请求结果。
-
服务端返回的数据,使用JSON格式而非XML。
尽管http支持使用不同的输出格式,例如纯文本,JSON,CSV,XML,RSS甚至HTML。如果我们使用的面向用户的服务,应该选择JSON作为通信中使用的标准数据交换格式,包括请求和响应。此外,application/JSON是一种通用的MIME类型,具有实用、精简、易读的特点。
-
前后端时间统一为"yyyy-MM-dd HH:mm:ss",统一为GMT。
-
在接口路径中不要加入版本号,版本控制在HTTP头信息中体现,有利于向前兼容。