Java类库中定义的可以通过预检查方式规避的RuntimeException
异常不应该通过catch
的方式来处理,比如:NullPointerException
,IndexOutOfBoundsException
等等【说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过catch NumberFormatException
来实现】
正例:
if (obj != null) {...}
反例:try { obj.method(); } catch (NullPointerException e) {…}
catch
时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch
尽可能进行区分异常类型,再做对应的异常处理【说明:对大段代码进行try-catch
,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。】try
块放到了事务代码中,catch
异常后,如果需要回滚事务,一定要注意手动回滚事务finally
块必须对资源对象、流对象进行关闭,有异常也要做try-catch
。【如果JDK7及以上,可以使用try-with-resources
方式】finally
块中使用return
【说明:try
块中的return
语句执行成功后,并不马上返回,而是继续执行finally
块中的语句,如果此处存在return
语句,则在此直接返回,无情丢弃掉try
块中的返回点。】// 反例
private int x = 0;
public int checkReturn() {
try {
// x 等于 1,此处不返回
return ++x;
} finally {
// 返回的结果是 2
return ++x;
}
}
RPC、二方包、或动态生成类
的相关方法时,捕捉异常必须使用Throwable
类来进行拦截。【说明:通过反射机制来调用方法,如果找不到方法,抛出NoSuchMethodException
。什么情况会抛出NoSuchMethodError
呢?二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出NoSuchMethodError
。】null
,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null
值【说明:本手册明确防止NPE
是调用者的责任
。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null
的情况。】防止NPE
,是程序员的基本修养,注意NPE
产生的场景
1)返回类型为基本数据类型,
return
包装数据类型的对象时,自动拆箱有可能产生 NPE。【反例:public int f() { return Integer 对象}
,如果为null
,自动解箱抛NPE
】
2)数据库的查询结果可能为null
3)集合里的元素即使isNotEmpty
,取出的数据元素也可能为null
。
4)远程调用返回对象时,一律要求进行空指针判断,防止NPE
5)对于Session
中获取的数据,建议进行NPE
检查,避免空指针。
6)级联调用obj.getA().getB().getC();
一连串调用,易产生NPE
。正例:使用JDK8的Optional
类来防止NPE问题。
unchecked / checked
异常,避免直接抛出new RuntimeException()
,更不允许抛出Exception
或者Throwable
,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如:DAOException / ServiceException
等。http/api
开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC
调用优先考虑使用Result
方式,封装isSuccess()
方法、“错误码”、“错误简短信息”【说明:关于RPC
方法返回方式使用Result
方式的理由:1)使用抛异常返回方式,调用方如果没有捕获到就会产生运行时错误。2)如果不加栈信息,只是new自定义异常,加入自己的理解的error message
,对于调用端解决问题的帮助不会太多。如果加了栈信息,在频繁调用出错的情况下,数据序列化和传输的性能损耗也是问题。】避免出现重复的代码(Don‘t Repeat Yourself
),即DRY原则【说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化】
Log4j、Logback
)中的API,而应依赖使用日志框架SLF4J
中的API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private static final Logger logger = LoggerFactory.getLogger(Test.class);
appName_logType_logName.log
。logType:日志类型,如stats/monitor/access
等;logName:日志描述。这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找【说明:推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。正例:force-web
应用中单独监控时区转换异常,如:force_web_timeZoneConvert.log
】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()
方法调用,无谓浪费方法调用的开销。】// 正例
// 如果判断为真,那么可以输出trace和debug级别的日志
if (logger.isDebugEnabled()) {
logger.debug("Current ID is: {} and name is: {}", id, getName());
}
log4j.xml
中设置additivity=false
【正例:<logger name="com.taobao.dubbo.config" additivity="false">
】案发现场信息
和异常堆栈信息
。如果不处理,那么通过关键字throws
往上抛出【正例:logger.error(各类参数或者对象 toString() + "_" + e.getMessage(), e);
】debug
日志;有选择地输出info
日志;如果使用warn
来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志【说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
】warn
日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出error
级别,避免频繁报警【说明:注意日志输出的级别,error
级别只记录系统逻辑出错、异常或者重要的错误
信息】AIR
原则【说明:单元测试在线上运行时,感觉像空气(AIR)一样并不存在,但在测试质量的保障上,却是非常关键的。好的单元测试宏观上来说,具有自动化、独立性、可重复执行的特点。A:Automatic
(自动化)I:Independent
(独立性)R:Repeatable
(可重复)】System.out
来进行人肉验证,必须使用assert
来验证method2
需要依赖method1
的执行,将执行结果作为method2
的输入】check in
时单元测试都会被执行。如果单测对外部环境(网络、服务、中间件等)有依赖,容易导致持续集成机制的不可用。正例:为了不受外界环境影响,要求设计代码时就把SUT的依赖改成注入,在测试时用spring
这样的DI
框架注入一个本地(内存)实现或者Mock
实现】src/test/java
,不允许写在业务代码目录下【说明:源码编译时会跳过此目录,而单元测试框架默认是扫描此目录】DAO
层,Manager
层,可重用度高的Service
,都应该进行单元测试。】BCDE
原则,以保证被测试模块的交付质量【B:Border
,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等; C:Correct
,正确的输入,并得到预期的结果; D:Design
,与设计文档相结合,来编写单元测试; E:Error
,强制错误信息输入(如:非法数据、异常流程、业务允许外等),并得到预期的结果】权限控制校验
【说明:防止没有做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容、修改他人的订单】脱敏
【说明:中国大陆个人手机号码显示为:137****0969,隐藏中间4位,防止隐私泄露】SQL 注
入,禁止字符串拼接SQL访问数据库page size
过大导致内存溢出;2.恶意order by
导致数据库慢查询;3.任意重定向;4.SQL注入;5.反序列化注入;6.正则输入源串拒绝服务ReDoS;说明:Java代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的结果。】CSRF(Cross-site request forgery)
跨站请求伪造是一类常见编程漏洞。对于存在CSRF漏洞的应用/网站,攻击者可以事先构造好URL,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。】表达是与否概念的字段,必须使用is_xxx
的方式命名,数据类型是unsigned tinyint
(1表示是,0表示否)【说明:任何字段如果为非负数,必须是unsigned。注意:POJO类中的任何布尔类型的变量,都不要加is
前缀,所以,需要在<resultMap>
设置从is_xxx
到Xxx
的映射关系。数据库表示是与否的值,使用tinyint
类型,坚持is_xxx
的命名方式是为了明确其取值含义与取值范围】
正例:表达
逻辑删除
的字段名is_deleted
,1表示删除,0表示未删除
表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑【说明:MySQL在 Windows下不区分大小写,但在Linux下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝】
正例:
aliyun_admin
,rdc_config
,level3_name
反例:AliyunAdmin,rdcConfig,level_3_name
DO
类名也是单数形式,符合表达习惯】desc、range、match、delayed
等,请参考MySQL
官方保留字pk_字段名
;唯一索引名为uk_字段名
;普通索引名则为idx_字段名
【说明:pk_
即primary key
;uk_
即unique key
;idx_
即index
的简称】decimal
,禁止使用float
和double
【说明:在存储的时候,float
和double
都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过decimal
的范围,建议将数据拆成整数和小数并分开存储】char
定长字符串类型varchar
是可变长字符串,不预先分配存储空间,长度不要超过5000
,如果存储长度大于此值,定义字段类型为text
,独立出来一张表,用主键来对应,避免影响其它字段索引效率id, create_time, update_time
【说明:其中id
必为主键,类型为bigint unsigned
、单表时自增、步长为1。create_time, update_time
的类型均为datetime
类型。】alipay_task / force_project / trade_config
】varchar
超长字段,更不能是text
字段。3)不是唯一索引的字段】(正例
:商品类目名称使用频率高,字段长度短,名称基本一不变,可在相关联的表中冗余存储类目名称,避免关联查询)500万
行或者单表容量超过2GB
,才推荐进行分库分表
【说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。如下表,其中无符号值可以避免误存负数,且扩大了表示范围。
对象 | 年龄区间 | 类型 | 字节 | 表示范围 |
---|---|---|---|---|
人 | 150岁之内 | tinyint unsigned | 1 | 无符号值:0到255 |
龟 | 数百岁 | smallint unsigned | 2 | 无符号值:0到65535 |
恐龙化石 | 数千万年 | int unsigned | 4 | 无符号值:0到约42.9亿 |
太阳 | 约50亿年 | bigint unsigned | 8 | 无符号值:0到约10的19次方 |
唯一特性
的字段,即使是多个字段的组合,也必须建成唯一索引【说明:不要以为唯一索引影响了insert
速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生】join
。需要join
的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引
【说明:即使双表join也要注意表索引、SQL性能】varchar
字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可【说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上,可以使用count(distinct left(列名, 索引长度))/count(*)
的区分度来确定】B-Tree
的最左前缀匹配
特性,如果左边的值未确定,那么无法使用此索引】如果有order by
的场景,请注意利用索引的有序性。order by
最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现file_sort
的情况,影响查询性能
正例:where a=? and b=? order by c; 索引:a_b_c
反例:索引如果存在范围查询,那么索引有序性无法利用,如:WHERE a>10 ORDER BY b; 索引 a_b 无法排序。
利用覆盖索引来进行查询操作,避免回表【说明:如果一本书需要知道第11章是什么标题,会翻开第11章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。】
正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效果,用
explain
的结果,extra
列会出现:using index
利用延迟关联或者子查询优化超多分页场景【说明:MySQL并不是跳过offset
行,而是取offset+N
行,然后返回放弃前offset
行,返回N
行,那当offset
特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行SQL改写】
正例:先快速定位需要获取的id段,然后再关联:
SELECT a.* FROM 表 1 a, (select id from 表 1 where 条件 LIMIT 100000,20 ) b where a.id=b.id
SQL性能优化的目标:至少要达到range
级别,要求是ref
级别,如果可以是consts
最好【说明:1)consts
单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。2)ref
指的是使用普通的索引(normal index)。3)range
对索引进行范围检索。】
反例:
explain
表的结果,type=index,索引物理文件全扫描,速度非常慢,这个index
级别比较range
还低,与全表扫描是小巫见大巫。
建组合索引的时候,区分度最高的在最左边【说明:存在非等号和等号混合时,在建索引时,请把等号条件的列前置。如:where c>? and d=?
那么即使c的区分度更高,也必须把d放在索引的最前列,即索引idx_d_c
】
正例:如果
where a=? and b=?
,如果a列的几乎接近于唯一值,那么只需要单建idx_a
索引即可
创建索引时避免有如下极端误解
1)宁滥勿缺。认为一个查询就需要建一个索引
2)宁缺勿滥。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度
3)抵制惟一索引。认为业务的惟一性一律需要在应用层通过“先查后插”方式解决
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应直接返回,避免执行后面的分页语句student_id
是主键,那么成绩表中的student_id
则为外键。如果更新学生表中的student_id
,同时触发成绩表中的student_id
更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度】in
操作能避免则避免,若实在避免不了,需要仔细评估in后边的集合元素数量,控制在1000
个之内utf-8
编码,注意字符统计函数的区别。【说明:SELECT LENGTH("轻松工作");
返回为12;SELECT CHARACTER_LENGTH("轻松工作");
返回为4;如果需要存储表情,那么选择utf8mb4
来进行存储,注意它与utf-8
编码的区别。】TRUNCATE TABLE
比DELETE
速度快,且使用的系统和事务日志资源少,但TRUNCATE
无事务且不触发trigger
,有可能造成事故,故不建议在开发代码中使用此语句【说明:TRUNCATE TABLE
在功能上与不带WHERE
子句的DELETE
语句相同】
*
作为查询的字段列表,需要哪些字段必须明确写明【说明:1)增加查询分析器解析成本。2)增减字段容易与resultMap
配置不一致。3)无用字段增加网络消耗,尤其是text
类型的字段】is
,而数据库字段必须加is_
,要求在resultMap
中进行字段与属性之间的映射。【说明:参见定义POJO类以及数据库字段定义规定,在<resultMap>
中增加映射,是必须的。在MyBatis Generator
生成的代码中,需要进行对应的修改】resultClass
当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义;反过来,每一个表也必然有一个POJO
类与之对应【说明:配置映射关系,使字段与DO
类解耦,方便维护。】sql.xml
配置参数使用:#{},#param#
不要使用${}
此种方式容易出现SQL 注入HashMap
与Hashtable
作为查询结果集的输出【说明:resultClass=”Hashtable”
,会置入字段名和属性值,但是值的类型不可控】gmt_modified
字段值为当前时间update table set c1=value1,c2=value2,c3=value3;
这是不对的。执行SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是增加binlog
存储@Transactional
事务不要滥用。事务会影响数据库的QPS
,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等<isEqual>
中的compareValue是与属性值对比的常量,一般是数字,表示相等时带上此条件;<isNotEmpty>
表示不为空且不为null时执行;<isNotNull>
表示不为null值时执行。分层异常处理规约
在
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层输出的封装业务逻辑的对象
AO
(Application Object):应用对象,在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高
VO
(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象
Query
:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用Map类来传输
二方库依赖
1)
GroupID
格式:com.{公司/BU }.业务线 [.子业务线]
,最多4级。正例:com.taobao.jstorm
或com.alibaba.dubbo.register
2)ArtifactID
格式:产品线名-模块名
。语义不重复不遗漏,先到中央仓库去查证一下。正例:dubbo-client / fastjson-api / jstorm-tool
3)Version
:主版本号.次版本号.修订号。【1.主版本号:产品方向改变,或者大规模 API 不兼容,或者架构不兼容升级;2.次版本号:保持相对兼容性,增加主要功能特性,影响范围极小的 API 不兼容修改;3.修订号:保持完全兼容性,修复 BUG、新增次要功能特性等】
说明:注意起始版本号必须为:1.0.0,而不是0.0.1,正式发布的类库必须先去中央仓库进行查证,使版本号有延续性,正式版本号不允许覆盖升级。如当前版本:1.3.3,那么下一个合理的版本号:1.3.4或1.4.0或2.0.0
<dependencies>
语句块中,所有版本仲裁放在<dependencyManagement>
语句块中【说明:<dependencyManagement>
里只是声明版本,并不实现引入,因此子项目需要显式的声明依赖,version
和scope
都读取自父pom。而<dependencies>
所有声明在主pom的<dependencies>
里的依赖都会自动引入,并默认被所有的子项目继承】为避免应用二方库的依赖冲突问题,二方库发布者应当遵循以下原则
1)精简可控原则。移除一切不必要的API和依赖,只包含Service API、必要的领域模型对象、Utils类、常量、枚举等
2)稳定可追溯原则。每个版本的变化应该被记录,二方库由谁维护,源码在哪里,都需要能方便查到
高并发服务器建议调小TCP
协议的time_wait
超时时间【操作系统默认240
秒后,才会关闭处于time_wait
状态的连接,在高并发访问下,服务器端会因为处于time_wait
的连接数太多,可能无法建立新的连接,所以需要在服务器上调小此等待值】
正例:在linux服务器上请通过变更
/etc/sysctl.conf
文件去修改该缺省值(秒):net.ipv4.tcp_fin_timeout = 30
File Descriptor
,简写为 fd)【说明:主流操作系统的设计是将TCP/UDP
连接采用与文件一样的方式去管理,即一个连接对应于一个fd。主流的linux服务器默认所支持最大fd数量为1024
,当并发连接数很大时很容易因为fd不足而出现“open too many files”
错误,导致新的连接无法建立。建议将linux服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关)】-XX:+HeapDumpOnOutOfMemoryError
参数,让JVM
碰到OOM
场景时输出dump
信息【说明:OOM的发生是有概率的,甚至相隔数月才出现一例,出错时的堆内信息对解决问题非常有帮助】在线上生产环境,JVM的Xms和Xmx设置一样大小的内存容量,避免在GC后调整堆大小带来的压力
以上内容便是我一边读,一边思考整理出来的。《Java开发手册》是实践开发中提炼出来的精华
,当中有很多小点都是值得拿出来仔细推敲,往往小的规约背后隐藏着大学问。或许你和我一样,有些地方并不熟悉或者暂时并不理解为什么这样规约,个人觉得这些困惑的点读一遍、思考一遍远远不够,应该收藏下来,有空的时候多读、多思考。如果开发中遇到了手册中类似的场景,那么收获会更大,理解会更深。最后,正如手册中提到的愿景,“码出高效,码出质量”,希望你我都能码出一个新的高度。
Java开发最佳实践(二) ——《Java开发手册》之"异常处理、MySQL 数据库"
原文:https://www.cnblogs.com/itwild/p/12353164.html