# 后端代码规范
# 版本更新
| 日期 | 备注 |
|---|---|
| 2020-05-19 | 5、数据结构的基础权限和数据溯源字段说明变更 |
# 1、前言
现代软件行业的高速发展对开发者的综合素质要求越来越高,因为不仅是编程知识点,其它维度的知识点也会影响到软件的最终交付质量。
比如:数据库的表结构和索引设计缺陷可能带来软件上的架构缺陷或性能风险;工程结构混乱导致后续维护艰难;没有鉴权的漏洞代码易被黑客攻击等等。本规范以 Java 开发者为中心视角,划分为编程规约、异常日志、安全规约3个维度,再根据内容特征,细分成若干二级子目录。
“说明”对规约做了适当扩展和解释;“正例”提倡什么样的编码和实现方式;“反例”说明需要提防的雷区,以及真实的错误案例。
规范的愿景是码出高效,码出质量。现代软件架构的复杂性需要协同开发完成,如何高效地协同呢?无规矩不成方圆,无规范难以协同,比如,制订交通法规表面上是要限制行车权,实际上是保障公众的人身安全,试想如果没有限速,没有红绿灯,谁还敢上路行驶?对软件来说,适当的规范和标准绝不是消灭代码内容的创造性、优雅性,而是限制过度个性化,以一种普遍认可的统一方式一起做事,提升协作效率,降低沟通成本。代码的字里行间流淌的是软件系统的血液,质量的提升是尽可能少踩坑,杜绝踩重复的坑,切实提升系统稳定性,码出质量。
# 2、概述
该文档阐述使用中台提供的框架开发项目时,编程代码编写基本规范,非接入中台和项目开发过程要点,如需了解后者,请阅读“接口建协云说明>>后端 (opens new window)”和“后端>>共用组件”。
# 3、编程规约
# 3.1、命名风格
- 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例:_name / __name / $name / name_ / name$ / name__
- 代码中的命名严禁使用拼音与英文混合的方式。
说明:正确的拼写和语法可以让阅读者易于理解,避免歧义。
正例:projectAddress/ price / userName
反例:gcAdress / jcName
- 类名使用 UpperCamelCase 风格
正例: XmlService / TcpUdpDeal
反例: XMLService / TCPUDPDeal
- 方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从驼峰形式。
正例: localValue / getHttpMessage() / inputUserId
- 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例:MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例:MAX_COUNT / EXPIRED_TIME
- 抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类命名以它要测试的类的名称开始,以 Test 结尾。
- 类型与中括号紧挨相连来表示数组。
正例:定义整形数组 int[] arrayDemo;
- 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。
正例:应用工具类包名为 com.hhh.ai.util
- 避免在子父类的成员变量之间、或者不同代码块的局部变量之间采用完全相同的命名,使可读性降低。
- 杜绝完全不规范的缩写,避免望文不知义。
反例:AbstractClass“缩写”命名成 AbsClass;condition“缩写”命名成 condi,此类随意缩写严重降低了代码的可阅读性。
- 各层命名规约
A) Controller 层方法命名
1)类名必须使用Controller后缀。
2)引用Service实现类,使用@Autowired,属性类型为Service接口类名,属性名称为实现类名称,如:private ILoginService loginService。
3)获取单个对象的方法用 get 做前缀。
4)获取多个对象的方法用 list 做前缀。
5)获取多个对象并分页的方法用list做前缀,使用Page做后缀。
6)插入的方法用 save/add 做前缀。
7)删除的方法用 delete 做前缀。
8)修改的方法用 edit/update 做前缀。
B) Service 层方法命名
1)引用Service实现类,使用@Autowired,属性类型为Service接口类名,属性名称为实现类名称,如:private ILoginService loginService。
2)获取单个对象的方法用 get 做前缀。
3)获取多个对象的方法用 list 做前缀。
4)获取多个对象并分页的方法用list做前缀,使用Page做后缀。
5)插入的方法用 save/add 做前缀。
6)删除的方法用 delete 做前缀。
7)修改的方法用 edit/update 做前缀。
C) Mapper 层方法命名
1)获取单个对象的方法用 select 做前缀。
2)获取多个对象的方法用 list 做前缀。
3)获取多个对象并分页的方法用list做前缀,使用Page做后缀。
4)插入的方法用 insert 做前缀。
5)删除的方法用 delete 做前缀。
6)修改的方法用 update 做前缀。
D) Entity 层方法命名
1)实体类名和数据表命名有差异时(实体类驼峰命名对应数据表下划线命名,这里的差异指实体类和数据表的单词无法完全对应),使用@TableName注解。
2)属性名和表字段名有差异时,(与2)同理),使用@TableField注解。
# 3.2、常量定义
- 在 long 或者 Long 赋值时,数值后使用大写的 L,不能是小写的 l,小写容易跟数字 1 混淆,造成误解。
说明:Long a = 2l; 写的是数字的 21,还是 Long 型的 2。
# 3.3、代码格式
- IDE 的 text file encoding 设置为 UTF-8。
- 如果是大括号内为空,则简洁地写成{}即可,大括号中间无需换行和空格;如果是非空代码块应:
1)左大括号前不换行。
2)左大括号后换行。
3)右大括号前换行。
4)右大括号后还有 else 等代码则不换行;表示终止的右大括号后必须换行。
- 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格;而左大括号前需要空格。详见第 5 条下方正例提示。
反例:if (空格 a == b 空格)
- if/for/while/switch/do 等保留字与括号之间都必须加空格。
- 任何二目、三目运算符的左右两边都需要加一个空格。
说明:运算符包括赋值运算符=、逻辑运算符&&、加减乘除符号等。
- 采用 4 个空格缩进,禁止使用 tab 字符。
说明:如果使用 tab 缩进,必须设置 1 个 tab 为 4 个空格。IDEA 设置 tab 为 4 个空格时,请勿勾选 Use tab character;而在 eclipse 中,必须勾选 insert spaces for tabs。
- 注释的双斜线与注释内容之间有且仅有一个空格。
正例:// 这是示例注释,请注意在双斜线之后有一个空格
- 在进行类型强制转换时,右括号与强制转换值之间不需要任何空格隔开。
正例:
long first = 1000000000000L;
int second = (int)first + 2;
- 方法参数在定义和传入时,多个参数逗号后边必须加空格。
正例:下例中实参的 args1,后边必须要有一个空格。
method(args1, args2, args3);
# 3.4、OO规约
- 所有的覆写方法,必须加@Override 注解。
说明:getObject()与 get0bject()的问题。一个是字母的 O,一个是数字的 0,加@Override 可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。
- 不能使用过时的类或方法。
- Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用 equals。
正例:"test".equals(object);
反例:object.equals("test");
- 所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
说明:对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数据,
都会在堆上产生,并不会复用已有对象,推荐使用 equals 方法进行判断。
- 浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用 equals 来判断。
说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数。
反例:
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
if (a == b) {
// 预期进入此代码快,执行其它业务逻辑
// 但事实上a==b的结果为false
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) {
// 预期进入此代码快,执行其它业务逻辑
// 但事实上equals的结果为false
}
正例: 指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
float diff = 1e-6f;
if (Math.abs(a - b) < diff) {
System.out.println("true");
}
// 使用 BigDecimal 来定义值,再进行浮点数的运算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.equals(y)) {
System.out.println("true");
}
- 序列化类新增属性时,请不要修改 serialVersionUID 字段,避免反序列失败。
- 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在 init 方法中。
- 各层级规约
A) Controller 层
1)每个controller负责范围限定:入参、入参校验/处理、出参,禁止编写任何逻辑代码。
2)前后端分离下,使用@RestController替代@Controller注解
3)Api注解使用@PostMapping、@GetMapping、@DeleteMapping等指定请求方式替代@RequestMapping。
4)Api均使用application/json提交数据方式。
5)禁止引用Mapper层。
6)分页/列表的方法,返回TableResult,其他返回Result。
7)Controller私有方法,必须使用private修饰。
B) Service层
1)每个实现方法负责范围限定:所有逻辑操作。
2)事务注解写在实现方法上,禁止写在类上。
3)接口类命名必须以大写“I”开头,如:ILoginService。
4)实现类命名以“Impl”后缀结束,如:LoginServiceImpl。
5)无特殊要求,引用Mapper实现类使用@Autowired。
6)简易的逻辑查询,使用QueryWrapper封装查询规则,不适用Mapper的自定义sql。
7)逻辑处理错误时,抛出BusinessException异常。
C) Mapper层
1)sql语句写在mapper.xml上。
2)同一功能块,公共查询条件应该封装<sql>标签
3)访问使用逻辑删除的数据表,必须带上逻辑删除字段查询,写死AND invalid = 0。
D) Entity层
1)使用Lombok的@Data、@Getter、@Setter等替代手写代码。
2)逻辑删除属性使用@TableLogic。
3)实体类的属性非表字段时,使用@TableField(exist = false)注解。
4)属性如果是日期格式,使用LocalDateTime。
# 3.5、集合处理
- 关于 hashCode 和 equals 的处理,遵循如下规则
1)只要覆写 equals,就必须覆写 hashCode。
2)因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两个方法。
3)如果自定义对象作为 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 子列表的所有操作最终会反映到原列表上。
- 使用 Map 的方法keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。
- Collections 类返回的对象,如:emptyList()/singletonList()等都是 immutable list,不可对其进行添加或者删除元素的操作。
反例:如果查询无结果,返回 Collections.emptyList()空集合对象,调用方一旦进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。
- 使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。
- 在使用 Collection 接口任何实现类的 addAll()方法时,都要对输入的集合参数进行 NullPointerException 判断。
说明:在 ArrayList#addAll 方法的第一行代码即 Object[] a = c.toArray(); 其中 c 为输入集合参数,如果为 null,则直接抛出异常。
- 在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行 instanceof 判断,避免抛出 ClassCastException 异常。
反例:
List<String> generics = null;
List notGenerics = new ArrayList(10);
notGenerics.add(new Object());
notGenerics.add(new Integer(1));
generics = notGenerics;
// 此处抛出 ClassCastException 异常
String string = generics.get(0);
- 不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对 Iterator 对象加锁。
# 3.6、并发处理
- 获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
说明:资源驱动类、工具类、单例工厂类都需要注意。
- 线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
- 线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
5.SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。
6.必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。
尽量在代理中使用 try-finally 块进行回收。
正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}
- 高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
- 对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。
- 在使用阻塞等待获取锁的方式中,必须在 try 代码块之外,并且在加锁方法与 try 代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在 finally 中无法解锁。
* 说明一:如果在 lock 方法与 try 代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功获取锁。
* 说明二:如果 lock 方法在 try 代码块之内,可能由于其它方法抛出异常,导致在 finally 代码块中, unlock 对未加锁的对象解锁,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),抛出
IllegalMonitorStateException 异常。
* 说明三:在 Lock 对象的 lock 方法实现中可能抛出 unchecked 异常,产生的后果与说明二相同。
正例:
Lock lock = new XxxLock();
// ...
lock.lock();
try {
doSomething();
doOthers();
} finally{
lock.unlock();
}
反例:
Lock lock = new XxxLock();
// ...
try {
// 如果此处抛出异常,则直接执行 finally 代码块
doSomething();
// 无论加锁是否成功,finally 代码块都会执行
lock.lock();
doOthers();
} finally {
lock.unlock();
}
- 在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果当前线程不持有锁,则抛出 IllegalMonitorStateException 异常。
正例:
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock(); if (isLocked) {
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
}
- 并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
# 3.7、控制语句
- 在一个 switch 块内,每个 case 要么通过 continue/break/return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
说明:注意 break 是退出 switch 语句块,而 return 是退出方法体。
- 当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null 判断。
- 在 if/else/for/while/do 语句中必须使用大括号。应避免死循环,超过5000次循环应记录日志。
说明:即使只有一行代码,避免采用单行的编码方式:if (condition) statements;
- 在高并发场景中,避免使用“等于”判断作为中断或退出的条件。
说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。
反例:判断剩余数量等于 0 时,终止发放报告,但因为并发处理错误导致报告数量瞬间变成了负数,这样的话,活动无法终止。
# 3.8、注释规约
- 类、类属性、类方法的注释必须使用 Javadoc 规范,使用 /**内容*/ 格式,不得使用// xxx 方式。
说明:在 IDE 编辑窗口中,Javadoc 方式会提示相关注释,生成 Javadoc 可以正确输出相应注释;在 IDE 中,工程调用方法时,不进入方法即可悬浮提示方法、参数、返回值的意义,提高阅读效率。
- 所有的抽象方法(包括接口中的方法)必须要用 Javadoc 注释、除了返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
说明:对子类的实现要求,或者调用注意事项,请一并说明。
- 所有的类都必须添加创建者和创建日期。
- 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释使用/* */注释,注意与代码对齐。
- 所有的枚举类型字段必须要有注释,说明每个数据项的用途。
- 方法中,每个逻辑代码块应当有注释说明代码块的作用。
# 3.9、入参校验
- 使用javabean接收入参,要校验的bean必须加注解@Valid。
正例:
public Result monitorData(@RequestBody @Valid MonitorClientData clientData) {}
- 接收入参的bean中,增加校验注解。
正例:
@NotBlank(message = "发送端编号不能为空")
@Size(max = 32,message = "发送端编号超过最大长度限制")
private String clientCode;
- 常用的校验注解
| 注解 | 类型 | 说明 |
|---|---|---|
| @NotNull | 任何类型 | 属性不能为null |
| @NotEmpty | 集合 | 集合不能为null,且size大于0 |
| @NotBlanck | 字符串、字符 | 字符类不能为null,且去掉空格之后长度大于0 |
| @AssertTrue | Boolean、boolean | 布尔属性必须是true |
| @Min | 数字类型(原子和包装) | 限定数字的最小值(整型) |
| @Max | 同@Min | 限定数字的最大值(整型) |
| @DecimalMin | 同@Min | 限定数字的最小值(字符串,可以是小数) |
| @DecimalMax | 同@Min | 限定数字的最大值(字符串,可以是小数) |
| @Range | 数字类型(原子和包装) | 限定数字范围(长整型) |
| @Length | 字符串 | 限定字符串长度 |
| @Size | 集合 | 限定集合大小 |
| @Past | 时间、日期 | 必须是一个过去的时间或日期 |
| @Future | 时期、时间 | 必须是一个未来的时间或日期 |
| 字符串 | 必须是一个邮箱格式 | |
| @Pattern | 字符串、字符 | 正则匹配字符串 |
# 3.10、其他
- 获取当前毫秒数 System.currentTimeMillis(); 而不是 new Date().getTime();
- 日期格式化时,传入 pattern 中表示年份统一使用小写的 y。
说明:日期格式化时,yyyy表示当天所在的年,而大写的YYYY代表是week in which year,意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的YYYY就是下一年。另外需要注意:
* 表示月份是大写的 M
* 表示分钟则是小写的 m
* 24 小时制的是大写的 H
* 12 小时制的则是小写的 h
正例:
// 表示日期和时间的格式如下所示
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
# 4、异常日志
# 4.1、异常处理
- Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catch NumberFormatException 来实现。
正例:
if (obj != null)
{
// ...
}
反例:
try {
obj.method();
} catch (NullPointerException e) {
// ...
}
- 异常不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
- catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。
对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。
说明:对大段代码进行 try-catch,使程序无法根据不同的异常做出正确的应激反应,也不利于定位问题,这是一种不负责任的表现。
正例:用户注册的场景中,如果用户输入非法字符,或用户名称已存在,或用户输入密码过于简单,在程序上作出分门别类的判断,并提示给用户。
- 捕获异常是为了处理它,不要捕获了却什么都不处理而抛弃之,如果不想处理它,请将该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可以理解的内容。
- 有 try 块放到了事务代码中,catch 异常后,如果需要回滚事务,一定要注意手动回滚事务。
- finally 块必须对资源对象、流对象进行关闭,有异常也要做 try-catch。
说明:如果 JDK7 及以上,可以使用 try-with-resources 方式。
- 不要在 finally 块中使用 return。
说明:try 块中的 return 语句执行成功后,并不马上返回,而是继续执行 finally 块中的语句,如果此处存在 return 语句,则在此直接返回,无情丢弃掉 try 块中的返回点。
- 捕获异常与抛异常,必须是完全匹配,或者捕获异常是抛异常的父类。
说明:如果预期对方抛的是绣球,实际接到的是铅球,就会产生意外情况。
- 在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable 类来进行拦截。
说明:通过反射机制来调用方法,如果找不到方法,抛出 NoSuchMethodException。什么情况会抛出NoSuchMethodError 呢?
二方包在类冲突时,仲裁机制可能导致引入非预期的版本使类的方法签名不匹配,或者在字节码修改框架(比如:ASM)动态创建或修改类时,修改了相应的方法签名。
这些情况,即使代码编译期是正确的,但在代码运行期时,会抛出 NoSuchMethodError。
- 中台定义了三类异常,BusinessException、AuthenticationException、ForbiddenException,分别用于抛出业务异常、认证异常、权限异常。
# 4.2、日志规约(文件)
- 所有日志文件至少保存 15 天,并以天为单位进行文件分割(性能异常优化调试阶段应按1小时进行文件分割)。
- 在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append()方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
- 避免重复打印日志,浪费磁盘空间。
- 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。
正例:logger.error(各类参数或者对象 toString() + "_" + e.getMessage(), e);
- 谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。
# 4.3、日志规约(入库)
- 即用户业务执行产生的操作日志,保存到数据库中,类型分为:执行日志、错误日志、登录日志、修改日志(保留备用)。
- 每个数据库都应该包含四类日志表,执行日志至少保存5天,错误日志至少保存15天。
- 日志由中台提供的架构自行捕捉,不需要重写或每个业务自定义入库。
- 日志表结构由中台发布,禁止开发者私改数据结构,影响日志采集。
# 5、数据结构
# 5.1、基础权限
基础权限层级包括: 企业、部门、个人。
- 分别对应数据表字段:unit_id、management_id、creator_id(个人id,只要用于中台)、creator_code(个人ucCode,用于第三方接入应用)。
- 分别对应实体类字段:unitId、managementId、creatorId、creatorCode。
基础权限层级冗余字段包括:企业名称、部门名称、个人名称。
- 分别对应数据表字段:unit_name、management_name、creator_name。
- 分别对应实体类字段:unitName、managementName、creatorName。
- 数据结构拥有完整的基础权限属性时,应有以下字段应有以下字段(建议使用):
| 字段名 | 实体属性 | 类型(长度) |
|---|---|---|
| unit_id | unitId | varchar(32) |
| unit_name | unitName | varchar(100) |
| management_id | managementId | varchar(32) |
| management_name | managementName | varchar(100) |
| creator_id | creatorId | varchar(32) |
| creator_name | creatorName | varchar(50) |
- 或:
| 字段名 | 实体属性 | 类型(长度) |
|---|---|---|
| unit_id | unitId | varchar(32) |
| unit_name | unitName | varchar(100) |
| management_id | managementId | varchar(32) |
| management_name | managementName | varchar(100) |
| creator_code | creatorCode | varchar(32) |
| creator_name | creatorName | varchar(50) |
# 5.2、数据溯源
数据溯源字段包括:创建时间、修改时间、修改人。
- 分别对应数据表字段:create_time、update_time、update_person_id(个人id,只要用于中台)、update_person_code个人ucCode,用于第三方接入应用。
- 分别对应实体类属性:createTime、updateTime、updatePersonId、updatePersonCode
数据溯源冗余字段包括:修改人名称。
- 分别对应数据表字段:update_person_name。
- 分别对应实体类属性:updatePersonName。
- 数据结构拥有完整的数据溯源属性时,应有以下字段(建议使用):
| 字段名 | 实体属性 | 类型(长度) |
|---|---|---|
| create_time | createTime | datetime |
| update_time | updateTime | datetime |
| update_person_id | updatePersonId | varchar(32) |
| update_person_name | updatePersonName | varchar(50) |
- 或:
| 字段名 | 实体属性 | 类型(长度) |
|---|---|---|
| create_time | createTime | datetime |
| update_time | updateTime | datetime |
| update_person_code | updatePersonCode | varchar(32) |
| update_person_name | updatePersonName | varchar(50) |
# 5.3、逻辑删除
重要且需要长期存档的数据,应道使用逻辑删除。
- 逻辑删除对应数据表字段:invalid。
- 逻辑删除对应实体类属性:invalid。
- 字段值为0时,未删除数据,字段值为1时,已删除数据。在业务逻辑上不可读写。
- 数据结构拥有完整的逻辑删除属性时,应有以下字段:
| 字段名 | 实体属性 | 类型(长度) |
|---|---|---|
| invalid | invalid | int(1) |
注意:使用mybatis-plus时,逻辑删除字段需要手动加“@TableLogic”注解。
# 6、工具使用
- 禁止使用spring等第三方框架提供的工具类和工具方法,减少代码对第三方框架的依赖。
- 推崇使用jdk原生工具,原生工具使用麻烦者,如只限于当前应用公共方法,应封装应用级工具,如是基础公共方法,由中台封装到hhh-util工具包中,再进行使用。
- 阅读“接口建协云说明>>后端 (opens new window)”了解hhh-util封装的工具。
- hhh-util必须脱离依赖第三方框架,不应引用第三方框架。
- 如工具类需再融入第三方框架,应当新建项目引用hhh-util再进行封装,保证工具包纯净和可复用性。
# 7、Api规约
- Api请求数据类型为application/json,出入均为json数据。
- Api返回结果应为TableResult和Result类。TableResult是分页数据结构类,Result是基础数据结构类。
- Result固有属性如下:
| 属性名 | 类型 | 说明 |
|---|---|---|
| code | int | 状态码 |
| status | int | 状态码,为兼容旧前端产品 |
| msg | String | 结果说明 |
| data | T | 结果数据 |
- TableResult继承Result类,除包含Result属性,自定义属性如下:
| 属性名 | 类型 | 说明 |
|---|---|---|
| count | long | 总行数 |
- 约定状态码:
| 错误码 | 错误说明 | 排查方法 |
|---|---|---|
| 0 | 请求成功 | - |
| 1 | 业务错误 | 检查传入业务数据 |
| 2 | 认证错误 | 验证token是否正确 |
| 3 | 系统错误 | 系统内部错误,联系管理员查看原因 |
| 4 | 权限不足 | 联系管理员授予用户权限 |
| 10 | 参数不能为空 | 检查必要参数 |
| 11 | 数据已存在(账号、手机号、统一信用代码等) | 检查账号、手机、统一社会信用代码唯一性 |
| 12 | 数据已经不存在 | 检查传入数据 |
| 13 | 数据格式不正确 | 检查数据格式 |
| 14 | 存在关联数据 | - |
| 99 | 未知错误 | 系统出现未知错误,联系管理员查看原因 |
| 10010 | 频繁访问限制 | - |
| 10011 | IP禁用限制 | 当前IP已被禁用 |
| 10013 | 错误次数上限 | - |
| 10020 | 数字签名不匹配 | 检查凭证正确性 |
# 8、安全规约
- 隶属于用户个人的页面或者功能必须进行权限控制校验。
说明:防止没有做权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容、修改他人的订单。
- 用户敏感数据禁止直接展示,必须对展示数据进行脱敏。
说明:中国大陆个人手机号码显示为:137****0969,隐藏中间 4 位;身份证隐藏第9开始的6位,防止隐私泄露。
- 用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库。
- 用户请求传入的任何参数必须做有效性验证。
说明一:忽略参数校验可能导致:
* page size 过大导致内存溢出
* 恶意 order by 导致数据库慢查询
* 任意重定向
* SQL 注入
* 反序列化注入
* 正则输入源串拒绝服务 ReDoS
说明二:Java 代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的结果。
- 禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据。
- 表单、AJAX 提交应执行 CSRF 安全验证。
说明:CSRF(Cross-site request forgery)跨站请求伪造是一类常见编程漏洞。对于存在 CSRF 漏洞的应用/网站,攻击者可以事先构造好 URL,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。
# 附录
# 附1 历史版本
| 序号 | 版本 | 变更描述 |
|---|---|---|
| 1 | v1.0 | 拟稿 |
# 附2 专有名词解释
- POJO(Plain Ordinary Java Object): 在本规范中,POJO 专指有 setter / getter / toString 的简单类,包括 DO/DTO/BO/VO 等。
- OOP(Object Oriented Programming): 本规范泛指类、对象的编程处理方式。
- OOM(Out Of Memory): 源于 java.lang.OutOfMemoryError,当 JVM 没有足够的内存来为对象分配空间并且垃圾回收器也无法回收空间时,系统出现的严重状况。回收空间时,系统出现的严重状况。