SpringBoot中各种成员的初始化顺序及依赖注入中的一些小技巧(静态成员注入及@Lazy的使用)

Example

如果我们的类有如下成员变量:

@Component
public class A {

  @Autowired
  public B b;  // B is a bean

  public static C c;  // C is also a bean

  public static int count;

  public float version;

  public A() {
    System.out.println("This is A constructor.");
  }

  @Autowired
  public A(C c) {
    A.c = c;
    System.out.println("This is A constructor with c argument.");
  }

  @PostConstruct
  public void init() {

    count = 5;
    System.out.println("This is A post construct.");
  }
}

下面的结论可以通过在构造函数里打断点Debug来观察。

  • 首先初始化的是static成员变量, 此处的count采用默认值0。
  • 然后初始化的是非static成员变量,此处的version采用默认值0.0。
  • 然后Spring在实例化A时选择的构造函数的原则是:如果有构造函数被@Autowired所修饰,则采用该构造函数(注意,@Autowired(required = true)只能修饰一个构造函数),否则采用默认的无参构造函数。此处采用的构造函数为
    @Autowired
    public A(C c) {
      this.c = c;
      System.out.println("This is A constructor with c argument.");
    }
    
    注意执行完该构造函数后,此时的成员变量B并没有被注入,值还是null。
  • Spring容器选择合适的Bean注入b (DI阶段)。
  • 执行被@PostConstruct修饰的init()函数。

总之,在上面这个例子中,各成员变量的初始化执行顺序为:“static 成员变量 ”--> “非static成员变量” --> “被@Autowired修饰的构造函数” --> “被@Autowired修饰的成员变量b” --> “被@PostConstruct修饰的init()函数”。

上述过程也是一个Bean完整生成的过程,因此要注意不同于一般类的实例化,Bean在构造函数完成后还有DI(Dependency Injection)阶段,通过Spring容器注入它所依赖的其他Bean,想要在一个Bean生成完后进行其他操作,可以使用@PostConstruct

Tips

1. 静态成员依赖注入

有时我们想要对静态成员进行依赖注入(通常是Field dependency injection,即直接在成员上加@Autowired,此种做法不推荐),直接在静态成员上加@Autowired是无效的(其值总为null),这是因为静态成员变量是类的属性,不属于任何对象,而Spring实现Field dependency injection 是要依靠基于实例的reflection(反射)进行的。在这个例子中,Spring通过反射生成bean a, 并且发现a使用了bean b,然后去生成bean b,再次利用反射生成setter方法将b注入进a,这样就实现了Field dependency injection。通过上述过程我们可以知道static成员由于不属于任何实例,所以无法实现这样的依赖注入,但是我们可以通过Constructor dependency injection(构造函数依赖注入)来实现。以上面的例子为例,Spring在生成bean a(调用A的构造函数)时,由于A的构造函数带有参数c,Spring将在容器里寻找是否有符合c类型的bean,找到后将bean c赋值给构造函数的参数c,然后当执行到A.c = c时成员变量c就被“注入”成功了。

2. Bean的延迟生成与注入

如果我们希望某个Bean不要在Spring容器启动时初始化(这样可以加快应用的启动速度),而是在用到时才实例化,可以用@Lazy这个注解。将这个注解加在@Bean、@Component、@Service、@Configuration等注解上时,这些注解所修饰的Bean将在第一次引用时才实例化。我们举个简单的例子:

存在一个普通Bean

@Component
public class CommonBean {
    public CommonBean () {
        System.out.println("This is CommonBean constructor.");
    }
}

以及加了@Lazy的“LazyBean”

@Lazy
@Component
public class LazyBean {
    public LazyBean() {
        System.out.println("This is LazyBean constructor.");
    }
}

假设由于某种原因,Spring扫描路径的顺序是LazyBean先于CommonBean,如果LazyBean不加@Lazy注解,则Bean的生成顺序永远是LazyBean先于CommonBean;若是加上@Lazy,则CommonBean先生成,并且如果没有其他地方引用LazyBean(例如没有其他Bean @Autowired LazyBean),那么LazyBean将一直不进行初始化(直观表现为它的构造函数一直未被调用)。在实际应用中,如果Bean的数目比较多,无法立即理清依赖关系,但确定自己的Bean需要在其他Bean生成之后才生成时,可以通过此方法简单的控制Bean的生成顺序。

注意,若是在CommonBean里引用了LazyBean ,则不论LazyBean有没有使用@Lazy,它都会先完成实例化,即Bean之间引用的作用会大于@Lazy的作用。

@Lazy同样可以加在@Autowired注解上,比如

@Component
public class CommonBean {
    @Lazy
    @Autowired
    private LazyBean lazyBean;
}

在CommonBean生成过程中的DI阶段,它不关心LazyBean是否存在于Spring容器中(一般情况下,如果没有添加@Lazy注解并且没有类型为LazyBean的Bean在容器中的情况下, 会抛找不到Bean的异常),它会生成一个有同样接口的代理(由于是代理,没有真正的功能,但是LazyBean的构造函数及@PostConstruct所修饰的方法都会被调用),只有真正要调用LazyBean的方法时,该代理才会在自身内部生成LazyBean实例(此时不会再次调用构造函数及@PostConstruct所修饰的方法,调用代理的各种方法将会转发到生成的真正的LazyBean中),这样也相当于实现了延迟加载Bean的功能。

你可能感兴趣的:(SpringBoot中各种成员的初始化顺序及依赖注入中的一些小技巧(静态成员注入及@Lazy的使用))