# 后端规范
# 命名规范
- 【强制】 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例:name / name / $name / name / name$ / name
- 【强制】 所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,纯拼音命名方式更要避免采用。
正例:ali / alibaba / taobao / cainiao/ aliyun/ youku / hangzhou 等国际通用的名称,可视同英文。
反例:DaZhePromotion [打折] / getPingfenByName() [评分] / String fw[福娃] / int 某变量 = 3
- 【强制】 代码和注释中都要避免使用任何语言的种族歧视性词语。
正例:日本人 / 印度人 / blockList / allowList / secondary
反例:RIBENGUIZI / Asan / blackList / whiteList / slave
- 【强制】 类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。
正例: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 前缀,否则部分框架解析会引起序列化错误。
反例:定义为基本数据类型 Boolean isDeleted
的属性,它的方法也是 isDeleted()
,框架在反向解析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。
10.【强制】 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
正例:应用工具类包名为 com.alibaba.ei.kunlun.aap.util
、类名为 MessageUtils
(此规则参考 spring 的框架结构)
11.【强制】 避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。
说明:子类、父类成员变量名相同,即使是 public
类型的变量也能够通过编译,另外,局部变量在同一方法内的不同代码块中同名也是合法的,这些情况都要避免。对于非 setter/getter 的参数名称也要避免与成员变量名称相同。
反例:
public class ConfusingName {
public int stock;
// 非 setter/getter 的参数名称,不允许与本类成员变量同名
public void get(String alibaba) {
if (condition) {
final int money = 666;
// ...
}
for (int i = 0; i < 10; i++) {
// 在同一方法体中,不允许与其它代码块中的 money 命名相同
final int money = 15978;
// ...
}
}
}
class Son extends ConfusingName {
// 不允许与父类的成员变量名称相同
public int stock;
}
12.【强制】 杜绝完全不规范的缩写,避免望文不知义。
反例:AbstractClass
缩写成 AbsClass
;condition
缩写成 condi
;Function
缩写”成 Fu
,此类随意缩写严重降低了代码的可阅读性。
13.【推荐】 为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达。
正例:对某个对象引用的 volatile
字段进行原子更新的类名为 AtomicReferenceFieldUpdater
。
反例:常见的方法内变量为 int a
;的定义方式。
14.【推荐】 在常量与变量的命名时,表示类型的名词放在词尾,以提升辨识度。
正例:startTime / workQueue / nameList / TERMINATED_THREAD_COUNT
反例:startedAt / QueueOfWork / listName / COUNT_TERMINATED_THREAD
15.【推荐】 如果模块、接口、类、方法使用了设计模式,在命名时需体现出具体模式。
说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
正例:
public class OrderFactory
public class LoginProxy
public class ResourceObserver
16.【推荐】 接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,确定与接口方法相关,并且是整个应用的基础常量。
正例:接口方法签名 void commit();
接口基础常量 String COMPANY = "alibaba";
反例:接口方法定义 public abstract void f();
说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现。
17.接口和实现类的命名有两套规则:
1)【强制】 对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用Impl 的后缀与接口区别。
正例:CacheServiceImpl 实现 CacheService 接口。
2)【推荐】 如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形容词)。
正例:AbstractTranslator 实现 Translatable 接口。
18.【参考】枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。
正例:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKNOWN_REASON。
19.【参考】各层命名规约:
A) Service/DAO 层方法命名规约
获取对象的方法用
select
做前缀。获取多个对象的方法用
list
做结尾获取统计值的方法用
count
做结尾插入的方法用
save/insert
做前缀删除的方法用
remove/delete
做前缀。修改的方法用
update
做前缀。
# 注释规范
- 【强制】 类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用// xxx 方式。
说明:在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释;在 IDE中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
- 【强制】 所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
说明:对子类的实现要求,或者调用注意事项,请一并说明。
- 【强制】 所有的类都必须添加创建者和创建日期。
说明:在设置模板时,注意 IDEA 的@author 为${USER}
,而日期的设置统一为 yyyy/MM/dd 的格式。
正例:
/**
* xxxxx
* @author yangguanbao
* @date 2016/10/31
*/
- 【强制】 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/* */注释,注意与代码对齐。
private void serviceMethods() {
//单行注释
代码块1
/**
* 多行注释
* 多行注释
*/
代码块2
}
- 【推荐】 与其“半吊子”英文来注释,不如用中文注释把问题说清楚。专有名词与关键字保持英文原文即可。
反例:“TCP 连接超时”解释成“传输控制协议连接超时”,理解反而费脑筋。
- 【推荐】 代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。
说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。
【推荐】 在类中删除未使用的任何字段、方法、内部类;在方法中删除未使用的任何参数声明与内部变量。
【参考】谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除。
说明:代码被注释掉有两种可能性:1)后续会恢复此段代码逻辑。2)永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉即可,假如需要查阅历史代码,登录代码仓库即可。
9.【参考】对于注释的要求:第一、能够准确反映设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看的,使其能够快速接替自己的工作。
10.【参考】好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释又是相当大的负担。
反例:
// put elephant into fridge
put(elephant, fridge);
方法名 put,加上两个有意义的变量名 elephant
和 fridge
,已经说明了这是在干什么,语义清晰的代码不需要额外的注释。
11.【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
待办事宜(TODO):(标记人,标记时间,[预计处理时间])表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个 Javadoc 标签)。
错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。
12.【参考】类说明和申明。类说明和上面引用包之间必须要有空行,类说明和类申明之间不需要空行类说明必须包含描述@author、@date。类注释一般必须放在所有的“import”语句之后,类定义之前,主要声明该类可以做什么,以及创建者、创建日期等一些信息。
13.【推荐】 Java doc常用标签。
(1)@author 作者名,
(2)@version 版本号[,时间],
(3)@see 相关的连接类、方法,
(4)@since 类或方法从什么版本开始存在的,
(5)@param 对方法参数描述,
(6)@return 对返回值的描述,
(7)@throws 抛出异常,
(8)@deprecated 不建议使用或将要废除的方法或类。使用时一定注意。
# 日志规范
# 1. 【强制】 应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架SLF4J中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
说明:日志框架SLF4J的使用方式(推荐使用 SLF4J)
使用 SLF4J:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);
# 2. 【强制】 所有日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以“应用名.log”来保存,保存在/home/admin/应用名/logs/目录下,过往日志
格式为: {logname}.log.{保存日期}
,日期格式:yyyy-MM-dd
正例:以 aap 应用为例,日志保存在/home/admin/aapserver/logs/aap.log,历史日志名称为aap.log.2016-08-01
# 3. 【强制】 根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不少于六个月,并且进行网络多机备份。
# 4. 【推荐】 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log。logType:日志类型,如 stats/monitor/access 等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。
说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。
正例:mppserver 应用中单独监控时区转换异常,如:mppserver_monitor_timeZoneConvert.log
# 5. 【强制】 在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
# 6. 【强制】 对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断。
说明:虽然在 debug(参数)的方法体内第一行代码 isDisabled(Level.DEBUG_INT)为真时(Slf4j 的常见实现Log4j 和 Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName())这种参数内有 getName()方法调用,无谓浪费方法调用的开销。
正例:
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) {
logger.debug("Current ID is: {} and name is: {}", id, getName());
}
# 7. 【强制】 避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false。
正例:<logger name="com.taobao.dubbo.config" additivity="false">
# 8. 【强制】 生产环境禁止直接使用 System.out 或 System.err 输出日志或使用e.printStackTrace()打印异常堆栈。
说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。
# 9. 【强制】 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。
正例:logger.error("inputParams:{} and errorMessage:{}", 各类参数或者对象 toString(), e.getMessage(), e);
# 10.【推荐】 日志打印时禁止直接用 JSON 工具将对象转换成 String。
说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。
正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString()方法。
# 11.【推荐】 谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
# 12.【推荐】 可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。
# 13.【推荐】 尽量用英文来描述日志错误信息,如果日志中的错误信息用英文描述不清楚的话使用中文描述即可,否则容易产生歧义。
说明:国际化团队或海外部署的服务器由于字符集问题,使用全英文来注释和描述日志错误信息
# 14.【推荐】 禁止在大循环中记录日志信息。
# 15.【推荐】 日志输出,有异常时应带上异常对象。
说明:如果没有异常对象信息,会导致对日志进行错误分析时无法定位错误原因
正例:必须使用logger.error("错误分析描述", e)
形式;加上错误分析描述方便从错误日志中查询特定问题的日志。
反例:直接调用e.printStackTrace()
。
# 异常处理规范
# 1. 【强制】 Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。
正例:if (obj != null) {...}
反例:try { obj.method(); } catch (NullPointerException e) {…}
# 2. 【强制】 异常捕获后不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
# 3. 【强制】 catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。
对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
# 4. 【强制】 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
# 5. 【强制】 事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。
# 6. 【强制】 finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
# 7. 【强制】 不要在 finally 块中使用 return。
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
反例:
private int x = 0;
public int checkReturn() {
try {
// x 等于 1,此处不返回
return ++x;
} finally {
// 返回的结果是 2
return ++x;
}
}
# 8. 【强制】 捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
# 9. 【强制】 在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable类来进行拦截。
说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出NoSuchMethodError 呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
# 10.【推荐】 方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
说明:本手册明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回 null 的情况。
# 11.【推荐】 防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
- 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f() { return Integer 对象}
, 如果为 null
,自动解箱抛 NPE。
数据库的查询结果可能为
null
。集合里的元素即使 isNotEmpty,取出的数据元素也可能为
null
。远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
级联调用
obj.getA().getB().getC()
;一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
# 12.【推荐】 定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException()
,
更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定
义过的自定义异常,如:DAOException / ServiceException 等。
# 13.【参考】对于公司外的 http/api 开放接口必须使用 errorCode;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封装 isSuccess()方法、errorCode、
errorMessage;而应用内部直接抛出异常即可。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
# 14.【推荐】 异常处理方式:
说明:
(1)处理掉异常,一般多出现在UI层,使用统一的异常信息显示界面显示异常;
注意:
a.界面显示的异常信息,应是业务语义,让用户知道下一步该怎么处理;
b.不允许不作任何操作,直接隐藏掉异常的作法,有特殊理由,务必输出日志,并用注释说明原因。
(2)将异常转换包装为另一种异常抛出。此时,应该将原始异常作为新异常的cause传入,以便异常信息的追踪;
(3)将异常再次抛出。
# 15.【推荐】 异常实例化时,需要指明子异常信息,对于由其它异常引起的异常,需要将原异常作为cause传入,对于异常信息需要参数化的异常,还需要传入参数信息。
# 安全规范
# 1. 【强制】 隶属于用户个人的页面或者功能必须进行权限控制校验。
说明:防止没有做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容。
# 2. 【强制】 用户敏感数据禁止直接展示,必须对展示数据进行脱敏。
说明:中国大陆个人手机号码显示:139****1219
,隐藏中间 4 位,防止隐私泄露。
# 3. 【强制】 用户请求传入的任何参数必须做有效性验证。
说明:忽略参数校验可能导致:
page size 过大导致内存溢出
恶意 order by 导致数据库慢查询
缓存击穿
任意重定向
SQL 注入,Shell 注入,反序列化注入
正则输入源串拒绝服务 ReDoS
Java 代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用
的是特殊构造的字符串来验证,有可能导致死循环的结果。
# 4. 【强制】 禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据。
# 5. 【强制】 URL 外部重定向传入的目标地址必须执行白名单过滤。
# 6. 【强制】 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。
说明:如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并
造成短信平台资源浪费。
# 7. 【推荐】 发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略。
# 8. 【推荐】 CSRF(Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在 CSRF 漏洞的应用/网站,攻击者可以事先构造好 URL,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。
正例:表单、AJAX 提交必须执行 CSRF 安全验证。
# 9. 【强制】 禁止使用Statement。说明:使用PrepareStament,PrepareStament可以有效防御SQL注入攻击,其原理是:MySQL执行树会根据预设的参数做转义,把注入的SQL当作一个参数执行,而不会当作语句执行。
【强制】 mytabis :xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入。
说明:SQL注入是一种常见攻击方式,由于开发者采用sql 拼凑的方式,用来自网络中不安全的参数形成sql 语句访问数据库, 攻击者常常采用该漏洞组合成非法的sql 语句,使得信息泄露, 访问到本来没有权限查看的内容或者直接破坏数据库信息等。发生SQL Injection 有以下几种方式:
进入程序的数据来自不可信赖的资源。
数据用于动态构造一个SQL查询。
反例:某系统签名大量被恶意修改,即是因为对于危险字符 # --没有进行转义,导致数据库更新时,where后边的信息被注释掉,对全库进行更新。
# 单元测试规范
# 1. 【强制】 好的单元测试必须遵守 AIR 原则。
说明:单元测试在线上运行时,感觉像空气(AIR)一样感觉不到,但在测试质量的保障上,却是非常关键
的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。
A:Automatic(自动化)
I:Independent(独立性)
R:Repeatable(可重复)
# 2. 【强制】 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
# 3. 【强制】 保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序。
反例:method2 需要依赖 method1 的执行,将执行结果作为 method2 的输入。
# 4. 【强制】 单元测试是可以重复执行的,不能受到外界环境的影响。
说明:单元测试通常会被放到持续集成中,每次有代码 check in 时单元测试都会被执行。如果单测对外部
环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。
正例:为了不受外界环境影响,要求设计代码时就把 SUT 的依赖改成注入,在测试时用 spring 这样的 DI
框架注入一个本地(内存)实现或者 Mock 实现。
# 5. 【强制】 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。
说明:只有测试粒度小才能在出错时尽快定位到出错位置。单测不负责检查跨类或者跨系统的交互逻辑,那是集成测试的领域。
# 6. 【强制】 核心业务、核心应用、核心模块的增量代码确保单元测试通过。
说明:新增代码及时补充单元测试,如果新增代码影响了原有单元测试,请及时修正。
# 7. 【强制】 单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。
说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录。
# 8. 【推荐】 单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率都要达到 100%
说明:在工程规约的应用分层中提到的 DAO 层,Manager 层,可重用度高的 Service,都应该进行单元测试。
# 9.【推荐】 在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,单元测试最好覆盖所有测试用例(UC)。
# 10.【推荐】 单元测试作为一种质量保障手段,在项目提测前完成单元测试,不建议项目发布后补充单元测试用例。
# 11.【参考】为了更方便地进行单元测试,业务代码应避免以下情况:
构造方法中做的事情过多。
存在过多的全局变量和静态方法。
存在过多的外部依赖。
存在过多的条件语句。
说明:多层条件语句建议使用卫语句、策略模式、状态模式等方式重构。
# 12.【参考】不要对单元测试存在如下误解:
那是测试同学干的事情。本文是开发手册,凡是本文内容都是与开发同学强相关的。
单元测试代码是多余的。系统的整体功能与各单元部件的测试正常与否是强相关的。
单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。
单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障。
# 性能规范
# 1. 【强制】 禁止在循环中访问数据库。
说明:建议采用批量接口访问。
正例:
BusinessDataServiceHelper.loadFormCache(ids, "zsjt_originalbill");
反例:
for (DynamicObject object : rows) {
BusinessDataServiceHelper.loadSingle(object.get("id"), "zsjt_originalbill");
}
# 2. 【推荐】 查询操作,尽量避免查询所有的字段。
说明:
查询大数据量时应考虑增加更严格的过滤条件或分批查询处理;
应按需取数,除加载单据或基础资料外,取数必须指定字段。
【强制】 禁止在循环中访问redis。
说明:如需大量存取缓存时,可以考虑减少查询次数或增加本地线程缓存减少对Redis压力。
- 【推荐】 频繁访问的数据应增加缓存,缓存的对象类型应考虑最大程度共享。
# 接口规范
# 1. 【强制】 前后端交互的 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 头来确定。
# 2. 【推荐】 前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}。
说明:此条约定有利于数据层面上的协作更加高效,减少前端很多琐碎的 null 判断。
# 3. 【参考】服务端发生错误时,返回给前端的响应信息必须包含 HTTP 状态码,errorCode、
errorMessage、用户提示信息四个部分。
说明:四个部分的涉众对象分别是浏览器、前端开发、错误排查人员、用户。其中输出给用户的提示信息
要求:简短清晰、提示友好,引导用户进行下一步操作或解释错误原因,提示信息可以包括错误原因、上 下文环境、推荐操作等。 errorCode:参考附表 3。errorMessage:简要描述后端出错原因,便于错误排
查人员快速定位问题,注意不要包含敏感数据信息。
正例:常见的 HTTP 状态码如下
200 OK: 表明该请求被成功地完成,所请求的资源发送到客户端。
401 Unauthorized: 请求要求身份验证,常见对于需要登录而用户未登录的情况。
403 Forbidden:服务器拒绝请求,常见于机密信息或复制其它登录用户链接访问服务器的情况。
404 Not Found: 服务器无法取得所请求的网页,请求资源不存在。
500 Internal Server Error: 服务器内部错误。
# 4. 【强制】 在前后端交互的 JSON 格式数据中,所有的 key 必须为小写字母开始的
lowerCamelCase 风格,符合英文表达习惯,且表意完整。
正例:errorCode / errorMessage / assetStatus / menuList / orderList / configFlag
反例:ERRORCODE / ERROR_CODE / error_message / error-message / errormessage /
ErrorMessage / msg
# 5. 【强制】 对于需要使用超大整数的场景,服务端一律使用 String 字符串类型返回,禁止使用
Long 类型。
说明:Java 服务端如果直接返回 Long 整型数据给前端,JS 会自动转换为 Number 类型(注:此类型为双
精度浮点数,表示原理与取值范围等同于 Java 中的 Double
)。Long 类型能表示的最大值是 2 的 63 次方
-1,在取值范围之内,超过 2 的 53 次方 (9007199254740992)的数值转化为 JS 的 Number 时,有些数
值会有精度损失。扩展说明,在 Long 取值范围内,任何 2 的指数次整数都是绝对不会存在精度损失的,所
以说精度损失是一个概率问题。若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,
双精度浮点数的尾数位只有 52 位。
反例:通常在订单号或交易号大于等于 16 位,大概率会出现前后端单据不一致的情况,比如,"orderId":
362909601374617692,前端拿到的值却是: 362909601374617660。
# 6. 【强制】 HTTP 请求通过 URL 传递参数时,不能超过 2048 字节。
说明:不同浏览器对于 URL 的最大长度限制略有不同,并且对超出最大长度的处理逻辑也有差异,2048
字节是取所有浏览器的最小值。
反例:某业务将退货的商品 id 列表放在 URL 中作为参数传递,当一次退货商品数量过多时,URL 参数超长,
传递到后端的参数被截断,导致部分商品未能正确退货。
# 7. 【强制】 HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错。
说明:nginx 默认限制是 1MB,tomcat 默认限制为 2MB,当确实有业务需要传较大内容时,可以通过调
大服务器端的限制。
# 8. 【强制】 在翻页场景中,用户输入参数的小于 1,则前端返回第一页参数给后端;后端发现用
户输入的参数大于总页数,直接返回最后一页。
# 9. 【推荐】 服务器返回信息必须被标记是否可以缓存,如果缓存,客户端可能会重用之前的请求
结果。
说明:缓存有利于减少交互次数,减少交互的平均延迟。
正例:http 1.1 中,s-maxage 告诉服务器进行缓存,时间单位为秒,用法如下,
·
response.setHeader("Cache-Control", "s-maxage=" + cacheSeconds);`
# 10. 【推荐】 服务端返回的数据,使用 JSON 格式而非 XML。
说明:尽管 HTTP 支持使用不同的输出格式,例如纯文本,JSON,CSV,XML,RSS 甚至 HTML。如果我
们使用的面向用户的服务,应该选择 JSON 作为通信中使用的标准数据交换格式,包括请求和响应。此外,
application/JSON 是一种通用的 MIME 类型,具有实用、精简、易读的特点。
# 11. 【推荐】 前后端的时间格式统一为"yyyy-MM-dd HH:mm:ss",统一为 GMT。
# 12.【参考】在接口路径中不要加入版本号,版本控制在 HTTP 头信息中体现,有利于向前兼容。
说明:当用户在低版本与高版本之间反复切换工作时,会导致迁移复杂度升高,存在数据错乱风险。
# 13.【推荐】 GET请求时参数列表要encode
# 14.【推荐】 过深的导航容易导致url膨胀,不易维护,如 GET /zoos/1/areas/3/animals/4 ,尽量使用查询参数代替路径中的实体导航,如 GET/animals?zoo=1&area=3 ;
# 开发流程/设计规范
# 1. 【强制】 存储方案和底层数据结构的设计获得评审一致通过,并沉淀成为文档。
说明:有缺陷的底层数据结构容易导致系统风险上升,可扩展性下降,重构成本也会因历史数据迁移和系
统平滑过渡而陡然增加,所以,存储方案和数据结构需要认真地进行设计和评审,生产环境提交执行后,
需要进行 double check。
正例:评审内容包括存储介质选型、表结构设计能否满足技术方案、存取性能和存储空间能否满足业务发
展、表或字段之间的辩证关系、字段名称、字段类型、索引等;数据结构变更(如在原有表中新增字段)
也需要进行评审通过后上线。
# 2. 【强制】 如果某个业务对象的状态超过 3 个,使用状态图来表达并且明确状态变化的各个触发
条件。
说明:状态图的核心是对象状态,首先明确对象有多少种状态,然后明确两两状态之间是否存在直接转换
关系,再明确触发状态转换的条件是什么。
正例:淘宝订单状态有已下单、待付款、已付款、待发货、已发货、已收货等。比如已下单与已收货这两
种状态之间是不可能有直接转换关系的。
# 3. 【强制】 如果系统中某个功能的调用链路上的涉及对象超过 3 个,使用时序图来表达并且明确
各调用环节的输入与输出。
说明:时序图反映了一系列对象间的交互与协作关系,清晰立体地反映系统的调用纵深链路。
# 4. 【强制】 如果系统中模型类超过 5 个,并且存在复杂的依赖关系,使用类图来表达并且明确类
之间的关系。
说明:类图像建筑领域的施工图,如果搭平房,可能不需要,但如果建造蚂蚁 Z 空间大楼,肯定需要详细
的施工图。
# 5. 【强制】 如果系统中超过 2 个对象之间存在协作关系,并且需要表示复杂的处理流程,使用活
动图来表示。
说明:活动图是流程图的扩展,增加了能够体现协作关系的对象泳道,支持表示并发等。
# 6. 【推荐】 系统架构设计时明确以下目标:
确定系统边界。确定系统在技术层面上的做与不做。
确定系统内模块之间的关系。确定模块之间的依赖关系及模块的宏观输入与输出。
确定指导后续设计与演化的原则。使后续的子系统或模块设计在一个既定的框架内和技术方向上继续演化。
确定非功能性需求。非功能性需求是指安全性、可用性、可扩展性等。
# 7. 【推荐】 需求分析与系统设计在考虑主干功能的同时,需要充分评估异常流程与业务边界。
反例:用户在淘宝付款过程中,银行扣款成功,发送给用户扣款成功短信,但是支付宝入款时由于断网演
练产生异常,淘宝订单页面依然显示未付款,导致用户投诉。
# 8. 【推荐】 类在设计与实现时要符合单一原则。
说明:单一原则最易理解却是最难实现的一条规则,随着系统演进,很多时候,忘记了类设计的初衷。
# 9.【推荐】 谨慎使用继承的方式来进行扩展,优先使用聚合/组合的方式来实现。
说明:不得已使用继承的话,必须符合里氏代换原则,此原则说父类能够出现的地方子类一定能够出现,
比如,“把钱交出来”,钱的子类美元、欧元、人民币等都可以出现。
# 10.【推荐】 系统设计阶段,根据依赖倒置原则,尽量依赖抽象类与接口,有利于扩展与维护。
说明:低层次模块依赖于高层次模块的抽象,方便系统间的解耦。
# 11.【推荐】 系统设计阶段,注意对扩展开放,对修改闭合。
说明:极端情况下,交付的代码是不可修改的,同一业务域内的需求变化,通过模块或类的扩展来实现。
# 12.【推荐】 系统设计阶段,共性业务或公共行为抽取出来公共模块、公共配置、公共类、公共方法等,在系统中不出现重复代码的情况,即 DRY 原则(Don't Repeat Yourself)。
说明:随着代码的重复次数不断增加,维护成本指数级上升。随意复制和粘贴代码,必然会导致代码的重复,
在维护代码时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
正例:一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,这个时候请抽取:
private boolean checkParam(DTO dto) {...}
# 13.【推荐】 避免如下误解:敏捷开发 = 讲故事 + 编码 + 发布。
说明:敏捷开发是快速交付迭代可用的系统,省略多余的设计方案,摒弃传统的审批流程,但核心关键点上
的必要设计和文档沉淀是需要的。
反例:某团队为了业务快速发展,敏捷成了产品经理催进度的借口,系统中均是勉强能运行但像面条一样
的代码,可维护性和可扩展性极差,一年之后,不得不进行大规模重构,得不偿失。
# 14.【参考】设计文档的作用是明确需求、理顺逻辑、后期维护,次要目的用于指导编码。
说明:避免为了设计而设计,系统设计文档有助于后期的系统维护和重构,所以设计结果需要进行分类归
档保存。
# 15. 【参考】可扩展性的本质是找到系统的变化点,并隔离变化点。
说明:世间众多设计模式其实就是一种设计模式即隔离变化点的模式。
正例:极致扩展性的标志,就是需求的新增,不会在原有代码交付物上进行任何形式的修改。
# 16.【参考】设计的本质就是识别和表达系统难点。
说明:识别和表达完全是两回事,很多人错误地认为识别到系统难点在哪里,表达只是自然而然的事情,
但是大家在设计评审中经常出现语焉不详,甚至是词不达意的情况。准确地表达系统难点需要具备如下能
力: 表达规则和表达工具的熟练性。抽象思维和总结能力的局限性。基础知识体系的完备性。深入浅出的 生动表达力。
# 17.【参考】代码即文档的观点是错误的,清晰的代码只是文档的某个片断,而不是全部。
说明:代码的深度调用,模块层面上的依赖关系网,业务场景逻辑,非功能性需求等问题是需要相应的文
档来完整地呈现的。
# 18.【参考】在做无障碍产品设计时,需要考虑到:
- 所有可交互的控件元素必须能被 tab 键聚焦,并且焦点顺序需符合自然操作逻辑。
- 用于登录校验和请求拦截的验证码均需提供图形验证以外的其它方式。
- 自定义的控件类型需明确交互方式。
正例:用户登录场景中,输入框的按钮都需要考虑 tab 键聚焦,符合自然逻辑的操作顺序如下,“输入用
户名,输入密码,输入验证码,点击登录”,其中验证码实现语音验证方式。如果有自定义标签实现的控
件设置控件类型可使用 role
属性。