java实战开发|阿里巴巴《Java开发手册(黄山版)》阅读笔记

前言

书籍开源地址:https://github.com/alibaba/p3c


本文主要节选一些部分开发中一些自己没有注意到的点,会选择性的舍去一些已经养成的好的编码习惯及一些阿里企业特性比较强的规范。
一、编码风格 (一)命名风格 (二)常量定义 1.【强制】不允许任何魔法值(即未经预先定义的常量)直接出现在代码中。
反例:
// 开发者 A 定义了缓存的 key。 String key = "Id#taobao_" + tradeId; cache.put(key, value); // 开发者 B 使用缓存时直接复制少了下划线,即 key 是"Id#taobao" + tradeId,导致出现故障。 String key = "Id#taobao" + tradeId; cache.get(key);

2.【强制】long 或 Long 赋值时,数值后使用大写 L,不能是小写 l,小写容易跟数字混淆,造成误解。
说明:public static final Long NUM = 2l; 写的是数字的 21,还是 Long 型的 2?
3.【强制】浮点数类型的数值后缀统一为大写的 D 或 F。
正例:
public static final double HEIGHT = 175.5D; public static final float WEIGHT = 150.3F;

(三)代码格式 (四)OOP规约 7.【强制】所有整型包装类对象之间值的比较,全部使用 equals 方法比较。
说明:对于 Integer var = ? 在 -128 至 127 之间的赋值,Integer 对象是在 IntegerCache.cache 产生,会复用已有对象,这个区间内的 Integer 值可以直接使用 == 进行判断,但是这个区间之外的所有数据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。
8.【强制】任何货币金额,均以最小货币单位且为整型类型进行存储。
9.【强制】浮点数之间的等值判断,基本数据类型不能使用 == 进行比较,包装数据类型不能使用 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 }

正例:
(1)指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。
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"); }

(2)使用 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.compareTo(y) == 0) { System.out.println("true"); }

10.【强制】BigDecimal 的等值比较应使用 compareTo() 方法,而不是 equals() 方法。
说明:equals() 方法会比较值和精度(1.0 与 1.00 返回结果为 false),而 compareTo() 则会忽略精度。
12.【强制】禁止使用构造方法 BigDecimal(double) 的方式把 double 值转化为 BigDecimal 对象。
说明:BigDecimal(double) 存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:BigDecimal g = new BigDecimal(0.1F);实际的存储值为:0.100000001490116119384765625
正例:
优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了 Double 的toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。
BigDecimal recommend1 = new BigDecimal(“0.1”);
BigDecimal recommend2 = BigDecimal.valueOf(0.1);
17.【强制】POJO 类必须写 toString 方法。使用 IDE 中的工具 source > generate toString 时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString()。
说明:在方法执行抛出异常时,可以直接调用 POJO 的 toString() 方法打印其属性值,便于排查问题。
18.【强制】禁止在 POJO 类中,同时存在对应属性 xxx 的 isXxx() 和 getXxx() 方法。
说明:框架在调用属性 xxx 的提取方法时,并不能确定哪个方法一定是被优先调用到,神坑之一。
23.【推荐】循环体内,字符串的连接方式,使用 StringBuilder 的 append 方法进行扩展。
反例:
String str = "start"; for (int i = 0; i < 100; i++) { str = str + "hello"; }

说明:反编译出的字节码文件显示每次循环都会 new 出一个 StringBuilder 对象,然后进行 append 操作,最后通过toString() 返回 String 对象,造成内存资源浪费。
(五)日期时间 1.【强制】日期格式化时,传入 pattern 中表示年份统一使用小写的 y。
说明:日期格式化时,yyyy 表示当天所在的年,而大写的 YYYY 代表是 week in which year(JDK7 之后引入的概念),意思是当天所在的周属于的年份,一周从周日开始,周六结束,只要本周跨年,返回的 YYYY 就是下一年。
正例:
表示日期和时间的格式如下所示:
new SimpleDateFormat(“yyyy-MM-dd HH:mm:ss”)
反例:
某程序员因使用 YYYY/MM/dd 进行日期格式化,2017/12/31 执行结果为 2018/12/31,造成线上故障。
2.【强制】在日期格式中分清楚大写的 M 和小写的 m,大写的 H 和小写的 h 分别指代的意义。
说明:日期格式中的这两对字母表意如下:
1)表示月份是大写的 M
2)表示分钟则是小写的 m
3)24 小时制的是大写的 H
4)12 小时制的则是小写的 h
3.【强制】获取当前毫秒数:System.currentTimeMillis();而不是 new Date().getTime()
说明:获取纳秒级时间,则使用 System.nanoTime 的方式。在 JDK8 中,针对统计时间等场景,推荐使用 Instant 类。
4.【强制】不允许在程序任何地方中使用:1)java.sql.Date 2)java.sql.Time 3)java.sql.Timestamp。
说明:第 1 个不记录时间,getHours() 抛出异常;第 2 个不记录日期,getYear() 抛出异常;第 3 个在构造方法super((time / 1000) * 1000),在 Timestamp 属性 fastTime 和 nanos 分别存储秒和纳秒信息。
反例:java.util.Date.after(Date) 进行时间比较时,当入参是 java.sql.Timestamp 时,会触发 JDK BUG(JDK9 已修复),可能导致比较时的意外结果。
6.【推荐】避免公历闰年 2 月问题。闰年的 2 月份有 29 天,一年后的那一天不可能是 2 月 29 日。
7.【推荐】使用枚举值来指代月份。如果使用数字,注意 Date,Calendar 等日期相关类的月份 month 取值范围从 0 到 11 之间。
说明:参考 JDK 原生注释,Month value is 0-based. e.g., 0 for January.
正例:Calendar.JANUARY,Calendar.FEBRUARY,Calendar.MARCH 等来指代相应月份来进行传参或比较。
(六)集合处理 2.【强制】判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size() == 0 的方式。
说明:在某些集合中,前者的时间复杂度为 O(1),而且可读性更好。
正例:
Map, Object> map = new HashMap<>(16); if (map.isEmpty()) { System.out.println("no element in this map."); }

3.【强制】在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要使用参数类型为 BinaryOperator,参数名为 mergeFunction 的方法,否则当出现相同 key 时会抛出IllegalStateException 异常。
说明:参数 mergeFunction 的作用是当出现 key 重复时,自定义对 value 的处理策略。
正例:
List> pairArrayList = new ArrayList<>(3); pairArrayList.add(new Pair<>("version", 12.10)); pairArrayList.add(new Pair<>("version", 12.19)); pairArrayList.add(new Pair<>("version", 6.28)); // 生成的 map 集合中只有一个键值对:{version=6.28} Map, Double> map = pairArrayList.stream() .collect(Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));

反例:
Stringl] departments = new String[]{"RDC","RDC","KKB"}; // 抛出 IllegalStateException 异常 Map map = Arrays.stream(departments) .collect(Collectors.toMap(String::hashCode, str -> str));

4.【强制】在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value为 null 时会抛 NPE 异常。
说明:在 java.util.HashMap 的 merge 方法里会进行如下的判断:
if (value =https://www.it610.com/article/= null || remappingFunction == null) throw new NullPointerException();

反例:
List> pairArrayList = new ArrayList<>(2); pairArrayList.add(new Pair<>("version1", 8.3)); pairArrayList.add(new Pair<>("version2", null)); // 抛出 NullPointerException 异常 Map, Double> map = pairArrayList.stream() .collect(Collectors.toMap(Pair::getKey, Pair::getValue, (v1, v2) -> v2));

5.【强制】ArrayList 的 subList 结果不可强转成 ArrayList,否则会抛出 ClassCastException 异常
java.util.RandomAccessSubList cannot be cast to java.util.ArrayList。
说明:subList() 返回的是 ArrayList 的内部类 SubList,并不是 ArrayList 本身,而是 ArrayList 的一个视图,对于SubList 的所有操作最终会反映到原列表上。
6.【强制】使用 Map 的方法 keySet() / values() / entrySet() 返回集合对象时,不可以对其进行添加元素操作,否则会抛出 UnsupportedOperationException 异常。
?
7.【强制】Collections 类返回的对象,如:emptyList() / singletonList() 等都是 immutable list,不可对其进行添加或者删除元素的操作。
反例:如果查询无结果,返回 Collections.emptyList() 空集合对象,调用方一旦在返回的集合中进行了添加元素的操作,就会触发 UnsupportedOperationException 异常。
8.【强制】在 subList 场景中,高度注意对父集合元素的增加或删除,均会导致子列表的遍历、增加、删除产生 ConcurrentModificationException 异常。
说明:抽查表明,90% 的程序员对此知识点都有错误的认知。
9.【强制】使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为0 的空数组。
反例:直接使用 toArray 无参方法存在问题,此方法返回值只能是 Object[]类,若强转其它类型数组将出现ClassCastException 错误。
正例:
List> list = new ArrayList<>(2); list.add("guan"); list.add("bao"); String[] array = list.toArray(new String[0]);

说明:使用 toArray 带参方法,数组空间大小的 length:
1)等于 0,动态创建与 size 相同的数组,性能最好。
2)大于 0 但小于 size,重新创建大小等于 size 的数组,增加 GC 负担。
3)等于 size,在高并发情况下,数组创建完成之后,size 正在变大的情况下,负面影响与 2 相同。
4)大于 size,空间浪费,且在 size 处插入 null 值,存在 NPE 隐患。
10.【强制】使用 Collection 接口任何实现类的 addAll() 方法时,要对输入的集合参数进行 NPE 判断。
说明:在 ArrayList#addAll 方法的第一行代码即 Object[] a = c.toArray();其中 c 为输入集合参数,如果为 null,则直接抛出异常。
11.【强制】使用工具类 Arrays.asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add/ remove / clear 方法会抛出 UnsupportedOperationException 异常。
说明:asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。Arrays.asList 体现的是适配器模式,只是转换接口,后台的数据仍是数组。
Stringl] str = new String[i "yang", "guan", "bao" }; List list = Arrays.asList(str);

第一种情况:list.add(“yangguanbao”); 运行时异常。
第二种情况:str[0] = “change”; list 中的元素也会随之修改,反之亦然。
12.【强制】泛型通配符来接收返回的数据,此写法的泛型集合不能使用 add 方法,而不能使用 get 方法,两者在接口调用赋值的场景中容易出错。
说明:扩展说一下 PECS(Producer Extends Consumer Super) 原则,即频繁往外读取内容的,适合用,经常往里插入的,适合用
13.【强制】在无泛型限制定义的集合赋值给泛型限制的集合时,在使用集合元素时,需要进行instanceof 判断,避免抛出 ClassCastException 异常。
说明:毕竟泛型是在 JDK5 后才出现,考虑到向前兼容,编译器是允许非泛型集合与泛型集合互相赋值。
反例:
List> generics = null; List notGenerics = new ArrayList(10); notGenerics.add(new Object()); notGenerics.add(new Integer(1)); generics = notGenerics; // 此处抛出 ClassCastException 异常 String string = generics.get(0);

14.【强制】不要在 foreach 循环里进行元素的 remove / add 操作。remove 元素请使用 iterator 方式,如果并发操作,需要对 iterator 对象加锁。
正例:
List> list = new ArrayList<>(); list.add("1"); list.add("2"); Iterator> iterator = list.iterator(); while (iterator.hasNext()) { String item = iterator.next(); if (删除元素的条件) { iterator.remove(); } }

反例:
for (String item : list) { if ("1".equals(item)) { list.remove(item); } }

说明:反例中的执行结果肯定会出乎大家的意料,那么试一下把“1”换成“2”会是同样的结果吗?
java实战开发|阿里巴巴《Java开发手册(黄山版)》阅读笔记
文章图片

16.【推荐】泛型集合使用时,在 JDK7 及以上,使用 diamond 语法或全省略。
说明:菱形泛型,即 diamond,直接使用<>来指代前边已经指定的类型。
正例:
// diamond 方式,即<> HashMap, String> userCache = new HashMap<>(16); // 全省略方式 ArrayList users = new ArrayList(10);

17.【推荐】集合初始化时,指定集合初始值大小。
说明:HashMap 使用构造方法 HashMap(int initialCapacity) 进行初始化时,如果暂时无法确定集合大小,那么指定默认值(16)即可。
正例:initialCapacity = (需要存储的元素个数 / 负载因子) + 1。注意负载因子(即 loaderfactor)默认为 0.75,如果暂时无法确定初始值大小,请设置为 16(即默认值)。
反例:HashMap 需要放置 1024 个元素,由于没有设置容量初始大小,随着元素增加而被迫不断扩容,resize() 方法总共会调用 8 次,反复重建哈希表和数据迁移。当放置的集合元素个数达千万级时会影响程序性能。
18.【推荐】使用 entrySet 遍历 Map 类集合 KV,而不是 keySet 方式进行遍历。
说明:keySet 其实是遍历了 2 次,一次是转为 Iterator 对象,另一次是从 hashMap 中取出 key 所对应的value。而 entrySet 只是遍历了一次就把 key 和 value 都放到了 entry 中,效率更高。如果是 JDK8,使用Map.forEach 方法。
正例:values()返回的是 V 值集合,是一个 list 集合对象;keySet()返回的是 K 值集合,是一个 Set 集合对象;entrySet()返回的是 K-V 值组合集合。
示例:
Map, String> map = new HashMap, String>(); map.put("1", "value1"); map.put("2", "value2"); map.put("3", "value3"); for (Map.Entry, String> entry : map.entrySet()) { System.out.println("key= " + entry.getKey() + " and value= "https://www.it610.com/article/+ entry.getValue()); }

19.【推荐】高度注意 Map 类集合 K/V 能不能存储 null 值的情况,如下表格:
java实战开发|阿里巴巴《Java开发手册(黄山版)》阅读笔记
文章图片

反例:由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,而事实上,存储null 值时会抛出 NPE 异常。
21.【参考】利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的contains()进行遍历去重或者判断包含操作。
(七)并发处理 1. 【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
说明:资源驱动类、工具类、单例工厂类都需要注意。
2.【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
正例:
自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeatureOfGroup:
public class UserThreadFactory implements ThreadFactory { private final String namePrefix; private final AtomicInteger nextId = new AtomicInteger(1); // 定义线程组名称,在利用 jstack 来排查问题时,非常有帮助 UserThreadFactory(String whatFeatureOfGroup) { namePrefix = "FromUserThreadFactory's" + whatFeatureOfGroup + "-Worker-"; } @Override public Thread newThread(Runnable task) { String name = namePrefix + nextId.getAndIncrement(); Thread thread = new Thread(null, task, name, 0, false); System.out.println(thread.getName()); return thread; } }

3.【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程
说明:线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
4.【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
3)ScheduledThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
5.【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:
private static final ThreadLocal dateStyle = new ThreadLocal() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd"); } };

说明:如果是 JDK8 的应用,可以使用 Instant 代替 Date,LocalDateTime 代替 Calendar,DateTimeFormatter 代替SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe。
6.【强制】必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally 块进行回收。
正例:
objectThreadLocal.set(userInfo); try { // ... } finally { objectThreadLocal.remove(); }

7.【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明:尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用 RPC 方法。
8.【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
说明:线程一需要对表 A、B、C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A、B、C,否则可能出现死锁。
9.【强制】在使用阻塞等待获取锁的方式中,必须在 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(); }

10.【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
说明:Lock 对象的 unlock 方法在执行时,它会调用 AQS 的 tryRelease 方法(取决于具体实现类),如果当前线程不持有锁,则抛出 IllegalMonitorStateException 异常。
正例:
Lock lock = new XxxLock(); // ... boolean isLocked = lock.tryLock(); if (isLocked) { try { doSomething(); doOthers(); } finally { lock.unlock(); } }

11.【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用 version 作为更新依据。
说明:如果每次访问冲突概率小于 20%,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于 3 次
12.【强制】多线程并行处理定时任务时,Timer 运行多个 TimeTask 时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。
13.【推荐】资金相关的金融敏感信息,使用悲观锁策略。
说明:乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。
正例:悲观锁遵循一锁二判三更新四释放的原则。
14.【推荐】使用 CountDownLatch 进行异步转同步操作,每个线程退出前必须调用 countDown 方法,线程执行代码注意 catch 异常,确保 countDown 方法被执行到,避免主线程无法执行至 await 方法,直到超时才返回结果。
说明:注意,子线程抛出异常堆栈,不能在主线程 try-catch 到。
15.【推荐】避免 Random 实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一 seed 导致的性能下降。
说明:Random 实例包括 java.util.Random 的实例或者 Math.random() 的方式。
正例:在 JDK7 之后,可以直接使用 API ThreadLocalRandom,而在 JDK7 之前,需要编码保证每个线程持有一个单独的 Random 实例。
16.【推荐】通过双重检查锁(double-checked locking),实现延迟初始化需要将目标属性声明为volatile 型,(比如修改 helper 的属性声明为 private volatile Helper helper = null; )
正例:
public class LazyInitDemo { private volatile Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized(this) { if (helper == null) { helper = new Helper(); } } } return helper; } // other methods and fields... }

17.【参考】volatile 解决多线程内存不可见问题对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题
说明:如果是 count++操作,使用如下类实现:
AtomicInteger count = new AtomicInteger(); count.addAndGet(1);

如果是 JDK8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好(减少乐观锁的重试次数)。
18.【参考】HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中注意规避此风险。
19.【参考】ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。
(八)控制语句 1.【强制】在一个 switch 块内,每个 case 要么通过 continue / break / return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止;在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。
说明:注意 break 是退出 switch 语句块,而 return 是退出方法体
2.【强制】当 switch 括号内的变量类型为 String 并且此变量为外部参数时,必须先进行 null 判断
反例:如下的代码输出是什么?
public class SwitchString { public static void main(String[] args) { method(null); } public static void method(String param) { switch (param) { // 肯定不是进入这里 case "sth": System.out.println("it's sth"); break; // 也不是进入这里 case "null": System.out.println("it's null"); break; // 也不是进入这里 default: System.out.println("default"); } } }

3.【强制】在 if / else / for / while / do 语句中必须使用大括号。
反例: if (condition) statements;
说明:即使只有一行代码,也要采用大括号的编码方式
4.【强制】三目运算符 condition ? 表达式 1:表达式 2 中,高度注意表达式 1 和 2 在类型对齐时,可能抛出因自动拆箱导致的 NPE 异常。
说明:以下两种场景会触发类型对齐的拆箱操作:
1)表达式 1 或 表达式 2 的值只要有一个是原始类型。
2)表达式 1 或 表达式 2 的值的类型不一致,会强制拆箱升级成表示范围更大的那个类型。
反例:
Integer a = 1; Integer b = 2; Integer c = null; Boolean flag = false; // a*b 的结果是 int 类型,那么 c 会强制拆箱成 int 类型,抛出 NPE 异常 Integer result = (flag ? a * b : c);

5.【强制】在高并发场景中,避免使用“等于”判断作为中断或退出的条件。
说明:如果并发控制没有处理好,容易产生等值判断被“击穿”的情况,使用大于或小于的区间判断条件来代替。
反例:判断剩余奖品数量等于 0 时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。
9.【推荐】不要在其它表达式(尤其是条件表达式)中,插入赋值语句。
说明:赋值点类似于人体的穴位,对于代码的理解至关重要,所以赋值语句需要清晰地单独成为一行。
反例:
public Lock getLock(boolean fair) { // 算术表达式中出现赋值操作,容易忽略 count 值已经被改变 threshold = (count = Integer.MAX_VALUE) - 1; // 条件表达式中出现赋值操作,容易误认为是 sync == fair return (sync = fair) ? new FairSync() : new NonfairSync(); }

10.【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try-catch 操作(这个 try-catch 是否可以移至循环体外)
12.【推荐】公开接口需要进行入参保护,尤其是批量操作的接口。
反例:某业务系统,提供一个用户批量查询的接口,API 文档上有说最多查多少个,但接口实现上没做任何保护,导致调用方传了一个 1000 的用户 id 数组过来后,查询信息后,内存爆了。
13.【参考】下列情形,需要进行参数校验
1)调用频次低的方法。
2)执行时间开销很大的方法。此情形中,参数校验时间几乎可以忽略不计,但如果因为参数错误导致中间执行回
退,或者错误,那得不偿失。
3)需要极高稳定性和可用性的方法。
4)对外提供的开放接口,不管是 RPC / API / HTTP 接口。
5)敏感权限入口。
14.【参考】下列情形,不需要进行参数校验:
1)极有可能被循环调用的方法。但在方法说明里必须注明外部参数检查。
2)底层调用频度比较高的方法。毕竟是像纯净水过滤的最后一道,参数错误不太可能到底层才会暴露问题。一般 DAO
层与 Service 层都在同一个应用中,部署在同一台服务器中,所以 DAO 的参数校验,可以省略。
3)被声明成 private 只会被自己代码所调用的方法,如果能够确定调用方法的代码传入参数已经做过检查或者肯定不
会有问题,此时可以不校验参数。
(九)注释规约 11.【参考】好的命名、代码结构是自解释的,注释力求精简准确、表达到位。避免出现注释的另一个极端:过多过滥的注释,代码的逻辑一旦修改,修改注释又是相当大的负担。
反例:
// put elephant into fridge put(elephant, fridge);

方法名 put,加上两个有意义的变量名称 elephant 和 fridge,已经说明了这是在干什么,语义清晰的代码不需要额外的注释。
(十)前后端规约 2.【强制】前后端数据列表相关的接口返回,如果为空,则返回空数组[]或空集合{}。
说明:此条约定有利于数据层面上的协作更加高效,减少前端很多琐碎的 null 判断。
7.【强制】HTTP 请求通过 URL 传递参数时,不能超过 2048 字节。
说明:不同浏览器对于 URL 的最大长度限制略有不同,并且对超出最大长度的处理逻辑也有差异,2048 字节是取所有浏览器的最小值。
反例:某业务将退货的商品 id 列表放在 URL 中作为参数传递,当一次退货商品数量过多时,URL 参数超长,传递到后端的参数被截断,导致部分商品未能正确退货。
8.【强制】HTTP 请求通过 body 传递内容时,必须控制长度,超出最大长度后,后端解析会出错
说明:nginx 默认限制是 1MB,tomcat 默认限制为 2MB,当确实有业务需要传较大内容时,可以调大服务器端的限制。
9.【强制】在翻页场景中,用户输入参数的小于 1,则前端返回第一页参数给后端;后端发现用户输入的参数大于总页数,直接返回最后一页。3
10.【强制】服务器内部重定向必须使用 forward;外部重定向地址必须使用 URL 统一代理模块生成,否则会因线上采用 HTTPS 协议而导致浏览器提示“不安全”,并且还会带来 URL 维护不一致的问题。
14.【参考】在接口路径中不要加入版本号,版本控制在 HTTP 头信息中体现,有利于向前兼容。
说明:当用户在低版本与高版本之间反复切换工作时,会导致迁移复杂度升高,存在数据错乱风险。
(十一)其他 1.【强制】在使用正则表达式时,利用好其预编译功能,可以有效加快正则匹配速度
说明:不要在方法体内定义:Pattern pattern = Pattern.compile(“规则”);
2.【强制】避免用 ApacheBeanutils 进行属性的 copy
说明:ApacheBeanUtils 性能较差,可以使用其他方案比如 SpringBeanUtils,CglibBeanCopier,注意均是浅拷贝。
3.【强制】velocity 调用 POJO 类的属性时,直接使用属性名取值即可,模板引擎会自动按规范调用 POJO的 getXxx(),如果是 boolean 基本数据类型变量(boolean 命名不需要加 is 前缀),会自动调 isXxx()方法。
说明:注意如果是 Boolean 包装类对象,优先调用 getXxx() 的方法。
6.【强制】枚举 enum(括号内)的属性字段必须是私有且不可变。
二、异常日志 (一)错误码 (二)异常处理 1.【强制】Java 类库中定义的可以通过预检查方式规避的 RuntimeException 异常不应该通过 catch 的方式来处理,比如:NullPointerException,IndexOutOfBoundsException 等等。
说明:无法通过预检查的异常除外,比如,在解析字符串形式的数字时,可能存在数字格式错误,不得不通过 catchNumberFormatException 来实现。
正例:if (obj != null) {…}
反例:try { obj.method(); } catch (NullPointerException e) {…}
2.【强制】异常捕获后不要用来做流程控制,条件控制。
说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
5.【强制】事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务
10.【推荐】方法的返回值可以为 null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回 null 值。
说明:本规约明确防止 NPE 是调用者的责任。即使被调用方法返回空集合或者空对象,对调用者来说,也并非高枕无忧,必须考虑到远程调用失败,运行时异常等场景返回 null 的情况。
11.【推荐】防止 NPE,是程序员的基本修养,注意 NPE 产生的场景
1)返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE
反例:public int method() { return Integer 对象; },如果为 null,自动解箱抛 NPE。
2)数据库的查询结果可能为 null。
3)集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4)远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5)对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6)级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
(三)日志规约 2.【强制】日志文件至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。对于当天日志,以“应用名.log”来保存,保存在/{统一目录}/{应用名}/logs/目录下,过往日志格式为
{logname}.log.{保存日期},日期格式:yyyy-MM-dd
正例:以 mppserver 应用为例,日志保存/home/admin/mppserver/logs/mppserver.log,历史日志名称为 mppserver.log.2021-11-28
3.【强制】根据国家法律,网络运行状态、网络安全事件、个人敏感信息操作等相关记录,留存的日志不少于六个月,并且进行网络多机备份。
5.【强制】在日志输出时,字符串变量之间的拼接使用占位符的方式。
说明:因为 String 字符串的拼接会使用 StringBuilder 的 append() 方式,有一定的性能损耗。使用占位符仅是替换动作,可以有效提升性能。
正例:logger.debug(“Processing trade with id : {} and symbol : {}”, id, symbol);
6.【强制】对于 trace / debug / info 级别的日志输出,必须进行日志级别的开关判断:
说明:虽然在 debug(参数) 的方法体内第一行代码 isDisabled(Level.DEBUG_INT) 为真时(Slf4j 的常见实现 Log4j 和Logback),就直接 return,但是参数可能会进行字符串拼接运算。此外,如果 debug(getName()) 这种参数内有
getName() 方法调用,无谓浪费方法调用的开销。
正例:
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志 if (logger.isDebugEnabled()) { logger.debug("Current ID is: {} and name is: {}", id, getName()); }

7.【强制】避免重复打印日志,浪费磁盘空间,务必在日志配置文件中设置 additivity=false
正例:
8.【强制】生产环境禁止使用 System.out 或 System.err 输出或使用 e.printStackTrace() 打印异常堆栈。
说明:标准日志输出与标准错误输出文件每次 Jboss 重启时才滚动,如果大量输出送往这两个文件,容易造成文件大小超过操作系统大小限制。
9.【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字throws 往上抛出。
正例:
logger.error("inputParams: {} and errorMessage: {}", 各类参数或者对象 toString(), e.getMessage(), e);

10.【强制】日志打印时禁止直接用 JSON 工具将对象转换成 String。
说明:如果对象里某些 get 方法被覆写,存在抛出异常的情况,则可能会因为打印日志而影响正常业务流程的执行。
正例:打印日志时仅打印出业务相关属性值或者调用其对象的 toString() 方法。
11.【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志;有选择地输出 info 日志;如果使用 warn来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。
说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
12.【推荐】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。如非必要,请不要在此场景打出 error 级别,避免频繁报警。
说明:注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。
14.【推荐】为了保护用户隐私,日志文件中的用户敏感信息需要进行脱敏处理。
说明:日志排查问题时,推荐使用订单号、UUID 之类的唯一编号进行查询。
三、单元测试 四、安全规约 1.【强制】隶属于用户个人的页面或者功能必须进行权限控制校验
说明:防止没有做水平权限校验就可随意访问、修改、删除别人的数据,比如查看他人的私信内容
2.【强制】用户敏感数据禁止直接展示,必须对展示数据进行脱敏。
说明:中国大陆个人手机号码显示:139****1219,隐藏中间 4 位,防止隐私泄露。
3.【强制】用户输入的 SQL 参数严格使用参数绑定或者 METADATA 字段值限定,防止 SQL 注入,禁止字符串拼接 SQL 访问数据库。
反例:某系统签名大量被恶意修改,即是因为对于危险字符#–没有进行转义,导致数据库更新时,where 后边的信息被注释掉,对全库进行更新。
4.【强制】用户请求传入的任何参数必须做有效性验证。
说明:忽略参数校验可能导致:
  • 页面 page size 过大导致内存溢出
  • 恶意 order by 导致数据库慢查询
  • 缓存击穿
  • SSRF
  • 任意重定向
  • SQL 注入,Shell 注入,反序列化注入
  • 正则输入源串拒绝服务 ReDoS
扩展:Java 代码用正则来验证客户端的输入,有些正则写法验证普通用户输入没有问题,但是如果攻击人员使用的是特殊构造的字符串来验证,有可能导致死循环的结果。
5.【强制】禁止向 HTML 页面输出未经安全过滤或未正确转义的用户数据。
说明:XSS 跨站脚本攻击。它指的是恶意攻击者往 Web 页面里插入恶意 html 代码,当用户浏览时,嵌入其中 Web 里面的 html 代码会被执行,造成获取用户 cookie、钓鱼、获取用户页面数据、蠕虫、挂马等危害。
6.【强制】表单、AJAX 提交必须执行 CSRF 安全验证。
【java实战开发|阿里巴巴《Java开发手册(黄山版)》阅读笔记】说明:CSRF (Cross-site request forgery) 跨站请求伪造是一类常见编程漏洞。对于存在 CSRF 漏洞的应用/网站,攻击者可以事先构造好 URL,只要受害者用户一访问,后台便在用户不知情的情况下对数据库中用户参数进行相应修改。
7.【强制】URL 外部重定向传入的目标地址必须执行白名单过滤。
说明:攻击者通过恶意构造跳转的链接,可以向受害者发起钓鱼攻击。
8.【强制】在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。
说明:如注册时发送验证码到手机,如果没有限制次数和频率,那么可以利用此功能骚扰到其它用户,并造成短信平台资源浪费。
9.【强制】对于文件上传功能,需要对于文件大小、类型进行严格检查和控制。
说明:攻击者可以利用上传漏洞,上传恶意文件到服务器,并且远程执行,达到控制网站服务器的目的。
10.【强制】配置文件中的密码需要加密。
11.【推荐】发贴、评论、发送等即时消息,需要用户输入内容的场景。必须实现防刷、内容违禁词过滤等风控策略。
五、MySQL数据库 (—)建表规约 2.【强制】表名、字段名必须使用小写字母或数字,禁止出现数字开头禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。
正例:aliyun_admin,rdc_config,level3_name
反例:AliyunAdmin,rdcConfig,level_3_name
3.【强制】表名不使用复数名词。
说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。
4.【强制】禁用保留字,如 desc、range、match、delayed 等,请参考 MySQL 官方保留字。
5.【强制】主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。
说明:pk_即 primary key;uk_即 unique key;idx_即 index 的简称。
6.【强制】小数类型为 decimal,禁止使用 float 和 double。
说明:在存储的时候,float 和 double 都存在精度损失的问题,很可能在比较值的时候,得到不正确的结果。如果存储的数据范围超过 decimal 的范围,建议将数据拆成整数和小数并分开存储。
7.【强制】如果存储的字符串长度几乎相等,使用 char 定长字符串类型。
8.【强制】varchar 是可变长字符串,不预先分配存储空间,长度不要超过 5000,如果存储长度大于此值,定义字段类型为 text,独立出来一张表,用主键来对应,避免影响其它字段索引率。
9.【强制】表必备三字段:id,create_time,update_time。
说明:其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。create_time,update_time 的类型均为datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。
10.【强制】在数据库中不能使用物理删除操作,要使用逻辑删除。
说明:逻辑删除在数据删除后可以追溯到行为操作。不过会使得一些情况下的唯一主键变得不唯一,需要根据情况来酌情解决。
14.【推荐】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循
1)不是频繁修改的字段。
2)不是唯一索引的字段。
3)不是 varchar 超长字段,更不能是 text 字段。
正例:各业务线经常冗余存储商品名称,避免查询时需要调用 IC 服务获取。
15.【推荐】单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
说明:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。
16.【参考】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度
正例:无符号值可以避免误存负数,且扩大了表示范围:
java实战开发|阿里巴巴《Java开发手册(黄山版)》阅读笔记
文章图片

(二)索引规约 1.【强制】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。
说明:不要以为唯一索引影响了 insert 速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
2.【强制】超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。
说明:即使双表 join 也要注意表索引、SQL 性能。
3.【强制】在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
说明:索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为 20 的索引,区分度会高达 90%以上,可以使用 count(distinct left(列名,索引长度)) / count(*) 的区分度来确定。
4.【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
5.【推荐】如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 filesort 的情况,影响查询性能。
正例:where a = ? and b = ? order by c;索引:a_b_c
反例:索引如果存在范围查询,那么索引有序性无法利用,如:WHERE a > 10 ORDER BY b;索引 a_b 无法排序。
6.【推荐】利用覆盖索引来进行查询操作,避免回表。
说明:如果一本书需要知道第 11 章是什么标题,会翻开第 11 章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。
正例:能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效果,用 explain的结果,extra 列会出现:using index。
7.【推荐】利用延迟关联或者子查询优化超多分页场景
说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。
正例:先快速定位需要获取的 id 段,然后再关联:SELECT t1.* FROM 表 1 as t1 , (select id from 表 1 where 条件 LIMIT 100000 , 20) as t2 where t1.id = t2.id
8.【推荐】SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 const 最好。
说明:
1)consts 单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
2)ref 指的是使用普通的索引(normal index)。
3)range 对索引进行范围检索。
反例:explain 表的结果,type = index,索引物理文件全扫描,速度非常慢,这个 index 级别比较 range 还低,与全表扫描是小巫见大巫。
9.【推荐】建组合索引的时候,区分度最高的在最左边。
正例:如果 where a = ? and b = ?,a 列的几乎接近于唯一值,那么只需要单建 idx_a 索引即可。
说明:存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where c > ? and d = ? 那么即使c 的区分度更高,也必须把 d 放在索引的最前列,即建立组合索引 idx_d_c。
10.【推荐】防止因字段类型不同造成的隐式转换,导致索引失效。
11.【参考】创建索引时避免有如下极端误解:
1)索引宁滥勿缺。认为一个查询就需要建一个索引。
2)吝啬索引的创建。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度。
3)抵制唯一索引。认为唯一索引一律需要在应用层通过“先查后插”方式解决。
(三)SQL语句 1.【强制】不要使用 count(列名) 或 count(常量) 来替代 count(),count() 是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。
说明:count(*) 会统计值为 NULL 的行,而 count(列名) 不会统计此列为 NULL 值的行。
2.【强制】count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1 , col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
3.【强制】当某一列的值全是 NULL 时,count(col) 的返回结果为 0;但 sum(col) 的返回结果为 NULL,因此使用 sum() 时需注意 NPE 问题。
正例:可以使用如下方式来避免 sum 的 NPE 问题:SELECT IFNULL(SUM(column) , 0) FROM table;
4.【强制】使用 ISNULL() 来判断是否为 NULL 值。
说明:NULL 与任何值的直接比较都为 NULL。
1)NULL<>NULL 的返回结果是 NULL,而不是 false。
2)NULL=NULL 的返回结果是 NULL,而不是 true。
3)NULL<>1 的返回结果是 NULL,而不是 true。
反例:在 SQL 语句中,如果在 null 前换行,影响可读性。
select * from table where column1 is null and column3 is not null;而 ISNULL(column) 是一个整体,简洁易懂。
从性能数据上分析,ISNULL(column) 执行效率更快一些。
5.【强制】代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句。
8.【强制】数据订正(特别是删除或修改记录操作)时,要先 select,避免出现误删除的情况,确认无误才能执行更新语句。
9.【强制】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定。
说明:对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名(或表名),并且操作列在多个表中存在时,就会抛异常。
正例:select t1.name from first_table as t1 , second_table as t2 where t1.id = t2.id;
反例:在某业务中,由于多表关联查询语句没有加表的别名(或表名)的限制,正常运行两年后,最近在某个表中增加一个同名字段,在预发布环境做数据库变更后,线上查询语句出现出 1052 异常:Column ‘name’ infield list is ambiguous。
11.【推荐】in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在1000 个之内。
12.【参考】因国际化需要,所有的字符存储与表示,均采用 utf8 字符集,那么字符计数方法需要注意
说明:
SELECT LENGTH(“轻松工作”);–返回为 12
SELECT CHARACTER_LENGTH(“轻松工作”);–返回为 4
如果需要存储表情,那么选择 utf8mb4 来进行存储,注意它与 utf8 编码的区别。
13.【参考】TRUNCATE TABLE 比 DELETE 速度快,且使用的系统和事务日志资源少,但 TRUNCATE无事务且不触发 trigger,有可能造成事故,故不建议在开发代码中使用此语句。
说明:TRUNCATE TABLE 在功能上与不带 WHERE 子句的 DELETE 语句相同。
(四)ORM映射 1.【强制】在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
说明:
1)增加查询分析器解析成本。
2)增减字段容易与 resultMap 配置不一致。
3)无用字段增加网络消耗,尤其是 text 类型的字段。
4.【强制】sql.xml 配置参数使用:#{},#param# 不要使用 ${} 此种方式容易出现 SQL 注入。
5.【强制】不允许直接拿 HashMap 与 Hashtable 作为查询结果集的输出。
反例:某同学为避免写一个xxx,直接使用 Hashtable 来接收数据库返回结果,结果出现日常是把 bigint 转成 Long 值,而线上由于数据库版本不一样,解析成 BigInteger,导致线上问题。
7.【强制】更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。
9.【参考】@Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。

    推荐阅读