Spring核心 | Bean的定义与控制、纯Java运行与@Bean

2个月前

先介绍Bean的生命周期功能以及状态定义功能,然后谈谈纯Java运行与@Bean。

转载本文需注明出处:微信公众号EAWorld,违者必究。

Spring的整个运转机制就是围绕着IoC容器以及Bean展开的。IoC就是一个篮子,所有的Bean都向里面扔。除了提供篮子功能创建并存放Bean之外,IoC还要负责管理Bean与Bean之间的关系——依赖注入。之前也提到Bean是Spring核心容器的最小工作单元,Spring一些更高级的功能(例如切面、代理)都是在Bean的基础上实现。

除了管理Bean与Bean之间的关系,IoC还提供了对Bean自身进行控制的各项功能,本文将先介绍Bean的生命周期功能以及状态定义功能,然后谈谈纯Java运行与@Bean。

前置依赖

Bean与Bean之间存在依赖关系,可以是强依赖(通过XML和注解直接声明依赖)、也可以是弱依赖(ApplicationContextAware等方式获取)。当一个Bean需要另外一个Bean完成初始化后自身才能工作时,例如一个Bean依赖DataSoruce,但是DataSource的初始化需要较长时间。这个时候用depends-on声明前置依赖即可:

<!-- 依赖多个Bean使用,号分割 -->
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
</bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

延迟加载

通常情况下,所有的singleton(http://t.cn/E6Wwy06)类型的Bean都会在容器创建后进行初始化,简单的说就是启动Jvm就开始创建(实际上是创建ApplicationContext的某个实现类实例之后)。

IoC支持所有的singleton Bean在使用时再加载,这样做的好处是可以大大节省初始化的时间。但是如果你的应用对启动时间的长短并不敏感,建议让所有的 singleton 都启动时加载。这样可以在启动时就发现一些问题,而不是在运行很久直到使用时才由用户去触发这个问题。或者可以根据场景来使用决定是否延迟,例如开发时使用延迟加载,而在集成测试或上生产时关闭。

可以设置全局延迟加载,也可以设置某个Bean延迟加载:

<beans default-lazy-init="true">
    <!-- 所有的Bean知道使用的时候才会进行加载... -->
</beans>

<!-- 只有lazy类延迟加载 -->
<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.foo.AnotherBean"/>

需要注意的是,在设置某个单独的Bean延迟加载时,如果有某个没有延迟加载的Bean要依赖他,那实际上也会在初始化的时候就加载。

还要强调一下,这里的“加载”仅仅是为了表示一个类被Ioc创造并放置容器中,和classLoad方法将class文件中的字节码加载到方法区的加载是两个概念。

延迟加载在设计模式上是单例模式一种延伸,通常也被称为懒汉模式。单例通常有双重锁+volatile、静态类和枚举三种方式实现。在Effective Java一书中对三种模式都有深入的解析。而对于Spring容器而言,枚举的方式肯定不好用了,静态类由于属于自身代码级别应该也不会用,所以双重锁的实现方式较为可信。不过我没去看过源码,仅属于猜测。

生命周期方法

初始化方法

当一个Bean完成初始化并注入各项参数之后,初始化回掉方法会被调用,简单的说就是完成创建之后会被调用。实现初始化回调方法有2个路径:1.继承org.springframework.beans.factory.InitializingBean接口,然后实现 afterPropertiesSet方法。2.在Bean的XML配置上使用init-method属性来制定要调用的初始化:

继承实现:

<bean id="a" class="x.y.A" />

package x.y;
public class A implements InitializingBean {
    public void afterPropertiesSet(){
        // init
    }
}

配置实现:

<bean id="a" class="x.y.A" init-method="init" />

package x.y;
public class A {
    public void init(){}
}

2种方法都等效,实际使用是我们应该使用哪一种方法呢?

InitializingBean是Spring早期实现的一个生命周期回调方法。但是在JCP推出JSR-250和JSR-330规范之后,Spring的大神们开始意识到基于元编程思想和配置手段来实现非侵入式框架(Not Coupled)才是正道。所以现在都是推荐使用配置文件和JSR-250的@PostConstruct(关于各种Annotation的使用请关注后续的文章)。现在依然保留InitializingBean应该是考虑到兼容问题。

销毁方法

与创建方法相对应的是销毁方法。当一个类将要被销毁之前,对应的销毁回调方法会被调用。销毁方法也有一个继承实现和配置+注解实现:

继承实现:

<bean id="a" class="x.y.A" />

package x.y;
public class A implements DisposableBean {
    public void destroy(){
        // 销毁资源
    }
}

配置实现:

<bean id="a" class="x.y.A" destroy-method="cleanUp" />

package x.y;
public class A {
    public void cleanUp(){
        // 销毁资源
    }
}

依然建议销毁手段也使用配置或@PreDestroy来设定销毁方法。

全局配置初始化与销毁方法

IoC容器还提供了全局配置初始化与销毁方法的配置:

package x.y;
public class A {
    public void init(){
        // 初始化资源
    }
    public void destroy(){
        // 销毁资源
    }
}

<beans default-init-method="init" default-destroy-method="destroy">
     <bean id="a" class="x.y.A"/>
     <!-- bean configuration -->
</beans>

通过在<beans>标签上使用default-init-method和default-destroy-method 属性参数,可以为容器中所有的Bean统一指定初始化和销毁的生命周期方法。

如果在<beans>上设定2个默认的生命周期方法,同时在<bean>上也指定了init-method或destroy-method,回调方法会以<bean>上的配置为准。这样就保证全局配置与单独配置可以共存。

使用初始化或销毁2个生命周期方法注意的要点:

初始化和销毁都提供了3种手段:XML配置、注解、以及实现接口。系统的各个部分会交由不同的团队开发,不遵循统一的规范,建议使用满足JSR规范的注解——@PostConstruct、@PreDestroy。如果是统一的团队,准训一致的规范,建议使用<beans>的属性统一名称使用全局配置。

如果Bean设计到代理模式时(例如使用了AOP),那么生命周期方法被调用时,有可能代理类还没有被创建出来。因为生命周期方法是实体类完成对应工作之后就会被调用,而与代理类无关。

3.0新增容器启动方法

在3.0之前的Spring核心框架中,我们启动一个Spring容器必须使用一个XML文件。而到了3.X之后的版本Spring为创建容器新增了一个入口类——AnnotationConfigApplicationContext。

AnnotationConfigApplicationContext和过去的ClassPathXmlApplicationContext、FileSystemXmlApplicationContext等方法不同的是他不用再指定任何XML配置文件,而是可以通过指定类向容器添加Bean。我们通过几个简单的例子来说明他的使用。

以下例子只用于说明问题,源码请到 gitee 自行 clone(http://t.cn/E6Wvo51),本节的代码在 chkui.springcore.example.javabase.simple 包中。

直接添加Bean

我们可以通过AnnotationConfigApplicationContext直接向容器添加指定的类作为Bean,先定义我们的class:

package chkui.springcore.example.javabase.simple.pureBean;

class LolBean {
  public String toString() {
    return "I AM LOL!";
  }
}

class WowBean {
  public String toString() {
    return "I AM WOW!";
  }
}

然后向容器添加这些Bean:

package chkui.springcore.example.javabase.simple;

public class WithoutAnnotation {
  public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(WowBean.class, LolBean.class);
    System.out.println(ctx.getBean(WowBean.class));
    System.out.println(ctx.getBean(LolBean.class));
  }
}

这样就启动了一个Spring的容器,并且容器中包含了WowBean和LolBean这两个类的单例。

替代<beans>标签

@Configuration在之前介绍Spring核心容器的文章中出现过一两次,配合各种注解的使用@Configuration可以替代<beans>配置中的所有功能。基本上AnnotationConfigApplicationContext和@Configuration组合使用就可以实现Spring容器纯Java启动。请看下面的例子。

我们在前面例子的基础上增加几个类:

package chkui.springcore.example.javabase.simple.bean;

public class DotaBean {
  public String toString() {
    return "I AM Dota!";
  }
}

@Component
public class PseBean {

  @Override
  public String toString() {
    return "I AM PSE!";
  }
}

注意DotaBean上是没有@Component注解的。然后添加@Configuration配置:

package chkui.springcore.example.javabase.simple.bean;

@Configuration
@ComponentScan("chkui.springcore.example.javabase.simple.bean")
public class Config {
  @Bean
  public DotaBean dotaBean() {
    return new DotaBean();
  }
}

最后运行他们:

package chkui.springcore.example.javabase.simple;

public class WithScan {
  public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(Config.class, WowBean.class, LolBean.class);
    System.out.println(ctx.getBean(Config.class));
    System.out.println(ctx.getBean(PseBean.class));
    System.out.println(ctx.getBean(WowBean.class));
    System.out.println(ctx.getBean(LolBean.class));
    System.out.println(ctx.getBean(DotaBean.class));
  }
}

@Component已经在《Stereotype组件与Bean扫描(http://t.cn/E6WhYYk)》这篇文章介绍过,@ComponentScan的作用等价于<context:component-scan/>标签,属性参数都是一一对应的,只不过前者是驼峰命名规则(camelCase)——@ComponentScan(basePackages="..."),后者是短横线命名规则(kebab-case)——<context:component-scan base-package="..."/>。实际上使用Annotation来替换XML配置中的内容,大部分都使用这种转换方式。

@Configuration和@Bean标签会在后续的内容中详细介绍。@Bean主要用于方法标记,表明这个方法返回一个要添加到容器中的Bean。

AnnotationConfigApplicationContext的其他使用方法

除了以上常规的使用方法,AnnotationConfigApplicationContext还有其他方式向容器添加Bean。

可以使用AnnotationConfigApplicationContext::register方法来添加配置和Bean:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    //动态添加配置文件
    ctx.register(Config1.class, Config2.class);
    //动态添加Bean
    ctx.register(Bean1.class);
    //刷新
    ctx.refresh();
}

注意最后的refresh方法,这个方法来源于ConfigurableApplicationContext接口,然后是在AbstractApplicationContext中实现的。他的过程相当于销毁之前已经创建的资源,然后再重新创建了一个新的容器。这里的代码会执行以下几步:

new AnnotationConfigApplicationContext():创建一个新的容器,容器中没有自定义的Bean。

AnnotationConfigApplicationContext::register:向容器添加BeanDefinition(http://t.cn/E6WzQ7W),但是这些BeanDefinition并没有转化为容器中的Bean。

ConfigurableApplicationContext::refresh():纳入新添加的BeanDefinition重建容器。

还可以直接使用AnnotationConfigApplicationContext::scan方法扫描指定的路径:

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.acme");
    ctx.refresh();
}

执行原理和上面介绍的一样。

需要注意的是:如果你的工程中需要使用AnnotationConfigApplicationContext::register、AnnotationConfigApplicationContext::scan等方法创建容器和其中Bean的依赖关系,所有的Bean都只能在register或scan中添加。如果你既在AnnotationConfigApplicationContext的构造方法中添加了Bean,又使用AnnotationConfigApplicationContext::refresh()方法会抛出一个重复执行refresh的异常。AnnotationConfigApplicationContext::refresh()方法全局也只能被调用一次。

@Bean注解

@Bean注解等价于配置文件中的<bean>标签,对应的参数也是将短横线命名切换为驼峰命名——<bean init-method="..."> => @Bean(initMethod="...")。@Bean注解只能使用在方法上,方法必须是在@Configuration标记的类或者其他Bean中,两者存在的差异会在后续的文章中介绍。下面通过一个例子来说明Bean的使用。

以下例子只用于说明问题,源码请到 gitee 自行 clone(http://t.cn/E6Wvo51),本节的代码在 chkui.springcore.example.javabase.beanAnnotation 包中。

定义两个要添加到容器中的Bean:

package chkui.springcore.example.javabase.beanAnnotation.bean;

class FinalFantasy {
  @Override
  public String toString() {
    return "Final Fantasy 1~15";
  }
  public void init() {
    System.out.println("Final Fantasy init!");
  }
  
  public void destroy() {
    System.out.println("Final Fantasy destroy!");
  }
}

class DragonQuest {
  public String toString() {
    return "Dragon Quest 1~11";
  }
  
  @PostConstruct
  public void init() {
    System.out.println("Dragon Quest init!");
  }
  
  @PreDestroy
  public void destroy() {
    System.out.println("Dragon Quest destroy!");
  }
}

定义一个功能接口及其实现类:

package chkui.springcore.example.javabase.beanAnnotation.bean;

interface Support {
  void setFinalFantasy(FinalFantasy ff);
  FinalFantasy getFinalFantasy();
}
class SupportImpl implements Support {
  private FinalFantasy ff; 
  public void setFinalFantasy(FinalFantasy ff) {
    this.ff = ff;
  }
  public FinalFantasy getFinalFantasy() {
    return ff;
  }
}

然后顶一个@Configuration类:

package chkui.springcore.example.javabase.beanAnnotation.bean;

public class BeanAnnotationConfig {
  @Bean
  public Support support(FinalFantasy ff) {
    Support support = new SupportImpl();
    support.setFinalFantasy(ff);
    return support;
  }
  
  @Bean(initMethod="init", destroyMethod="destroy")
  @Description("Final Fantasy")
  public FinalFantasy finalFantasy() {
    return new FinalFantasy();
  }
  
  @Bean(name= {"dragon-quest", "DragonQuest"})
  public DragonQuest dragonQuest() {
    return new DragonQuest();
  }
}

最后运行他们:

public class BeanAnnotApp {

  public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(BeanAnnotationConfig.class);
    Support support = ctx.getBean(Support.class);
    System.out.println(support.getFinalFantasy());
    System.out.println(ctx.getBean(DragonQuest.class));
  }

}

在配置类BeanAnnotationConfig中,我们配置了3个Bean。这里的写在方法上的@Bean注解和写在配置文件中的<bean>注解一个效果:

@Bean中的initMethod和destroyMethod对应<bean>标签中的init-method和destroy-method属性。

@Bean中的name参数只有一个值时相当于id,有多个的时候相当于设置了多个别名

Support support(FinalFantasy ff):我们可以直接在方法中暴露参数来引入其他Bean,这就类似于配置中ref的功能。

如果不指定initMethod和destroyMethod,使用JSR-330的生命周期注解(@PostConstruct、@PreDestroy)同样有效

关于作者:陈葵,目前现任职某跨境安全支付公司技术总监,中山大学密码学与信息安全专业硕士。对金融级安全支付,高可用性云应用,分布式事物、DevOps有多年的经验。虽肩负团队管理的任务,但对Coding依然保持极大的兴趣,熟读Spring、React、Tensorflow等各类开源项目的核心代码。目前主导通过数据分析+AI提升风控模型能力的研究。



关于EAWorld:微服务,DevOps,数据治理,移动架构原创技术分享。长按二维码关注!

COMMENTS

需要 后方可回复
如果没有账号可以 一个帐号。