文章目录
前言一、命名风格二、变量定义三、代码格式四、OOP 规则五、 集合处理六、控制语句七、注释规约八、异常处理九、编码检查十、编码安全十一、其他约定十二、SqlServer 数据库建表规约索引规约SQL 语句
前言
本文的java代码开发的通用规范,适用于新手上路和大部分职场
一、命名风格
采用英文全称进行命名,保持各层级命名基本统一。命名英文单词用全称,尽量不要使用简称。模块包命名全部使用小写,命名方式:com.项目名称.项目模块.系统分层(如:com.cdcw.modules.controller)。类名使用 UpperCamelCase 风格,遵从大驼峰形式,多个英文单词以大写字母间隔,尽量避免使用缩写,不要使用’_’’-’等特殊符号,以下情形例外:DO / BO / DTO / VO / AO 正例:MarcoPolo / UserDO / XmlService / TcpUdpDeal / TaPromotion 反例:macroPolo / UserDo / XMLService / TCPUDPDeal / TAPromotion属性定义位置在类定义开始,按照 public,protected,package,private 顺序定义。方法命名采用“动作+属性” 的方法。并且,动作以小写字母开始,属性以大写字母开始。常用的动作有:is、get、set、save、add、del 等。示例:getName()。方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式。 正例: localValue / getHttpMessage() / inputUserId常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚。 正例:MAX_STOCK_COUNT 反例:MAX_COUNT抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾。Model 类中布尔类型的变量,都不要加 is,否则部分框架解析会引起序列化错误。 反例:定义为基本数据类型 Boolean isDeleted;的属性,它的方法也是 isDeleted(),RPC 框架在反向解析的时候,“以为”对应的属性名称是 deleted,导致属性获取不到,进而抛出异常。不要使用 SqlServer 数据库保留字段命名属性。对于 Service 和 DAO 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀与接口区别。 正例:CacheManagerImpl 实现 CacheManager 接口。为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。 正例:从远程仓库拉取代码的类命名为 PullCodeFromRemoteRepository 反例:变量 int a;的随意命名方式。接口类中的方法和属性不要加任何修饰符号(public 也不要加),保持代码的简洁性,并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是 与接口方法相关,并且是整个应用的基础常量。 正例:接口方法签名:void f(); 接口基础常量表示:String COMPANY =“alibaba”; 反例:接口方法定义:public abstract void f(); 说明:JDK8 中接口允许有默认实现,那么这个 default 方法,是对所有实现类都有价值的默 认实现。枚举类名建议带上 Enum 后缀,枚举成员名称需要全大写,单词用下划线隔开。 说明:枚举其实就是特殊的常量类,且构造方法被默认强制是私有。 正例:枚举名字为 ProcessStatusEnum 的成员名称:SUCCESS / UNKOWN_REASON。各层命名规约: A) Service/DAO 层方法命名规约 1) 获取单个对象的方法用 get 做前缀。 2) 获取多个对象的方法用 list 做前缀。 3) 获取统计值的方法用 count 做前缀。 4) 插入的方法用 save/insert 做前缀。 5) 删除的方法用 remove/delete 做前缀。 6) 修改的方法用 update 做前缀。
二、变量定义
不要使用一个常量类维护所有常量,按常量功能进行归类,分开维护。 说明:大而全的常量类,非得使用查找功能才能定位到修改的常量,不利于理解和维护。 正例:缓存相关常量放在类 CacheConsts 下;系统配置相关常量放在类ConfigConsts 下。
三、代码格式
废弃的/无用的代码一律直接删除,禁止以注释等方式保留。如需查看历史代码,通过 SVN/Git 的 history 找回(无用的代码会干扰团队成员的阅读/或被误调,越积越多会导致代码维护成本增高)。 补充:master 按照规范,开发分支允许以注释方式保留。接口类中的方法不需添加 public 修饰符。需要序列化的 Bean 类统一实现 Serializable 接口并用 IDE 生成 serialVersionUID。
Eg:
public class MyEntity implements Serializable
{
private
static final
long serialVersionUID
= 123456L;
...
}
常用字符串统一定义在常量类里,如: “utf-8”, “yyyyMMdd”。避免数字类型比较的坑: 统一采用 equals 进行比较其值,不用==进行比较,避免踩坑。方法单一职责: 单个方法代码行数控制在 100 行以内,超长的需要拆分(拆分成多个方法或类)。遵循: Don’t Repeat Yourself,即 DRY 原则。避免进行简单的复制粘贴修改,当出现重复代码时思考是否封装。可异步执行的耗时操作采用异步处理:使用 Spring @Async 或 MQ,或夜间 Timer 定时。常用数据考虑缓存,存入 Redis,设置缓存过期时间。保证写一致性的逻辑,在外层方法上添加事务。大括号的使用约定。如果是大括号内为空,则简洁地写成{}即可,不需要换行;如果是非空代码块则: 1) 左大括号前不换行。 2) 左大括号后换行。 3) 右大括号前换行。 4) 右大括号后还有 else 等代码则不换行 表示终止的右大括号后必须换行。左小括号和字符之间不能出现空格;同样,右小括号和字符之间也不能出现空格。 反例:if (空格 a == b 空格)if/for/while/switch/do 等保留字与括号之间都必须加空格。任何二目、三目运算符的左右两边都需要加一个空格。 说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等。注释的双斜线与注释内容之间有且仅有一个空格。 正例:// 注释内容,注意在//和注释内容之间有一个空格。方法参数在定义和传入时,多个参数逗号后边必须加空格。 正例:下例中实参的"a",后边必须要有一个空格。method(“a”, “b”, “c”)。IDE 的 text file encoding 设置为 UTF-8; IDE 中文件的换行符 使用 Unix 格式,不要使用 Windows 格式。方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。 说明:没有必要插入多个空行进行隔开。Bean 属性拷贝推荐用 Spring BeanCopier 或者 Mapstruct,避免 Apache BeanUtils 或调用 setter。
四、OOP 规则
所有的覆写方法,必须加@Override 注解。不能使用过时的类或方法。Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。 正例:“test”.equals(object); 反例:object.equals(“test”);所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。 说明:对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑, 推荐使用 equals 方法进行判断。RPC 方法的返回值和参数必须使用包装数据类型。构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init方法中。当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读。循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。 说明:反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过 toString 方法返回 String 对象,造成内存资源浪费。 反例:
String str
= "start";
for (int i
= 0; i
< 100; i
++) {
str
= str
+ "hello";
}
慎用 Object 的 clone 方法来拷贝对象。 说明:对象的 clone 方法默认是浅拷贝,若想实现深拷贝需要重写 clone 方法实现属性对象的拷贝。
五、 集合处理
使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一样的数组,大小就是 list.size()。 说明:使用 toArray 带参方法,入参分配的数组空间不够大时,toArray 方法内部将重新分配内存空间,并返回新数组地址;如果数组元素大于实际所需,下标为[ list.size() ]的数组元素将被置为 null,其它数组元素保持原值,因此最好将方法入参数组大小定义与集合元素个数一致。 正例:
List
<String
> list
= new ArrayList
<String
>(2);
list
.add("guan");
list
.add("bao");
String
[] array
= new String
[list
.size()];
array
= list
.toArray(array
);
反例: 直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现 ClassCastException 错误。 2. 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请 使用 Iterato 方式,如果并发操作,需要对 Iterator 对象加锁。 正例:
Iterator
<String
> iterator
= list
.iterator();
while (iterator
.hasNext()) {
String item
= iterator
.next();
if (删除元素的条件
) {
iterator
.remove();
}
}
反例:
List
<String
> list
= new ArrayList
<String
>();
list
.add("1");
list
.add("2");
for (String item
: list
) {
if ("1".equals(item
)) {
list
.remove(item
);
}
}
使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
六、控制语句
if/else/for/while 语句后必须使用大括号,即使只有一行代码。(需求总是变化的,一行是暂时的)。嵌套层次过多的代码块利用反向思维缩减层次。禁止在循环中执行耗时的操作,如在循环中执行 SQL 语句/调用外部服务等。在一个 switch 块内,每个 case 要么通过 break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止。除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。 说明:很多 if 语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能明确什么样的条件执行什么样的语句,那么,如果阅读者分析逻辑表达式错误呢? 正例:
final boolean existed
= (file
.open(fileName
, "w") != null
) && (...) ||
(...);
if (existed
) {
...
}
反例:
if ((file
.open(fileName
, "w") != null
) && (...) || (...)) {
...
}
循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)。下列情形,需要进行参数校验: 1) 调用频次低的方法。 2) 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,那得不偿失。 3) 需要极高稳定性和可用性的方法。 4) 对外提供的开放接口,不管是 RPC/API/HTTP 接口。
七、注释规约
Java 文件统一添加固定 Header,通过 IDE 统一配置(code templates),主要标注该类的作用,对类进行简单描述;记录类的作者和类的创建日期。
Eg:
接口和方法统一添加 Java Doc 标准注释,描述该方法的主要意义,对方法的参数进行说明。
Eg:
<T
> boolean
addList(String key
, List
<T
> valueList
);
属性注释必须说明该属性意义。
Eg:
private String name
;
Dao 层、Service 层接口中的方法都必须按照方法的注释要求进行注释。
Eg:
List
<UserImpl
> getList(Page page
, Long orgId
);
List
<UserDetail
> getList(Page page
, String content
);
需暂留的弃用类/方法添加 @Deprecated 废弃标记 和 @see 链接指向新接口
Eg:
@Deprecated
public class JedisUtils
{…
}
类、类属性、类方法的注释必须使用 Javadoc 规范,使用/**内容 */格式,不得使用// xxx 方式。所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。说明:对子类的实现要求,或者调用注意事项,请一并说明。方法内部单行注释,在被注释语句上方另起一行,使用// 注释。方法内部多行注释 使用/* */注释,注意与代码对齐。所有的枚举类型字段必须要有注释,说明每个数据项的用途。代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。 说明:代码与注释更新不同步,就像路网与导航软件更新不同步一样,如果导航软件严重滞后,就失去了导航的意义。谨慎注释掉代码。在上方详细说明,而不是简单地注释掉。如果无用,则删除。对于注释的要求:第一、能够准确反应设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看的,使其能够快速接替自己的工作。好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释是相当大的负担。特殊注释标记,请注明标记人与标记时间。注意及时处理这些标记,通过标记扫描, 经常清理此类标记。线上故障有时候就是来源于这些标记处的代码。 1) 待办事宜(TODO):( 标记人,标记时间,[预计处理时间]) 表示需要实现,但目前还未实现的功能。这实际上是一个 Javadoc 的标签,目前的 Javadoc 还没有实现,但已经被广泛使用。只能应用于类,接口和方法(因为它是一个Javadoc 标签)。 2) 错误,不能工作(FIXME):(标记人,标记时间,[预计处理时间])在注释中用 FIXME 标记某代码是错误的,而且不能工作,需要及时纠正的情况。
八、异常处理
下层有常异,必须抛向上一层,捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容,controller 禁止向外部抛出系统底层异常及错误码。为了使系统能够更好的跟踪运行情况,必须把底层异常放入新异常中。如果一个层要抛出多个异常,那么所有自定义异常必须统一继承一个父类异常。这样上层可以通过父类异常捕获。异常统一在控制器层处理,控制器层以下的层次在处理异常时,只需要把低层的异常类放到本层约定的异常类中,并抛出,如有需要可以加适当的异常消息。调用外部服务等可能异常的代码块,用 try/catch 代码块捕获并在catch 中记录异常跟踪日志及业务逻辑处理。禁止吞掉异常信息 1)禁止 catch 里不做任何记录和处理,吞掉异常及其堆栈信息; 2)禁止: logger.error(“XXX 操作异常”) 或 logger.error(“XXX 操作异 常”+e) 或 e.printStackTrace(); 3)正确: logger.error(“XXX 操作异常”, e)对于非预期的条件,尽量增加 else 记录跟踪日志。禁止通过 System.*.out()打印日志(单元测试例外)。日志记录 logger 需使用 Slf4J/log4j 代理声明,禁止绑死具体日志系统的 API,避免后期更换日志组件导致代码的大量改动。如采用了 lombok,可用 @Slf4j 注解替代以上声明。
Eg:
private
static final Logger log
=
LoggerFactory
.getLogger(OrganizationServiceImpl
.class
);
对 trace/debug/info 级别的日志输出,必须使用占位符形式,避免直接 String 拼接异常信息(即使日志级别不匹配也会执行拼接操作空耗资源)。
Eg:
log
.debug("当前用户 id: {} ,操作对象: {}=>{} ", userId
, objectType
,
objectId
);
或条件输出形式如:
if(log
.isDebugEnabled()){
log
.debug("当前用户 id
: “
+id
+” ,操作对象
: “
+ objectType
+”
=>
“
+ objectId
);
}
避免 NPE(NullPointException) 1)equals 比较将非空对象前置如"true".equals(request.getParameter(“isXx”)),即使后者为空也不会导致NPE。 2)数据库字段可空的映射属性使用包装类型定义:如基本数据类型的 int 映射到数据库的 null 值将产生 NPE,而用吧包装类型Integer 则不会。 3)可能为空的变量进行必要判空,并在非预期条件下打印必要的跟踪日志,不但避免 NPE,还非常便于跟踪调试。 4)级联调用 obj.getA().getB().getC() 易产生 NPE,先进行判空或使用 JDK8的 Optional 类包装。 5)调用 Dubbo 接口拿到返回值时,进行判空。 6)封装统一的判空类用于常用类型的判空,代码需要判空时统一调用即可。 如 XX.isEmpty(), XX.isNotEmpty()。 12.如果一个方法业务逻辑比较复杂,在一个方法中进行处理,该方法将会臃肿、不宜阅读和维护,建议每个方法代码行不要大于 50 行,建议多使用方法重构。建议对方法中需要进行说明的地方进行注释,如参数、方法中多个步骤的业务逻辑处理。(备注,代码行数指的是代码语句行数,不是编辑器行数)。
九、编码检查
后端服务及其他需要自测的代码,编写对应的单元测试类,统一采用 Junit,禁止直接在原 Java 类中写 main()方法自测。
Eg:
Junit 单元测试类示例
:
public class TestApollo
{
@Test
public
void testApolloConfig(){
String appId
= Foundation
.app().getAppId();
Assert
.assertNotNull(appId
);
}
}
十、编码安全
用户敏感数据禁止列表批量展示/导出,必须进行脱敏,如客户手机号,身份证号。禁止直接写死登录账号密码,给系统造成巨大的安全风险;禁止代码/配置文件中出现生产环境的明文密码(改为加密存放)。禁止使用侵犯他人版权的代码,字体,图片。对用户输入数据必须做有效性检查。
十一、其他约定
在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度。 说明:不要在方法体内定义: Pattern pattern = Pattern.compile(规则);注意 Math.random() 这个方法返回是 double 类型,注意取值的范围 0≤x<1(能够 取到零值,注意除零异常),如果想获取整数类型的随机数,不要将 x 放大 10 的若干倍然后 取整,直接使用 Random 对象的 nextInt 或 者 nextLong 方法。获取当前毫秒数 System.currentTimeMillis(); 而不是 new Date().getTime();任何数据结构的构造或初始化,都应指定大小,避免数据结构无限增长吃光内存。
十二、SqlServer 数据库
建表规约
表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只 出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。 说明:SqlServer 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。 正例:feitian_admin,rdc_config,level3_name 反例:Feitian_Admin,rdcConfig,level_3_name禁用保留字,如 desc、range、match、delayed 等,请参考 SqlServer官方保留字。主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 dx_字段名。 说明:pk_ 即 primary key;uk_ 即 unique key;idx_ 即 index 的简称。小数类型为 decimal,禁止使用 float 和 double。如果存储的字符串长度几乎相等,使用 char 定长字符串类型。varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text。表必备三个字段:id,create_time, update_time, delete_flag对于 Boolean 型的字段,采用 decimal 类型表和字段都需要添加注释信息。单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。 说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。
索引规约
业务上具有唯一特性的字段,即使是多个字段的组合,也必须建成唯一索引。 说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。超过三个表禁止 join。需要 join 的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。 说明:即使双表 join 也要注意表索引、SQL 性能。在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。 说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*)的区分度来确定。创建索引时避免有如下极端误解: 1)宁滥勿缺。认为一个查询就需要建一个索引。 2)宁缺勿滥。认为索引会消耗空间、严重拖慢更新和新增速度。 3)抵制惟一索引。认为业务的唯一性一律需要在应用层通过“先查后插”方式解决。
SQL 语句
不要使用 count(列名)或 count(常量)来替代 count(),count()是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。 说明:count(*)会统计值为 NULL 的行,而 count(列名)不会统计此列为NULL 值的行。count(distinct col)计算该列除 NULL 之外的不重复行数,注意 count(di col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为 NULL。不得使用外键与级联,一切外键概念必须在应用层解决。禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。如果有全球化需要,所有的字符存储与表示,均以 utf-8 编码,注意字符的区别。 说明: SELECT LENGTH(" 轻 松 工 作 ") ; 返 回 为 12 SELECT CHARACTER_LENGTH(“轻松工作”);返回为 4 如果需要存储表情,那么选择 utfmb4 来进行存储,注意它与-8 编码的区别。更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。@Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。