felixu

一次重构引发的对 Static 的探讨

背景描述

这次的问题源起于我要做一次手动参数校验,这在 Spring Boot 中,我们可以使用 SmartValidator 来进行,当有较多的地方需要做类似的操作,确实可以考虑将这手动校验的逻辑进行封装,而这次重构也正是基于其他同学的封装展开的,下面我们可以看一下相关代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
@AllArgsConstructor
public class SmartValidateUtil {

public static SmartValidator smartValidator;

public static void validateAnnotation(Object object, String name) throws BindException {
checkSmartValidator();
BeanPropertyBindingResult result = new BeanPropertyBindingResult(object, name);
smartValidator.validate(object, result);
if (result.hasErrors()) {
throw new BindException(result);
}
}

static void checkSmartValidator(){
if (smartValidator == null)
smartValidator = ApplicationUtils.getBean(SmartValidator.class);
}
}

有兴趣的可以思考一下有多少不合理的地方,这里便于理解,也给出 ApplicationUtils#getBean(Class type) 的相关代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ApplicationUtils implements ApplicationContextAware {

private static ApplicationContext context;

@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}

/**
* 根据 Bean 的名称获取 Bean
*/
public static Object getBean(String name) {
return context.getBean(name);
}

/**
* 根据 Bean 的类型获取 Bean
*/
public static <T> T getBean(Class<T> type) {
return context.getBean(type);
}
}

代码分析

这里我依次罗列我认为有问题的地方,后续也会给出重构之后的代码,如果有不合理的地方,也欢迎指出:

  1. 既然作为工具类了,Spring 的注解在这里就显得突兀了;
  2. 变量的访问限定符;
  3. 屏蔽了原有的分组校验功能,限制功能,这不合适;
  4. 静态变量用静态方法初始化的逻辑画蛇添足(这也是争论最多的地方了)。

在进行重构之后的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SmartValidateUtil {

private static final SmartValidator SMART_VALIDATOR = ApplicationUtils.getBean(SmartValidator.class);

public static void validate(Object target, String name, Object... validationHints) throws BindException {
BeanPropertyBindingResult result = new BeanPropertyBindingResult(target, name);
if (Objects.isNull(validationHints) || validationHints.length == 0)
SMART_VALIDATOR.validate(target, result);
else
SMART_VALIDATOR.validate(target, result, validationHints);
if (result.hasErrors()) {
throw new BindException(result);
}
}

public static void validate(Object target, String name) throws BindException {
validate(target, name, new Object[0]);
}
}

前三点我觉得应该算是很容易理解的点了,真正的争论在于第四点。

从代码中可以知道,这个 SmartValidator 来源于 Spring 的容器,很多人觉得原来的写法合理,理由如下:

静态变量初始化的时候,Spring 的容器尚未启动,此时使用 ApplicationUtils.getBean 进行赋值,结果肯定会是 null,所以使用静态方法,在使用的时候去判断一次,如果是 null,再去容器中获取完成赋值,此时容器已经启动完成了,也就不会再为 null 了。而我的写法中,直接赋值,肯定会是 null

然而,实际情况真的是这样吗。

这里的关键其实只有一点被 static 修饰的变量,它是在什么时候初始化的?这其实是个很基础的问题,然而在我们日常使用的时候却常常被忽略。

只要知道这个问题的答案,那么也就知道了我重构的依据,那么是什么时候初始化的呢,static 修饰的变量,在类第一次被显示使用的时候初始化(这里不会展开去说,有兴趣的可以自己找相关文章)。所以这里真正会发生什么呢?

在整个 Spring 容器的启动过程中,ApplicationUtils.getBean(SmartValidator.class) 压根没有执行,此时的 SMART_VALIDATOR 只是分配了默认的零值,所以也就不存在从容器中获取不到而赋值为 null 的情况,而当代码中第一次使用 SmartValidateUtil#validate() 时,SMART_VALIDATOR 才会真正的去初始化,也就是此时 ApplicationUtils.getBean(SmartValidator.class) 执行,而此时 Spring 的容器一定是正常启动完成了,所以此时 getBean 得到的结果其实与原写法中的判断之后再赋值的结果是一致的。

这便是我认为使用静态方法判断之后再去赋值是脱裤子放屁多此一举的依据。

当然这个是可以验证的,你可以在 ApplicationUtils#getBean(Class type) 的逻辑中输出当前的入参,观察它什么时候输出的,很容易验证它在第一次使用 SmartValidateUtil 时执行的。

总结

Static 作为一个日常编码中十分常用的一个关键字,本来关于他的相关知识也十分基础,但是在实际的使用过程中,真的有注意他的一些特性吗,恐怕也并非如此。由此可见,日常编码过程中,可能还是缺乏一些对代码的思考。这个讨论在我重构完之后,在组内讨论过,同时也在一个技术交流群中讨论过,然而不乏一些经验丰富的人同样会搞错,能第一时间 Get 到点的人很少很少,但是等说到 Static 什么时候初始化的,大家都懂。其实想表达什么呢,无非:

  1. 写代码不是想当然的,严谨的看问题固然好,但是也不要画蛇添足嘛,对于不确定的问题,大可以写段代码验证一下;
  2. 适当封装是合理的,但是若非必要,不要屏蔽原有功能,降低可用性;
  3. 写代码之前尽量做到心里大致有数,不要搞的四不像,不能代码写了,以后还要被说这是哪个沙雕写的代码啊。