1.概述
此规范是在 《阿里巴巴Java开发手册》,《唯品会Java开发手册》基础上进行裁剪和引用。
2.规范正文
命名规约
Rule 1. 【强制】所有相关命名只能是英文字母。
Rule 2. 【强制】禁止拼音缩写,避免阅读者费劲猜测;尽量不用拼音,除非中国式业务词汇没有通用易懂的英文对应。
WARNING
反例: DZ[打折] / getPFByName() [评分]
尽量避免:Dazhe / DaZhePrice
Rule 4. 【强制】类名与接口名使用UpperCamelCase风格,遵从驼峰形式
Tcp, Xml等缩写也遵循驼峰形式。
WARNING
正例:UserId / XmlService / TcpUdpDeal / UserVO
反例:UserID / XMLService / TCPUDPDeal / UserVo
- Sonar-101:Class names should comply with a naming convention
- Sonar-114:Interface names should comply with a naming convention
Rule 5. 【强制】方法名、参数名、成员变量、局部变量使用lowerCamelCase风格,遵从驼峰形式
WARNING
正例: localValue / getHttpMessage();
- Sonar-100:Method names should comply with a naming convention
- Sonar-116:Field names should comply with a naming convention
- Sonar-117:Local variable and method parameter names should comply with a naming convention
Rule 6. 【强制】常量命名全大写,单词间用下划线隔开。力求语义表达完整清楚,不要嫌名字长
WARNING
正例: MAX_STOCK_COUNT
反例: MAX_COUNT
例外:当一个static final字段不是一个真正常量,比如不是基本类型时,不需要使用大写命名。
WARNING
private static final Logger logger = Logger.getLogger(MyClass.class);
例外:枚举常量推荐全大写,但如果历史原因未遵循也是允许的,所以我们修改了Sonar的规则。
- Sonar-115:Constant names should comply with a naming convention
- Sonar-308:Static non-final field names should comply with a naming convention
Rule 7. 【强制】 抽象类使用Base开头;异常类使用Exception结尾;测试类以它要测试的类名开始,以Test结尾
WARNING
正例:BaseView, TimeoutException,UserServiceTest
- Sonar-2166:Classes named like "Exception" should extend "Exception" or a subclass
- Sonar-3577:Test classes should comply with a naming convention
Rule 8. 【强制】类型与中括号紧挨相连来定义数组**
WARNING
正例:定义整形数组 int[] arrayDemo。
反例:在 main 参数中,使用 String args[] 来定义
Rule 9. 【强制】POJO类中布尔类型的变量名,不要加is前缀,否则部分框架解析会引起序列化错误
WARNING
反例:Boolean isSuccess的成员变量,它的GET方法也是isSuccess(),部分框架在反射解析的时候,“以为”对应的成员变量名称是success,导致出错。。
Rule 10. 【强制】包名全部小写。点分隔符之间尽量只有一个英语单词,即使有多个单词也不使用下划线或大小写分隔,包名统一使用单数形式
WARNING
正例: com.jm.javatool
反例: com.jm.java_tool, com.jm.javaTool
Rule 11. 【强制】避免成员变量,方法参数,局部变量的重名复写,引起混淆
- 类的私有成员变量名,不与父类的成员变量重名
- 方法的参数名/局部变量名,不与类的成员变量重名 (getter/setter例外)
下面错误的地方,Java在编译时很坑人的都是合法的,但给阅读者带来极大的障碍。
WARNING
public class A {
int foo;
}
public class B extends A {
int foo; //WRONG
int bar;
public void hello(int bar) { //WRONG
int foo = 0; //WRONG
}
public void setBar(int bar) { //OK
this.bar = bar;
}
}
- Sonar-2387: Child class fields should not shadow parent class fields
- Sonar: Local variables should not shadow class fields
Rule 12.【推荐】命名的好坏,在于其“模糊度”
1)如果上下文很清晰,局部变量可以使用 list 这种简略命名, 否则应该使用 userList 这种更清晰的命名。
2)禁止a1, a2, a3 这种带编号的没诚意的命名方式。
3)方法的参数名叫bookList ,方法里的局部变量名叫theBookList 也是很没诚意。
4)如果一个应用里同时存在Account、AccountInfo、AccountData 类,或者一个类里同时有getAccountInfo()、getAccountData(),save()、 store() 的函数,阅读者将非常困惑。
5)callerId 与calleeId,mydearfriendswitha 与mydearfriendswithb 这种拼写极度接近,考验阅读者眼力的。
Rule 15.【推荐】如果使用到了通用的设计模式,在类名中体现,有利于阅读者快速理解设计思想
WARNING
正例:OrderFactory, LoginProxy ,ResourceObserver
Rule 17.接口和实现类的命名有两套规则
1)【强制】对于 Service 和 DAO, 类,基于 SOA 的理念,暴露出来的服务一定是接口,内部的实现类用 Impl 的后缀
WARNING
正例:CacheServiceImpl 实现 CacheService接口
2)【推荐】如果是形容能力的接口名称,取对应的形容词为接口名。
WARNING
正例:BaseTranslator 实现 Translator
格式规约
Rule 1. 【强制】项目统一代码格式插件进行格式化 (spotless-maven-plugin)
Rule 2. 【推荐】 用小括号来限定运算优先级
我们没有理由假设读者能记住整个Java运算符优先级表。除非作者和Reviewer都认为去掉小括号也不会使代码被误解,甚至更易于阅读。
WARNING
if ((a == b) && (c == d))
- Sonar-1068:Limited dependence should be placed on operator precedence rules in expressions,我们修改了三目运算符foo!=null?foo:"" 不需要加括号。
Rule 3. 【推荐】类内方法定义的顺序,不要“总是在类的最后添加新方法”
一个类就是一篇文章,想象一个阅读者的存在,合理安排方法的布局。
1)顺序依次是:构造函数 > (公有方法>保护方法>私有方法) > getter/setter方法。
如果公有方法可以分成几组,私有方法也紧跟公有方法的分组。
2)当一个类有多个构造方法,或者多个同名的重载方法,这些方法应该放置在一起。其中参数较多的方法在后面。
WARNING
public Foo(int a) {...}
public Foo(int a, String b)
public void foo(int a) {...}
public void foo(int a, String b)
3)作为调用者的方法,尽量放在被调用的方法前面。
WARNING
public void foo() {
bar();
}
public void bar()
Rule 10. 【强制】IDE的text file encoding设置为UTF-8; IDE中文件的换行符使用Unix格式,不要使用Windows格式
注释规约
Rule 1. 【强制】类、类的公有成员、方法的注释必须使用Javadoc规范,使用/** xxx /格式,不得使用***//xxx****方式
正确的JavaDoc格式可以在IDE中,查看调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
Rule 3.【强制】所有类必须增加创建人,创建日期
WARNING
正例:
/**
@author yangguanbao
@date 2021/11/26
**/
代码后续还会有多人多次维护,而创建人可能会离职,让我们相信源码版本控制系统对更新记录能做得更好。
Rule 7. 【强制】代码修改的同时,注释也要进行相应的修改。尤其是参数、返回值、异常、核心逻辑等的修改
Rule 9.【推荐】基本的注释要求
完全没有注释的大段代码对于阅读者形同天书,注释是给自己看的,即使隔很长时间,也能清晰理解当时的思路;注释也是给继任者看的,使其能够快速接替自己的工作。
代码将被大量后续维护,注释如果对阅读者有帮助,不要吝啬在注释上花费的时间。(但也综合参见规则2,3)
第一、能够准确反应设计思想和代码逻辑;第二、能够描述业务含义,使别的程序员能够迅速了解到代码背后的信息。
除了特别清晰的类,都尽量编写类级别注释,说明类的目的和使用方法。
除了特别清晰的方法,对外提供的公有方法,抽象类的方法,同样尽量清晰的描述:期待的输入,对应的输出,错误的处理和返回码,以及可能抛出的异常。
Rule 12. 【推荐】TODO 标记,清晰说明代办事项和处理人
“对那些临时的, 短期的解决方案, 或已经够好但仍不完美的代码使用 TODO 注释. ”
清晰描述待修改的事项,保证过几个月后仍然能够清楚要做什么修改。
如果近期会处理的事项,写明处理人。如果远期的,写明提出人。
通过 IDE 和 Sonar 的标记扫描,经常清理此类标记,线上故障经常来源于这些标记但未处理的代码。
正例:
WARNING
// TODO(who): to do what.
// TODO(who): when to do what.
// TODO(calvin): use xxx to replace yyy.
反例:
WARNING
// TODO: refactor it
不推荐使用 FIXME 标记。这样,在项目中搜索 TODO 就可以得到完整的 TODO list,而不需要搜索 TODO 和 FIXME。
Rule 13. 【推荐】通过更清晰的代码来避免注释
在编写注释前,考虑是否可以通过更好的命名,更清晰的代码结构,更好的函数和变量的抽取,让代码不言自明,此时不需要额外的注释。
Rule 14. 【推荐】删除空注释,无意义注释
《Clean Code》建议,如果没有想说的,不要留着IDE自动生成的,空的@param,@return,@throws 标记,让代码更简洁。
反例:方法名为put,加上两个有意义的变量名elephant和fridge,已经说明了这是在干什么,不需要任何额外的注释。
WARNING
/**
- put elephant into fridge.
- @param elephant
- @param fridge
- @return
*/
public void put(Elephant elephant, Fridge fridge);
Rule 15. 【强制】方法进行修改时,只要不是同一个人需要增加修改人和修改时间
如果多个人修改,需要用,分割
WARNING
/**
*
@author xxx,ddd
@date 2021/11/26,2021/11/29
@return
*/
public void put(Elephant elephant, Fridge fridge);
方法设计
Rule 1. 【推荐】方法的长度度量
方法尽量不要超过100行,或其他团队共同商定的行数。
另外,方法长度超过8000个字节码时,将不会被JIT编译成二进制码。
- Sonar-107: Methods should not have too many lines,默认值改为100
- Facebook-Contrib:Performance - This method is too long to be compiled by the JIT
Rule 2.【推荐】不使用不稳定方法,如com.sun.*包下的类,底层类库中internal包下的类
com.sun.*,sun.*包下的类,或者底层类库中名称为internal的包下的类,都是不对外暴露的,可随时被改变的不稳定类。
Rule 3. 【推荐】尽量减少重复的代码,抽取方法
超过5行以上重复的代码,都可以考虑抽取公用的方法。
Rule 4. 【推荐】方法参数最多不超过5个
1)如果多个参数同属于一个对象,直接传递对象。
例外: 你不希望依赖整个对象,传播了类之间的依赖性。
2)将多个参数合并为一个新创建的逻辑对象。
例外: 多个参数之间毫无逻辑关联。
3)将函数拆分成多个函数,让每个函数所需的参数减少。
Rule 6.【推荐】下列情形,需要进行参数校验
1) 调用频次低的方法。
2) 执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回退,或者错误,代价更大。
3) 需要极高稳定性和可用性的方法。
4) 对外提供的开放接口,不管是RPC/HTTP/公共类库的API接口。
如果使用Apache Validate 或 Guava Precondition进行校验,并附加错误提示信息时,注意不要每次校验都做一次字符串拼接。
WARNING
//WRONG
Validate.isTrue(length > 2, "length is "+keys.length+", less than 2", length);
//RIGHT
Validate.isTrue(length > 2, "length is %d, less than 2", length);
Rule 7.【推荐】下列情形,不需要进行参数校验
1) 极有可能被循环调用的方法。
2) 底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。
比如,一般DAO层与Service层都在同一个应用中,所以DAO层的参数校验,可以省略。
3) 被声明成private,或其他只会被自己代码所调用的方法,如果能够确定在调用方已经做过检查,或者肯定不会有问题则可省略。
即使忽略检查,也尽量在方法说明里注明参数的要求,比如vjkit中的@NotNull,@Nullable标识。
Rule 9.【推荐】返回值可以为Null,可以考虑使用JDK8的Optional类
不强制返回空集合,或者空对象。但需要添加注释充分说明什么情况下会返回null值。
本手册明确防止NPE是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败、序列化失败、运行时异常等场景返回null的情况。
JDK8的Optional类的使用这里不展开。
Rule 12.【强制】正被外部调用的接口,不允许修改方法签名,避免对接口的调用方产生影响
只能新增新接口,并对已过时接口加@Deprecated注解,并清晰地说明新接口是什么。
Rule 13.【推荐】不使用****@Deprecated****的类或方法
接口提供方既然明确是过时接口并提供新接口,那么作为调用方来说,有义务去考证过时方法的新实现是什么。
比如java.net.URLDecoder 中的方法decode(String encodeStr) 这个方法已经过时,应该使用双参数decode(String source, String encode)。
类设计
Rule 1.【推荐】 定义变量与方法参数时,尽量使用接口而不是具体类
使用接口可以保持一定的灵活性,也能向读者更清晰的表达你的需求:变量和参数只是要求有一个Map,而不是特定要求一个HashMap。
例外:如果变量和参数要求某种特殊类型的特性,则需要清晰定义该参数类型,同样是为了向读者表达你的需求。
Rule 2. 【推荐】类的长度度量
类尽量不要超过300行,或其他团队共同商定的行数。
对过大的类进行分拆时,可考虑其内聚性,即类的属性与类的方法的关联程度,如果有些属性没有被大部分的方法使用,其内聚性是低的。
Rule 3.【强制】所有的子类覆写方法,必须加****@Override****注解
比如有时候子类的覆写方法的拼写有误,或方法签名有误,导致没能真正覆写,加@Override可以准确判断是否覆写成功。
而且,如果在父类中对方法签名进行了修改,子类会马上编译报错。
另外,也能提醒阅读者这是个覆写方法。
最后,建议在IDE的Save Action中配置自动添加@Override注解,如果无意间错误同名覆写了父类方法也能被发现。
Rule 4.【强制】静态方法不能被子类覆写
因为它只会根据表面类型来决定调用的方法。
WARNING
Base base = new Children();
// 下句实际调用的是父类的静态方法,虽然对象实例是子类的。
base.staticMethod();
Rule 5.静态方法访问的原则
5.1【推荐】避免通过一个类的对象引用访问此类的静态变量或静态方法,直接用类名来访问即可
目的是向读者更清晰传达调用的是静态方法。可在IDE的Save Action中配置自动转换。
WARNING
int i = objectA.staticMethod(); // WRONG
int i = ClassA.staticMethod(); // RIGHT
- Sonar-2209: "static" members should be accessed statically
- Sonar-2440: Classes with only "static" methods should not be instantiated
5.2 【推荐】除测试用例,不要static import 静态方法
静态导入后忽略掉的类名,给阅读者造成障碍。
例外:测试环境中的assert语句,大家都太熟悉了。
- Sonar-3030: Classes should not have too many "static" imports 但IDEA经常自动转换static import,所以暂不作为规则。
5.3【推荐】尽量避免在非静态方法中修改静态成员变量的值
WARNING
// WRONG
public void foo() {
ClassA.staticFiled = 1;
}
- Sonar-2696: Instance methods should not write to "static" fields
- Sonar-3010: Static fields should not be updated in constructors
Rule 6.【推荐】 内部类的定义原则
当一个类与另一个类关联非常紧密,处于从属的关系,特别是只有该类会访问它时,可定义成私有内部类以提高封装性。
另外,内部类也常用作回调函数类,在JDK8下建议写成Lambda。
内部类分匿名内部类,内部类,静态内部类三种。
- 匿名内部类 与 内部类,按需使用:
在性能上没有区别;当内部类会被多个地方调用,或匿名内部类的长度太长,已影响对调用它的方法的阅读时,定义有名字的内部类。
- 静态内部类 与 内部类,优先使用静态内部类:
- 非静态内部类持有外部类的引用,能访问外类的实例方法与属性。构造时多传入一个引用对性能没有太大影响,更关键的是向阅读者传递自己的意图,内部类会否访问外部类。
- 非静态内部类里不能定义static的属性与方法。
- Sonar-2694: Inner classes which do not reference their owning classes should be "static"
- Sonar-1604: Anonymous inner classes containing only one method should become lambdas
Rule 7.【推荐】使用getter/setter方法,还是直接public成员变量的原则
除非因为特殊原因方法内联失败,否则使用getter方法与直接访问成员变量的性能是一样的。
使用getter/setter,好处是可以进一步的处理:
- 通过隐藏setter方法使得成员变量只读
- 增加简单的校验逻辑
- 增加简单的值处理,值类型转换等
建议通过IDE生成getter/setter。
但getter/seter中不应有复杂的业务处理,建议另外封装函数,并且不要以getXX/setXX命名。
如果是内部类,以及无逻辑的POJO/VO类,使用getter/setter除了让一些纯OO论者感觉舒服,没有任何的好处,建议直接使用public成员变量。
例外:有些序列化框架只能从getter/setter反射,不能直接反射public成员变量。
Rule 8.【强制】除了保持兼容性的情况,总是移除无用属性、方法与参数
特别是private的属性、方法、内部类,private方法上的参数,一旦无用立刻移除。信任代码版本管理系统。
- Sonar-3985: Unused "private" classes should be removed
- Sonar-1068: Unused "private" fields should be removed
- Sonar: Unused "private" methods should be removed
- Sonar-1481: Unused local variables should be removed
- Sonar-1172: Unused method parameters should be removed
Rule 9.【推荐】final关键字与性能无关,仅用于下列不可修改的场景
1) 定义类及方法时,类不可继承,方法不可覆写;
2) 定义基本类型的函数参数和变量,不可重新赋值;
3) 定义对象型的函数参数和变量,仅表示变量所指向的对象不可修改,而对象自身的属性是可以修改的。
控制语句
Rule 1.【强制】switch的规则
1)在一个switch块内,每个case要么通过break/return等来终止,要么注释说明程序将继续执行到哪一个case为止;
2)在一个switch块内,都必须包含一个default语句并且放在最后,即使它什么代码也没有。
WARNING
String animal = "tomcat";
switch (animal) {
case "cat":
System.out.println("It's a cat.");
break;
case "lion": // 执行到tiger
case "tiger":
System.out.println("It's a beast.");
break;
default:
// 什么都不做,也要有default
break;
}
Rule 3. 【强制】if, else, for, do, while语句必须使用大括号,即使只有单条语句
曾经试过合并代码时,因为没加括号,单条语句合并成两条语句后,仍然认为只有单条语句,另一条语句在循环外执行。
其他增加调试语句等情况也经常引起同样错误。
可在开发工具的Save Action中配置自动添加。
if (a == b) {
...
}
例外:一般由开发工具生成的equals()函数
Rule 7.【推荐】限定方法的嵌套层次
所有if/else/for/while/try的嵌套,当层次过多时,将引起巨大的阅读障碍,因此一般推荐嵌套层次不超过4。
通过抽取方法,或哨兵语句(见Rule 2)来减少嵌套。
public void applyDriverLicense() {
if (isTooYoung()) {
System.out.println("You are too young to apply driver license.");
return;
}
if (isTooOld()) {
System.out.println("You are too old to apply driver license.");
return;
}
System.out.println("You've applied the driver license successfully.");
return;
}
Rule 10.【推荐】循环体中的语句要考量性能,操作尽量移至循环体外处理
1)不必要的耗时较大的对象构造;
2)不必要的try-catch(除非出错时需要循环下去)。
Rule 15.【推荐】布尔表达式中的布尔运算符(&&,||)的个数不超过4个,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性
WARNING
//WRONG
if ((file.open(fileName, "w") != null) && (...) || (...)|| (...)) {
...
}
//RIGHT
boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
if (existed || (...)) {
...
}
Rule 16.【推荐】简单逻辑,善用三目运算符,减少if-else语句的编写
WARNING
s != null ? s : "";
Rule 17.【推荐】减少使用取反的逻辑
不使用取反的逻辑,有利于快速理解。且大部分情况,取反逻辑存在对应的正向逻辑写法。
WARNING
//WRONG
if (!(x >= 268)
//RIGHT
if (x < 268)
并发处理
Rule 2. 【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯
1)创建单条线程时直接指定线程名称
WARNING
Thread t = new Thread();
t.setName("cleanup-thread");
2) 线程池则使用guava或自行封装的ThreadFactory,指定命名规则。
WARNING
//guava 或自行封装的ThreadFactory
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat(threadNamePrefix + "-%d").build();
ThreadPoolExecutor executor = new ThreadPoolExecutor(..., threadFactory, ...);
Rule 3. 【推荐】尽量使用线程池来创建线程
除特殊情况,尽量不要自行创建线程,更好的保护线程资源。
WARNING
//WRONG
Thread thread = new Thread(...);
thread.start();
同理,定时器也不要使用Timer,而应该使用ScheduledExecutorService。
因为Timer只有单线程,不能并发的执行多个在其中定义的任务,而且如果其中一个任务抛出异常,整个Timer也会挂掉,而ScheduledExecutorService只有那个没捕获到异常的任务不再定时执行,其他任务不受影响。
Rule 20. 【强制】线程池不允许使用 Executors去创建,避资源耗尽风险
Executors返回的线程池对象的弊端 :
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
应通过 new ThreadPoolExecutor(xxx,xxx,xxx,xxx)这样的方式,更加明确线程池的运行规则,合理设置Queue及线程池的core size和max size,建议使用vjkit封装的ThreadPoolBuilder。
Rule 21. 【强制】正确停止线程
Thread.stop()不推荐使用,强行的退出太不安全,会导致逻辑不完整,操作不原子,已被定义成Deprecate方法。
停止单条线程,执行Thread.interrupt()。
停止线程池:
- ExecutorService.shutdown(): 不允许提交新任务,等待当前任务及队列中的任务全部执行完毕后退出;
- ExecutorService.shutdownNow(): 通过Thread.interrupt()试图停止所有正在执行的线程,并不再处理还在队列中等待的任务。
最优雅的退出方式是先执行shutdown(),再执行shutdownNow(),vjkit的ThreadPoolUtil进行了封装。
注意,Thread.interrupt()并不保证能中断正在运行的线程,需编写可中断退出的Runnable,见规则5。
Rule 22. 【推荐】缩短锁
1) 能锁区块,就不要锁整个方法体;
WARNING
//锁整个方法,等价于整个方法体内synchronized(this)
public synchronized boolean foo(){};
//锁区块方法,仅对需要保护的原子操作的连续代码块进行加锁。
public boolean foo() {
synchronized(this) {
...
...
}
//other stuff
}
2)能用对象锁,就不要用类锁。
WARNING
//对象锁,只影响使用同一个对象加锁的线程
synchronized(this) {
...
}
//类锁,使用类对象作为锁对象,影响所有线程。
synchronized(A.class) {
...
}
日志规约
Rule 1. 【强制】应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架SLF4J,JCL—Jakarta Commons Logging)中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理
Rule 2. 【强制】日志文件至少保存15天,个人操作等相关记录,保留不少于6个月
Rule 8. 【强制】****生产环境禁止使用 System.out 或 System.err 输出或使用 e.printStackTrace() 打印异常堆栈
Rule 15. 【强制】****格式为{日志类型}.{保存日期}.log 保存当前应用的logs 目录下
日志类型 info,error 日期格式:yyyy-MM-dd 日志保存在logs/error.2016-08-01.log
Rule 16. 【强制】** **springboot 环境下 dev,test,fix 输出 info 以上日志级别,prod只输出error级别日志
maven pom配置
<profiles>
<profile>
<id>dev</id>
<properties>
<profiles.active>dev</profiles.active>
</properties>
</profile>
<profile>
<id>test</id>
<properties>
<profiles.active>test</profiles.active>
</properties>
</profile>
<profile>
<id>fix</id>
<properties>
<profiles.active>fix</profiles.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<profiles.active>prod</profiles.active>
</properties>
</profile>
</profiles>
2 application配置文件配置
spring.profiles.active=@profiles.active@
接口规约
Rule 1. 【强制】http接口采用 restful 风格
- GET 查询接口(用于查询数据)
- POST 提交接口(用于插入数据、提交安全加密数据等)
- PUT 更新接口(用于更新数据,具有破坏性)
- DELETE 删除接口(用于删除数据,具有破坏性)
Rule 2. 【推荐】****URL前缀风格做一定的安全认证区分
前缀 | 作用 | 示例 |
---|---|---|
paas/模块或服务名称 | 平台业务接口 | /paas/basic/v1.2.0/user/接口方法名 |
lan/模块或服务名称 | 内部接口,仅内调用 | /lan/basic/v1.2.0/user/接口方法名 |
open/模块或服务名称 | 为外部系统或第三方提供的接口 💡 注:此类接口进行签名验证,采用sa-token | /open/basic/v1.2.0/user/接口方法名 |
Rule 3. 【推荐】****外部系统或第三方提供的接口需要接口签名
https://sa-token.cc/doc.html#/plugin/api-sign?id=_1
使用 Sa-Token 框架完成 API 参数签名
接下来步入正题,使用 Sa-Token 内置的 sign 模块,方便的完成 API 签名创建、校验等步骤:
- 不限制请求的参数数量,方便组织业务需求代码。
- 自动补全 nonce、timestamp 参数,省时省力。
- 自动构建签名,并序列化参数为字符串。
- 一句代码完成 nonce、timestamp、sign 的校验,防伪造请求调用、防参数篡改、防重放攻击。
api-sign 模块已内嵌到核心包,只需要引入 sa-token 本身依赖即可:(请求发起端和接收端都需要引入)
查看官网使用最新版本
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
请求发起端和接收端需要配置一个相同的秘钥,在application.yml 中配置:
sa-token:
sign:
# API 接口签名秘钥 (随便乱摁几个字母即可)
secret-key: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor复制到剪贴板错误复制成功
请求发起端构建签名
// 请求地址
String url = "http://b.com/api/addMoney";
// 请求参数
Map<String, Object> paramMap = new LinkedHashMap<>();
paramMap.put("userId", 10001);
paramMap.put("money", 1000);
// 更多参数,不限制数量...
// 补全 timestamp、nonce、sign 参数,并序列化为 kv 字符串
String paramStr = SaSignUtil.addSignParamsAndJoin(paramMap);
// 将参数字符串拼接在请求地址后面
url += "?" + paramStr;
// 发送请求
String res = HttpUtil.request(url);
// 根据返回值做后续处理
System.out.println("server 端返回信息:" + res);复制到剪贴板错误复制成功
请求接受端校验签名
// 为指定用户添加指定余额
@RequestMapping("addMoney")
public SaResult addMoney(long userId, long money) {
// 1、校验请求中的签名
SaSignUtil.checkRequest(SaHolder.getRequest());
// 2、校验通过,处理业务
System.out.println("userId=" + userId);
System.out.println("money=" + money);
// 3、返回
return SaResult.ok();
}
如上代码便可简单方便的完成 API 接口参数签名校验,当请求端的秘钥不对,或者请求参数被篡改、请求被重放时,均无法通过SaSignUtil.checkRequest
Rule 4. 【推荐】http接口版本控制
API的版本号统一放入URL,如:https://api.example.com/paas/iot/version/device
具体参见 DevOps规范
WARNING
1 当请求正确的版本地址时,会自动匹配版本的对应接口。
2 高版本的接口的新增和修改不会影响低版本。
Rule 5. 【强制】路径使用名词表示资源,而不是动词,并且为复数形式
WARNING
正例: /users
**
**反例: /getUsers
Rule 6. 【强制】路径中的单词全部小写,使用连字符分隔
在路径中使用连字符(短横线)-来分隔单词,而不是下划线或驼峰式命名
WARNING
正例:/user-profile
反例:/userProfile 或 /user_profiles
Rule 7. 【强制】路径参数只能使用主键或唯一标识
WARNING
/users/
Rule 8. 【强制】条件选择必须以 url 参数形式出现,参数风格为驼峰,如果多个字段以,分割
WARNING
/employee?pageNum=1&pageSize=20&sort=age,name&order=desc,asc
Rule 9. 【强制】返回多条数据路径后缀要以 /list 结尾
WARNING
/paas/basic/v1.2.0/users/list
Rule 10. 【强制】返回分页数据路径后缀要以 /page 结尾
WARNING
/paas/basic/v1.2.0/users/page
Rule 11. 【建议】http 参数无论是 get, post, delete, put 类型参数风格为驼峰
WARNING
反例: user_name, user-name
Rule 12. 【强制】分页请求参数名为 page,page,sort,order
参数名称 | 参数类型 | 描述 | 示例 |
---|---|---|---|
page | Int | 分页查询-当前页 | 1 |
size | Int | 分页查询-每页显示条数 | 20 |
sort | String | 排序字段 | |
order | String | 排序方向 | asc或desc |
Rule 13. 【强制】http接口响应为 json 格式,结构为 code, msg, data
参数名称 | 参数类型 | 描述 | 示例 |
---|---|---|---|
code | Int | 响应状态码 0 为成功 负数 为失败 | 0 |
msg | String | 响应提示 | 成功 |
data | Object | 业务数据 |
Rule 14. 【强制】http接口分页响应在 data 里,结构为 total,size,current,pages,records
参数名称 | 参数类型 | 描述 | 是否必须 | 示例 |
---|---|---|---|---|
total | String | 总记录数 | 是 | 100 |
size | String | 每页记录数 | 是 | |
current | String | 当前页数 | 是 | |
pages | String | 总页数 | 是 | |
records | List | 结果集 | 是 |
{
"code": 0,
"msg": "成功",
"data": {
"total":"111",
"size":"11",
"current":"11",
"pages":"11",
"records":[]
}
}
Rule 15. 【强制】所有返回的Long类型字段统一装换成String类型,解决前端无法处理Long类型问题
Rule 16. 【强制】接口必须采用swagger2进行注解 ,接口组织形式以功能命名,接口必须有参数描述和返回值描述说明
示例:
HTTP方法 | 接口动作类型 | 接口方法名后缀 | 接口动作解释 | 示例 |
---|---|---|---|---|
POST | 增 | 无 | 新增、插入、添加 | POST /paas/basic/v1.2.0/users |
DELETE | 删 | 无 | 物理删除、逻辑删除 | DELETE /paas/basic/v1.2.0/users |
PUT | 改 | 无 | 修改、更新 | PUT /paas/basic/v1.2.0/users |
GET | 查 | 无 | 单条(一个结果) | GET /paas/basic/v1.2.0/users/1 |
GET | 查 | list | 列表(多个结果) | GET /paas/basic/v1.2.0/users/list |
GET | 查 | page | 分页 | GET /paas/basic/v1.2.0/users/page |
查询类接口示例:
例如 GET /pass/iot/v1.2.0/tasks?creator=ming 表示查询所有 ming 用户创建的任务
例如 GET /pass/iot/v1.2.0/tasks?keyword=ming 表示查询任意列包含 ming 关键词的任务
例如 GET /pass/iot/v1.2.0/tasks?status=pending,complete 表示查询状态为阻塞和完成的任务
例如 GET /pass/iot/v1.2.0/tasks?weight=10&weight=20 表示查询权重在 10 和 20 之间的任务
例如 GET /pass/iot/v1.2.0/tasks?weight=10 表示查询权重大于 10 的任务
例如 GET /pass/iot/v1.2.0/tasks/page?pageNum=1&pageSize=10 表示每页 10 条查询第一页数据
例如 GET /pass/iot/v1.2.0/tasks?sort=createdAt&order=desc 表示以创建时间降序查询数据
删除类接口示例:
例如 DELETE /pass/iot/v1.2.0/tasks/1 表示删除 id 为 1 的任务
例如 DELETE /pass/iot/v1.2.0/tasks?ids=1,2,3 表示批量删除 id 为 1 或 2 或 3 的任务
更多参考->[RESTful 规范]
数据库规约
Rule 1. 【强制】数据库命名规范
类型 | 描述 | |
---|---|---|
数据库名 | 以 jg 开头,多个单词用_分割,列如:jg_hn_gis_dev,jg_alarm_center_dev 等,以环境后缀结尾开发为_dev,测试_test,正式环境没有后缀 | |
表名 | 以模块名_功能名 开头,多个单词用_分割,例如:iot_device_info,basic_user_info等 | |
实体表 | 无 | |
关系表 | 以 _ref 结尾 | |
汇总表 | 以_dws 结尾 | |
临时表 | 以 _temp 结尾 | |
备份表 | 以 _bak 结尾 | |
日志表 | 以 _log 结尾 | |
字典表 | 以 _dict 结尾 | |
配置表 | 以 _config 结尾 | |
索引 | 以idx开头,加字段名,并用_分割,列如:idx_is_delete,idx_create_time 等 | |
视图 | 规则同表名,以 _view 结尾 | |
触发器 | 名称多个单词以_分割,并以_trigger 结尾 | |
字段 | 名称全部小写,多个单词以_分割 |
Rule 2. 【强制】部分字段命名规范
描述 | 字段名称 | 字段类型 | 描述 |
---|---|---|---|
逻辑删除字段 | del_flag | int2 | 如果表中存在逻辑删除字段名称为del_flag 0正常 1 删除,关系表不应该存在逻辑删除字段 不要使用is开头,因为很多序列化库对is开头处理不友好 |
时间字段 | create_time | timestamp | 创建时间,字段采用时间戳类型 |
状态字段 | status | int2 | |
排序字段 | sort | int2 |
Rule3. 【强制】版本管理规范
版本管理采用flyway框架
规范
1 sql文件为服务用到的相关表或视图等
2 规范为 V+版本号+__+名称.sql
3 每个服务负责管理自己的相关sql,并统一存放到 resources\db\migration目录中
4 sql文件里的表名一定要有表空间的名称
类型 | 规则 | 示例 | 描述 |
---|---|---|---|
版本号 | Vx.x.x.x | V10.1.0.0 | 采用4位版本号,前三位为产品定义,后一位为sql版本迭代号 |
名称 | artifactId去掉前缀和后缀 | base | 采用 pom的artifactId,去掉前缀和后缀,jm-base-service 去掉后为base |
示例:
V10.1.0.0__base.sql 当表结构发生变动后 V10.1.0.1__base.sql
服务配置
application.yml 文件配置
talbe规则 | 示例 |
---|---|
flyway_schema_history_artifactId去掉前缀和后缀 | flyway_schema_history_base |
spring:
flyway:
baseline-on-migrate: true
schemas: flyway
table=flyway_schema_history_base
当方法使用@PostConstruct注解并访问数据库表时,需要再类上增加@DependsOn("flywayInitializer")注解
示例:
Rule4. 【推荐】对于每次都需要执行不需要版本管理的Sql文件,使用springboot 自带的配置
spring:
sql:
init:
data-locations: classpath:db/a.sql
mode: always
sql文件统一放到resources目录下的db目录中,使用data-location进行sql文件地址的配置,并配置mode为always
Rule5. 【强制】禁止在表中建立预留字段
预留字段的命名很难做到见名识义 预留字段无法确认存储的数据类型,所以无法选择合适的类型 对预留字段类型的修改。
Rule6. 【强制】禁止在数据库中存储图片,文件等大的二进制数据
Rule7. 【强制】禁止跨库查询
Rule8. 【强制】禁止使用外键,如果有外键完整性约束,需要应用程序控制
**更多参考-> **设计开发规范- 关系型数据库(PostgreSQL) - 文档中心
项目规约
Rule 1. 【推荐】单模块项目和多模块项目目录划分参见Java项目规范
Rule 2. 【推荐】controller、service、impl、mapper、dto、entity、vo 文件夹可以根据项目具体业务模块细化子文件夹
Rule 3. 【强制】controller ,service ,mapper 接口或类命名要以相应的关键字结尾,如果实现了Service接口要以ServiceImpl结尾,并且首字母大写
WARNING
正例:UserController,UserService,UserMapper****
Rule 4. 【强制】controller 类中只能引用 service 类,方法里不能有业务逻辑处理及参数的规范化
1 不能引用 mapper 或 dao, controller 类中的方法只处理参数校验、实体封装和转化、service方法调用、异常捕捉、日志记录以及权限控制,不可参与其他业务逻辑处理 ** **
2 入参和出参的类必须放在 domain 目录下 vo,dto,entity 目录下
- 入参,其中新增、修改和删除方法的类使用Command结尾,查询使用Query结尾,也可以使用普通入参,但是最多可以有5个参数,超过5个必须定义类放在vo目录下。
- 出参,使用Result结尾,多于两个参数,必须定义类放在vo目录下。如果底层的出参是Entity或Dto,且都能满足前端需要,可直接把Entity或Dto作为出参,否则严禁直接将Dto或Entity当做入参或出参使用,分页情况除外。
Rule 5. 【强制】 service 类中只能引用自己的 mapper 接口和其它 service 类及参数的规范化
1 不可引用其它的 mapper 接口,且 service 类中不可进行循环依赖, service 类中主要进行业务逻辑处理、事务管理、异常捕捉以及 service 和 mapper方法调用等, 再处理异常时,不可在接口方法上面直接 throws,但是可以在方法内部对异常进行 throw
2
- 入参,可以用Vo、Dto、普通入参,其中Dto实体类放在 domain --> dto 文件夹中,文件结尾都用 Dto,同样普通入参超过5个必须定义实体类。
- 出参,可以用Dto,Entity,如果Entity文件包含返回结果的所有字段,才可用Entity文件进行返回,其余情况需要定义Dto实体类。
- 严禁直接将Entity作为入参,Vo作为出参
Rule 6. 【强制】 mapper 参数的规范化
- 入参,可以用Entity、Dto、Vo、Map、普通参数,其中Entity实体类放在 domain --> entity 文件夹中,文件结尾都用 Entity。可结合实际情况选择入参类型,但尽量使用已存在的实体,不建议再创建新的实体类。
- 出参,可以用Entity,Dto,如果单表返回可用Entity,多表联合返回只能使用Dto。
- 严禁直接将Vo作为出参
Rule 7. 【强制】vo 目录下的类名称要以 Command 或 Query 结尾,entity 目录下的类名称要以 Entity 结尾,dto 目录下的类名称要以 Dto 结尾
Command 结尾的类代表 新增、修改和删除 ,Query结尾的类代表查询
WARNING
正例:UserInfoAddCommand,UserInfoUpdateCommand,UserInfoQuery
UserInfoEntity,UserInfoTempDto
Rule 8. 【建议】Controller,Service和Mapper方法命名规则
操作 | 命名 | 类 |
---|---|---|
增加 | 以add开头 | Controller和Service |
以insert开头 | Mapper | |
删除 | 以remove开头 | Controller和Service |
以delete开头 | Mapper | |
修改 | 以update开头 | Controller,Service和Mapper |
单条查询 | 以get开头 | Controller和Service |
以select开头 | Mapper | |
多条查询和分页 | 以find开头 | Controller和Service |
以select开头 | Mapper |
Rule 9. 【建议】异常码规则
异常码必须为5位负数 以 10000 开始 规则如下:
-10000 多模或服务情况下 以100为区间,如 A 模块或服务为 -10001 到 - 10100 B模块或服务为 -10101 到 -10200
其它规约
Rule 1. 【推荐】使用 dependency-check-maven 第三方库漏洞扫描
Rule 2. 【推荐】通过maven构建自动生成dockerfile pom配置
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<version>3.1.0</version>
<executions>
<execution>
<id>generate-dockerfile</id>
<phase>package</phase>
<configuration>
<target>
<echo file="${project.basedir}/Dockerfile" append="false">
<![CDATA[
FROM openjdk:11.0-jre-slim
ADD ./target/${artifactId}-${env.version}.jar /opt/hn/${artifactId}.jar
CMD ["java","-jar","/opt/hn/${artifactId}.jar"]
]]>
</echo>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
Rule 3. 【强制】spring配置文件采用 yml格式,所有自定义配置均采用公司唯一标识开头主要为了和系统和第三方框架定义参数区别
示例:
jg:
storage:
ip: ${SERVICE_HOST:localhost}
port: ${UPLOAD_SERVICE_PORT:8085}
nginx-url: ${NGINX_URL:http://localhost:8080/}
</plugin>
3. 规范落地
规则落地主要依靠springboot框架二次封装和Sonar代码规则检查
4. 参考资料
5. 定制记录
只记录较大的改动,对更多条目内容的重新组织与扩写,则未一一冗述。
(一) 命名规约
对应** **阿里规范《命名风格》一章
框架规范 | 阿里规范 | 修改 |
---|---|---|
**1.**所有相关命名只能是英文字母 | **1.**所有编程相关的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束 | 改写规则 |
2. 禁止拼音缩写 | 2. 严禁使用拼音与英文混合的方式 | 改写规则 |
**** | **3.**代码和注释中都要避免使用任何人类语言中的种族歧视性或侮辱性词语 | 删除规则 |
4.类名必须使用 UpperCameCase风格 | **4.**类名使用 UpperCamelCase 风格,以下情形例外:DO / PO / DTO / BO / VO / UID 等 | 改写规则 |
12. 命名的好坏,在于其“模糊度” | 12,13,14 | 合并 |
**** | 16,18 | 删除规则 |
19. 各层命名规约 | 删除规则,新创建规则不和通用命名规范放在一起,具有特殊性 |
(二) 格式规约
对应** **阿里规范《代码格式》一章
框架规范 | 阿里规范 | 修改 |
---|---|---|
1. 项目组统一的代码格式插件进行格式化 | 规则1-8 | **用IDE模版代替逐条描述 **同时对Tab/空格不做硬性规定 |
2. 用小括号来限定运算优先级 | **** | 新增规则 |
3. 类内方法定义的顺序 | **** | 新增规则 |
**** | 11.单个方法行数不超过80行 | 删除规则,非格式规约,移动方法设计 |
**** | 12.没有必要增加若干空格来对齐 | 删除规则,现在很少人这么做 |
**** | 13. 不同逻辑、不同语义 | 删除规则 |
(三) 注释规约
对应** **阿里规范《注释规约》一章
框架规范 | 阿里规范 | 修改 |
---|---|---|
9. 注释的基本要求 | 9. 对于注释的要求 | 扩写规则 |
**** | **2,4,5,6,8,9,10,11 ** | 删除规则 |
**12.**TODO 标记,清晰说明代办事项和处理人 | 12 | 修改规则 |
**13.**通过更清晰的代码来避免注释 | **** | 增加规则 |
14.删除空注释,无意义注释 | **** | 增加规则 |
**15.**方法进行修改时,只要不是同一个人需要增加修改人和修改时间 | **** | 增加规则 |
(四) 方法设计
框架规范 | 阿里规范 | 修改 |
---|---|---|
**** | 规则 6,7,12,13 从阿里规范《控制语句》一章** 移入** | 移动规则 |
**** | 规则 9 从阿里规范《异常处理》一章** 移入** | 移动规则 |
1,2,3,4 | **** | 新建规则 |
(六) 控制语句
对应** **阿里规范《控制语句》一章
框架规范 | 阿里规范 | 修改 |
---|---|---|
**7.**限定方法的嵌套层次 | **** | 修改规则 |
**1. **switch的规则 | 1,2 | 合并规则 |
**** | 4,5,6,8,9,11 | 删除规则 |
15.布尔表达式中的运算符个数不超过4个 | **** | 增加规则 |
16.善用三目运算符 | **** | 增加规则 |
**17.**减少使用取反的逻辑 | **** | 增加规则 |
**** | 13. 下列情形,需要进行参数校验 | 移到《方法规约》 |
**** | 14. 下列情形,不需要进行参数校 | 移到《方法规约》 |
(七) 并发处理
对应** **阿里规范《并发处理》一章
框架规范 | 阿里规范 | 修改 |
---|---|---|
**20. **线程池不允许使用 Executors去创建,避资源耗尽风险 | **** | 增加规则 |
**21. **正确停止线程 | **** | 增加规则 |
**22.**缩短锁 | **** | 增加规则 |
(八) 日志规约
对应** **阿里规范《日志规约》一章
框架规范 | 阿里规范 | 修改 |
---|---|---|
**2.**日志文件至少保存15天,个人操作等相关记录,保留不少于6个月 | 2,3 | 合并规则 |
**** | 4,5,6,7,9,10,11,12,13,14 | 删除规则 |
15.格式为{日志类型}.{保存日期}.log 保存当前应用的logs 目录下 | **** | 增加规则 |
16. springboot 环境下 dev,test,fix 输出 info 以上日志级别,prod只输出error级别日志 | **** | 增加规则 |