知学云 Java 编码规范¶
本规范根据产品中心实践过程中经验累积,结合美团、阿里等大厂公开规范整理出一套Java编码规范,为知学云Java开发人员提供规范与指引。
修订记录¶
提交者 | 更新日期 | 备注 |
---|---|---|
产品中心 | 2020.1.8 | 修改 |
产品中心 | 2020.2.8 | 定稿 |
架构部 | 2020.5.12 | 优化正反例格式、添加错误码规范 |
通用规范¶
知学云规范¶
-
【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
正例:
反例:private static final String REG = "\\d+"; boolean isMatch = Pattern.matches(REG, content);
// 不允许直接将常量直接使用 boolean isMatch = Pattern.matches("\\d+", content);
-
【强制】关于基本数据类型与包装数据类型的使用标准如下
- 所有的
Entity
类属性必须使用包装数据类型 RPC
方法的返回值和参数必须使用包装数据类型
正例:
// 考试分数,默认为 null,能够很好的区分是否参与考试获得分数 Integer score;
反例:
// 考试分数,默认值 0 ,不能区分是参与考试获取的结果还是未参与考试 int score;
- 所有的
-
【强制】
Java
类中配置文件通过注入Environment
或者实现EnvironmentAware
方式获取正例:
private String value; @Autowired public void setEnvironment(Environment env) { value = env.getProperty("some.things", String.class, "some things"); }
反例:
/** * 不允许用这种注解方式获取 */ @Value("#{xxxxx}") private String value;
-
【强制】所有的相同类型的包装类对象之间值的比较,全部使用
equals
方法比较 -
【强制】
Object
的equals
方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals
正例:
"some string".equals(other);
反例:
other.equals("some string");
-
【强制】所有的覆写方法,必须加
@Override
注解 -
【强制】禁止通过一个类的对象引用访问此类的静态变量或静态方法,直接用类名来访问即可
-
【强制】常量定义
static
在final
前面正例:
public static final String EXAMPLE = "example";
反例:
public final static String EXAMPLE = "example";
-
【强制】
Optional
严禁直接使用get()
方法正例:
Optional<T> opt = Optional.of(object); opt.ifPresent(o -> xxx);
反例:
Optional<T> opt = Optional.of(object); opt.get();
-
【强制】数据值是否为空不明确,则通过
Optional.ofNullable()
方法进行包装正例:
Optional<T> opt = Optional.ofNullable(object);
反例:
Optional<T> opt = Optional.of(object);
-
【强制】工具类、常量类不应有
public
构造器,应该添加private
构造函数,防止被实例化正例:
public final class Constant { private Constant() { } }
反例:
public final class Constant { // 默认无参构造函数 public Constant() { } }
-
【强制】不能使用过时的类或方法
说明:接口提供方既然明确是过时接口,那么有义务同时提供新的接口;作为调用方来说,有义务去考证过时方法的新实现是什么,并及时作出调整。
-
【强制】通过
stream
转换成map
的键值对,必须过滤空值key
正例:
examples.stream().filter(l -> null != key).collect(Collectors.toMap(key -> key, value -> value));
反例:
examples.stream().collect(Collectors.toMap(key -> key, value -> value));
-
【强制】
map
、orElse
、orElseGet
、orElseThrow
必须接收并使用返回值,否则使用ifPresent
方法即可;orElseThrow
修改为ErrorCode.xxx.throwIf(boolean)
正例:
error.throwIf(!Optional.ofNullable(object).isPresent)
反例:
Optional.orElseThrow(() -> new Exception(error));
-
【强制】方法复杂度值最大允许为15,否则必须整改以降低复杂度
方法:根据实际情况将条件分支提取方法,减少复杂度,保证方法的可读性
反例:
if (条件1) { } else if (条件2 && 条件3) { } else if (条件4 && 条件5) { } ...
-
【强制】
jooq
中使用fetchOne
多个值返回报异常;不确定数据量时,查询带上limit 1
获取数据 -
【强制】数据获取必须按需查询,避免没有必要的资源消耗
正例:
Field<?>[] fields = {field1, field2}; e.select(Fields.start().add(fields).end()) .from(TABLE) .where(conditions) .fetch(r -> r.into(fields).into(TABLE.class)));
-
【强制】请求的
url
的长度大于2048
,则需要由get
请求修改成post
请求 -
【推荐】所有的局部变量(
jdk
基本数据类型)使用基本数据类型 -
【推荐】
Java
类中Bean
注入方式,将注解标注在set方法之上正例:
private Dao dao; @Autowired public void setDao(Dao dao) { this.dao = dao; }
反例:
@Autowired private Dao dao;
-
【推荐】类内方法定义的顺序依次是:公有方法或保护方法 > 私有方法 >
getter
/setter
方法 -
【推荐】当一个类有多个构造方法,或者多个同名方法,这些方法应该按顺序放置在一起,便于阅读
-
【推荐】方法尽量单一功能,有相同含义不同参数,使用方法重载
正例:
public int sum(int a, int b) { return a + b; } public int sum(int a, int b, int c) { return sum(a, b) + c; }
-
【建议】递归的跳出条件需要前置,尽量写尾递归
说明:在没有递归到底之前,那些中间变量会一直保存着,因此每一次递归都需要开辟一个新的栈空间,较大的数很容易就栈溢出了。便可以通过每轮递归结束后刷新当前的栈空间,复用了栈,解决递归的栈溢出问题,像这样的
return
后面不附带任何变量的递归写法,也就是递归发生在函数最尾部,我们称之为'尾递归'。正例:
public int func(final int fact, final int num) { // 尾递归 return num == 1 ? fact : func(fact * num, num - 1); }
反例:
public int func(final int num) { if (num == 1) { return num; } else { return num * func(num - 1); } }
命名风格¶
-
【强制】所有的字符串常量值里不能出现大写, 多个单词用
-
号连接, 包括URL
// 不能写成 typeOne public static final String TYPE = "type-one"; // 不能写成batchDelete @RequestMapping(value = "/user/batch-delete")
-
【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例:
_name
/__name
/$name
/name_
/name$
/name__
-
【强制】代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。不允许直接使用拼音 !!! note "" 说明:正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式也要避免采用。
正例:
exam
/zhixueyun
/shenzhen
,通用的中文名称(如城市名称、公司名称等)可视同英文。
反例:KaoShiChengJi
[考试成绩] /getPingfenByName
() [评分] /int 某变量 = 3
-
【强制】类名使用
UpperCamelCase
风格。正例:
UserService
/ExamService
/ErrorCode
反例:userService
/examService
/ErrorCODE
-
【强制】方法名、参数名、成员变量、局部变量都统一使用
lowerCamelCase
风格,必须遵从驼峰形式。正例:
localValue
/getMessage()
/memberId
-
【强制】抽象类命名使用
Abstract
或Base
开头;异常类命名使用Exception
结尾;测试类命名以它要测试的类名开始,以Test
结尾;枚举类命名使用Enum
后缀;常量类命名使用Content
后缀。 -
【强制】常量、枚举成员命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
-
【强制】类型与中括号紧挨相连来定义数组。 正例:
// 数组定义 // 方式一:初始空数组,设置长度 int[] arr = new int[10]; // 方式二:初始带有数据的数组,没有数据则可以直接 arr = {} int[] arr = {1,2,3,.....};
-
【强制】
Entity
类中布尔类型的变量,都不要加is
前缀,否则部分框架解析会引起序列化错误。反例:
boolean isSuccess;
-
【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
-
【强制】杜绝完全不规范的缩写,避免望文不知义。
反例:
kscj
[考试成绩] -
【强制】
long
或者Long
初始赋值时,使用大写的L
,不能是小写的l
,小写容易跟数字1
混淆,造成误解。 -
【推荐】如果模块、接口、类、方法使用了设计模式,在命名时体现出具体模式。
说明:将设计模式体现在名字中,有利于阅读者快速理解架构设计理念。
正例:
OrderFactory
/LoginProxy
/ResourceObserver
-
【推荐】为了达到代码自解释的目标,任何自定义编程元素在命名时,使用尽量完整的单词组合来表达其意。
代码格式¶
-
【强制】大括号的使用约定。如果是大括号内为空,则简洁地写成
{}
即可,不需要换行;如果是非空代码块则:- 左大括号前不换行
- 左大括号后换行
- 右大括号前换行
- 右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行
-
【强制】 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格
- 【强制】
if
/for
/while
/switch
/do
等保留字与括号之间都必须加空格 -
【强制】任何二目、三目运算符的左右两边都需要加一个空格
说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等
-
【强制】采用 4 个空格缩进,禁止使用
tab
字符 - 【强制】注释的双斜线与注释内容之间有且仅有一个空格。
-
【强制】单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则:
- 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进,参考下文示例
- 运算符与下文一起换行
- 方法调用的点符号与下文一起换行
- 方法调用时,多个参数,需要换行时,在逗号后进行
- 在括号前不要换行
-
【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格。
// 代码格式——正例
public static void main(String[] args) {
// 缩进 4 个空格
String say = "hello";
// 运算符的左右必须有一个空格 int flag = 0;
// 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格
if (flag == 0) {
System.out.println(say);
}
// 左大括号前加空格且不换行;左大括号后换行
if (flag == 1) {
System.out.println("world");
// 右大括号前换行,右大括号后有 else,不用换行
} else {
System.out.println("ok");
// 在右大括号后直接结束,则必须换行
}
StringBuffer sb = new StringBuffer();
// 超过 120 个字符的情况下,换行缩进 4 个空格,点号和方法名称一起换行
sb.append("zi").append("xin")...
.append("huang")...
.append("huang")...
.append("huang");
}
集合处理¶
-
【强制】使用工具类
Arrays.asList()
把数组转换成集合时,不能使用其修改集合相关的方法,它的add
/remove
/clear
方法会抛出UnsupportedOperationException
异常。说明:
asList
的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList
体现的是适配器模式,只是转换接口,后台的数据仍是数组。String[] str = new String[] { "you", "me" }; List list = Arrays.asList(str); // 运行时异常 list.add("he"); // list.get(0) 也会随之修改 str[0] = "she";
-
【强制】关于
hashCode
和equals
的处理,遵循如下规则:- 只要重写
equals
,就必须重写hashCode
。 - 因为
Set
存储的是不重复的对象,依据hashCode
和equals
进行判断,所以Set
存储的对象必须重写这两个方法。 - 如果自定义对象作为
Map
的键,那么必须重写hashCode
和equals
。
说明:
String
重写了hashCode
和equals
方法,所以我们可以非常愉快地使用String
对象作为key
来使用。 - 只要重写
-
【强制】
ArrayList
的subList
结果不可强转成ArrayList
,否则会抛出ClassCastException
异常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList
说明:
subList
返回的是ArrayList
的内部类SubList
,并不是ArrayList
,而是ArrayList
的一个视图,对于SubList
子列表的所有操作最终会反映到原列表上。 -
【强制】不要在
foreach
循环里进行元素的remove
/add
操作。remove
元素请使用Iterator
方式,如果并发操作,需要对Iterator
对象加锁。正例:
Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (删除元素的条件) { iterator.remove(); } }
反例:
java List<String> list = new ArrayList<String>(); list.add("1"); list.add("2"); for (String item : list) { if ("1".equals(item)) { list.remove(item); } }
-
【强制】 在 JDK7 版本及以上,
Comparator
要满足如下三个条件,不然Arrays.sort
,Collections.sort
会报IllegalArgumentException
异常。说明:三个条件如下
1) x,y 的比较结果和 y,x 的比较结果相反
2) x>y,y>z,则 x>z
3) x=y,则 x,z 比较结果和 y,z 比较结果相同反例:下例中没有处理相等的情况,实际使用中可能会出现异常:
new Comparator<Student>() { @Override public int compare(Student o1, Student o2) { return o1.getId() > o2.getId() ? 1 : -1; } };
-
【推荐】高度注意
Map
类集合K/V
能不能存储null
值的情况,如下表格:集合类 Key Value Super 说明 Hashtable 不允许为 null 不允许为 null Dictionary 线程安全 ConcurrentHashMap 不允许为 null 不允许为 null AbstractMap 锁分段技术(JDK8:CAS) TreeMap 不允许为 null 允许为 null AbstractMap 线程不安全 HashMap 允许为 null 允许为 null AbstractMap 线程不安全 由于
HashMap
的干扰,很多人认为ConcurrentHashMap
是可以置入null
值,而事实上,存储 null 值时会抛出NPE
异常。
Service API 规范¶
-
【强制】写
service
接口的时候, 必须能保证一个操作在一个方法里做完 -
【强制】所有接口方法都要打上
@Transactional
标记, 所有的查询方法都要加上readonly=true
方法名¶
-
【推荐】插入方法前缀
insert
, 更新方法前缀update
, 删除方法前缀delete
, 根据id查找前缀get
, 其它查找前缀find
-
【推荐】如果方法名后缀与接口名前缀相同, 则省略
-
【推荐】如果同类型的方法有多个, 则根据参数添加后缀区分
正例:
public class RoleService { Role insert(); Role get(); // 这里的getByXxx根据参数添加名称后缀 Role getByName(String name); Role getByType(Integer type); }
反例:
public class RoleService { // 这里的insertRole应该省略Role Role insertRole(); // 同理getRole改为get Role getRole(); }
方法参数¶
-
【强制】非必传参数必须用
Optional
包起来, 反言之, 没有用Optional
包起来的参数都是必传参数正例:
public String demo(String id, Optional<String> name);
-
【强制】参数没有超过10个以上, 不能传实体
正例:
public User insert(String name, Optional<String> fullName)
反例:
java public User insert(User user)
方法返回值¶
- 【强制】接口方法不允许返回值为
void
- 【强制】
List
类型的返回值不允许返回null
, 如果没有数据返回空List
MQ 规范¶
下文的消息是指用户执行业务操作时系统生产的MQ消息
所有的消息都使用com.zxy.common.base.message.Message
类发送与接收
消息分类(type
属性)¶
所有的类型都是5位数, 从左到右定义如下:
-
第1位表示模块类别, 如: 人资模块为
1
, 考试模块为2
-
第2-3位表示业务类别, 如: 人员操作为
01
, 部门操作为02
-
第4-5位表示操作类别, 如: 添加人员为
01
, 修改人员为02
完整示例: 10101 为添加人员消息
消息头(headers
属性)¶
headers
属性是键值对, 键和值都是String
类型,一般用来存放消息相关的一些ID
值
消息体(payload
属性)¶
payload
属性可以用来传一些实体对象
提示
一般来说,这个属性是不允许设值,如果有需求,请在开发群里发起讨论,根据讨论结果确定(详见异步开发规范-4)。
消息定义方式¶
每个工程的api
模块中都应该有以下消息定义常量类:
- MessageTypeContent:消息类型
- MessageHeaderContent:消息头
异步开发规范¶
-
【强制】异步中根据
id
查询数据必须使用getOptional
, 否则容易出现异常如果异步消费时数据已经在其他场景删除了,则会抛出异常。
正例:
dao.getOptional(id).ifPresent(obj -> xxxx);
反例:
dao.get(id);
-
【强制】字段按需修改
正例:
dao.execute(e -> e.update(TABLE) .set(TABLE.NAME, "example") .where(TABLE.ID.eq("id")) .execute());
jooq
DSLContext中update
语句必须要调用execute
才会执行。反例:
dao.update(xxx);
-
【强制】未知数据大小使用分页处理数据,否则容易造成内存溢出
正例:
// 查询的时候分页,再处理 List<Object> list = dao.limit(page * pageSize, pageSize).fetch(xxx);
反例:
// 不允许一次性查询list数据内存中处理 List<Object> list = dao.fetch(xxx);
-
【强制】异步消息中参数传递,不使用
Payload
传数据,保证关键数据可查可追溯,否则MQ故障之后,数据无法找回真实案例:考试提交异步,将用户考试答题记录通过
Payload
传递,而MQ发送前未做任何存储处理,一旦MQ发生故障,数据丢失,是非常危险的。 -
【强制】参数大数据量时,在异步中查询数据,进行分批处理
- 【推荐】异步中数据处理,为提升性能、提高吞吐量以及避免调用
rpc
带来的消耗,可以直接使用Dao
操作数据
错误码规范¶
- 【强制】需要自定义错误码的工程
api
模块中都应该有ErrorCode
常量类,其实现com.zxy.common.base.exception.Code
接口 - 【强制】系统自定义异常使用
com.zxy.common.base.exception.UnprocessableException
抛出 - 【推荐】系统自定义异常抛出的几种写法
- 直接
throw UnprocessableException
throw new UnprocessableException(ErrorCode.CODE);
- 使用
Code.throwIf
,当条件为true
时抛出异常,适用于逻辑条件单一场景ErrorCode.CODE.throwIf(null == a);
- 使用
Code.throwUnless
,当条件为false
时抛出异常,适用于逻辑条件单一场景ErrorCode.CODE.throwUnless(a.isPresent());
- 使用
Code
对Supplier
接口的get()
实现,适用于Optional.orElseThrow()
Optional.ofNullable(a).orElseThrow(ErrorCode.CODE);
- 使用
Code
接口的wrap
方法,尝试执行wrap
函数并捕获指定异常并自动转换为UnprocessableException
抛出// 示例1:不指定要捕获的异常 ErrorCode.CODE.wrap(() -> { // this is some try codes... }); // 示例2:指定要捕获的异常类 ErrorCode.CODE.wrap(() -> { // this is some try codes... }, IOException.class);
- 直接
-
【附录】系统级错误码一览表
模块 错误码 说明 common-restful-support 900000 参数校验失败 common-restful-support 900001 参数必填 common-restful-support 900002 格式不匹配 common-restful-support 900003 文件大小超出 common-restful-support 900004 不允许的文件类型 common-rpc 900005 数据不存在 common-restful-support 900008 超出时间范围内允许访问次数 common-restful-support 900009 超出当日允许访问次数 common-rpc 910000 数据错误 common-office 910001 数据类型错误 common-office 910002 长度不匹配 common-office 910003 必填 common-office 910004 邮箱不合法 common-office 910005 手机号码不合法 common-office 910006 身份证不合法 common-office 910007 编码不合法 common-office 910008 序号不合法 common-office 910009 超过最大限制行数 业务错误码见业务系统错误码