Spring Bean 定义常见错误

        Spring 的核心是围绕 Bean 进行的。不管是 Spring Boot 还是 Spring Cloud,只要名称中带有 Spring 关键字的技术都脱离不了 Bean,而要使用一个 Bean 少不了要先定义出来,所以定义一个 Bean 就变得格外重要了。

当然,对于这么重要的工作,Spring 自然给我们提供了很多简单易用的方式。然而,这种简单易用得益于 Spring 的“约定大于配置”,但我们往往不见得会对所有的约定都了然于胸,所以仍然会在 Bean 的定义上犯一些经典的错误。

接下来我们就来了解下那些经典错误以及它们背后的原理,你也可以对照着去看看自己是否也曾犯过,后来又是如何解决的。

案例 1:隐式扫描不到 Bean 的定义

在构建 Web 服务时,我们常使用 Spring Boot 来快速构建。例如,使用下面的包结构和相关代码来完成一个简易的 Web 版 HelloWorld:

 其中,负责启动程序的 Application 类定义如下:

package com.spring.puzzle.class1.example1.application
//省略 import
@SpringBootApplication
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

提供接口的 HelloWorldController 代码如下:

package com.spring.puzzle.class1.example1.application
//省略 import
@RestController
public class HelloWorldController {@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld";};
}

上述代码即可实现一个简单的功能:访问http://localhost:8080/hi 返回 helloworld。两个关键类位于同一个包(即 application)中。其中 HelloWorldController 因为添加了 @RestController,最终被识别成一个 Controller 的 Bean。

但是,假设有一天,当我们需要添加多个类似的 Controller,同时又希望用更清晰的包层次和结构来管理时,我们可能会去单独建立一个独立于 application 包之外的 Controller 包,并调整类的位置。调整后结构示意如下:

案例解析

要了解 HelloWorldController 为什么会失效,就需要先了解之前是如何生效的。对于 Spring Boot 而言,关键点在于 Application.java 中使用了 SpringBootApplication 注解。而这个注解继承了另外一些注解,具体定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
//省略非关键代码
}

从定义可以看出,SpringBootApplication 开启了很多功能,其中一个关键功能就是 ComponentScan,参考其配置如下:

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class)

当 Spring Boot 启动时,ComponentScan 的启用意味着会去扫描出所有定义的 Bean,那么扫描什么位置呢?这是由 ComponentScan 注解的 basePackages 属性指定的,具体可参考如下定义:

public @interface ComponentScan {/*** Base packages to scan for annotated components.* <p>{@link #value} is an alias for (and mutually exclusive with) this* attribute.* <p>Use {@link #basePackageClasses} for a type-safe alternative to* String-based package names.*/
@AliasFor("value")
String[] basePackages() default {};
//省略其他非关键代码
}

而在我们的案例中,我们直接使用的是 SpringBootApplication 注解定义的 ComponentScan,它的 basePackages 没有指定,所以默认为空(即{})。此时扫描的是什么包?这里不妨带着这个问题去调试下(调试位置参考 ComponentScanAnnotationParser#parse 方法),调试视图如下:

从上图可以看出,当 basePackages 为空时,扫描的包会是 declaringClass 所在的包,在本案例中,declaringClass 就是 Application.class,所以扫描的包其实就是它所在的包,即 com.spring.puzzle.class1.example1.application。

对比我们重组包结构前后,我们自然就找到了这个问题的根源:在调整前,HelloWorldController 在扫描范围内,而调整后,它已经远离了扫描范围(不和 Application.java 一个包了),虽然代码没有一丝丝改变,但是这个功能已经失效了。

所以,综合来看,这个问题是因为我们不够了解 Spring Boot 的默认扫描规则引起的。我们仅仅享受了它的便捷,但是并未了解它背后的故事,所以稍作变化,就可能玩不转了。

问题修正

针对这个案例,有了源码的剖析,我们可以快速找到解决方案了。当然了,我们所谓的解决方案肯定不是说把 HelloWorldController 移动回原来的位置,而是真正去满足需求。在这里,真正解决问题的方式是显式配置 @ComponentScan。具体修改方式如下: 

@SpringBootApplication
@ComponentScan("com.spring.puzzle.class1.example1.controller")
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

通过上述修改,我们显式指定了扫描的范围为 com.spring.puzzle.class1.example1.controller。不过需要注意的是,显式指定后,默认的扫描范围(即 com.spring.puzzle.class1.example1.application)就不会被添加进去了。另外,我们也可以使用 @ComponentScans 来修复问题,使用方式如下:

@ComponentScans(value = { @ComponentScan(value = "com.spring.puzzle.class1.example1.controller") })

顾名思义,可以看出 ComponentScans 相比较 ComponentScan 多了一个 s,支持多个包的扫描范围指定。

此时,细心的你可能会发现:如果对源码缺乏了解,很容易会顾此失彼。以 ComponentScan 为例,原有的代码扫描了默认包而忽略了其它包;而一旦显式指定其它包,原来的默认扫描包就被忽略了。

案例 2:定义的 Bean 缺少隐式依赖

初学 Spring 时,我们往往不能快速转化思维。例如,在程序开发过程中,有时候,一方面我们把一个类定义成 Bean,同时又觉得这个 Bean 的定义除了加了一些 Spring 注解外,并没有什么不同。所以在后续使用时,有时候我们会不假思索地去随意定义它,例如我们会写出下面这样的代码:

@Service
public class ServiceImpl {private String serviceName;public ServiceImpl(String serviceName){this.serviceName = serviceName;}}

ServiceImpl 因为标记为 @Service 而成为一个 Bean。另外我们 ServiceImpl 显式定义了一个构造器。但是,上面的代码不是永远都能正确运行的,有时候会报下面这种错误:

Parameter 0 of constructor in com.spring.puzzle.class1.example2.ServiceImpl required a bean of type 'java.lang.String' that could not be found.

那这种错误是怎么发生的呢?下面我们来分析一下。

案例解析

当创建一个 Bean 时,调用的方法是

AbstractAutowireCapableBeanFactory#createBeanInstance。它主要包含两大基本步骤:寻找构造器和通过反射调用构造器创建实例。对于这个案例,最核心的代码执行,你可以参考下面的代码片段:

// Candidate constructors for autowiring?
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {return autowireConstructor(beanName, mbd, ctors, args);
}

Spring 会先执行 determineConstructorsFromBeanPostProcessors 方法来获取构造器,然后通过 autowireConstructor 方法带着构造器去创建实例。很明显,在本案例中只有一个构造器,所以非常容易跟踪这个问题。

autowireConstructor 方法要创建实例,不仅需要知道是哪个构造器,还需要知道构造器对应的参数,这点从最后创建实例的方法名也可以看出,参考如下(即 ConstructorResolver#instantiate):

private Object instantiate(String beanName, RootBeanDefinition mbd, Constructor<?> constructorToUse, Object[] argsToUse) 

那么上述方法中存储构造参数的 argsToUse 如何获取呢?换言之,当我们已经知道构造器 ServiceImpl(String serviceName),要创建出 ServiceImpl 实例,如何确定 serviceName 的值是多少?

很明显,这里是在使用 Spring,我们不能直接显式使用 new 关键字来创建实例。Spring 只能是去寻找依赖来作为构造器调用参数。

那么这个参数如何获取呢?可以参考下面的代码片段(即 ConstructorResolver#autowireConstructor):

argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw, paramTypes, paramNames,getUserDeclaredConstructor(candidate), autowiring, candidates.length == 1);

我们可以调用 createArgumentArray 方法来构建调用构造器的参数数组,而这个方法的最终实现是从 BeanFactory 中获取 Bean,可以参考下述调用:

return this.beanFactory.resolveDependency(new DependencyDescriptor(param, true), beanName, autowiredBeanNames, typeConverter);

如果用调试视图,我们则可以看到更多的信息:

 如图所示,上述的调用即是根据参数来寻找对应的 Bean,在本案例中,如果找不到对应的 Bean 就会抛出异常,提示装配失败。

问题修正

从源码级别了解了错误的原因后,现在反思为什么会出现这个错误。追根溯源,正如开头所述,因为不了解很多隐式的规则:我们定义一个类为 Bean,如果再显式定义了构造器,那么这个 Bean 在构建时,会自动根据构造器参数定义寻找对应的 Bean,然后反射创建出这个 Bean。

了解了这个隐式规则后,解决这个问题就简单多了。我们可以直接定义一个能让 Spring 装配给 ServiceImpl 构造器参数的 Bean,例如定义如下:

//这个bean装配给ServiceImpl的构造器参数“serviceName”
@Bean
public String serviceName(){return "MyServiceName";
}

再次运行程序,发现一切正常了。

所以,我们在使用 Spring 时,不要总想着定义的 Bean 也可以在非 Spring 场合直接用 new 关键字显式使用,这种思路是不可取的

另外,类似的,假设我们不了解 Spring 的隐式规则,在修正问题后,我们可能写出更多看似可以运行的程序,代码如下:

@Service
public class ServiceImpl {private String serviceName;public ServiceImpl(String serviceName){this.serviceName = serviceName;}public ServiceImpl(String serviceName, String otherStringParameter){this.serviceName = serviceName;}
}

如果我们仍用非 Spring 的思维去审阅这段代码,可能不会觉得有什么问题,毕竟 String 类型可以自动装配了,无非就是增加了一个 String 类型的参数而已。

但是如果你了解 Spring 内部是用反射来构建 Bean 的话,就不难发现问题所在:存在两个构造器,都可以调用时,到底应该调用哪个呢?最终 Spring 无从选择,只能尝试去调用默认构造器,而这个默认构造器又不存在,所以测试这个程序它会出错。

案例 3:原型 Bean 被固定

接下来,我们再来看另外一个关于 Bean 定义不生效的案例。在定义 Bean 时,有时候我们会使用原型 Bean,例如定义如下:

@Service
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class ServiceImpl {
}

然后我们按照下面的方式去使用它:

@RestController
public class HelloWorldController {@Autowiredprivate ServiceImpl serviceImpl;@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + serviceImpl;};
}

结果,我们会发现,不管我们访问多少次http://localhost:8080/hi,访问的结果都是不变的,如下:

helloworld, service is : com.spring.puzzle.class1.example3.error.ServiceImpl@4908af

很明显,这很可能和我们定义 ServiceImpl 为原型 Bean 的初衷背道而驰,如何理解这个现象呢?

案例解析

当一个属性成员 serviceImpl 声明为 @Autowired 后,那么在创建 HelloWorldController 这个 Bean 时,会先使用构造器反射出实例,然后来装配各个标记为 @Autowired 的属性成员(装配方法参考 AbstractAutowireCapableBeanFactory#populateBean)。

具体到执行过程,它会使用很多 BeanPostProcessor 来做完成工作,其中一种是 AutowiredAnnotationBeanPostProcessor,它会通过 DefaultListableBeanFactory#findAutowireCandidates 寻找到 ServiceImpl 类型的 Bean,然后设置给对应的属性(即 serviceImpl 成员)。

关键执行步骤可参考

AutowiredAnnotationBeanPostProcessor.AutowiredFieldElement#inject:

protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {Field field = (Field) this.member;Object value;//寻找“bean”if (this.cached) {value = resolvedCachedArgument(beanName, this.cachedFieldValue);}else {//省略其他非关键代码value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);}if (value != null) {//将bean设置给成员字段ReflectionUtils.makeAccessible(field);field.set(bean, value);}
}

 待我们寻找到要自动注入的 Bean 后,即可通过反射设置给对应的 field。这个 field 的执行只发生了一次,所以后续就固定起来了,它并不会因为 ServiceImpl 标记了 SCOPE_PROTOTYPE 而改变。

所以,当一个单例的 Bean,使用 autowired 注解标记其属性时,你一定要注意这个属性值会被固定下来。

问题修正

通过上述源码分析,我们可以知道要修正这个问题,肯定是不能将 ServiceImpl 的 Bean 固定到属性上的,而应该是每次使用时都会重新获取一次。所以这里我提供了两种修正方式:

1. 自动注入 Context

即自动注入 ApplicationContext,然后定义 getServiceImpl() 方法,在方法中获取一个新的 ServiceImpl 类型实例。修正代码如下:

@RestController
public class HelloWorldController {@Autowiredprivate ApplicationContext applicationContext;@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + getServiceImpl();};public ServiceImpl getServiceImpl(){return applicationContext.getBean(ServiceImpl.class);}}

2. 使用 Lookup 注解

类似修正方法 1,也添加一个 getServiceImpl 方法,不过这个方法是被 Lookup 标记的。修正代码如下:

@RestController
public class HelloWorldController {@RequestMapping(path = "hi", method = RequestMethod.GET)public String hi(){return "helloworld, service is : " + getServiceImpl();};@Lookuppublic ServiceImpl getServiceImpl(){return null;}  }

通过这两种修正方式,再次测试程序,我们会发现结果已经符合预期(每次访问这个接口,都会创建新的 Bean)。

重点回顾

不难发现,要使用好 Spring,就一定要了解它的一些潜规则,例如默认扫描 Bean 的范围、自动装配构造器等等。如果我们不了解这些规则,大多情况下虽然也能工作,但是稍微变化,则可能完全失效,例如在案例 1 中,我们也只是把 Controller 从一个包移动到另外一个包,接口就失效了。

另外,通过这三个案例的分析,我们也能感受到 Spring 的很多实现是通过反射来完成的,了解了这点,对于理解它的源码实现会大有帮助。例如在案例 2 中,为什么定义了多个构造器就可能报错,因为使用反射方式来创建实例必须要明确使用的是哪一个构造器。

最后,我想说,在 Spring 框架中,解决问题的方式往往有多种,不要拘泥于套路。就像案例 3,使用 ApplicationContext 和 Lookup 注解,都能解决原型 Bean 被固定的问题一样。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/662754.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2024人工智能可以报考的证书有哪些

人工智能&#xff08;AI&#xff09;是致力于解决通常与人类智能相关联的认知性问题的计算机科学领域&#xff0c;这些问题包括学习、创造和图像识别等。 关键的人工智能技术有哪些&#xff1f; 深度学习神经网络构成了人工智能技术的核心。神经网络反映了人脑中发生的过程。大…

JOSEF约瑟 HRTH-Y-2H2D跳位合位 电源监视继电器 导轨安装

HRTH-Y-2H2D跳位合位 电源监视继电器&#xff1a; HRTH-Y-2H2D-X-T&#xff1b;HRTH-Y-2Z-X-T&#xff1b;HRTH-Y-2H-X-T&#xff1b; HRTH-J-2H2D-X-T&#xff1b;HRTH-J-2Z-X-T;HRTH-J -2H-X-T&#xff1b; HRTH-Y-2H2D&#xff1b;HRTH-Y-2Z&#xff1b;HRTH-Y-2H&#xff1…

遇到ubuntu设置交叉编译环境的问题

今天交叉编译器一直没安装成功&#xff0c;环境变量也配置了还是不对&#xff0c;最后发现Ubuntu是64位的要装 然后就好了 另外在进行嵌入式Linux开发的时候&#xff0c;要把主机、虚拟机、以及开发板设置在同一网段下&#xff0c;虚拟机一般设成临时的就可以&#xff0c;但是…

CS144--Chapter0--wsl2+docker环境搭建

我的笔记本配置 荣耀magicbook16&#xff0c;容量是500G&#xff0c;芯片是R7-5800 由于笔记本容量较小&#xff0c;因此考虑这个方案&#xff0c;对于台式机用户&#xff0c;建议可以直接用虚拟机或者双系统。 前言 斯坦福官网给出的方法是用他们的镜像&#xff08;基于Ubu…

抖音短视频矩阵营销系统源头独立开发搭建

开发背景 抖音短视频矩阵系统源码开发采用模块化设计&#xff0c;包括账号分析、营销活动、数据监控、自动化管理等功能。通过综合分析账号数据&#xff0c;快速发现账号的优势和不足&#xff0c;并提供全面的营销方案&#xff0c;以提高账号曝光率和粉丝数量。同时&#xff0c…

vue2 组件注册

简单分享怎么将组件注册为全局组件&#xff0c;主要分为三部分&#xff1a; 一、使用 Vue.install 方法将自义定的组件挂载到 Vue 实例上&#xff0c;如下&#xff1a; 二、注册为全局组件&#xff0c;如下&#xff1a; 三、页面使用&#xff0c;如下&#xff1a;

Oracle 12.2 暴力处理sysaux空间占满问题

基本环境 数据库&#xff1a;oracle 12.2 RAC 操作系统&#xff1a;unix&solaris 11.3 报错现像 今天处理别的问题查看告警日志偶然发现大量的报错&#xff0c;无法扩展SYSAUX表空间 于是登录系统&#xff0c;查看系统表空间使用情况&#xff0c;发现SYSAUX表空间用满了 …

【知识点】Java常用

文章目录 基础基础数据类型内部类Java IOIO多路复用重要概念 Channel **通道**重要概念 Buffer **数据缓存区**重要概念 Selector **选择器** 关键字final 元注解常用接口异常处理ErrorException JVM与虚拟机JVM内存模型本地方法栈虚拟机栈 Stack堆 Heap方法区 Method Area (JD…

深兰科技陈海波出席CTDC2024第五届首席技术官领袖峰会:“民主化AI”的到来势如破竹

1月26日&#xff0c;CTDC 2024 第五届首席技术官领袖峰会暨出海创新峰会在上海举行。深兰科技创始人、董事长陈海波受邀出席了本届会议&#xff0c;并作为首个演讲嘉宾做了题为“前AGI时代的生产力革命范式”的行业分享。 作为国内顶级前瞻性技术峰会&#xff0c;CTDC首席技术官…

【lesson34】基础IO之引入fd(文件描述符)

文章目录 基础IO要讲的知识点介绍引入fd&#xff08;文件描述符&#xff09; 基础IO要讲的知识点介绍 1.复习一下C语言的接口 2.直接使用系统接口 3.分析系统接口的细节&#xff0c;引入fd&#xff08;文件描述符&#xff09; 4.fd的周边问题&#xff08;fd的理解、fd和file的…

2024年美赛 (C题MCM)| 温网积分 |数学建模完整代码+建模过程全解全析

当大家面临着复杂的数学建模问题时&#xff0c;你是否曾经感到茫然无措&#xff1f;作为2022年美国大学生数学建模比赛的O奖得主&#xff0c;我为大家提供了一套优秀的解题思路&#xff0c;让你轻松应对各种难题。 让我们来看看美赛的C题&#xff01; 完整内容可以在文章末尾领…

虚拟机安装archlinux

1、创建虚拟机 2、安装系统4、为了方便&#xff0c;修改密码并使用dos窗口连接 5、磁盘分区 由于新建虚拟机时是8G&#xff0c;所以只建一个分区就行 6、格式化分区并挂载 7、更新镜像 rootarchiso ~ # pacman -Sy 8、 pacstrap -i /mnt base base-devel linux linux-f…

2024-02-02 work

1. 整理C语言指针&#xff0c;函数指针&#xff0c;数组指针&#xff0c;指针数组的区别并给出一个简单示例

7、应急响应-战中溯源反制对抗上线蚁剑CSGobySqlmap等安全工具

用途&#xff1a;个人学习笔记&#xff0c;欢迎指正 目录 背景&#xff1a; 一、后门修改反制上线 二、Linux溯源反制-SQL注入工具-SQLMAP 1、测试反弹编码加密&#xff1a; 2、构造注入点页面test.php固定注入参数值&#xff0c;等待攻击者进行注入 3、红队攻击者进行注…

期权定价模型系列[12]SVI随机波动率模型

SVI模型 SVI 模型由 Gatheral&#xff08;2004&#xff09;提出&#xff0c;模型假定市场不存在日历套利机会和蝶式套利机会&#xff0c; 并在这个条件下构建一个一般化参数模型&#xff0c;具体形式为&#xff1a; SVI模型的原理是基于市场数据进行 SVI 表达式的参数优化&am…

MacBook安装虚拟机VMware Fusion

MacBook安装虚拟机VMware Fusion 官方下载地址: https://customerconnect.vmware.com/cn/downloads/info/slug/desktop_end_user_computing/vmware_fusion/11_0 介绍 之前的版本都要收费,现在出了对个人免费的版本, 棋哥给的破解版的版本是8,升级系统后用不了了. 官方去下载…

Flink实时数仓同步:流水表实战详解

一、背景 在大数据领域&#xff0c;初始阶段业务数据通常被存储于关系型数据库&#xff0c;如MySQL。然而&#xff0c;为满足日常分析和报表等需求&#xff0c;大数据平台采用多种同步方式&#xff0c;以适应这些业务数据的不同存储需求。这些同步存储方式包括离线仓库和实时仓…

【2024美赛】A题(中英文):资源可用性与性别比例Problem A: Resource Availability and Sex Ratios

【2024美赛】A题&#xff08;中英文&#xff09;&#xff1a;资源可用性与性别比例Problem A: Resource Availability and Sex Ratios 写在最前面2024美赛翻译 —— 跳转链接 中文赛题问题A&#xff1a;资源可用性与性别比例需要检查的问题包括&#xff1a; 英文赛题Problem A:…

【HarmonyOS应用开发】Web组件的使用(十三)

文章末尾含&#xff1a;Web组件抽奖案例&#xff08;ArkTS&#xff09;-示例源码下载 Web组件的使用 一、概述 相信大家都遇到过这样的场景&#xff0c;有时候我们点击应用的页面&#xff0c;会跳转到一个类似浏览器加载的页面&#xff0c;加载完成后&#xff0c;才显示这个页…

数据结构 归并排序详解

1.基本思想 归并排序&#xff08;MERGE-SORT&#xff09;是建立在归并操作上的一种有效的排序算法,该算法是采用分治法&#xff08;Divide andConquer&#xff09;的一个非常典型的应用。 将已有序的子序列合并&#xff0c;得到完全有序的序列&#xff0c;即先使每个子序列有序…