阿里巴巴Java开发规范手册(四)

时间:2022-02-28 13:09:39

二、异常日志

(一)异常处理

  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开发手册》 喜欢请支持正版