Skip to main content

SpringBoot Bean 概念理解和使用

Bean 简单解释

Spring 维护一个容器(IoC),容器内有很多的Bean。如果需要某些Bean,就直接 @Autowired 即可。 Spring 会寻找对应的 Bean 自动装配,如果找不到则抛出异常。

图例: 用 Bean 之前,用 Bean 之后

iPhone 和 Android 手机都有摄像机功能。它们每次构造时都要新建摄像机对象,否则调用的时候会抛出 NullPointer

不使用 Bean,构造函数里面要手动初始化

上图可以看到,iPhone 和 Android 同时要初始化摄像机,可见已经有重复的代码了。

改成用 Bean 的方法:

声明 Bean 之后,用 @Autowired 就可以自动赋值

具体的步骤:

  1. PhoneConfig 类用 @Bean 通知 Spring 新建一个 Camera 对象,放到 Bean 池。
  2. iPhone类 @Autowired 通知 Spring 抽一个 Camera 对象过来。
  3. AndroidPhone类 同 iPhone 一样的做法。
  4. Spring 在 Bean 池找到有 Camera 这个对象,就直接装到有 @Autowired 一方。

引入 Spring Bean 后,new iPhone() 时,Camera 对象就已经被 Spring 装上去了,不用再 new Camera()

声明与装配 Bean

有很多种方法给 Spring 添加 Bean,传统的 Spring App 用 XML 定义:

用 XML 声明 bean
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Action"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>
    <bean id="myid" class="app.demo.MyBean"></bean>
</beans>

XML没有语法检查,更不会管 class 是否合理,所以容易出错,而且 XML 头部还有固定格式,我相信大家都不喜欢记这些东西。这也是 Spring 一直被社区要求改进的一点,xml配置文件维护起来一点也不方便。

我推荐用 annotation,即 @XXXX 的方法来添加 Bean。

用 @Component 和 @Bean 声明

@Component 标注在类之前,可以让 Spring 来扫描里面 Bean。

同时,这个类也会被当作 Bean。

@Component
class MyComponent {
  @Bean
  String ipv4Address () {
    return "127.0.0.1";
  }
  void print() {
    System.out.println("[MyComponent] 我不仅导出一个 String 类型的 bean,我自己也是一个 bean");
  }
}
装配示例
// 声明 SpringBootApplication 后,Spring 才知道这个类是程序入口
@SpringBootApplication
class MyApp implements CommandLineRunner{

  // 自动装上 String 类型的 Bean
  @Autowired
  String address;

  // 自动装上 MyComponent 对象
  @Autowired
  MyComponent myComponent;
  
  // 实现 CommandLineRunner 方法,将在 SpringApplication.run 时调用
  @Override
  public void run(String... args) throws Exception {
    // 不需要 this.address = XXXX 一堆东西,直接调用
    System.out.println("字符串 bean 自动装配的值: " + this.address);
    this.myComponent.print();
  }

  // SpringApplication.run 才能打开 Spring 的功能
  public static void main (String[] args) {
    SpringApplication.run(MyApp.class, args);
  }
}

运行结果

字符串 bean 自动装配的值: 127.0.0.1
[MyComponent] 我不仅导出一个 String 类型的 bean,我自己也是一个 bean

用 @Configuration 和 @Service 声明

根据源代码,@Configuration@Service@Component 有同样的效果。

@Component, @Configuration, @Service 区别

通常情况下,是为了规范 Bean 的创建和管理。

Component, Service, Configuration 负责各自功能

  • @Component 推荐声明到工具类前,@Component 内部不能再嵌 @Autowired
@Component
public class CryptoTool {}
  • @Configuration 推荐声明到配置类
@Configuration
public class MyConfig {
  @Bean
  String ipv4Address () {
    return "0.0.0.0";
  }
  @Autowired
  CryptoTool cryptoTool;
}
  • @Service 推荐声明到业务处理类
@Service
public class UserService {
  @Autowired
  CryptoTool cryptoTool;
  @Autowired
  MyConfig myConfig;
  // CRUD
}

Bean 的生命周期

Bean 默认一直存在 Spring 容器里面,供多个类引用。但是 Bean 也可以指定声明周期。

@Scope 声明 Bean 的生命周期:

@Component
class MyComponent {
  @Bean
  @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
  String ipv4Address () {
    return "0.0.0.0";
  }
}
@Scope 值 描述
SCOPE_SINGLETON @Autowired 每次装配的都是同一个对象
SCOPE_PROTOTYPE @Autowired 每次装配都会新建一个对象

用 ID 区分 Bean

刚才声明的 Bean 有一个问题,@Autowired 时根据类型装配的。如果声明多个 String Bean,则 Spring 不知道应该用哪个 Bean 装配,于是报错。

例子: 自动装配时遇到 2个字符串Bean
@Component
class MyComponent {
  @Bean
  String ipv4Address () {
    return "0.0.0.0";
  }
  @Bean
  String ipv6Address () {
    return "::";
  }
}

@Configuration
class MyConfig {
  @Autowired
  String inetAddress; 
  // 报错 expected single matching bean but found 2: ipv4Address,ipv6Address
}

给 Bean 定义 ID 可以按要求装配:

@Component
class MyComponent {
  @Bean
  @Qualifier("ipv4_addr")
  String ipv4Address () {
    return "0.0.0.0";
  }
  @Bean
  @Qualifier("ipv6_addr")
  String ipv6Address () {
    return "::";
  }
}

再通过 Bean ID 装配对应的 bean:

@Configuration
class MyConfig {
  @Autowired
  @Qualifier("ipv4_addr")
  String ipv4Address; 
  
  @Autowired
  @Qualifier("ipv6_addr")
  String ipv6Address; 
}

从配置文件装配

Spring 约定自动读取的配置文件路径: src/main/resources/application.properties

或是 YAML 格式: src/main/resources/application.yml

以下2个配置文件任选一个,它们的效果是一样的,推荐 yaml 格式

application.properties
myconfig.port=9662
application.yml
myconfig:
  port: 9662

按上面的配置文件,装配时用 @Value 注入,将自动装上 9662:

@Configuration
class MyConfig {
  @Autowired
  @Value("${myconfig.port}") // 9662,装上!
  Integer port;
}

配置文件再从环境变量读取

部署容器程序经常用到环境变量,Spring 配置文件也支持从环境变量读取。

application.yml
myconfig:
  port: ${PORT}

使用时,指定环境变量:

export PORT=9663

小结

Spring 项目不可避免地用到很多 Bean。但简单来说,Bean 只是的另一种管理对象的方式而已。

利用 Bean 可以免去很多手写代码的情形,像 new 一堆东西,然后赋值,又或者 try catch 一个读配置文件的操作,用 Autowired、Value 就可以解决了。

附录和引用

实际操作可能有遇到很多的问题,将自己遇到的简单整理一下。

定义了 Service 但是 Service 是在另一个包导致没有创建对应的 Bean (StackOverflow)

@SpringBootApplication 默认是扫描 @TA 的程序所在的包

示例: Spring怎么扫包?
import org.luo3.myapp;

@SpringBootApplication
class MyApp {}

  • SpringBootApplication 将扫描 org.luo3.myapp.* 的类并创建所需的 Bean。
  • SpringBootApplication 不扫描 org.luo4.myapp.*
  • SpringBootApplication 不扫描 org.luo3.yourapp.*
解决方法
import org.luo3.myapp;

@SpringBootApplication(scanBasePackages={"org.luo3.myapp", "org.luo3.otherpackage"})
class MyApp {}

SpringBoot application.yml 如何通过环境变量传入 (StackOverflow)