一、研发流程规范
二、SQL编码规范
数据库命名规范:数据库名一律小写,必须以字母开头。库名包含多个单词的,以下划线“_”分隔。如果采用分库方案,分库编号从“0”开始,用“0”左补齐为四位。
表名规范:表名一律小写,必须以字母开头。表名中包含多个单词的,以下划线“_”分隔。如果采用分表方案,同时分表编号从“0”开始,用“0”左补齐为四位。建议使用‘数据库名_表名’形式,例如:tkn_users。
字段名和字段类型规范:字段名一律小写,必须以字母开头,言简意赅且不含拼写错误的单词(限用有歧义的缩写形式)。字段名中包含多个单词的,以“_”分隔。字段类型越小越好,并留有一定余地。字段类型尽量设置为not null(特别是primary key和unique key引用的字段,更要注意这一点);数字类型尽量设置为unsigned(防止溢出之后数值变负);不要保存default数值,以免表结构中存在业务逻辑。primary key引用的字段,主表必须为数字型非负非空自增id,分表必须为数字型非负非空自增id或数字型非负非空id。注意约定俗成的字段,如user_id、gmt_create和gmt_modified。意义相同的情况下,要使用约定俗成的字段。其中,gmt_create和gmt_modified字段需要定义为datetime not null,user_id需要定义为bigint unsigned。在表中使用扩展字段如features时,尽量使用key-value形式存储,每一个key_value使用‘;’隔开,key全部小写,必须以字母开头,多个单词使用'_'分割,不适用含有歧义的缩写形式。
索引规范:不要使用含有null的列:只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。尽量使用段索引:对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的列,如果在前10个或20个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。最好使用数字做索引。扩展索引:索引修改尽量使用扩展索引,不要新建索引。比如表中已经有a的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。索引列排序:MySQL查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作;尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引。like语句操作:一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。like “%aaa%” 不会使用索引而like “aaa%”可以使用索引。不要在列上进行运算:select * from users where YEAR(adddate)<2007,将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成:select * from users where adddate<’2007-01-01′。索引顺序:mysql会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d的顺序可以任意调整。
此外,分表查询:分表查询必须添加分表键作为参数,以防止tddl对表进行join查询,如无法使用分表键需要考虑分表键设置是否合理。单表数据量:单表数据量不超过500W,超过1000W的表需考虑使用分表,如无法使用分表需要控制表记录数,历史数据考虑迁移。
三、异常处理规范
在项目IDCM中,异常ServiceException继承自RuntimeException。
package com.alibaba.tboss.exception; public class ServiceException extends RuntimeException { private static final long serialVersionUID = -91784960796452539L; protected String errCode; public ServiceException(String errorMsg){ super(errorMsg); } public ServiceException(String errorMsg, Exception e){ super(errorMsg, e); } public ServiceException(ErrorCode msgObj){ super(msgObj.getFullMsg()); this.errCode = msgObj.getCode(); } public ServiceException(ErrorCode msgObj, String... arg){ super(msgObj.getFullMsg(arg)); this.errCode = msgObj.getCode(); } public ServiceException(ErrorCode msgObj, Throwable cause){ super(msgObj.getFullMsg(), cause); this.errCode = msgObj.getCode(); } public String getErrCode() { return this.errCode; } public void setErrCode(String errCode) { this.errCode = errCode; } }
异常的总体原则:忽略不能处理的异常,留给框架统一处理。仔细定义业务异常,谨慎处理,不要吃掉任何异常。
数据访问层:DAO中的方法可以不声明抛出异常(推荐)或声明抛出DAOException(这个异常是SqlMapClientDaoSupportEx类封装结果,没有什么作用)。数据访问层一般不要定义业务异常。
//正确的声明方式: CustomerSettings getCustomerSettingsById(long customerId); // 推荐 CustomerSettings getCustomerSettingsById(long customerId) throws DAOException; //不推荐的声明方式:这样会强制要求调用它的类捕获这个异常处理 CustomerSettings getCustomerSettingsById(long customerId) throws Exception;
业务层:BO中的方法可以不声明抛出异常(推荐)或声明抛出BOException或其他的checked exception(业务异常)。对于业务异常,需要强制外部调用处理的,需合理规范和定义业务异常类。
//正确的声明方式: public Long updateTrade(String tradeNo,String alitradeno,String totalFee); public Long updateTrade(String tradeNo,String alitradeno,String totalFee) throws BOException; //对于一些特殊的业务异常: public Long updateTrade(String tradeNo,String alitradeno,String totalFee) throws TradeException; public class TradeException extends Exception {...}
展现层:Controller中的方法可以声明抛出Exception,并且在处理过程中不catch任何Exception。框架会捕获并做后续操作,框架在捕到异常后,会打到root logger中,并且重定向到通用的错误页面。如果你需要对特定的异常做特殊处理(如跳转到其他的错误页面)的话,才需要考虑抓住异常,这时请正确地记录这个异常,以方便后期跟踪问题。
其他规范:如果不是一定要处理,尽可能忽略RuntimeException(包括DAOException和BOException等),留给框架处理。如果需要抓异常,尽量抓某一个特定的异常(如TradeException),不要将所有Exception全部抓住。抓住异常后,要么记录异常详细信息到日志文件,要么带上原有异常重新抛出,不要两样都做,避免重复记录。不要两样都不做,会吃掉异常。不要使用e.printStackTrace(),因为有性能问题,而且不能指定记录日志的文件。
// 不推荐这种方式:抓住异常log一下再抛出来,这是多余的,框架会为我们处理。 // 当然,如果需要抛出的是checked exception则另当别论 try { .... } catch (Exception e) { log.error("abcd", e); throw new BOException("abcd", e); } // 吃掉了异常,外面不知道怎么回事 try { .... } catch (Exception e) { } // 没有正确记录异常,日志文件中记录的信息太少不利于错误跟踪 try { .... } catch (Exception e) { log.error("abcd" + e.getMessage); } try { .... } catch (Exception e) { log.error("abcd" + e); } try { .... } catch (Exception e) { log.error(e); } // 正确的使用 try { .... } catch (Exception e) { log.error("abcd", e); }
在我们的项目IDCM中,是如何处理异常信息的呢?
RPC层处理异常信息:
@ResourceMapping("rackList") public DataResult<Rack> queryRackList(@RequestParams PagePara pagePara, @RequestParams Rack rack, @RequestParam(name = "operateType") String operateType) { DataResult<Rack> dataResult = new DataResult<Rack>(); try { // TODO 业务逻辑处理 } catch (Exception e) { logger.error("queryRackList err : ", e); if (e instanceof ServiceException) throw (ServiceException) e; else throw new ServiceException("查询数据失败"); } return dataResult; }
BoImpl层的异常信息:
@Override public DataResult<WorkOrderMain> queryOrderPagination(WorkOrderMain orderMain, PagePara pagePara) { DataResult<WorkOrderMain> dr = new DataResult<WorkOrderMain>(); try { // TODO 调用dao层的业务逻辑 } catch (Exception e) { logger.error(" WorkOrderLogisticsBoImpl_queryOrderPagination_error [orderMain={}]:", JSON.toJSON(orderMain).toString(), e); throw new ServiceException(ErrorCode.Query_Error, e); } return dr; }
四、日志规范
日志的作用: 记录重要数据、操作以备日后核对;记录案发现场,方便日后定位问题。日志的分类: 按日志的用途可以分为系统异常日志,应用相关日志,用户操作日志等。不同类型的日志分开记录,系统异常日志一般记录在root logger下;如velocity相关的日志也可以分记到不同的日志文件中,方便问题查找。记录日志INFO或DEGUB级别的应该先做log.isXXXenabled()判断。(如果是应用日志就打算记录成INFO级别则不用这一条)业务代码中,尽可能少用DEBUG级别的日志,容易混淆业务逻辑。在通用的底层代码中,可以适当用一些INFO和DEBUG级别的日志。在调试代码时可以利用这些日志信息,避免过于深入的跟踪。
异常日志需要打印异常的详细信息,如stackTrace等,方便问题查找。现在的框架会统一处理异常。自己捕获异常处理后如果不继续往外抛出,不要忘记记录该异常。
import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; Log logger = LogFactory.getLog(getClass());//名字统一用logger Log log = LogFactory.getLog(getClass()); //不推荐
构造log格式:必须有的字段:时间(%d{yy-MM-dd HH:mm:ss})、日志级别( %-5p)、类名(%c)、行号(%L) log内容(%m%n)
//正确 logger.error("xxxxx",e); //错误 logger.error("xxxxx"+e);
异常log,需要错误堆栈,异常对象,要放到第二个参数。
# 正确 $!{param} # 错误 $param
变量前面一定要有!。
#推荐
$!{param}
最好加上大括号(不强制),变量名前后加上{}。
五、Java编码规范
重载方法不应该分开:当一个类有多个构造函数,或者多个同名成员方法时,这些函数应该写在一起,不应该被其他成员分开。
命名:包名:包名全部用小写字母,通过 . 将各级连在一起。不应该使用下划线。类名:类型的命名,采用以大写字母开头的大小写字符间隔的方式(UpperCamelCase)。class命名一般使用名词或名词短语。interface的命名有时也可以使用形容词或形容词短语。annotation没有明确固定的规范。测试类的命名,应该以它所测试的类的名字为开头,并在最后加上Test结尾。例如:HashTest 、 HashIntegrationTest。(PS:经常见到写成TestXXXX的类名,以后注意了)。方法名:方法命名,采用以小写字母开头的大小写字符间隔的方式(lowerCamelCase)。方法命名一般使用动词或者动词短语。在JUnit的测试方法中,可以使用下划线,用来区分测试逻辑的名字,经常使用如下的结构:test<MethodUnderTest>_<state> 。例如:testPop_emptyStack 。测试方法也可以用其他方式进行命名。常量名:常量命名,全部使用大写字符,词与词之间用下划线隔开。(CONSTANCE_CASE),常量一般使用名词或者名词短语命名。PS:常量是一个静态成员变量,但不是所有的静态成员变量都是常量。在选择使用常量命名规则给变量命名时,需要明确这个变量是否是常量。例如,如果这个变量的状态可以发生改变,那么这个变量几乎可以肯定不是常量。只是计划不会发生改变的变量不足以成为一个常量,但是我们这里把这种情况也定义为常量,所以也要大写,下面是常量和非常量的例子:
// 常量 static final int NUMBER = 5; static final List<String> NAMES = Collections.unmodifiableList(Arrays.asList("ed", "ann")); static final ImmutableList<String> NAMES2 = ImmutableList.of("ed", "ann"); static final SomeMutableType[] EMPTY_ARRAY={}; enum SomeEnum{ENUM_CONSTANT} //不是常量 static String nonFinal = "non-final"; final String nonStatic = "non-static"; static final Set<String> mutableCollection = new HashSet<String>();//集合不是不可变的 //下面三种情况虽然内部存放对象可能会改变,但是属于计划不会发生改变的变量,我们也算做常量,所以也要大写。 static final ImmutableSet<SomeMutableType> MUTABLE_ELEMENTS=ImmutableSet.of(mutable); static final Logger LOGGER=Logger.getLogger(MyClass.getName()); static final String[] NON_EMPTY_ARRAY = { "these", "can", "change" };
非常量的成员变量名:非常量的成员变量命名(包括静态变量和非静态变量),采用lowerCamelCase命名。一般使用名词或名词短语。参数名:参数命名采用lowerCamelCase命名。#应该避免使用一个字符作为参数的命名方式#。特殊变量或参数名:DTO(Data Transfer Object):在变量、类名、参数等地方使用的时候,如果要用DTO结尾做名字,必须用大写。例如:UserDTO.class、userDTO、userDTOs、userDTOList。DO(Data Object咱们这边经常用在DB操作):在变量、类名、参数等地方使用的时候,如果要用DO结尾做名字,必须用大写。文件后缀:Service表明这个类是个服务类,里面包含了给其他类提同业务服务的方法。DAO这个类封装了数据访问方法。Impl这个类是一个实现类,而不是接口(通常是实现DAO和Service)。
六、代码格式规范
if(a!=null)return true 需要改成: if(a!=null){ return true; }
花括号一般用在if, else, for, do, 和 while等语句。甚至当它的实现为空或者只有一句话时,也需要使用。
if(){ if(){ if(){ } } }
if语句的嵌套层数保证在3层以内。太多层嵌套,最后自己都看不懂。直观感受一下,加上代码块,就会很复杂,而且还会有else。尤其是在代码不断增加需求的过程中,要重构一些代码,造成代码if嵌套混乱。
类的文件代码长度不要超过1000行。方法的长度不要超过150行,超出的考虑拆分一下。
public enum EnumExample { EXAMPLE_ONE, EXAMPLE_TWO; } public enum EnumExample { /** 例子一 */ EXAMPLE_ONE("1"), /** 例子二 */ EXAMPLE_TWO("2"); private EnumExample(String index) { } }
枚举经常需要被外部引用,如果名字不能直接表达,需要加javadoc。枚举名需要全大写,单词用“_”分割。
局部变量:局部变量不应该习惯性地放在语句块的开始处声明,而应该尽量离它第一次使用的地方最近的地方声明,以减小它们的使用范围。局部变量应该在声明的时候就进行初始化。如果不能在声明时初始化,也应该尽快完成初始化。
switch必须有default:每个switch语句中,都需要显式声明default标签。即使没有任何代码也需要显示声明。
修饰符的顺序:多个类和成员变量的修饰符,按Java Lauguage Specification中介绍的先后顺序排序。具体是:
public protected private abstract static final transient volatile synchronized native strictfp
除注释外的代码,不允许出现中文。经常看到log里面用中文,这样不允许,还有用中文直接equals比较,也同样不允许。
项目中禁止使用System.out.println()。如果有log需求,直接用log4j好了,可以随意指定输出位置。
@Override public String toString() { //xxxxxx } }
@Override:@override 都应该使用。@override annotations只要是符合语法的,都应该使用。PS:如果没写Override intellij&eclipse默认都会有警告出现。
异常捕获,不应该被忽略:一般情况下,catch住的异常不应该被忽略,而是都需要做适当的处理。例如将错误日志打印出来,或者如果认为这种异常不会发生,则应该作为断言异常重新抛出。
try { return Integer.parseInt(response); }catch (NumberFormatException ok){ // 不是数字没关系,继续往下走就行啦 }
如果这个catch住的异常确实不需要任何处理,也应该通过注释做出说明。
try { emptyStack.pop(); fail(); } catch (NoSuchMethodException expected) { }
在测试类里,有时会针对方法是否会抛出指定的异常,这样的异常是可以被忽略的。但是这个异常通常需要命名为: expected。
七、javadoc规范
对外的接口一定要有javadoc(强制),例如一些hsf服务什么的接口必须要有。
第一种: /** * 我多行我开心 */ 第二种: /** 我单行我快乐 */
@从句:所有标准的@从句,应该按照如下的顺序添加:@param、@return、@throws、@deprecated。并且这四种@从句,不应该出现在一个没有描述的Javadoc块中。
何处应该使用Javadoc:至少,Javadoc应该应用于所有的public类、public和protected的成员变量和方法。和少量例外的情况。例外情况如下:例外一:方法本身已经足够说明的情况。当方法本身很显而易见时,可以不需要javadoc。例如:getFoo。没有必要加上javadoc说明“Returns the foo”。单元测试中的方法基本都能通过方法名,显而易见地知道方法的作用。因此不需要增加javadoc。注意:有时候不应该引用此例外,来省略一些用户需要知道的信息。例如:getCannicalName 。当大部分代码阅读者不知道canonical name是什么意思时,不应该省略Javadoc,认为只能写/** Returns the canonical name. */例外二:重载方法:重载方法有时不需要再写Javadoc。
八、二方库版本控制规范
对外公开的api类二方库版本控制规范 版本格式:主版本号.次版本号.修订号,版本号递增规则如下: 主版本号:当你做了不兼容的API 修改。 次版本号:当你做了向下兼容的功能性新增(新增接口、接口兼容性修改等)。 修订号:当你做了向下兼容的问题修正或者实现的修改。 其他版本版本编译信息加到“主版本号.次版本号.修订号”的后面,作为延伸。 好处:版本号及其更新方式包含了相邻版本间的底层代码和修改内容的信息,通过版本号可以知道如何升级,例如:次版本/修订版本升级了,就可以考虑升级,因为是向后兼容的。 版本控制规范: 标准的版本号XYZ格式,其中X、Y和Z为非负的整数,X是主版本、Y是次版本号、而Z为修订号。每个元素必须以数值来递增。例如:1.9.1 -> 1.10.0 -> 1.11.0。 有版本号的二方库发布后,禁止改变该版本的内容。任何修改都必须增加新版本发行(snapshot除外)。 主版本号为零(0.yz)的软件处于开发初始阶段,一切都可能随时被改变。这样的公共API 不应该被视为稳定版。 1.0.0 的版本号用于界定公共API 的形成。这一版本之后所有的版本号更新都基于公共API 及其修改内容。 修订号Z(xyZ | x > 0)“必须”在只做了向下兼容的修正时才递增。这里的修正指的是针对不正确结果而进行的内部修改(例如:修bug)或者在内部程序有大量新功能或改进被加入时递增(我们业务需求经常干的)。在任何公共API的功能被标记为弃用时也“必须”递增(例如:废弃了某个接口)。 次版本号Y(xYz | x > 0)“必须”在有向下兼容的新功能出现时递增。(例如:新增接口/接口参数从int变为Integer类似),但每当次版本号(Y)递增时,修订号(Z)“必须”归零。 主版本号X(Xyz | X > 0)“必须”在有任何不兼容的修改被加入公共API时递增。其中“可以”包括次版本号(Y)及修订级别(Z)的改变。每当主版本号递增时,次版本号和修订号“必须”归零。 先行版本(例如SNAPSHOT版本、alpha等)可以标注在修订版(Z)之后,先加上一个连接号()再加上一连串以句点分隔的标识符号来修饰。标识符号“必须”由ASCII码的英数字和连接号[0-9A-Za-z]组成,且“禁止”留白。数字型的标识符号“禁止”在前方补零。被标上先行版本号则表示这个版本并非稳定而且可能无法达到兼容的需求。范例:1.0.0-snapshot、1.0.0-alpha、1.0.0-alpha.1、 1.0.0-0.3.7、1.0.0-x.7.z.92。 我们的实践方案 根据上面的规范,我们的对外二方库,可以遵循上面的方式开发,不能使用SNAPSHOT版本,对内的使用或者不适用SNAPSHOT都可以。 如果是对外的api(例如:market.open.share),有对应的open.client这种sdk,open.share和open.client必须联动升级,例如:open.share 主次版本修改open.client也必须跟着修改,并且版本一致。 外部使用,market举例,针对有open.client这种sdk的对外api二方库,使用api的时候,直接依赖open.client就可以了,不需要手动指定open.share,原因见上一条。 对外公布的二方库里面增加文件(README.md) 记录每个主版本和次版本号的升级日志。 对外open的二方库只能依赖其他open的二方库或者三方库,不能依赖自己的非open的二方库。 PS: 对内的二方库使用SNAPSHOT版本,要大写
九、三方库的使用
import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; List<String> keys = Lists.newArrayListWithCapacity(infos.size()); Map<K, V> resultMap = Maps.newHashMapWithExpectedSize(tmpList.size()); Set<String> sets=Sets.newHashSet();
创建集合&Map:推荐使用guava的静态方法,创建集合。
字符串操作:优先使用:StringUtils(org.apache.commons.lang3.StringUtils)常用方法:isBlank、isNotBlank。
数组操作:优先使用:ArrayUtils(org.apache.commons.lang3.ArrayUtils)。常用方法:isEmpty、isNotEmpty、toString(final Object array)。
集合操作:优先使用:CollectionUtils(org.apache.commons.collections4.CollectionUtils)常用方法:isEmpty、isNotEmpty、size。
Map操作:优先使用:MapUtils(org.apache.commons.collections4.MapUtils)常用方法:isEmpty、isNotEmpty。
除上面以外还有NumberUtils、DateFormatUtils、DateUtils等优先使用org.apache.commons.lang3这个包下的,不要使用org.apache.commons.lang包下面的。原因是commons.lang这个包是从jdk1.2开始支持的所以很多1.5/1.6的特性是不支持的,例如:泛型。
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; public class User { private String name; private User(String name) { this.name = name; } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } public static void main(String[] args) { System.out.println(new User("testname")); } } 输出:User[name=testname]
重载toString方法:优先使用:ToStringBuilder(org.apache.commons.lang3.builder.ToStringBuilder)。
相关第三方库依赖如下:
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.3.2</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.0</version> </dependency> <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>18.0</version> </dependency>
十、消灭警告规范
消灭警告:警告本来就是可能有问题的代码,还是有可能会影响正常功能。或者至少去掉会让代码更简洁,PS:警告eclipse左侧会显示屎黄色的小标。
用findbugs扫一下:好处:帮助我们发现很多问题和优化意见(貌似最多的就是空指针)。首先装个findbugs插件,装好了右键执行,完成后在Bug Explorer视图里面看结果。
十一、codereview规范
codereview:发布代码前在aone指定codereview的人,进行代码审查。但是代码审查并不是说只找一个人,这可以是团队多人一起做。进行reivew的人也不一定必须是老人,有时我们可能觉得“经验比较浅,不能对别人codereivew”,其实并不一定,俗话说:三个臭皮匠,顶个诸葛亮。即使是经验欠缺,多个人review,也能完成高质量的代码。
怎么做codereivew呢?
- codereivew的人要做那些事情,简单来说就是看代码,具体看什么下面是一些建议:
- 实现正确吗(如果能看出来最好,看不出来,那也没办法,reivew的人不是测试)
- 代码实现清晰易读吗 (优美的代码,会让人看得入迷的)
- 是不是最好的实现,有没有重构的空间
- 有没有没有考虑到的点,特别是对其他部分代码会否造成影响(例如:发布没兼容老业务,会不会对线上造成影响)
- 测试代码同样需要审查(有空的话,测试用例也过一下)
十二、