谈谈ali与Google的Java开发规范


声明:本文转载自https://my.oschina.net/u/3386233/blog/1603557,转载目的在于传递更多信息,仅供学习交流之用。如有侵权行为,请联系我,我会及时删除。

  无规矩不成方圆,编码规范就如同协议,有了Http、TCP等各种协议,计算机之间才能有效地通信,同样的,有了一致的编码规范,程序员之间才能有效地合作。道理大家都懂,可现实中的我们,经常一边吐槽别人的代码,一边写着被吐槽的代码,究其根本,就是缺乏遵从编码规范的意识!多年前,Google发布Google Java Style来定义Java编码时应遵循的规范;今年年初阿里则发布阿里巴巴Java 开发手册,并随后迭代了多个版本,直至9月份又发布了pdf终极版。这两大互联网巨头的初衷,都是希望能够统一标准,使业界编码达到一致性,提升沟通和研发效率,这对于我们码农无疑是很赞的一笔福利呀。笔者将两份规范都通读了一遍,其中列举的不少细则跟平时的编码习惯基本是符合的,不过还是有不少新奇的收获,忍不住记录在此,供日后念念不忘~

Java开发规范总览

一、Google Java Style

  Google的java开发规范主要分为6大部分:源文件基本规范、源文件结构、代码格式、命名、编程实践和Javadoc,各部分概要如下:

1、源文件基本规范(source file basics):文件名、文件编码、特殊字符的规范要求 2、源文件结构(source file structure):版权许可信息、package、import、类申明的规约 3、代码格式(formatting):大括号、缩进、换行、列长限制、空格、括号、枚举、数组、switch语句、注4、解、注释、和修饰符等格式要求 5、命名(Naming):标识符、包名、类名、方法名、常量名、非常量成员名、参数名、局部变量的命名规范 6、编程实践(Programming Practices):@override、异常捕获、静态成员、Finalizers等用法规约

二、阿里巴巴Java开发手册

  阿里的Java开发手册相对于前者更上一层楼,它除了基本的编程风格的规约外,还给出了日志、单元测试、安全、MySQL、工程结构等代码之外的规约,据说是阿里近万名开发同学集体智慧的结晶,相当了得,还是挺值得借鉴一下的。各部分概要如下:

1、编程规约:命名风格、常量、代码格式、OOP、集合处理、并发、控制语句、注释等 2、异常日志:异常处理、日志的命名、保留时间、输出级别、记录信息等 3、单元测试:AIR原则(Automatic,Independent,Repeatable)、单侧的代码目录、目标,单侧的写法,即BCDE原则(Border,Correct,Design,Error) 4、安全规约:权限校验、数据脱敏、参数有效校验、CSRF安全过滤、防重放限制、风控策略等 5、MySQL数据库:建表、索引、SQL语句、ORM映射等 6、工程结构:应用分层、二方库依赖(坐标命名、接口约定、pom配置)、服务器端各项配置(TCP超时、句柄数、JVM参数等)

熟知的规范

  对于大家已经烂熟于心并已习惯遵守的一些编码规范,比如类名、常量的命名、数组的定义、Long类型的字面等,就不在此一一列出了,只想就一些平时编码中较容易个性化,并可能会存在争议的规范进行一番探讨。为了便于说明,用G表示规范出自于Google Java StyleA表示规范出自于阿里巴巴Java开发手册

[A]IDE的text file encoding设置为UTF-8;IDE中文件的换行符使用Unix格式,不要使用Windows格式([G]文件编码:UTF-8)

  看似简单的一个编码约定,在实际开发过程中却经常出现不一致,由于我们是中文操作系统,系统编码是GBK。当两个协作的开发人员IDE,一个采用系统默认编码,一个设置为UTF-8,那么二人看对方写的中文注释就各自都是乱码了,很尴尬。对于“换行符使用Unix格式”,这个在编写shell和hive脚本时踩过好几次坑,而且错误提示很隐晦,一时半会还真察觉不出来,只能说这个规范请务必遵守!

[A]代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。

  大多数程序员还是都会遵从英文的命名方式,但在实际工作中还真有遇到过拼音与英文混用的命名,比如创建报文的函数命名为createBaowen,看起来怪怪的,有点不伦不类。

[A]抽象类命名使用Abstract或Base开头;异常类使用Exception结尾;测试类以它要测试的类的名称开始,以Test结尾

  以spring源码为例,其抽象类都是以Abstract开头,异常类以Exception结尾,测试类则是以Tests结尾。

[A]POJO类中布尔类型的变量,都不要加is,否则部分框架解析会引起序列化错误。

  这个问题一说大家都知道,但实际却是很容易被忽视!因为Boolean通常表达“是”或“否”的意思,可能一遇到布尔变量,大家会习惯性地将它与is关联起来,“很自然”地就会以is开头定义变量。但笔者想说的是,这其实反应了至少两个问题:1、对JavaBean属性命名规范不熟;2、对框架解析POJO的原理不熟,如RPC反向解析、spring MVC参数绑定、MyBatis处理映射等。

private boolean isActive; //lombok、Eclipse生成getter、setter的结果如下,框架会误把变量解析成active public boolean isActive() {   return isActive; } public void setActive(boolean isActive) {   this.isActive = isActive; } 

  在搞清这两个问题前,还是建议老老实实按规范来吧。

包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,类名若有复数含义,则可使用复数形式。

  实际工作中看到过包名包含下划线的,如org.sherlockyb.user_manage.dao,还是有必要统一一下。

[A]不允许任何魔法值(即未经定义的常量)直接出现在代码中。 反例:String key = "Id#taobao_" + tradeId; ​ cache.put(key, value);

  避免硬编码问题是每个程序员都应该具备的基本素养,硬编码所带来的可读性差、维护困难等问题,众所周知。

[A,G]采用空格缩进,禁止使用tab字符。

  这是Google和ali一致的规约,只不过前者是一个tab对应2个空格,后者则是4个空格。之所以不提倡tab键,是因为不同的IDE对tab键的“翻译”默认有所差异,容易因不同程序员的个性化而导致同一份代码的格式混乱。

[A,G]单行字符数限制不超过120/100个字符,超出需要换行,换行时遵循如下规则: 1)[A,G]第二行相对于第一行缩进4个空格,从第三行开始,不再继续缩进。 2)[A]运算符或方法调用的点符号与下文一起换行([G]若是非赋值运算符,则在该符号前断开;若是赋值运算符foreach中的分号,则在该符号后断开)。 4)[A]方法调用时,多个参数,需要换行时,在逗号后进行([G]逗号与前面的内容留在同一行)。 5)在括号前不要换行。

  对于单行字符限制,阿里的是120,Google的是100。个人觉得120略长,特别是当用笔记本码代码时,对于超限的代码行,经常要用横向滚动条,不太友好,个人推荐100的限制。

没有必要增加若干空格来使某一行的字符与上一行对应位置的字符对齐。

  在变量较多时,这种对齐是一种累赘。虽说有IDE的自动格式化功能,但多人协作时,难保各自的格式化没有差异,会因格式变化而造成不必要的代码行改动,无疑会给你的代码合并徒增困扰。

方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。

  代码分块就如同文章分段,整洁的代码具有更强的自解释性。

外部正在调用或者二方库依赖的接口,不允许修改方法签名,避免对接口调用方产生影响。作为提供方,接口过时必须加@Deprecated注解,并清晰地说明采用的新接口或者新服务是什么;作为调用方,有义务去考证过时方法的新实现是什么。

  接口契约,是使用方和调用方良好协作的有效保障,请务必遵守。

所有的相同类型的包装类对象之间值的比较,全部用equals方法比较。 说明:对于Integer var = ?在**-128至127**范围内的赋值,Integer对象是在IntegerCache.cache产生,会复用已有对象,这个区间内的Integer值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是个大坑,推荐使用equals方法进行判断。

  这里补充几点,除了Integer,其他包装类型如Long、Byte等都有各自的cache。这里只提到了等值比较,对于>,<等非等值比较,没必要手动拆箱去比较,包装类型之间直接可以比较大小,亲测有效。例如:

Long a = new Long(1000L); Long b = new Long(222L); Long c = new Long(2000L); Assert.isTrue(a > b && a < c);  //断言成功 

[A]关于基本数据类型与包装数据类型的使用标准如下: 1)所有的POJO类属性必须使用包装数据类型。 2)RPC方法的返回值和参数必须使用包装数据类型。 3)所有的局部变量使用基本数据类型。 说明:POJO类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题,或者入口检查,都由使用者来保证

  基本类型作为入参和返回值有多种弊病,如不情愿的默认值,NPE风险等,除了局部变量,其他慎用。

序列化类新增属性时,请不要修改serialVersionUID字段,避免反序列化失败;如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID值。

  serialVersionUID是Java为每个序列化类产生的版本标识:版本相同,相互之间则可序列化和反序列化;版本不同,反序列化时会抛出InvalidClassException。因不同的jdk编译很可能会生成不同的serialVersionUID默认值,通常需要显式指定,如1L。

[A]final可以声明类、成员变量、方法、以及本地变量,下列情况使用final关键字: 1)不允许被继承的类,如:String类。 2)不允许修改引用的域对象,如:POJO类的域变量。 3)不允许被重写的方法,如:POJO类的setter方法。 4)不允许运行过程中重新赋值的局部变量,如传递给匿名内部类的局部变量。

  final关键字有诸多好处,比如JVM和Java应用都会缓存final变量,以提高性能;final变量可在多线程环境下放心共享,无需额外的同步开销;JVM会对final修饰的方法、变量及类进行优化等,详情可见深入理解Java中的final关键字

慎用Object的clone方法来拷贝对象。 说明:对象的clone方法默认是浅拷贝,特别是引用类型成员。若想实现深拷贝,需要重写clone方法实现属性对象的拷贝。

  Java中的赋值操作都是值传递,比如我们常用来“复制”DTO的工具,无论是spring的BeanUtils.copyProperties,还是Apache commons的BeanUtils.cloneBean,实际上也只是两个DTO之间成员的引用复制,成员指向的对象还是同一个,用到此类工具的时候要有这个意识,不然容易踩坑。

[A]类成员与方法访问控制从严: 1)如果不允许外部直接通过new来创建对象,那么构造方法必须是private。 2)工具类不允许有public或default构造方法。 3)类非static成员变量并且与子类共享,必须是protected。 4)类非static成员变量并且仅在本类使用,必须是private。 5)类static成员变量如果仅在本类使用,必须是private。 6)若是static成员变量,必须考虑是否为final。 7)类成员方法只供类内部调用,必须是private。 8)类成员方法只对继承类公开,那么限制为protected。 说明:任何类、方法、参数、变量,严控访问范围。过于宽泛的访问范围,不利于模块解耦。

  最小权限原则(Principal of least privilege,POLP)是每个程序员应遵守的,可有效避免数据以及功能受到错误或恶意行为的破坏。

[A]ArrayList的subList结果不可强转成ArrayList,否则会抛出ClassCastException异常。

  这里补充一点,SubList并未实现Serializable接口,若RPC接口的List类型参数接受了SubList类型的实参,则在RPC调用时会报出序列化异常。比如我们常用的guava中的Lists.partition,切分后的子list实际都是SubList类型,在传给RPC接口之前,需要用**new ArrayList()**包一层,否则会报序列化异常。

[A]在subList场景中,高度注意对原集合元素个数的修改,会导致子列表的遍历、增加、删除均会产生ConcurrentModificationException异常。

  这个还是得从源码的角度来解释。SubList在构造时实际是直接持有了原list的引用,其add、remove等操作实际都是对原list的操作,我们不妨以add为例:

public void add(int index, E element) {   rangeCheckForAdd(index);   checkForComodification();        // 检查this.modCount与原list的modCount是否一致   l.add(index+offset, element);    // 原list新增了一个元素   this.modCount = l.modCount;      // 将原list更新后的modCount同步到this.modCount   size++; } 

  可以看出,SubList生成之后,通过SubList进行add、remove等操作时,modCount会同步更新,所以没问题;而如果此后还对原list进行add、remove等操作,SubList是感知不到modCount的变化的,会造成modCount不一致,从而报出ConcurrentModificationException异常。故通常来讲,从原list取了SubList之后,是不建议再对原list做结构上的修改的

[A]使用工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常。

  类似的,guava的Maps.toMap方法,返回的是一个ImmutableMap,是不可变的,不能对其调用add、remove等操作,使用时应该有这个意识!

在JDK7版本及版本以上,Comparator必须满足:1)x,y比较结果和y,x比较结果相反;2)x>y,y>z,则x>z;3)x=y,则x,z比较结果和y,z比较结果相同。不然Arrays.sort,Collections.sort会报IllegalArgumentException异常。

  JDK从1.6升到1.7之后,默认排序算法由MergeSort变为TimSort,对于任意两个比较元素x、y,其Comparator结果一定要是确定的,特别是对于x=y的情况,确定返回0,否则可能出现Comparison method violates its general contract!错误。

[A]线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 说明:Executors返回的线程池对象的弊端如下: 1)FixedThreadPoolSingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。 2)CachedThreadPoolScheduledThreadLocal:允许的创建线程数为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。

  现在一般很少会用Executors去创建线程池了,通常会使用spring的ThreadPoolExecutorFactoryBean或者guava的MoreExecutors.listeningDecorator对前者包装一下,对于像线程数、队列大小等都是通过配置来设定。

[A]高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

  一句话概括就是,能不锁就不锁,即便锁,也尽量使锁的粒度最小化。

[A]表达异常分支时,少用if-else方式,可使用卫语句代替。对于if()...else if()...else...方式,请勿超过3层。对于超过的,可使用卫语句、策略模式、状态模式等来实现。

if(condition) {   ...   return obj; } // 接着写else的业务逻辑代码; 

  冗长的if-else可读性差,维护困难,推荐使用卫语句,逻辑清晰明了。

[A]代码修改的同时,注释也做同步修改,尤其是参数、返回值、异常、核心逻辑等的修改。

  这个在实际工程代码中还真看到过不少,代码与注释牛头不对马嘴,尽量别留坑给后来者,应该算在程序猿的基本素养之内吧。

谨慎注释掉代码。在上方详细说明,而不是简单的注释掉。如果无用,则删除。 说明:代码被注释掉有两种可能:1)后续会恢复此段代码逻辑。2)永久不用。前者如果没有备注信息,难以知晓注释动机。后者建议直接删掉(代码仓库保存了历史代码)。

  这个就更无力吐槽了,比上一条更常见,so,这条规范强烈推荐!

1)对于注释的要求:第一、能准确反映设计思想和代码逻辑;第二、能描述业务含义,使别人能迅速了解到代码背后的信息;第三、好的命名、代码结构是自解释性的,注释力求精简准确、表达到位。避免过多过滥的注释。 2)finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。若是JDK7及以上,可使用try-with-resources。不能再finally块中使用return,finally块中的return返回后方法结束执行,不会再执行try块中的return语句。 3)防止NPE,是程序员的基本素养,注意NPE产生的场景:   1.返回类型为基本数据类型,return包装数据类型的对象时,自动拆箱有可能产生NPE   2.数据库的查询结果可能为null。   3.远程调用返回对象时,一律要求进行空指针判断,防止NPE。   4.对于Session中获取的数据,建议NPE检查,避免空指针。   5.级联调用obj.getA().getB().getC();一连串调用,易产生NPE。正例:使用JDK8的Optional类来防止NPE问题。 4)在代码中使用“抛异常”还是“返回错误码”,对于公司外的http/api开放接口必须使用“错误码”;而应用内部推荐异常抛出;跨应用间RPC调用优先考虑使用Result方式,封装isSuccess()方法、“错误码”、“错误简短信息”。 5)避免出现重复的代码(Don't Repeat Yourself),即DRY原则。

  以上几条,皆是毫无争议的基本规范,且行且遵守。

1)日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。 2)对trace/debug/info级别的日志输出,必须使用条件输出形式或者使用占位符的方式。以避免不必要的字符串拼接,浪费系统资源。 3)避免重复打印日志,浪费磁盘空间,对于特定包的日志,务必设置additivity=false。 4)异常信息应该包括两类信息:案发现场信息异常堆栈信息。如果不处理,则通过关键字throws往上抛。

  关于日志的几条不错的规范。日志作为服务器行为的日常轨迹,对于统计分析、故障排错意义巨大,要慎重对待才是。

1)好的单元测试必须遵守AIR原则。   A:Automatic(自动化)。全自动执行,非交互式的。使用assert验证,而非System.out。   I:Independent(独立性)。单侧用例之间不能产生依赖,互相独立。   R:Repeatable(可重复)。可重复执行,不能受到外界环境的影响。对于外部依赖,通过spring等DI框架注入一个本地(内存)实现或者Mock实现。 2)单元测试的基本目标:语句覆盖率达到70%;核心模块的语句覆盖率和分支覆盖率都要达到100%。 3)编写单元测试代码遵守BCDE原则:   B:Border,边界值测试,包括循环边界、特殊取值、特殊时间点、数据顺序等。   C:Correct,正确的输入,并得到预期的结果。   D:Design,与设计文档相结合,来编写单元测试。   E:Error,强制错误信息输入(如:非法数据、异常流程、非业务允许输入等),并得到预期结果。

  关于单元测试的几条不错的规范。单元测试是代码质量的有效保障!太多的想当然、自以为是,往往会跳过单测,最终自食其果。曾经的笔者也犯过类似毛病,还好及时纠正。

新奇的收获

  这里将列出一些笔者觉得有新收获的规范,有的是平时编码过程中没有严格遵守的,比如switch中default偶尔加偶尔不加;有的则是目前还不太清楚的规范。

[A]杜绝完全不规范的缩写,避免望文不知义。 反例:AbstractClass的“缩写”命名成AbsClass;condition的“缩写”命名成condi,此类随意缩写严重降低了