详解Bean注入Spring的方式

xml 方式
注解方式
@Configuration + @Bean
@Import
FactoryBean
BDRegistryPostProcessor

从Spring的IOC特性入手,聊一聊Spring中把Bean注入Spring容器的几种方式

我们先来简单了解下IOC的概念:IOC即控制反转,也称为依赖注入,是指将对象的创建或者依赖关系的引用从具体的对象控制转为框架或者IOC容器来完成,也就是依赖对象的获得被反转了。

可以简单理解为原来由我们来创建对象,现在由Spring来创建并控制对象

一、xml 方式

最早接触Spring的时候,所有的bean的注入得依靠xml文件来完成

它的注入方式分为:set方法注入、构造方法注入、字段注入,而注入类型分为值类型注入(8种基本数据类型)和引用类型注入(将依赖对象注入)

1.1、set方法注入

<bean name="teacher" class="org.springframework.demo.model.Teacher">
    <property name="name" value="阿Q"></property>
</bean>

对应的实体类代码

public class Teacher {

 private String name;

 public void setName(String name) {
  this.name = name;
    }
}

xml方式存在的缺点如下

xml文件配置起来比较麻烦,既要维护代码又要维护配置文件,开发效率低;
项目中配置文件过多,维护起来比较困难;
程序编译期间无法对配置项的正确性进行验证,只能在运行期发现并且出错之后不易排查;
解析xml时,无论是将xml一次性装进内存,还是一行一行解析,都会占用内存资源,影响性能。

1.2、属性注入

通过属性注入的方式非常常用,这个应该是大家比较熟悉的一种方式

@Service
public class UserService {
    @Autowired
    private Wolf1Bean wolf1Bean;//通过属性注入
}

1.3、setter 方法注入

除了通过属性注入,通过 setter 方法也可以实现注入

@Service
public class UserService {
    private Wolf3Bean wolf3Bean;
    
    @Autowired  //通过setter方法实现注入
    public void setWolf3Bean(Wolf3Bean wolf3Bean) {
        this.wolf3Bean = wolf3Bean;
    }
}

1.4、构造器注入

当两个类属于强关联时,我们也可以通过构造器的方式来实现注入

@Service
public class UserService {
  private Wolf2Bean wolf2Bean;
    
     @Autowired //通过构造器注入
    public UserService(Wolf2Bean wolf2Bean) {
        this.wolf2Bean = wolf2Bean;
    }
}

二、注解方式

随着Spring的发展,Spring 2.5开始出现了一系列注解,除了我们经常使用的@Controller、@Service、@Repository、@Component 之外,还有一些比较常用的方式

2.1、@Configuration + @Bean

当我们需要引入第三方的jar包时,可以用==@Bean注解来标注,同时需要搭配@Configuration==来使用。

@Configuration用来声明一个配置类,可以理解为xml的标签

@Bean 用来声明一个bean,将其加入到Spring容器中,可以理解为xml的标签

简单样例:将 RedisTemplate 注入 Spring

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        ......
        return redisTemplate;
    }
}

2.2、@Import

我们在翻看Spring源码的过程中,经常会看到@Import注解,它也可以用来将第三方jar包注入Spring,但是它只可以作用在类上

例如在注解EnableSpringConfigured上就包含了@Import注解,用于将SpringConfiguredConfiguration配置文件加载进Spring容器。

@Import(SpringConfiguredConfiguration.class)
public @interface EnableSpringConfigured {}
// @Import的value值是一个数组,一个一个注入比较繁琐,因此我们可以搭配ImportSelector接口来使用,用法如下:

@Configuration
@Import(MyImportSelector.class)
public class MyConfig {}

// 其中selectImports方法返回的数组就会通过@Import注解注入到Spring容器中
public class MyImportSelector implements ImportSelector {
 @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{"org.springframework.demo.model.Teacher","org.springframework.demo.model.Student"};
    }
}

无独有偶,ImportBeanDefinitionRegistrar接口也为我们提供了注入bean的方法

@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {
    ......
}

我们点击AspectJAutoProxyRegistrar类,发现它实现了ImportBeanDefinitionRegistrar接口,它的registerBeanDefinitions方法便是注入bean的过程

如果觉得源代码比较难懂,可以看一下我们自定义的类

@Configuration
@Import(value = {MyImportBeanDefinitionRegistrar.class})
public class MyConfig {}

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
                                        BeanDefinitionRegistry registry) {
            RootBeanDefinition tDefinition = new RootBeanDefinition(Teacher.class);
            // 注册 Bean,并指定bean的名称和类型
            registry.registerBeanDefinition("teacher", tDefinition);
        }
    }
}

这样我们就把Teacher类注入到Spring容器中了。

三、接口注入

在上面的三种常规注入方式中(属性注入、set方法注入、构造器注入),假如我们想要注入一个接口,而当前接口又有多个实现类,那么这时候就会报错,因为 Spring 无法知道到底应该注入哪一个实现类。

比如我们上面的三个类全部实现同一个接口 IWolf,那么这时候直接使用常规的,不带任何注解元数据的注入方式来注入接口 IWolf。

@Autowired
private IWolf iWolf;

在这里插入图片描述
启动程序就会报错,就是说本来应该注入一个类,但是 Spring 找到了多个累,所以没法确认到底应该用哪一个类。这个问题如何解决呢?

解决思路主要有以下 5 种:

3.1、通过配置文件和 @ConditionalOnProperty 注解实现

通过 @ConditionalOnProperty注解可以结合配置文件来实现唯一注入
下面示例就是说如果配置文件中配置了 lonely.wolf=test1,那么就会将 Wolf1Bean 初始化到容器,此时因为其他实现类不满足条件,所以不会被初始化到 IOC 容器,所以就可以正常注入接口:

@Component
@ConditionalOnProperty(name = "lonely.wolf",havingValue = "test1")
public class Wolf1Bean implements IWolf{
}

当然,这种配置方式,编译器可能还是会提示有多个 Bean,但是只要我们确保每个实现类的条件不一致,就可以正常使用。

3.2、通过其他 @Condition 条件注解

除了上面的配置文件条件,还可以通过其他类似的条件注解,如:

@ConditionalOnBean:当存在某一个 Bean 时,初始化此类到容器。
@ConditionalOnClass:当存在某一个类时,初始化此类的容器。
@ConditionalOnMissingBean:当不存在某一个 Bean 时,初始化此类到容器。
@ConditionalOnMissingClass:当不存在某一个类时,初始化此类到容器。

类似这种实现方式也可以非常灵活的实现动态化配置。

不过上面介绍的这些方法似乎每次都只能固定注入一个实现类,那么如果我们就是想多个类同时注入,不同的场景可以动态切换而又不需要重启或者修改配置文件,又该如何实现呢?

3.3、通过 @Resource 注解动态获取

如果不想手动获取,我们也可以通过 @Resource 注解的形式动态指定 BeanName 来获取:

@Component
public class InterfaceInject {

    @Resource(name = "wolf1Bean")
    private IWolf iWolf;
}

如上所示则只会注入 BeanName 为 wolf1Bean 的实现类

3.4、通过集合注入

除了指定 Bean 的方式注入,我们也可以通过集合的方式一次性注入接口的所有实现类

@Component
public class InterfaceInject {

    @Autowired
    List<IWolf> list;

    @Autowired
    private Map<String,IWolf> map;
}

上面的两种形式都会将 IWolf 中所有的实现类注入集合中。如果使用的是 List 集合,那么我们可以取出来再通过 instanceof 关键字来判定类型;而通过 Map 集合注入的话,Spring 会将 Bean 的名称(默认类名首字母小写)作为 key 来存储,这样我们就可以在需要的时候动态获取自己想要的实现类。

3.5、@Primary 注解实现默认注入

除了上面的几种方式,我们还可以在其中某一个实现类上加上 @Primary 注解来表示当有多个 Bean 满足条件时,优先注入当前带有 @Primary 注解的 Bean:

@Component
@Primary
public class Wolf1Bean implements IWolf{
}

四、FactoryBean

提到FactoryBean,就不得不与BeanFactory比较一番

BeanFactory : 是 Factory, IOC容器或者对象工厂,所有的Bean都由它进行管理
FactoryBean : 是Bean ,是一个能产生或者修饰对象生成的工厂 Bean,实现与工厂模式和修饰器模式类似

那么FactoryBean是如何实现bean注入的呢?

先定义实现了FactoryBean接口的类

public class TeacherFactoryBean implements FactoryBean<Teacher> {

 /**
  * 返回此工厂管理的对象实例
  **/
 @Override
 public Teacher getObject() throws Exception {
  return new Teacher();
 }

 /**
  * 返回此 FactoryBean 创建的对象的类型
  **/
 @Override
 public Class<?> getObjectType() {
  return Teacher.class;
 }

}

然后通过 @Configuration + @Bean的方式将TeacherFactoryBean加入到容器中

@Configuration
public class MyConfig {

 	@Bean
 	public TeacherFactoryBean teacherFactoryBean(){
  		return new TeacherFactoryBean();
 	}
}

注意:我们没有向容器中注入Teacher, 而是直接注入的TeacherFactoryBean,然后从容器中拿Teacher这个类型的bean,成功运行。

五、BDRegistryPostProcessor

源码

public interface BeanDefinitionRegistryPostProcessor extends BeanFactoryPostProcessor {
    // 注册bean到spring容器中
 void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException;
}

@FunctionalInterface
public interface BeanFactoryPostProcessor {
 void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException;
}

BeanFactoryPostProcessor接口是BeanFactory的后置处理器,方法postProcessBeanFactory对bean的定义进行控制。今天我们重点来看看postProcessBeanDefinitionRegistry方法:它的参数是BeanDefinitionRegistry,顾名思义就是与BeanDefinition注册相关的。

图片
通过观察该类,我们发现它里边包含了registerBeanDefinition方法,这个不就是我们想要的吗?为了能更好的使用该接口来达到注入bean的目的,我们先来看看Spring是如何操作此接口的。

看下invokeBeanFactoryPostProcessors方法,会发现没有实现PriorityOrdered和Ordered的bean(这种跟我们自定义的实现类有关)会执行以下代码。

while (reiterate) {
    ......
    invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
    ......
}

进入该方法

private static void invokeBeanDefinitionRegistryPostProcessors(
    Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, 
    BeanDefinitionRegistry registry) {

    for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessBeanDefinitionRegistry(registry);
    }
}

会发现实现了BeanDefinitionRegistryPostProcessor接口的bean,其postProcessBeanDefinitionRegistry方法会被调用,也就是说如果我们自定义接口实现该接口,它的postProcessBeanDefinitionRegistry方法也会被执行。

实战

话不多说,直接上代码。自定义接口实现类

public class MyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor {

 /**
  * 初始化过程中先执行
  **/
 @Override
 public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
  RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Teacher.class);
  //Teacher 的定义注册到spring容器中
  registry.registerBeanDefinition("teacher", rootBeanDefinition);
 }

 /**
  * 初始化过程中后执行
  **/
 @Override
 public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
}

启动类代码

public static void main(String[] args) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    MyBeanDefinitionRegistryPostProcessor postProcessor = new MyBeanDefinitionRegistryPostProcessor();
    //将自定义实现类加入 Spring 容器
    context.addBeanFactoryPostProcessor(postProcessor);
    context.refresh();
    Teacher bean = context.getBean(Teacher.class);
    System.out.println(bean);
}

启动并打印结果

org.springframework.demo.model.Teacher@2473d930
发现已经注入到Spring容器中了

以上就是我们总结的几种将bean注入Spring容器的方式

六、获取bean的方式

在 Spring 项目中,手动获取 Bean 需要通过 ApplicationContext 对象,这时候可以通过以下 5 种方式进行获取

6.1、直接注入

最简单的一种方法就是通过直接注入的方式获取 ApplicationContext 对象,然后就可以通过 ApplicationContext 对象获取 Bean

@Component
public class InterfaceInject {
    @Autowired
    private ApplicationContext applicationContext;//注入

    public Object getBean(){
        return applicationContext.getBean("wolf1Bean");//获取bean
    }
}

6.2、通过 ApplicationContextAware 接口获取

通过实现 ApplicationContextAware 接口来获取 ApplicationContext 对象,从而获取 Bean。需要注意的是,实现 ApplicationContextAware 接口的类也需要加上注解,以便交给 Spring 统一管理(这种方式也是项目中使用比较多的一种方式)

@Component
public class SpringContextUtil implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    /**
     * 通过名称获取bean
     */
    public static <T>T getBeanByName(String beanName){
        return (T) applicationContext.getBean(beanName);
    }

    /**
     * 通过类型获取bean
     */
    public static <T>T getBeanByType(Class<T> clazz){
        return (T) applicationContext.getBean(clazz);
    }
}

封装之后,我们就可以直接调用对应的方法获取 Bean 了

Wolf2Bean wolf2Bean = SpringContextUtil.getBeanByName("wolf2Bean");
Wolf3Bean wolf3Bean = SpringContextUtil.getBeanByType(Wolf3Bean.class);

6.3、通过 ApplicationObjectSupport 和 WebApplicationObjectSupport 获取

这两个对象中,WebApplicationObjectSupport 继承了 ApplicationObjectSupport,所以并无实质的区别。

同样的,下面这个工具类也需要增加注解,以便交由 Spring 进行统一管理:

@Component
public class SpringUtil extends /*WebApplicationObjectSupport*/ ApplicationObjectSupport {
    private static ApplicationContext applicationContext = null;

    public static <T>T getBean(String beanName){
        return (T) applicationContext.getBean(beanName);
    }

    @PostConstruct
    public void init(){
        applicationContext = super.getApplicationContext();
    }
}

有了工具类,在方法中就可以直接调用了

@RestController
@RequestMapping("/hello")
@Qualifier
public class HelloController {
    @GetMapping("/bean3")
    public Object getBean3(){
        Wolf1Bean wolf1Bean = SpringUtil.getBean("wolf1Bean");
        return wolf1Bean.toString();
    }
}

6.4、通过 HttpServletRequest 获取

通过 HttpServletRequest 对象,再结合 Spring 自身提供的工具类 WebApplicationContextUtils 也可以获取到 ApplicationContext 对象,而 HttpServletRequest 对象可以主动获取(如下 getBean2 方法),也可以被动获取(如下 getBean1 方法)

@RestController
@RequestMapping("/hello")
@Qualifier
public class HelloController {

    @GetMapping("/bean1")
    public Object getBean1(HttpServletRequest request){
        //直接通过方法中的HttpServletRequest对象
        ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
        Wolf1Bean wolf1Bean = (Wolf1Bean)applicationContext.getBean("wolf1Bean");

        return wolf1Bean.toString();
    }

    @GetMapping("/bean2")
    public Object getBean2(){
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();//手动获取request对象
        ApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());

        Wolf2Bean wolf2Bean = (Wolf2Bean)applicationContext.getBean("wolf2Bean");
        return wolf2Bean.toString();
    }
}

七、谈谈 @Autowrite 和 @Resource 以及 @Qualifier 注解的区别

@Autowrite:通过类型去注入,可以用于构造器和参数注入。当我们注入接口时,其所有的实现类都属于同一个类型,所以就没办法知道选择哪一个实现类来注入

@Resource:默认通过名字注入,不能用于构造器和参数注入。如果通过名字找不到唯一的 Bean,则会通过类型去查找。如下可以通过指定 name 或者 type 来确定唯一的实现

@Resource(name = "wolf2Bean",type = Wolf2Bean.class)
 private IWolf iWolf;

而 @Qualifier 注解是用来标识合格者,当 @Autowrite 和 @Qualifier 一起使用时,就相当于是通过名字来确定唯一

@Qualifier("wolf1Bean")
@Autowired
private IWolf iWolf;

那可能有人就会说,我直接用 @Resource 就好了,何必用两个注解结合那么麻烦,这么一说似乎显得 @Qualifier 注解有点多余?

@Qualifier 注解是多余的吗?

我们先看下面声明 Bean 的场景,这里通过一个方法来声明一个 Bean (MyElement),而且方法中的参数又有 Wolf1Bean 对象,那么这时候 Spring 会帮我们自动注入 Wolf1Bean

@Component
public class InterfaceInject2 {
    @Bean
    public MyElement test(Wolf1Bean wolf1Bean){
        return new MyElement();
    }
}

然而如果说我们把上面的代码稍微改一下,把参数改成一个接口,而接口又有多个实现类,这时候就会报错了

@Component
public class InterfaceInject2 {
    @Bean
    public MyElement test(IWolf iWolf){//此时因为IWolf接口有多个实现类,会报错
        return new MyElement();
    }
}

而 @Resource 注解又是不能用在参数中,所以这时候就需要使用 @Qualifier 注解来确认唯一实现了(比如在配置多数据源的时候就经常使用 @Qualifier 注解来实现)

@Component
public class InterfaceInject2 {
    @Bean
    public MyElement test(@Qualifier("wolf1Bean") IWolf iWolf){
        return new MyElement();
    }
}
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值