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本身。