Spring - 常见编程错误之Bean的定义
问题一: 启动类没有扫描到 Bean
案例演示
项目结构:注意写个配置文件。
1.pom
文件:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.2.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2.启动类:
@SpringBootApplication
public class Main8080 {
public static void main(String[] args) {
SpringApplication.run(Main8080.class, args);
}
}
3.Controller
类:
@Controller
public class MyController {
@GetMapping("/hello")
@ResponseBody
public String hello() {
return "hello world";
}
}
结果如下:
原理分析
从启动类的注解@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 {
// ... 省略
@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
String[] scanBasePackages() default {};
}
我们都知道用@ComponentScan
这个注解可以指定扫描包的路径。也看到了@SpringBootApplication
注解里面包含了scanBasePackages
这个属性,允许我们在启动的时候指定对应的扫描包路径。只不过我们的案例中并没有去显示的调用,因此默认是{}
。
因此我们再来看下@EnableAutoConfiguration
这个注解,它主要将各种项目中需要用到的类自动导入进来:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
我们关注其中的@AutoConfigurationPackage
注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {}
这里可见通过@Import
注解引入了Registrar
这个类:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
// 进行Bean的注册
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
// ...
}
我们可以看出,在注册Bean
的时候,通过构造函数创建了一个PackageImports
类,重点来了,我们看下这个构造:
PackageImports(AnnotationMetadata metadata) {
AnnotationAttributes attributes = AnnotationAttributes
.fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));
List<String> packageNames = new ArrayList<>();
// ...
// 因为我们没有去指定任何的扫描包路径,因此这里的packageNames是空的
if (packageNames.isEmpty()) {
packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));
}
this.packageNames = Collections.unmodifiableList(packageNames);
}
如果我们在启动类上没有声明任何的扫描包路径,那么就会根据当前这个启动类的元数据信息,去创建出对应的扫描路径:
此时对应的扫描路径为:com.application
而Spring
会扫描com.application
这个路径以及其子路径下的所有Bean
。而我们项目中的Controller
类所在路径,并不在这个扫描路径下,因此它并不会被加载到Spring
容器中。因此我们调用它的相关方法是不可行的。
解决方案
将Controller
类(以及你Spring
项目中需要用到的Bean
)放入到启动类所在路径及之下:
重新访问对应路径:http://localhost:8080/hello
切记:启动类应该放到项目的最外层。 也可以顺带复习下我写的另一篇文章:SpringBoot自动装配原理。
问题二: 有参构造 Bean 报错
案例演示
创建一个带有 有参构造 函数的类
@Component
public class User {
private String name;
public User(String name) {
this.name = name;
}
}
运行起来出错(当你代码这么写的时候就已经提示出错了):
原理分析
背景:我们给Bean
创建了一个有参构造函数。
这里做一个简单的介绍,Spring
创建Bean
的时候,都是通过AbstractAutowireCapableBeanFactory.createBean()
方法中实现的
@Override
protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
try {
// 创建bean
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
// ..
return beanInstance;
}
// ...catch
}
doCreateBean()
函数主要是三个步骤(这里很重要,也是个重点):
createBeanInstance()
:创建实例。populateBean()
:属性注入。initializeBean()
:初始化。
那么毫无疑问的,构造函数的调用就是属于第一个步骤,创建实例阶段了。
protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
// 1.根据Class属性来解析Class
Class<?> beanClass = resolveBeanClass(mbd, beanName);
// ...
// 根据参数来解析构造函数
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
// 构造函数注入
return autowireConstructor(beanName, mbd, ctors, args);
}
// 构造函数注入
ctors = mbd.getPreferredConstructors();
if (ctors != null) {
return autowireConstructor(beanName, mbd, ctors, null);
}
// 若以上都不命中,则使用默认构造来创建
return instantiateBean(beanName, mbd);
}
这里我们主要关注两类代码:
autowireConstructor(beanName, mbd, ctors, args);
:根据名称、构造函数、对应的构造参数去创建一个实例。instantiateBean(beanName, mbd);
:调用无参构造。
那么问题来了,我们案例提供的User
类,自带一个有参构造函数,参数是name
,那么这里对应的应该走有参构造的实例创建逻辑,如图:(只要显式地创建了有参构造,都会优先走有参构造的逻辑)
可是呢,再看下args
参数:它是null
。
由于传入的args
参数为null
,因此无法正确地执行有参构造函数,因此就报错了,并且还附带建议:
Consider defining a bean of type 'java.lang.String' in your configuration.
什么意思呢?来看下下面的解决方案.
解决方案
第一种,根据人家给的建议,给出一个String
类型的Bean
,作为User
这个有参构造的参数。我们可以写个自定义的配置类:
@Configuration
public class MyConfig {
@Bean
public String userName() {
return "Hello";
}
}
Controller
类做出修改:(User
类加个get/set
方法)
@Controller
public class MyController {
@Autowired
private User user;
@GetMapping("/hello")
@ResponseBody
public String hello() {
return user.getUserName();
}
}
结果如下:
这种方式并不推荐,一般情况下,我们对于Spring
的Bean
,是不会自己去定义一个有参构造的,那么第二种:我们只需要把有参构造删除即可,让程序走默认的无参构造逻辑。
最后,关于Spring
中Bean
的创建加载原理,可以看下我写的这篇文章Spring源码系列:Bean的加载。
问题三: 原型 Bean 竟指向同一个对象
案例演示
User
类:指定其Scope
为prototype
。(即多例,希望每次调用都是一个不同的对象,默认情况下Spring
容器中的Bean
都是单例的)
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class User {
}
Controller
类:
@Autowired
private User user;
@GetMapping("/hello")
@ResponseBody
public String hello() {
return Math.random() + user.toString();
}
结果如下:
可以发现,不管调用几次,都是同一个User
对象。原型 prototype
失效了。
原理分析
代码里我们通过@Autowired
注解引入的userBean
,那么我们看下@Autowired
注解源码:
其本身没啥特殊的,但是呢但是呢,上面的注释写着,请看AutowiredAnnotationBeanPostProcessor
这个类,我们看下这个类的类关系图:
它是BeanPostProcessor
的一个实现类,BeanPostProcessor
用于动态的修改应用程序上下文bean
。即做后处理操作,这时候bean
已经实例化成功。
那么就可以知道,通过@Autowired
注解引入的userBean
,必定是经过了一个后处理动作。 我们看下它实现的MergedBeanDefinitionPostProcessor
接口下的postProcessMergedBeanDefinition
方法。
@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
metadata.checkConfigMembers(beanDefinition);
}
↓↓↓↓↓↓↓↓↓↓findAutowiringMetadata↓↓↓↓↓↓↓↓↓↓
private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
// Fall back to class name as cache key, for backwards compatibility with custom callers.
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
// Quick check on the concurrent map first, with minimal locking.
InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
synchronized (this.injectionMetadataCache) {
metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
if (metadata != null) {
metadata.clear(pvs);
}
// 可以猜以下,这行代码是这里最为关键的地方
metadata = buildAutowiringMetadata(clazz);
this.injectionMetadataCache.put(cacheKey, metadata);
}
}
}
return metadata;
}
最终的重要逻辑在于buildAutowiringMetadata()
函数,关于元数据的构建:
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
return InjectionMetadata.EMPTY;
}
List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
// 要处理的目标对象
Class<?> targetClass = clazz;
// 这一块本质就是区别字段属性和方法,通过反射拿到他们。
do {
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
// 拿到字段元数据信息,并放入到集合elements中
ReflectionUtils.doWithLocalFields(targetClass, field -> {
// ...
});
// 拿到方法元数据信息,并放入到集合elements中
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
// ...
});
elements.addAll(0, currElements);
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);
// 将元数据信息和目标类封装成InjectionMetadata对象
return InjectionMetadata.forElements(elements, clazz);
}
public static InjectionMetadata forElements(Collection<InjectedElement> elements, Class<?> clazz) {
return (elements.isEmpty() ? InjectionMetadata.EMPTY : new InjectionMetadata(clazz, elements));
}
到这里为止,目标类所需的相关字段、方法的元数据信息已经拿到并且封装好,并且加入到一个集合中。接下来就需要进行属性的注入操作了。
上文提到过,创建一个bean
一共有三个步骤,第二个步骤就是属性的注入操作。也就是执行populateBean
函数:
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {
// ..
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
// ..
pvs = pvsToUse;
}
}
// ..
}
可见这里我们执行了postProcessProperties()
方法,巧的是,AutowiredAnnotationBeanPostProcessor
这个类中就有对应的实现:
@Override
public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
// ..
metadata.inject(bean, beanName, pvs);
return pvs;
}
public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
Collection<InjectedElement> checkedElements = this.checkedElements;
Collection<InjectedElement> elementsToIterate =
(checkedElements != null ? checkedElements : this.injectedElements);
// 这里就是第一步中,关于实例的创建过程中,元数据集合。里面包含目标类所需的字段、方法等信息。
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
// ..
element.inject(target, beanName, pvs);
}
}
}
到这里为止:
protected void inject(Object target, @Nullable String requestingBeanName, @Nullable PropertyValues pvs)
throws Throwable {
// 反射去将值赋值给对应的字段
if (this.isField) {
Field field = (Field) this.member;
ReflectionUtils.makeAccessible(field);
field.set(target, getResourceToInject(target, requestingBeanName));
}
else {
// 如果是方法,就通过反射的方式执行对应的方法即可。
if (checkPropertySkipping(pvs)) {
return;
}
try {
Method method = (Method) this.member;
ReflectionUtils.makeAccessible(method);
method.invoke(target, getResourceToInject(target, requestingBeanName));
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}
总结以下很简单:
- 通过
@Autowired
注解引入的类,Spring
会执行对应的后处理动作。相关入口在于AutowiredAnnotationBeanPostProcessor
这个类。 AutowiredAnnotationBeanPostProcessor
类实现了两个接口,主要做两件事。postProcessMergedBeanDefinition
方法负责将目标类的字段和方法元数据信息收集起来。postProcessProperties()
负责将对应的属性进行赋值。如果是方法,就执行一遍。
那么回到案例本身,为什么我指定了prototype
原型,每次调用的对象还是同一个呢?
- 这个对象通过
@Autowired
注解引入。会对这个类做对应的后处理操作。 - 最后通过反射的方式,对这个类的字段进行赋值,方法被调用。但是这个过程只执行了一次。即通过
@Autowired
注解引入这一个时机。之后这个值就被固定下来了。 - 可以这么说,通过
@Autowired
注解引入的对象一定是单例的。
解决方案
我们只需要解决这一点:
- 我们换一种方式去引入这个
bean
。保证每次请求,拿到的bean
都是新的,而不是通过@Autowired
注解被固定住的。
方案一:
@Autowired
private ApplicationContext applicationContext;
@GetMapping("/hello")
@ResponseBody
public String hello() {
return Math.random() + getUser().toString();
}
public User getUser() {
return applicationContext.getBean(User.class);
}
结果如下:
方式二:通过Lookup
注解:
@GetMapping("/hello")
@ResponseBody
public String hello() {
return Math.random() + getUser().toString();
}
@Lookup
public User getUser() {
return null;
}
这种方式的原理就不多解释了,大概就是:
@Lookup
注解代表可以在运行时用新方法代替现有的方法。- 最后会通过
BeanFactory
来获取Bean
。因此能保证每次请求都能重新获取一次Bean
。
详细可以看Spring源码系列:标签的解析原理中关于 lookup-method
标签的作用的讲解。
总结
Springboot
中的启动类,切记要放到项目架构的最外层。因为默认会根据你启动类所在的包路径去扫描。- 项目中没有事不要在声明
Bean
的时候害带有参构造,如果用了请注意传参问题。否则就会报错。另外,如果有多个有参构造,Spring
会默认使用无参构造。它并不知道你使用的究竟是哪一个。 - 使用
@Autowired
引入的bean
是个单例,哪怕你对其设置了原型prototype
也没有用。因为它做了后处理操作,通过反射机制来进行赋值,这个过程只执行一次,因此使用@Autowired
引入的bean
是固定不变的。