Java Spring Bean

前言

在庞大的java体系中,Spring有着举足轻重的地位,它给每位开发者带来了极大的便利和惊喜。都知道Spring是创建和管理bean的工厂,它提供了多种定义bean的方式,能够满足日常工作中的多种业务场景。
Spring中有哪些方式可以定义bean?
很多人会说出以下三种:
2021-07-10-08-26-45-586062.png
实际上Spring的功能远比想象中更强大。

1、xml文件配置bean

先从xml配置bean开始,它是Spring最早支持的方式。后来,随着SpringBoot越来越受欢迎,该方法目前已经用得很少了,但还是有必要了解一下。

1.1 构造器

如果之前有在bean.xml文件中配置过bean的经历,那么对如下的配置肯定不会陌生:

  1. <bean id="personService" class="com.sue.cache.service.test7.PersonService">
  2. </bean>

这种方式是以前使用最多的方式,它默认使用了无参构造器创建bean。
当然还可以使用有参的构造器,通过<constructor-arg>标签来完成配置。

  1. <bean id="personService" class="com.sue.cache.service.test7.PersonService">
  2. <constructor-arg index="0" value="susan"></constructor-arg>
  3. <constructor-arg index="1" ref="baseInfo"></constructor-arg>
  4. </bean>

其中:

  • index表示下标,从0开始。
  • value表示常量值
  • ref表示引用另一个bean

    1.2 setter方法

    除此之外,Spring还提供了另外一种思路:通过setter方法设置bean所需参数,这种方式耦合性相对较低,比有参构造器使用更为广泛。
    先定义Person实体:

    1. @Data
    2. public class Person {
    3. private String name;
    4. private int age;
    5. }

    它里面包含:成员变量name和age,getter/setter方法。
    然后在bean.xml文件中配置bean时,加上<property>标签设置bean所需参数。

    1. <bean id="person" class="com.sue.cache.service.test7.Person">
    2. <property name="name" value="susan"></constructor-arg>
    3. <property name="age" value="18"></constructor-arg>
    4. </bean>

    1.3 静态工厂

    这种方式的关键是需要定义一个工厂类,它里面包含一个创建bean的静态方法。例如:

    1. public class SusanBeanFactory {
    2. public static Person createPerson(String name, int age) {
    3. return new Person(name, age);
    4. }
    5. }

    接下来定义Person类如下:

    1. @AllArgsConstructor
    2. @NoArgsConstructor
    3. @Data
    4. public class Person {
    5. private String name;
    6. private int age;
    7. }

    它里面包含:成员变量name和age,getter/setter方法,无参构造器和全参构造器。
    然后在bean.xml文件中配置bean时,通过factory-method参数指定静态工厂方法,同时通过<constructor-arg>设置相关参数。

    1. <bean class="com.sue.cache.service.test7.SusanBeanFactory" factory-method="createPerson">
    2. <constructor-arg index="0" value="susan"></constructor-arg>
    3. <constructor-arg index="1" value="18"></constructor-arg>
    4. </bean>

    1.4 实例工厂方法

    这种方式也需要定义一个工厂类,但里面包含非静态的创建bean的方法。

    1. public class SusanBeanFactory {
    2. public Person createPerson(String name, int age) {
    3. return new Person(name, age);
    4. }
    5. }

    Person类跟上面一样,就不多说了。
    然后bean.xml文件中配置bean时,需要先配置工厂bean。然后在配置实例bean时,通过factory-bean参数指定该工厂bean的引用。

    1. <bean id="susanBeanFactory" class="com.sue.cache.service.test7.SusanBeanFactory">
    2. </bean>
    3. <bean factory-bean="susanBeanFactory" factory-method="createPerson">
    4. <constructor-arg index="0" value="susan"></constructor-arg>
    5. <constructor-arg index="1" value="18"></constructor-arg>
    6. </bean>

    1.5 FactoryBean

    上面的实例工厂方法每次都需要创建一个工厂类,不方面统一管理。这时可以使用FactoryBean接口。

    1. public class UserFactoryBean implements FactoryBean<User> {
    2. @Override
    3. public User getObject() throws Exception {
    4. return new User();
    5. }
    6. @Override
    7. public Class<?> getObjectType() {
    8. return User.class;
    9. }
    10. }

    在它的getObject方法中可以实现自己的逻辑创建对象,并且在getObjectType方法中可以定义对象的类型。
    然后在bean.xml文件中配置bean时,只需像普通的bean一样配置即可。

    1. <bean id="userFactoryBean" class="com.sue.async.service.UserFactoryBean">
    2. </bean>

    :::tips 注意:getBean("userFactoryBean");获取的是getObject方法中返回的对象。而getBean("&userFactoryBean");获取的才是真正的UserFactoryBean对象。 ::: 通过上面五种方式,在bean.xml文件中把bean配置好之后,Spring就会自动扫描和解析相应的标签,并且创建和实例化bean,然后放入Spring容器中。
    虽说基于xml文件的方式配置bean,简单而且非常灵活,比较适合一些小项目。但如果遇到比较复杂的项目,则需要配置大量的bean,而且bean之间的关系错综复杂,这样久而久之会导致xml文件迅速膨胀,非常不利于bean的管理。

    2、Component注解

    为了解决bean太多时,xml文件过大,从而导致膨胀不好维护的问题。在Spring2.5中开始支持:@Component@Repository@Service@Controller等注解定义bean。
    如果有看过这些注解的源码的话,就会惊奇得发现:其实后三种注解也是@Component
    2021-07-10-08-26-45-679806.png2021-07-10-08-26-45-794774.png2021-07-10-08-26-45-922461.png
    @Component系列注解的出现,带来了极大的便利。不需要像以前那样在bean.xml文件中配置bean了,现在只用在类上加ComponentRepositoryServiceController,这四种注解中的任意一种,就能轻松完成bean的定义。

    1. @Service
    2. public class PersonService {
    3. public String get() {
    4. return "data";
    5. }
    6. }

    其实,这四种注解在功能上没有特别的区别,不过在业界有个不成文的约定:

  • Controller 一般用在控制层

  • Service 一般用在业务层
  • Repository 一般用在数据层
  • Component 一般用在公共组件上

不过,需要特别注意的是,通过这种@Component扫描注解的方式定义bean的前提是:需要先配置扫描路径。
目前常用的配置扫描路径的方式如下:

  1. 在applicationContext.xml文件中使用<context:component-scan>标签。例如:

    1. <context:component-scan base-package="com.sue.cache" />
  2. 在SpringBoot的启动类上加上@ComponentScan注解,例如:

    1. @ComponentScan(basePackages = "com.sue.cache")
    2. @SpringBootApplication
    3. public class Application {
    4. public static void main(String[] args) {
    5. new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    6. }
    7. }
  3. 直接在SpringBootApplication注解上加,它支持ComponentScan功能:

    1. @SpringBootApplication(scanBasePackages = "com.sue.cache")
    2. public class Application {
    3. public static void main(String[] args) {
    4. new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    5. }
    6. }

    当然,如果需要扫描的类跟SpringBoot的入口类,在同一级或者子级的包下面,无需指定scanBasePackages参数,Spring默认会从入口类的同一级或者子级的包去找。

    1. @SpringBootApplication
    2. public class Application {
    3. public static void main(String[] args) {
    4. new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
    5. }
    6. }

    此外,除了上述四种@Component注解之外,SpringBoot还增加了@RestController注解,它是一种特殊的@Controller注解,所以也是@Component注解。
    @RestController还支持@ResponseBody注解的功能,即将接口响应数据的格式自动转换成json。
    2021-07-10-08-26-46-032866.png
    @Component是日常工作中最多的定义bean的方式。

    3、JavaConfig

    @Component系列注解虽说使用起来非常方便,但是bean的创建过程完全交给Spring容器来完成,没办法自己控制。
    Spring从3.0以后,开始支持JavaConfig的方式定义bean。它可以看做Spring的配置文件,但并非真正的配置文件,需要通过编码java代码的方式创建bean。例如:

    1. @Configuration
    2. public class MyConfiguration {
    3. @Bean
    4. public Person person() {
    5. return new Person();
    6. }
    7. }

    在JavaConfig类上加@Configuration注解,相当于配置了<beans>标签。而在方法上加@Bean注解,相当于配置了<bean>标签。
    此外,SpringBoot还引入了一些列的@Conditional注解,用来控制bean的创建。

    1. @Configuration
    2. public class MyConfiguration {
    3. @ConditionalOnClass(Country.class)
    4. @Bean
    5. public Person person() {
    6. return new Person();
    7. }
    8. }

    @ConditionalOnClass注解的功能是当项目中存在Country类时,才实例化Person类。换句话说就是,如果项目中不存在Country类,就不实例化Person类。
    这个功能非常有用,相当于一个开关控制着Person类,只有满足一定条件才能实例化。
    Spring中使用比较多的Conditional还有:

  • ConditionalOnBean
  • ConditionalOnProperty
  • ConditionalOnMissingClass
  • ConditionalOnMissingBean
  • ConditionalOnWebApplication

下面用一张图整体认识一下@Conditional家族:
2021-07-10-08-26-46-204781.png
有了这些功能,终于可以告别麻烦的xml时代了。

4、Import注解

通过前面介绍的@Configuration@Bean相结合的方式,可以通过代码定义bean。但这种方式有一定的局限性,它只能创建该类中定义的bean实例,不能创建其他类的bean实例,如果想创建其他类的bean实例该怎么办呢?
这时可以使用@Import注解导入。

4.1 普通类

Spring4.2之后@Import注解可以实例化普通类的bean实例。例如:
先定义了Role类:

  1. @Data
  2. public class Role {
  3. private Long id;
  4. private String name;
  5. }

接下来使用@Import注解导入Role类:

  1. @Import(Role.class)
  2. @Configuration
  3. public class MyConfig {
  4. }

然后在调用的地方通过@Autowired注解注入所需的bean。

  1. @RequestMapping("/")
  2. @RestController
  3. public class TestController {
  4. @Autowired
  5. private Role role;
  6. @GetMapping("/test")
  7. public String test() {
  8. System.out.println(role);
  9. return "test";
  10. }
  11. }

没有在任何地方定义过Role的bean,但Spring却能自动创建该类的bean实例,这是为什么呢?
这正是@Import注解的强大之处。
此时,有些朋友可能会问:@Import注解能定义单个类的bean,但如果有多个类需要定义bean该怎么办呢?这个需求@Import注解也支持。

  1. @Import({Role.class, User.class})
  2. @Configuration
  3. public class MyConfig {
  4. }

甚至,如果想偷懒,不想写这种MyConfig类,springboot也欢迎。

  1. @Import({Role.class, User.class})
  2. @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class,
  3. DataSourceTransactionManagerAutoConfiguration.class})
  4. public class Application {
  5. public static void main(String[] args) {
  6. new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);
  7. }
  8. }

可以将@Import加到SpringBoot的启动类上。
这样也能生效?
SpringBoot的启动类一般都会加@SpringBootApplication注解,该注解上加了@SpringBootConfiguration注解。
2021-07-10-08-26-46-371894.png
@SpringBootConfiguration注解,上面又加了@Configuration注解
2021-07-10-08-26-46-479028.png
所以,SpringBoot启动类本身带有@Configuration注解的功能。

4.2 Configuration

上面介绍了@Import注解导入普通类的方法,它同时也支持导入Configuration类。
先定义一个Configuration类:

  1. @Configuration
  2. public class MyConfig2 {
  3. @Bean
  4. public User user() {
  5. return new User();
  6. }
  7. @Bean
  8. public Role role() {
  9. return new Role();
  10. }
  11. }

然后在另外一个Configuration类中引入前面的Configuration类:

  1. @Import({MyConfig2.class})
  2. @Configuration
  3. public class MyConfig {
  4. }

这种方式,如果MyConfig2类已经在Spring指定的扫描目录或者子目录下,则MyConfig类会显得有点多余。因为MyConfig2类本身就是一个配置类,它里面就能定义bean。
但如果MyConfig2类不在指定的Spring扫描目录或者子目录下,则通过MyConfig类的导入功能,也能把MyConfig2类识别成配置类。这就有点厉害了喔。
其实下面还有更高端的玩法。
swagger作为一个优秀的文档生成框架,在Spring项目中越来越受欢迎。接下来,以swagger2为例,介绍一下它是如何导入相关类的。
众所周知,引入swagger相关jar包之后,只需要在SpringBoot的启动类上加上@EnableSwagger2注解,就能开启swagger的功能。
其中@EnableSwagger2注解中导入了Swagger2DocumentationConfiguration类。
2021-07-10-08-26-46-567094.png
该类是一个Configuration类,它又导入了另外两个类:

  • SpringfoxWebMvcConfiguration
  • SwaggerCommonConfiguration

2021-07-10-08-26-46-669079.png
SpringfoxWebMvcConfiguration类又会导入新的Configuration类,并且通过@ComponentScan注解扫描了一些其他的路径。
2021-07-10-08-26-46-848353.png
SwaggerCommonConfiguration同样也通过@ComponentScan注解扫描了一些额外的路径。
2021-07-10-08-26-46-956354.png
如此一来,通过一个简单的@EnableSwagger2注解,就能轻松的导入swagger所需的一系列bean,并且拥有swagger的功能。

4.3 ImportSelector

上面提到的Configuration类,它的功能非常强大。但怎么说呢,它不太适合加复杂的判断条件,根据某些条件定义这些bean,根据另外的条件定义那些bean。
那么,这种需求该怎么实现呢?
这时就可以使用ImportSelector接口了。
首先定义一个类实现ImportSelector接口:

  1. public class DataImportSelector implements ImportSelector {
  2. @Override
  3. public String[] selectImports(AnnotationMetadata importingClassMetadata) {
  4. return new String[]{"com.sue.async.service.User", "com.sue.async.service.Role"};
  5. }
  6. }

重写selectImports方法,在该方法中指定需要定义bean的类名,注意要包含完整路径,而非相对路径。
然后在MyConfig类上@Import导入这个类即可:

  1. @Import({DataImportSelector.class})
  2. @Configuration
  3. public class MyConfig {
  4. }

不过,这个注解还有更厉害的用途。
@EnableAutoConfiguration注解中导入了AutoConfigurationImportSelector类,并且里面包含系统参数名称:spring.boot.enableautoconfiguration
2021-07-10-08-26-47-048352.png
AutoConfigurationImportSelector类实现了ImportSelector接口。
2021-07-10-08-26-47-225586.png
并且重写了selectImports方法,该方法会根据某些注解去找所有需要创建bean的类名,然后返回这些类名。其中在查找这些类名之前,先调用isEnabled方法,判断是否需要继续查找。
2021-07-10-08-26-47-417881.png
该方法会根据ENABLED_OVERRIDE_PROPERTY的值来作为判断条件。
2021-07-10-08-26-47-526196.png
而这个值就是spring.boot.enableautoconfiguration
换句话说,这里能根据系统参数控制bean是否需要被实例化。
实现ImportSelector接口的好处主要有以下两点:

  1. 把某个功能的相关类,可以放到一起,方面管理和维护。
  2. 重写selectImports方法时,能够根据条件判断某些类是否需要被实例化,或者某个条件实例化这些bean,其他的条件实例化那些bean等。能够非常灵活的定制化bean的实例化。

    4.4 ImportBeanDefinitionRegistrar

    通过上面的这种方式,确实能够非常灵活的自定义bean。
    但它的自定义能力,还是有限的,它没法自定义bean的名称和作用域等属性。
    有需求,就有解决方案。
    接下来,一起看看ImportBeanDefinitionRegistrar接口的神奇之处。
    先定义CustomImportSelector类实现ImportBeanDefinitionRegistrar接口:

    1. public class CustomImportSelector implements ImportBeanDefinitionRegistrar {
    2. @Override
    3. public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
    4. RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class);
    5. registry.registerBeanDefinition("role", roleBeanDefinition);
    6. RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
    7. userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
    8. registry.registerBeanDefinition("user", userBeanDefinition);
    9. }
    10. }

    重写registerBeanDefinitions方法,在该方法中可以获取BeanDefinitionRegistry对象,通过它去注册bean。不过在注册bean之前,先要创建BeanDefinition对象,它里面可以自定义bean的名称、作用域等很多参数。
    然后在MyConfig类上导入上面的类:

    1. @Import({CustomImportSelector.class})
    2. @Configuration
    3. public class MyConfig {
    4. }

    所熟悉的fegin功能,就是使用ImportBeanDefinitionRegistrar接口实现的:
    2021-07-10-08-26-47-668779.png

    5、PostProcessor

    除此之外,Spring还提供了专门注册bean的接口:BeanDefinitionRegistryPostProcessor
    该接口的方法postProcessBeanDefinitionRegistry上有这样一段描述:
    2021-07-10-08-26-47-820042.png
    修改应用程序上下文的内部bean定义注册表标准初始化。所有常规bean定义都将被加载,但是还没有bean被实例化。这允许进一步添加在下一个后处理阶段开始之前定义bean。
    如果用这个接口来定义bean,要做的事情就变得非常简单了。只需定义一个类实现BeanDefinitionRegistryPostProcessor接口。

    1. @Component
    2. public class MyRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {
    3. @Override
    4. public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
    5. RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class);
    6. registry.registerBeanDefinition("role", roleBeanDefinition);
    7. RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
    8. userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
    9. registry.registerBeanDefinition("user", userBeanDefinition);
    10. }
    11. @Override
    12. public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    13. }
    14. }

    重写postProcessBeanDefinitionRegistry方法,在该方法中能够获取BeanDefinitionRegistry对象,它负责bean的注册工作。
    里面还多了一个postProcessBeanFactory方法,没有做任何实现。
    这个方法其实是它的父接口:BeanFactoryPostProcessor里的方法。
    2021-07-10-08-26-47-913796.png
    在应用程序上下文的标准bean工厂之后修改其内部bean工厂初始化。所有bean定义都已加载,但没有bean将被实例化。这允许重写或添加属性甚至可以初始化bean。

    1. @Component
    2. public class MyPostProcessor implements BeanFactoryPostProcessor {
    3. @Override
    4. public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    5. DefaultListableBeanFactory registry = (DefaultListableBeanFactory)beanFactory;
    6. RootBeanDefinition roleBeanDefinition = new RootBeanDefinition(Role.class);
    7. registry.registerBeanDefinition("role", roleBeanDefinition);
    8. RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
    9. userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
    10. registry.registerBeanDefinition("user", userBeanDefinition);
    11. }
    12. }

    既然这两个接口都能注册bean,那么他们有什么区别?

  • BeanDefinitionRegistryPostProcessor 更侧重于bean的注册
  • BeanFactoryPostProcessor 更侧重于对已经注册的bean的属性进行修改,虽然也可以注册bean。

此时,有些朋友可能会问:既然拿到BeanDefinitionRegistry对象就能注册bean,那通过BeanFactoryAware的方式是不是也能注册bean呢?
从下面这张图能够看出DefaultListableBeanFactory就实现了BeanDefinitionRegistry接口。
2021-07-10-08-26-48-007543.png
这样一来,如果能够获取DefaultListableBeanFactory对象的实例,然后调用它的注册方法,不就可以注册bean了?
说时迟那时快,定义一个类实现BeanFactoryAware接口:

  1. @Component
  2. public class BeanFactoryRegistry implements BeanFactoryAware {
  3. @Override
  4. public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
  5. DefaultListableBeanFactory registry = (DefaultListableBeanFactory) beanFactory;
  6. RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(User.class);
  7. registry.registerBeanDefinition("user", rootBeanDefinition);
  8. RootBeanDefinition userBeanDefinition = new RootBeanDefinition(User.class);
  9. userBeanDefinition.setScope(ConfigurableBeanFactory.SCOPE_PROTOTYPE);
  10. registry.registerBeanDefinition("user", userBeanDefinition);
  11. }
  12. }

重写setBeanFactory方法,在该方法中能够获取BeanFactory对象,它能够强制转换成DefaultListableBeanFactory对象,然后通过该对象的实例注册bean。
发现竟然报错了:
2021-07-10-08-26-48-109989.png
为什么会报错?
Spring中bean的创建过程顺序大致如下:
2021-07-10-08-26-48-220900.png
BeanFactoryAware接口是在bean创建成功,并且完成依赖注入之后,在真正初始化之前才被调用的。在这个时候去注册bean意义不大,因为这个接口是获取bean的,并不建议去注册bean,会引发很多问题。
此外,ApplicationContextRegistryApplicationListener接口也有类似的问题,可以用他们获取bean,但不建议用它们注册bean。