你只管使用,一切交给IOC。

通过组件注册,IOC容器就自动帮助我们管理bean。​

创建项目,引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>

@Bean注册组件

创建一个实例类User

1
2
3
4
5
6
7
8
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class User {
private Long id;
private String name;
}

接着创建一个配置类,在配置类中通过@Bean注解注册User类:

1
2
3
4
5
6
7
8
@Configuration
public class Config {

@Bean
public User user() {
return new User();
}
}

在SpringBoot入口类中编写如下代码:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
User user = applicationContext.getBean(User.class);
System.out.println(user);
}
}

运行启动类,控制台输出如下:

1
User(id=1, name=KHighness)

@ComponentSacn扫描包

在这个SpringBoot启动类所在包下创建其他类:UserDao、UserService、UserController,并且标注上响应注解,给User类标注@Component注解。

并修改配置类:

1
2
3
4
5
@Configuration
@ComponentScan("top.parak")
public class WebConfig {

}

在启动类中获取所有的bean:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
String[] names = applicationContext.getBeanDefinitionNames();
Arrays.stream(names).forEach(System.out::println);
}
}

运行启动类,观察控制台,有如下输出:

1
2
3
4
5
6
7
KHighnessApplication
org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
webConfig
userController
userDao
user
userService

@Scope组件作用域

默认情况下,在Spring的IOC容器中每个组件都是单例的,即无论在任何地方注入多少次,这些对象都是同一个。

将配置类修改回去:

1
2
3
4
5
6
7
8
@Configuration
public class WebConfig {

@Bean
public User user() {
return new User(1L, "KHighness");
}
}

修改SpringBoot启动类如下:

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
Object user1 = applicationContext.getBean("user");
Object user2 = applicationContext.getBean("user");
System.out.println(user1 == user2);
}
}

运行启动类,控制台输出如下:

1
true

可以通过@Scope修改作用域,具体如下:

类型 描述
singleton 单实例(默认),在Spring IOC容器启动的时候会调用方法创建对象然后纳入到IOC容器中,以后每次获取都是直接从IOC容器中获取
prototype 多实例,IOC容器启动的时候并不会去创建对象,而是在每次获取的时候才会去调用方法创建对象
request 一个请求对应一个实例
session 同一个session对应一个实例

@Lazy懒加载

懒加载是针对单例模式而言的,容器启动的时候会调用方法创建对象然后纳入到IOC容器中。

在User注册的地方打印一句话以观察:

1
2
3
4
5
@Bean
public User user() {
System.out.println("向IOC容器中注册user");
return new User(1L, "KHighness");
}

在IOC容器启动后加入一句话以观察:

1
2
3
4
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
System.out.println("IOC容器创建完毕");

运行启动类,观察控制台,有如下输出:

1
2
3
4
5
向IOC容器中注册user
2021-06-28 16:43:13.064 INFO 49772 --- [ main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
2021-06-28 16:43:13.165 INFO 49772 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2021-06-28 16:43:13.168 INFO 49772 --- [ main] top.parak.KHighnessApplication : Started KHighnessApplication in 1.081 seconds (JVM running for 1.667)
IOC容器创建完毕

可以看到,在IOC容器创建完毕之前,组件已经注册到容器中。

将User修改为懒加载注册:

1
2
3
4
5
6
@Bean
@Lazy
public User user() {
System.out.println("向IOC容器中注册user");
return new User(1L, "KHighness");
}

将启动类修改一下,获取user:

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
System.out.println("IOC容器创建完毕");
Object user = applicationContext.getBean("user");
System.out.println(user);
}
}

运行启动类,观察控制台,有如下输出:

1
2
3
IOC容器创建完毕
向IOC容器中注册user
User(id=1, name=KHighness)

从输出看到,创建时并没有注册user,获取时才去注册,可以证明懒加载成功。​

条件注册

@Conditional

使用@Conditional注解我们指定组件注册的条件,即满足特定条件才将组件纳入到IOC容器中。

在使用该注解之前,先创建一个类,实现Condition接口:

1
2
3
4
5
6
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
return false;
}
}

该接口包含一个matches方法,包含两个入参:

  1. ConditionContext:上下文信息;
  2. AnnotatedTypeMetadata:注解信息。

简单完善一下实现类:

1
2
3
4
5
6
7
public class MyCondition implements Condition {
@Override
public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
String osName = conditionContext.getEnvironment().getProperty("os.name");
return osName != null && osName.contains("windows");
}
}

将这个条件添加到注册User的地方:

1
2
3
4
5
6
@Bean
@Conditional(MyCondition.class)
public User user() {
System.out.println("向IOC容器中注册user");
return new User(1L, "KHighness");
}

在Windows系统下,User会被成功注册,其他操作系统下,这个组件则不会被注册到IOC容器中。

@Profile

@Profile可以根据不同的环境变量来注册不同的组件。

新建一个接口CalculateService,用于求和:

1
2
3
4
public interface CalculateService {
Integer sum(Integer... value);
}

接着添加两个实现类Java7CalculateServiceImplJava8CalculateServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
@Service
@Profile("java7")
public class Java7CalculateServiceImpl implements CalculateService {
@Override
public Integer sum(Integer... value) {
System.out.println("Java7环境下执行");
int res = 0;
for (int val : value)
res += val;
return res;
}
}
1
2
3
4
5
6
7
8
9
@Service
@Profile("java8")
public class Java8CalculateServiceImpl implements CalculateService {
@Override
public Integer sum(Integer... value) {
System.out.println("Java8环境下执行");
return Arrays.stream(value).reduce(0, Integer::sum);
}
}

通过@Profile注解我们实现了:当环境变量包含java7的时候,Java7CalculateServiceImpl将会被注册到IOC容器中;当环境变量包含java8的时候,Java8CalculateServiceImpl将会被注册到IOC容器中。

修改启动类如下:

1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.profiles("java8")
.run(args);
CalculateService service = applicationContext.getBean(CalculateService.class);
System.out.println("求和结果:" + service.sum(1, 2, 3, 4, 5));
}
}

运行启动类,观察控制台,有如下输出:

1
2
Java8环境下执行
求和结果:15

同理,如果设置了环境变量java7的时候,将会输出Java7环境下执行

导入组件

@Import

上面学习了使用@Bean和@@ComponentScan扫描实现组件注册,除此之外,我们还可以使用@Import来快速地往IOC容器中添加组件。

创建一个新的类Hello

1
2
public class Hello {
}

然后在配置类中导入这个组件:

1
2
3
@Configuration
@Import({Hello.class})
public class WebConfig {...

修改启动类,查看所有组件的名称:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class KHighnessApplication {
public static void main(String[] args) {
ConfigurableApplicationContext applicationContext = new SpringApplicationBuilder(KHighnessApplication.class)
.web(WebApplicationType.SERVLET)
.run(args);
String[] names = applicationContext.getBeanDefinitionNames();
Arrays.stream(names).forEach(System.out::println);
}
}

运行启动类,观察控制台,有如下输出:

1
top.parak.entity.Hello

可以看到,通过@Import我们可以快速向容器中添加组件,默认名称为全类名。

ImportSelector

通过@Import可以实现组件导入,如果需要一次性导入较多组件,我们可以使用@ImportSelector来实现。

查看ImportSelector源码:

1
2
3
4
5
6
7
8
9
public interface ImportSelector {

/**
* Select and return the names of which class(es) should be imported based on
* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);

}

ImportSelector是一个接口,包含一个selectImports方法,方法返回的类的全类名数组(即需要导入到IOC容器中组件的全类名数组),包含一个@AnnotationMetadata类型入参,通过这个参数我们可以获取到使用ImportSelector的类的全部注解信息。

新建三个类,分别为OrangeBananaPineapple

新建一个ImportSelector实现类MyImportSelector

1
2
3
4
5
6
7
8
9
10
public class MyImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
return new String[] {
"top.parak.entity.Orange",
"top.parak.entity.Banana",
"top.parak.entity.Pineapple"
};
}
}

接着通过@Import注解将MyImportSelector把丧组件快速导入到容器中:

1
2
@Import(MyImportSelector.class)
public class WebConfig {...

运行启动类,观察控制台,发现有如下输出:

1
2
3
top.parak.entity.Orange
top.parak.entity.Banana
top.parak.entity.Pineapple

说明三个组件已经批量导入。

ImportBeanDefinitionRegistor

除了上述两种向IOC容器中导入组件的方法外,我们还可以使用ImportBeanDefinitionRegistrar来手动往IOC容器中导入组件。

查看ImportBeanDefinitionRegistrar源码:

1
2
3
4
5
6
public interface ImportBeanDefinitionRegistrar {

default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
}

}

ImportBeanDefinitionRegistrar是一个接口,包含一个registerBeanDefinitions方法,该方法包含两个入参:

  1. AnnotationMetadata:可以通过它获取类的注解信息;
  2. BeanDefinitionRegistry:Bean定义注册器,包含了一些和Bean有关的方法:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public interface BeanDefinitionRegistry extends AliasRegistry {

    void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
    throws BeanDefinitionStoreException;

    void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

    BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

    boolean containsBeanDefinition(String beanName);

    String[] getBeanDefinitionNames();

    int getBeanDefinitionCount();

    boolean isBeanNameInUse(String beanName);

    }
    这里借助BeanDefinitionRegistryregisterBeanDefinition方法往IOC容器中注册Bean。该方法包含两个入参,第一个参数为需要注册的Bean名称,第二个参数为Bean的定义信息,它是一个接口,我们可以用其实现类RootBeanDefinition来完成:

为了演示ImportBeanDefinitionRegistrar的使用,新增一个类Blueberry

创建一个ImportBeanDefinitionRegistrar实现类MyImportBeanDefinitionRegistrar

1
2
3
4
5
6
7
8
9
10
11
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
final String beanName = "blueberry";
boolean contain = registry.containsBeanDefinition(beanName);
if (!contain) {
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Blueberry.class);
registry.registerBeanDefinition(beanName, rootBeanDefinition);
}
}
}

然后在配置类中导入:

1
2
3
@Configuration
@Import({MyImportBeanDefinitionRegistrar.class})
public class WebConfig {...

运行启动类,观察控制台,有如下输出:

1
blueberry

说明组件注册成功。

FactoryBean

Spring提供了FactoryBean接口,我们可以通过该接口来注册组件,该接口包含两个抽象方法和一个默认方法:


为了演示FactoryBean的作用,新建一个类Cherry

然后创建FactoryBean的实现类CherryFactoryBean

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CherryFactoryBean implements FactoryBean<Cherry> {

@Override
public Cherry getObject() throws Exception {
return new Cherry();
}

@Override
public Class<?> getObjectType() {
return Cherry.class;
}

@Override
public boolean isSingleton() {
return false;
}
}

最后在配置类中注册即可:

1
2
3
4
@Bean
public CherryFactoryBean cherryFactoryBean() {
return new CherryFactoryBean();
}

修改启动类,获取cherryFactoryBean

1
2
Object bean = applicationContext.getBean("cherryFactoryBean");
System.out.println(bean.getClass());

运行启动类,观察控制台,有如下输出:

1
class top.parak.entity.Cherry

可以看到,虽然获取的是ID为cherryFactoryBean的组件,但是实际上获取到的是getObject里返回的对象。

如果要获取cherryFactoryBean本身,则可以这样做:

1
2
Object bean = applicationContext.getBean("&cherryFactoryBean");
System.out.println(bean.getClass());

重新运行,输出如下:

1
class top.parak.entity.CherryFactoryBean

为什么这里加上&前缀就可以获取到相应的工厂类?

查看BeanFactory源码就可以看到原因:

1
2
3
4
5
6
7
8
9
public interface BeanFactory {

/**
* Used to dereference a {@link FactoryBean} instance and distinguish it from
* beans <i>created</i> by the FactoryBean. For example, if the bean named
* {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject}
* will return the factory, not the instance returned by the factory.
*/
String FACTORY_BEAN_PREFIX = "&";