• Spring中如何@Bean与@Component用在同一个类上会怎么样?
  • 发布于 1个月前
  • 77 热度
    0 评论
前言
最近,在进行开发的过程中,发现之前的一个写法,类似如下:
@Configuration
public class UserConfig {
    @Value("${user.name}")
    private String userName;

    @Bean
    public UserManager userManager() {
        UserManager userManager = new UserManager(userName);
        return userManager;
    }
}
@Configuration 加 @Bean 会创建一个 userName 不为 null 的 UserManager 对象
@Component
public class UserManager {
    private String userName;
    
    public UserManager() {
        System.out.println("User 空构造方法 this = " + this);
    }
    public UserManager(String userName) {
        this.userName = userName;
        System.out.println("User 非空构造方法 this = " + this);
    }
    public String getUserName(){
        return userName;
    }
}
@Component 也会创建一个 userName 为 null 的 UserManager 对象
.那么我们在其他对象中注入 UserManager 对象时,到底注入的是哪个对象?
.Spring 容器中到底有几个 UserManager 类型的对象?
验证

验证方式有很多,可以 debug 跟源码,看看 Spring 容器中到底有几个 UserManager 对象,也可以直接从 UserManager 构造方法下手,看看哪几个构造方法被调用等等。我们从构造方法下手,看看 UserManager 到底实例化了几次?

备注:spring-boot-starter-parent版本2.0.3.RELEASE


查看控制台输出
2024-04-13 12:43:12.682  INFO 8184 --- [           main] org.example.TestApplication              : Starting TestApplication on weijsh with PID 8184 (E:\IdeaProjects\self\target\classes started by yian in E:\IdeaProjects\self)
2024-04-13 12:43:12.687  INFO 8184 --- [           main] org.example.TestApplication              : No active profile set, falling back to default profiles: default
2024-04-13 12:43:12.777  INFO 8184 --- [           main] ConfigServletWebServerApplicationContext : Refreshing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5ad851c9: startup date [Sat Apr 13 12:43:12 CST 2024]; root of context hierarchy
2024-04-13 12:43:13.627  INFO 8184 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'userManager' with a different definition: replacing [Generic bean: class [org.example.entity.UserManager]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [E:\IdeaProjects\self\target\classes\org\example\entity\UserManager.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=userConfig; factoryMethodName=userManager; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/example/config/UserConfig.class]]
2024-04-13 12:43:14.624  INFO 8184 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2024-04-13 12:43:14.661  INFO 8184 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-04-13 12:43:14.661  INFO 8184 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.31
2024-04-13 12:43:14.667  INFO 8184 --- [ost-startStop-1] o.a.catalina.core.AprLifecycleListener   : The APR based Apache Tomcat Native library which allows optimal performance in production environments was not found on the java.library.path: [D:\software\jdk\jdk1.8.0_181\bin;C:\Windows\Sun\Java\bin;C:\Windows\system32;C:\Windows;D:\software\vmware\bin\;C:\Windows\system32;C:\Windows;C:\Windows\System32\Wbem;C:\Windows\System32\WindowsPowerShell\v1.0\;C:\Windows\System32\OpenSSH\;C:\Program Files (x86)\ATI Technologies\ATI.ACE\Core-Static;D:\software\jdk\jdk1.8.0_181\bin;D:\software\maven\apache-maven-3.6.0\bin;D:\software\git\cmd;C:\Users\Administrator\AppData\Local\Microsoft\WindowsApps;;.]
2024-04-13 12:43:14.767  INFO 8184 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-04-13 12:43:14.767  INFO 8184 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1991 ms
2024-04-13 12:43:14.982  INFO 8184 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Servlet dispatcherServlet mapped to [/]
2024-04-13 12:43:14.988  INFO 8184 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2024-04-13 12:43:14.988  INFO 8184 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2024-04-13 12:43:14.989  INFO 8184 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2024-04-13 12:43:14.989  INFO 8184 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
User 非空构造方法 this = org.example.entity.UserManager@14b030a0
2024-04-13 12:43:15.348  INFO 8184 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2024-04-13 12:43:15.686  INFO 8184 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5ad851c9: startup date [Sat Apr 13 12:43:12 CST 2024]; root of context hierarchy
2024-04-13 12:43:15.749  INFO 8184 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2024-04-13 12:43:15.750  INFO 8184 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2024-04-13 12:43:15.780  INFO 8184 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2024-04-13 12:43:15.781  INFO 8184 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2024-04-13 12:43:16.164  INFO 8184 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2024-04-13 12:43:16.224  INFO 8184 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2024-04-13 12:43:16.229  INFO 8184 --- [           main] org.example.TestApplication              : Started TestApplication in 4.242 seconds (JVM running for 6.265)
2024-04-13 12:43:22.723  INFO 8184 --- [      Thread-10] ConfigServletWebServerApplicationContext : Closing org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext@5ad851c9: startup date [Sat Apr 13 12:43:12 CST 2024]; root of context hierarchy
2024-04-13 12:43:22.733  INFO 8184 --- [      Thread-10] o.s.j.e.a.AnnotationMBeanExporter        : Unregistering JMX-exposed beans on shutdown
只有有参构造方法被调用了,无参构造方法岿然不动(根本没被调用),答案也就清晰了,没得选了呀,只能是 @Configuration加 @Bean 创建的  的 UserManager 对象

源码解析
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
    /**
     * Explicitly specify the name of the Spring bean definition associated
     * with this Configuration class. If left unspecified (the common case),
     * a bean name will be automatically generated.
     * <p>The custom name applies only if the Configuration class is picked up via
     * component scanning or supplied directly to a {@link AnnotationConfigApplicationContext}.
     * If the Configuration class is registered as a traditional XML bean definition,
     * the name/id of the bean element will take precedence.
     * @return the suggested component name, if any (or empty String otherwise)
     * @see org.springframework.beans.factory.support.DefaultBeanNameGenerator
     */
    @AliasFor(annotation = Component.class)
    String value() default "";
}
@Configuration能够修饰Class、interface和enum,用的最多的还是标注在类上,相当于把该类作为spring的xml配置文件中的<beans>,用于配置spring容器,@Configuration往往会结合@Bean来使用,组成了基于Java类的配置,是spring的推荐配置方式。

其中 ConfigurationClassPostProcessor 与 @Configuration 息息相关,其类继承结构图如下:

它实现了BeanFactoryPostProcessor接口和PriorityOrdered接口,关于BeanFactoryPostProcessor,那么我们从AbstractApplicationContext的refresh方法调用的invokeBeanFactoryPostProcessors(beanFactory)开始,来跟下源码:


此时完成了com.lee.qsl包下的componentscan及子包下的UserConfig、UserController和UserManager都被扫描出来

注意,此刻@Bean的处理还未开始,UserManager是通过@Component 而被扫描出来的;此时Spring容器中beanDefinitionMap中的UserManager是这样的

接下来一步很重要,与我们想要的答案息息相关

for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
    BeanDefinition bdCand = holder.getBeanDefinition( ).get0riginatingBeanDefinition( );
    if (bdCand == null) {
        bdCand = holder.getBeanDefinition( );
        if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
            parse(bdCand.getBeanClassName( ),holder.getBeanName());
        }
    }
}
循环递归处理 UserConfig 、 UserController 和 UserManager ,把它们都封装成 ConfigurationClass ,递归扫描 BeanDefinition 循环完之后,我们来看看 configClasses

UserConfig bean定义信息中beanMethods中有一个元素[BeanMethod:name=userManager,declaringClass=com.lee.qsl.config.UserConfig]然后我们接着往下走,来仔细看看答案出现的环节

是不是有什么发现?@Component修饰的UserManager定义直接被覆盖成了@Configuration + @Bean修饰的UserManager定义,Bean定义类型也由ScannedGenericBeanDefinition替换成了ConfigurationClassBeanDefinition 。后续通过BeanDefinition创建实例的时候,创建的自然就是@Configuration + @Bean修饰的UserManager,也就是会反射调用UserManager的有参构造方法。

自此,答案也就清楚了,Spring其实给出了提示
#堆代码 duidaima.com
2024-04-13 14:29:36.185  INFO 8956 --- [           main] o.s.b.f.s.DefaultListableBeanFactory     : Overriding bean definition for bean 'userManager' with a different definition: replacing [Generic bean: class [org.example.entity.UserManager]; scope=singleton; abstract=false; lazyInit=false; autowireMode=0; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=null; factoryMethodName=null; initMethodName=null; destroyMethodName=null; defined in file [E:\IdeaProjects\self\target\classes\org\example\entity\UserManager.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=userConfig; factoryMethodName=userManager; initMethodName=null; destroyMethodName=(inferred); defined in class path resource [org/example/config/UserConfig.class]]
Spring 升级优化
可能Spring团队意识到了info级别太不显眼的问题,或者说意识到了直接覆盖的处理方式不太合理,所以在 Spring 5.1.2.RELEASE (Spring Boot 2.1.0.RELEASE)做出了优化处理,我们来具体看看。启动直接报错,Spring 也给出了提示
Description:

The bean 'userManager', defined in class path resource [org/example/config/UserConfig.class], could not be registered. A bean with that name has already been defined in file [E:\IdeaProjects\self\target\classes\org\example\entity\UserManager.class] and overriding is disabled.

Action:

Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
我们来跟下源码,主要看看与 Spring 5.0.7.RELEASE 的区别

新增了配置项allowBeanDefinitionOverriding来控制是否允许BeanDefinition覆盖,默认情况下是不允许的,我们可以在配置文件中配置:spring.main.allow-bean-definition-overriding=true ,允许BeanDefinition覆盖。
用户评论