编程规约
命名风格
-
强制 定义变量时不可用
$
、_
开始,也不可以用$
、_
结束~
: 表示位操作,位非!
: 表示逻辑非,常用于boolean
类型操作@
: 表示注解#
: 用于注释中使用,在IDEA
中可以使用@see #方法名()
,快速跳转至方法,例:
/** * @see #b() */ public void a() { } /** * @see #a() */ public void b() { }
$
: 没有什么特殊用法%
: 取余运算^
: 异或运算&
: 与符号*
: 乘符号(
)
_
: 下划线可以用在变量中国,但是不能用于首尾-
+
=
-
强制 类名使用大驼峰
-
强制 方法名、参数名、成员变量、局部变量使用小驼峰
-
强制 常量全部大写,单词间用下划线隔开
-
强制 抽象类以
Abstract
或者Base
开头,异常类用Expection
结尾,测试类命名以它要测试的类的名称开始,以Test
结尾 -
强制
POJO
中的boolean
类型不要以is
开头(boolean
对象自动生成getter
方法时会以is
开头) -
强制 包名统一使用小写,且点分隔符之间有且有一个自然语义的单词,包名统一使用单数形式(存在例外),类名如果存在复数形式可以使用复数形式
-
强制 杜绝不规范的缩写
-
推荐 在常量和变量的命名时,表示类型的名词放在词尾,以提升辨识度
-
推荐 接口类中的方法不要加任何修饰符号(
public
也不要加),保证代码的简洁性,并加上JavaDoc注释 -
参考 各层命名规约:
Service/DAO
层命名方法规约- 获取单个对象的方法用
get
做前缀 - 获取多个对象的方法用
list
最前缀,复数形式解为如:listObjects
- 获取统计值的方法用
count
做前缀 - 插入的方法用
save/insert
做前缀 - 删除的方法用
remove/delete
做前缀 - 修改的方法用
update
做前缀
- 获取单个对象的方法用
- 领域数据命名规约
- 数据对象:
xxxDO
,xxx
即为数据表名 - 数据传输对象:
xxxDTO
,xxx
为业务领域相关的名称 - 展示对象:
xxxVO
,xxx
一般为网页名称 POJO
是DO/DTO/BO/VO
的统称,禁止命名为xxxPOJO
- 数据对象:
常量定义
- 强制 不允许任何魔法值出现(魔法值: 未经预先定义,凭空出现在代码中的值)
- 强制 在
Long
或者long
赋值时,数值后使用大写字母L
,而不是小写字母l
- 推荐 不要使用一个常量类来维护所有常量,要按常量功能进行分类,分开维护
- 推荐 常量的复用层次有五层:
跨应用共享常量
(放置在二方库中,通常是client.jar
中的constant
目录下)、应用内共享常量
(放置在一方库中,通常是子模块中的constant
目录下)、子工程内共享常量
(在当前子工程的constant
目录下)、包内共享常量
(在当前包下单独的constant
目录下)、类内共享常量
(在类内部的private static final
定义)
代码格式
-
强制 大括号为空,简洁地写成
{}
即可,大括号中间无需换行和空格,如果非空代码块:- 左大括号前不换行
- 左大括号后换行
- 有大括号前换行
- 右括号后还有
else
等代码则不换行,表示终止的右大括号必须换行
-
强制 右小括号和右边相邻字符间不出现空格;右小括号和左边相邻字符之间也不出现空格;左大括号前需要加空格
-
强制 ``if/for/while/switch/do` 等保留字与括号之间都必须加空格
-
强制 任何二目、三目运算符的左右两边都需要加一个空格
-
强制 采用4个空格缩进,禁止使用
Tab
字符,例:
// 本部分代码适用于以上多条与缩进有关规范
public static void main(String[] args) {
// 缩进4个字符
String say = "hello";
// 运算符的左右必须有一个空格
int flag = 0;
// 关键词if与括号之间必须有一个空格,括号内的‘f’与左括号,‘0’与右括号不需要空格
if (flag == 0) {
System.out.println(say);
}
// 左大括号前加空格且不换行,左大括号后换行
if (flag == 1) {
System.out.println("world");
// 右大括号前换行,右大括号后有else,不用换行
} else {
System.out.println("ok");
// 在右大括号后直接结束,则必须换行
}
}
-
强制 注释的双斜线与注释内容之间有且只有一个空格
-
强制 在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开,例:
int second = (int)first + 2;
-
强制 单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
- 第二行相对第一行缩进4个字符,从第三行开始,不再进行缩进
- 运算符与下文一起换行
- 方法调用的点符号与下文一起换行
- 方法调用中的多个参数需要换行时,在逗号后进行
- 在括号前不要换行
-
强制 方法参数在定义和传入时,多个参数逗号后面必须加空格
-
强制
IDE
的text file encoding
设为UTF-8
;IDE
中文件的换行符使用Unix
格式,不要使用Windows
格式
OOP规格
-
强制 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器编译成本,直接用类名来访问即可
-
强制 所有的覆写方法,必须加
@Override
注解(@Override
注解可以准确判断是否覆写成功) -
强制 相同参数类型,相同业务含义,才可以使用
Java
的可变参数, 避免使用Object- 可变参数必须放置在参数列表的最后,建议开发者尽量不要使用可变参数编程
// Person类
public class Person {
}
// 某方法
public void getPerson(Person... person) {
}
// 调用
类.getPerson(new Person(), new Person(), ..., new Person());
- 强制 外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响;接口过时时必须加
@Deprecated
注解,并清晰地说明采用的新接口或者新服务是什么 - 强制 不能使用过时的类和方法
- 强制
Object
的equals()
方法容易抛出指针异常,应使用常量或确定有值得对象来调用equals()
,例:
// 正例
"test".equals(object);
// 反例
object.equals("test");
// 推荐使用
java.util.Objects.equals("test", object);
- 强制 所有整型包装类对象之间值的比较,全部使用
equals()
方法比较(Integer
创建范围在[-128, 127]
之间时不创建对象) - 强制 任何货币金额,均以最小货币单位且整数类型来进行存储(人民币用分)
- 强制 浮点数之间的等值判断,基本数据类型不能用
==
来比较(使用equals()
),包装数据类型不能用equals()
来判断(使用BigDecimal
compareTo()
) - 强制 定义数据对象
DO
类时,属性类型要与数据库字段类型相匹配 - 强制 禁止使用构造方法
BigDecimal(double)
的方式把double
值转化为BigDecimal
对象,优先推荐入参为String
的构造方法,或使用BigDecimal
的valueOf()
方法 - 强制 所有的
POJO
类属性必须使用包装数据类型(Integer
默认值为null
,int
默认值为0
) - 强制
RPC
方法的返回值和参数必须使用包装数据类型 - 强制 定义
DO/DTO/VO
等POJO
类时, 不要设定任何默认属性值 - 强制 序列化类新增属性时, 请不要修改
serialVersionUID
字段, 避免反序列失败; 如果完全不兼容升级, 避免反序列化混乱, 那么请修改serialVersionUID
值 - 强制 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在
init
方法中 - 强制
POJO
类必须写toString
方法, 如果继承了另一个POJO
类, 注意在前面加一下super.toString
- 强制 禁止在
POJO
类中同时存在对应属性xxx
的isXxx()
和getXxx()
方法 - 推荐 使用索引访问用
String
的split
方法得到的数组时,需对最后一个分隔符后有无内容做检查 - 推荐 循环体内的字符串连接应使用
StringBuilder
的append
方法进行扩展 - 推荐 类成员与方法访问控制从严:
- 如果不允许外部直接通过
new
来创建对象,那么构造方法必须是private
- 工具类不允许有
public
或default
构造方法 - 类非
static
成员变量并且与子类共享,必须是protected
- 类非
static
成员变量并且尽在本类中使用,必须是private
- 类
static
成员变量如果仅在本类中使用,必须是private
- 若是
static
成员变量,考虑是否为final
- 类成员方法只供类内部调用,必须是
private
- 类成员方法只对继承类公开,需要限制为
protected
- 如果不允许外部直接通过
日期时间
- 强制 日期格式化时, 传入
patterm
中表示年份统一使用小写的y
- 强制 在日期格式中分清楚大写的
M
和小写的m
, 大写的H
和小写的h
分别取代的意义 - 强制 获取当前毫秒数使用
System.currentTimeMillis()
而不是new Date().getTime()
如果想要更加准确的纳秒级时间值,使用System.nanoTime
的方式 - 强制 不允许在程序的任何地方使用
java.sql.Date
java.sql.Time
java.sql.Timestamp
- 强制 不要在程序中写死一年为365天,避免在公历闰年时出现日期转换错误或程序逻辑错误,可使用:
// 获取今年的天数
int daysOfThisYear = LocalDate.now().lengthOfYear();
// 获取指定某天的天数
localDate.of(2000, 9, 18).lengthOfYear();
集合处理
-
强制 关于
hashCode
和equals
的处理,应遵循如下规则:- 只要覆写
equals
,就必须覆写hashCode
- 因为
Set
存储的是不重复的对象,根据hashCode
和equals
进行判断,所以Set
存储的对象必须覆写这两种方法 - 如果自定义对象为
Map
的键,那么必须覆写hashCode
和equals
- 只要覆写
-
强制 判断所有集合内部的元素是否为空,使用
isEmpty()
方法,而不是.size() == 0
的方法 -
强制/续具体实现 在使用
java.util.stream.Collevtors
类的toMap()
方法转为Map
集合时,一定要使用含有参数类型为BinaryOperator
,参数名为mergeFunction
的方法,否则当出现相同key
时会抛出IIIegaIStateException
异常,例:
List<Pair<String, Double>> pairArrayList = new ArrayList<>(3);
pairArrayList.add(new Pair<>("version", 12.10));
pairArrayList.add(new Pair<>("version", 12.19));
pairArrayList.add(new Pair<>("version", 6.28));
Map<String, Double> map = pairArrayList.stream().collect(
Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2)
);
map.entrySet().stream().forEach(
e -> System.out.println(e.getKey() + "..." + e.getValue())
);
-
强制 在使用
java.util.Stream.Collectors
类的toMap()
方法转为Map
集合时,一定要注意当value
为null
时会抛NPE
异常 -
强制
ArrayList
的subList
结果不可强转成ArrayList
,否则会抛出ClassCastException
异常(subList
返回的是ArrayList
的内部类SubList
,并不是ArrayList
,而是ArrayList
的一个视图,对于SubList
子列表的所有操作最终会反映到原列表上) -
说明:
subList()
返回的是ArrayList
的内部类SubList
,并不是ArrayList
本身,而是ArrayList
的一个视图,对于SubList
的所有操作最终都会反映到原列表上 -
强制 使用
Map
的方法keySet()/values()/entrySet()
返回集合对象时,不可以对其进行添加元素操作,否则会抛出UnsupportedOperationExpection
异常 -
强制
Collections
类返回的对象,如:emptylist()/singletonList()
等都是immutable list
,不可对其进行添加或者删除元素的操作 -
强制 在
subList
场景中,高度注意 对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生ConcurrentModificationExeception
异常 -
强制 使用集合转数组的方法,必须使用集合的
toArray(T[] array)
传入的是类型完全一致、长度为0的空数组(如果使用toArray()
的无参方法,返回值只能是Object[]类,如果对结果进行强转,会发生ClassCastException
错误)List<String> list = new ArrayList<>(2); list.add("guan"); list.add("bao"); String[] array = list.toArray(new String[0]);
使用
toArray
带参方法,数组空间大小的lengt
:- 等于0: 动态创建与
size
相同的数组,性能最好 - 大于0但小于size: 重新创建大小等于
size
的数组,增加GC
负担 - 等于size: 在高并发情况下,数组创建完成之后,
size
正在变大的情况下,负面影响与上相同 - 大于size: 空间浪费,且在
size
处插入NULL
值,存在NPE
隐患
- 等于0: 动态创建与
-
强制 在使用
Collection
接口任何实现类的addAll()
方法时,都要对输入的集合参数进行NPE
判断 -
强制 使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear
方法会抛出UnsupportedOperationException
异常 -
强制 泛型通配符
<? extends T>
来接收返回的数据,此写法的泛型集合不能使用add
方法(这样子写泛型表示这样一个范围,而指定一个类型传入不符合这个范围,可能是现在java
的一个不足),而<? extends T>
不能使用get
方法(这样子返回,因为泛型中是一个范围,所以返回只能是一个Object
),两者在接口调用赋值的场景中容易出错 -
强制 在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行
instanceof
判断,避免抛出ClassCastExpection
异常 -
强制 不要在
foreach
循环里进行元素的remove/add
操作。remove
元素请使用Iteratoe
方式,如果并发操作,需要对Iterator
对象加锁 -
强制 在
JDK7
版本及以上,Comparator
实现类要满足如下三个条件,不然Arrays.sort/Collections.sort
会抛出IllegalArgumentExpection
异常- 自反性: x,y的比较结果和y,x的比较结果相反
- x > y,y > z,则 x > z
- x = y,则x,z比较结果和y,z比较结果相同
-
推荐 集合初始化时,指定集合初始值大小
-
推荐 使用
entrySet
遍历Map
类集合KV
,而不是KeySet
方式进行遍历(遍历KeySet
的方式其实是遍历了两次,第一次是先将KeySet
转换为Iterator
对象,第二次是取出Key
所对应的Value
)
并发处理
- 强制 获取单例对象需要保证线程安全,其中的方法也要保证线程安全(资源驱动类、工具类、单例工厂类等)
- 强制 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯
- 强制 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程(不通过线程池,可能会造成创建大量同类线程导致消耗过多资源)
- 强制 线程池不允许使用
Executors
去创建,而是通过ThreadPoolExecutor
的方式来创建 - 强制
SimpleDataFormat
是线程不安全的类,一般不要定义为statis
变量,如果定义为static
,必须加锁,或者使用DateUtils
工具类(JDK8可以使用Instant
代替Date
,LocalDateTime
代替Calendar
,DateTimeFormatter
代替SimpleDateFormat
) - 强制 必须回收自定义的
ThreadLocal
变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal
变量,可能会影响后续业务逻辑和造成内存泄漏等问题,尽量在代理中使用try-finally
块进行回收 - 强制 高并发时,同步调用应该去考量锁的性能损耗,能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁
- 强制 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
- 强制 在使用阻塞等待获取锁的方式中,必须在
try
代码块之外,并且在加锁方法与try
代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally
中无法解锁 - 强制 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁,锁的释放规则与锁的阻塞等待方式相同
- 强制 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用
version
作为更新依据 - 强制 多线程并行处理定时任务时,
Timer
运行多个TimeTask
时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用ScheduledExecutorService
则没有这个问题 - 推荐 资金相关的金融敏感信息,使用悲观锁策略
- 推荐 避免
Random
实例被多线程调用,虽然共享该实例是线程安全的,但会因为竞争同一个send
导致性能下降(JDK7
之后可以使用ThreadLocalRandom
) - 参考
HashMap
在容量不够进行resize
时由于高并发可能会出现死链,导致CPU
飙升,开发过程中应尽量避免使用该数据结构
控制语句
-
强制 在一个
switch
块内,每个case
要么通过continue/break/return
等来终止,要么注释说明程序将继续执行到哪一个case
为止;在一个switch
块内,都必须包含一个default
-
强制 当
switch
括号内的变量类型为String
并且此变量为外部参数时,必须先进行null
判断 -
强制 在
if/else/for/while/do
语句中必须使用大括号 -
强制 三目运算符
condition ? 表达式1 : 表达式2
中,高度注意表达式1和2在类型对齐时,可能抛出因自动拆箱导致的NPE异常
,例:Integer a = 1; Integer b = 2; Integer c = null; Boolean flag = false; Integer result = (flag ? a * b : c);
-
强制 在高并发场景中,避免使用 “
等于
” 判断作为中断或退出的条件 (剩余票数为0,可能某一个线程中数目小于0,此时程序永远不能终止) -
推荐 表达异常的分支时,少用
if-else
方式,这种方式可以改写为:if (condition) { ... return obj; } // 接着写 else 的业务逻辑代码;
-
推荐 除了常用方法(
getXxx/isXxx
)外,不要在条件判断语句中执行复杂的语句,这样会增加理解成本
注释规约
- 强制 类、类属性、类方法的注释必须使用
javadoc
规范,使用/**内容*/
格式,不得使用// xxx
方式 - 强制 所有的抽象方法(包括接口中的方法)必须使用
javadoc
注释,除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能 - 强制 所有的类都必须添加创建者和创建日期
- 强制 方法内部单行注释,在被注释语句上方另起一行,使用
//
注释。方法内部多行注释使用/* */
注释,注意与代码的对齐 - 强制 所有的美剧类型字段必须要有注释,说明每个数据的含义
- 推荐 代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改
- 参考 特殊注释标记:
- 待办事项(
TODO
): (标记人,标记时间,[预计处理时间]) - 错误,不能工作(
FIXME
): (标记人,标记时间,[预计处理时间])
- 待办事项(
前后端规约
- 强制 前后端交互的API,必须明确协议、域名、路径、请求方法、请求内容、状态码、响应体
- 协议: 生产环境必须使用
HTTPS
- 路径: 每一个API需对应一个路径,表示API具体的请求地址
- 代表一种资源,只能为名词,推荐使用复数,不能为动词,请求方法已经表达动作意义
- URL路径不能使用大写,单词如果需要分隔,统一使用下划线
- 路径禁止携带表示请求内容比如(.json、.xml)的后缀,通过
accept
头表达即可
- 请求方式: 对具体操作的定义,常见的请求方法如下
- GET: 从服务器取出资源
- POST: 在服务器创建一个资源
- PUT: 在服务器更新资源
- DELETE: 从服务器删除资源
- 请求内容: URL带的参数必须无敏感信息或符合安全要求,body里带参数时必须设置
Content-Type
- 响应体: 响应体body可放置多种数据类型,由Content-Type头来确定
- 协议: 生产环境必须使用
- 强制 前后端数据列表相关的接口返回,如果为空,则返回空数组或空集合
- 强制 服务端发生错误时,返回给前端的响应信息必须包含
HTTP状态码
、errorCode
、errorMessage
、用户提示信息
四个部分 - 强制 在前后端交互的
JSON格式数据
中,所有的key
都因该为小写字母开始的小驼峰风格,表达完整 - 强制
errorMessage
是前后端错误追踪体制的体现,可以在前端输出到type = "hidden"
文字类控件中,或者用户端的日志中,帮助我们快速定位到问题所在 - 强制 对于需要使用超大整数的场景,服务端一律使用
String
字符串的类型返回,禁止使用Long
类型 - 强制 HTTP请求通过URL传递参数时,不能超过2048字节
- 强制 HTTP请求通过body传递内容时,必须控制长度,超出最大长度后,后端解析会出错
- 强制 在翻页场景中,用户输入参数若小于1,则前端返回第一页参数给后端;后端发现用户输入的参数大于总页数,直接返回最后一页
- 强制 服务器内部重定向必须使用
forward
;外部重定向地址必须使用URL统一代理模块生成,否则会因为线上采用HTTPS协议而导致浏览器提示不安全,并且还会带来URL维护不一致的问题
其他
- 强制 在使用正则表达式时,利用好其预编译功能,可以提升正则匹配效率(不要放在方法体内定义)
- 强制 避免使用
Apache Beanutils
进行属性的copy
,可以使用比如Spring BeanUtils
来进行拷贝 - 强制 后台输送给页面的变量时必须加
$!{var}
中的感叹号(velocity
模板引擎) - 强制
Math.random()
方法返回的是double
类型,注意取值的范围0<=x<1
,如果想获取整数类型的随机数,不要将x
方法10的若干倍然后取整,直接使用Random
对象 - 推荐 任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存
异常日志
错误码
- 强制 错误码的制定规则: 快速溯源、沟通标准化
- 错误码的重点是,谁的错? 错在哪? 错误码必须能够快速知晓错误来源,可快速判断是谁的问题
- 错误码必须能够进行清晰的比对(equals)
- 错误码有利于团队快速对错误原因达到一致认知
- 强制 错误码不体现版本号和错误等级信息
- 强制 全部正常,但不得不填充错误码时返回00000
- 强制 错误码为字符串类型,共5位,分成两个部分: 错误产生来源 + 四位数字编号
- 错误产生来源:
- A: 错误来源于用户,比如参数错误、用户版本过低等
- B: 错误来源与当前系统,比如业务逻辑问题、程序健壮性问题
- C: 错误来源于第三方服务,比如
CDN
服务出错,消息投递超时等
- 数字编号参考手册末尾文档,大类步长相差100
- 错误产生来源:
- 强制 编号不与公司业务架构,更不与组织架构挂钩,以先到先得的原则在统一平台上进行,审批生效,编码即被永久固定
- 强制 错误码使用者避免随意定义新的错误码
- 强制 错误码不能直接输出给用户作为提示信息使用
异常处理
- 强制 Java类库中定义的可以通过预检查方式规避的
RuntimeException
不应该通过catch
方式来处理(NullPointerException
、IndexOutOfBoundsException
) - 强制 异常捕获后不要用来做流程控制、条件控制(异常的处理效率比条件判断方式低得多)
- 强制
catch
时分清楚稳定代码和非稳定代码(对一大段程序块进行try...catch
),稳定代码指的是不论如何不会出错的代码。对于非稳定代码的catch
尽可能进行区分异常类型,再做对应的异常处理 - 强制 捕获异常是为了处理它,不要捕获了不做任何处理,如果不想做任何处理,把它抛给调用者;最外层的业务使用者必须处理异常,将其转化为用户可以理解的内容
- 强制 事务场景中,抛出异常被
catch
后,如果需要回滚,一定要注意手动回滚事务 - 强制
finally
块必须对资源对象、流对象进行关闭,有一场也要做try-catch
- 强制 不要在
finally
块中使用return
(如果在try
块中存在return
语句,并不会马上返回,而是继续执行finally
语句中的内容,而此时如果finally
中存在return
语句,则直接在finally
中的return
语句中返回) - 强制 捕获异常与抛异常,必须完全匹配,或者捕获异常是抛异常的父类
- 强制 在调用
RPC
、二方包、或者动态生成类的相关方法时,捕获异常必须使用Throwable
类来进行拦截(因为我们并不知道会抛出什么异常)- 一方包: 本工程中的各模块的相互依赖
- 二方包: 公司内部的依赖库,一般指公司内部的其他项目发布的jar包
- 三方包: 公司之外的开源库,比如: apache、ibm、google等公司发布的依赖
- 推荐 方法的返回值可以为
null
,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下不会返回null
值 - 推荐 容易产生
NPE
的情况:- 返回类型为基本数据类型,
return
包装数据类型的对象时,自动拆箱时可能产生NPE
- 数据库的查询结果可能是
null
- 集合里的元素可能是
null
,即使该集合isNotEmpty
- 远程调用返回对象时,一律要求进行空指针判断,防止
NPE
- 对于
Session
中获取的数据,建议进行NPE
检查,避免空指针 - 级联调用
obj.getA().getB().getC()
时,容易产生NPE
- 返回类型为基本数据类型,
日志规约
-
强制 应用中不可直接使用日志系统(
Log4j
、Logback
)中的API
,而应依赖使用日志框架(SLF4J
、JCL-Jakarta Commons Logging
)中的API
,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一 -
强制 所有日志文件至少保存15天,因为有些异常具备以“周”为频次发生的特点,对于当天日志,以“应用名.log”来保存,存放在
/home/admin/应用名/log/
目录下,过往日志格式为:{logname}.log.{保存日期}
,日期格式:yyyy-MM-dd
-
强制 根据国家相关规定,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存日志不少于六个月,并且进行网络多机备份
-
强制 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log
。logType
: 日志类型,如:stats/monitor/access
等;logName
: 日志描述。这种命名的好处: 通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找 -
强制 在日志输出时,字符串变量之间的拼接使用占位符的方式
-
强制 对于
trace/debug/info
级别的日志输出,必须进行日志级别的开关判断// 先进行判断,如果为真,可以输出trace或者debug级别的日志 if (logger.isDebugEnabled()) { logger.debug("..."); }
-
强制 避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置
additivity = false
-
强制 生产环境禁止直接使用
System.out
或者System.err
输出日志或使用e.printStackTrace()
打印异常日志 -
强制 异常信息应该包括两类信息: 案发现场信息和异常堆栈信息,如果不进行处理,那么通过关键字
throws
向上抛出 -
强制 日志打印时禁止直接使用
JSON
工具将对象转换为String
(因为JSON
工具将对象转换为字符串时会调用get
方法,如果get
方法存在抛出异常的情况,就会影响业务的进行) -
推荐 谨慎地记录日志,生产环境禁止输出
debug
日志,有选择地输出info
日志,如果使用warn
来记录刚上线时的业务行为信息,一定要注意日志输出量的问题 -
推荐 可以使用
warn
日志级别来记录用户输入参数问题错误的情况
单元测试
-
强制 好的单元测试必须遵守
AIR
原则单元测试在线上运行时,感觉像空气(air)一样感觉不到,但在测试质量的保障上又是非常关键的,好的单元测试需要具有自动化、独立性、可持续执行的特点:
- A: Automatic(自动化)
- I: Independen(独立性)
- R: Repeatable(可持续性)
-
强制 单元测试应该是全自动执行的,并且非交互式的;测试用例通常是被定期执行的,执行过程必须完全自动化才有意义;输出结果需要人工检查的测试不是一个好的单元测试;单元测试中不准使用
System.out
来人工验证,必须使用assert
来验证 -
强制 为了保证单元测试稳定可靠且便于维护,单元测试用例之间不能相互调用,不能依赖执行的先后顺序
-
强制 单元测试是可以重复执行的,不能受到外界环境的影响
-
强制 对于单元测试,要保证测试粒度足够小,有助于精确定位问题,单测粒度至多是类级别,一般是方法级别
-
强制 核心业务、核心应用、核心模块的增量代码确保单元测试通过
-
强制 单元测试代码必须写在如下工程目录:
src/test/java
,不允许写在业务模块目录下(源码进行编译时会跳过单元测试目录,单元测试框架默认扫描该目录) -
推荐 单元测试的基本目标: 语句覆盖率达到
70%
;核心模块的语句覆盖了和分支覆盖率都要达到100%
-
推荐 编写单元测试代码遵守
BCDE
原则:B
:Border
,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等C
:Correct
,正确的输入,并得到期望的结果D
:Design
,与设计文档相结合,来编写单元测试E
:Error
,强制错误信息输入(非法数据、异常流程、业务允许外),并得到期望的结果
-
推荐 对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,或者直接操作数据库把数据插入进去,请使用程序导入或者插入的方式准备数据
-
推荐 和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。或者对单元测试产生的数据有明确的前后缀标识
安全规约
- 强制 隶属于用户个人的页面或者功能必须进行权限控制校验
- 强制 用户敏感数据禁止直接显示,必须对展示数据进行脱敏
- 强制 用户输入的
SQL
参数必须严格使用参数绑定或者METADATA
字段进行限定,放置SQL
注入,禁止字符串拼接SQL
访问数据库 - 强制 用户传入的任何数据必须做有效性验证(可以使用正则表达式来验证用户的输入,注意特殊的字符串造成的死循环)
- 强制 禁止向
HTML
页面输出未经安全过滤或未正确转义的用户数据 - 强制 表单、
AJAX
提交必须执行CSRF安全验证(攻击者使用了某用户的身份,然后发送恶意请求,因为是正常用户的身份,所以在服务器的角度来看是完全合法的) - 强制
URL
外部重定向传入的目标地址必须执行白名单过滤 - 强制 在使用平台资源,比如短信、邮件、下盘等,必须实现正确的防重放的机制,比如数量限制、疲劳度控制、验证码校验,避免被滥刷导致资源损耗
- 推荐 发帖、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容等违禁词过滤等风控策略
MySQL数据库
建表规约
- 强制 表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是
unsigned tinyint
(1表示是,0表示否)- 任何字段表示非负数,必须是
unsigned
- 因为
POJO
类中的任何布尔类型的变量不能加is
前缀,所以,需要在<resultMap>
中设置从is_xxx
到Xxx
的资源映射
- 任何字段表示非负数,必须是
- 强制 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字,数据库字段名修改代价很大,所以字段名称需要慎重考虑
- 强制 表中不使用复数名词
- 强制 禁用保留字,比如:
desc
、range
、match
等 - 强制 主键索引名为pk_字段名;唯一索引名为uk_字段名;普通索引名为idx_字段名
- 强制 小数类型为
decimal
,禁止使用float
和double
- 强制 如果存储的字符串长度几乎相等,使用
char
定长字符串类型 - 强制
varchar
是可变长度字符串,不预先分配存储空间,长度不要超过5000,如果存储长度大于此值,定义字段类型为text
,独立出一张表,用主键来对应,避免影响其他字段索引效率 - 强制 表必备三字段:
id
: 主键,类型为bigint unsigned
,单表时自增create_time
:datetime
类型,主动创建时updated_time
:datetime
类型,被动修改时
- 推荐 表的命名最好是遵循业务名称_表的作用
- 推荐 库名和应用名称尽量一致
- 推荐 单表行数超过500万行或者单表容量超过2GB,才推荐进行分库分表
索引规约
- 强制 业务上具有唯一性质的字段,即使是组合字段,也必须建成唯一索引
- 强制 超过三个表禁止
join
,需要join
的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引 - 强制 在
varchar
字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度 - 强制 页面搜索严谨左模糊或者全模糊,如果需要请走搜索引擎来解决
- 推荐
SQL
性能优化的目标: 至少要达到range
级别,要求是ref
级别,如果可以consts
最好
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
问题,可以使用ifnull()
函数来防止NPE
问题 - 强制 使用
isnull()
来判断是否为NULL
值(NULL
与任何值的直接比较都为NULL
) - 强制 代码中写分页查询逻辑时,若
count
为0应直接返回,避免执行后面的分页语句 - 强制 不得使用外键与级联,一切外键概念必须在应用层解决
- 强制 禁止使用存储过程,存储过程难以调试和扩展,也没有移植性
- 强制 数据订正,特别是删除或修改记录操作时,要先
select
,避免出现误操作,确认无误才能执行更新语句 - 强制 对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定
ORM映射
- 强制 在表查询中,一律不要使用
*
作为查询的字段列表,需要哪些字段必须明确写明,原因:- 增加查询分析器解析成本
- 增减字段容易与
resultMap
配置不一致 - 无用字段增加网络消耗,尤其是
text
类型的字段
- 强制
POJO
类的布尔属性不能加is
,数据库字段必须加is_
,要求在resultMap
中进行字段与属性之间的映射 - 强制 不要用
resultClass
当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义<resultMap>
,换句话说,每一个表也必然有一个<resultMap>
与之对应 - 强制
sql.xml
配置属性使用#{}
、#param#
,不要使用${}
,这种方式容易出现SQL
注入 - 强制
iBATIS
自带的queryForList(String statementName, int start, int size)
不推荐使用(它实现的逻辑是将整个表中的数据查出来,然后分割出所需数据) - 强制 不允许直接拿
HashMap
与HashTable
作为查询结果集输出 - 强制 更新数据库表记录时,必须同时更新记录对应的
update_time
字段值为当前时间 - 推荐 不要写一个大而全的数据更新接口
- 参考
<isEqual>
中的compareValue
是与属性值对比的常量,一般是数字,表示相等时带上此条件;<isNotEmpty>
表示不为空且不为null
时执行;<isNotNull>
表示不为null
值时执行
工程结构
应用分层
- 参考 分层领域模型规约:
DO(Data Object)
: 此对象与数据库表结构一一对应,通过DAO
层向上传输数据源对象DTO(Data Transfer Object)
: 数据传输对象,Service
或Manager
向外传输的对象BO(Bussiness Object)
: 业务对象,由Service
层输出的封装业务逻辑对象AO(Application Object)
: 应用对象,在Web
层与Service
层之间抽象的复用对象模型,极为贴近展示层,复用度不高VO(View Object)
: 显示层对象,通常是Web
向模板渲染引擎层传输的对象Query
: 数据查询对象,各层接收上层的查询请求,注意超过2个参数的查询封装,禁止使用Map
类来传输
二方库依赖
- 强制 定义
GAV
规则:GroudID
格式:com.{公司/BU}.业务线[.子业务线]
,最多4级ArtifactID
格式:产品线名-模块名
(语义不重复不遗漏)Version
: 版本
- 强制 二方库版本号命名方式:
主版本号.次版本号.修订号
- 主版本号: 产品方向改变,或者大规模
API
不兼容,或者架构不兼容升级 - 次版本号: 保持相对兼容性,增加主要功能特性,影响范围极小的
API
不兼容修改 - 修订号: 保持完全兼容性,修复BUG、新增次要功能特性等
- 主版本号: 产品方向改变,或者大规模
- 强制 二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的
POJO
对象
服务器
- 推荐 高并发服务器建议调小
TCP
协议的time_wait
超时时间 - 推荐 调大服务器所支持的最大文件句柄数(
File Descriptor
,简写为fd
)
设计规约
- 强制 在需求分析阶段,如果与系统交互的
User
超过一类并且相关的User Case
超过5个,使用用例图来表达更加清晰地结构化需求 - 强制 如果某个业务对象的状态超过3个,使用状态图来表达并且明确状态变化的各个触发条件
- 强制 如果系统中模型类超过5个,并且存在复杂的依赖关系,使用类图来表达并且明确类之间的关系
- 强制 如果系统中超过2个对象之间存在协作关系,并且需要表示复杂的处理流程,使用活动图来表示
- 推荐 谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现
后续添加
- 对于暂时被注释掉,后续可能恢复使用的代码片段,在注释代码的上方统一使用三斜杠(
///
)来说明注释掉代码的理由