SpringBoot开发中的一些小细节(十一)

SpringBoot开发中的一些小细节(十一)

2019, Jul 10    

前言

最近正式入职,第一周的事情主要就是进行代码的Code Review,将自己曾经的实习时候写的代码看一看,清掉一部分过时的代码。同时也从前辈们的分享中学习到了不少的小细节。 以及看了阿里巴巴的开发规范规约,将一些比较重要的地方、原来没有注意到的地方记录一下。

测试用例

  1. 在写Junit的时候,可以写一个BaseTest,以后test直接继承这个类即可,就不用每次都新建一个类然后在上面重新写上好多注解了。

     @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)  
     @RunWith(SpringJUnit4ClassRunner.class)  
     @Slf4j  
     public class BaseTests {
     }
    
  2. 在写CRUD的测试用例的时候,一定要写成一个闭环,新加了测试用例,最后测试结束的时候一定要删除,防止在数据库中产生脏数据。

阿里巴巴的规约阅读笔记

  1. 外部正在调用或者二方库依赖的接口(二方库就是公司内部各个服务之间相互依赖的库)不允许修改方法的签名,避免对接口调用方造成影响。
  2. 接口过时的时候必须加上@Deprecated注解。
  3. Object的equals方法容易报NPE,应该使用常量或者确定有值的对象来使用equals方法。

    正例:”test”.equals(object);
    反例:object.equals(“test”);

  4. 所有整型包装类对象之间值的比较,全部使用equals方法比较。

    说明:对于 Integer var = ? 在-128 至 127 范围内的赋值,Integer 对象是在 IntegerCache.cache 产 生,会复用已有对象,这个区间内的 Integer 值可以直接使用==进行判断,但是这个区间之外的所有数 据,都会在堆上产生,并不会复用已有对象,这是一个大坑,推荐使用 equals 方法进行判断。

  5. 浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来判断。
  6. 定义数据对象DO类时,属性类型要与数据库字段类型相匹配。

    正例:数据库字段的 bigint 必须与类属性的 Long 类型相对应。
    反例:某个案例的数据库表 id 字段定义类型 bigint unsigned,实际类对象属性为 Integer,随着 id 越来 越大,超过 Integer 的表示范围而溢出成为负数。

  7. 为了防止精度损失,禁止使用构造方法 BigDecimal(double)的方式把 double 值转化为 BigDecimal 对象。

    说明:BigDecimal(double)存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。
    如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0.10000000149
    正例:优先推荐入参为 String 的构造方法,或使用 BigDecimal 的 valueOf 方法,此方法内部其实执行了 Double 的 toString,而 Double 的 toString 按 double 的实际能表达的精度对尾数进行了截断。

    BigDecimal recommend1 = new BigDecimal("0.1");
    BigDecimal recommend2 = BigDecimal.valueOf(0.1);
    
  8. 所有POJO类属性必须使用包装数据类型。
  9. RPC方法和返回值必须使用包装数据类型。
  10. 所有的局部变量推荐使用基本数据类型。

    说明:POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE问题,或者入库检查,都由使用者来保证。
    正例:数据库的查询结果可能是null,因为自动拆箱,用基本数据类型接收有NPE风险。 反例:比如显示成交总额涨跌情况,即正负 x%,x为基本数据类型,调用的RPC 服务,调用不成功时,返回的是默认值,页面显示为 0%,这是不合理的,应该显示成中划线。所以包装数据类型的 null 值,能 够表示额外的信息,如:远程调用失败,异常退出。

  11. 定义DO、DTO、VO等POJO类的时候,不要设定任何属性的默认值。

    反例:POJO 类的createTime默认值为 new Date(),但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。

  12. 构造方法里面禁止加入任何业务逻辑,如果有初始化逻辑,请放在init方法中。
  13. 序列化类新增属性的时候,不要修改serialVersionUID字段避免反序列化失败;如果完全不兼容升级,避免反序列化混乱,那么请修改serialVersionUID的值。
  14. 使用索引访问String的split方法得到的数组时,需要做到最后一个分隔符后有误内容的检查,否则有抛IndexOutOfBoundsException的风险
    String str = "a,b,c,,";
    String[] ary = str.split(",");
    // 预期大于 3,结果是 3 System.out.println(ary.length);
    
  15. 循环体类,字符串的连接方式,使用StringBuilder的append方法进行拓展,或者使用Guava的Joiner来进行拓展。如果使用str=str+“new str”的方式来进行字符串连接的话,每一次循环都会产生一个新的StringBuilder对象然后进行append操作,最后通过toString方法返回String对象,造成内存资源的浪费。

    反例

    String str = "start";
    for (int i = 0; i < 100; i++) {
    str = str + "hello"; }
    
  16. 表达异常的分支时,少用if-else方式,这种方式可以改写成:
    if (condition) { ...
    return obj; }
    // 接着写 else 的业务逻辑代码;
    

    说明:如果非使用 if()…else if()…else…方式表达逻辑,避免后续代码维护困难,请勿超过3层。 正例:超过3层的if-else的逻辑判断代码可以使用卫语句、策略模式、状态模式等来实现,其中卫语句,即代码逻辑先考虑失败、异常、中断、退出等直接返回的情况,以方法多个出口的方式,解决代码中判断分支嵌套的问题,这是逆向思维的体现。

    public void findBoyfriend(Man man) {
    if (man.isUgly()) { System.out.println("本姑娘是外貌协会的资深会员");
    return; }
    if (man.isPoor()) { System.out.println("贫贱夫妻百事哀");
    return; }
    if (man.isBadTemper()) { System.out.println("银河有多远,你就给我滚多远");
    return; }
    System.out.println("可以先交往一段时间看看"); }
    //he,woman
    
  17. 除常用方法(如getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高可读性。

    说明:很多if语句内的逻辑表达式相当复杂,与、或、取反混合运算,甚至各种方法纵深调用,理解成本非常高。如果赋值一个非常好理解的布尔变量名字,则是件令人爽心悦目的事情。
    正例:

    // 伪代码如下
    final boolean existed = (file.open(fileName, "w") != null) && (...) || (...);
    
  18. 在高并发场景中,避免使用”等于”判断作为中断或退出的条件。

    如果并发控制没有做好,那么容易产生等值判断被“击穿”的情况,最好使用区间判断来代替。
    反例: 判断剩余奖品数量等于0时,终止发放奖品,但因为并发处理错误导致奖品数量瞬间变成了负数,这样的话,活动无法终止。

  19. 类、类属性、类方法的注释必须使用Javadoc规范,使用/**内容*/格式,不得使用 // xxx 方式。
  20. 所有的抽象方法(包括接口中的方法)必须要用Javadoc注释、除了返回值、参数、 异常说明外,还必须指出该方法做什么事情,实现什么功能。 说明:对子类的实现要求,或者调用注意事项,请一并说明。
  21. 所有的类都必须添加创建者和创建日期。
  22. 方法内部单行注释,在被注释语句上方另起一行,使用//注释。方法内部多行注释 使用/* */注释,注意与代码对齐。
  23. 所有的枚举类型字段必须要有注释,说明每个数据项的用途。
  24. 避免出现重复的代码(Don’t Repeat Yourself),即DRY原则。说明:随意复制和粘贴代码,必然会导致代码的重复,在以后需要修改时,需要修改所有的副本,容易遗漏。必要时抽取共性方法,或者抽象公共类,甚至是组件化。
  25. 应用中不可直接使用日志系统(Log4j、Logback)中的API,而应依赖使用日志框架 SLF4J 中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。

Code Review

  1. 使用Response代替Response,list数据为空时后端不要返回NULL而要返回空集合。
  2. DTO(Data Transfer Object)类一般是用于两个系统之间的数据传输,而VO(Value Object)一般是用于后端向前端进行展示的数据对象。
  3. 尽量不要使用BeanUtils.copyProperties,可以在目标方法里面写一个构造方法。使用getter和setter,使用上面那个工具类,如果属性有一点不一样的时候,就会出错。
  4. 一个方法需要判断是不是为空的时候,可以在入参的时候就进行判断。
  5. 尽量不要使用map来进行返回,最好是通过定义对象的方式来进行返回。