Spring框架新手快速上手系列:(二)体验一把自己配置低级容器

每天进步一点点。不积跬步,无以至千里。

上一讲,我们提到,Spring容器有低级容器和高级容器之分。低级容器不够智能,需要你给它配置很多东西它才能工作,比如你需要告诉它去哪里寻找BeanDefinitions,给它添加一些列的BeanFactoryPostProcessor或者BeanPostProcessor等,它才能具备处理创建bean的相关能力,甚至连@Autowired注解都是通过org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor这个BeanPostProcessor处理后才能注入相应的依赖的。

使用低级容器这么复杂,有那么多模版式的工作需要做,所以Spring提供了高级容器,XXApplicationContext,通过它的refresh方法你就可以看到它为了让低级容器智能起来做了哪些工作了。

我们这一讲就打算自己操纵低级容器,像refresh方法那样配置一下低级容器,让它可用。希望读者尽可能在IDEA中自己实践,不要只看个热闹。

准备工作

创建一个空的maven工程,在pom.xml中增加如下依赖:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.3.1.RELEASE</version>
        </dependency>

因为spring-boot-starter依赖会帮我们引入spring-core、spring-beans、spring-context、spring-aop等依赖,这样可以省去我们自己手动添加一个个依赖。

创建一个接口DemoService:

package org.example.service;

public interface DemoService {
    String getName();
}

创建一个接口实现类DemoServiceImpl:

package org.example.service.impl;

import org.example.service.DemoService;

public class DemoServiceImpl implements DemoService {
    @Override
    public String getName() {
        return "炒饭";
    }
}

创建一个包含main方法的入口类MainClass:

package org.example;

import org.example.service.DemoService;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;

public class MainClass {

    public static void main(String[] args) {
        // Spring低级容器的实现类
        DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
				// 从容器中获取一个bean
        DemoService demoService = defaultListableBeanFactory.getBean(DemoService.class);
				// 调用bean的方法
        System.out.println(demoService.getName());
    }
}

工程的目录结构:

在这里插入图片描述

开始DIY之旅

上面准备好了代码,当我们运行MainClass的main方法的时候会报错:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.example.service.DemoService' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:352)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:343)
	at org.example.MainClass.main(MainClass.java:17)

原因是,我们在创建DefaultListableBeanFactory实例defaultListableBeanFactory和从defaultListableBeanFactory里面getBean这两个步骤之间还缺少一些工作没做。

将bean定义放到容器中

我们想从Spring容器中取出bean的前提是容器中要有这个bean的定义,即这个bean的BeanDefinition,这样Spring容器才能根据这个BeanDefinition创建出你要的bean,让我们增加将bean定义放到Spring容器中的代码:

package org.example;

import org.example.service.DemoService;
import org.example.service.impl.DemoServiceImpl;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;

public class MainClass {

    public static void main(String[] args) {
        // Spring低级容器的实现类
        DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
        // 将BeanDefinition放到容器中
        BeanDefinition demoBeanDefinition = new RootBeanDefinition(DemoServiceImpl.class);
        defaultListableBeanFactory.registerBeanDefinition("demoService", demoBeanDefinition);
        
        DemoService demoService = defaultListableBeanFactory.getBean(DemoService.class);
        System.out.println(demoService.getName());
    }
}

再次运行一下main方法,控制台会输出:

[main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'demoService'
炒饭

Process finished with exit code 0

漂亮地从Spring容器中取出了bean,并且成功地调用了bean的方法。

如果我们再增加一些bean,难道要一个一个地为它们创建BeanDefinition,然后一个一个地注册到容器中么?这样效率太低了,Spring已经为我们考虑到了,它提供了一些工具类,可以将bean定义批量自动注册到容器中。让我们以org.springframework.context.annotation.ClassPathBeanDefinitionScanner为例,体验一把,修改后的main方法内部如下:

       // Spring低级容器的实现类
        DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
        // 将BeanDefinition放到容器中,注意这儿的扫描器的构造器将Spring容器传递进去了,这样它才能知道将bean定义注册到哪里
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(defaultListableBeanFactory);
				// 扫描指定包下面的bean定义
        scanner.scan("org.example");

        DemoService demoService = defaultListableBeanFactory.getBean(DemoService.class);
        System.out.println(demoService.getName());

再次运行main方法,又报错了:

NoSuchBeanDefinitionException: No qualifying bean of type 'org.example.service.DemoService' available

为什么?因为我们没告诉它,哪些类需要作为bean定义注册到Spring容器中(前面我们是通过代码new RootBeanDefinition(DemoServiceImpl.class)硬编码的方式,指明了那个类要封装成BeanDefinition,所以可以工作),难道你打算把一个包下面的所有类都放到容器中么?难道你定义的XXUtil类、XXDto、XXConstant、XXEnum都是要作为bean放到Spring容器中么?所以,你得标记一下哪些类是bean的定义,注解就是最好的标记,所以我们需要将包下面需要作为bean定义的类打上诸如@Component@Service@Configuration标记,这样扫描器才会识别出来,它是bean定义,而不是阿猫阿狗都往容器里扔。

我们将DemoServiceImpl类上面加上@Service(或者@Componenet也行,只是语义上有区别而已)注解再试试,这下运行main方法又能成功完成取出bean,调用bean的方法了。

这里有个重要的知识点,和Spring关系不大,但是却和Java的类加载有关,虽然可以不提,但我还是想提一下。你有没有想过,scanner是如何判断一个类是否要作为bean定义的呢?你可能会说,这还不简单,并给出类似如下代码:

        Class<?> clazz = Class.forName("org.example.service.impl.DemoServiceImpl");
        Annotation[] declaredAnnotations = clazz.getAnnotations();
        for (Annotation annotation : declaredAnnotations) {
            if (annotation.getClass() == Service.class || annotation.getClass() == Component.class || ...){
                // 说明clazz就是Spring bean定义
            }
        }

但我想说的是,ClassPathBeanDefinitionScanner并不是这么干的,因为这样有一个问题,Class.forName方法会将一个类装载到Java虚拟机中,如果你发现它不是一个有着@Componenet、@Service等注解的类,你能从Java虚拟机中将它卸载掉么?对不起,JDK中没有提供卸载类的方法,那么相当于加载了一个可能没有用的类到Java虚拟机中了。比如有人新建工程,从别的工程中拷贝了几百上千个类过来,真正用到的就一小部分,难道那些从来没有被引用的类都会被加载到Java虚拟机中么?并不是的,只有被虚拟机中的类用到的类才会被加载,或者你手动用Class.forName加载。

那不能用Class.forName的方式,要怎么办呢?

其实Java类编译后的.clsss文件,你也可以把它当作普通文件看待。我们只需要把它当作文件读取,看看文件的内容中是否有那些注解即可,文件读完发现不符合条件跳过即可,也不会对Java虚拟机造成垃圾。

可以通过org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan方法内调用的org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#findCandidateComponents调用的org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#scanCandidateComponents调用的org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider#isCandidateComponent方法,看它是如何判断类是否符合条件的,其实就是我上面说的那样。

注意:我们这儿是直接给scanner传入了一个路径,真实项目中都是在配置类上加上@ComponenetScan注解,在注解中写上路径的,如果不写路径,就会采用当前配置类的路径作为包扫描路径,SpringBoot就是这么干的,约定大于配置思想的运用。

让bean可以注入其它的bean

好了,让我们更进一步完善我们的功能,假设我们的DemoServiceImpl内部依赖了一个DemoRepository。

DemoRepository类定义如下:

package org.example.repository;

import org.springframework.stereotype.Component;

@Component
public class DemoRepository {
    
    public String getName(){
        return "皮拉夫大王";
    }
}

DemoServiceImpl修改一下:

package org.example.service.impl;

import org.example.repository.DemoRepository;
import org.example.service.DemoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class DemoServiceImpl implements DemoService {

    @Autowired
    private DemoRepository demoRepository;

    @Override
    public String getName() {
        return demoRepository.getName();
    }
}

让我们再运行一下main方法,又报错了:

15:12:53.080 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'demoServiceImpl'
Exception in thread "main" java.lang.NullPointerException
	at org.example.service.impl.DemoServiceImpl.getName(DemoServiceImpl.java:21)
	at org.example.MainClass.main(MainClass.java:22)

在这里插入图片描述

错误原因是DemoServiceImpl中的demoRepository是null,即没有注入成功。为什么没有注入成功?我们不是加了@Autowired注解了么?低级容器DefaultListableBeanFactory你怎么不听话,为什么不给我注入一个DemoRepository bean!

哈哈,这就是它叫低级容器的原因,很多东西都需要我们去配置,让我们给它再赋能一下,给它一个抓手AutowiredAnnotationBeanPostProcessor,MainClass修改如下:

package org.example;

import org.example.service.DemoService;
import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;

/**
 * @author pilaf
 * @description
 * @date 2022-08-24 08:17
 **/
public class MainClass {

    public static void main(String[] args)  {
        // Spring低级容器的实现类
        DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
        // 将BeanDefinition放到容器中
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(defaultListableBeanFactory);
        scanner.scan("org.example");
        // 给低级容器一个处理@Autowired注解的抓手
        AutowiredAnnotationBeanPostProcessor zhuashou = new AutowiredAnnotationBeanPostProcessor();
        zhuashou.setBeanFactory(defaultListableBeanFactory);
        defaultListableBeanFactory.addBeanPostProcessor(zhuashou);
        DemoService demoService = defaultListableBeanFactory.getBean(DemoService.class);
        System.out.println(demoService.getName());
    }
}

再次运行main方法,控制台完美输出:

[main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'demoServiceImpl'
[main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'demoRepository'
皮拉夫大王

Process finished with exit code 0

Spring高级容器是通过org.springframework.context.annotation.AnnotationConfigUtils#registerAnnotationConfigProcessors方法将AutowiredAnnotationBeanPostProcessor注册到Spring低级容器的。(方法内部有这么一行RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);

自定义一个bean后置处理器

bean后置处理器能够在bean实例创建后,初始化方法执行前或初始化方法执行后对bean进行处理。比如处理@Autowired注解的AutowiredAnnotationBeanPostProcessor,它就是bean后置处理器。让我们看看这个熟悉的陌生人都有哪些功能。

先看它的构造器:

	public AutowiredAnnotationBeanPostProcessor() {
		this.autowiredAnnotationTypes.add(Autowired.class);
		this.autowiredAnnotationTypes.add(Value.class);
		try {
			this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
					ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()));
			logger.trace("JSR-330 'javax.inject.Inject' annotation found and supported for autowiring");
		}
		catch (ClassNotFoundException ex) {
			// JSR-330 API not available - simply skip.
		}
	}

它指明了自己支持的几个注解:@Autowired@Value还有@javax.inject.Inject,就是说bean字段上的这三个注解都是它处理的。

不知道你看到这里有没有一个冲动,要自己实现一个属于你的Autowired注解和注解处理器,那么来吧。

定义一个自己的@MyAutowired注解:

package org.example.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAutowired {
}

定一个自己的注解处理器:

package org.example.bpp;

import org.example.annotation.MyAutowired;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanPostProcessor;

import java.lang.reflect.Field;


public class MyAutowiredBeanPostProcessor implements BeanPostProcessor {
    
    private BeanFactory beanFactory;

    public MyAutowiredBeanPostProcessor(BeanFactory beanFactory) {
        this.beanFactory = beanFactory;
    }

    // 正常情况下,每个bean创建的时候都会走到这儿 ,除非你的bean比这个BeanPostProcessor创建还早
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        // 获取bean的字段
        Field[] declaredFields = bean.getClass().getDeclaredFields();
        for (Field declaredField : declaredFields) {
            // 看看字段是否有@MyAutowired注解
            MyAutowired[] annotationsByType = declaredField.getAnnotationsByType(MyAutowired.class);
            if (annotationsByType.length > 0) {
              	// 防止字段是private不允许修改,设置accessible为true就可以了
                declaredField.setAccessible(true);
                try {
                  	// 反射赋值,从Spring容器中取出你需要的字段类型的bean塞给目标bean
                    declaredField.set(bean, beanFactory.getBean(declaredField.getType()));
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        return bean;
    }
}

然后将DemoServiceImpl中的@Autowired换成我们的@MyAutowired注解:

package org.example.service.impl;

import org.example.annotation.MyAutowired;
import org.example.repository.DemoRepository;
import org.example.service.DemoService;
import org.springframework.stereotype.Service;

@Service
public class DemoServiceImpl implements DemoService {
		// 注意这儿的注解被换成我们自定义的了
    @MyAutowired
    private DemoRepository demoRepository;

    @Override
    public String getName() {
        return demoRepository.getName();
    }
}

再给低级容器一个得力的抓手:

package org.example;

import org.example.bpp.MyAutowiredBeanPostProcessor;
import org.example.service.DemoService;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;

public class MainClass {

    public static void main(String[] args)  {
        // Spring低级容器的实现类
        DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();
        // 将BeanDefinition放到容器中
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(defaultListableBeanFactory);
        scanner.scan("org.example");
        // 给低级容器一个处理@MyAutowired注解的抓手
        MyAutowiredBeanPostProcessor zhuashou = new MyAutowiredBeanPostProcessor(defaultListableBeanFactory);
        defaultListableBeanFactory.addBeanPostProcessor(zhuashou);

        DemoService demoService = defaultListableBeanFactory.getBean(DemoService.class);
        System.out.println(demoService.getName());
    }
}

好了,运行一下main方法,成功了!控制台输出:

[main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'demoServiceImpl'
[main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'demoRepository'
皮拉夫大王
Disconnected from the target VM, address: '127.0.0.1:51437', transport: 'socket'

Process finished with exit code 0

怎么样,经过这一次DIY的尝试,是不是对Spring容器没那么陌生了,并且有了想进一步了解它的欲望了?没错,我就是希望大家能通过一些自己的尝试,去感受Spring容器,遇到问题就debug,问题解决了,理解就深了,再也不用在面试前,去往上搜“有没有人告诉我Spring bean的生命周期的详细的八股文,面试前需要背”。你多debug几次org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory#doCreateBean方法,就可以将一般的面试官按在地上摩擦了。

添加配置bean

我们前面说过,生产环境都是通过在配置类上的@ComponentScan注解中使用包路径,指出要扫描的范围。我们下面来体验一下。

增加一个配置bean:

package org.example.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@ComponentScan("org.example")
@Configuration
public class MyConfiguration {
}

修改MainClass:

package org.example;

import org.example.bpp.MyAutowiredBeanPostProcessor;
import org.example.config.MyConfiguration;
import org.example.service.DemoService;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ConfigurationClassPostProcessor;

public class MainClass {

    public static void main(String[] args)  {
        // Spring低级容器的实现类
        DefaultListableBeanFactory defaultListableBeanFactory = new DefaultListableBeanFactory();

        // 必须得先将配置bean的BeanDefinition放到容器中,否则Spring容器去哪儿找@ComponenetScan去。需要从它的配置bean上找
        RootBeanDefinition configBeanDef = new RootBeanDefinition(MyConfiguration.class);
        defaultListableBeanFactory.registerBeanDefinition("myConfiguration", configBeanDef);

        // 配置bean上的@ComponenentScan注解需要ConfigurationClassPostProcessor这个BeanFactory后置处理器去处理,
        // 它会将@ComponenetScan注解后面的包路径下的bean定义都扫描注册到Spring容器中
        ConfigurationClassPostProcessor configurationClassPostProcessor = new ConfigurationClassPostProcessor();
        configurationClassPostProcessor.postProcessBeanDefinitionRegistry(defaultListableBeanFactory);

        // 给低级容器一个处理@MyAutowired注解的抓手
        MyAutowiredBeanPostProcessor zhuashou = new MyAutowiredBeanPostProcessor(defaultListableBeanFactory);
        defaultListableBeanFactory.addBeanPostProcessor(zhuashou);

        DemoService demoService = defaultListableBeanFactory.getBean(DemoService.class);
        System.out.println(demoService.getName());
    }
}

运行main方法,控制台完美输出:

[main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'demoServiceImpl'
[main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'demoRepository'
皮拉夫大王

Process finished with exit code 0

总结

本文通过手动配置低级容器,来实现获取bean、注入bean、将配置bean上面的包中的bean都注册到Spring容器的过程,让我们对Spring容器的原理不再感到神秘,不就是一堆辅助对象来帮忙把我们定义的类抽象成bean定义放到Spring容器中,并在获取bean的时候,按需干预一下嘛。这篇文章用到了Spring两大扩展点,分别是BeanFactory后置处理ConfigurationClassPostProcessor和Bean后置处理器AutowiredAnnotationBeanPostProcessor,前者用于后置处理Spring容器,后者用于后置处理Spring中的Bean。

我们做的这些工作正是各种Spring高级容器中都会做的,比如大名鼎鼎的AbstractApplicationContext#refresh方法中所列出的步骤。SpringBoot应用在Spring高级容器之上又包了一层,让高级容器更强大了,简化了很多配置,开箱即用,但是要想用好,还需要去熟悉它的源码才行。本文试图消除Spring初学者对源码的恐惧,让初学者有信心自己去探索Spring源码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值