美文网首页
从一份Java规范说起

从一份Java规范说起

作者: zclzhangcl | 来源:发表于2017-02-27 22:11 被阅读0次

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

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

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


    自工作以来,虽年限不长,但是阅读了不少代码编写规范,包含日常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<HybridQueryOrderDO> list = Lists.newArrayList();
            list.add(null);
            list.add(HybridQueryOrderDO.queryByMainOrderIds(Lists.newArrayList(1)));
            for (HybridQueryOrderDO hybridQueryOrderDO : list){
                System.out.println(hybridQueryOrderDO.getMobile());
            }
        }
    

    现实情况出现过这种case:

            List<HybridQueryOrderDO> 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都完蛋。


    接口参数校验.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<T> 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规范说起

          本文链接:https://www.haomeiwen.com/subject/uionwttx.html