封面画师:adsuger     封面ID:79919479

SpringBoot 配置类

1. 问题由来

一日,我在水群,一小伙伴在做SpringBoot国际化的代码时提出:

  • “类上面加上@Configuration了,为何还要通过@Bean才能生效?”
  • 我便凭着我三脚猫的Spring只是给他说了@Configuration和@Bean的含义与区别。
  • 小伙伴反驳道:“在类上使用@Configuration了,这个类就是组件了,为何还要使用@Bean添加组件呢?”
  • 我很不解,便又给他解释了一遍:@Configuration相当于是<beans>标签,这个标签内啥都没有,如果向给里面添加组件,就要用到<bean>标签来添加组件,而对应的就是@Bean注解。
  • 小伙伴却说:@Configuration中有@Component,也是组件啊?
  • 我半信半疑,打开IDEA,点开俩注解,卧槽?😱
  • 小伙伴又说:为什么我SpringBoot配置类中有些方法不使用@Bean注解却又不会报错?但是有些不使用却又要报错?
1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
// 比如这个方法,添加一个拦截器,没有使用@Bean,但是生效又不报错
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/index.html","/","/user/login",
"/css/*","/js/*","/img/*");
}
}
  • 最后,我在风中凌乱,我好菜… 😭

从这件事可以引出以下几个问题:

  1. Spring中@Component与@Bean的区别
  2. SpringBoot中配置类下有些情况不使用@Bean也能进行配置,但有些情况必须使用@Bean
  3. Spring中@Configuration与@Bean
  4. Spring中@Configuration与@Component的区别

我们接下来针对这几个问题进行一一解答!👊

2. @Component与@Bean的区别

先来几个参考链接,慢慢看,中文英文都有、国内国外也有:😜

Spring中@Component与@Bean的区别

Spring中@Component与@Bean的区别

@Component 和 @Bean 的区别

@Bean和@Component之间的区别?

纯英文原版参考链接:Spring series, part 5: @Component vs @Bean


我们知道:@Component@Bean的目的是一样的,都是注册bean到Spring容器中。

对于@Component及其衍生注解,它们注释在类上,从而告诉Spring,这些类是一个bean,通过类路径扫描自动检测并注入到Spring容器中。因此,我们在Spring项目中一般需要使用XML配置或@ComponentScan注解来扫描包,而在SpringBoot项目中,由于其自动扫描包的特性,SpringBoot会自动扫描与启动类同属目录及以下目录的包。

对于@Bean注解,它可以作用与方法和注释上,只能用于在配置类中显式声明单个bean。当@Bean注解在@Configuration注解的配置类中的方法上时,将方法的返回值作为Bean对象存入到Spring的IoC容器中。

这里就可以引出@Configuration和@Bean为什么要搭配使用,@Configuration和@Component的区别,这些问题在下文进行解释。

为什么@Component和@Bean不是“既生瑜何生亮”的关系?

假设我们有一个需要在多个应用程序中共享的模块,这个模块包含了一些服务,但并非所有应用都需要这些服务。如果在这些服务类上使用@Component并在应用程序中使用组件扫描,我们最终可能会检测到超过必要的Bean数量,不需要的Bean也扫描加载了。这时候必须调整组件扫描的过滤或提供即使未使用的Bean也可以运行的配置,否则,Spring应用程序上下文将无法启动。在这种情况下,最好使用@Bean注释并仅实例化那些在每个应用程序中单独需要的Bean。

除此之外,如果我们使用了第三方库中的组件(类),此时我们没有源码,无法在类上使用@Component注解,此时就需要使用@Bean,将方法的返回值作为Bean对象存入到Spring的IoC容器中。这里的分析就可以参考我们最开始说的@Component@Bean的区别了。

3. SpringBoot 配置类中@Bean的使用

从问题由来中,我们提出一个问题:SpringBoot配置类下有些情况不使用@Bean也能进行配置,但有些情况必须使用@Bean。

为什么SpringBoot配置类下有些情况不使用@Bean也能进行配置

这个问题其实十分简单,我们使用第三方配置时需要使用@Bean添加到Spring容器中,而使用重写的方法不需要@Bean注解,因为@Configuration将涉及的组件添加到容器中了。

我们可以把配置类看成一个Service层的实现类,我们可以直接在实现类中编写实现的方法,然后使用@Service注解将实现类注册到Bean中,就可以使用接口调用实现类中的方法。

我们可以进行一个实验:

编写配置类:

1
2
3
4
5
6
7
8
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
// 随便重写一个方法,这个方法没有含义,进行的配置也是为了方便测试
@Override
public void configureViewResolvers(ViewResolverRegistry registry) {
registry.jsp("/WEB-INF/jsp/html", ".html");
}
}

然后,我们进行测试,全局搜索:doDispatch()方法,并打一个断点,方便测试:

doDispatch

然后Debug模式启动程序:

MyMvcConfig_configureViewResolvers_test1

我们可以看到,我们进行的配置出现在Spring容器中,至于为什么会出现在这个位置,下面给出两张类图,或许可以简单地解释它:

InternalResourceViewResolver

ViewResolverComposite

至于为什么会出现在viewResolver下,我们在 SpringBoot-Web开发须知 一文中已经说过, 实现了视图解析器接口的类,我们都可以把它当作视图解析器

我们可以得出简单的结论:SpringBoot Web开发中的配置类(配置类实现了WebMvcConfigurer),使用重写的方法不需要@Bean注解,因为SpringBoot已经将涉及的组件添加到容器中了。

那么事实真的是这样吗?是SpringBoot自动帮我们添加到容器中的吗?

我们 去掉 配置上的@Configuration,也不使用@Component及其衍生注解在配置类上,然后我们以同样的方式,使用Debug模式启动程序:

MyMvcConfig_configureViewResolvers_test2

我们发现,去掉注解后,配置不见了,配置的视图解析器不见了。

最终,我们可以得出:并不是SpringBoot自动帮我们配置了,SpringBoot只是帮我们扫描到了@Configuration注解。而配置的视图解析器能出现在Spring容器中,是依靠@Configuration注解,将配置的视图解析器添加到容器中。

为什么有些情况必须使用@Bean

接下来,我们再来说说为什么有些情况必须使用@Bean:

如果我们使用了第三方库中的组件(类),此时我们没有源码,无法在类上使用@Component注解,此时就需要使用@Bean,将方法的返回值作为Bean对象存入到Spring的IoC容器中。这里的分析就可以参考我们最开始说的@Component@Bean的区别了。

4. @Configuration与@Bean

主要解决:为什么@Bean要依赖@Configuration

@Configuration@Bean是Spring中常用的用于配置的Bean的两个注解(将@Bean注解的方法返回的类注入spring)。

@Configuration相当于是<beans>标签,这个标签内啥都没有,如果向给里面添加组件,就要用到<bean>标签来添加组件,而对应的就是@Bean注解。

那么@Configuration@Bean为什么要一起使用?

我们先看看@Bean注解的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
@AliasFor("name")
String[] value() default {};
@AliasFor("value")
String[] name() default {};
@Deprecated
Autowire autowire() default Autowire.NO;
boolean autowireCandidate() default true;
String initMethod() default "";
String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
}

我们可以看到,@Bean注解只能作用与方法和注解之上,无法作用于类上。

既然如此,想单独使用@Bean肯定是不行的了。什么?不知道为啥不能单独使用?😶

其实很简单,想想就知道了,因为@Bean的作用限制,Spring起码得扫描到类才能吧,而要扫描到类,就需要用@Configuration或者@Component注解,所以单个@Bean是不能单独使用的。

我们可以回忆一下,我们在Spring中是怎么使用注解的​~😏

如果还是有XML文件,我们至少需要进行以下配置来扫描注解:

1
<context:component-scan base-package="XXXXX"/>

如果没有XML,至少配置类上需要有@ComponentScan注解来扫描包,在扫描包时,这些包下配置类的@Configuration注解是不能省略的(可以前往 Spring注解 一文中查看)。

SpringBoot没有XML的配置,使用的全注解形式,它会扫描与启动类同属目录及以下目录的包,因此,这个时候就需要用@Configuration或者@Component注解。那么这俩注解有啥区别呢?👇

5. @Configuration与@Component的区别

最后一个问题也来几个链接,慢慢看,深刻理解!👊

参考链接:

源码分析:Spring @Configuration 和 @Component 区别

推荐这篇: @Configuration和@Component区别 给这篇作者点个赞!👍

我们可以进入@Configuration注解:

@Configuration

1
2
3
4
5
6
7
8
9
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
@AliasFor(annotation = Component.class)
String value() default "";
boolean proxyBeanMethods() default true;
}

这时候,我们可以看到在@Configuration注解中也有@Component注解,这也是为什么我在 3. SpringBoot 配置类中@Bean的使用 中测试时要求不加@Configuration注解的同时也不要加@Component注解。因为加了Component也可以把我们编写的配置添加到容器中。

那么,既然这两个注解都可以将组件添加到容器中,他们就没有什么区别了吗?

那肯定是有区别的啊,两个东西没有区别的话,那为啥Spring官方除了@Component注解外,还要搞个@Configuration注解。


那么它俩到底有啥区别呢?

如果你看了我在这个问题前推荐的几篇文章后,已经搞懂了原因,那么接下来的内容可以无视了。如果你懒得看,也没有关系,我给你讲一讲我的理解:

首先,我们先给出结论:

在被@Configuration标记的配置类中,一个@Bean标注的方法可以调用当前类另一个被@Bean标记的方法,即上一个方法注入的bean可以被下一个方法获取使用 。但是,使用@Component就无法做到。

5.1 测试准备

接下来,我们对结论进行测试!

首先需要两个实体类,这两个实体类要存在调用的关系:

Department.java:

1
2
3
4
5
6
public class Department {
private Integer id;
private String departmentName;

// 省略自动生成的一些方法
}

User.java:

1
2
3
4
5
6
7
8
9
10
public class User{
private Integer id;
private String username;
private String password;
private String email;
private String gender;
private Department dept;

// 省略自动生成的一些方法
}

编写一个配置类MyConfig.java:

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
@Configuration
//@Component
public class MyConfig {

/*@Autowired
public Department dept;*/

@Bean
public User user(){
User user = new User();
user.setId(2);
user.setUsername("mofan");
user.setPassword("123456");
user.setGender("boy");
user.setEmail("cy.mofan@qq.com");
user.setDept(dept());
return user;
}

@Bean
//@Scope("prototype")
public Department dept(){
Department department = new Department();
department.setId(1);
department.setDepartmentName("dept");
return department;
}

}

在配置上有些注解被注释,不直接删除它们,到时候测试直接取消注释即可。

编写一个测试类:

1
2
3
4
5
6
7
8
9
10
11
12
public class tests {
public static void main(String[] args) {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(MyConfig.class);
// 获取bean
User user = context.getBean("user",User.class);
// 获取bean
Department dept = context.getBean("dept",Department.class);
boolean result = user.getDept() == dept;
System.out.println(result?"同一个dept":"不同的dept");
}
}

5.2 测试过程

在测试准备中,我们在配置类上使用了@Configuration注解,然后我们启动测试类进行测试:

相同对象

然后,我们将@Configuration注解注释掉,启用@Component注解,在这个使用我们的IDE已经出现了爆红的情况:

1
2
user.setDept(dept());
// Method annotated with @Bean is called directly. Use dependency injection instead.

但是这不影响测试,我们再次启动测试类进行测试:

不同对象

思考:这是什么情况?不是说@Configuration@Component注解差不多吗?为什么结果会这样呢?

5.3 测试总结

使用@Configuration注解后:

user()方法中的user.setDept(dept());会由Spring代理执行,Spring发现方法所请求的Bean已经在容器中,那么就直接返回容器中的Bean(这个Bean是单例的)。所以全局只有一个Department对象的实例。

使用@Component注解后:

执行user()方法中的user.setDept(dept());不会由Spring代理执行,会直接调用dept()方法获取一个 全新 的Department对象实例,所以全局有多个Department对象实例。


❗️造成出现这样结论的原因是:

@Configuration 注解的配置类中所有带 @Bean 注解的方法都会被动态代理,因此调用该方法返回的都是同一个实例。

其工作原理是:如果方式是首次被调用那么原始的方法体会被执行并且结果对象会被注册到Spring上下文中,之后所有的对该方法的调用仅仅只是从Spring上下文中取回该对象返回给调用者。

在上面的第二段代码中,user.setDept(dept());只是纯Java方式的调用,多次调用该方法返回的是不同的对象实例。

😎简单来说:

程序执行到@Configuration 注解的配置类中所有带 @Bean 注解的方法时,会检查返回的对象是否存在Spring容器中,如果不存在,就添加进行,而后,所有对该方法的调用都是直接从Spring容器中取出来使用。

5.4 结论拓展

使用@Component会造成了两个dept不同,主要就是因为使用的纯Java方式的调用,要想规避这种情况我们需要从Spring容器中获得对应的Bean就可以了,即:将容器中的Bean注入到类中。

我们可以对代码进行改写:使用@Component的同时,使用@Autowired进行依赖注入。

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
@Component
public class MyConfig {

// 使用依赖注入
@Autowired
public Department dept;

@Bean
public User user(){
User user = new User();
user.setId(2);
user.setUsername("mofan");
user.setPassword("123456");
user.setGender("boy");
user.setEmail("cy.mofan@qq.com");
// 这里也进行了修改
user.setDept(dept);
return user;
}

@Bean
//@Scope("prototype")
public Department dept(){
Department department = new Department();
department.setId(1);
department.setDepartmentName("dept");
return department;
}
}

我们再次运行测试类,查看结果:

相同对象


我们已经知道在@Configuration标记的配置类下的@Bean注解的方法,返回的是单例的对象。

Spring注解 一文中,我们已经指出@Bean注解返回的bean对象默认是单例的,如果我们想要返回的是多例的对象,我们只需要在@Bean注解下@Scope("prototype")就可以了。

我们可以在 5.1 测试准备 中,将@Scope("prototype")的注解取消,然后启动测试类并运行,查看结果为:

不同对象

6. @Component与其衍生注解

StackOverflow链接:

What’s the difference between @Component, @Repository & @Service annotations in Spring?

可以阅读一手,顺便练习英文。