# SpringBoot3教程 - 6 注册组件

在 Spring IOC 容器中注册组件可以使用 xml 和 注解,在 SpringBoot 中全部使用注解来注册。

如果是我们自己编写的类,那么可以使用如下注解注入到 Spring 容器中:

@Component: 用于标记通用组件类。

@Controller: 用于标记控制器类。

@Service: 用于标记服务层类。

@Repository: 用于标记数据访问层类。

但是如果是别人的类,我们是没有办法修改代码添加注解的,在 SpringBoot中可以使用 Java 配置类的方式,通过@Configuration@Bean注解来定义Bean。

# 6.1 @Bean注解

在 Spring 中可以通过在 xml 配置文件中注册第三方的组件,SpringBoot 中没有 xml 配置文件了,所以都是通过类来完成的。

首先创建一个配置类,添加 @Configuration 注解,然后在其中编写方法,方法使用@Bean注解,方法返回的对象会被添加到 Spring IOC 容器中。

举个例子:

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    @Bean  // 将对象添加到Spring容器中
    public UserInfo userInfo() {
        UserInfo userInfo = new UserInfo();  // UserInfo是随意创建用来测试的类
        userInfo.setUsername("逗比笔记");
        userInfo.setAge(3);
        return userInfo;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面的配置类就相当于以前 Spring 的xml 配置文件,@Bean 注解相当于 <bean/> 标签创建的对象,对象在 Spring IOC容器中的名称就是上面方法的名称,要改名字直接使用@Bean("名称") 即可

一般情况下,使用@Component 代替 @Configuration 也能正常工作,但是推荐配置类使用 @Configuration 注解。


编写一个单元测试方法,查看能否获取容器中的对象:

package com.doubibiji.hellospringboot;

import com.doubibiji.hellospringboot.bean.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class HelloSpringbootApplicationTests {

    @Autowired
    private UserInfo userInfo;

    @Test
    void contextLoads() {
        System.out.println(userInfo);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

执行结果,可以获取到值:

UserInfo(username=逗比笔记, age=3)
1

如果在使用 @Bean 创建对象的时候,依赖于另一个 Spring 容器中的对象,只需要将另一个对象作为方法的参数即可。

举个栗子:

下面的 Dog 和 UserInfo 都是我随意创建的两个类。

UserInfo 类对象需要 Dog 类对象,只需要在方法上添加 Dog 类的参数,Spring 会自动注入。

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.Dog;
import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    @Bean
    public Dog dog() {
        Dog dog = new Dog();
        dog.setName("大黄");
        dog.setAge(2);
        return dog;
    }

    @Bean  // 将对象添加到Spring容器中
    public UserInfo userInfo(Dog dog) {
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername("逗比笔记");
        userInfo.setAge(3);
        userInfo.setDog(dog);
        return userInfo;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 6.2 @Import注解

除了使用 @Bean 注解注册组件到 Spring IOC 容器中,还可以使用 @Import 注解。

举个栗子:

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(UserInfo.class)
public class MyConfig {

}
1
2
3
4
5
6
7
8
9
10
11

上面通过 @Import 注解,将类作为 bean 注册到 Spring IOC 容器中。通过 @Import 注解的方式注册,默认情况下,对象在 Spring IOC 容器中的名字是类的全类名。

也可以一次注入多个:

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.Dog;
import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({UserInfo.class, Dog.class})
public class MyConfig {

}
1
2
3
4
5
6
7
8
9
10
11
12

但是如果要注册的类很多,这样写就很不优雅,而且麻烦。

那么可以使用下面 ImportSelector 接口的实现方式。

# 6.3 ImportSelector接口

我们可以创建一个类,实现 ImportSelector 接口,并通过实现 selectImports 方法动态决定要导入的类。

举个栗子:

package com.doubibiji.hellospringboot.config;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {
                "com.doubibiji.hellospringboot.bean.UserInfo",
                "com.doubibiji.hellospringboot.bean.Dog"
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

selectImports 方法返回的是类的全类名的数组。

然后使用 @Import 注解将 MyImportSelector 注册到 Spring IOC 容器中。这样会将selectImports 方法返回的类注册到 Spring IOC 容器中。

package com.doubibiji.hellospringboot.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import(MyImportSelector.class)
public class MyConfig {

}
1
2
3
4
5
6
7
8
9
10

在SpringBoot中很多组件,都是使用了上面这种方式,但是是将类名写在文件中,在代码中读取文件中的类名进行导入。

举个栗子,将要导入的类写到一个文件中,在 resources 目录下创建一个my.imports 文件:

com.doubibiji.hellospringboot.bean.UserInfo
com.doubibiji.hellospringboot.bean.Dog
1
2

然后在 MyImportSelector 中按行读取文件中的数据,然后返回:

package com.doubibiji.hellospringboot.config;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {

        List<String> imports = new ArrayList<>();

        //读取配置文件的内容
        InputStream is = MyImportSelector.class.getClassLoader().getResourceAsStream("my.imports");
        BufferedReader br = new BufferedReader(new InputStreamReader(is));
        String line;
        try {
            // 按行读取
            while ((line = br.readLine()) != null){
                imports.add(line);
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (null != br) {
                try {
                    br.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }

        }

        return imports.toArray(new String[0]);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

SpringBoot 中的组件都是使用类似的方式实现自动配置的。我们自己开发的项目,在项目中一般使用 @Bean 注解的方式。

# 6.4 注册条件

在前面使用 @Bean 注解,将类的对象注册到 Spring IOC容器中。

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {
    @Bean
    public UserInfo userInfo() {
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername("逗比笔记");
        userInfo.setAge(3);
        return userInfo;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

但是上面的属性值是写死在代码中的,这种方式耦合性高,不易于维护,我们可以将属性值定义在配置文件中。

# 1 配置文件注入

例如在 application.yaml中定义属性如下:

userInfo:
  username: 逗比笔记
  age: 3
1
2
3

然后在配置类中,将配置注入到 @Bean 所在方法的参数:

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    @Bean
    public UserInfo userInfo(@Value("${userInfo.username}") String username, 
                             @Value("${userInfo.age}") int age) {
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername(username);
        userInfo.setAge(age);
        return userInfo;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

可以看到 @Value 除了用在属性上,还可以用到方法的参数上。


用单元测试类测试一下:

package com.doubibiji.hellospringboot;

import com.doubibiji.hellospringboot.bean.UserInfo;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class HelloSpringbootApplicationTests {

    @Autowired  // 使用容器中的userInfo注入
    private UserInfo userInfo;

    @Test
    void contextLoads() {
        System.out.println(userInfo);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

执行结果:

UserInfo(username=逗比笔记, age=3)
1

# 2 按条件注册

但是现在有一个问题,当配置文件中没有定义 userInfo.usernameuserInfo.age 的时候,代码运行会报错,提示找不到对应的配置。

那么问题就来了,能否根据配置文件中是否有指定的配置来注册呢,如果有指定的配置,就注册Bean,如果没有,就不注册。

当然是可以的,SpringBoot 提供 @Conditional 注解,但是这个注解使用起来比较麻烦,所以还提供了很多的衍生注解,最常用的介绍三个:

  • @ConditionalOnProperty : 当配置文件中存在对应的属性,才注册该Bean;通过这个注解,可以让配置了指定的配置才注册组件。
  • @ConditionalOnMissingBean :当Spring容器中不存在指定类型的Bean时,才注册该Bean;通过这个注解,通常可以提供默认的实现,如果用户有自己的实现了,那么默认的实现就不注册了。
  • @ConditionalOnClass :当存在指定的类时(不是存在Spring容器中),才注册该Bean;如果一个组件依赖于另一个组件,那么只有另一个组件存在的时候,才注册这个组件。
  • @ConditionalOnBean :当容器中存在指定的 Bean 时,才注册该Bean。

下面介绍一下使用。


@ConditionalOnProperty 注解演示:

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    /**
     * 当存在userInfo.username和userInfo.age的时候才注册
     */
    @ConditionalOnProperty(prefix = "userInfo", name = {"username", "age"})
    @Bean
    public UserInfo userInfo(@Value("${userInfo.username}") String username,
                             @Value("${userInfo.age}") int age) {
        UserInfo userInfo = new UserInfo();
        userInfo.setUsername(username);
        userInfo.setAge(age);
        return userInfo;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

上面的代码,当存在userInfo.username和userInfo.age配置的时候才注册UserInfo的Bean。


@ConditionalOnMissingBean 注解演示:

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.Dog;
import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    @Bean
    public Dog dog() {
        Dog dog = new Dog();
        return dog;
    }

    /**
     * 如果容器中存在Dog类对象,就不注册,如果不存在,才注册
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(Dog.class)
    public UserInfo userInfo() {
        UserInfo userInfo = new UserInfo();
        return userInfo;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

上面的代码,使用 @ConditionalOnMissingBean(Dog.class) 表示当不存在 Dog类的 bean 时才注册UserInfo ,所以上面是不会注册UserInfo 的 Bean的。**需要注意,上面注册 dog() 方法必须放在 userInfo() 方法前面,如果放在后面,注解检查到没有注册 Dog 的bean,则会注册UserInfo。 **


@ConditionalOnClass 注解演示:

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    /**
     * 如果容器中存在Dog类对象,就不注册,如果不存在,才注册
     * @return
     */
    @Bean
    @ConditionalOnClass(name = "com.doubibiji.hellospringboot.bean.Dog")
    public UserInfo userInfo() {
        UserInfo userInfo = new UserInfo();
        return userInfo;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在相面的代码中,如果环境中存在 com.doubibiji.hellospringboot.bean.Dog 就注册 UserInfo,注意不是 Spring 容器中存在。

如果想判断容器中存在指定的 Bean 才注册当前的 Bean,可以时候用 @ConditionalOnBean 注解。


@ConditionalOnBean 注解演示:

package com.doubibiji.hellospringboot.config;

import com.doubibiji.hellospringboot.bean.Dog;
import com.doubibiji.hellospringboot.bean.UserInfo;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {

    @Bean
    public Dog dog() {
        Dog dog = new Dog();
        return dog;
    }

    /**
     * 如果容器中存在Dog类对象,就不注册,如果不存在,才注册
     * @return
     */
    @Bean
    @ConditionalOnBean(Dog.class)
    public UserInfo userInfo() {
        UserInfo userInfo = new UserInfo();
        return userInfo;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

在上面的代码中,@ConditionalOnBean(Dog.class) 判断的是 Spring 容器中存在 Dog 类的 Bean,才注册 UserInfo。


还有一些 @Conditional 开头的衍生注解,可以根据注解名称判断是用于怎么样的判断条件的,自行了解一下。