无规矩不成方圆,编码规范就如同协议,有了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 Style
,A表示规范出自于阿里巴巴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)FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool和ScheduledThreadLocal:允许的创建线程数为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,此类随意缩写严重降低了代码的可阅读性。
说来惭愧,这类不规范的缩写,笔者之前还真干过几次。有时候是觉着变量太长,导致明明逻辑很简单的一条语句,就超过了列限制,于是乎主观地缩写命名,如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开发手册
的作者孤尽大神的采访名言来结束此文:
别人都说我们是搬砖的码农,但我们知道自己是追求个性的艺术家。也许我们不会过多在意自己的外表和穿着,但在我们不羁的外表下,骨子里追求着代码的美、系统的美,代码规范其实就是一个对程序美的定义。
与原文同步更新。
网友评论