Java作为一门名副其实的工业级语言,语法友好,学习简单,大规模的应用给代码质量的管控带来了困难,特别是团队开发中,开发过程中的规范会直接影响最终项目的稳定性。
善医者“未有形而除之”,提高工程健壮性最好的方式是在代码出现问题之前就排除掉,不给Bug出现的机会。一份好的开发规范就可以起到这样的作用,大大减少产品上线后的问题。
《阿里巴巴Java开发手册》是阿里巴巴的内部编码规范,阿里官方的Java代码规范标准, 手册以Java应用开发为维度,分为编程规约、异常日志规约、MYSQL规约、工程规约、安全规约五个章节,给出了强制、推荐、参考三个级别,每条规范都有推荐的约束力度,从命名到项目拆分,不仅规范了一些开发细节,也提出了很多工程开发的哲学,值得好好阅读。
下面记录一些比较有启发的条款,提纲挈领,快速学习。
一、编程规约
1.如果使用到了设计模式,建议在类名中体现出具体模式
将设计模式体现在名字中,有利于阅读者快速理解架构设计思想。
2.相同参数类型,相同业务含义,才可以使用 Java 的可变参数,避免使用 Object
可变参数必须放置在参数列表的最后,尽量不用可变参数编程。
3.对外暴露的接口签名,原则上不允许修改方法签名,避免对接口调用方产生影响
接口过时必须加@Deprecated 注解,并清晰地说明采用的新接口或者新服务是什么。
4.关于基本数据类型与包装数据类型的使用标准如下
1) 所有的POJO类属性必须使用包装数据类型
2) RPC方法的返回值和参数必须使用包装数据类型
3) 所有的局部变量【推荐】使用基本数据类型
POJO 类属性没有初值是醒使用者在需要使用时,必须自己显式地进行赋值,任何 NPE 问题,或者入库检查,都由使用者来保证。数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。
5.注意 serialVersionUID 不一致会抛出序列化运行时异常
序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败;如果完全不兼容升级,避免反序列化混乱,那么请修改 serialVersionUID 值。
6.POJO 类必须写 toString 方法
使用 IDE 的中工具:source> generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString。 在方法执行抛出异常时,可以直接调用 POJO 的 toString()方法打印其属性值,便于排查问题。
7.final 可提高程序响应效率,声明成 final 的情况:
1) 不需要重新赋值的变量,包括类属性、局部变量
2) 对象参数前加final,表示不允许修改引用的指向
3) 类方法确定不允许被重写
8.慎用 Object 的 clone 方法来拷贝对象
对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象 的拷贝。
9.类成员与方法访问控制从严
1) 如果不允许外部直接通过new来创建对象,那么构造方法必须是private
2) 工具类不允许有public或default构造方法
3) 类非static成员变量并且与子类共享,必须是protected 4) 类非static成员变量并且仅在本类使用,必须是private
5) 类static成员变量如果仅在本类使用,必须是private
6) 若是static成员变量,必须考虑是否为final
7) 类成员方法只供类内部调用,必须是private
8) 类成员方法只对继承类公开,那么限制为protected
任何类、方法、参数、变量,严控访问范围。过宽泛的访问范围,不利于模块解耦。思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 Service 方法,或者一个 public 的成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,如果无限制的到处跑,那么你会担心的。
10.ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException 异常
subList 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList ,而是 ArrayList 的一个视图,对于SubList子列表的所有操作最终会反映到原列表上。
11.使用工具类 Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法
使用add/remove/clear 方法会抛出 UnsupportedOperationException 异常。asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList体现的是适配器模式,只是转换接口,后台的数据仍是数组。
12.不要在 foreach 循环里进行元素的 remove/add 操作
remove 元素请使用 Iterator方式,如果并发操作,需要对 Iterator 对象加锁。
13.获取单例对象需要保证线程安全,其中的方法也要保证线程安全
资源驱动类、工具类、单例工厂类都需要注意。
14.线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式
这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 Executors 返回的线程池对象的弊端如下: 1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
15.SimpleDateFormat 是线程不安全的类,一般不要定义为static变量
如果定义为static,必须加锁,或者使用 DateUtils 工具类。 注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal<DateFormat> df = new ThreadLocal<DateFormat>() { @Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
} };
16.高并发时,同步调用应该去考量锁的性能损耗
能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
17.并发修改同一记录时,避免更新丢失
要么在应用层加锁,要么在缓存加锁,要么在 数据库层使用乐观锁,使用 version 作为更新依据。 如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次。
18.对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁
19.使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用countDown
方法,线程执行代码注意 catch 异常,确保 countDown 方法可以执行,避免主线程无法执行 至 await 方法,直到超时才返回结果。注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。
20.避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。
Random 实例包括 java.util.Random 的实例或者 Math.random()实例。
21.volatile 解决多线程内存不可见问题
对于一写多读,是可以解决变量同步问题, 但是如果多写,同样无法解决线程安全问题。如果是 count++操作,使用如下类实现: AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
22.ThreadLocal 无法解决共享对象的更新问题,建议使用 static 修饰
这个变量是针对一个线程内所有操作共有的,所以设置为静态变量,所有此类实例共享 此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。
二、异常日志
1.对大段代码进行 try-catch,这是不负责任的表现
catch 时请分清稳定代码和非稳 定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分 异常类型,再做对应的异常处理。
2.捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之
如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
3.在代码中使用“抛异常”还是“返回错误码”
对于公司外的 http/api 开放接口必须 使用“错误码”;而应用内部推荐异常抛出;跨应用间 RPC 调用优先考虑使用 Result 方式,封 装 isSuccess、“错误码”、“错误简短信息”。
4.避免出现重复的代码(Don’t Repeat Yourself),即DRY原则
随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。
5.对trace/debug/info 级别的日志输出,必须使用条件输出形式或者使用占位符的方
式
6.异常信息应该包括两类信息:案发现场信息和异常堆栈信息
如果不处理,那么往上抛。
三、MySQL 规约
1.表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint ( 1表示是,0表示否),此规则同样适用于odps建表。 任何字段如果为非负数,必须是unsigned。
2.小数类型为 decimal,禁止使用 float 和 double
float 和 double 在存储的时候,存在精度损失的问题,很可能在值的比较时,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数分开存储。
3.表必备三字段:id, gmtcreate, gmtmodified
其中id必为主键,类型为unsigned bigint、单表时自增、步长为1。gmtcreate, gmtmodified 的类型均为 date_time 类型。
4.单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表
如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。避免过度设计。
5.业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引
6.在 varchar 字段上建立索引时,必须指定索引长度
没必要对全字段建立索引,根据实际文本区分度决定索引长度。 说索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分 度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度 来确定。
7.利用覆盖索引来进行查询操作,来避免回表操作
能够建立索引的种类:主键索引、唯一索引、普通索引,而覆盖索引是一种查询的一种 效果,用explain的结果,extra列会出现:using index。如果索引包含所有满足查询需要的数据的索引成为覆盖索引(Covering Index),也就是平时所说的不需要回表操作
8.利用延迟关联或者子查询优化超多分页场景
MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
9.SQL 性能优化的目标
至少要达到 range 级别,要求是 ref 级别,如果可以是 consts 最好。
1)consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。 2)ref 指的是使用普通的索引(normal index)。 3)range 对索引进行范围检索。
10.不要使用 count(列名)或 count(常量)来替代 count(*)
count()就是 SQL92 定义 的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。 count()会统计值为 NULL 的行,而 count(列名)不会统计此列为 NULL 值的行。
11.使用 ISNULL()来判断是否为 NULL 值
注意,NULL与任何值的直接比较都为 NULL
12.不得使用外键与级联,一切外键概念必须在应用层解决
外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
13.iBATIS 自带的 queryForList(String statementName,int start,int size)不推荐使用
其实现方式是在数据库取到 statementName 对应的 SQL 语句的所有记录,再通过 subList 取 start,size 的子集合,线上因为这个原因曾经出现过 OOM。
14.不要写一个大而全的数据更新接口
传入为 POJO 类,不管是不是自己的目标更新字段, 都进行 update table set c1=value1,c2=value2,c3=value3; 这是不对的。
执行 SQL 时,尽量不要更新无改动的字段,一是易出错;二是效率低;三是 binlog 增加存储。
四、工程规约
1.高并发服务器建议调小 TCP 协议的 time_wait 超时时间
操作系统默认 240 秒后,才会关闭处于 timewait 状态的连接,在高并发访问下,服 务器端会因为处于 timewait 的连接数太多,可能无法建立新的连接,所以需要在服务器上 调小此等待值。 正例:在 linux 服务器上请通过变更/etc/sysctl.conf 文件去修改该缺省值(秒): net.ipv4.tcpfintimeout = 30
2.调大服务器所支持的最大文件句柄数(File Descriptor,简写为fd)
主流操作系统的设计是将 TCP/UDP 连接采用与文件一样的方式去管理,即一个连接对应于一个 fd。主流的 linux 服务器默认所支持最大 fd 数量为 1024,当并发连接数很大时很 容易因为 fd 不足而出现“open too many files”错误,导致新的连接无法建立。 建议将 linux 服务器所支持的最大句柄数调高数倍(与服务器的内存数量相关)。
五、安全规约
1. 隶属于用户个人的页面或者功能必须进行权限控制校验
防止没有做水平权限校验就可随意访问、操作别人的数据,比如查看、修改别人的订单。
2. 用户敏感数据禁止直接展示,必须对展示数据脱敏
查看个人手机号码会显示成:158****9119,隐藏中间 4 位,防止隐私泄露。
3. 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入, 禁止字符串拼接 SQL 访问数据库
4. 用户请求传入的任何参数必须做有效性验证
忽略参数校验可能导致: page size过大导致内存溢出 恶意order by导致数据库慢查询 任意重定向 SQL注入 反序列化注入 正则输入源串拒绝服务ReDoS——Java 代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题, 但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的效果。
5. 禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据
6. 表单、AJAX 交必须执行 CSRF 安全过滤
CSRF(Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在 CSRF 漏洞的应用/网站,攻击者可以事先构造好 URL,只要受害者用户一访问,后台便在用户 不知情情况下对数据库中用户参数进行相应修改。
7. 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放限制, 如数量限制、疲劳度控制、验证码校验,避免被滥刷、资损
如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其 它用户,并造成短信平台资源浪费。