二、异常日志
(一)异常处理
1、Java 类库中定义的一类 RuntimeException 可以通过预先检查进行规避,而不应该
通过 catch 来处理,比如:IndexOutOfBoundsException,NullPointerException 等等
2、异常不要用来做流程控制,条件控制,因为异常的处理效率比条件分支低。
3、对大段代码进行 try-catch,这是不负责任的表现。catch 时请分清稳定代码和非稳定
代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行
区分异常类型,再做对应的异常处理
4、捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,
请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,
将其转化为用户可以理解的内容
5、有 try 块放到了事务代码中,catch 异常后,如果需要回滚事务,一定要注意手动回滚事务
6、finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch
7、不能在 finally 块中使用 return,finally 块中的 return 返回后方法结束执行,
不会再执行 try 块中的 return 语句
8、捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类
9、方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分
说明什么情况下会返回 null 值。调用方需要进行 null 判断防止 NPE 问题
10、防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
(1)返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE
(2)数据库的查询结果可能为 null。
(3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
(4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
(5) 对于 Session 中获取的数据,建议 NPE 检查,避免空指针。
(6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题
11、定义时区分 unchecked / checked 异常,避免直接抛出 new RuntimeException(),
更不允许抛出 Exception 或者 Throwable,应使用有业务含义的自定义异常。推荐业界已
定义过的自定义异常,如:DAOException / ServiceException 等
12、在代码中使用“抛异常”还是“返回错误码”,对于公司外的 http/api 开放接口
必须使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用
Result 方式,封装 isSuccess()方法、“错误码”、“错误简短信息”。
说明:关于 RPC 方法返回方式使用 Result 方式的理由:
(1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。
(2)如果不加栈信息,只是 new 自定义异常,加入自己的理解的 error message,
对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况
下,数据序列化和传输的性能损耗也是问题
13、避免出现重复的代码(Don’t Repeat Yourself),即 DRY 原则。
说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,
需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,
甚至是组件化。
正例:一个类中有多个 public 方法,都需要进行数行相同的参数校验操作,
这个时候请抽取:
private boolean checkParam(DTO dto) {...}
(二)日志规约
1、应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架
SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Abc.class);
2、日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点
3、应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log。logType:日志类型,推荐分类有
stats/desc/monitor/visit 等;logName:日志描述。
这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,
也有利于归类查找
4、对 trace/debug/info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式
5、避免重复打印日志,浪费磁盘空间,务必在 log4j.xml 中设置 additivity=false
6、异常信息应该包括两类信息:案发现场信息和异常堆栈信息。
如果不处理,那么通过关键字 throws 往上抛出
7、谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;
如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,
避免把服务器磁盘撑爆,并记得及时删除这些观察日志
8、可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。
注意日志输出的级别,error 级别只记录系统逻辑出错、异常等重要的错误信息。
如非必要,请不要在此场景打出 error 级别
三、单元测试
1、好的单元测试必须遵守 AIR 原则。
说明:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,
但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,
具有自动化、独立性、可重复执行的特点。
A:Automatic(自动化)
I:Independent(独立性)
R:Repeatable(可重复)
2、单元测试应该是全自动执行的,并且非交互式的。测试框架通常是定期执行的,
执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的
单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证
3、保持单元测试的独立性。为了保证单元测试稳定可靠且便于维护,单元测试用例之间
决不能互相调用,也不能依赖执行的先后次序
4、单元测试是可以重复执行的,不能受到外界环境的影响
5、对于单元测试,要保证测试粒度足够小,有助于精确定位问题。
单测粒度至多是类级别,一般是方法级别
6、核心业务、核心应用、核心模块的增量代码确保单元测试通过
7、单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下
8、单元测试的基本目标:语句覆盖率达到 70%;核心模块的语句覆盖率和分支覆盖率
都要达到 100%
9、编写单元测试代码遵守 BCDE 原则,以保证被测试模块的交付质量。
B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。
C:Correct,正确的输入,并得到预期的结果。
D:Design,与设计文档相结合,来编写单元测试。
E:Error,强制错误信息输入(如:非法数据、异常流程、非业务允许输入等),
并得到预期的结果
10、对于数据库相关的查询,更新,删除等操作,不能假设数据库里的数据是存在的,
或者直接操作数据库把数据插入进去,请使用程序插入或者导入数据的方式来准备数据
11、和数据库相关的单元测试,可以设定自动回滚机制,不给数据库造成脏数据。
或者对单元测试产生的数据有明确的前后缀标识
12、对于不可测的代码建议做必要的重构,使代码变得可测,避免为了达到测试要求而
书写不规范测试代码
13、在设计评审阶段,开发人员需要和测试人员一起确定单元测试范围,
单元测试最好覆盖所有测试用例(UC)
14、单元测试作为一种质量保障手段,不建议项目发布后补充单元测试用例,
建议在项目提测前完成单元测试
15、为了更方便地进行单元测试,业务代码应避免以下情况:
构造方法中做的事情过多。
存在过多的全局变量和静态方法。
存在过多的外部依赖。
存在过多的条件语句
16、不要对单元测试存在如下误解:
那是测试同学干的事情。本文是开发手册,凡是本文内容都是与开发同学强相关的。
单元测试代码是多余的。汽车的整体功能与各单元部件的测试正常与否是强相关的。
单元测试代码不需要维护。一年半载后,那么单元测试几乎处于废弃状态。
单元测试与线上故障没有辩证关系。好的单元测试能够最大限度地规避线上故障
四、安全规约
1、隶属于用户个人的页面或者功能必须进行权限控制校验
2、用户敏感数据禁止直接展示,必须对展示数据进行脱敏
3、用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,
防止 SQL 注入,禁止字符串拼接 SQL 访问数据库
4、用户请求传入的任何参数必须做有效性验证
5、禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据
6、表单、AJAX 提交必须执行 CSRF 安全过滤
7、在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制,
如数量限制、疲劳度控制、验证码校验,避免被滥刷、资损
8、发贴、评论、发送即时消息等用户生成内容的场景必须实现防刷、
文本内容违禁词过滤等风控策略
(五)、MySQL数据库
(一)建表规约
1、表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint
( 1 表示是,0 表示否)
2、表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间
只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,
所以字段名称需要慎重考虑
3、表名不使用复数名词
4、禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字
5、键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名
6、小数类型为 decimal,禁止使用 float 和 double
7、如果存储的字符串长度几乎相等,使用 char 定长字符串类型
8、varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,
如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,
避免影响其它字段索引效率
9、表必备三字段:id, gmt_create, gmt_modified
10、表的命名最好是加上“业务名称_表的作用”。
11、库名与应用名称尽量一致
12、如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释
13、字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:
(1)不是频繁修改的字段。
(2)不是 varchar 超长字段,更不能是 text 字段
14、单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表
15、合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度
(二)索引规约
1、业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引
2、超过三个表禁止 join。需要 join 的字段,数据类型必须绝对一致;多表关联查询时,
保证被关联的字段需要有索引
3、在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,
根据实际文本区分度决定索引长度即可
4、页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决
5、如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的
一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能
6、利用覆盖索引来进行查询操作,避免回表
7、利用延迟关联或者子查询优化超多分页场景
8、SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,
如果可以是 consts最好
9、建组合索引的时候,区分度最高的在最左边
10、防止因字段类型不同造成的隐式转换,导致索引失效
11、创建索引时避免有如下极端误解:
(1)宁滥勿缺。认为一个查询就需要建一个索引。
(2)宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。
(3)抵制惟一索引。认为业务的惟一性一律需要在应用层通过“先查后插”方式解决
(三)SQL语句
1、不要使用 count(列名)或 count(常量)来替代 count(*),count(*)是 SQL92 定义的标准统计
行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关
2、count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2)
如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0
3、当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为NULL
因此使用 sum()时需注意 NPE 问题
4、使用 ISNULL()来判断是否为 NULL 值。
说明:NULL 与任何值的直接比较都为 NULL。
(1) NULL<>NULL 的返回结果是 NULL,而不是 false。
(2) NULL=NULL 的返回结果是 NULL,而不是 true。
(3) NULL<>1 的返回结果是 NULL,而不是 true
5、在代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句
6、不得使用外键与级联,一切外键概念必须在应用层解决
7、禁止使用存储过程,存储过程难以调试和扩展,更没有移植性
8、数据订正时,删除和修改记录时,要先 select,避免出现误删除,确认无误才能
执行更新语句
9、in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,
控制在 1000 个之内
10、如果有全球化需要,所有的字符存储与表示,均以 utf-8 编码,
注意字符统计函数的区别
11、TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,
但 TRUNCATE无事务且不触发 trigger,有可能造成事故,
故不建议在开发代码中使用此语句
(四)ORM映射
1、在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明
2、POJO 类的布尔属性不能加 is,而数据库字段必须加 is_,要求在 resultMap 中进行
字段与属性之间的映射
3、不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,
也需要定义;反过来,每一个表也必然有一个与之对应
4、sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入
5、iBATIS 自带的 queryForList(String statementName,int start,int size)不推荐使用
6、不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出
7、更新数据表记录时,必须同时更新记录对应的 gmt_modified 字段值为当前时间
8、不要写一个大而全的数据更新接口。传入为 POJO 类,不管是不是自己的目标更新字
段,都进行 update table set c1=value1,c2=value2,c3=value3; 这是不对的。
执行 SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是增加 binlog 存储
9、@Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要
考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等
10、<isEqual>中的 compareValue 是与属性值对比的常量,一般是数字,表示相等时
带上此条件;<isNotEmpty>表示不为空且不为 null 时执行;
<isNotNull>表示不为 null 值时执行
六、工程结构
(一)应用分层
1、开放接口层:可直接封装 Service 方法暴露成 RPC 接口;通过 Web 封装成 http 接口;
进行网关安全控制、流量控制等
2、终端显示层:各个端的模板渲染并执行显示的层。当前主要是 velocity 渲染,JS 渲染,
JSP 渲染,移动端展示等
3、Web 层:主要是对访问控制进行转发,各类基本参数校验,或者不复用的业务简单处理等
4、Service 层:相对具体的业务逻辑服务层
5、Manager 层:通用业务处理层,它有如下特征:
(1)对第三方平台封装的层,预处理返回结果及转化异常信息;
(2)对 Service 层通用能力的下沉,如缓存方案、中间件通用处理;
(3)与 DAO 层交互,对多个 DAO 的组合复用
6、DAO 层:数据访问层,与底层 MySQL、Oracle、Hbase 等进行数据交互
7、外部接口或第三方平台:包括其它部门 RPC 开放接口,基础平台,
其它公司的 HTTP 接口
8、(分层异常处理规约)在 DAO 层,产生的异常类型有很多,无法用细粒度的异常进行
catch,使用 catch(Exception e)方式,并 throw new DAOException(e),不需要打印
日志,因为日志在 Manager/Service 层一定需要捕获并打到日志文件中去,如果同台
服务器再打日志,浪费性能和存储。在 Service 层出现异常时,必须记录出错日志到
磁盘,尽可能带上参数信息,相当于保护案发现场。如果 Manager 层与 Service 同机
部署,日志方式与 DAO层处理一致,如果是单独部署,则采用与 Service 一致的处理
方式。Web 层绝不应该继续往上抛异常,因为已经处于顶层,如果意识到这个异常
将导致页面无法正常渲染,那么就应该直接跳转到友好错误页面,加上用户容易理解的
错误提示信息。开放接口层要将异常处理成错误码和错误信息方式返回
9、分层领域模型规约:
DO(Data Object):与数据库表结构一一对应,通过 DAO 层向上传输数据源对象
DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象
BO(Business Object):业务对象。由 Service 层输出的封装业务逻辑的对象
AO(Application Object):应用对象。在 Web 层与 Service 层之间抽象的复用对象
模型,极为贴近展示层,复用度不高
VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象
Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,
禁止使用 Map 类来传输
(二)二方库依赖
1、定义 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:详细规定参考下方
2、二方库版本号命名方式:主版本号.次版本号.修订号
(1)主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级。
(2)次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改。
(3)修订号:保持完全兼容性,修复 BUG、新增次要功能特性等
3、线上应用不要依赖 SNAPSHOT 版本(安全包除外)
4、二方库的新增或升级,保持除功能点之外的其它 jar 包仲裁结果不变。如果有改变,
必须明确评估和验证,建议进行 dependency:resolve 前后信息比对,如果仲裁结果完全
不一致,那么通过 dependency:tree 命令,找出差异点,进行<excludes>排除 jar 包
5、二方库里可以定义枚举类型,参数可以使用枚举类型,但是接口返回值不允许使用
枚举类型或者包含枚举类型的 POJO 对象
6、依赖于一个二方库群时,必须定义一个统一的版本变量,避免版本号不一致
7、禁止在子项目的 pom 依赖中出现相同的 GroupId,相同的 ArtifactId,但是不同的Version
8、所有 pom 文件中的依赖声明放在<dependencies>语句块中,所有版本仲裁放在
<dependencyManagement>语句块中
9、二方库不要有配置项,最低限度不要再增加配置项
10、为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则:
(1)精简可控原则。移除一切不必要的 API 和依赖,只包含 Service API、必要的领域
模型对象、Utils 类、常量、枚举等。如果依赖其它二方库,尽量是 provided 引入,
让二方库使用者去依赖具体版本号;无 log 具体实现,只依赖日志框架。
(2)稳定可追溯原则。每个版本的变化应该被记录,二方库由谁维护,源码在哪里,
都需要能方便查到。除非用户主动升级版本,否则公共二方库的行为不应该发生变化
(三)服务器
1、高并发服务器建议调小 TCP 协议的 time_wait 超时时间。
2、调大服务器所支持的最大文件句柄数(File Descriptor,简写为 fd)
3、给 JVM 设置-XX:+HeapDumpOnOutOfMemoryError 参数,让 JVM 碰到 OOM 场景时
输出dump 信息
4、在线上生产环境,JVM 的 Xms 和 Xmx 设置一样大小的内存容量,避免在 GC 后
调整堆大小带来的压力
5、服务器内部重定向使用 forward;外部重定向地址使用 URL 拼装工具类来生成,
否则会带来 URL 维护不一致的问题和潜在的安全风险。
附 2:本手册专有名词
1. POJO(Plain Ordinary Java Object): 在本手册中,POJO 专指只有 setter / getter/ toString 的
简单类,包括 DO/DTO/BO/VO 等。
2. GAV(GroupId、ArtifactctId、Version): Maven 坐标,是用来唯一标识 jar 包。
3. OOP(Object Oriented Programming): 本手册泛指类、对象的编程处理方式。
4. ORM(Object Relation Mapping): 对象关系映射,对象领域模型与底层数据之间的转换,
本文泛指 iBATIS, mybatis 等框架。
5. NPE(java.lang.NullPointerException): 空指针异常。
6. SOA(Service-Oriented Architecture): 面向服务架构,它可以根据需求通过网络对松散耦合的
粗粒度应用组件进行分布式部署、组合和使用,有利于提升组件可重用性,可维护性。
7. 一方库: 本工程内部子项目模块依赖的库(jar 包)。
8. 二方库: 公司内部发布到*仓库,可供公司内部其它应用依赖的库(jar 包)。
9. 三方库: 公司之外的开源库(jar 包)。
10. IDE(Integrated Development Environment): 用于提供程序开发环境的应用程序,一般包括
代码编辑器、编译器、调试器和图形用户界面等工具,本《手册》泛指 IntelliJ IDEA和 eclipse
出自《阿里巴巴Java开发手册》 喜欢请支持正版