Java 开发手册

一、编程规约

(一) 命名风格

① 所有编程相关的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。

说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,纯拼音命名方式更要避免采用。
正例:ali / alibaba / taobao / cainiao/ aliyun/ youku / hangzhou 等国际通用的名称,可视同英文。
反例:DaZhePromotion [打折] / getPingfenByName() [评分] / String fw [福娃] / int 某变量 = 3

② 禁止使用非标准的英文缩写。

反例:condition 缩写成 condi

③ 命名的好坏在于其「模糊度」。

  • 如果上下文很清晰,局部变量可以使用 list 这种简略命名,否则应使用 userList 这种更清晰的命名。
  • 禁止 list1 / list2 / list3 这种带编号的命名方式。

④ 类名使用 UpperCamelCase 风格,但以下情形例外:DO / BO / DTO / VO / AO / PO / UID 等。

正例:ForceCode / UserDO / HtmlDTO / XmlService / TcpUdpDeal / TaPromotion
反例:forcecode / UserDo / HTMLDto / XMLService / TCPUDPDeal / TAPromotion

⑤ 方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格。

说明:在使用 IDEA 时自动提示会带上 DO 、 BO 等后缀,需要手动清理保证命名干净

⑥ 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。

正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例:MAX_COUNT / EXPIRED_TIME

⑦ 常量与变量命名时,表示类型的名词放在词尾,以提升辨识度。

说明:集合类型的变量名需体现其数据类型,如 List 类型则变量名就该以 List 为后缀,同理 Set 、Map 等,时间类字段以 Time 为后缀,日期类字段以 Date 为后缀。
正例:orderList / userNameMap / payTime / activityDate

⑧ 抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾;工具类命名使用 Utils 结尾。

⑨ 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用 单数形式,但是类名如果有复数含义,类名可以使用复数形式。

正例:应用工具类包名为 com.alibaba.ei.kunlun.aap.util、类名为 MessageUtils(此规则参考 Spring 的框架结构)

⑩ 接口和实现类的命名,实现类用 Impl 后缀与接口区别,并单独放 impl 包中。

⑪ 枚举类命名使用 Enum 后缀,枚举值名称需全大写,单词间用下划线隔开,枚举字段与其代表含义对应。

说明:枚举类根据用途划分常见的有以下几种

  • 类型类,以 TypeEnum 后缀,如 RecipientTypeEnum
  • 状态类,以 StatusEnum 后缀,如 StaffStatusEnum
  • 其他

⑫ Service / DAO 层方法命名规约:

  • Service 层获取单个对象的方法用 get 做前缀。
  • Service 层获取多个对象的方法用 get 做前缀且 List 结尾,如 getOrderList。
  • DAO 层查询方法用 select 做前缀不需要带对象名,如果 MyBatis DAO 层生成工具中自带的 selectByCondition 方法不满足使用需要自定义的话,推荐命名 selectByCustomCondition。
  • 获取统计值的方法用 count 做前缀。
  • 插入的方法用 insert 做前缀。
  • 删除的方法用 delete / remove 做前缀。
  • 修改的方法用 update 做前缀。
  • 即包含插入又包含修改的方法用 save 做前缀。\

⑬ 领域模型命名规约:

  • 数据对象:xxxDO ,xxx 为数据表名。
  • 持久对象:xxxPO ,xxx 为业务领域相关名称,用于 DAO 层多表联合查询。
  • 数据传输对象:xxxDTO,xxx 为业务领域相关名称。
  • 业务对象:xxxBO,xxx 为业务领域相关名称。
  • 展示对象:xxxVO,xxx 一般为网页名称。
  • 接口传参:以 Param 为后缀。

    同一类型请求或响应参数类建议使用统一前缀,便于排列时展示在一起,必要时候为同一类型实体分包,以便于维护。

⑭ 业务概念命名规约:统一使用已定义的名词,优先与表字段定义一致。

反例:两位工程师对「分类」这一概念分别定义为 Category 及 Classify

(二) 常量定义

① 不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。

② 在 long 或者 Long 赋值时,数值后使用大写字母 L,不能是小写字母 l,小写容易跟数字混淆,造成误解。

说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2?

③ 不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。

说明:大而全的常量类,杂乱无章,使用查找功能才能定位到修改的常量,不利于理解,也不利于维护。
正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类 SystemConfigConsts 下。

④ 如果变量值仅在一个固定范围内变化用 enum 类型来定义。

(三) 代码格式

① 方法之间用一个空行分隔即可,禁止连续空行。

② 方法体内必要情况可以用一个空行增加可读性,不过禁止连续空行。

③ 单行字符数不超过 IDEA 最右侧边界线,超出需要换行,换行时遵循如下原则:

  • 第二行相对第一行缩进 4 个空格,从第三行开始不再继续缩进。
  • 运算符与下文一起换行。
  • 方法调用的点符号与下文一起换行。
  • 方法调用中多个参数需要换行时,在逗号后进行。
  • lambda 表达式如果超出一行字符限制时,则从 stream() 方法后换行,每个方法单独一行。
  • 实例化带 @Builder 注解的类时从 build() 方法后换行,每个字段单独一行 。

④ 接口实现类中 private 方法集中在类底部,方法定义顺序依次是:public 方法 > protected 方法 > private 方法。

⑤ JavaDoc 注释中推荐使用 Map key -> value 的形式代替 Map<key, value>,后者格式化会不断缩进导致格式错乱。

⑥ 推荐经常使用 IDEA 的快捷键格式化下代码,前提是缩进采用 4 个空格而并非 tab 字符。

⑦ 推荐 IDEA 提交代码时勾上 Optimize imports,避免有时候忘记清理无用 imports。

⑧ 调大 IDEA 的 Imports 设置,改为 99。

说明:import 不会降低代码的执行效率,但会影响到代码的编译速度。
位置:Editor > Code Style > Java
Class count to use import with ‘

Names count to use static import with ‘*’

⑨ 在类中删除未使用的任何字段、方法、内部类;在方法中删除未使用的任何参数声明与内部变量。

⑩ 对于一些特殊场景(如使用大量的字符串拼接成一段文字,或者想把大量的枚举值排成一行),为了避免 IDE自动格式化,可以使用 @formatter.off 和 @formatter.on 来包装这段代码,让 IDE 忽略它。

(四) 方法设计

① 单个方法行数尽量不要超过 80 行。

② 方法的语句保持在同一个抽象层级上。

反例:一个方法里,前 20 行代码在进行很复杂的基本价格计算,然后调用一个折扣计算函数,再调用一个赠品计算函数。
正例:将前 20 行也封装成一个价格计算函数,使整个方法在同一抽象层级上。

③ 尽量减少重复代码,必要时候封装方法。

说明:超过 5 行以上的重复代码,都可以考虑抽取成公用方法。

④ 方法参数最好不超过 3 个,最多不超过 7 个。

  • 如果多个参数用属于一个对象,直接传递对象。
  • 将多个参数合并为一个新创建的逻辑对象。
  • 将方法拆分成多个方法,使每个方法所需参数减少。

⑤ 以下情况需要进行参数校验:

  • 调用频次低的方法。
  • 执行时间开销很大的方法。此情形中参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退或者错误,代价更大。
  • 需要极高稳定性和可用性的方法。
  • 对外提供的开放接口,不管是 RPC / HTTP / 公共类库的 API 接口。

⑥ 以下情况不需要进行参数校验:

  • 底层调用频率比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才暴露。
  • 被声明成 private,或其他只会被自己调用的方法。

⑦ 返回值可以为 null,不强制返回空集合或者空对象,但需要添加注释充分说明什么情况下会返回 null。

⑧ 正被外部调用的接口,不允许修改方法签名,避免对接口调用方产生影响。

说明:只能新增接口,并对已过期的接口加 @Deprecated 注解,并清晰的说明新接口。

(五) OOP 规约

① 接口类中的方法和属性不要加任何修饰符,保持代码的简洁性,并加上有效的 JavaDoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,确定与接口方法相关,并且是整个应用的基础常量。

② 避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名访问即可。

③ 所有的覆写方法必须加 @Override 注解 。

④ 接口过时必须加 @Deprecated 注解,并清晰说明采用的新接口或者新服务是什么。

⑤ 不能使用过时的类或者方法。

⑥ Object 的 equals 方法容易抛空指针异常,应使用常量或者确定有值的对象调用 equals。

⑦ 所有相同类型的包装类对象之间值的比较全部使用 equals 方法比较。

⑧ 基本数据类型与包装数据类型的使用标准:

  • 所有 POJO 类属性必须使用包装数据类型。
  • RPC 方法的返回值和参数必须使用包装数据类型。
  • 所有的局部变量推荐使用基本数据类型。

    包装数据类型的坏处:

    • Integer 24 字节,而 int 4 字节。
    • 包装数据类型每次赋值需要额外创建对象,如 Integer num = 200,除非数值在缓存区间内才会复用已缓存对象,Integer.IntegerCache 默认缓存区间为 -128 到 127。
    • 包装数据类型有 == 比较的陷阱。

包装数据类型的好处:

  • 能表达 null 的语义,比如数据库查询结果为 null,如果用基本数据类型有 NPE 风险。
  • 集合需要包装数据类型,除非使用数组或者特殊的基本数据类型集合。
  • 泛型需要包装数据类型。

⑨ 理论上每张表都有对应的 Mapper 及 Service ,Mapper 只允许出现在相应表的 Service 中,不允许在其他 Service 直接注入。

⑩ 原则上不允许在 for 循环体内进行数据库操作,批量插入或批量更新数据时可使用 MyBatis 代码自动生成器中的 batchInsert 或 batchUpdateById 方法。

⑪ 使用常量类中的常量时需指定常量类名,避免出现 import static。

⑫ 原则上不应使用已过期及不稳定的类或方法。

(六) 日期时间

① 日期类推荐使用 Java8 LocalDate 及 LocalDateTime.

② 字段类型为 LocalDateTime 或 LocalDate 时,需要添加 @JsonFormat 注解并设置 pattern 属性来确保返回给前端的时间格式准确

@JsonFormat(pattern = “yyyy-MM-dd HH:mm:ss”)

③ 不允许在程序任何地方中使用:java.sql.Date、java.sql.Time、java.sql.Timestamp。

说明:第 1 个不记录时间,getHours() 抛出异常;第 2 个不记录日期,getYear() 抛出异常;第 3 个在构造方法 super((time / 1000) * 1000),在 Timestamp 属性 fastTime 和 nanos 分别存储秒和纳秒信息。
反例: java.util.Date.after(Date)进行时间比较时,当入参是 java.sql.Timestamp 时,会触发 JDK BUG(JDK9 已修复),可能导致比较时的意外结果。

④ 获取当前毫秒数:System.currentTimeMillis();而不是 new Date().getTime()。

(七) 集合处理

① 判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式。

说明:在某些集合中,前者的时间复杂度为 O(1),而且可读性更好。

② ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常:java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。

说明:subList() 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 本身,而是 ArrayList 的一个视图,对于 SubList 的所有操作最终会反映到原列表上。

③ Collections 类返回的对象,如:emptyList() / singletonList() 等都是 immutable list,不可对其进行添加或者删除元素的操作。

④ 一般情况下需要返回空集合时推荐使用 Collections.emptyList() 或 Collections.emptyMap(),而不是 new ArrayList<>()。

⑤ 推荐引入 guava 包,简化集合操作。

⑥ 使用 entrySet 而不是 keySet 遍历 Map 类集合。

(八) 并发处理

① 创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。

② 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。

说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者「过度切换」的问题。

③ 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

说明:Executors 返回的线程池对象的弊端如下:

  • FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
  • CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

④ 必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用, 如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用 try-finally 块进行回收。

(九) 控制语句

① 在一个 switch 块内,每个 case 要么通过 break/return 来终止,要么注释说明程序继续执行到哪一个 case 为止;在一个 switch 块内都必须包含一个 default 语句并且放在最后,即便它什么代码都没有。

② 在 if / else / for / while / do 语句中必须使用大括号。

③ 三目运算符 condition ? 表达式 1 : 表达式 2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。

④ 表达异常分支时,尽量少用 if-else 方式,可以在不满足条件的 if 条件中直接 return 减少代码的缩进。

⑤ 不要在条件判断中执行其它复杂的语句,将复 杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。

⑥ 避免采用取反逻辑运算符。

说明:取反逻辑不利于快速理解,并且取反逻辑写法一般都存在对应的正向逻辑写法。
正例:使用 if (x < 628) 来表达 x 小于 628。
反例:使用 if (!(x >= 628)) 来表达 x 小于 628。

⑦ 表达式中能造成短路概率较大的逻辑尽量放在前面,使得后面的判断可以免于执行。

(十) 注释规约

① 类、类属性、类方法的注释必须使用 JavaDoc 规范,使用 /*内容/ 格式,不得使用 //xxx 方式。

② 所有的类都必须添加创建者信息。

③ 方法内部单行注释,在被注释语句上方另起一行,使用 // 注释;方法内部多行注释使用 /**/ 注释,注意与代码对齐。

④ 代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。

⑤ 特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描, 经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。

  • 待办事宜(TODO):(标记人,标记时间,[预计处理时间]) 表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个 Javadoc 标签)。
  • 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。

⑥ 通过更清晰的代码来避免注释,让代码不言自明。

⑦ 删除空注释,无意义注释。如果没有想说的,不要留着 IDE 自动生成的,空的 @param,@return,@throws 标记,让代码更简洁。

(十一) 前后端规约

① 接口路由不能使用大写,单词如果需要分隔,统一使用下划线。

② 前后端数据列表相关的接口返回,如果为空,则返回空集合。

③ 服务端发生错误时,返回给前端的响应信息必须包含 HTTP 状态码,errorCode、 errorMessage、用户提示信息四个部分。

说明:四个部分的涉众对象分别是浏览器、前端开发、错误排查人员、用户。其中输出给用户的提示信息,要求简短清晰、提示友好,引导用户进行下一步操作或解释错误原因,提示信息可以包括错误原因、上 下文环境、推荐操作等。 errorCode 自定义。errorMessage 简要描述后端出错原因,便于错误排查人员快速定位问题,注意不要包含敏感数据信息。

④ 服务端返回的数据,使用 JSON 格式而非 XML。

⑤ 在接口路径中不要加入版本号,版本控制在 HTTP 头信息中体现,有利于向前兼容。

说明:当用户在低版本与高版本之间反复切换工作时,会导致迁移复杂度升高,存在数据错乱风险。

⑥ 接口返回参数是确定值时,定义响应参数类而非使用

(十二) 接口文档

① Controller 添加 @Api 注解。

说明:tags 属性用于描述当前控制器用途。
如需分组和排序可通过加字母及数字实现,如 A 5.4 医生收入

② Controller 中使用 PathVariable 形式的接口方法添加 @ApiParam 注解。

说明:name 属性表示字段名,value 属性表示字段描述,required 属性表示是否必传。

1
2
3
4
5
// 正例
@ApiParam(name = "id", value = "词条ID", example = "1", required = true)

// 反例 描述不准确且缺少 example 及 required
@ApiParam(name = "id", value = "id")

② 实体类添加 @ApiModel 注解。

说明:description 属性用于描述当前类含义。
description = ${动作} + ${业务领域名称} + 参数

1
2
3
4
5
6
7
8
// 正例
DiseaseCategoryQueryParam:查询病种参数
DiseaseCategoryCreateParam:新增病种参数
DiseaseCategoryUpdateParam:编辑病种参数
DiseaseCategoryDeleteParam:删除病种参数
// 反例
AssistantSaveParam:顾问保存参数 // 应动词在前名词在后
CustomFormCopyParam:复制表单 // 最后缺少参数两字

③ 实体类字段添加 @ApiModelProperty 注解。

说明:value 属性表示字段名,example 属性表示示例值(字段为集合时示例值用 [] 表示,字段为对象时示例值用 {} 表示),required 属性表示是否必传,非必传时可省略该属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 正例
@ApiModelProperty(value = "疾病名称", example = "伤寒", required = true)
private String diseaseName;

@ApiModelProperty(value = "医生所属医院科室列表", example = "[]", required = true)
private List<DoctorHospitalDepartmentBO> doctorHospitalDepartmentList;

@ApiModelProperty(value = "用户基础信息", example = "{}")
private UserBaseInfoBO baseInfo;

// 反例 缺少 example 及 required
@ApiModelProperty("不良生活习惯名称")
private String badHabitName;

(十三) 其他

① 对于「明确停止使用的代码和配置」,如方法、变量、类、配置文件、动态配置属性等要坚决从程序中清理出去,避免造成过多垃圾代码

② 开发过程中使用 IDEA Alibaba Java Coding Guidelines 插件实时检测,关注 IDEA 右上角分析结果是否为绿勾。

③ 提交代码前先设置 Git 用户名及邮箱,确保使用的是真实姓名及工作邮箱。

④ 避免用 Apache Beanutils 进行属性的 copy。

说明:Apache BeanUtils 性能较差,可以使用其他方案比如 Spring BeanUtils, Cglib BeanCopier,注意均是浅拷贝。

⑤ 变量声明尽量靠近使用的分支,不要过早声明。

说明:如果方法以及退出或进入其他分支,变量就白白初始化了。


二、异常日志

(一) 异常处理

① Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。

说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。
正例:if (obj != null) {…}
反例:try { obj.method(); } catch (NullPointerException e) {…}

② 异常捕获后不要用来做流程控制,条件控制。

说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。

③ catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。 对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。

说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。

④ 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。

⑤ 事务场景中,抛出异常被catch后,如果需要回滚,一定要注意手动回滚事务。

⑥ finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。

说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。

⑦ 不要在 finally 块中使用 return。

说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
反例:

1
2
3
4
5
6
7
8
9
10
11
> private int x = 0;
> public int checkReturn() {
> try {
> // x 等于 1,此处不返回
> return ++x;
> } finally {
> // 返回的结果是 2
> return ++x;
> }
> }
>

(二) 日志规约

① 应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架 (SLF4J)中的 API,使用门面模式的日志框架,有利于维护和 各个类的日志处理方式统一。

说明:日志框架(SLF4J)的使用方式:

1
2
3
4
> import org.slf4j.Logger;
> import org.slf4j.LoggerFactory;
> private static final Logger logger = LoggerFactory.getLogger(Test.class);
>

② 在日志输出时,字符串变量之间的拼接使用占位符的方式。

说明:因为 String 字符串的拼接会使用 StringBuilder 的 append() 方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例:logger.debug(“Processing trade with id: {} and symbol: {}”, id, symbol);

③ 对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断。

说明:虽然在 debug(参数)的方法体内第一行代码 isDisabled(Level.DEBUG_INT) 为真时(Slf4j 的常见实现
Log4j 和 Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName())
这种参数内有 getName()方法调用,无谓浪费方法调用的开销。
正例:

1
2
3
4
5
> // 如果判断为真,那么可以输出 trace 和 debug 级别的日志
> if (logger.isDebugEnabled()) {
> logger.debug("Current ID is: {} and name is: {}", id, getName());
> }
>

④ 避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false。

正例:

⑤ 生产环境禁止直接使用 System.out 或 System.err 输出日志或使用 e.printStackTrace() 打印异常堆栈。

说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。

⑥日志打印时禁止直接用 JSON 工具将对象转换成 String。

说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。
正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString() 方法。

⑦ 谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。

说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。
记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?

⑧可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。

说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。


三、安全规约

① 隶属于用户个人的页面或者功能必须进行权限控制校验。

说明:防止没有做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容。

② 用户敏感数据禁止直接展示,必须对展示数据进行脱敏。

说明:中国大陆个人手机号码显示:139****1219,隐藏中间 4 位,防止隐私泄露。

③ 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入, 禁止字符串拼接 SQL 访问数据库。

反例:某系统签名大量被恶意修改,即是因为对于危险字符 # –没有进行转义,导致数据库更新时,where 后边的信息被注释掉,对全库进行更新。

④ 用户请求传入的任何参数必须做有效性验证。

说明:忽略参数校验可能导致:

  • page size 过大导致内存溢出
  • 恶意 order by 导致数据库慢查询
  • 缓存击穿
  • SSRF(Server-side Request Forge, 服务端请求伪造)
  • 任意重定向
  • SQL 注入,Shell 注入,反序列化注入
  • 正则输入源串拒绝服务 ReDoS

Java 代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用 的是特殊构造的字符串来验证,有可能导致死循环的结果。

⑤ 表单、AJAX提交必须执行CSRF安全验证。

说明:CSRF(Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在 CSRF 漏洞的应用/网站,攻击者可以事先构造好 URL,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。

⑥ 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。

说明:如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。

⑦ 发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略。


四、MySQL 数据库

(一) 建表规约

① 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。

正例:aliyun_admin / rdc_config / level3_name
反例:AliyunAdmin / rdcConfig / level_3_name

② 表名不使用复数名词。

说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。

③ 禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。

④ 唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。

说明:uk_ 即 unique key;idx_ 即 index 的简称。

⑤ 小数类型为 decimal,禁止使用 float 和 double。

说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的 结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。

⑥ varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。

⑦ 表必备四字段:id, create_time, update_time, is_deleted。

说明:其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。create_time, update_time 均为 datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新,is_deleted 表示删除状态,原则上数据不允许物理删除,只允许逻辑删除。

⑧ 表的命名最好是遵循「业务名称_表的作用」。

正例:alipay_task / force_project / trade_config

⑨ 库名与应用名称尽量一致。

⑩ 表、字段末尾必须添加 Comment 注释,修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。

⑪ 字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:

  • 不是频繁修改的字段。
  • 不是唯一索引的字段。
  • 不是 varchar 超长字段,更不能是 text 字段。

正例:各业务线经常冗余存储商品名称,避免查询时需要调用 IC 服务获取。

⑫ 存储引擎使用 InnoDB,字符集使用 utf8bm4。

⑬ 类型 / 状态等枚举字段注释格式统一为:字段描述(枚举值1:含义 枚举值2:含义 枚举值N:含义),其中外部为中文括号,内部为英文冒号。

⑭ 无特殊要求时数值类型字段推荐使用无符号,字段定义增加 unsigned。

⑮ 无特殊要求时把字段定义为 NOT NULL 并且提供默认值。

说明:

  • null 的列使索引 / 索引统计 / 值比较都更加复杂,对 MySQL 来说更难优化。
  • null 这种类型 MySQL 内部需要进行特殊处理,增加数据库处理记录的复杂性;同等条件下,表中有较多空字段的时候,数据库的处理性能会降低很多。
  • null 值需要更多的存储空间,无论是表还是索引中每行中为 null 的列都需要额外的空间来标识。
  • 处理 null 时,只能采用 is null 或 is not null,而不能采用 =、in、<、<>、!=、not in 这些操作符号。如:where name ! = ‘linjian’,如果存在 name 为 null 值的记录,查询结果就不会包含 name 为 null 值的记录。

⑩ 表达是否概念的字段,使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(3)。

(二) 索引规约

① 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。

说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外, 即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。

② 需要 join 的字段,数据类型保持绝对一致;多表关联查询时, 保证被关联的字段需要有索引。

③ 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。

说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度)) / count(*) 的区分度来确定。

④ 利用延迟关联或者子查询优化超多分页场景。

说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
正例:先快速定位需要获取的 id 段,然后再关联:
SELECT t1.* FROM 表 1 as t1, (select id from 表 1 where 条件 LIMIT 100000,20 ) as t2 where t1.id=t2.id

⑤ 建组合索引的时候,区分度最高的在最左边。

正例:如果 where a = ? and b = ?,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。

说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where c > ? and d = ? 那么即使 c 的区分度更高,也必须把 d 放在索引的最前列,即建立组合索引 idx_d_c。

⑥ 防止因字段类型不同造成的隐式转换,导致索引失效。

⑦ 单表索引建议控制在 5 个之内。

⑧ 单个索引字段数不允许超过 5 个。

说明:字段超过 5 个时,实际已经起不到有效过滤数据的作用了。

(三) SQL 语句

① 代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。

② 不得使用外键与级联,一切外键概念必须在应用层解决。

说明:学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。

③ 禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。

④ 对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名进行限定。

说明:对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名,并操作列在多个表中存在时,就会抛异常。
正例:select t1.name from table_first as t1 , table_second as t2 where t1.id = t2.id;

反例:在某业务中,由于多表关联查询语句没有加表的别名的限制,正常运行两年后,最近在某个表中增加一个同名字段,在预发布环境做数据库变更后,线上查询语句出现出 1052 异常:Column ‘name’ in field list is ambiguous。

⑤ SQL 语句中表的别名前加 as,并且以 t1、t2、t3、…的顺序依次命名。

说明:别名可以是表的简称,或者是依照表在 SQL 语句中出现的顺序,以 t1、t2、t3 的方式命名。别名前加 as 使别名更容易识别。
正例:select t1.name from table_first as t1, table_second as t2 where t1.id = t2.id;

⑥ in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。

⑦ SQL 语句中关键字要求大写。

⑧ 在 WHERE 条件的字段上使用函数或者表达式时需注意索引是否生效问题。

⑨ 不允许 DELETE 操作,原则上只允许逻辑删除,特殊情况走审批。

(四) ORM 映射

① 在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。

说明:1)增加查询分析器解析成本。2)增减字段容易与 resultMap 配置不一致。3)无用字段增加网络消耗,尤其是 text 类型的字段。

② sql.xml 配置参数使用:#{},#param# 不要使用 ${} 此种方式容易出现 SQL 注入。

③ 更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。


五、Git 规约

(一) Git 分支规约

① 分支类型

开发分支

用于开发新需求。当需求涉及多个项目时,可以保持分支名一致方便后续识别,提测或修改 BUG 后将该分支合并到部署分支。

bugfix 分支

用于修复线上 BUG。当出现线上问题需要立即修复时,创建该类型分支,测试完成后合并到 master 分支上线。

部署分支

用于 Jenkins 部署。根据不同环境细分为 dev、test、stable 三个分支,原则上不允许在部署分支上直接修改 BUG 或添加新需求。

master 分支

主干分支,也是线上部署分支。以上所有分支必须基于当时最新的 master 分支创建,上线前将开发分支合并到 master 分支。

② 分支命名

开发分支:dev + 日期 + 创建人(姓名首字母) + 需求描述
bugfix 分支:bugfix + 日期 + 创建人 + bug 描述
部署分支:按照不同环境固定为 dev(开发环境)、test(测试环境)、stable(预发环境)

③ 分支维护

分支太多会变得很难管理,并且合并分支会出现代码冲突的风险。所以每次上线后,要及时清理开发分支,未上线的开发分支要经常合并 master 分支避免落后过多版本引起的代码冲突。

(二) Git Commit Message 规约

Git 每次提交代码都需要写 commit message。一般来说,commit message 应该清晰明了,说明本次提交的目的,具体做了什么操作。但是在日常开发中,大家的 commit message 千奇百怪,中英文混合使用、fix bug 等各种笼统的 message 司空见怪,这就导致后续代码维护成本特别大,有时自己都不知道自己的 fix bug 修改的是什么问题。基于以上这些问题,我们希望通过某种方式来监控用户的 git commit message,让规范更好的服务于质量,提高大家的研发效率。

Angular 规范是目前使用最广的写法,比较合理和系统化,并且有配套的工具(IDEA 就有插件支持这种写法)

① Commit Message 格式

1
2
// 冒号后空一格再写 subject
<type>: <subject>
  • type:提交类型
  • subject:对 commit 目的的简短描述

② Commit Message 示例

结合日常开发场景及 Angular 规范,常用的 type 归纳为以下八种:

feat

新功能

1
2
feat: 获取医生列表
feat: 添加按对象字段去重工具类方法

fix

修复 bug。subject 应描述问题现象,而不是解决方案,fix 类型本身就代表修复,故 subject 里无需出现表示修复含义的字样。

1
2
3
4
5
6
7
8
# 正例
fix: double 精度问题
fix: IM 消息撤回时间不正确
fix: 领取会员缺少扫码时间是否超过48小时的校验
# 反例
fix: 代码优化 // type 应为 refactor
fix: 时间为空 // 描述过于笼统
fix: 领取会员时增加时间限制 // 应描述问题,而不是解决方案

docs

修改文档,如修改配置文件

1
2
docs: 修改 application.properties 配置文件的 meta.url 地址
docs: 补充埋点参数说明

style

修改代码格式,不影响代码含义,如格式化、清理无用文件等

1
2
3
4
style: 删除多余空行
style: 清理无用类
style: 清理无用 imports 格式化代码
style: 优化 swagger 及 javadoc 参数说明

refactor

重构,既不是添加新功能也不是修复 bug 的代码改动,如修改方法实现、修改方法或者参数名、调整类包路径

1
2
3
4
refactor: 拆分及优化医生分组 service 实现
refactor: 修改自定义消息推送设置参数名
refactor: 使用线程池分批处理运营规则器任务
refactor: 调整枚举类包路径

test

新增/修改测试用例或添加测试功能

1
2
test: 微信被动回复用户消息测试
test: swagger allowableValues 字段测试

build

修改构建系统或者外部依赖

1
2
3
build: 升级 fastjson 版本
build: 添加 jackson 依赖
build: 修改 beyond-remote 版本

revert

回滚某个提交

1
revert: 微信被动回复用户消息测试


六、开发/测试/上线流程

(一) 开发流程

开发前

  • 需求评审完后,开发人员拆分任务,并估算工时
  • 禅道建任务
  • 设计数据库表结构,并进行表结构评审
  • 基于 master 拉取开发分支
  • 复杂业务逻辑需画时序图或流程图

开发中

  • 进行单元测试及接口自测
  • 前后端联调进行功能自测
  • 更新禅道任务进度

开发完成后

  • 进行充分的流程自测
  • 发送提测申请
  • 提测后持续跟进测试 BUG,解决 BUG 时补充 BUG 原因,便于后续项目复盘
  • 上线后清理开发分支

(二) 测试流程

测试前

  • 测试人员与产品、设计、开发进行测试用例评审
  • 收到开发人员提交的提测申请后进行冒烟测试,若不通过直接打回

测试中

  • 测试环境进行两轮测试,禅道提交 BUG 时明确 BUG 级别,补充必要信息,如用户信息、错误日志截图等
  • 需要模拟线上环境时,需上预发环境再测一轮

测试完成后

  • 测试一轮完成后,通知产品、业务验收及设计走查
  • 测试二轮完成后,发送测试报告

(三) 上线流程

上线前

  • 测试人员基于开发人员的提测申请发送测试报告
  • 项目 PM 制定上线计划以确保线上发布井然有序。上线计划包括但不仅限于:
    • 具体发布时间
    • 需要做哪些前期准备(如系统发布公告、SQL、前后端配置项、服务器环境搭建、代码合并、依赖打包等)
    • 应用发布顺序(先基础服务层再应用层)及发布人员
    • 发布完成后是否需要运营或产品配置相关业务数据
    • 小程序或 APP 是否提交审核
    • 回滚方案
  • 项目 PM 提交系统发布申请,需包含测试报告上线计划
  • 部门主管及产品经理审批系统发布申请

上线中

  • 所有发布人员在项目群中协同,发布完成后及时艾特下游依赖方
  • DBA 执行 SQL,有条件可以通过 SQL 审核平台操作,避免人工误操作
  • 开发人员按上线计划依次发布应用,并检查应用启动情况

上线后

  • 运营或产品经理配置业务数据
  • 测试人员进行回归测试

附⑴:版本历史

版本号 版本日期 创建人 备注
1.0.0 2021-05-12 天明 初稿                                                                       
1.0.1 2021-05-29 天明 ⑴ 增加「异常日志」大类
⑵ 增加「安全规约」大类
⑶ 增加「Git 规约」大类
⑷ 增加「开发/测试/上线流程」大类
⑸「编程规约」增加部分规则
⑹「MySQL 数据库」增加部分规则

附⑵:IDE 代码检测插件

① Alibaba Java Coding Guidelines
② SonarLint
③ QAPlug

附⑶:操作指南

① 设置 Git 用户名及邮箱

全局设置

对当前用户生效,配置信息会保存在 ~/.gitconfig 文件中,~ 表示当前用户目录。

1
2
git config --global user.name "username"  
git config --global user.email "email"

局部设置

只对当前仓库生效,配置信息会保存在当前仓库根目录的 /.git/config 文件中。

1
2
git config user.name "username"  
git config user.email "email"

② 通过正则表达式批量将 DO 类中字段的 JavaDoc 注释转为 Swagger @ApiModelProperty 注解

Regex:\/**\n * (.)(\n \\/)
target:@ApiModelProperty(value = “$1”, example = “”)