• 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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    2.启动类:

    @SpringBootApplication
    public class Main8080 {
        public static void main(String[] args) {
            SpringApplication.run(Main8080.class, args);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.Controller类:

    @Controller
    public class MyController {
        @GetMapping("/hello")
        @ResponseBody
        public String hello() {
            return "hello world";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    结果如下:
    在这里插入图片描述

    原理分析

    从启动类的注解@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 {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们都知道用@ComponentScan这个注解可以指定扫描包的路径。也看到了@SpringBootApplication注解里面包含了scanBasePackages这个属性,允许我们在启动的时候指定对应的扫描包路径。只不过我们的案例中并没有去显示的调用,因此默认是{}

    因此我们再来看下@EnableAutoConfiguration这个注解,它主要将各种项目中需要用到的类自动导入进来:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们关注其中的@AutoConfigurationPackage注解:

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @Import(AutoConfigurationPackages.Registrar.class)
    public @interface AutoConfigurationPackage {}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这里可见通过@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]));
    	}
    	// ...
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们可以看出,在注册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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果我们在启动类上没有声明任何的扫描包路径,那么就会根据当前这个启动类的元数据信息,去创建出对应的扫描路径:
    在这里插入图片描述
    此时对应的扫描路径为: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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    运行起来出错(当你代码这么写的时候就已经提示出错了):
    在这里插入图片描述

    原理分析

    背景:我们给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
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    doCreateBean()函数主要是三个步骤(这里很重要,也是个重点):

    1. createBeanInstance()创建实例
    2. populateBean()属性注入
    3. 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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这里我们主要关注两类代码:

    • 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.
    
    • 1

    什么意思呢?来看下下面的解决方案.

    解决方案

    第一种,根据人家给的建议,给出一个String类型的Bean,作为User这个有参构造的参数。我们可以写个自定义的配置类:

    @Configuration
    public class MyConfig {
        @Bean
        public String userName() {
            return "Hello";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Controller类做出修改:(User类加个get/set方法)

    @Controller
    public class MyController {
        @Autowired
        private User user;
    
        @GetMapping("/hello")
        @ResponseBody
        public String hello() {
            return user.getUserName();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    结果如下:
    在这里插入图片描述
    这种方式并不推荐,一般情况下,我们对于SpringBean,是不会自己去定义一个有参构造的,那么第二种:我们只需要把有参构造删除即可,让程序走默认的无参构造逻辑。

    最后,关于SpringBean的创建加载原理,可以看下我写的这篇文章Spring源码系列:Bean的加载

    问题三: 原型 Bean 竟指向同一个对象

    案例演示

    User类:指定其Scopeprototype。(即多例,希望每次调用都是一个不同的对象,默认情况下Spring容器中的Bean都是单例的

    @Component
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    public class User {
        
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    Controller类:

    @Autowired
    private User user;
    
    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return Math.random() + user.toString();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    结果如下:
    在这里插入图片描述
    可以发现,不管调用几次,都是同一个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;
    }
    
    • 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

    最终的重要逻辑在于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));
    }
    
    • 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
    • 30
    • 31

    到这里为止,目标类所需的相关字段、方法的元数据信息已经拿到并且封装好,并且加入到一个集合中。接下来就需要进行属性的注入操作了。

    上文提到过,创建一个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;
    		}
    	}
    	// ..
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    可见这里我们执行了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);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    到这里为止:

    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();
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    总结以下很简单:

    1. 通过@Autowired注解引入的类,Spring会执行对应的后处理动作。相关入口在于AutowiredAnnotationBeanPostProcessor这个类。
    2. AutowiredAnnotationBeanPostProcessor类实现了两个接口,主要做两件事。
    3. postProcessMergedBeanDefinition方法负责将目标类的字段和方法元数据信息收集起来。
    4. postProcessProperties()负责将对应的属性进行赋值。如果是方法,就执行一遍。

    那么回到案例本身,为什么我指定了prototype原型,每次调用的对象还是同一个呢?

    1. 这个对象通过@Autowired注解引入。会对这个类做对应的后处理操作。
    2. 最后通过反射的方式,对这个类的字段进行赋值,方法被调用。但是这个过程只执行了一次。即通过@Autowired注解引入这一个时机。之后这个值就被固定下来了。
    3. 可以这么说,通过@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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    结果如下:
    在这里插入图片描述

    方式二:通过Lookup注解:

    @GetMapping("/hello")
    @ResponseBody
    public String hello() {
        return Math.random() + getUser().toString();
    }
    
    @Lookup
    public User getUser() {
        return null;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这种方式的原理就不多解释了,大概就是:

    1. @Lookup注解代表可以在运行时用新方法代替现有的方法。
    2. 最后会通过 BeanFactory 来获取 Bean。因此能保证每次请求都能重新获取一次Bean

    详细可以看Spring源码系列:标签的解析原理中关于 lookup-method标签的作用的讲解。

    总结

    1. Springboot中的启动类,切记要放到项目架构的最外层。因为默认会根据你启动类所在的包路径去扫描。
    2. 项目中没有事不要在声明Bean的时候害带有参构造,如果用了请注意传参问题。否则就会报错。另外,如果有多个有参构造,Spring会默认使用无参构造。它并不知道你使用的究竟是哪一个。
    3. 使用@Autowired引入的bean是个单例,哪怕你对其设置了原型prototype也没有用。因为它做了后处理操作,通过反射机制来进行赋值,这个过程只执行一次,因此使用@Autowired引入的bean是固定不变的。
  • 相关阅读:
    GROUP BY与COUNT(也可以换成其他聚合函数)用法详解
    软件设计模式系列之二十三——策略模式
    基于JAVA医院疫情隔离室管理系统计算机毕业设计源码+系统+数据库+lw文档+部署
    yolov8中train.py、val.py、predict.py的区别,什么时候该用哪个?
    如何愉快地搞定本科毕业论文、查重 / 计算机、人工智能、数据科学等专业
    【MySQL】内置函数——字符串函数
    【C++】模版-初阶
    java毕业设计“小世界”私人空间mybatis+源码+调试部署+系统+数据库+lw
    gem5学习(25):用于异构SoC的片上网络模型——Garnet2.0
    文件上传漏洞--Upload-labs--Pass17--条件竞争
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/126348387