前戏
每个项目组都需要有自己的规范,大家遵守这套规范,防止每个人写的代码龙飞凤舞,
增加维护成本。在代码评审时,依赖这套规范,遇到需要制定成规范的地方,
在大家都同意的情况下,也可动态更新至规范中。
将走过的坑和弯路总结出来,让后面的人走捷径。
基于google java style 规范
基于去哪儿网db规范
基于东软的规范
基于阿里巴巴java开发手册
命名篇
通用命名规则
1.代码中的命名均不能以下划线或美元符号开始与结束。
2.代码中的命名严格禁止拼音和英文混合的方式,更不要使用中文的命名,纯拼音也要避免,
但例如alibaba,taobao等国际通用的名称可以。
3.杜绝完全不规范的缩写,避免望文不知义。
反例: AbstractClass “缩写”命名成 AbsClass;condition “缩写”命名成 condi ,
此类随意缩写严重降低了代码的可阅读性。
4.项目中严禁在不同的目录下散落相同名字的java类名
包命名
包名全部小写,连续的单词只是简单地连接起来,不使用下划线。
单词全部使用单数形式,如果包里的类名有复数含义,则可以使用复数形式。
com.公司名.项目名.模块名.xxx
类、接口、抽象类、构造函数命名
大驼峰式写法,以大写字母开头,如果有多个单词,每个单词头字母大写。
类名通常是名词或名词短语,接口名称有时可能是形容词或形容词短语。
例如:StudentInfo。
~驼峰式命名示例
Prose form Correct Incorrect
------------------------------------------------------------------
"XML HTTP request"
XmlHttpRequest XMLHTTPRequest
"new customer ID"
newCustomerId newCustomerID
"inner stopwatch"
innerStopwatch innerStopWatch
"supports IPv6 on iOS"
supportsIpv6OnIos supportsIPv6OnIOS
"YouTube importer"
YouTubeImporter
类命名详细规范
抽象类:Abstract开头
异常类:Exception结尾
测试类:Test结尾
控制器类:Controller结尾
逻辑类:Service结尾
持久层类:DAO结尾
接口实现类:Impl结尾(接口则不需要任何后缀),基于SOA的理念,暴露出来的服务一定是接口,多用于RPC服务
数据传输对象类:DTO结尾
数据对象类/实体类:表名(阿里:表名+DO)
展示对象类:VO结尾
工具类:Util结尾
枚举类:Enum结尾(成员变量全部大写,使用下划线连接单词)
常量类:Constant结尾
设计模式类:将设计模式体现在名字中,有利于阅读者快速理解架构设计思想,例如:OrderFactory,LoginProxy
方法、对象、变量、参数命名
小驼峰式写法,除首个单词首字母小写,其余单词首字母大写,剩余字母小写。
方法名通常是动词或动词短语。非常量字段名通常是名词或名词短语。
方法名的长度控制在30个字符以内。
不要使用set/get开头,容易和setter/getter方法混淆。
例如:nameExample
数组命名
中括号是数组类型的一部分,数组定义如下: String[] args;
请勿使用 String args[] 的方式来定义。
常量命名
所有字母大写,用下划线连接单词,
把意思表达清楚,不要嫌长,但也别特别长。
例如:NAME_EXAMPLE
DAO/Service层方法命名规则
新增:insertXXX
删除:deleteXXX
修改:updateXXX
新增or修改:saveXXX
查询单个对象:findOneXXX
查询分页对象:findPageXXX
查询统计值:findCountXXX
批量动作:batch动作XXX
变量&常量定义篇
不要使用类变量调用缓存,缓存变更后,类变量的值不会改变。
例如:public static final String PTF_GROUP_NUMBER = MemcachedUtil.getConfigValue("tmm_ptf_admingroup");
不要在一个代码块的开头把局部变量一次性都声明了
如果全都声明则会有浪费内存的风险,在第一次需要使用它时才声明,降低浪费率。
拒绝魔法数字,魔法值
即未经定义的常量,常量需要抽出来形成常量类。
反例:
String key ="
Id#taobao_"+ tradeId;
cache . put(key , value);
if(type==5){
}
long 或者Long初始赋值时,必须使用大写的 L
不能是小写的 l ,小写容易跟数字1 混淆,造成误解。
不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护
如:缓存相关的常量放在类: CacheConsts 下 ;
系统配置相关的常量放在类: ConfigConsts 下,易维护。
格式篇
必须使用eclipse中的pmd,checkstyle,findbug插件对代码进行检查
且需要引入alex java conventions2.0.xml,这个xml是我们以前写好的配置用eclipse导出来的。
只要引入这个xml,就会全自动format所有的格式。
引入方式:Window->Preference->Java->Code Style->Formatter->Import..
eclipse中设置自动格式化保存:java->editor->save actions->perform the selected actions on save
大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行
如果 是非空代码块则:
1)左大括号前不换行。
2)左大括号后换行。
3)右大括号前换行。
4)右大括号后还有else等代码则不换行;表示终止右大括号后必须换行。
左括号和后一个字符之间不出现空格;
同样,右括号和前一个字符之间也不出现空格
if / for / while / switch / do 等保留字与左右括号之间都必须加空格。
任何运算符左右必须加一个空格
运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号、三目运行符等。
缩进采用 4 个空格,禁止使用 tab 字符
如果使用 tab 缩进,必须设置 1 个 tab 为 4 个空格。因为tab在不同的环境下的配置不一样。
对于非空块和块状结构,大括号遵循Kernighan和Ritchie风格
if (condition()) {
try {
something();
} catch (ProblemException e) {
recover();
}
}
大括号与if, else, for, do, while语句一起使用
即使只有一条语句(或是空),也应该把大括号写上,这样让代码阅读起来更加有效。
单行字符数限制不超过 100 个,超出需要换行,换行时遵循如下原则
1.第二行相对第一行缩进 4 个空格,参考示例。
2.运算符与下文一起换行。
3.方法调用的点符号与下文一起换行。
4.在多个参数超长,逗号后进行换行。
5.在括号前不要换行,见反例。
正例:
StringBuffer sb = new StringBuffer();
//超过 100 个字符的情况下,换行缩进 4 个空格,并且方法前的点符号一起换行
sb.append("zi").append("xin")...
.append("huang")...
.append("huang")...
.append("huang");
反例:
StringBuffer sb = new StringBuffer();
//超过 100 个字符的情况下,不要在括号前换行
sb.append("zi").append("xin")...append
("huang");
//参数很多的方法调用可能超过 100 个字符,不要在逗号前换行
method(args1, args2, args3, ...
, argsX);
方法参数在定义和传入时,多个参数逗号后边必须加空格
正例:下例中实参的" a ",后边必须要有一个空格。
method("a", "b", "c");
不同的业务逻辑之间或者不同的语义之间插入一个空行
相同业务逻辑和语义之间不需要插入空行,没有必要插入多行空格进行隔开。
面向对象编程(OOP)篇
避免通过一个类的对象引用访问此类的静态变量或静态方法
无谓增加编译器解析成本,直接用类名来访问即可。
所有的覆写方法(从父类或接口中),必须加@Override注解
该注解可以帮你检查正确性,且看到这个注解就知道是覆盖方法。
反例: getObject() 与 get0bject() 的问题。一个是字母的 O ,一个是数字的 0,
加@Override可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,
其实现类会马上编译报错。
对外暴露的接口签名,原则上不允许修改方法签名
避免对接口调用方产生影响。接口过时必须加@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(a, b);(JDK 7 引入的工具类 )
所有的相同类型的包装类对象之间值的比较,全部使用 equals()比较
说明:对于 Integer var =?在-128 至 127 之间的赋值, Integer 对象是在
IntegerCache.cache产生,会复用已有对象, 这个区间内的Integer值可以直接使用==进行
判断, 但是这个区间之外的所有数据, 都会在堆上产生, 并不会复用已有对象, 这是一个大坑,
推荐使用 equals 方法进行判断。
Integer x = 55555;
Integer y = 55555;
System.out.println(x==y);//输出为false
关于基本数据类型与包装数据类型的使用标准如下:
1.所有的POJO类属性必须使用包装数据类型。
2.RPC方法的返回值和参数必须使用包装数据类型。
3.所有的局部变量【推荐】使用基本数据类型。
说明: POJO类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,
任何 NPE问题(空指针异常,NullPointerException),或者入库检查,都由使用者来保证。
正例:数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。
反例:比如显示成交总额涨跌情况,即正负x%,x为基本数据类型,调用的RPC服务,
调用不成功时,返回的是默认值,页面显示:0%,这是不合理的,应该显示成中划线-。
所以包装数据类型的null值,能够表示额外的信息,如:远程调用失败,异常退出。
定义DO/DTO/VO等POJO类时,不要设定任何属性默认值
反例: POJO类的gmtCreate默认值为new Date();
但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,
导致创建时间被修改成当前时间。
序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列失败
如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。
POJO类必须写toString()
使用 IDE 的中工具:source>generate toString 时,如果继承了另一个 POJO 类,
注意在前面加一下super.toString。
说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString() 方法打印其属性值,
便于排查问题,
必须使用带反射的toString()方法。
访问用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(true) {
return data + 100;
} else {
return 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.不需要重新赋值的变量,包括类属性、局部变量。
2.对象参数前加final,表示不允许修改引用的指向。
3.类方法确定不允许被重写。
慎用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 方法,
或者一 个 public 的成员变量,删除一下,不得手心冒点汗吗?
变量像自己的小孩,尽量在自己的视 线内,变量作用域太大,如果无限制的到处跑,
那么你会担心的。
集合篇
关于hashCode()和equals()的处理,遵循如下规则
1.只要重写 equals,就必须重写 hashCode 。
2.因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的
对象必须重写这两个方法。
3.如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals 。
正例: String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象
作为 key 来使用。
ArrayList的subList 结果不可强转成ArrayList
否则会抛出ClassCastException
异常: java.util.RandomAccessSubList cannot be cast to java.util.ArrayList ;
说明: subList 返回的是ArrayList 的内部类SubList ,并不是ArrayList ,而是
ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。
(
subList(x,y)方法类似于String的subString方法,从集合中截取从第x下标到y下标的数据形成一个子集合,
注意这个子集合里的变动会影响父集合,例如add/remove操作)
在subList场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均产生 ConcurrentModificationException异常。
使用集合转数组的方法,必须使用集合的 toArray(T[] array)
传入的是类型完全一样的数组,大小就是list.size() 。
反例:直接使用toArray 无参方法存在问题,此方法返回值只能是Object[] 类,
若强转其它类型数组将出现ClassCastException 错误。
正例:
List list = new ArrayList(2);
list.add("guan");
list.add("bao");
String[] array = new String[list.size()];
array = list.toArray(array);
Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法
它的add/remove/clear方法会抛出UnsupportedOperationException异常。
说明: asList的返回对象是一个Arrays内部类, 并没有实现集合的修改方法。
Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
String[] str = new String[] { "a", "b" };
List list = Arrays.asList(str);
第一种情况: list.add("c"); 运行时异常。
第二种情况: str[0]= "gujin"; 那么list.get(0)也会随之修改。
不要在foreach循环里进行元素的remove/add操作
会出异常。remove元素请使用 Iterator方式,如果并发操作,需要对 Iterator对象加锁。
反例:
List a = new ArrayList();
a.add("1");
a.add("2");
for (String temp : a) {
if("1".equals(temp)){
a.remove(temp);
}
}
正例:
Iterator it = a.iterator();
while(it.hasNext()){
String temp = it.next();
if(删除元素的条件){
it.remove();
}
}
集合初始化时,尽量指定集合初始值大小
说明: ArrayList 尽量使用 ArrayList(int initialCapacity) 初始化。
例子:
List arrayList = new ArrayList();
如果像上面这样使用默认的构造方法,初始容量被设置为10。当ArrayList中的元素超过10个以后,
会重新分配内存空间,使数组的大小增长到16。
可以通过调试看到动态增长的数量变化:10->16->25->38->58->88->...
也可以使用下面的方式进行声明:
List arrayList = new ArrayList(4);
将ArrayList的默认容量设置为4。当ArrayList中的元素超过4个以后,会重新分配内存空间,使数组的大小增长到7。
可以通过调试看到动态增长的数量变化:4->7->11->17->26->...
那么容量变化的规则是什么呢?请看下面的公式:
((旧容量 * 3) / 2) + 1
一旦容量发生变化,就要带来额外的内存开销,和时间上的开销。
所以,在已经知道容量大小的情况下, 需要指定默认容量大小的方式。
~使用 entrySet 遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历。
说明: keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出
key 所对应的 value 。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效
率更高。如果是 JDK 8,使用 Map.foreach 方法。
正例: values() 返回的是 V 值集合,是一个 list 集合对象 ;keySet() 返回的是 K 值集合,是
一个 Set 集合对象 ;entrySet() 返回的是 K - V 值组合集合。
Map m = new HashMap();
m.put("a", "1");
m.put("b", "2");
m.put("c", "3");
for(Entry e : m.entrySet()) {
System.out.println(e.getKey());
System.out.println(e.getValue());
}
~高度注意 Map 类集合 K / V 能不能存储 null 值的情况,如下表格:
集合类
|
Key
|
Value
|
Super
|
说明
|
Hashtable
|
不允许为 null
|
不允许为 null
|
Dictionary
|
线程安全
|
ConcurrentHashMap
|
不允许为 null
|
不允许为 null
|
AbstractMap
|
分段锁技术
|
TreeMap
|
不允许为 null
|
允许为 null
|
AbstractMap
|
线程不安全
|
HashMap
|
允许为 null
|
允许为 null
|
AbstractMap
|
线程不安全
|
反例: 由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,注意存储
null 值时会抛出 NPE 异常。
合理利用好集合的有序性 (sort) 和稳定性 (order)
避免集合的无序性 (unsort) 和不稳定性 (unorder) 带来的负面影响。
说明:稳定性指集合每次遍历的元素次序是一定的。有序性是指遍历的结果是按某种比较规则
依次排列的。如: ArrayList 是 order / unsort;HashMap 是 unorder / unsort;TreeSet 是
order / sort 。
利用 Set 元素唯一的特性
可以快速对一个集合进行去重操作,避免使用 List 的contains 方法进行遍历、对比、去重操作。
控制语句篇
~在一个 switch 块内,每个 case 要么通过 break / return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止 ;
在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
~在 if / else / for / while / do 语句中必须使用大括号,即使只有一行代码,避免使用
下面的形式: if (condition) statements;
~在逻辑判断中尽量使用卫语句:
if(condition){
...
return obj;
}
// 接着写 else 的业务逻辑代码;
卫语句:打破“单一出口”规则,代码结构清晰更重要,if判断成功后直接就抛异常或方法返回。
如果某个条件极其罕见,就应该单独检查该条件,并在该条件为真时立刻从函数中返回。
这样的单独检查常常被称为“卫语句”。
或者状态设计模式:把状态形成一个接口或抽象类,不同的状态都是其子类或实现类,在子类中实现业务逻辑,
并可以方便的加入新状态,只需要改变对象的状态便可改变对象的行为,缺点是实现方式较复杂。
~除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复
杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
说明:很多 if 语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能明确什么
样的条件执行什么样的语句,那么,如果阅读者分析逻辑表达式错误呢?
正例:
//伪代码如下
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 接口。
5.敏感权限入口。
~方法中不需要参数校验的场景:
1.极有可能被循环调用的方法,不建议对参数进行校验。但在方法说明里必须注明外部参
数检查。
2.底层的方法调用频度都比较高,一般不校验。毕竟是像纯净水过滤的最后一道,参数错
误不太可能到底层才会暴露问题。一般 DAO 层与 Service 层都在同一个应用中,部署在同一
台服务器中,所以 DAO 的参数校验,可以省略。
3.被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参
数已经做过检查或者肯定不会有问题,此时可以不校验参数。
注释篇
~必须使用eclipse引入alex codetemplates2.0.xml,引入方式:Window->Preference->Java->Code Style->Code Template->Import..
只要引入这个xml,就会全自动format所有的格式。
~类、成员变量、方法(setter/getter方法除外)的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用 // xxx 方式。
说明:在 IDE 编辑窗口中, Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释 ;
在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
~所有的抽象方法 (包括接口中的方法) 必须要用 Javadoc 注释、除了返回值、参数、
异常说明外,还必须指出该方法做什么事情,实现什么功能。
说明:对子类的实现要求,或者调用注意事项,请一并说明。
~所有的类与方法都必须添加创建者信息以及创建时间。
~方法内部单行注释,在被注释语句上方另起一行,使用//注释。
方法内部多行注释使用/* */注释,注意与代码对齐。
~所有的枚举类型字段必须要有注释,说明每个数据项的用途。
~与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。
反例:“ TCP 连接超时”解释成“传输控制协议连接超时”,理解反而费脑筋。
~代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。
说明: 代码与注释更新不同步, 就像路网与导航软件更新不同步一样, 如果导航软件严重滞后,就失去了导航的意义。
~注释掉的代码尽量要配合说明,而不是简单的注释掉。
说明:代码被注释掉有两种可能性:
1.后续会恢复此段代码逻辑。
2.永久不用。
前者如果没 有备注信息,难以知晓注释动机。后者建议直接删掉 ( 代码仓库保存了历史代码 ) 。
~对于注释的要求:
1.能够准确反应设计思想和代码逻辑 ;
2.能够描述业务含 义,使别的程序员能够迅速了解到代码背后的信息。
完全没有注释的大段代码对于阅读者形同 天书,注释是给自己看的,即使隔很长时间,
也能清晰理解当时的思路; 注释也是给继任者看 的,使其能够快速接替自己的工作。
~好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的
一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。
反例:
// put elephant into fridge
put(elephant, fridge);
方法名 put ,加上两个有意义的变量名 elephant 和 fridge ,已经说明了这是在干什么,
语义清晰的代码不需要额外的注释。
~特殊注释标记, 请注明标记人与标记时间。 注意及时处理这些标记, 通过标记扫描,
经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
1.待办事宜 (
//TODO) : ( 标记人,标记时间, [ 预计处理时间 ])
表示需要实现, 但目前还未实现的功能。 这实际上是一个 Javadoc 的标签, 目前的 Javadoc
还没有实现, 但已经被广泛使用。 只能应用于类, 接口和方法 ( 因为它是一个 Javadoc 标签 ) 。
2. 错误,不能工作 (
//FIXME) : ( 标记人,标记时间, [ 预计处理时间 ])
在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。
~修改代码
若代码的作者不是当前人,那么当前人需要加入注释声明该代码由本人改动过。
/**
*修改了变量的值 modified by alex.yang 2016年4月2日12:25:56
*/
int n = 4;
~非java文件注释
// 这里是注释
异常篇
~不要捕获 Java 类库中定义的继承自RuntimeException的运行时异常类,如:
IndexOutOfBoundsException / NullPointerException,这类异常由程序员预检查来规避,保证程序健壮性。
正例: if(obj != null) {...}
反例: try { obj.method() } catch(NullPointerException e){...}
~异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低。
大家要养成一种sence,就是抛异常是一件非常耗内存的事情,并且内存是一个有限的资源。
~对大段代码进行 try - catch ,这是不负责任的表现。 catch时请分清稳定代码和非稳定代码,
稳定代码指的是无论如何不会出错的代码。 对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
~捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。
最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
~有try块放到了事务代码中, catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
如果是作为spring的aop事务回滚,在默认情况下,如果想回滚请默认请抛出RuntimeException。
~finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。
说明:如果 JDK 7,可以使用try-with-resources方式。
~不能在 finally 块中使用return , finally 块中的 return 返回后方法结束执行,不
会再执行 try 块中的 return 语句。(面试题)
~捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
~方法的返回值可以为null ,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。
调用方需要进行null判断防止 NPE 问题。
说明:
本规约明确防止NPE是调用者的责任。即使你能确定被调用方法返回的不是null,也并非高枕无忧,
必须考虑到远程调用失败,运行时异常等场景返回 null 的情况。
~防止 NPE ,是程序员的基本修养,注意 NPE 产生的场景:
1.返回类型为包装数据类型,有可能是 null ,返回 int 值时注意判空。
反例: public int f() { return Integer 对象}; 如果为 null ,自动解箱抛NPE 。
2.数据库的查询结果可能为null 。
3.集合里的元素即使isNotEmpty,取出的数据元素也可能为null 。
4.远程调用返回对象,一律要求进行NPE判断。
5.对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
6.级联调用 obj.getA().getB().getC();一连串调用,易产生NPE。
~在代码中使用“抛异常”还是“返回错误码”,对于公司外的 http / api 开放接口必须使用“错误码”;
而应用内部推荐异常抛出 ; 跨应用间RPC调用优先考虑使用Result方式,封装 isSuccess 、“错误码”、“错误简短信息”。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
1.使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
2.如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message ,对于调用
端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输
的性能损耗也是问题。
~定义时区分 unchecked / checked 异常,避免直接使用 RuntimeException 抛出,
更不允许抛出 Exception 或者 Throwable , 应使用有业务含义的自定义异常。
推荐业界已定义过的自定义异常,如: DAOException / ServiceException 等。
~避免出现重复的代码 (Don't Repeat Yourself) ,即DRY原则。
说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,
容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是共用模块。
正例: 一个类中有多个 public 方法, 都需要进行数行相同的参数校验操作, 这个时候请抽取:
private boolean checkParam(DTO dto){...}
~抛异常后,堆栈信息需要打出来,不然看log根本不知道有过异常。
日志篇
~应用中不可直接使用日志系统 (Log4j , Logback) 中的 API ,而应依赖使用日志框架
SLF4J中的API ,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
~日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。
生产环境中需要有定时任务自动删除原生log,原生log会被分布式日志框架抓取放到云端。
~应用中的扩展日志(如打点、临时监控、访问日志等 ) 命名方式:
appName_logType_logName.log 。
logType :日志类型,推荐分类有stats / desc / monitor / visit 等;
logName :日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,
什么类型,什么目的,也有利于归类查找。
正例: mppserver 应用中单独监控时区转换异常,如:
mppserver_monitor_timeZoneConvert.log
说明:推荐对日志进行分类,错误日志和业务日志尽量分开存放,便于开发人员查看,
也便于通过日志对系统进行及时监控。
~对 trace / debug / info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
说明: logger.debug( "Processing trade with id : " + id + " symbol : " + symbol);
如果日志级别是warn,上述日志不会打印, 但是会执行字符串拼接操作, 如果 symbol 是对象,
会执行 toString() 方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
正例: ( 条件 )
if (logger.isDebugEnabled()) {
logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
}
正例: ( 占位符 )
logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);
~避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity = false 。
正例:
~异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么往上抛。
正例: logger.error(各类参数或者对象 toString + "_" + e.getMessage(), e);
~可以使用warn日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。
注意日志输出的级别, error 级别只记录系统逻辑出错、异常等重要的错误信息。如非必要,
请不要在此场景打出 error 级别。
~谨慎地记录日志。生产环境禁止输出 debug 日志 ; 有选择地输出 info 日志 ; 如果使
用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,
并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。
记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
mysql篇
命名前缀
t_ 表
v_ 视图
tre_ 关联表
f_ 函数
p_ 存储过程
tmp_ 临时库/表,并以日期(年月日)作为后缀
bak_ 备份库/表,并以日期(年月日)作为后缀
inx_ 索引(格式为:inx_表名_字段名)
命名规则
库名,表名,字段名,
全部小写,使用下划线连接。
库名,表名,字段名禁止使用mysql保留字。
所有sql关键词全部大写,比如SELECT,UPDATE,FROM,ORDER,BY等。
通常用作逻辑外键命名:表名+"_id"
如果两张表有同样意义的字段,请根据表名作为前缀作为区分,例如:
t_teacher表中的teacher_phone,与t_user表中的user_phone.
基础表都需要如下列
key_id char 32 not null primary_key 主键(baseDao自动生成UUID)
update_date timestamp 最后更新日期(baseDao自动更新时间,无需手动)
create_date datetime CURRENT_TIMESTAMP 创建日期 (baseDao自动更新时间,无需手动)
create_user_id char 32 创建用户ID
update_user_id char 32 更新用户ID
is_used tinyint 1 是否使用(默认值为1,1是使用,0为不使用,优点为出现在需要找被删除的旧数据的时候,绝对可以找回来。)
库表设计
~不在数据库中参与业务的计算(存储过程,函数,触发器,外键, 视图),是保证数据库运行稳定的一个好的最佳实践。(阿里DBA)。让数据库做最擅长的事(去哪儿DBA)
~所有表全部使用InnoDB存储引擎
~所有字符集全部使用utf-8
~做DB设计时,需要有ER图
~表结构变更须由库表所属团队发起
~所有在测试环境执行的表结构变更的SQL必须经过DBA review
~单表数据量控制在5000w以内,要有预见性
字段设计
~经常查询的地方需要做一些冗余字段,不用符合第三范式。
~尽可能小的设置字段长度(性能提高,节省空间)
~所有表都不要使用物理外键(因为外键会造成表之间的耦合关系,即使有级联更新和删除,也会造成性能上的极大消耗)
~所有表和字段都需要有中文注释
~使用UNSIGNED存储非负整数
~所有字段设置为not null(执行查询时,数据库不用匹配null值,提高性能),字符类型的默认值为'',数值类型默认值为0,数值类型的字段请使用UNSIGNED属性,日期类型的默认值为'1970-01-01 00:00:00.0'
~所有的布尔值字段,如is_hot、is_student,都必须设置一个默认值,tinyint(1),0为“否”,1为“是”
(性别:1为男,0为女)
~将enum类型转换成tinyint
~每张表必须要有主键
~不要在数据库中存储图片,文件等大数据,不要存TEXT,BLOB等。
~禁止使用分区表
~拆分大字段和访问频率低的字段,分离热冷数据。
~用DECIMAL代替FLOAT和DOUBLE存储精确浮点数
~不要物理删除任何生产环境的数据,需要往核心表里添加data_desc字段,且有变更需要记录。
索引设计
~单表中索引数量不超过5个
~单个索引中的字段数量不超过5个
~重要的字段必须被索引
"UPDATE、DELETE语句的WHERE条件列 "ORDER BY、GROUP BY、DISTINCT的字段 "多表JOIN的字段 !
~索引不是越多越好
~综合评估数据密度和分布,考虑查询和更新比例
~不在低基数列上建立索引,例如“性别”
~不在索引列进行数学运算和函数运算,否则会无法使用索引,导致全表扫描。
~不在索引列使用%前导的查询,例如:like "%ab",否则会无法使用索引,导致全表扫描
~不在索引列使用负向查询,例如:not in/like,否则会无法使用索引,导致全表扫描
sql设计
~ 不要写select *,用什么查什么,否则大幅降低性能。
从数据库里读出越多的数据,那么查询就会变得越慢。并且,如果你的数据库服务器和WEB服务器是两台独立的服务器的话,这还会增加网络传输的负载。
~例如count(*)的地方尽量用1,1>anycol>*,因为不用查字典表,select count(1)。
~mysql的日期和字符是相同的,所以不需要像oracle那样做另外的转换,比如:
select e.username from employee e where e.birthday>='1998-12-31 11:30:45'。
~避免在where字句中对字段施加函数,如果是业务要求的除外,但需要在编写时候咨询DBA。比如DATE_FORMAT(p.PAYMENT_DATE, '%Y-%m-%d') >= DATE_FORMAT('2014-10-01', '%Y-%m-%d')
~避免复杂查询,可以考虑把一个sql拆成2个sql(仁者见仁智者见智),或者把逻辑都扔到java里,尽量不要写n+1查询。
~尽量使用单表查询,避免多表JOIN。JOIN的后续ON条件不能用OR判断,比如SELECT A.C1,B.C2 FROM A,B ON(A.ID=B.PID OR B.TAG=A.TAR_GET); OR性能非常低
~使用join代替子查询(性能提升,尤其是在有索引的时候。因为 MySQL不需要在内存中创建临时表来完成这个逻辑上的需要两个步骤的查询工作。)
join写法:select * from t_a a left join t_b b on a.id = b.a_id where b.name = 'xxxx'
子查询写法:select * from t_a where id = (select a_id from t_b where name='xxxx')
笛卡尔积:select * from a,b where a.id = b.a_id and b.name = 'xxx';
尽量不要使用子查询,必须禁止使用笛卡尔积的写法。
~不要where语句中使用NOW(),CURDATE()方法,以及其他类似的mysql函数,因为性能有问题,导致查询缓存不开启,如果在建有索引的列上使用函数会将索引的效果发挥不出来,
并且数据库服务器时间与应用服务器时间可能不一致。
~当只要一行数据时使用 LIMIT 1,提升效率
~千万不要 ORDER BY RAND()
~如果用到索引请遵循
最左原则,并且不能同时用到2个范围条件:where col1=100 and col2 between 1000 and 2000 and col3 > 3000
~避免在数据库中进行数学/逻辑运算,mysql不擅长数学运算和逻辑判断。
~减少与数据库的交互次数,例如:UPDATE … WHERE ID IN(10,20,50,…)
~拒绝大sql,拒绝复杂sql,充分利用查询缓存
~使⽤用in代替or,in的值不超过1000个
~使⽤用EXPLAIN诊断,避免⽣生成临时表
~要用union all⽽不是union
~禁⽌止单条SQL语句同时更新多个表
其他
~写到应用程序的SQL语句,除特殊情况外,禁止一切DDL操作,例如:create,drop,alter,grant,remove
~禁止在数据库中存储明文密码
RestfulUrl篇
后端对外提供的api的命名方式:
~不能使用动词
~单个数据查询使用名词单数,集合数据查询,则需要名词变复数
~单个单词使用小写,多个单词连接使用驼峰
~如果url有层次关系可以使用xxx/xxx的形式,例如:/father/son
统一前缀:
http(s)://xxx.xxx.com/api/domain_name/v1/xxxxxx
domain_name:speakhi或者teenager
后缀规则:
GET /tickets - 获取 tickets 列表,如果带查询条件的查询,则把查询条件放到json中作为参数传递,主键才放到url上。
GET /ticket/12 - 获取一个单独的 ticket,查询带多个条件的规则同上,把/数据名称改成:起个别的名。
POST /ticket - 创建一个新的 ticket
PUT /ticket/12 - 更新 ticket #12
DELETE /ticket/12 - 删除 ticket #12
GET /ticket/12/messages - 获取ticket #12下的消息列表
不符合CRUD操作那该怎么办
Github的API
PUT /gists/:id/star 来进行star
DELETE /gists/:id/unstar来进行 unstar 。
PUT /user/following/12345 Follow a user(follow是个动作,不是条数据)
DELETE /user/following/12345 Unfollow a user
文件路径篇
按照模块来分子文件夹,例如:
src:
/user
/user/controller
/user/service
/user/dao
/user/entity
/user/util
/user/constant
多线程篇
未完待续
其他篇
~不要写超级臃肿的类或页面或sql,例如1000行的java文件,2000行的jsp。
控制在最多600行左右每个文件,如果超过了就考虑重构以及抽出新的类。
无论是哪层都可以抽出来,比如一个订单的Service写的太多了,
那就把能划分在一个单元内的的Service方法抽出去,形成一个新的Service类。
~编写功能时,在前台与后台都要加上长度的校验,非空校验等等,不要传进来是空就抛异常。
~注意代码注释的编写,方法级,代码级,类级,一眼能看明白的不用写代码级注释。
~不要使用字符串拼装的方式来写sql,防止注入攻击,使用alex封好的baseDao方法可以杜绝此问题。
~import不要使用通配符,不要import java.util.*; 使用快捷键 ctrl + shift + o
~每个类文件中只有一个class
~必须要进行定期的生产环境log检查以及相关健康,如果没有这个机制,也许你的web项目已经进入了亚健康状态。
~所有涉及到编码集的地方全部使用utf-8
~不要出现初级java语法级别错误,例如:if(abc=="5"),需要深入理解java的值传递。
~一个项目里不要起相同的文件名,即使不在一个包下。方便快速定位ctrl+shift+r
~当一个类有多个构造函数,或是多个同名方法,这些函数/方法应该按顺序出现在一起,中间不要放进其它函数/方法。
~尽量不使用N+1查询,可以使用缓存或者先查询一次,然后在遍历集合的时候做逻辑操作。
~如果是自己try,catch的异常,必须在catch中添加logger.error
~能查询一次sql的地方,再便利数据的东西去判断的地方,就不要用多次数据库查询去查,提高性能。(尽量杜绝子查询)
~entity实体复制的情况请不要自己再手工复制了,请使用相关的工具类: BeanUtils.copyProperties(dest, orig);
~easyui页面中标识为,hidden:true的放到一起并加一个回车来标识逻辑分割。包括 |