从一份Java规范说起

最近在微信群里看到阿里的Java编码规范,主要是Java后端的代码编写规范与约定,分为强制、参考、推荐3个不同等级,涵盖了日常开发的很多细节。

趟了无数的坑,才能写出这份规范。

这篇博客也是主要写一下自己对这份规范的理解与实践。


从一份Java规范说起_第1张图片

自工作以来,虽年限不长,但是阅读了不少代码编写规范,包含日常Java开发约定、框架使用注意事项、DB使用规范。逐渐习惯按照一些约定进行编程,但任何团队的开发素质都是参差不齐的,编程习惯也是有很大差异的。个人经历了eclipse与intellij idea,svn与git,windows与mac等切换,若工作年限长一些,可能经历的会更多。废话不多说,逐节谈一下理解吧。

1、编程规约

1.1、命名规约

主要是POJO、变量、方法名、类名、包名、命名英文化的强制要求,方便阅读维护。提到一个问题:bool类型的变量不能以is开头。这个问题遇到过不止一次,开发的时候需要注意。还有2个命名习惯问题,
一是方法内的局部变量命名。个人习惯是用ide推荐的命名(intellij idea),若非Java的对象类型,一般是对象类型的lowerCamelCase风格,有时也会命名成实际的作用。
二是数字2、4,由于这2个数字英文同to,for,所以有时可以见到这种命名,最著名的就是Log4j。但是个人不太推荐,虽然任何规范里都没禁止过。

1.2、常量定义

提到了禁止魔术值的使用,常量的定义要分层分类;
常量的分层分类,若有纵表或提供平台级服务,那么常量的分层分类便很有必要;
还有一种case, 就是基础类型转换时,推荐使用括号包含起来,更易阅读:

long secondOfHour = (long) day * 60 * 60;//不推荐
long secondOfHour = (long) (day * 60 * 60);//推荐

1.3、格式规约

包含大括号的使用、系统关键字之间留空格、禁止使用tab、换行原则。禁止tab或用4个空格替换,这一条去github上看一些个人源码就知道,会导致格式混乱。

1.4、oop规约

这段较长,凸显其重要之处。捡重要的说吧。
覆写方法时,需要加@Override关键字;
构造方法与POJO对象的set/get方法内禁止有逻辑,遇到过在set方法内写了一段逻辑,难以排查;
对象equals判等时,确定有值的在前,避免NPE:

bool isInvalid = Enum.PhoneUser.getValue.equals(dto.getUserType());

所有POJO的数据类型必须使用包装类型,并且不允许有默认值;使用方负责判空。见过许多代码,有对接口返回或者POJO的字段不判空就直接用的,也有对包装类型未判空就intValue/equals的;
序列化类新增属性时,勿修改 serialVersionUID 字段,否则会导致反序列化失败;在不兼容变时一定修改该值;
慎用 Object 的 clone 方法来拷贝对象,原因是这个方法是浅拷贝,如果要使用,最好覆写该POJO的clone方法;

1.5、集合处理

equals与hashcode要修改需要成对,不可单独修改其中一个;
Set、Map的key值对象均需要覆写这2个方法,否则无效;
注意Arrays、ArrayList.sublist的使用;
提到了Map类集合k/v对null的容忍以及线程安全问题;
以及集合的有序性、稳定性;
集合这一块在实际使用时出现的问题是最多的,也不是一两句可以说完的,这份规范对这一块比较简略。
比如下面这段代码,一般来说是不会有问题的,

        for (HybridQueryOrderDO hybridQueryOrderDO : list){
            System.out.println(hybridQueryOrderDO.getMobile());
        }

但是下面这种情况,就会有问题了:

public static void main(String[] args) {
        List list = Lists.newArrayList();
        list.add(null);
        list.add(HybridQueryOrderDO.queryByMainOrderIds(Lists.newArrayList(1)));
        for (HybridQueryOrderDO hybridQueryOrderDO : list){
            System.out.println(hybridQueryOrderDO.getMobile());
        }
    }

现实情况出现过这种case:

        List list = Lists.newArrayList();
        //other logic
        list.addAll(reserveOrderRemoteService.queryOrderByIdList(Lists.newArrayList(1,2,3)));

对于这种情况有三步:团队规范、codeview、工具检测。

1.6、并发处理

并发处理是Java开发里的热点问题,自Java5引入并发集合包之后一直在改进这方面的使用,包括Java8的lambda表达式、stream流。这份规范里没有重复这些,而是更多的从实际使用中来约定、限制。
并发,我认为要理解、注意这几点:

临界资源、线程安全、性能

这份规范里提到了以下几点:
创建线程时指定名称,方便异常时定位回溯;
禁止自行创建线程,而应通过线程池,要注意线程池也可能会oom;
加锁时,若有多个条件,那么解锁时也要按顺序,否则会deadLock;
高并发时,简化、减少锁的使用,提高性能;
HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升。不论是Java8以下的树结构,或者Java8的树+红黑树结构,均可能出现这种情况;

并发处理和集合一样,也不是靠一份规范就解决问题,更重要的是实践、总结。而出现并发问题时,排查起来相对集合问题困难很多,这就需要在写代码时很小心,并且多做测试、一起review,养成良好的编程习惯,在出问题时方能快速定位、排查、解决。

1.7、控制语句

这里更多的是一些推荐习惯问题。但是我在维护项目时,也遇到了一些不良习惯。

if(true) //do something
if(false) return null;

这种写法有时就会惹祸。我一直推荐在if后无论何时均要加上{}。
对于这里的推荐的,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性,这一点我认为对于条件特别多,并且存在与或非关系时更方便阅读:

bool isNewUser = //logic
bool isNotPdUser = //logic
bool isInitStatus = //logic
bool isCanRefund = (isNewUser || isNotPdUser) && isInitStatus;

对于if、else嵌套层级较深的,这里也给出了两种方案:卫语句/状态模式。在实际开发时,有时采用卫语句反而更难维护。

if (refunded){

}else if (unRefund){
  if (verified){

  }else if (init){
    if (unpay){

    }else{

    }
  }
}

实际上对于业务层,对于复杂的业务,出现这种嵌套条件语句不足为奇,若改成卫语句,若有修改,想必也是很麻烦的,而遇到熟悉业务的人,很可能会出现漏条件的情况。
规范里还提到了方法中对参数的校验问题,以下几种情况是要对参数做校验的:

1) 调用频次低的方法。
2) 执行时间开销很大的方法,参数校验时间几乎可以忽略不计,但如果因为参数错误导致
   中间执行回退,或者错误,那得不偿失。
3) 需要极高稳定性和可用性的方法。
4) 对外供的开放接口,不管是 RPC/API/HTTP 接口。 
5) 敏感权限入口。

对于下面的内部实现process,参数校验应该是接口A/B的各有一份主要入参的校验,process有一份全面的参数校验,绝对不要A、B各有一套完整的参数校验,process内已有,否则逻辑有调整,A/B都完蛋。


从一份Java规范说起_第2张图片
接口参数校验.png

1.8、注释规约

关于注释,有2种论调,一种是代码自注释,包括类、方法、局部变量,名如其实,这样阅读起来无障碍,读完即可知其意;还有一种是代码需要详尽的注释,最好是包含逻辑、变更人、日期等。
从实际来看,若业务稳定,采用任意注释方式均可,但若业务快速发展,那么采取代码自注释+核心逻辑简要注释更靠谱,并要求代码提交时带上业务变更信息,这样方便查看变更。
有一点是规范里没提到的,就是如果有中文注释,ide的编码要调成utf-8,否则暴露出去之后,别人就可能无法阅读了。

1.9、其他

比较散,就不细说了。

这些就是关于java的开发规范,但是在实际中,还有一些场景并没有提到,包括:

1)第三方工具包的使用

guava包、Google工具包、Apache工具包、lombok工具,遇到不会的,或者看到项目里有现成的工具,秉承拿来主义,会直接使用而不去深究背后的逻辑,这个时候很容易出意外。在用这些工具代码前,最好是了解一下源码或者实现,这样才能避免踩坑或挖坑。

2)spring注解与spring配置文件

不论使用哪一种都可以,但是对于一个类,无特殊情况,不要采用spring注解+xml配置文件,或set、get方法+lombok等混用的情况,采用其中一个即可。

3)重视ide侧边栏语法提醒

很多开发同学,都可能没注意到Intellij Idea代码区有语法提醒,有不少语法、逻辑错误都会被检测到。推荐消灭黄色的提醒。当然,也有误提醒,但是我相信能看到这篇文章的同学都能判断的出来。

2、异常日志

异常处理是Java开发中必须要做好的一件事,日志是在追查问题、case重现时必不可少的工具。

2.1、异常处理

对于异常而言,有以下约定:
1)Java类库中继承自RuntimeException的异常,无需显式捕获;
2)try catch代码要尽量简短,不能大段catch。尽量不要在循环中try catch;
3)异常捕获了必须要处理,最起码要打印一条日志,否则捕获这个异常无意义。不处理可以将异常逐层上抛;
4)finally要合理使用;
5)防止NPE是基本素养;
实际开发时,经常有自定义异常的场景,如流程控制。这种情况是要捕获或者严格处理对应的异常类型,而不能直接catch RuntimeException,否则业务异常很可能就失效了。如果是catch了多层异常,那么此时就要注意先后顺序,否则可能出现deadcode。
还有一种情况和异常有关,当返回类型是包装类型,带一些辅助字段时,严禁将异常堆栈塞入msg字段。首先是业务方很可能对堆栈信息无法处理,其次异常堆栈的大量信息序列化与反序列化都很耗时,若是高请求量接口,还会耗费带宽。

public class Response implements Serializable {
    private static final long serialVersionUID = -1L;
    private boolean success = true;
    private int code = RemoteCode.SUCCESS.getVal();
    private String msg = "成功";
    private T result;
   ……
}

2.2、日志打印

日志是在排查线上问题时最重要的工具之一。一个具有良好格式的日志记录,会很方便的排查线上问题。但是一个冗余、混乱的日志,也会造成严重后果。
1)日志要保存一定周期,分文件、分级输出;
2)避免重复打印日志,配置文件内additivity=false。这个配置的意思是,若配置文件的appender若有继承关系时,则同一份日志只会在子appender内输出,而不会同时输出。减少了一定的日志输出;
3)输出异常日志时,应同时输出现场和异常堆栈信息,否则很可能是无效日志;
开发时,遇到几个常见的问题。逐一说明。
1)出现了e.printTrace,这样随意的代码打印不出来想要的结果;
2)注意公司日志框架可能存在的问题。比如下面这条日志输出的源码:

    /**
     * Error level message
     */
    void error(Object message, Throwable t);

    /**
     * Error level message
     */
    void error(Object message);

若习惯性的logger.error(e),那么最终将输出的是java.lang.Exception,不要说现场了,连堆栈信息都拿不到;
3)若有条件,可以将异常与系统打点/熔断/降级结合,进行相应的处理;
除此之外,很多时候去线上排查日志,需要多台机器进行并行查询,单台机器逐个进行grep的话,效率很低。后面会开一篇单独讲如何利用linux的一些命令并行访问日志。

3、MySQL规约

数据库是开发时经常涉及的,其中也是有很多的隐藏知识点。

3.1、建表规约

建表作为常见操作,有以下几点注意事项:
1)表名、字段名在命名时要审慎,字段的类型也要审慎。例如字段名,字段改名,innoDB的做法是先生成一张临时表,将字段改名,随后将原表数据同步过来,然后将原表改名或者软删,最后将临时表改成原表名,这样就实现了字段改名。从步骤就可以看出,对于大表,同步数据是一个大过程,并且同步过程中还要避免数据同步问题。加字段也是类似的步骤。
以下是伪操作

create table tmp;
sync table data from origin to tmp;
rename table origin as origin_tmp;
rename table tmp as origin;
……(other check and sync operations);
delete origin_tmp;

2)对于超大的varchar字段,考虑单独建一张表,相应字段为text类型,同时进行主键关联,避免对原表的索引效率影响;
3)任意表内必须存在主键id,创建时间与更新时间。更新时间在进行归档时特别有效。而新增时间对于追溯有一定作用。除非是枚举表,否则这几个字段都不应该缺失;
除此之外,还有几点在设计时可供参考:
1)对于一些核心大表,在设计时,可适当留几个字段备用,可在业务未来扩展时进行改名。字段改名的成本是小于新增字段的。

3.2、索引规约

索引对于日常的DB使用效率是明显的,索引的好坏差异非常明显。但是这份规约对于索引部分写的很克制,内容很少,实际在使用时的注意事项远不止这里提到的几点。
1)业务具有唯一性的数据,必须加上唯一索引。应用层无法保证不产生脏数据;
2)多表join关联查询时,需要注意关联字段要索引,并且类型相同。这里有一个坑点时,若类型不同,那么DB会进行隐式类型转换,这样可能会导致索引失效。

select a.name from table_a a,table_b where a.id=b.out_id;

若a.id与b.out_id类型不一致,那么此时会有隐式类型转换,并且可能会导致索引失效;
3)模糊匹配时只能左匹配。以下前两种模糊匹配是无法走索引的,效率奇低,第三个是正确用法;

select * from table where name like %ssss%;(模糊匹配全文中含ssss的,错误用法)
select * from table where name like %ssss;(模糊匹配全文以ssss结尾的,错误用法)
select * from table where name like ssss%;(模糊匹配全文以ssss开头的,错误用法)

即使这样,也不推荐过多使用like进行模糊匹配操作;
4)利用覆盖索引,避免回表查询影响效率。
5)利用延迟关联或者子查询优化超多翻页问题。大翻页在泛条件查询时会出现:

select * from table where add_date > '2010' limit 1000000,100 ;

由于MySQL InnoDB在进行翻页时,会遍历拿到前1000000条数据,然后抛弃掉,再偏移100,拿到数据进行返回,这样在查询时效率会非常低,改成如下几种方式:

select * from table where id in (select id from table where add_date > '2010' limit 1000000,100);
select * from table where id > (select id from table where add_date > '2010' limit 1000000,1) limit 20;

除了列举的这两种写法,还有别的技巧就不一一赘述。
不过要注意的是,这种超大翻页的场景应在系统设计时就应避免,如不允许无条件查询,或者仅允许一页一页翻,不允许输入页数快速跳转等。
6)创建索引时需要注意区分度。区分度高的才是一个合格的索引。区分计算有公式:

count(distinct left(列名, 索引长度))/count(*)

在实际使用时,对于不确定的sql,可以使用explain函数来确定是否走索引了,避免无索引查询。

3.3、SQL规约

这节实际中更多的是约定于解释,我选择几条我自己遇到过的来说明。
1)不使用外键,采用逻辑关联解决外键关联。外键在数据变更操作时会有级联更新,影响DB操作效率,不推荐。
2)删除数据时可以先查询,避免写错了产生悲剧。虽然很多公司有DBA审查SQL,但是不能依赖DBA的操作,需要养成良好习惯;

3.5、ORM规约

采用SSH/SSI/SSM结构之后,底层的orm一般用hibernate/ibatis/mybatis的居多,那么在实际使用时,有一些注意事项。
1)不要在xml配置文件中使用${},易出现注入风险。这个一般都会被公司级的sql扫描工具扫描到。
2)在更新时,对于未改变的数据,最好不要传入参数,原因如规范中所说:

一是易出错;二是效率低;三是 binlog 增加存储

3)spring中的@Transactional,会影响qps。同时在使用事务时,需要注意异常回滚后的关联回滚,如缓存、业务、消息撤回、统计调整等。

4、工程规约

这部分比较多是一些约定。

你可能感兴趣的:(从一份Java规范说起)