1 - 11 Spring源码分析(注解开发)B站 雷丰阳 如何向Spring容器中注册bean的知识 参考博主李阿昀

李阿昀的主页

Spring源码分析(注解开发)

自己做的思维导图
2-11如何向Spring容器中注册bean的知识

2 Spring注解驱动开发第2讲——使用@Configuration和@Bean给容器中注册组件

我们在使用注解方式向Spring的IOC容器中注入JavaBean时,如果没有在@Bean注解中明确指定bean的名称,那么就会使用当前方法的名称来作为bean的名称;如果在@Bean注解中明确指定了bean的名称,那么就会使用**@Bean注解中指定的名称来作为bean的名称。**


/**
 * 以前配置文件的方式被替换成了配置类,即配置类==配置文件
 * @author liayun
 *
 */
// 这个配置类也是一个组件 
@Configuration // 告诉Spring这是一个配置类
public class MainConfig {

	// @Bean注解是给IOC容器中注册一个bean,类型自然就是返回值的类型,id默认是用方法名作为id
	@Bean("person")
	public Person person01() {
		return new Person("liayun", 20);
	}
	
}

3 Spring注解驱动开发第3讲——使用@ComponentScan自动扫描组件并指定扫描规则

点进ComponentScan注解

//默认全都扫描
boolean useDefaultFilters() default true;
//包含
ComponentScan.Filter[] includeFilters() default {};
//排除
ComponentScan.Filter[] excludeFilters() default {};

includeFilters()方法指定Spring扫描的时候按照什么规则只需要包含哪些组件,而excludeFilters()方法指定Spring扫描的时候按照什么规则排除哪些组件。两个方法的返回值都是Filter[]数组,在ComponentScan注解类的内部存在Filter注解类

扫描时排除注解标注的类

现在有这样一个需求,除了@Controller和@Service标注的组件之外,IOC容器中剩下的组件我都要,即相当于是我要排除@Controller和@Service这俩注解标注的组件。要想达到这样一个目的,我们可以在MainConfig类上通过@ComponentScan注解的excludeFilters()方法实现。例如,我们在MainConfig类上添加了如下的注解。

@ComponentScan(value="com.atguigu", excludeFilters={
		/*
		 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
		 * classes:除了@Controller和@Service标注的组件之外,IOC容器中剩下的组件我都要,即相当于是我要排除@Controller和@Service这俩注解标注的组件。
		 */
		@Filter(type=FilterType.ANNOTATION, classes={Controller.class, Service.class})
}) // value指定要扫描的包

扫描时只包含注解标注的类

@ComponentScan(value="com.atguigu", includeFilters={
		/*
		 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
		 * classes:我们需要Spring在扫描时,只包含@Controller注解标注的类
		 */
		@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
}, useDefaultFilters=false) // value指定要扫描的包

ComponentScan注解类上有一个@ComponentScans注解(重复注解)

@ComponentScan(value="com.atguigu", includeFilters={
		/*
		 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
		 * classes:我们需要Spring在扫描时,只包含@Controller注解标注的类
		 */
		@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
}, useDefaultFilters=false) // value指定要扫描的包
@ComponentScan(value="com.atguigu", includeFilters={
		/*
		 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
		 * classes:我们需要Spring在扫描时,只包含@Service注解标注的类
		 */
		@Filter(type=FilterType.ANNOTATION, classes={Service.class})
}, useDefaultFilters=false) // value指定要扫描的包

可以看到,同时输出了@Controller注解和@Service注解标注的组件名称。
当然了,如果你使用的是Java 8之前的版本,那也没有问题,虽然我们再也不能直接在类上写多个@ComponentScan注解了,但是我们可以在类上使用@ComponentScans注解,同样也可以指定多个@ComponentScan,如下所示。

@ComponentScans(value={
		@ComponentScan(value="com.meimeixia", includeFilters={
				/*
				 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
				 * classes:我们需要Spring在扫描时,只包含@Controller注解标注的类
				 */
				@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
		}, useDefaultFilters=false), // value指定要扫描的包
		@ComponentScan(value="com.meimeixia", includeFilters={
				/*
				 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
				 * classes:我们需要Spring在扫描时,只包含@Service注解标注的类
				 */
				@Filter(type=FilterType.ANNOTATION, classes={Service.class})
		}, useDefaultFilters=false) // value指定要扫描的包
})

总结

我们可以使用@ComponentScan注解来指定Spring扫描哪些包,可以使用excludeFilters()方法来指定扫描时排除哪些组件,也可以使用includeFilters()方法来指定扫描时只包含哪些组件。当使用includeFilters()方法指定只包含哪些组件时,需要禁用掉默认的过滤规则。

4 Spring注解驱动开发第4讲——自定义TypeFilter指定@ComponentScan注解的过滤规则

Spring的强大之处不仅仅是提供了IOC容器,能够通过过滤规则指定排除和只包含哪些组件,它还能够通过自定义TypeFilter来指定过滤规则。如果Spring内置的过滤规则不能够满足我们的需求,那么我们便可以通过自定义TypeFilter来实现我们自己的过滤规则。

FilterType中常用的规则

在使用@ComponentScan注解实现包扫描时,我们可以使用@Filter指定过滤规则,在@Filter中,通过type来指定过滤的类型。而@Filter注解中的type属性是一个FilterType枚举,其源码如下图所示。

public enum FilterType {

	/**
	 * Filter candidates marked with a given annotation.
	 * @see org.springframework.core.type.filter.AnnotationTypeFilter
	 */
	ANNOTATION,

	/**
	 * Filter candidates assignable to a given type.
	 * @see org.springframework.core.type.filter.AssignableTypeFilter
	 */
	ASSIGNABLE_TYPE,

	/**
	 * Filter candidates matching a given AspectJ type pattern expression.
	 * @see org.springframework.core.type.filter.AspectJTypeFilter
	 */
	ASPECTJ,

	/**
	 * Filter candidates matching a given regex pattern.
	 * @see org.springframework.core.type.filter.RegexPatternTypeFilter
	 */
	REGEX,

	/** Filter candidates using a given custom
	 * {@link org.springframework.core.type.filter.TypeFilter} implementation.
	 */
	CUSTOM

}

FilterType.ANNOTATION:按照注解进行包含或者排除

例如,使用@ComponentScan注解进行包扫描时,如果要想按照注解只包含标注了@Controller注解的组件,那么就需要像下面这样写了。

@ComponentScan(value="com.meimeixia", includeFilters={
		/*
		 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
		 * classes:我们需要Spring在扫描时,只包含@Controller注解标注的类
		 */
		@Filter(type=FilterType.ANNOTATION, classes={Controller.class})
}, useDefaultFilters=false) // value指定要扫描的包

FilterType.ASSIGNABLE_TYPE:按照给定的类型进行包含或者排除

例如,使用@ComponentScan注解进行包扫描时,如果要想按照给定的类型只包含BookService类(接口)或其子类(实现类或子接口)的组件,那么就需要像下面这样写了。

@ComponentScan(value="com.atguigu", includeFilters={
		/*
		 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
		 */
		// 只要是BookService这种类型的组件都会被加载到容器中,不管是它的子类还是什么它的实现类。记住,只要是BookService这种类型的
		@Filter(type=FilterType.ASSIGNABLE_TYPE, classes={BookService.class})
}, useDefaultFilters=false) // value指定要扫描的包

此时,只要是BookService这种类型的组件,都会被加载到容器中。也就是说,当BookService是一个Java类时,该类及其子类都会被加载到Spring容器中;当BookService是一个接口时,其子接口或实现类都会被加载到Spring容器中。

FilterType.ASPECTJ:按照ASPECTJ表达式进行包含或者排除 不怎么用!

@ComponentScan(value="com.atguigu", includeFilters={
		/*
		 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
		 */
		@Filter(type=FilterType.ASPECTJ, classes={AspectJTypeFilter.class})
}, useDefaultFilters=false) // value指定要扫描的包

FilterType.REGEX:按照正则表达式进行包含或者排除 不怎么用!

@ComponentScan(value="com.atguigu", includeFilters={
		/*
		 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
		 */
		@Filter(type=FilterType.REGEX, classes={RegexPatternTypeFilter.class})
}, useDefaultFilters=false) // value指定要扫描的包

FilterType.CUSTOM:按照自定义规则进行包含或者排除

如果实现自定义规则进行过滤时,自定义规则的类必须是org.springframework.core.type.filter.TypeFilter接口的实现类。

要想按照自定义规则进行过滤,首先我们得创建org.springframework.core.type.filter.TypeFilter接口的一个实现类,例如MyTypeFilter,该实现类的代码一开始如下所示。

package com.atguigu.config;

import java.io.IOException;

import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

public class MyTypeFilter implements TypeFilter {

	/**
	 * 参数:
	 * metadataReader:读取到的当前正在扫描的类的信息
	 * metadataReaderFactory:可以获取到其他任何类的信息的(工厂)
	 */
	@Override
	public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {

		return false; // 这儿我们先让其返回false

	}

}

当我们实现TypeFilter接口时,需要实现该接口中的match()方法,match()方法的返回值为boolean类型。当返回true时,表示符合规则,会包含在Spring容器中;当返回false时,表示不符合规则,那就是一个都不匹配,自然就都不会被包含在Spring容器中。另外,在match()方法中存在两个参数,分别为MetadataReader类型的参数和MetadataReaderFactory类型的参数,含义分别如下。

metadataReader:读取到的当前正在扫描的类的信息
metadataReaderFactory:可以获取到其他任何类的信息的工厂

然后,使用@ComponentScan注解进行如下配置。

FilterType枚举中的每一个枚举值的含义我都讲解完了,说了这么多,其实只有ANNOTATION和ASSIGNABLE_TYPE是比较常用的,ASPECTJ和REGEX不太常用,如果FilterType枚举中的类型无法满足我们的需求时,我们也可以通过实现org.springframework.core.type.filter.TypeFilter接口来自定义过滤规则,此时,将@Filter中的type属性设置为FilterType.CUSTOM,classes属性设置为自定义规则的类所对应的Class对象。

实现自定义过滤规则

过滤规则

package com.atguigu.config;

import java.io.IOException;

import org.springframework.core.io.Resource;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.ClassMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.filter.TypeFilter;

public class MyTypeFilter implements TypeFilter {

	/**
	 * 参数:
	 * metadataReader:读取到的当前正在扫描的类的信息
	 * metadataReaderFactory:可以获取到其他任何类的信息的(工厂)
	 */
	@Override
	public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
		// 获取当前类注解的信息
		AnnotationMetadata annotationMetadata = metadataReader.getAnnotationMetadata();
		// 获取当前正在扫描的类的类信息,比如说它的类型是什么啊,它实现了什么接口啊之类的
		ClassMetadata classMetadata = metadataReader.getClassMetadata();
		// 获取当前类的资源信息,比如说类的路径等信息
		Resource resource = metadataReader.getResource();
		// 获取当前正在扫描的类的类名
		String className = classMetadata.getClassName();
		System.out.println("--->" + className);
		// 现在来指定一个规则
		if (className.contains("er")) {
			return true; // 匹配成功,就会被包含在容器中
		}

		return false;
	}

}

配置类

package com.atguigu.config;

import org.springframework.context.annotation.*;

import com.atguigu.bean.Person;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;

// 这个配置类也是一个组件
@ComponentScans(value={
		@ComponentScan(value="com.atguigu", includeFilters={
				/*
				 * type:指定你要排除的规则,是按照注解进行排除,还是按照给定的类型进行排除,还是按照正则表达式进行排除,等等
				 */
				// 指定新的过滤规则,这个过滤规则是我们自个自定义的,过滤规则就是由我们这个自定义的MyTypeFilter类返回true或者false来代表匹配还是没匹配
				@ComponentScan.Filter(type=FilterType.CUSTOM, classes={MyTypeFilter.class})
		}, useDefaultFilters=false) // value指定要扫描的包
})
@Configuration // 告诉Spring这是一个配置类
public class MainConfig {

	// @Bean注解是给IOC容器中注册一个bean,类型自然就是返回值的类型,id默认是用方法名作为id
	@Bean
	public Person person() {
		return new Person("liayun", 20);
	}

}

此时,结果信息中输出了使用@Service和@Controller这俩注解标注的组件的名称,分别是bookController和bookService。

从以上输出的结果信息中,你还可以看到输出了一个myTypeFilter,你不禁要问了,为什么会有myTypeFilter呢?这就是因为我们现在扫描的是com.atguigu包,该包下的每一个类都会进到这个自定义规则里面进行匹配,若匹配成功,则就会被包含在容器中。

5 Spring注解驱动开发第5讲——使用@Scope注解设置组件的作用域

写在前面

Spring容器中的组件默认是单例的,在Spring启动时就会实例化并初始化这些对象,并将其放到Spring容器中,之后,每次获取对象时,直接从Spring容器中获取,而不再创建对象。如果每次从Spring容器中获取对象时,都要创建一个新的实例对象,那么该如何处理呢?此时就需要使用@Scope注解来设置组件的作用域了。

@Scope注解概述

从@Scope注解类的源码中可以看出,在@Scope注解中可以设置如下值:

ConfigurableBeanFactory#SCOPE_PROTOTYPE
ConfigurableBeanFactory#SCOPE_SINGLETON
org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
org.springframework.web.context.WebApplicationContext#SCOPE_SESSION

在这里插入图片描述
其中,request和session作用域是需要Web环境来支持的,这两个值基本上使用不到。当我们使用Web容器来运行Spring应用时,如果需要将组件的实例对象的作用域设置为request和session,那么我们通常会使用

request.setAttribute("key", object);
session.setAttribute("key", object);
这两种形式来将对象实例设置到request和session中,而不会使用@Scope注解来进行设置。

单实例bean作用域

对象在Spring容器中默认是单实例的,Spring容器在启动时就会将实例对象加载到Spring容器中,之后,每次从Spring容器中获取实例对象,都是直接将对象返回,而不必再创建新的实例对象了。

多实例bean作用域

@Configuration
public class MainConfig2 {
	
	@Scope("prototype") // 通过@Scope注解来指定该bean的作用范围,也可以说成是调整作用域
	@Bean("person")
	public Person person() {
		return new Person("美美侠", 25);
	}
	
}

此时,输出的person对象和person2对象已经不是同一个对象了。

单实例bean作用域如何创建对象?

从以上输出的结果信息中可以看出,Spring容器在创建的时候,就将@Scope注解标注为singleton的组件进行了实例化,并加载到了Spring容器中。

这说明,Spring容器在启动时,将单实例组件实例化之后,会即刻加载到Spring容器中,以后每次从容器中获取组件实例对象时,都是直接返回相应的对象,而不必再创建新的对象了。

多实例bean作用域如何创建对象?

当向Spring容器中获取Person实例对象时,Spring容器才会实例化Person对象,再将其加载到Spring容器中去。

当对象的Scope作用域为多实例时,每次向Spring容器获取对象时,它都会创建一个新的对象并返回。很显然,以上获取到的person和person2就不是同一个对象了

单实例bean注意的事项

单实例bean是整个应用所共享的,所以需要考虑到线程安全问题,之前在玩SpringMVC的时候,SpringMVC中的Controller默认是单例的,有些开发者在Controller中创建了一些变量,那么这些变量实际上就变成共享的了,Controller又可能会被很多线程同时访问,这些线程并发去修改Controller中的共享变量,此时很有可能会出现数据错乱的问题,所以使用的时候需要特别注意。

多实例bean注意的事项

多实例bean每次获取的时候都会重新创建,如果这个bean比较复杂,创建时间比较长,那么就会影响系统的性能,因此这个地方需要注意点。

自定义Scope的实现

如果Spring内置的几种scope都无法满足我们的需求时,我们可以自定义bean的作用域。

如何实现自定义Scope呢?

第一步,实现Scope接口。

我们先来看下Scope接口的源码,如下所示。

package org.springframework.beans.factory.config;

import org.springframework.beans.factory.ObjectFactory;

public interface Scope {

	/**
	 * 返回当前作用域中name对应的bean对象
	 * @param name 需要检索的bean对象的名称
	 * @param objectFactory 如果name对应的bean对象在当前作用域中没有找到,那么可以调用这个objectFactory来创建这个对象
	 */
	Object get(String name, ObjectFactory<?> objectFactory);

	/**
	 * 将name对应的bean对象从当前作用域中移除
	 */
	Object remove(String name);

	/**
	 * 用于注册销毁回调,若想要销毁相应的对象,则由Spring容器注册相应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象
	 */
	void registerDestructionCallback(String name, Runnable callback);

	/**
	 * 用于解析相应的上下文数据,比如request作用域将返回request中的属性
	 */
	Object resolveContextualObject(String key);

	/**
	 * 作用域的会话标识,比如session作用域的会话标识是sessionId
	 */
	String getConversationId();

}

第二步,将自定义Scope注册到容器中。

此时,需要调用org.springframework.beans.factory.config.ConfigurableBeanFactory#registerScope这个方法,咱们看一下这个方法的声明。

/**
	 * Register the given scope, backed by the given Scope implementation. //向容器中 注册自定义的 scope
	 * @param scopeName the scope identifier//  scopeName  作用域 名称
	 * @param scope the backing Scope implementation// scope  作用域 对象 
	 */
	void registerScope(String scopeName, Scope scope);

实现类

@Override
	public void registerScope(String scopeName, Scope scope) {
		Assert.notNull(scopeName, "Scope identifier must not be null");//范围标识符不能为空
		Assert.notNull(scope, "Scope must not be null");//范围不能为空
		if (SCOPE_SINGLETON.equals(scopeName) || SCOPE_PROTOTYPE.equals(scopeName)) {//不能注册已经存在的命名空间
			throw new IllegalArgumentException("Cannot replace existing scopes 'singleton' and 'prototype'");
		}
		Scope previous = this.scopes.put(scopeName, scope);//注册命名空间
		if (previous != null && previous != scope) {
			if (logger.isInfoEnabled()) {
				logger.info("Replacing scope '" + scopeName + "' from [" + previous + "] to [" + scope + "]");
			}
		}
		else {
			if (logger.isDebugEnabled()) {
				logger.debug("Registering scope '" + scopeName + "' with implementation [" + scope + "]");
			}
		}
	}

第三步,使用自定义的作用域。

也就是在定义bean的时候,指定bean的scope属性为自定义的作用域名称

一个自定义Scope实现案例

例如,我们来实现一个线程级别的bean作用域,同一个线程中同名的bean是同一个实例,不同的线程中的bean是不同的实例。

这里,要求bean在线程中是共享的,所以我们可以通过ThreadLocal来实现,ThreadLocal可以实现线程中数据的共享。

首先,我们在com.atguigu.scope包下新建一个ThreadScope类,如下所示。

package com.atguigu.scope;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.config.Scope;

/**
 * 自定义本地线程级别的bean作用域,不同的线程中的bean是不同的实例,同一个线程中同名的bean是同一个实例
 * @author liayun
 *
 */
public class ThreadScope implements Scope {

	public static final String THREAD_SCOPE = "thread";

	private ThreadLocal<Map<String, Object>> beanMap = new ThreadLocal() {

		@Override
        protected Object initialValue() {
            return new HashMap<>();
        }

	};

	/**
	 * 返回当前作用域中name对应的bean对象
	 * @param name:需要检索的bean对象的名称
	 * @param objectFactory:如果name对应的bean对象在当前作用域中没有找到,那么可以调用这个objectFactory来创建这个bean对象
	 */
	@Override
	public Object get(String name, ObjectFactory<?> objectFactory) {
		Object bean = beanMap.get().get(name);
		if (Objects.isNull(bean)) {
			bean = objectFactory.getObject();
			beanMap.get().put(name, bean);
		}
		return bean;
	}

	/**
	 * 将name对应的bean对象从当前作用域中移除
	 */
	@Override
	public Object remove(String name) {
		return this.beanMap.get().remove(name);
	}

	/**
	 * 用于注册销毁回调,若想要销毁相应的对象,则由Spring容器注册相应的销毁回调,而由自定义作用域选择是不是要销毁相应的对象
	 */
	// bean作用域范围结束的时候调用的方法,用于bean的清理
	@Override
	public void registerDestructionCallback(String name, Runnable callback) {
		System.out.println(name);
	}

	/**
	 * 用于解析相应的上下文数据,比如request作用域将返回request中的属性
	 */
	@Override
	public Object resolveContextualObject(String key) {
		return null;
	}

	/**
	 * 作用域的会话标识,比如session作用域的会话标识是sessionId
	 */
	@Override
	public String getConversationId() {
		return Thread.currentThread().getName();
	}

}

在ThreadScope类中,我们定义了一个THREAD_SCOPE常量,该常量是在定义bean的时候给scope使用的。

然后,我们在com.atguigu.config包下创建一个配置类,例如MainConfig3,并使用@Scope(“thread”)注解标注Person对象的作用域为Thread范围,如下所示。

package com.atguigu.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Scope;

import com.atguigu.bean.Person;

/**
 * 测试@Scope注解设置的作用域
 * @author liayun
 *
 */
@Configuration
public class MainConfig3 {

	@Scope("thread")
	@Bean("person")
	public Person person() {
		System.out.println("给容器中添加咱们这个Person对象...");
		return new Person("美美侠", 25);
	}

}

接着,我们在IOCTest类中创建一个test04()方法,我们所要做的事情就是在该方法中创建Spring容器,并向Spring容器中注册ThreadScope对象。最后,使用循环创建两个Thread线程,并分别在每个线程中获取两个Person对象,如下所示。

从以上输出的结果信息中可以看到,bean在同样的线程中获取到的是同一个bean的实例,不同的线程中bean的实例是不同的。
注意:这里测试时**,我将Person类进行了相应的调整,将toString()方法注释掉了**,如下所示。

直接打印地址

package com.meimeixia.bean;

public class Person {
	
	private String name;
	private Integer age;
	
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Integer getAge() {
		return age;
	}
	public void setAge(Integer age) {
		this.age = age;
	}
	public Person(String name, Integer age) {
		super();
		this.name = name;
		this.age = age;
	}
	public Person() {
		super();
		// TODO Auto-generated constructor stub
	}
	
//	@Override
//	public String toString() {
//		return "Person [name=" + name + ", age=" + age + "]";
//	}
	
}

Spring注解驱动开发第6讲——如何实现懒加载?看这一篇就够了!!

Spring在启动时,默认会将单实例bean进行实例化,并加载到Spring容器中去。也就是说,单实例bean默认是在Spring容器启动的时候创建对象,并且还会将对象加载到Spring容器中。如果我们需要对某个bean进行延迟加载,那么该如何处理呢?此时,就需要使用到@Lazy注解了。

什么是懒加载呢?

何为懒加载呢?懒加载就是Spring容器启动的时候,先不创建对象,在第一次使用(获取)bean的时候再来创建对象,并进行一些初始化。

懒加载模式

我们再来看看懒加载这种模式。首先,我们在MainConfig2配置类中的person()方法上加上一个@Lazy注解,以此将Person对象设置为懒加载,如下所示。

@Configuration
public class MainConfig2 {

	@Lazy
	@Bean("person")
	public Person person() {
		System.out.println("给容器中添加咱们这个Person对象...");
		return new Person("美美侠", 25);
	}
	
}

可以看到,此时只是打印出了IOC容器创建完成这样一条信息,说明此时只创建了IOC容器,而并没有创建bean对象。

那么,加上@Lazy注解后,bean对象是何时被创建的呢?我们可以试着在IOCTest类中的test05()方法中获取一下Person对象,如下所示。

这说明,我们在获取bean对象的时候,创建出了bean对象并加载到Spring容器中去了。

那么,问题又来了,只是第一次获取bean对象的时候创建出了它吗?多次获取会不会创建多个bean对象呢?我们再来完善下测试用例,在IOCTest类中的test05()方法里面,再次获取一个Person对象,并比较两次获取的Person对象是否相等,如下所示。

从以上输出结果中可以看出,使用@Lazy注解标注后,单实例bean对象只是在第一次从Spring容器中获取时被创建,以后每次获取bean对象时,直接返回创建好的对象。

小结

懒加载,也称延时加载,仅针对单实例bean生效。 单实例bean是在Spring容器启动的时候加载的,添加@Lazy注解后就会延迟加载,在Spring容器启动的时候并不会加载,而是在第一次使用此bean的时候才会加载,但当你多次获取bean的时候并不会重复加载,只是在第一次获取的时候才会加载,这不是延迟加载的特性,而是单实例bean的特性。

Spring注解驱动开发第7讲——如何按照条件向Spring容器中注册bean?这次我懂了!!

当bean是单实例,并且没有设置懒加载时,Spring容器启动时,就会实例化bean,并将bean注册到IOC容器中,以后每次从IOC容器中获取bean时,直接返回IOC容器中的bean,而不用再创建新的bean了。

若bean是单实例,并且使用@Lazy注解设置了懒加载,则Spring容器启动时,不会立即实例化bean,自然就不会将bean注册到IOC容器中了,只有第一次获取bean的时候,才会实例化bean,并且将bean注册到IOC容器中。

若bean是多实例,则Spring容器启动时,不会实例化bean,也不会将bean注册到IOC容器中,只是在以后每次从IOC容器中获取bean的时候,都会创建一个新的bean返回。

其实,Spring支持按照条件向IOC容器中注册bean,满足条件的bean就会被注册到IOC容器中,不满足条件的bean就不会被注册到IOC容器中。接下来,我们就一起来探讨一下Spring中是如何实现按照条件向IOC容器中注册bean的。

@Conditional注解概述

@Conditional注解可以按照一定的条件进行判断,满足条件向容器中注册bean,不满足条件就不向容器中注册bean。

@Conditional注解是由Spring Framework提供的一个注解,它位于 org.springframework.context.annotation包内,定义如下。

从@Conditional注解的源码来看,@Conditional注解不仅可以添加到类上,也可以添加到方法上。在@Conditional注解中,还存在着一个Condition类型或者其子类型的Class对象数组,Condition是个啥呢?我们点进去看一下。

可以看到,它是一个接口。所以,我们使用@Conditional注解时,需要写一个类来实现Spring提供的Condition接口,它会匹配@Conditional所符合的方法(这句话怎么说的那么不明白啊!),然后我们就可以使用我们在@Conditional注解中定义的类来检查了。

我们可以在哪些场合使用@Conditional注解呢?@Conditional注解的使用场景如下图所示。

在这里插入图片描述

向Spring容器注册bean

不带条件注册bean

我们在MainConfig2配置类中新增person01()方法和person02()方法,并为这两个方法添加@Bean注解,如下所示。

package com.atguigu.config;

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

import com.atguigu.bean.Person;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;

@Configuration
public class MainConfig2 {

	// 通过@Scope注解来指定该bean的作用范围,也可以说成是调整作用域
//	@Scope("prototype")
	@Bean("person")
	@Lazy
	public Person person() {
		return new Person("美美侠", 25);
	}

	@Bean("bill")
	public Person person01() {
		return new Person("Bill Gates", 62);
	}

	@Bean("linus")
	public Person person02() {
		return new Person("linus", 48);
	}

}

带条件注册bean

现在,我们就要提出一个新的需求了,比如,如果当前操作系统是Windows操作系统,那么就向Spring容器中注册名称为bill的Person对象;如果当前操作系统是Linux操作系统,那么就向Spring容器中注册名称为linus的Person对象。要想实现这个需求,我们就得要使用@Conditional注解了。

使用Spring中的AnnotationConfigApplicationContext类就能够获取到当前操作系统的类型,如下所示。

AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);
ConfigurableEnvironment environment = applicationContext.getEnvironment(); // 拿到IOC运行环境
// 动态获取坏境变量的值,例如操作系统的名字
String property = environment.getProperty("os.name"); // 获取操作系统的名字,例如Windows 10
System.out.println(property);

要想使用@Conditional注解,我们需要实现Condition接口来为@Conditional注解设置条件,所以,这里我们创建了两个实现Condition接口的类,它们分别是LinuxCondition和WindowsCondition,如下所示。

package com.atguigu.condition;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
* 判断操作系统是否是Linux系统
* @author liayun
*
*/
public class LinuxCondition implements Condition {

    /**
    * ConditionContext:判断条件能使用的上下文(环境)
    * AnnotatedTypeMetadata:当前标注了@Conditional注解的注释信息
    */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 判断操作系统是否是Linux系统

        // 1. 获取到bean的创建工厂(能获取到IOC容器使用到的BeanFactory,它就是创建对象以及进行装配的工厂)
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        // 2. 获取到类加载器
        ClassLoader classLoader = context.getClassLoader();
        // 3. 获取当前环境信息,它里面就封装了我们这个当前运行时的一些信息,包括环境变量,以及包括虚拟机的一些变量
        Environment environment = context.getEnvironment();
        // 4. 获取到bean定义的注册类
        BeanDefinitionRegistry registry = context.getRegistry();
        // 在这儿还可以做更多的判断,比如说我判断一下Spring容器中是不是包含有某一个bean,就像下面这样,如果Spring容器中果真包含有名称为person的bean,那么就做些什么事情...
        boolean definition = registry.containsBeanDefinition("person");


        String property = environment.getProperty("os.name");
        if (property.contains("linux")) {
            return true;
        }

        return false;
    }

}

package com.atguigu.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
* 判断操作系统是否是Windows系统
* @author liayun
*
*/
public class WindowsCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        String property = environment.getProperty("os.name");
        if (property.contains("Windows")) {
            return true;
        }
        return false;
    }

}

这里我得好好说道说道通过context的getRegistry()方法获取到的bean定义的注册对象,即BeanDefinitionRegistry对象了。它到底是个啥呢?我们可以点进去看一下它的源码,如下所示,可以看到它是一个接口。

// 4. 获取到bean定义的注册类
BeanDefinitionRegistry registry = context.getRegistry();
点进 BeanDefinitionRegistry

//BeanDefinitionRegistry Spring容器中所有的bean都是通过 BeanDefinitionRegistry 对象来进行注册的,
//因此我们可以用他来查看Spring容器中 注册了那些Bean
public interface BeanDefinitionRegistry extends AliasRegistry {

//registerBeanDefinition 该方法表明我们可以通过BeanDefinitionRegistry 对象在Spring容器中注册一个bean 
	void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
			throws BeanDefinitionStoreException;

	//removeBeanDefinition 该方法表明我们可以通过BeanDefinitionRegistry 对象在Spring容器中移除一个bean 
	void removeBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

	//getBeanDefinition 该方法表明我们可以通过BeanDefinitionRegistry 对象查看某个bean的定义信息
	BeanDefinition getBeanDefinition(String beanName) throws NoSuchBeanDefinitionException;

//containsBeanDefinition 该方法表明我们可以通过BeanDefinitionRegistry 对象查看容器中是否包含某一个bean的定义
	boolean containsBeanDefinition(String beanName);

	/**
	 * Return the names of all beans defined in this registry.
	 * @return the names of all beans defined in this registry,
	 * or an empty array if none defined
	 */
	String[] getBeanDefinitionNames();

	/**
	 * Return the number of beans defined in the registry.
	 * @return the number of beans defined in the registry
	 */
	int getBeanDefinitionCount();

	/**
	 * Determine whether the given bean name is already in use within this registry,
	 * i.e. whether there is a local bean or alias registered under this name.
	 * @param beanName the name to check
	 * @return whether the given bean name is already in use
	 */
	boolean isBeanNameInUse(String beanName);

}

在上图中我对BeanDefinitionRegistry接口的源码作了一点简要的说明。知道了,Spring容器中所有的bean都可以通过BeanDefinitionRegistry对象来进行注册,因此我们可以通过它来查看Spring容器中到底注册了哪些bean。而且仔细查看一下BeanDefinitionRegistry接口中声明的各个方法,你就知道我们还可以通过BeanDefinitionRegistry对象向Spring容器中注册一个bean、移除一个bean、查询某一个bean的定义信息或者判断Spring容器中是否包含有某一个bean的定义。

因此,我们可以在这儿做更多的判断,比如说我可以判断一下Spring容器中是不是包含有某一个bean,就像下面这样,如果Spring容器中果真包含有名称为person的bean,那么就做些什么事情,如果没包含,那么我们还可以利用BeanDefinitionRegistry对象向Spring容器中注册一个bean。
和BeanDefinitionRegistry没关系

package com.meimeixia.condition;

import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.env.Environment;
import org.springframework.core.type.AnnotatedTypeMetadata;

/**
* 判断操作系统是否是Linux系统
* @author liayun
*
*/
public class LinuxCondition implements Condition {

    /**
    * ConditionContext:判断条件能使用的上下文(环境)
    * AnnotatedTypeMetadata:当前标注了@Conditional注解的注释信息
    */
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 判断操作系统是否是Linux系统
        
        // 1. 获取到bean的创建工厂(能获取到IOC容器使用到的BeanFactory,它就是创建对象以及进行装配的工厂)
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        // 2. 获取到类加载器
        ClassLoader classLoader = context.getClassLoader();
        // 3. 获取当前环境信息,它里面就封装了我们这个当前运行时的一些信息,包括环境变量,以及包括虚拟机的一些变量
        Environment environment = context.getEnvironment();
        // 4. 获取到bean定义的注册类
        BeanDefinitionRegistry registry = context.getRegistry();
        
        // 在这儿还可以做更多的判断,比如说我判断一下Spring容器中是不是包含有某一个bean,就像下面这样,如果Spring容器中果真包含有名称为person的bean,那么就做些什么事情...
        boolean definition = registry.containsBeanDefinition("person");
//和上边的 判断一下Spring容器中是不是包含有某一个bean 没关系
        String property = environment.getProperty("os.name");
        if (property.contains("linux")) {
            return true;
        }
        
        return false;
    }

}



/**
* 判断操作系统是否是Windows系统
* @author liayun
*
*/
public class WindowsCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Environment environment = context.getEnvironment();
        String property = environment.getProperty("os.name");
        if (property.contains("Windows")) {
            return true;
        }
        return false;
    }

}

然后,我们就需要在MainConfig2配置类中使用@Conditional注解添加条件了。添加该注解后的方法如下所示。

@Conditional({WindowsCondition.class})
	@Bean("bill")
	public Person person01() {
		return new Person("Bill Gates", 62);
	}
	
	@Conditional({LinuxCondition.class})
	@Bean("linus")
	public Person person02() {
		return new Person("linus", 48);
	}

可以看到,输出结果中不再含有名称为linus的bean了,这说明程序中检测到当前操作系统为Windows 10之后,没有向Spring容器中注册名称为linus的bean。

此外,@Conditional注解也可以标注在类上,标注在类上的含义是:只有满足了当前条件,这个配置类中配置的所有bean注册才能生效,也就是对配置类中的组件进行统一设置。

package com.atguigu.config;

import com.atguigu.condition.LinuxCondition;
import com.atguigu.condition.WindowsCondition;
import org.springframework.context.annotation.*;

import com.atguigu.bean.Person;

@Configuration
// 对配置类中的组件进行统一设置
@Conditional({WindowsCondition.class}) // 满足当前条件,这个类中配置的所有bean注册才能生效
public class MainConfig2 {

	// 通过@Scope注解来指定该bean的作用范围,也可以说成是调整作用域
//	@Scope("prototype")
	@Bean("person")
	@Lazy
	public Person person() {
		return new Person("美美侠", 25);
	}

	@Conditional({LinuxCondition.class})
	@Bean("bill")
	public Person person01() {
		return new Person("Bill Gates", 62);
	}
	@Conditional({WindowsCondition.class})
	@Bean("linus")
	public Person person02() {
		return new Person("linus", 48);
	}

}

可以看到,没有任何bean的定义信息输出,这是因为程序检测到了当前操作系统为linux,没有向Spring容器中注册任何bean的缘故导致的。

@Conditional与@Profile这俩注解的对比

Spring 3.0也有一些和@Conditional相似的注解,它们是Spring SPEL表达式和Spring Profiles注解,但是Spring 4.0之后的@Conditional注解要比@Profile注解更加高级。@Profile注解用来加载应用程序的环境,该注解仅限于根据预定义属性编写条件检查,而@Conditional注解则没有此限制。

Spring中的@Profile和@Conditional这俩注解都是用来检查If…then…else的语义。然而,Spring 4.0之后的@Conditional注解是@Profile注解的更新用法。

Spring 3.0中的@Profile仅用于编写基于Environment变量的条件检查。配置文件可用于基于环境加载应用程序配置(这句话好绕口啊😭)。
Spring 4.0之后的@Conditional注解允许开发人员为条件检查定义用户定义的策略。此外,@Conditional注解还可以用于条件bean注册。

Spring注解驱动开发第8讲——使用@Import注解给容器中快速导入一个组件

我们知道,我们可以将一些bean组件交由Spring来管理,并且Spring还支持单实例bean和多实例bean。我们自己写的类,自然是可以通过包扫描+给组件标注注解(@Controller、@Servcie、@Repository、@Component)的形式将其注册到IOC容器中,但这种方式比较有局限性,局限于我们自己写的类,比方说我们自己写的类,我们当然能把以上这些注解标注上去了。

那么如果不是我们自己写的类,比如说我们在项目中会经常引入一些第三方的类库,我们需要将这些第三方类库中的类注册到Spring容器中,该怎么办呢?此时,我们就可以使用@Bean和@Import注解将这些类快速的导入Spring容器中。

接下来,我们来一起探讨下如何使用@Import注解给容器中快速导入一个组件。

@Import注解概述

我们先看一下@Import注解的源码,如下所示。

从源码里面可以看出@Import可以配合Configuration、ImportSelector以及ImportBeanDefinitionRegistrar来使用,下面的or表示也可以把Import当成普通的bean来使用。

注意:@Import注解只允许放到类上面,不允许放到方法上。

@Import注解的使用方式

@Import注解的三种用法主要包括:

直接填写class数组的方式

ImportSelector接口的方式,即批量导入,这是重点

ImportBeanDefinitionRegistrar接口方式,即手工注册bean到容器中

@Import导入组件的简单示例

没有使用@Import注解时的效果

首先,我们创建一个Color类,这个类是一个空类,没有成员变量和方法,如下所示。

package com.meimeixia.bean;

public class Color {

}

然后,我们在IOCTest类中创建一个testImport()方法,在其中输出Spring容器中所有bean定义的名字,来查看是否存在Color类对应的bean实例,以此来判断Spring容器中是否注册有Color类对应的bean实例。

@Test
public void testImport() {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);
    String[] definitionNames = applicationContext.getBeanDefinitionNames();
    for (String name : definitionNames) {
        System.out.println(name);
    }
}

运行以上testImport()方法之后,输出的结果信息如下所示。
在这里插入图片描述
可以看到Spring容器中并没有Color类对应的bean实例。

使用@Import注解时的效果

首先,我们在MainConfig2配置类上添加一个@Import注解,并将Color类填写到该注解中,如下所示。

package com.atguigu.config;

import com.atguigu.bean.Color;
import com.atguigu.condition.LinuxCondition;
import com.atguigu.condition.WindowsCondition;
import org.springframework.context.annotation.*;

import com.atguigu.bean.Person;

@Configuration
// 对配置类中的组件进行统一设置
@Conditional({WindowsCondition.class}) // 满足当前条件,这个类中配置的所有bean注册才能生效
@Import(Color.class) // @Import快速地导入组件,id默认是组件的全类名
public class MainConfig2 {

	// 通过@Scope注解来指定该bean的作用范围,也可以说成是调整作用域
//	@Scope("prototype")
	@Bean("person")
	@Lazy
	public Person person() {
		return new Person("美美侠", 25);
	}

	@Conditional({LinuxCondition.class})
	@Bean("bill")
	public Person person01() {
		return new Person("Bill Gates", 62);
	}
	@Conditional({WindowsCondition.class})
	@Bean("linus")
	public Person person02() {
		return new Person("linus", 48);
	}

}

可以看到,输出结果中打印了com.meimeixia.bean.Color,说明使用@Import注解快速地导入组件时,容器中就会自动注册这个组件,并且id默认是组件的全类名。

在这里插入图片描述

@Import注解还支持同时导入多个类,例如,我们再次创建一个Red类,如下所示。

package com.meimeixia.bean;

public class Red {

}

然后,我们也将以上Red类添加到@Import注解中,如下所示。

package com.meimeixia.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Lazy;

import com.meimeixia.bean.Color;
import com.meimeixia.bean.Person;
import com.meimeixia.condition.LinuxCondition;
import com.meimeixia.condition.WindowsCondition;

// 对配置类中的组件进行统一设置
@Conditional({WindowsCondition.class}) // 满足当前条件,这个类中配置的所有bean注册才能生效
@Configuration
@Import({Color.class, Red.class}) // @Import快速地导入组件,id默认是组件的全类名
public class MainConfig2 {
	
	@Lazy
	@Bean("person")
	public Person person() {
		System.out.println("给容器中添加咱们这个Person对象...");
		return new Person("美美侠", 25);
	}
	
	@Bean("bill")
	public Person person01() {
		return new Person("Bill Gates", 62);
	}
	
	@Conditional({LinuxCondition.class})
	@Bean("linus")
	public Person person02() {
		return new Person("linus", 48);
	}
	
}

接着,我们运行IOCTest类中的testImport()方法,发现输出的结果信息如下所示。
在这里插入图片描述

可以看到,结果信息中同时输出了com.atguigu.bean.Color和com.atguigu.bean.Red,说明Color类对应的bean实例和Red类对应的bean实例都导入到Spring容器中去了。

Spring注解驱动开发第9讲——在@Import注解中使用ImportSelector接口导入bean

在上一讲关于Spring的@Import注解的文章中,我们简单介绍了如何使用@Import注解给容器中快速导入一个组件,而我们知道,@Import注解总共包含三种使用方法,上一讲已经讲解完第一种方式了,今天我们就一起来学习下关于@Import注解非常重要的第二种方式,即ImportSelector接口的方式。

ImportSelector接口概述

ImportSelector接口是Spring中导入外部配置的核心接口,在Spring Boot的自动化配置和@EnableXXX(功能性注解)都有它的存在。我们先来看一下ImportSelector接口的源码,如下所示。
在这里插入图片描述
该接口文档上说的明明白白,其主要作用是收集需要导入的配置类,**selectImports()方法的返回值就是我们向Spring容器中导入的类的全类名。**如果该接口的实现类同时实现EnvironmentAware,BeanFactoryAware,BeanClassLoaderAware或者ResourceLoaderAware,那么在调用其selectImports()方法之前先调用上述接口中对应的方法如果需要在所有的@Configuration处理完再导入时,那么可以实现DeferredImportSelector接口。

在ImportSelector接口的selectImports()方法中,存在一个**AnnotationMetadata类型的参数,这个参数能够获取到当前标注@Import注解的类的所有注解信息,**也就是说不仅能获取到@Import注解里面的信息,还能获取到其他注解的信息。

ImportSelector接口实例

首先,我们创建一个MyImportSelector类实现ImportSelector接口,如下所示,先在selectImports()方法中返回null,后面我们再来改。

至于使用MyImportSelector类要导入哪些bean,就需要你在MyImportSelector类的selectImports()方法中进行设置了,只须在MyImportSelector类的selectImports()方法中返回要导入的类的全类名(包名+类名)即可。

接着,我们就要运行IOCTest类中的testImport()方法了,在运行该方法之前,咱们先在MyImportSelector类的selectImports()方法处打一个断点,debug调试一下,如下图所示。

可以清楚地看到,selectImports()方法中的AnnotationMetadata类型的参数确实获取到了当前标注@Import注解的类的所有注解信息,第一个获取到的注解是@Conditional,其他依此类推。

此时,我们按F8键,会发现Eclipse控制台打印了一个空指针异常,如下图所示。

因此要想不报这样一个空指针异常,咱们MyImportSelector类的selectImports()方法里面就不能返回一个null值了,不妨先返回一个空数组试试,就像下面这样。

// 方法不要返回null值,否则会报空指针异常
		return new String[]{}; // 可以返回一个空数组

现在总算是能输出点东西了,而且还不报空指针异常了。由于咱们在MyImportSelector类的selectImports()方法中返回的是一个空数组,所以还没有在IOC容器中注册任何组件,自然Eclipse控制台就没有输出通过ImportSelector接口的方式注册的任何组件的名字了。

接下来,我们就来创建两个Java类,它们分别是Bule类和Yellow类,如下所示。

package com.meimeixia.bean;

public class Bule {

}

package com.meimeixia.bean;

public class Yellow {

}

然后,我们将以上两个类的全类名返回到MyImportSelector类的selectImports()方法中,此时,MyImportSelector类的selectImports()方法如下所示。

package com.meimeixia.condition;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * 自定义逻辑,返回需要导入的组件
 * @author liayun
 *
 */
public class MyImportSelector implements ImportSelector {

	// 返回值:就是要导入到容器中的组件的全类名
	// AnnotationMetadata:当前标注@Import注解的类的所有注解信息,也就是说不仅能获取到@Import注解里面的信息,还能获取到其他注解的信息
	@Override
	public String[] selectImports(AnnotationMetadata importingClassMetadata) { // 在这一行打个断点,debug调试一下
		
		// 方法不要返回null值,否则会报空指针异常
//		return new String[]{}; // 可以返回一个空数组
		return new String[]{"com.meimeixia.bean.Bule", "com.meimeixia.bean.Yellow"};
	}

}

在这里插入图片描述

可以看到,输出结果中多出了com.meimeixia.bean.Bule和com.meimeixia.bean.Yellow。这说明使用ImportSelector接口的方式已经成功将Bule类和Yellow类导入到Spring容器中去了。

Spring注解驱动开发第10讲——在@Import注解中使用ImportBeanDefinitionRegistrar向容器中注册bean

在前面的文章中,我们学习了如何使用@Import注解向Spring容器中导入bean,不仅可以使用@Import注解快速向容器中导入bean,也可以在@Import注解中使用ImportSelector接口的方法导入bean,今天,我们就来说说,如何在@Import注解中使用ImportBeanDefinitionRegistrar向容器中注册bean。

ImportBeanDefinitionRegistrar接口的简要介绍

我们先来看看ImportBeanDefinitionRegistrar是个什么鬼,点击进入ImportBeanDefinitionRegistrar源码,如下所示。
在这里插入图片描述

由源码可以看出,ImportBeanDefinitionRegistrar本质上是一个接口。在ImportBeanDefinitionRegistrar接口中,有一个registerBeanDefinitions()方法,通过该方法,我们可以向Spring容器中注册bean实例。

Spring官方在动态注册bean时,大部分套路其实是使用ImportBeanDefinitionRegistrar接口。

所有实现了该接口的类都会被ConfigurationClassPostProcessor处理,ConfigurationClassPostProcessor实现了BeanFactoryPostProcessor接口,所以ImportBeanDefinitionRegistrar中动态注册的bean是优先于依赖其的bean初始化的,也能被aop、validator等机制处理。优先于依赖其他bean初始化,因为BeanFactoryPostProcessor的顺序在bean初始化和实例化之前

使用方法

ImportBeanDefinitionRegistrar需要配合@Configuration和@Import这俩注解,其中,@Configuration注解定义Java格式的Spring配置文件,@Import注解导入实现了ImportBeanDefinitionRegistrar接口的类。

ImportBeanDefinitionRegistrar接口实例

既然ImportBeanDefinitionRegistrar是一个接口,那我们就创建一个MyImportBeanDefinitionRegistrar类,去实现ImportBeanDefinitionRegistrar接口,如下所示。



public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

	/**
	 * AnnotationMetadata:当前类的注解信息
	 * BeanDefinitionRegistry:BeanDefinition注册类
	 * 
	 * 我们可以通过调用BeanDefinitionRegistry接口中的registerBeanDefinition方法,手动注册所有需要添加到容器中的bean
	 */
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

	}

}

可以看到,这里,我们先创建了MyImportBeanDefinitionRegistrar类的大体框架。然后,我们在MainConfig2配置类上的@Import注解中,添加MyImportBeanDefinitionRegistrar类,如下所示。



// 对配置类中的组件进行统一设置
@Conditional({WindowsCondition.class}) // 满足当前条件,这个类中配置的所有bean注册才能生效
@Configuration
@Import({Color.class, Red.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class}) // @Import快速地导入组件,id默认是组件的全类名
public class MainConfig2 {
	
	@Lazy
	@Bean("person")
	public Person person() {
		System.out.println("给容器中添加咱们这个Person对象...");
		return new Person("美美侠", 25);
	}
	
	@Bean("bill")
	public Person person01() {
		return new Person("Bill Gates", 62);
	}
	
	@Conditional({LinuxCondition.class})
	@Bean("linus")
	public Person person02() {
		return new Person("linus", 48);
	}
	
}

接着,创建一个RainBow类,作为测试ImportBeanDefinitionRegistrar接口的bean来使用,如下所示。

package com.meimeixia.bean;

public class RainBow {

}

紧接着,我们就要实现MyImportBeanDefinitionRegistrar类中的registerBeanDefinitions()方法里面的逻辑了,添加逻辑后的registerBeanDefinitions()方法如下所示。



public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {

	/**
	 * AnnotationMetadata:当前类的注解信息
	 * BeanDefinitionRegistry:BeanDefinition注册类
	 * 
	 * 我们可以通过调用BeanDefinitionRegistry接口中的registerBeanDefinition方法,手动注册所有需要添加到容器中的bean
	 */
	@Override
	public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
		boolean definition = registry.containsBeanDefinition("com.meimeixia.bean.Red");
		boolean definition2 = registry.containsBeanDefinition("com.meimeixia.bean.Bule");
		if (definition && definition2) {
			// 指定bean的定义信息,包括bean的类型、作用域等等
			// RootBeanDefinition是BeanDefinition接口的一个实现类
			RootBeanDefinition beanDefinition = new RootBeanDefinition(RainBow.class); // bean的定义信息
			// 注册一个bean,并且指定bean的名称
			registry.registerBeanDefinition("rainBow", beanDefinition);
		}
	}

}

以上registerBeanDefinitions()方法的实现逻辑很简单,就是判断Spring容器中是否同时存在以com.atguigu.bean.Red命名的bean和以com.atguigu.bean.Bule命名的bean,如果真的同时存在,那么向Spring容器中注入一个以rainBow命名的bean。

在这里插入图片描述

可以看到,此时输出了rainBow,说明Spring容器中已经成功注册了以rainBow命名的bean。

Spring注解驱动开发第11讲——面试官让我说说:如何使用FactoryBean向Spring容器中注册bean?

经过前面的学习,我们知道可以通过多种方式向Spring容器中注册bean。可以使用@Configuration注解结合@Bean注解向Spring容器中注册bean;可以按照条件向Spring容器中注册bean;可以使用@Import注解向容器中快速导入bean对象;可以在@Import注解中使用ImportBeanDefinitionRegistrar向容器中注册bean。

而在本文中,我就来讲讲如何使用FactoryBean向Spring容器中注册bean。

FactoryBean概述

一般情况下,**Spring是通过反射机制利用bean的class属性指定实现类来实例化bean的。**在某些情况下,实例化bean过程比较复杂,如果按照传统的方式,那么则需要在标签中提供大量的配置信息,配置方式的灵活性是受限的,这时采用编码的方式可以得到一个更加简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化bean的逻辑。

FactoryBean接口对于Spring框架来说占有非常重要的地位,Spring自身就提供了70多个FactoryBean接口的实现。它们隐藏了实例化一些复杂bean的细节,给上层应用带来了便利。从Spring 3.0开始,FactoryBean开始支持泛型,即接口声明改为FactoryBean的形式。

在Spring 4.3.12.RELEASE这个版本中,FactoryBean接口的定义如下所示。

在这里插入图片描述
T getObject():返回由FactoryBean创建的bean实例,如果isSingleton()返回true,那么该实例会放到Spring容器中单实例缓存池中
boolean isSingleton():返回由FactoryBean创建的bean实例的作用域是singleton还是prototype
Class getObjectType():返回FactoryBean创建的bean实例的类型

这里,需要注意的是:当配置文件中标签的class属性配置的实现类是FactoryBean时,通过 getBean()方法返回的不是FactoryBean本身,而是FactoryBean#getObject()方法所返回的对象,相当于FactoryBean#getObject()代理了getBean()方法。

FactoryBean案例

首先,创建一个ColorFactoryBean类,它得实现FactoryBean接口,如下所示。

package com.meimeixia.bean;

import org.springframework.beans.factory.FactoryBean;

/**
 * 创建一个Spring定义的FactoryBean
 * T(泛型):指定我们要创建什么类型的对象
 * @author liayun
 * 
 */
public class ColorFactoryBean implements FactoryBean<Color> {

	// 返回一个Color对象,这个对象会添加到容器中
	@Override
	public Color getObject() throws Exception {
		// TODO Auto-generated method stub
		System.out.println("ColorFactoryBean...getObject...");
		return new Color();
	}

	@Override
	public Class<?> getObjectType() {
		// TODO Auto-generated method stub
		return Color.class; // 返回这个对象的类型
	}

	// 是单例吗?
	// 如果返回true,那么代表这个bean是单实例,在容器中只会保存一份;
	// 如果返回false,那么代表这个bean是多实例,每次获取都会创建一个新的bean
	@Override
	public boolean isSingleton() {
		// TODO Auto-generated method stub
		return false;
	}

}

然后,我们在MainConfig2配置类中加入ColorFactoryBean的声明,如下所示。



// 对配置类中的组件进行统一设置
@Conditional({WindowsCondition.class}) // 满足当前条件,这个类中配置的所有bean注册才能生效
@Configuration
@Import({Color.class, Red.class, MyImportSelector.class, MyImportBeanDefinitionRegistrar.class}) // @Import快速地导入组件,id默认是组件的全类名
public class MainConfig2 {
	
	@Lazy
	@Bean("person")
	public Person person() {
		System.out.println("给容器中添加咱们这个Person对象...");
		return new Person("美美侠", 25);
	}

	@Bean("bill")
	public Person person01() {
		return new Person("Bill Gates", 62);
	}
	
	@Conditional({LinuxCondition.class})
	@Bean("linus")
	public Person person02() {
		return new Person("linus", 48);
	}

	@Bean
	public ColorFactoryBean colorFactoryBean() {
		return new ColorFactoryBean();
	}
	
}

这里需要小伙伴们注意的是:我在这里使用@Bean注解向Spring容器中注册的是ColorFactoryBean对象。

那现在我们就来看看Spring容器中到底都有哪些bean。我们所要做的事情就是,运行IOCTest类中的testImport()方法,此时,输出的结果信息如下所示。
在这里插入图片描述

可以看到,结果信息中输出了一个colorFactoryBean,我们看下这个colorFactoryBean到底是个什么鬼!此时,我们对IOCTest类中的testImport()方法稍加改动,添加获取colorFactoryBean的代码,并输出colorFactoryBean实例的类型,如下所示。

@Test
public void testImport() {
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(MainConfig2.class);
    String[] definitionNames = applicationContext.getBeanDefinitionNames();
    for (String name : definitionNames) {
        System.out.println(name);
    }
    
    // 工厂bean获取的是调用getObject方法创建的对象
    Object bean2 = applicationContext.getBean("colorFactoryBean");
    System.out.println("bean的类型:" + bean2.getClass());

}

在这里插入图片描述

可以看到,虽然我在代码中使用@Bean注解注入的是ColorFactoryBean对象,但是实际上从Spring容器中获取到的bean对象却是调用ColorFactoryBean类中的getObject()方法获取到的Color对象。

如何在Spring容器中获取到FactoryBean对象本身呢?

之前,我们使用@Bean注解向Spring容器中注册的是ColorFactoryBean,获取出来的却是Color对象。那么,小伙伴们可能会问了,我就想获取ColorFactoryBean实例,那么该怎么办呢?

其实,这也很简单,只需要在获取工厂Bean本身时,在id前面加上&符号即可,例如&colorFactoryBean。

 Object bean4 = applicationContext.getBean("&colorFactoryBean");
    System.out.println(bean4.getClass());

可以看到,在获取bean时,在id前面加上&符号就会获取到ColorFactoryBean实例对象。

那问题又来了!!为什么在id前面加上&符号就会获取到ColorFactoryBean实例对象呢?

接下来,我们就要揭开这个神秘的面纱了,打开BeanFactory接口,查看其源码。

public interface BeanFactory {

	/**
	 * Used to dereference a {@link FactoryBean} instance and distinguish it from
	 * beans <i>created</i> by the FactoryBean. For example, if the bean named
	 * {@code myJndiObject} is a FactoryBean, getting {@code &myJndiObject}
	 * will return the factory, not the instance returned by the factory.
	 */
	String FACTORY_BEAN_PREFIX = "&";

看到这里,是不是明白了呢?没错,在BeanFactory接口中定义了一个&前缀,只要我们使用bean的id来从Spring容器中获取bean时,Spring就会知道我们是在获取FactoryBean本身。

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值