Java开发手册之阿里巴巴
前言
《Java开发手册》是阿里巴巴集团技术团队的集体智慧结晶和经验总结,经历了多次大规模一线实战的检验及不断完善,公开到业界后,众多社区开发者踊跃参与,共同打磨完善,系统化地整理成册,当前的版本是嵩山版。对软件来说,适当的规范和标准绝不是消灭代码内容的创造性、优雅性,而是限制过度个性化,以一种普遍认可的统一方式一起做事,提升协作效率,降低沟通成本。代码的字里行间流淌的是软件系统的血液,质量的提升是尽可能少踩坑,杜绝踩重复的坑,切实提升系统稳定性,码出质量。
注意:此文章管仅笔者认为重要的,或者需要记住的,重要的记录下来,所写的仅为部分内容,供读者参考。另外附上:Java开发规约IDE插件
目录
一、编程规约
(一)命名风格
- 【强制】代码中的命名均不能以下划线或美元符号开头或结束
- 【强制】所有命名均严禁使用中英结合的方式,更不能直接以中文方式命名
- 【强制】类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO /PO / UID 等
- 【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格
- 【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长 正例: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 的映射关系 反例:定义为基本数据类型 Boolean isDeleted 的属性,它的方法也是 isDeleted(),框架在反向解析的时候,“误以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常
- 【强制】包名统一小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式
- 【强制】避免子父类的成员变量或不同代码块之间的局部变量之间采用相同命名
- 【推荐】命名时尽量使用编程元素的完整单词表达,增加可读性
- 【推荐】命名时如果使用了设计模式,在命名时尽量体现具体模式
- 【推荐】接口类中的方法和属性都不要加任何修饰符(public也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,确定与接口方法相关,并且是整个应用的基础常量。 说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默认实现
- 【推荐】接口和实现类的命名中,如果是形容能力的接口名称,取对应的形容词为接口名(通常是–able 的形容词),例如:AbstractTranslator 实现 Translatable 接口
- 【参考】枚举类名都加上Enum后缀,成员名称全部大写,单词用下划线分隔
- 【参考】查询单个对象用get做前缀,多个对象用list做前缀,统计数目用count做前缀
(二)常量定义
- 【强制】代码中不允许出现未先定义的常量
- 【推荐】常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量 跨应用共享常量:放置在二方库中,通常是 client.jar 中的 constant 目录下 应用内共享常量:放置在一方库中,通常是子模块中的 constant 目录下 子工程内部共享常量:即在当前子工程的 constant 目录下 包内共享常量:即在当前包下单独的 constant 目录下 类内共享常量:直接在类内部 private static final 定义
- 【推荐】如果变量值在某一个区间范围变化用枚举定义,例如一年多少季节,一周多少天
(三)代码格式
- 【强制】采用 4 个空格缩进,禁止使用 Tab 字符。IDEA 设置 Tab 为 4 个空格时,请勿勾选 Use tab character;而在 Eclipse 中,必须勾选 insert spaces for tabs。
- 【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则: 1)第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考示例 2)运算符与下文一起换行 3)方法调用的点符号与下文一起换行 4)方法调用中的多个参数需要换行时,在逗号后进行 5)在括号前不要换行
- 【强制】方法参数在定义和传入时,多个参数逗号后面必须加空格
- 【强制】IDE 的 text file encoding 设置为 UTF-8; IDE 中文件的换行符使用 Unix 格式,不要
使用 Windows 格式 - 【推荐】单个方法的总行数不超过 80 行
(四)OOP规约
- 【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可
- 【强制】所有的覆写方法,必须加@ Override 注解
- 【强制】外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生
影响。接口过时必须加@ Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么 - 【强制】不能使用过时的类或方法
- 【强制】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals,推荐使用 JDK7 引入的工具类 java.util.Objects#equals(Object a, Object b)
- 【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
说明:对于 Integer var = ? 在-128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生,
会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都
会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断 - 【强制】任何货币金额,均以最小货币单位且整型类型来进行存储
- 【强制】浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals来判断 说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数
- 【强制】BigDecimal 的等值比较应使用 compareTo()方法,而不是 equals()方法
- 【强制】禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象 正例:优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。BigDecimal recommend1 = new BigDecimal("0.1");BigDecimal recommend2 = BigDecimal.valueOf(0.1);
- 【强制】1)所有的POJO类必须使用包装类型 2)RPC 方法的返回值和参数必须使用包装数据类型 3)【推荐】所有的局部变量使用基本数据类型
- 【强制】定义 DO/DTO/VO 等 POJO 类时,不要设定任何属性默认值
- 【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中
- 【强制】POJO 类必须写 toString 方法,如果继承了另一个 POJO 类,注意在前面加一下 super.toString
- 【强制】禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx()和 getXxx()方法
- 【推荐】使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,否则会有抛 IndexOutOfBoundsException 的风险
(五)日期时间
- 【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y
- 【强制】获取当前毫秒数:System.currentTimeMillis(); 而不是 new Date().getTime() 说明:如果想获取更加精确的纳秒级时间值,使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间
等场景,推荐使用 Instant 类 - 【强制】不允许在程序任何地方中使用:1)java.sql.Date。 2)java.sql.Time。3)java.sql.Timestamp
- 【强制】不要在程序中写死一年为 365 天,避免在公历闰年时出现日期转换错误或程序逻辑错误,如需或许今年年数:// 获取今年的天数 int daysOfThisYear = LocalDate.now().lengthOfYear(); // 获取指定某年的天数 LocalDate.of(2011, 1, 1).lengthOfYear();
- 【推荐】使用枚举值来指代月份。如果使用数字,注意 Date,Calendar 等日期相关类的月份month 取值在 0-11 之间
(六)集合处理
- 【强制】关于 hashCode 和 equals 的处理,需注意:
①只要覆写 equals,就必须覆写 hashCode
②因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写
③如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals这两种方法 - 【强制】在使用 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
- 【强制】使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常
- 【强制】Collections 类返回的对象,如: emptyList() / singletonList() 等都是 immutable list,不可对其进行添加或者删除元素的操作
- 【强制】在 subList 场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常
- 【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组
- 【强制】在使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行NPE 判断
- 【强制】使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/remove/clear 方法会抛出 UnsupportedOperationException 异常
- 【强制】泛型通配符 <? extends T> 来接收返回的数据,此写法的泛型集合不能使用 add 方法,而 <? super T> 不能使用 get 方法,两者在接口调用赋值的场景中容易出错
- 【强制】在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof 判断,避免抛出 ClassCastException 异常
- 【强制】不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁
- 【强制】在 JDK 7 版本及以上, Comparator 实现类要满足如下三个条件,不然 Arrays . sort ,Collections . sort 会抛 IllegalArgumentException 异常
- 【推荐】集合泛型定义时,在 JDK7 及以上,使用 diamond 语法或全省略
- 【推荐】集合初始化时,指定集合初始值大小
- 【推荐】使用 entrySet 遍历 Map 类集合 KV ,而不是 keySet 方式进行遍历
- 【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
集合类 Key Value Super 说明 Hashtable 不允许为 null 不允许为 null Dictionary 线程安全 ConcurrentHashMap 不允许为 null 不允许为 null AbstractMap 锁分段技术(JDK8:CAS) TreeMap 不允许为 null 允许为 null AbstractMap 线程不安全 HashMap 允许为 null 允许为 null AbstractMap 线程不安全
(七)并发处理
- 【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全
- 【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
- 【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
- 【强制】 SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static ,必须加锁,或者使用 DateUtils 工具类
- 【强制】 必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用 try-finally 块进行回收
- 【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁 ; 能锁区块,就不要锁整个方法体 ; 能用对象锁,就不要用类锁对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
- 【强制】在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁
- 【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同
- 【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据
- 【强制】多线程并行处理定时任务时, Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题
- 【推荐】资金相关的金融敏感信息,使用悲观锁策略。乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新
- 【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至await 方法,直到超时才返回结果
- 【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed导致的性能下降通过双重检查锁 (double - checked locking)( 在并发场景下 ) 存在延迟初始化的优化问题隐患 ( 可参考 The " Double - Checked Locking is Broken " Declaration) ,推荐解决方案中较为简单一种 ( 适用于 JDK 5 及以上版本 ) ,将目标属性声明为 volatile 型,比如将 helper 的属性声明修改为`private volatile Helper helper = null;
(八)控制语句
- 【强制】在一个 switch 块内,每个 case 要么通过 continue/break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default语句并且放在最后,即使它什么代码也没有
- 【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null 判断
- 【强制】在 if / else / for / while / do 语句中必须使用大括号
- 【强制】在高并发场景中,避免使用”等于”判断作为中断或退出的条件,因为如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替
- 【推荐】表达异常的分支时,少用 if-else 方式 ,如果非使用 if()...else if()...else...方式表达逻辑,避免后续代码维护困难,请勿超过 3 层。超过三层的可以使用卫语句、策略模式、状态模式等来实现
- 【推荐】公开接口需要进行入参保护,尤其是批量操作的接口
(九)注释规约
- 【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容*/格式,不得使用// xxx 方式
- 【强制】所有的抽象方法 ( 包括接口中的方法 ) 必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能
- 【强制】方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/* */注释,注意与代码对齐
- 【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途
(十)前后端规约
- 【强制】前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{},减少前端很多琐碎的 null 判断
- 【强制】服务端发生错误时,返回给前端的响应信息必须包含 HTTP 状态码,errorCode、errorMessage、用户提示信息四个部分
常见的 HTTP 状态码 200 表明该请求被成功地完成,所请求的资源发送到客户端 401 请求要求身份验证,常见对于需要登录而用户未登录的情况 403 服务器拒绝请求,常见于机密信息或复制其它登录用户链接访问服务器的情况 404 服务器无法取得所请求的网页,请求资源不存在 500 服务器内部错误 - 【强制】对于需要使用超大整数的场景,服务端一律使用 String 字符串类型返回,禁止使用Long 类型,因为Java 服务端如果直接返回 Long 整型数据给前端,JS 会自动转换Number类型(注:此类型为双精度浮点数,表示原理与取值范围等同于 Java 中的 Double)。Long 类型能表示的最大值是 2 的 63 次方-1,在取值范围之内,超过 2 的 53 次方 (9007199254740992)的数值转化为 JS 的 Number 时,有些数值会有精度损失。扩展说明,在 Long 取值范围内,任何 2 的指数次整数都是绝对不会存在精度损失的,所以说精度损失是一个概率问题。若浮点数尾数位与指数位空间不限,则可以精确表示任何整数,但很不幸,双精度浮点数的尾数位只有 52 位。
- 【强制】HTTP 请求通过 URL 传递参数时,不能超过 2048 字节
- 【强制】HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错。例如:nginx 默认限制是 1MB,tomcat 默认限制为 2MB,当确实有业务需要传较大内容时,可以通过调大服务器端的限制。
- 【强制】服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL 统一代理模块生成,否则会因线上采用 HTTPS 协议而导致浏览器提示“不安全”,并且还会带来 URL 维护
不一致的问题 - 【推荐】前后端的时间格式统一为"yyyy-MM-dd HH:mm:ss",统一为 GMT
(十一)其他
- 【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度
- 【强制】避免用 Apache Beanutils 进行属性的 copy,因为Apache BeanUtils 性能较差,可以使用其他方案比如 Spring BeanUtils, Cglib BeanCopier,注意均是浅拷贝
- 【强制】注意 Math . random() 这个方法返回是 double 类型,注意取值的范围 0≤ x <1 ( 能够取到零值,注意除零异常 ) ,如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法
二、异常日志
(一)错误码
- 【强制】全部正常,但不得不填充错误码时返回五个零:00000
- 【强制】错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。错误产生来源分为 A/B/C,A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题;B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;四位数字编号从 0001 到 9999,大类之间的步长间距预留 100
- 【强制】错误码使用者避免随意定义新的错误码
- 【强制】错误码不能直接输出给用户作为提示信息使用
(二)异常处理
- 【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等
- 【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容
- 【强制】事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务
- 【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。说明:如果 JDK7 及以上,可以使用 try-with-resources 方式
- 【强制】不要在 finally 块中使用 return。因为try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点
- 【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类
- 【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable类来进行拦截。说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出NoSuchMethodError 呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError
-
注意 NPE 产生的场景 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE 数据库的查询结果可能为 null 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null 远程调用返回对象时,一律要求进行空指针判断,防止 NPE 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE 推荐:使用 JDK8 的 Optional 类来防止 NPE 问题
(三)日志规约
- 【强制】应用中不可直接使用日志系统 (Log 4 j 、 Logback) 中的 API ,而应依赖使用日志框架(SLF4J、JCL--Jakarta Commons Logging) 中的 API ,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一
- 【强制】所有日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以“应用名.log”来保存,保存在/home/admin/应用名/logs/目录下,过往日志格式为: {logname}.log.{保存日期},日期格式:yyyy-MM-dd
- 【强制】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不少于六个月,并且进行网络多机备份
- 【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。例如: logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
- 【强制】对于 trace / debug / info 级别的日志输出,必须进行日志级别的开关判断
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志 if (logger.isDebugEnabled()) { logger.debug("Current ID is: {} and name is: {}", id, getName()); } - 【强制】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity = false
<logger name="com.taobao.dubbo.config" additivity="false"> - 【强制】生产环境禁止直接使用 System.out 或 System.err 输出日志或使用e.printStackTrace()打印异常堆栈
- 【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出
logger.error("inputParams:{} and errorMessage:{}", 各类参数或者对象 toString(), e.getMessage(), e); - 【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String
三、单元测试
- 【强制】好的单元测试必须遵守 AIR 原则【Automatic(自动化)、Independent(独立性)、Repeatable(可重复)】
- 【强制】单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证
- 【强制】保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间决不能互相调用,也不能依赖执行的先后次序
- 【强制】单元测试是可以重复执行的,不能受到外界环境的影响
- 【强制】对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别
- 【强制】核心业务、核心应用、核心模块的增量代码确保单元测试通过
- 【强制】单元测试代码必须写在如下工程目录: src/test/java ,不允许写在业务代码目录下
四、安全规约
- 【强制】隶属于用户个人的页面或者功能必须进行权限控制校验
- 【强制】用户敏感数据禁止直接展示,必须对展示数据进行脱敏,例如:中国大陆个人手机号码显示:139****1219,隐藏中间 4 位,防止隐私泄露
- 【强制】用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库
- 【强制】用户请求传入的任何参数必须做有效性验证,忽略参数校验可能会导致:page size 过大导致内存溢出、恶意 order by 导致数据库慢查询、缓存击穿、SSRF、任意重定向、SQL 注入,Shell 注入,反序列化注入、正则输入源串拒绝服务 ReDoS
- 【强制】禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据
- 【强制】表单、 AJAX 提交必须执行 CSRF 安全验证
- 【强制】URL 外部重定向传入的目标地址必须执行白名单过滤
- 【强制】在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损
- 【推荐】发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略
五、MySQL数据库
(一)建表规约
- 【强制】表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)
注意:POJO 类中的任何布尔类型的变量,都不要加 is 前缀,所以,需要在<resultMap>设置从 is_xxx 到Xxx 的映射关系。数据库表示是与否的值,使用 tinyint 类型,坚持 is_xxx 的命名方式是为了明确其取值含义与取值范围。说明:任何字段如果为非负数,必须是 unsigned
- 【强制】表名、字段名必须使用小写字母或数字 , 禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑
- 【强制】表名不使用复数名词
- 【强制】禁用保留字,如 desc、range、match、delayed 等
- 【强制】主键索引名为 pk_ 字段名;唯一索引名为 uk _字段名 ; 普通索引名则为 idx _字段名
- 【强制】小数类型为 decimal,禁止使用 float 和 double
- 【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型
- 【强制】 varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text ,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
- 【强制】表必备三字段:id, create_time, update_time
- 【推荐】库名与应用名称尽量一致
- 【推荐】如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释
- 【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致
- 【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表
(二)索引规约
- 【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引
不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
- 【强制】超过三个表禁止 join 。需要 join 的字段,数据类型保持绝对一致 ; 多表关联查询时,保证被关联的字段需要有索引
- 【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度
- 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引
- 【推荐】利用延迟关联或者子查询优化超多分页场景
- 【推荐】建组合索引的时候,区分度最高的在最左边
- 【推荐】防止因字段类型不同造成的隐式转换,导致索引失效
(三)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。 - 【强制】代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句
- 【强制】不得使用外键与级联,一切外键概念必须在应用层解决
- 【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性
-
【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或
表名)进行限定 -
【推荐】 in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控
制在 1000 个之内
(四)ORM映射
- 【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明
- 【强制】POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射
- 【强制】不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义<resultMap>;反过来,每一个表也必然有一个<resultMap>与之对应
- 【强制】sql. xml 配置参数使用:#{},# param # 不要使用${} 此种方式容易出现 SQL 注入
- 【强制】iBATIS 自带的 queryForList(String statementName , int start , int size) 不推荐使用,因为其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList 取start,size 的子集合
- 【强制】不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出
- 【强制】更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间
- 【参考】@Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等
六、工程结构
(一)应用分层
- 【强制】根据业务架构实践,结合业界分层规范与流行技术框架分析,推荐分层结构如图所示,默认上层依赖于下层,箭头关系表示可直接依赖,如:开放 API 层可以依赖于 Web 层(Controller 层),也可以直接依赖于 Service 层,依此类推:
开放 API 层:可直接封装 Service 接口暴露成 RPC 接口;通过 Web 封装成 http 接口;网关控制层等。 终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,JSP 渲染,移动端展示等。 Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等 Service 层:相对具体的业务逻辑服务层。
Manager 层:通用业务处理层,它有如下特征:
1)对第三方平台封装的层,预处理返回结果及转化异常信息,适配上层接口
2)对 Service 层通用能力的下沉,如缓存方案、中间件通用处理
3)与 DAO 层交互,对多个 DAO 的组合复用
DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase、OB 等进行数据交互
第三方服务:包括其它部门 RPC 服务接口,基础平台,其它公司的 HTTP 接口,如淘宝开放平台、支付宝付款服务、高德地图服务等
外部数据接口:外部(应用)数据存储服务提供的接口,多见于数据迁移场景中 - 【参考】( 分层异常处理规约 ) 在 DAO 层,产生的异常类型有很多,无法用细粒度的异常进行 catch ,使用 catch(Exception e) 方式,并 throw new DAOException(e) ,不需要打印日志,因为日志在 Manager / Service 层一定需要捕获并打印到日志文件中去,如果同台服务器再打日志,浪费性能和存储。在 Service 层出现异常时,必须记录出错日志到磁盘,尽可能带上参数信息,相当于保护案发现场。 Manager 层与 Service 同机部署,日志方式与 DAO 层处理一致,如果是单独部署,则采用与 Service 一致的处理方式。 Web 层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,尽量加上友好的错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回
- 【参考】分层领域模型规约:
DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象
DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象
BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象
Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输
VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象
(二)二方库依赖
- 【强制】定义 GAV 遵从以下规则:
1) GroupID 格式:com.{公司/BU }.业务线 [.子业务线],最多 4 级。
说明:{公司/BU} 例如:alibaba/taobao/tmall/aliexpress 等 BU 一级;子业务线可选。
正例:com.taobao.jstorm 或 com.alibaba.dubbo.register
2) ArtifactID 格式:产品线名-模块名。语义不重复不遗漏,先到中央仓库去查证一下。
正例:dubbo-client / fastjson-api / jstorm-tool
3) Version:详细规定参考下方 - 【强制】二方库版本号命名方式:主版本号.次版本号.修订号
1)主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级。
2) 次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改。
3) 修订号:保持完全兼容性,修复 BUG、新增次要功能特性等 - 【强制】线上应用不要依赖 SNAPSHOT 版本 ( 安全包除外 ) ;正式发布的类库必须先去中央仓库进行查证,使 RELEASE 版本号有延续性,且版本号不允许覆盖升级
- 【强制】二方库的新增或升级,保持除功能点之外的其它 jar 包仲裁结果不变。如果有改变,必须明确评估和验证
- 【强制】二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用枚举类型或者包含枚举类型的 POJO 对象
- 【强制】禁止在子项目的 pom 依赖中出现相同的 GroupId ,相同的 ArtifactId ,但是不同的Version
- 【推荐】所有 pom 文件中的依赖声明放在< dependencies >语句块中,所有版本仲裁放在< dependencyManagement >语句块中
- 【推荐】不要使用不稳定的工具包或者 Utils 类
(三)服务器
- 【推荐】高并发服务器建议调小 TCP 协议的 time _ wait 超时时间。因为操作系统默认 240 秒后,才会关闭处于 time_wait 状态的连接,在高并发访问下,服务器端会因为处于 time_wait 的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值
- 【推荐】调大服务器所支持的最大文件句柄数 (File Descriptor ,简写为 fd)。因为主流操作系统的设计是将 TCP/UDP 连接采用与文件一样的方式去管理,即一个连接对应于一个 fd。主流的linux服务器默认所支持最大fd数量为1024,当并发连接数很大时很容易因为fd不足而出现“opentoo many files”错误,导致新的连接无法建立。建议将 linux 服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关)
- 【推荐】给JVM环境参数设置-XX:+HeapDumpOnOutOfMemoryError 参数,让JVM碰到OOM场景时输出 dump 信息
- 【推荐】在线上生产环境, JVM 的 Xms 和 Xmx 设置一样大小的内存容量,避免在 GC 后调整堆大小带来的压力
- 【参考】服务器内部重定向必须使用 forward; 外部重定向地址必须使用 URL Broker 生成,否则因线上采用 HTTPS 协议而导致浏览器提示“不安全“。此外,还会带来 URL 维护不一致的问题
七、设计规约
- 【强制】存储方案和底层数据结构的设计获得评审一致通过,并沉淀成为文档
- 【强制】在需求分析阶段,如果与系统交互的 User 超过一类并且相关的 User Case 超过 5 个,使用用例图来表达更加清晰的结构化需求
- 【强制】如果某个业务对象的状态超过 3 个,使用状态图来表达并且明确状态变化的各个触发条件
- 【强制】如果系统中某个功能的调用链路上的涉及对象超过 3 个,使用时序图来表达并且明确各调用环节的输入与输出
- 【强制】如果系统中模型类超过 5 个,并且存在复杂的依赖关系,使用类图来表达并且明确类之间的关系
- 【强制】如果系统中超过 2 个对象之间存在协作关系,并且需要表示复杂的处理流程,使用活动图来表示

浙公网安备 33010602011771号