• 谈谈阿里巴巴与Google的Java开发规范(2)

    发布:51Code 时间: 2018-01-15 10:27

  • 这个就更无力吐槽了,比上一条更常见,so,这条规范强烈推荐! 1)对于注释的要求:第一、能准确反映设计思想和代码逻辑;第二、能描述业务含义,使别人能迅速了解到代码背后的...

  • 这个就更无力吐槽了,比上一条更常见,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,此类随意缩写严重降低了代码的可阅读性。

    说来惭愧,这类不规范的缩写,笔者之前还真干过几次。有时候是觉着变量太长,导致明明逻辑很简单的一条语句,就超过了列限制,于是乎主观地缩写命名,如mergedRegionReportDtos缩写为mRegReportDtos,accountIdToHourReportDtos缩写为accountIdToHrDtos,相当混乱有木有!所以,如果对英文单词的缩写拿不定的话,还是直接用原单词吧,长点就长点,可读性很重要。

    [A]如果模块、接口、类、方法使用了设计模式,在命名时体现出具体模式,有利于阅读者快速理解架构设计理念。类示例:OrderFactory、LoginProxy、ResourceObserver。

    没啥好说的,同样是为了提升代码的自解释性。spring源码中随处可见这样的命名风格:AbstractAutowireCapableBeanFactory、Cglib2AopProxy、BeanDefinitionParserDelegate等

    [A]接口类中的方法和属性不要加任何修饰符号(public也不要加),保持代码的简洁性,并加上有效的Javadoc注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法有关,并且是整个应用的基础常量。 正例:接口方法签名:void f(); ? 接口基础常量表示:String COMPANY = "alibaba"; 反例:接口方法定义:public abstract void f(); 说明:JDK8中接口允许有默认实现,那么这个default方法,是对所有实现类都有价值的默认实现。

    目前所见过的组内代码,有太多的接口中方法都是加了public,也许是后来的编码者看到前任留下的已有方法都加了,为了保持一致,于是乎也加了public。说到底还是最初的良好规范没有形成,导致给后来者以错误的指引!简单才是美,把public 去掉吧。

    [A]接口的命名规则:如果是形容能力的接口名称,取对应的形容词做接口名(通常是-able的形式) 正例:AbstractTranslator实现Translatable

    Log4j中的AppenderAttachable,JDK中的AutoCloseable,Appendable等。

    [A]各层命名规约: A)Service/DAO层方法命名前缀规约 ? 1)获取对象时,单个用get/多个用list;2)获取统计值用count ? 3)插入用save/insert;4)删除用remove/delete;5)修改用update

    关于资源的CRUD,这块的方法命名相当乱,太容易个性化了!至少目前组内代码,要啥有啥:query与get并存,查询列表和计数的都是get,并未做区分;一会儿remove,一会儿delete;既有save也有insert。当你Ctrl+O的时候,想找个count某元素的方法时贼费劲,急需统一!

    [A]不要使用一个常量类维护所有常量,按常量功能进行归类,分开维护。 说明:大而全的常量类,非得使用查找功能才能定位到修改的常量,不利于理解和维护。 正例:缓存相关常量放在类CacheConsts下,系统配置相关常量放在类ConfigConsts下。

    [A]常量的复用层次有五层:跨应用共享常量、应用内共享常量、子工程内共享常量、包内共享常量、类内共享常量。 1)跨应用共享常量:放置在二方库中,通常是client.jar中的constant目录下。 2)应用内共享常量:放置在一方库中,通常是modules中的constant目录下。 3)子工程内共享常量:当前子工程的constant目录下。 4)包内共享常量:当前包下单独的constant目录下。 5)类内共享常量:直接在类内部private static final定义。

    常量的维护也可运用设计模式思想,单一职责,分层,严格控制作用域,使常量更清晰,易于理解,便于维护。

    [A]类内方法定义顺序依次是:共有方法或保护方法 > 私有方法 > getter/setter方法。但有个规则特例:[A,G]当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起。即重载永不分离。 说明:共有方法是类的调用者和维护者最关系的方法,首屏展示最好;保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现;因为承载的信息价值较低,所有Service和DAO的getter/setter方法放在类的最后。

    方法的排版要有秩序,这样在我们Ctrl+O的时候才能更方便的查阅方法列表。阿里的约定是比较通用的规则,对此,Google的看法则不同,它认为类的成员顺序不存在唯一的通用法则,重要的是,每个类应该以维护者所能解释的排序逻辑去排序它的成员。常见的反例:新的方法总是习惯性地添加到类的结尾,排序毫无意义。

    [A]对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。 说明:线程一需要对表A、B、C依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A、B、C,否则可能出现死锁。

    从死锁产生的条件出发来避免死锁。比如我们根据一批ids批量更新数据库记录时,预先对ids排序,也是一种能有效降低死锁发生概率的措施。

    [A]使用CountDownLatch进行异步转同步操作,每个线程退出前必须调用countDown方法,线程执行代码注意catch异常,确保countDown方法被执行到,避免主线程无法执行至await方法,直到超时才返回结果。

    避免Random实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed导致的性能下降。 说明:Random实例包括java.util.Random的实例或者Math.random的方式。 正例:在JDK7之后,可以直接使用API ThreadLocalRandom,而在JDK7之前,需要编码保证每个线程持有一个实例。

    volatile关键字解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是count++操作,使用如下类实现:AtomicInteger count = new AtomicInteger(); count.addAndGet(1); 如果是JDK8,推荐使用LongAdder对象,比AtomicLong性能更好(减少乐观锁的重试次数)。

    volatile关键字只是保证了同一个变量在多线程中的可见性,更多的是用于修饰作为开关状态的变量。但是volatile只提供了内存可见性,而没有提供原子性!volatile变量在每次被线程访问时,都强迫从主内存中重读该变量的值,而当该变量发生变化时,又会强迫线程将最近的值刷新到主内存,对于像boolean flag = true等原子性赋值操作是没问题的,但volatile不能保证复合操作的原子性,如count++。

    [A]除常用方法(如getXxx/isXxx)等外,不要在条件判断中执行其他复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。

    这个笔者之前确实有过这样的坏习惯,为了省略一条赋值语句,将if中的条件搞得比较复杂,代码冗长,可读性也差,得不偿失。

    [A]参数校验与否: 需要校验的:1)对外提供的开发接口,不管是RPC/API/HTTP接口;2)敏感权限入口;3)需要极高稳定性和可用性的方法 不需校验的:1)极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查要求。2)底层调用频度较高的方法。如一般Service会做参数校验,到了DAO层,参数校验可省略。3)被声明为private只会被自己代码所调用的方法,如果能确定传入参数已做过检查或者肯定不会有问题,此时可不校验参数。

    过多的参数校验,不仅是冗余代码,而且还影响性能,只在必要的时候做校验。

    1)隶属于用户个人的页面或功能必须进行权限控制校验。说明:防止没有做水平权限校验就可随意访问、修改、删除别人的数据。 2)用户请求传入的任何参数必须做有效性校验。忽略参数校验可能导致:1)page size过大导致内存溢出;2)恶意order by导致数据库慢查询;3)任意重定向;4)SQL注入;5)反序列化注入;6)正则输入源串拒绝服务ReDos 3)表单、AJAX提交必须执行CSRF(Cross-site request forgery)安全过滤 4)在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷,资损。 5)发帖、评论、发送即时消息等用户生成内容的场景必须实现防刷、文本内容违禁词过滤等风控策略。

    基本的安全意识还是要有的,一旦踩了坑,后果不堪设想。

    1)数据库表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是unsigned tinyint(1表示是,0表示否)。 2)禁用保留字,如desc、range、match、delayed等,参考MySQL官方保留字。 3)主键索引名为pk_字段名;唯一索引名为uk_字段名;普通索引名为idx_字段名。 4)varchar是可变长字符串,不预先分配存储空间,长度不要超过5000,如果大于此值,则选用text,独立出来一张表,用主键来对应,避免影响其他字段索引效率。 5)字段允许适当冗余,以提高查询性能,但必须考虑数据一致性。冗余字段应遵守:1.不是频繁修改;2.不是varchar超长字段,更不能是text字段。 6)单表行数超过500万行或者单表容量超过2GB,才推荐分库分表。 7)页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。 8)若有order by的场景,请注意利用索引的有序性。order by最后的字段是组合索引的一部分,并放在索引组合顺序的最后,避免出现file_sort的情况,影响查询性能。 正例:where a=? and b=? order by c; 索引:a_b_c 9)利用覆盖索引来进行查询操作,避免回表。很形象的比喻:如果一本书需要知道第11章是什么标题,会翻开第11章对应的那一页吗?目录(索引列)浏览一下就好,这个目录就是起到覆盖索引的目的。覆盖索引的explain结果中,extra列会出现:using index。 10)利用延迟关联或子查询优化超多分页场景。说明:MySQL并不是跳过offset行,而是取offset+N行,然后放弃前offset行,返回N行,那当offset特别大的时候,效率就非常低下。 11)建组合索引的时候,区分度最高的在最左边。举极端例子:如果where a=? and b=?,a的列几乎接近于唯一值,那么只需单建idx_a索引即可。 12)不要使用count(列名)或count(常量)来替代count(*),count(*)是SQL92定义的标准统计行数的语法,跟数据库无关,跟NULL和非NULL无关。count(列名)会忽略此列为NULL值的行。 13)不得使用外键与级联,一切外键概念必须在应用层解决。外键与级联更新适用于单机低并发,不适合分布式、高并发集群:级联更新时强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。 14)数据订正时,删除和修改记录时,要先select,避免出现误删除,确认无误后才能执行更新语句。 15)在表查询中,一律不要使用*作为查询的字段列表,需要哪些字段必须明确写明。 16)@Transactional事务不要滥用。事务会影响数据库的QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。

    数据库操作的一些基本常识,数据库性能变坏,多数情况是由于上层应用的不合理使用导致的。

    高并发服务器建议调小TCP协议的time_wait超时时间。 说明:操作系统默认240秒后,才会关闭处于time_wait状态的连接,在高并发访问下,服务器端会因为处于time_wait的连接数太多,可能无法建立新的连接,故需要在服务器上调小此阈值。对于Linux服务器,变更/etc/sysctl.conf中的net.ipv4.tcp_fin_timeout。

    个人补充

    这里补充一部分手册之外的规范,一些是笔者在实际工作中遇到过,实践过的经验,一些是组内大牛分享实践的,若有不合理的地方还请大家指正。

    1)客户端socket超时配置应区分连接超时和读超时。用connect timeout控制连接建立的超时时间,用read timeout控制流读取数据的超时时间。代码示例:

    socket.connect(new InetSocketAddress(host, port), 2000);  //设置连接超时为2s。 socket.setSoTimeout(10*1000);  //设置读超时为10s。

    2)对于QPS非常高的RPC接口,应该将RPC客户端socket的读超时尽量设短,以便当该接口不可用时,能快速超时返回,使客户端能及时处理,避免上层应用因此环节等待时间过长而将上层服务打垮。 例如,socket.setSoTimeout(1000),将读超时设置为1s。

    3)数据库查询时,除了order by需要利用索引的有序性,对于group by操作,在数据量大时,有无利用索引的性能差异特别大。

    4)数据库批量操作时,要分批进行,避免一次操作涉及记录数过多,导致事务超时。 例如:根据ids批量更新数据,先用Lists.partition分批拆分成多个子list,然后每个list走一次更新,使单个事务尽快结束,分批大小一般设置1000。

    5)字符串分割时,用Apache Commons中的StringUtils.splitPreserveAllTokens(...)代替JDK中的str.split(..),避免JDK对末尾空串的过滤导致结果与预期不一致。

    写在最后,笔者想用阿里巴巴Java开发手册的作者孤尽大神的采访名言来结束此文:

    别人都说我们是搬砖的码农,但我们知道自己是追求个性的艺术家。也许我们不会过多在意自己的外表和穿着,但在我们不羁的外表下,骨子里追求着代码的美、系统的美,代码规范其实就是一个对程序美的定义。
     

    本文原作者:佚名
    文章来源:开源中国
  • 上一篇:谈谈阿里巴巴与Google的Java开发规范(1)

    下一篇:编写高质量代码的思考

网站导航
Copyright(C)51Code软件开发网 2003-2018 , 沪ICP备16012939号-1