
Java 开发规范
本文内容摘选自Java开发手册黄山版
目录
- 一、编程规约
- 二、异常日志
- 三、单元测试
- 四、安全规约
- 五、MySQL 数据库
一、编程规约
(一) 命名风格
【强制】所有编程相关的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
【强制】类名使用 UpperCamelCase 风格,以下情形例外:DO / PO / DTO / BO / VO / UID 等。
【强制】方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格。
【强制】常量命名应该全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
【强制】抽象类命名使用 Abstract 或 Base 开头。
【强制】异常类命名使用 Exception 结尾。
【强制】测试类命名以它要测试的类的名称开始,以 Test 结尾。
【强制】类型与中括号紧挨相连来定义数组。
【强制】POJO 类中的任何布尔类型的变量,都不要加 is 前缀,否则部分框架解析会引起序列化错误。
说明:本文 MySQL 规约中的建表约定第 1 条,表达是与否的变量采用 is_xxx 的命名方式,所以需要在
设置从 is_xxx 到 xxx 的映射关系。 【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
【强制】避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可理解性降低。
【推荐】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用完整的单词组合来表达。
【推荐】在常量与变量命名时,表示类型的名词放在词尾,以提升辨识度。
【推荐】如果模块、接口、类、方法使用了设计模式,在命名时要体现出具体模式。
【推荐】接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义常量,如果一定要定义,最好确定该常量与接口的方法相关,并且是整个应用的基础常量。
接口和实现类的命名有两套规则:
- 【强制】对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别。
- 【推荐】如果是形容能力的接口名称,取对应的形容词为接口名(通常是 –able 结尾的形容词)。
【参考】枚举类名带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开。
【参考】各层命名规约:
A)Service / DAO 层方法命名规约:
- 获取单个对象的方法用 get 做前缀。
- 获取多个对象的方法用 list 做前缀,复数结尾,如:listObjects
- 获取统计值的方法用 count 做前缀。
- 插入的方法用 save / insert 做前缀。
- 删除的方法用 remove / delete 做前缀。
- 修改的方法用 update 做前缀。
B)领域模型命名规约:
- 数据对象:xxxDO,xxx 即为数据表名。
- 数据传输对象:xxxDTO,xxx 为业务领域相关的名称。
- 展示对象:xxxVO,xxx 一般为网页名称。
- POJO 是 DO / DTO / BO / VO 的统称,禁止命名成 xxxPOJO。
(二) 常量定义
- 【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
- 【强制】long 或 Long 赋值时,数值后使用大写 L;浮点数类型的数值后缀统一为大写的 D 或 F。
- 【推荐】不要使用一个常量类维护所有常量,要按常量功能进行归类,分开维护。
- 【推荐】如果变量值仅在一个固定范围内变化用 enum 类型来定义。
(三) 代码格式
【强制】如果大括号内为空,简洁地写成{}即可,大括号中间无需换行和空格;如果是非空代码块,则:
- 左大括号前不换行。
- 左大括号后换行。
- 右大括号前换行。
- 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行。
【强制】左小括号和右边相邻字符之间不需要空格;右小括号和左边相邻字符之间也不需要空格;而左大括号前需要加空格。
【强制】if / for / while / switch / do 等保留字与左右括号之间都必须加空格。
【强制】任何二目、三目运算符的左右两边都需要加一个空格。
【强制】采用 4 个空格缩进,禁止使用 Tab 字符。
【强制】注释的双斜线与注释内容之间有且仅有一个空格。
【强制】在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。
【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
- 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进。
- 运算符与下文一起换行。
- 方法调用的点符号与下文一起换行。
- 方法调用中的多个参数需要换行时,在逗号后进行。
- 在括号前不要换行,见反例。
【强制】方法参数在定义和传入时,多个参数逗号后面必须加空格。
【强制】IDE 的 text file encoding 设置为 UTF-8;IDE 中文件的换行符使用 Unix 格式,不要使用Windows 格式。
【推荐】单个方法的总行数不超过 80 行。(除注释之外的方法签名、左右大括号、方法内代码、空行、回车及任何不可见字符的总行数不超过 80 行。)
【推荐】不同逻辑、不同语义、不同业务的代码之间插入一个空行,分隔开来以提升可读性。
任何情形,没有必要插入多个空行进行隔开。
(四) OOP 规约
【强制】避免通过一个类的对象引用访问此类的静态变量或静态方法,无谓增加编译器解析成本,直接用类名来访问即可。
【强制】所有的覆写方法,必须加 @Override 注解。
【强制】相同参数类型,相同业务含义,才可以使用的可变参数,参数类型避免定义为 Object。
建议开发者尽量不用可变参数编程。
【强制】Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
【强制】浮点数之间的等值判断,基本数据类型不能使用 == 进行比较,包装数据类型不能使用 equals 进行判断。
【强制】BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法。
说明:equals() 方法会比较值和精度(1.0 与 1.00 返回结果为 false),而 compareTo() 则会忽略精度。
【强制】定义数据对象 DO 类时,属性类型要与数据库字段类型相匹配。
【强制】禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象。
说明:BigDecimal(double) 存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。
关于基本数据类型与包装数据类型的使用标准如下:
- 【强制】所有的 POJO 类属性必须使用包装数据类型。
- 【强制】RPC 方法的返回值和参数必须使用包装数据类型。
- 【推荐】所有的局部变量使用基本数据类型。
【强制】定义 DO / PO / DTO / VO 等 POJO 类时,不要设定任何属性默认值。
【强制】序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
说明:注意 serialVersionUID 不一致会抛出序列化运行时异常。
【强制】构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。
【强制】POJO 类必须写 toString 方法。如果继承了另一个 POJO 类,注意在前面加一下 super.toString()。
说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString() 方法打印其属性值,便于排查问题。
【强制】禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法。
【推荐】使用索引访问用 String 的 split 方法得到的数组时,需做最后一个分隔符后有无内容的检查,
【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读,
【推荐】类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 > getter / setter 方法。
【推荐】setter 方法中,参数名称与类成员变量名称一致,this.成员名=参数名。在 getter / setter 方法中,不要增加业务逻辑,增加排查问题的难度。
【推荐】循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
说明:反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过toString() 返回 String 对象,造成内存资源浪费。
【推荐】慎用 Object 的 clone 方法来拷贝对象。
说明:对象 clone 方法默认是浅拷贝,若想实现深拷贝需覆写 clone 方法实现域对象的深度遍历式拷贝。
【推荐】类成员与方法访问控制从严:
- 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 private。
- 工具类不允许有 public 或 default 构造方法。
- 类非 static 成员变量并且与子类共享,必须是 protected。
- 类非 static 成员变量并且仅在本类使用,必须是 private。
- 类 static 成员变量如果仅在本类使用,必须是 private。
- 若是 static 成员变量,考虑是否为 final。
- 类成员方法只供类内部调用,必须是 private。
- 类成员方法只对继承类公开,那么限制为 protected。
(五) 日期时间
【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。
说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后引入的概念),
【强制】在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。
- 表示月份是大写的 M
- 表示分钟则是小写的 m
- 24 小时制的是大写的 H
- 12 小时制的则是小写的 h
【强制】获取当前毫秒数:System.currentTimeMillis();而不是 new Date().getTime()。
【强制】不允许在程序任何地方中使用:
- 1)java.sql.Date
- 2)java.sql.Time
- 3)java.sql.Timestamp。
(六) 集合处理
【强制】关于 hashCode 和 equals 的处理,遵循如下规则:
- 只要覆写 equals,就必须覆写 hashCode。
- 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两种方法。
- 如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。
说明:String 因为覆写了 hashCode 和 equals 方法,所以可以愉快地将 String 对象作为 key 来使用。
【强制】判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式。
(七) 并发处理
(八) 控制语句
【强制】在一个 switch 块内,每个 case 要么通过 continue / break / return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null 判断。
【强制】在 if / else / for / while / do 语句中必须使用大括号。
【强制】三目运算符 condition ? 表达式 1:表达式 2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。
说明:以下两种场景会触发类型对齐的拆箱操作:
1)表达式 1 或 表达式 2 的值只要有一个是原始类型。
2)表达式 1 或 表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。【强制】在高并发场景中,避免使用“等于”判断作为中断或退出的条件。
说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。
【推荐】当方法的代码总行数超过 10 行时,return / throw 等中断逻辑的右大括号后需要加一个空行。
说明:这样做逻辑清晰,有利于代码阅读时重点关注。
【推荐】表达异常的分支时,少用 if-else 方式,这种方式可以改写成 if-if-if。
【推荐】除常用方法(如 getXxx / isXxx)等外不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。
【推荐】不要在其它表达式(尤其是条件表达式)中,插入赋值语句。
【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。
【推荐】避免采用取反逻辑运算符。
【推荐】公开接口需要进行入参保护,尤其是批量操作的接口。
【参考】下列情形,需要进行参数校验:
- 调用频次低的方法。
- 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。
- 需要极高稳定性和可用性的方法。
- 对外提供的开放接口,不管是 RPC / API / HTTP 接口。
- 敏感权限入口。
【参考】下列情形,不需要进行参数校验:
- 极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查。
- 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。
- 被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不会有问题,此时可以不校验参数。
(九) 注释规约
【强制】类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /** 内容 */ 格式,不得使用 // xxx方式。
【强制】所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数异常说明外,还必须指出该方法做什么事情,实现什么功能。
说明:对子类的实现要求,或者调用注意事项,请一并说明。
【强制】所有的类都必须添加创建者和创建日期。
【强制】方法内部单行注释,在被注释语句上方另起一行,使用 // 注释。方法内部多行注释使用 /* */注释,注意与代码对齐。
【强制】所有的枚举类型字段必须要有注释,说明每个数据项的用途。
【推荐】代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等。
【参考】对于注释的要求:
- 第一、能够准确反映设计思想和代码逻辑;
- 第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。
完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看的,使其能够快速接替自己的工作。
- 【参考】特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描,经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。
- 待办事宜(TODO):(标记人,标记时间,[预计处理时间])表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个Javadoc 标签)。
- 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。
(十) 前后端规约
【强制】前后端交互的 API,需要明确协议、域名、路径、请求方法、请求内容、状态码、响应体。说明:
协议:生产环境必须使用 HTTPS。
路径:每一个 API 需对应一个路径,表示 API 具体的请求地址:
- 代表一种资源,只能为名词,推荐使用复数,不能为动词,请求方法已经表达动作意义。
- URL 路径不能使用大写,单词如果需要分隔,统一使用下划线。
- 路径禁止携带表示请求内容类型的后缀,比如”.json”,”.xml”,通过 accept 头表达即可。
请求方法:对具体操作的定义,常见的请求方法如下:
- GET:从服务器取出资源。
- POST:在服务器新建一个资源。
- PUT:在服务器更新资源。
- DELETE:从服务器删除资源。
请求内容:URL 带的参数必须无敏感信息或符合安全要求;body 里带参数时必须设置 Content-Type。
响应体:响应体 body 可放置多种数据类型,由 Content-Type 头来确定。
【强制】前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}。
说明:此条约定有利于数据层面上的协作更加高效,减少前端很多琐碎的 null 判断。
【强制】服务端发生错误时,返回给前端的响应信息必须包含 HTTP 状态码,errorCode、errorMessage、用户提示信息四个部分。
说明:四个部分的涉众对象分别是浏览器、前端开发、错误排查人员、用户。其中输出给用户的提示信息要求:简短清晰、提示友好,引导用户进行下一步操作或解释错误原因,提示信息可以包括错误原因、上下文环境、推荐操作等。
【强制】在前后端交互的 JSON 格式数据中,所有的 key 必须为小写字母开始的 lowerCamelCase风格,符合英文表达习惯,且表意完整。
【强制】errorMessage 是前后端错误追踪机制的体现,可以在前端输出到 type=”hidden” 文字类控件中,或者用户端的日志中,帮助我们快速地定位出问题。
【强制】对于需要使用超大整数的场景,服务端一律使用 String 字符串类型返回,禁止使用 Long 类型。
【强制】HTTP 请求通过 URL 传递参数时,不能超过 2048 字节。
【强制】HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错。
说明:nginx 默认限制是 1MB,tomcat 默认限制为 2MB,当确实有业务需要传较大内容时,可以调大服务器端的限制。
【强制】在翻页场景中,用户输入参数的小于 1,则前端返回第一页参数给后端;后端发现用户输入的参数大于总页数,直接返回最后一页。
【强制】服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL 统一代理模块生成,否则会因线上采用 HTTPS 协议而导致浏览器提示“不安全”,并且还会带来 URL 维护不一致的问题。
【推荐】服务器返回信息必须被标记是否可以缓存,如果缓存,客户端可能会重用之前的请求结果。
说明:缓存有利于减少交互次数,减少交互的平均延迟。
【推荐】服务端返回的数据,使用 JSON 格式而非 XML。
说明:尽管 HTTP 支持使用不同的输出格式,例如纯文本,JSON,CSV,XML,RSS 甚至 HTML。如果我们使用的面向用户的服务,应该选择 JSON 作为通信中使用的标准数据交换格式,包括请求和响应。此外,application/JSON 是一种通用的 MIME 类型,具有实用、精简、易读的特点。【推荐】前后端的时间格式统一为”yyyy-MM-dd HH:mm:ss”,统一为 GMT。
【参考】在接口路径中不要加入版本号,版本控制在 HTTP 头信息中体现,有利于向前兼容。
说明:当用户在低版本与高版本之间反复切换工作时,会导致迁移复杂度升高,存在数据错乱风险。
(十一) 其他
【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。
说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);
【强制】注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0 ≤ x < 1(能够取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后取整,直接使用 Random 对象的 nextInt 或者 nextLong 方法。
【强制】枚举 enum(括号内)的属性字段必须是私有且不可变。
【推荐】不要在视图模板中加入任何复杂的逻辑运算。
说明:根据 MVC 理论,视图的职责是展示,不要抢模型和控制器的活。
【推荐】任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。
【推荐】及时清理不再使用的代码段或配置信息。
说明:对于垃圾代码或过时配置,坚决清理干净,避免程序过度臃肿,代码冗余。
对于暂时被注释掉,后续可能恢复使用的代码片断,在注释代码上方,统一规定使用三个斜杠(///)来说明注释掉代码的理由:
二、异常日志
(一) 错误码
【强制】错误码的制定原则:快速溯源、沟通标准化。
说明:错误码想得过于完美和复杂,就像康熙字典的生僻字一样,用词似乎精准,但是字典不容易随身携带且简单易懂。
- 错误码必须能够快速知晓错误来源,可快速判断是谁的问题。
- 错误码必须能够进行清晰地比对(代码中容易 equals)。
- 错误码有利于团队快速对错误原因达到一致认知。
【强制】错误码不体现版本号和错误等级信息。
说明:错误码以不断追加的方式进行兼容。错误等级由日志和错误码本身的释义来决定。
【强制】全部正常,但不得不填充错误码时返回五个零:00000。
【强制】错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号。
说明:错误产生来源分为 A/B/C。
A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题;
B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;
C 表示错误来源于第三方服务,比如 CDN服务出错,消息投递超时等问题;
四位数字编号从 0001 到 9999,大类之间的步长间距预留 100。【强制】编号不与公司业务架构,更不与组织架构挂钩,以先到先得的原则在统一平台上进行,审批生效,编号即被永久固定。
【强制】错误码使用者避免随意定义新的错误码。
说明:尽可能在原有错误码附表中找到语义相同或者相近的错误码在代码中使用即可。
【强制】错误码不能直接输出给用户作为提示信息使用。
(二) 异常处理
【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。
【强制】异常捕获后不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
【强制】catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
【强制】捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
【强制】事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。
【强制】finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
说明:如果 JDK7,可以使用 try-with-resources 方式。
【强制】不要在 finally 块中使用 return
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则会在此直接返回,无情丢弃掉 try 块中的返
回点。【强制】捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
【强制】在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常使用 Throwable 类进行拦截。
说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。
【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
说明:本规约明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败,运行时异常等场景返回 null 的情况。
【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
- 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE
- 数据库的查询结果可能为 null。
- 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
- 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
- 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
- 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
【推荐】定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException 等。
【参考】对于公司外的 http / api 开放接口必须使用错误码,而应用内部推荐异常抛出;跨应用间RPC 调用优先考虑使用 Result 方式,封装 isSuccess() 方法、错误码、错误简短信息;应用内部推荐异常抛出。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
- 使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
- 如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,对于调用端解决问题的帮助不会太多。
如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。
(三) 日志规约
【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架(SLF4J、JCL—Jakarta Commons Logging)中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
说明:日志框架(SLF4J、JCL–Jakarta Commons Logging)的使用方式(推荐使用 SLF4J)
【强制】日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以“应用名.log”来保存,保存在/{统一目录}/{应用名}/logs/目录下,过往日志格式为:{logname}.log.{保存日期},日期格式:yyyy-MM-dd
【强制】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不
少于六个月,并且进行网络多机备份。【强制】应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log。logType:日志类型,如 stats / monitor / access 等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。说明:推荐对日志进行分类,将错误日志和业务日志分开放,便于开发人员查看,也便于通过日志对系统进行及时监控。
三、单元测试
四、安全规约
五、MySQL 数据库
(一) 建表规约
【强制】表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)。
注意:POJO 类中的任何布尔类型的变量,都不要加 is 前缀,所以,需要在设置从 is_xxx 到 Xxx 的映射关系。数据库表示是与否的值,使用 tinyint 类型,坚持 is_xxx 的命名方式是为了明确其取值含义与取值范围。 说明:任何字段如果为非负数,必须是 unsigned。
【强制】表名、字段名必须使用小写字母或数字,禁止出现数字开头禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。
【强制】表名不使用复数名词。
说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。
【强制】禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。
【强制】主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。
【强制】小数类型为 decimal,禁止使用 float 和 double。
说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。
【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
【强制】varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引率。
【强制】表必备三字段:id,create_time,update_time。
说明:其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。create_time,update_time 的类型均为datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。
【强制】在数据库中不能使用物理删除操作,要使用逻辑删除。
说明:逻辑删除在数据删除后可以追溯到行为操作。不过会使得一些情况下的唯一主键变得不唯一,需要根据情况来酌情解决。
【推荐】表的命名最好是遵循“业务名称_表的作用”。
【推荐】库名与应用名称尽量一致。
【推荐】如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。
【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:
- 不是频繁修改的字段。
- 不是唯一索引的字段。
- 不是 varchar 超长字段,更不能是 text 字段。
【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。
(二) 索引规约
【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
【强制】超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。
说明:即使双表 join 也要注意表索引、SQL 性能。
【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名,索引长度)) / count(*) 的区分度来确定。
【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
【推荐】如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 filesort 的情况,影响查询性能。
【推荐】利用覆盖索引来进行查询操作,避免回表。
说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。
【推荐】利用延迟关联或者子查询优化超多分页场景。
说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
【推荐】SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 const 最好。
- consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
- ref 指的是使用普通的索引(normal index)。
- range 对索引进行范围检索。
【推荐】建组合索引的时候,区分度最高的在最左边。
【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。
【参考】创建索引时避免有如下极端误解:
- 索引宁滥勿缺。认为一个查询就需要建一个索引。
- 吝啬索引的创建。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度。
- 抵制唯一索引。认为唯一索引一律需要在应用层通过“先查后插”方式解决。
(三) SQL 语句
1.【强制】不要使用 count(列名) 或 count(常量) 来替代 count(),count() 是 SQL92 定义的标准统计行
数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
说明:count(*) 会统计值为 NULL 的行,而 count(列名) 不会统计此列为 NULL 值的行。
2.【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1 , col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
3.【强制】当某一列的值全是 NULL 时,count(col) 的返回结果为 0;但 sum(col) 的返回结果为 NULL,因此使用 sum() 时需注意 NPE 问题。
5.【强制】代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。
6.【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
7.【强制】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
8.【强制】数据订正(特别是删除或修改记录操作)时,要先 select,避免出现误删除的情况,确认无误才能执行更新语句。
9.【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定。
11.【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在1000 个之内。
12.【参考】因国际化需要,所有的字符存储与表示,均采用 utf8 字符集,那么字符计数方法需要注意。
如果需要存储表情,那么选择 utf8mb4 来进行存储,注意它与 utf8 编码的区别。
13.【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。
(四) ORM 映射
【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
- 增加查询分析器解析成本。
- 增减字段容易与 resultMap 配置不一致。
- 无用字段增加网络消耗,尤其是 text 类型的字段。
【强制】POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行字段与属性之间的映射。
【强制】不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义
;反过来,每一个表也必然有一个 与之对应。 说明:配置映射关系,使字段与 DO 类解耦,方便维护。
【强制】sql.xml 配置参数使用:#{},#param# 不要使用 ${} 此种方式容易出现 SQL 注入。
【强制】不允许直接拿HashMap与Hashtable 作为查询结果集的输出。
【参考】@Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。