本篇文章想和大家一起来讨论一下BeanFacotory复杂的家族体系背后的理念所在,本篇文章针对的是对spring体系结构和源码有过一定了解的小伙伴
先给大家回顾一下这两个主要的IOC容器类型
Spring提供了两种容器类型:BeanFactory和ApplicationContext。
BeanFactory
: 基础类型IoC容器,提供完整的IoC服务支持。如果没有特殊指定,默认采用延迟初始化策略(lazy-load)
。只有当客户端对象需要访问容器中的某个受管对象的时候,才对该受管对象进行初始化以及依赖注入操作。所以,相对来说,容器启动初期速度较快,所需要的资源有限。对于资源有限,并且功能要求不是很严格的场景,BeanFactory是比较合适的IoC容器选择。
ApplicationContext
:ApplicationContext在BeanFactory的基础上构建,是相对比较高级的容器实现,除了拥有BeanFactory的所有支持,ApplicationContext还提供了其他高级特性,比如事件发布、国际化信息支持等
,这些会在后面详述。ApplicationContext所管理的对象,在该类型容器启动之后,默认全部初始化并绑定完成。所以,相对于BeanFactory来说,ApplicationContext要求更多的系统资源,同时,因为在启动时就完成所有初始化,容器启动时间较之BeanFactory也会长一些。在那些系统资源充足,并且要求更多功能的场景中,ApplicationContext类型的容器是比较合适的选择。
通过下图,我们可以对BeanFactory和ApplicationContext之间的关系有一个更清晰的认识。
ApplicationContext间接继承自BeanFactory,所以说它是构建于BeanFactory之上的IoC容器。并且ApplicationContext还继承了其他三个接口
BeanFactory只是一个接口,我们最终需要一个该接口的实现来进行实际的Bean的管理,DefaultListableBeanFactory就是这么一个比较通用的BeanFactory实现类。
DefaultListableBeanFactory除了间接地实现了BeanFactory接口,还实现了BeanDefinitionRegistry接口,该接口才是在BeanFactory的实现中担当Bean注册管理的角色。
基本上,BeanFactory接口只定义如何访问容器内管理的Bean的方法,各个BeanFactory的具体实现类负责具体Bean的注册以及管理工作。
BeanDefinitionRegistry接口定义抽象了Bean的注册逻辑。通常情况下,具体的BeanFactory实现类会实现这个接口来管理Bean的注册。
打个比方说,BeanDefinitionRegistry就像图书馆的书架,所有的书是放在书架上的。
虽然你还书或者借书都是跟图书馆(也就是BeanFactory,或许BookFactory可能更好些)打交道,但书架才是图书馆存放各类图书的地方。
所以,书架相对于图书馆来说,就是它的“BookDefinitionRegistry”。 每一个受管的对象,在容器中都会有一个BeanDefinition的实例(instance)与之相对应,该BeanDefinition的实例负责保存对象的所有必要信息,包括其对应的对象的class类型、是否是抽象 类、构造方法参数以及其他属性等。当客户端向BeanFactory请求相应对象的时候,BeanFactory会通过这些信息为客户端返回一个完备可用的对象实例。
Spring的IoC容器支持两种配置文件格式:Properties文件格式和XML文件格式。当然,如果你愿意也可以引入自己的文件格式,前提是真的需要。
采用外部配置文件时,Spring的IoC容器有一个统一的处理方式。通常情况下,需要根据不同的外部配置文件格式,给出相应的BeanDefinitionReader实现类,由BeanDefinitionReader的相应实现类负责将相应的配置文件内容读取并映射到BeanDefinition,然后将映射后的BeanDefinition注册到一个BeanDefinitionRegistry,之后,BeanDefinitionRegistry即完成Bean的注册和加载。
当然,大部分工作,包括解析文件格式、装配BeanDefinition之类的工作,都是由BeanDefinitionReader的相应实现类来做的,BeanDefinitionRegistry只不过负责保管而已。整个过程类似于如下
代码:
BeanDefinitionRegistry beanRegistry = <某个BeanDefinitionRegistry实现类,通常为DefaultListableBeanFactory>;
BeanDefinitionReader beanDefinitionReader = new BeanDefinitionReaderImpl(beanRegistry);
beanDefinitionReader.loadBeanDefinitions("配置文件路径");
// 现在我们就取得了一个可用的BeanDefinitionRegistry实例
Spring提供了org.springframework.beans.factory.support.PropertiesBeanDefinitionReader类用于Properties格式配置文件的加载,所以,我们不用自己去实现BeanDefinitionReader,只要根据该类的读取规则,提供相应的配置文件即可。
Properties格式表达的依赖注入配置内容
djNewsProvider.(class)=..FXNewsProvider
# ----------通过构造方法注入的时候-------------
djNewsProvider.$0(ref)=djListener
djNewsProvider.$1(ref)=djPersister
# ----------通过setter方法注入的时候---------
# djNewsProvider.newsListener(ref)=djListener
# djNewsProvider.newPersistener(ref)=djPersister
大家不需要纠结为何要这样写,因为properties配置文件定义bean的依赖关系本身用的就少,所以目前大家看看,知道有这回事就行了
加载Properties配置的BeanFactory的使用演示
{
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory)bindViaPropertiesFile(beanRegistry);
FXNewsProvider newsProvider =
(FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaPropertiesFile(BeanDefinitionRegistry registry)
{
PropertiesBeanDefinitionReader reader =
new PropertiesBeanDefinitionReader(registry);
reader.loadBeanDefinitions("classpath:../../binding-config.properties");
return (BeanFactory)registry;
}
基于Properties的加载方式就是这么简单,所有的信息配置到Properties文件即可,不用再通过冗长的代码来完成对象的注册和依赖绑定。这些工作就交给相应的BeanDefinitionReader来做吧!哦,我的意思是,让给PropertiesBeanDefinitionReader来做。
注意 Spring提供的PropertiesBeanDefinitionReader是按照Spring自己的文件配置规则进 行加载的,而同样的道理,你也可以按照自己的规则来提供相应的Properties配置文件。只不过,现在需要实现你自己的“PropertiesBeanDefinitionReader”来读取并解析。
这个应该是大家最熟悉不过的了
FX新闻系统相关类对应XML格式的配置内容
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" ➥
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="djNewsProvider" class="..FXNewsProvider">
<constructor-arg index="0">
<ref bean="djNewsListener"/>
</constructor-arg>
<constructor-arg index="1">
<ref bean="djNewsPersister"/>
</constructor-arg>
</bean>
<bean id="djNewsListener" class="..impl.DowJonesNewsListener">
</bean>
<bean id="djNewsPersister" class="..impl.DowJonesNewsPersister">
</bean>
</beans>
加载XML配置文件的BeanFactory的使用演示
public static void main(String[] args)
{
DefaultListableBeanFactory beanRegistry = new DefaultListableBeanFactory();
BeanFactory container = (BeanFactory)bindViaXMLFile(beanRegistry);
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("djNewsProvider");
newsProvider.getAndPersistNews();
}
public static BeanFactory bindViaXMLFile(BeanDefinitionRegistry registry)
{
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry);
reader.loadBeanDefinitions("classpath:../news-config.xml");
return (BeanFactory)registry;
// 或者直接
//return new XmlBeanFactory(new ClassPathResource("../news-config.xml"));
}
与为Properties配置文件格式提供PropertiesBeanDefinitionReader相对应,Spring同样为XML格式的配置文件提供了现成的BeanDefinitionReader实现,即XmlBeanDefinitionReader。
XmlBeanDefinitionReader负责读取Spring指定格式的XML配置文件并解析,之后将解析后的文件内容映射到相应的BeanDefinition,并加载到相应的BeanDefinitionRegistry中(在这里是DefaultListableBeanFactory)。
这时,整个BeanFactory就可以放给客户端使用了。
除了提供XmlBeanDefinitionReader用于XML格式配置文件的加载,Spring还在DefaultListableBeanFactory的基础上构建了简化XML格式配置加载的XmlBeanFactory实现。从以上代码最后注释掉的一行,你可以看到使用了XmlBeanFactory之后,完成XML的加载和BeanFactory的初始化是多么简单.
注意 当然,如果你愿意,就像Properties方式可以扩展一样,XML方式的加载同样可以扩展。 虽然XmlBeanFactory基本上已经十分完备了,但如果出于某种目的,XmlBeanFactory或者默 认的XmlBeanDefinitionReader所使用的XML格式无法满足需要的话,你同样可以通过扩展 XmlBeanDefinitionReader或者直接实现自己的BeanDefinitionReader来达到自定义XML 配置文件加载的目的。Spring的可扩展性为你服务!
使用指定注解标注后的FXNews相关类
@Component
public class FXNewsProvider
{
@Autowired
private IFXNewsListener newsListener;
@Autowired
private IFXNewsPersister newPersistener;
public FXNewsProvider(IFXNewsListener newsListner,IFXNewsPersister newsPersister)
{
this.newsListener = newsListner;
this.newPersistener = newsPersister;
}
...
}
@Component
public class DowJonesNewsListener implements IFXNewsListener
{
...
}
@Component
public class DowJonesNewsPersister implements IFXNewsPersister
{
...
}
配置使用classpath-scanning功能
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" ➥
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ➥
xmlns:context="http://www.springframework.org/schema/context" ➥
xmlns:tx="http://www.springframework.org/schema/tx" ➥
xsi:schemaLocation="http://www.springframework.org/schema/beans ➥
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd ➥
http://www.springframework.org/schema/context ➥
http://www.springframework.org/schema/context/spring-context-2.5.xsd ➥
http://www.springframework.org/schema/tx ➥
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd">
<context:component-scan base-package="cn.spring21.project.base.package"/>
</beans>
< context:component-scan />会到指定的包(package)下面扫描标注有@Component的类,如果找到,则将它们添加到容器进行管理,并根据它们所标注的@Autowired为这些类注入符合条件的依赖对象。
在以上所有这些工作都完成之后,我们就可以像通常那样加载配置并执行当前应用程序了,如以下代码所示:
public static void main(String[] args)
{
ApplicationContext ctx = new ClassPathXmlApplicationContext("配置文件路径");
FXNewsProvider newsProvider = (FXNewsProvider)container.getBean("FXNewsProvider");
newsProvider.getAndPersistNews();
}
BeanFactory可以分层次(通过实现HierarchicalBeanFactory接口),容器A在初始化的时候,可以首先加载容器B中的所有对象定义,然后再加载自身的对象定义,这样,容器B就成为了容器A的父容器,容器A可以引用容器B中的所有对象定义:
BeanFactory parentContainer = new XmlBeanFactory(new ClassPathResource("父容器配置文件路径"));
BeanFactory childContainer = new XmlBeanFactory(new ClassPathResource("子容器配置文件路径"),parentContainer);
childContainer中定义的对象,如果通过parent指定依赖,则只能引用parentContainer中的对象定义
BeanFactory除了拥有作为IoC Service Provider的职责,作为一个轻量级容器,它还有着其他一些职责,其中就包括对象的生命周期管理。
scope用来声明容器中的对象所应该处的限定场景或者说该对象的存活时间,即容器在对象进入其相应的scope之前,生成并装配这些对象,在该对象不再处于这些scope的限定之后,容器通常会销毁这些对象。
Spring容器最初提供了两种bean的scope类型:singleton和prototype,但发布2.0之后,又引入了另外三种scope类型,即request、session和global session类型。
不过这三种类型有所限制,只能在Web应用中使用。也就是说,只有在支持Web应用的ApplicationContext中使用这三个scope才是合理的。
配置中的bean定义可以看作是一个模板,容器会根据这个模板来构造对象。但是要根据这个模板构造多少对象实例,又该让这些构造完的对象实例存活多久,则由容器根据bean定义的scope语意来决定。标记为拥有singleton scope的对象定义,在Spring的IoC容器中只存在一个实例,所有对该对象的引用将共享这个实例。该实例从容器启动,并因为第一次被请求而初始化之后,将一直存活到容器退出,也就是说,它与IoC容器“几乎”拥有相同的“寿命”。
针对声明为拥有prototype scope的bean定义,容器在接到该类型对象的请求的时候,会每次都重新生成一个新的对象实例给请求方。
虽然这种类型的对象的实例化以及属性设置等工作都是由容器负责的,但是只要准备完毕,并且对象实例返回给请求方之后,容器就不再拥有当前返回对象的引用,请求方需要自己负责当前返回对象的后继生命周期的管理工作,包括该对象的销毁。也就是说,容器每次返回给请求方一个新的对象实例之后,就任由这个对象实例“自生自灭”了
对于那些请求方不能共享使用的对象类型,应该将其bean定义的scope设置为prototype。这样,每个请求方可以得到自己对应的一个对象实例。通常,声明为prototype的scope的bean定义类型,都是一些有状态的,比如保存每个顾客信息的对象。
在Spring 2.0之后的版本中,容器提供了对scope的扩展点,这样,你可以根据自己的需要或者应用的场景,来添加自定义的scope类型。需要说明的是,默认的singleton和prototype是硬编码到代码中的,而request、session和global session,包括自定义scope类型,则属于可扩展的scope行列,它们都实现了 org.springframework.beans.factory.config.Scope接口,该接口定义如下:
public interface Scope {
Object get(String var1, ObjectFactory<?> var2);
@Nullable
Object remove(String var1);
void registerDestructionCallback(String var1, Runnable var2);
@Nullable
Object resolveContextualObject(String var1);
@Nullable
String getConversationId();
}
要实现自己的scope类型,首先需要给出一个Scope接口的实现类,接口定义中的4个方法并非都是必须的,但get和remove方法必须实现。
public class ThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> threadLoacal = new ThreadLocal<Map<String, Object>>() {
@Override
protected Map<String, Object> initialValue() {
return new HashMap<String, Object>();
}
};
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> scope = threadLoacal.get();
Object obj = scope.get(name);
// 不存在则放入ThreadLocal
if (obj == null) {
obj = objectFactory.getObject();
scope.put(name, obj);
System.out.println("Not exists " + name + "; hashCode: " + obj.hashCode());
} else {
System.out.println("Exists " + name + "; hashCode: " + obj.hashCode());
}
return obj;
}
@Override
public Object remove(String name) {
Map<String, Object> scope = threadLoacal.get();
System.out.println("bean被销毁");
return scope.remove(name);
}
@Override
public String getConversationId() {
return null;
}
@Override
public void registerDestructionCallback(String arg0, Runnable arg1) {
}
@Override
public Object resolveContextualObject(String arg0) {
return null;
}
}
在上述代码中,threadLocal用于做线程之间的数据隔离。换言之,threadLocal实现了相同的线程相同名字的bean是同一个对象;不同的线程的相同名字的bean是不同的对象。
同时,我们将对象的hashCode打印了出来。如果他们是相同的对象,则hashCode是相同的。
有了Scope的实现类之后,我们需要把这个Scope注册到容器中,才能供相应的bean定义使用。通常情况下,我们可以使用ConfigurableBeanFactory的以下方法注册自定义scope:
void registerScope(String scopeName, Scope scope);
其中,参数scopeName就是使用的bean定义可以指定的名称,比如Spring框架默认提供的自定义scope类型request或者session。参数scope即我们提供的Scope实现类实例。
对于以上的ThreadScope,如果容器为BeanFactory类型(当然,更应该实现ConfigurableBeanFactory),我们可以通过如下方式来注册该Scope:
Scope threadScope = new ThreadScope();
beanFactory.registerScope("thread",threadScope);
之后,我们就可以在需要的bean定义中直接通过“thread”名称来指定该bean定义对应的scope为以上注册的ThreadScope了.
<bean id="beanName" class="..." scope="thread"/>
除了直接编码调用ConfigurableBeanFactory的registerScope来注册scope,Spring还提供了一个专门用于统一注册自定义scope的BeanFactoryPostProcessor实现(有关BeanFactoryPostProcessor的更多细节稍后将详述),
即org.springframework.beans.factory.config.CustomScopeConfigurer。对于ApplicationContext来说,因为它可以自动识别并加载BeanFactoryPostProcessor,所以我们就可以直接在配置文件中,通过这个CustomScopeConfigurer注册来ThreadScope。
使用CustomScopeConfigurer注册自定义scope
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread" value="com.foo.ThreadScope"/>
</map>
</property>
</bean>
编码方式:
@Configuration
@ComponentScan
public class AppConfig {
@Bean
public static CustomScopeConfigurer customScopeConfigurer() {
CustomScopeConfigurer customScopeConfigurer = new CustomScopeConfigurer();
Map<String, Object> map = new HashMap<String, Object>();
map.put("threadScope", new ThreadScope());
// 配置scope
customScopeConfigurer.setScopes(map);
return customScopeConfigurer;
}
}
在以上工作全部完成之后,我们就可以在自己的bean定义中使用这个新增加到容器的自定义scope“thread”了,如下代码演示了通常情况下“thread”自定义scope的使用:
<bean id="beanName" class="..." scope="thread">
</bean>
注解方式使用:
@Scope("threadScope")
@Service
public class MessageServiceImpl implements MessageService {
public String getMessage() {
return "Hello World!";
}
}
这里先不对源码进行深入分析,只是跑一块砖,先让大家稍微了解一下底层是如何实现的
FactoryBean是Spring容器提供的一种可以扩展容器对象实例化逻辑的接口,请不要将其与容器名称BeanFactory相混淆。FactoryBean,其主语是Bean,定语为Factory,也就是说,它本身与其他注册到容器的对象一样,只是一个Bean而已,只不过,这种类型的Bean本身就是生产对象的工厂(Factory)。
当某些对象的实例化过程过于烦琐,通过XML配置过于复杂,使我们宁愿使用Java代码来完成这个实例化过程的时候,或者,某些第三方库不能直接注册到Spring容器的时候,就可以实现org.springframework.beans.factory.FactoryBean接口,给出自己的对象实例化逻辑代码。
public interface FactoryBean<T> {
String OBJECT_TYPE_ATTRIBUTE = "factoryBeanObjectType";
@Nullable
T getObject() throws Exception;
@Nullable
Class<?> getObjectType();
default boolean isSingleton() {
return true;
}
}
getObject()方法会返回该FactoryBean“生产”的对象实例,我们需要实现该方法以给出自己的对象实例化逻辑;
getObjectType()方法仅返回getObject()方法所返回的对象的类型,如果预先无法确定,则返回null;
isSingleton()方法返回结果用于表明,工厂方法(getObject())所“生产”的对象是否要以singleton形式存在于容器中。如果以singleton形式存在,则返回true,否则返回false;
如果我们想每次得到的日期都是第二天,这个该如何解决呢?
@Component
@RequiredArgsConstructor
@Data
public class MyDay {
private final LocalDateTime nextDay;
}
@Component
public class MyFactory implements FactoryBean<LocalDateTime> {
@Override
public LocalDateTime getObject() throws Exception {
return LocalDateTime.now().plusDays(1);
}
@Override
public Class<?> getObjectType() {
return LocalDateTime.class;
}
@Override
public boolean isSingleton() {
return false;
}
}
@SpringBootApplication
public class Main
{
public static void main(String[] args)
{
ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);
Object myDay = applicationContext.getBean("myDay");
System.out.println(myDay);
}
}
FactoryBean类型的bean定义,通过正常的id引用,容器返回的是FactoryBean所“生产”的对象类型,而非FactoryBean实现本身。
如果一定要取得FactoryBean本身的话,可以通过在bean定义的id之前加前缀&来达到目的
ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);
//myFactory是factory工厂的bean id,但是默认返回的是工厂生产出来的bean对象
Object myDay = applicationContext.getBean("myFactory");
System.out.println(myDay);
Object myDayFactory = applicationContext.getBean("&myFactory");
System.out.println(myDayFactory);
Spring容器内部许多地方了使用FactoryBean。下面是一些比较常见的FactoryBean实现,你可以参照FactoryBean的Javadoc以了解更多内容。
JndiObjectFactoryBean LocalSessionFactoryBean SqlMapClientFactoryBean ProxyFactoryBean TransactionProxyFactoryBean
在学习以下内容之前,先提一下有关 10 bean的scope的使用“陷阱”,特别是prototype在容器中的使用,以此引出本节将要介绍的Spring容器较为独特的功能特性:方法注入(Method Injection)以及方法替换(Method Replacement)。
这里举一个prototype使用陷阱:
验证一下:
好,下面继续看:
第一个结果与我们想象中的一致,第二个结果不一致
原因:
第一个实例注入后,MyDay再也没有重新向容器申请新的实例。所以,容器也不会重新为其注入新的LocalDateTime类型的实例。
知道原因之后,我们就可以解决这个问题了。解决问题的关键在于保证每次从容器中取得新的LocalDateTime实例,而不是每次都返回其持有的单一实例.
Spring容器提出了一种叫做方法注入(Method Injection)的方式,可以帮助我们解决上述问题。我们所要做的很简单,改造一下MyDay类:
@Component
public class MyDay {
private LocalDateTime nextDay;
@Lookup
public LocalDateTime getNextDay()
{
return nextDay;
}
}
重点在于getNextDay方法和Lookup注解的使用,下面对着两个进行介绍:
当我们需要在一个单例bean中引入另外一个bean,但是希望这个bean是非单例的时候可以使用lookup注解来实现. 该注解作用是通知容器,当该方法被调用的时候,每次返回指定类型的对象实例即可。
对于@Lookup修饰的方法,有相对应的标准
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
public|protected 要求方法必须是可以被子类重写和调用的
abstract 可选,如果是抽象方法,CGLIB的动态代理类就会实现这个方法,如果不是抽象方法,就会覆盖这个方法
return-type 是非单例的类型
no-arguments 不允许有参数
也就是说,该方法必须能够被子类实现或者覆写,因为容器会为我们要进行方法注入的对象使用Cglib动态生成一个子类实现,从而替代当前对象。
除了使用方法注入来达到“每次调用都让容器返回新的对象实例”的目的,还可以使用其他方式 达到相同的目的。
我们知道,即使没有方法注入,只要在实现getNextDay()方法的时候,能够保证每次调用BeanFactory的getBean(“myFactory”),就同样可以每次都取得新的NextDay对象实例。现在,我们唯一需要的,就是让MyDay拥有一个BeanFactory的引用
Spring框架提供了一个BeanFactoryAware接口,容器在实例化实现了该接口的bean定义的过程中,会自动将容器本身注入该bean。这样,该bean就持有了它所处的BeanFactory的引用。BeanFactoryAware的定义如下代码所示:
public interface BeanFactoryAware extends Aware {
void setBeanFactory(BeanFactory var1) throws BeansException;
}
我们让MyDay实现这个接口:
@Component
public class MyDay implements BeanFactoryAware {
private BeanFactory beanFactory;
public LocalDateTime getNextDay()
{
return (LocalDateTime) beanFactory.getBean("myFactory");
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory=beanFactory;
}
}
ObjectFactoryCreatingFactoryBean是Spring提供的一个FactoryBean实现,它返回一个ObjectFactory实例。
从ObjectFactoryCreatingFactoryBean返回的这个ObjectFactory实例可以为我们返回容器管理的相关对象。实际上, ObjectFactoryCreatingFactoryBean 实现了BeanFactoryAware接口,它返回的ObjectFactory实例只是特定于与Spring容器进行交互的一个实现而已。使用它的好处就是,隔离了客户端对象对BeanFactory的直接引用
。
使用该ObjectFactoryCreatingFactoryBean时,只需要指定他需要从容器中获取的BeanName,那么调用它的getObject方法,他每次就会去找容器要对应的对象实例
@Component
public class MyDay {
@Autowired
private ObjectFactoryCreatingFactoryBean springFactoryBean;
public LocalDateTime getNextDay() throws Exception {
ObjectFactory<Object> object = springFactoryBean.getObject();
return (LocalDateTime) object.getObject();
}
}
当然不能直接注入该bean,因为spring容器中默认没有该bean,我们需要先往里面注入一个该bean实例对象
public class Main
{
@Bean
public ObjectFactoryCreatingFactoryBean objectFactory()
{
ObjectFactoryCreatingFactoryBean factoryBean = new ObjectFactoryCreatingFactoryBean();
//设置绑定的bean名字
factoryBean.setTargetBeanName("myFactory");
return factoryBean;
}
public static void main(String[] args) throws Exception {
ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);
MyDay myDay = (MyDay)applicationContext.getBean("myDay");
MyDay myDay1 = (MyDay)applicationContext.getBean("myDay");
System.out.println(myDay1==myDay);
System.out.println(myDay1.getNextDay()==myDay.getNextDay());
}
}
提示
也可以使用ServiceLocatorFactoryBean
来代替ObjectFactoryCreatingFactoryBean
,该FactoryBean
可以让我们自定义工厂接口
,而不用非要使用Spring的ObjectFactory。可以参照该类定义的Javadoc取得更多信息,Javadoc中有详细的实例,足够让你了解该类的使用和功能。
Spring的IoC容器所起的作用,就像上图所展示的那样,它会以某种方式加载Configuration Metadata(通常也就是XML格式的配置信息),然后根据这些信息绑定整个系统的对象,最终组装成一个可用的基于轻量级容器的应用系统。
Spring的IoC容器实现以上功能的过程,基本上可以按照类似的流程划分为两个阶段,即容器启动阶段和Bean实例化阶段,如下图所示:
Spring的IoC容器在实现的时候,充分运用了这两个实现阶段的不同特点,在每个阶段都加入了相应的容器扩展点,以便我们可以根据具体场景的需要加入自定义的扩展逻辑。
容器启动伊始,首先会通过某种途径加载Configuration MetaData。除了代码方式比较直接,在大部分情况下,容器需要依赖某些工具类(BeanDefinitionReader)对加载的Configuration MetaData进行解析和分析,并将分析后的信息编组为相应的BeanDefinition,最后把这些保存了bean定义必要信息的BeanDefinition,注册到相应的BeanDefinitionRegistry,这样容器启动工作就完成了。下图演示了这个阶段的主要工作。
总地来说,该阶段所做的工作可以认为是准备性的,重点更加侧重于对象管理信息的收集。当然,一些验证性或者辅助性的工作也可以在这个阶段完成。
经过第一阶段,现在所有的bean定义信息都通过BeanDefinition的方式注册到了BeanDefinitionRegistry中。当某个请求方通过容器的getBean方法明确地请求某个对象,或者因依赖关系容器需要隐式地调用getBean方法时,就会触发第二阶段的活动。
该阶段,容器会首先检查所请求的对象之前是否已经初始化。如果没有,则会根据注册的BeanDefinition所提供的信息实例化被请求对象,并为其注入依赖。
如果该对象实现了某些回调接口,也会根据回调接口的要求来装配它。
当该对象装配完毕之后,容器会立即将其返回请求方使用。
如果说第一阶段只是根据图纸装配生产线的话,那么第二阶段就是使用装配好的生产线来生产具体的产品了。
Spring
提供了一种叫做BeanFactoryPostProcessor
的容器扩展机制。
该机制允许我们在容器实例化相应对象之前,对注册到容器的BeanDefinition
所保存的信息做相应的修改。
这就相当于在容器实现的第一阶段最后加入一道工序,让我们对最终的BeanDefinition
做一些额外的操作,比如修改其中bean
定义的某些属性,为bean
定义增加其他信息等。
如果要自定义实现BeanFactoryPostProcessor
,通常我们需要实现org.springframework. beans.factory.config.BeanFactoryPostProcessor
接口。
同时,因为一个容器可能拥有多个BeanFactoryPostProcessor
,这个时候可能需要实现类同时实现Spring
的org.springframework.core.
Ordered
接口,以保证各个BeanFactoryPostProcessor
可以按照预先设定的顺序执行(如果顺序紧要的话)。
但是,因为Spring已经提供了几个现成的BeanFactoryPostProcessor
实现类,所以,大多时候,我们很少自己去实现某个BeanFactoryPostProcessor
。
其中,org.springframework.beans. factory.config.PropertyPlaceholderConfigurer和org.springframework.beans.factory. config.Property OverrideConfigurer
是两个比较常用的BeanFactoryPostProcessor。
另外,为了处理配置文件中的数据类型与真正的业务对象所定义的数据类型转换,Spring
还允许我们通过
org.springframework.beans.factory.config.CustomEditorConfigurer
来注册自定义的PropertyEditor
以补助容器中默认的PropertyEditor
。
可以参考BeanFactoryPostProcessor的Javadoc来了解更多其实现子类的情况
我们可以通过两种方式来应用 BeanFactoryPostProcessor,分别针对基本的 IoC 容 器BeanFactory和较为先进的容器ApplicationContext。
对于BeanFactory来说,我们需要用手动方式应用所有的BeanFactoryPostProcessor
手动装配BeanFactory使用的BeanFactoryPostProcessor
// 声明将被后处理的BeanFactory实例
ConfigurableListableBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("..."));
// 声明要使用的BeanFactoryPostProcessor
PropertyPlaceholderConfigurer propertyPostProcessor = new PropertyPlaceholderConfigurer();
propertyPostProcessor.setLocation(new ClassPathResource("..."));
// 执行后处理操作
propertyPostProcessor.postProcessBeanFactory(beanFactory);
如果拥有多个BeanFactoryPostProcessor,我们可以添加更多类似的代码来应用所有的这些BeanFactoryPostProcessor。
对于ApplicationContext来说,情况看起来要好得多。因为ApplicationContext会自动识别配置文件中的BeanFactoryPostProcessor并应用它,所以,相对于BeanFactory,在ApplicationContext中加载并应用BeanFactoryPostProcessor,仅需要在XML配置文件中将这些BeanFactoryPostProcessor简单配置一下即可。
将相应BeanFactoryPostProcessor实现类添加到配置文件,ApplicationContext将自动识别并应用它。
通过ApplicationContext使用BeanFactoryPostProcessor
...
<beans>
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>conf/jdbc.properties</value>
<value>conf/mail.properties</value>
</list> 10
</property>
</bean>
...
</beans>
下面让我们看一下Spring提供的这几个BeanFactoryPostProcessor实现都可以完成什么功能。
通常情况下,我们不想将类似于系统管理相关的信息同业务对象相关的配置信息混杂到XML配置文件中,以免部署或者维护期间因为改动繁杂的XML配置文件而出现问题。
我们会将一些数据库连接信息、邮件服务器等相关信息单独配置到一个properties文件中,这样,如果因系统资源变动的话,只 需要关注这些简单properties配置文件即可。
嘿嘿,上面这种思想是不是有springboot那味了,这就对了,学习要学会类比和思考!
PropertyPlaceholderConfigurer
允许我们在XML配置文件中使用占位符(PlaceHolder),并将这些占位符所代表的资源单独配置到简单的properties文件中来加载。
以数据源的配置为例,使用了PropertyPlaceholderConfigurer之后,可以在XML配置文件配置数据源,而不用将连接地址、用户名和密码等都配置到XML中。
使用了占位符的数据源配置
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" ➥
destroy-method="close">
<property name="url">
<value>${jdbc.url}</value>
</property>
<property name="driverClassName">
<value>${jdbc.driver}</value>
</property>
<property name="username">
<value>${jdbc.username}</value>
</property>
<property name="password">
<value>${jdbc.password}</value>
</property>
<property name="testOnBorrow">
<value>true</value>
</property>
<property name="testOnReturn">
<value>true</value>
</property>
<property name="testWhileIdle">
<value>true</value>
</property>
<property name="minEvictableIdleTimeMillis">
<value>180000</value>
</property>
<property name="timeBetweenEvictionRunsMillis">
<value>360000</value>
</property>
<property name="validationQuery">
<value>SELECT 1</value>
</property>
<property name="maxActive">
<value>100</value>
</property>
</bean>
如果你使用过Ant或者Velocity等工具,就会发现${property}之类的表达很熟悉。现在,所有这些占位符所代表的资源,都放到了jdbc.properties文件中,如下所示:
jdbc.url=jdbc:mysql://server/MAIN?useUnicode=true&characterEncoding=ms932& ➥
failOverReadOnly=false&roundRobinLoadBalance=true
jdbc.driver=com.mysql.jdbc.Driver
jdbc.username=your username
jdbc.password=your password
基本每次连接数据库,都需要注入一个数据源,如果spring发现我们引入了数据库相关依赖后,能够自动注入一个数据源,向上面这样,通过占位符或者其他方式表名该出资源需要从某个properties或者yaml配置文件中读取,这就会方便很多,而这也就是springboot要做的事情
基本机制就是之前所说的那样。当BeanFactory在第一阶段加载完成所有配置信息时,BeanFactory中保存的对象的属性信息还只是以占位符的形式存在,如 { jdbc.url }、{jdbc.driver}。当PropertyPlaceholderConfigurer作为BeanFactoryPostProcessor被应用时,它会使用properties配置文件中的配置信息来替换相应BeanDefinition中占位符所表示的属性值。
这样,当进入容器实现的第二阶段实例化bean时,bean定义中的属性值就是最终替换完成的了。
PropertyPlaceholderConfigurer
不单会从其配置的properties
文件中加载配置项,同时还会检查Java的System类中的Properties,可以通过setSystemPropertiesMode()
或者setSystemPropertiesModeName()
来控制是否加载或者覆盖System相应Properties的行为。
PropertyPlaceholderConfigurer
提供了SYSTEM_PROPERTIES_MODE_FALLBACK、SYSTEM_PROPERTIES_MODE_NEVER和SYSTEM_ PROPERTIES_MODE_OVERRIDE
三种模式。
默认采用的是SYSTEM_PROPERTIES_ MODE_FALLBACK
,即如果properties
文件中找不到相应配置项,则到System
的Properties
中查找,我们还可以选择不检查System
的Properties
或者覆盖它。更多信息请参照PropertyPlaceholderConfigurer
的Javadoc
文档。
PropertyPlaceholderConfigurer可以通过占位符,来明确表明bean定义中的property与 properties文件中的各配置项之间的对应关系。
如果说PropertyPlaceholderConfigurer做的这些是“明事”的话,那相对来说,PropertyOverrideConfigurer所做的可能就有点儿“神不知鬼不觉”了。
可以通过PropertyOverrideConfigurer对容器中配置的任何你想处理的bean定义的property信息进行覆盖替换。
这听起来比较抽象,我们还是给个例子吧!比如之前的dataSource定义中,maxActive的值为100,如果我们觉得100不合适,那么可以通过PropertyOverrideConfigurer在其相应的properties文件中做如下所示配置,把100这个值给覆盖掉,如将其配置为200:
dataSource.maxActive=200
这样,当容器实例化对象的时候,该dataSource对象对应的maxActive值就是200,而不是原来XML配置中的100。
也就是说,PropertyOverrideConfigurer的properties文件中的配置项,覆盖掉了原来XML中的bean定义的property信息。
但这样的活动,只看XML配置的话,你根本看不出哪个bean定义的哪个property会被覆盖替换掉,只有查看PropertyOverrideConfigurer指定的properties配置文件才会了解。
基本上,这种覆盖替换对于bean定义来说是透明的。
如果要对容器中的某些bean定义的property信息进行覆盖,我们需要按照如下规则提供一个PropertyOverrideConfigurer使用的配置文件:
beanName.propertyName=value
也就是说,properties文件中的键是以XML中配置的bean定义的beanName为标志开始的(通常就是id指定的值),后面跟着相应被覆盖的property的名称,比如上面的maxActive。
下面是针对dataSource定义给出的PropertyOverrideConfigurer的propeties文件配置信息:
# pool-adjustment.properties 11
dataSource.minEvictableIdleTimeMillis=1000
dataSource.maxActive=50
这样,当按照如下代码,将PropertyOverrideConfigurer加载到容器之后,dataSource原来定义的默认值就会被pool-adjustment.properties文件中的信息所覆盖:
<bean class="org.springframework.beans.factory.config.PropertyOverrideConfigurer">
<property name="location" value="pool-adjustment.properties"/>
</bean>
pool-adjustment.properties中没有提供的配置项将继续使用原来XML配置中的默认值。
当容器中配置的多个PropertyOverrideConfigurer对同一个bean定义的同一个property值进行处理的时候,最后一个将会生效。
看到这里,是否可以让大家联想到springboot中的配置文件优先级机制呢? 动动小脑袋瓜,想想springboot是如何实现这个机制的
配置在properties文件中的信息通常都以明文表示,PropertyOverrideConfigurer
的父类PropertyResourceConfigurer
提供了一个protected类型的方法convertPropertyValue
,允许子类覆盖这个方法对相应的配置项进行转换,如对加密后的字符串解密之后再覆盖到相应的bean定义中。
当然,既然PropertyPlaceholderConfigurer
也同样继承了PropertyResourceConfigurer
,我们也可以针对PropertyPlaceholderConfigurer
应用类似的功能。
springboot的yaml文件中可以针对某个配置项进行加密,然后我们最终程序获取到的是解密后的内容,是否可以通过这种机制实现呢?
其他两个BeanFactoryPostProcessor都是通过对BeanDefinition中的数据进行变更以达到某种目的。与它们有所不同,CustomEditorConfigurer是另一种类型的BeanFactoryPostProcessor实现,它只是辅助性地将后期会用到的信息注册到容器,对BeanDefinition没有做任何变动。
我们知道,不管对象是什么类型,也不管这些对象所声明的依赖对象是什么类型,通常都是通过XML(或者properties甚至其他媒介)文件格式来配置这些对象类型。
但XML所记载的,都是String类型,即容器从XML格式的文件中读取的都是字符串形式,最终应用程序却是由各种类型的对象所构成。
要想完成这种由字符串到具体对象的转换(不管这个转换工作最终由谁来做),都需要这种转换规则相关的信息,而CustomEditorConfigurer就是帮助我们传达类似信息的。
Spring内部通过JavaBean的PropertyEditor来帮助进行String类型到其他类型的转换工作。
只要为每种对象类型提供一个 PropertyEditor ,就可以根据该对象类型取得与其相对应的PropertyEditor来做具体的类型转换。
Spring容器内部在做具体的类型转换的时候,会采用JavaBean框架内默认的PropertyEditor搜寻逻辑,从而继承了对原生类型以及java.lang.String.java.awt. Color和java.awt.Font等类型的转换支持。
同时,Spring框架还提供了自身实现的一些PropertyEditor,这些PropertyEditor大部分都位于org.springframework.beans.propertyeditors包下。以下是这些Spring提供的部分PropertyEditor的简要说明。
以上这些PropertyEditor,容器通常会默认加载使用,所以,即使我们不告诉容器应该如何对这些类型进行转换,容器同样可以正确地完成工作。
但当我们需要指定的类型没有包含在以上所提到的PropertyEditor之列的时候,就需要给出针对这种类型的PropertyEditor实现,并通过CustomEditorConfigurer告知容器,以便容器在适当的时机使用到适当的PropertyEditor。
通常情况下,对于Date类型,不同的Locale、不同的系统在表现形式上存在不同的需求。
如系统这个部分需要以yyyy-MM-dd的形式表现日期,系统那个部分可能又需要以yyyyMMdd的形式对日期进行转换。虽然可以使用Spring提供的CustomDateEditor,不过为了能够演示自定义PropertyEditor的详细流程,在此我们有必要“重新发明轮子”!
下面是对自定义PropertyEditor实现的简单介绍。
给出针对特定对象类型的PropertyEditor实现
假设需要对yyyy/MM/dd形式的日期格式转换提供支持。虽然可以直接让PropertyEditor实现类去实现java.beans.PropertyEditor接口,不过,通常情况下,我们可以直接继承java.beans.PropertyEditorSupport类以避免实现java.beans.PropertyEditor接口的所有方法。
就好像这次,我们仅仅让 DatePropertyEditor 完成从 String 到 java.util.Date 的转换,只需要实现setAsText(String)方法,而其他方法一概不管。
该自定义PropertyEditor类定义如下:
public class DatePropertyEditor extends PropertyEditorSupport{
private String dataPattern;
@Override
public void setAsText(String text) throws IllegalArgumentException {
DateTimeFormatter dateTimeFormatter=DateTimeFormatter.ofPattern(dataPattern);
LocalDateTime dateTime = LocalDateTime.parse(text, dateTimeFormatter);
setValue(dateTime);
}
public String getDataPattern() {
return dataPattern;
}
public void setDataPattern(String dataPattern) {
this.dataPattern = dataPattern;
}
}
如果仅仅是支持单向的从String到相应对象类型的转换,只要覆写方法setAsText(String)即可。
如果想支持双向转换,需要同时考虑getAsText()方法的覆写。
通过CustomEditorConfigurer注册自定义的PropertyEditor
如果有类似于DateFoo这样的类对LocalDateTime类型的依赖声明:
public class DateFoo {
private LocalDateTime date;
public LocalDateTime getDate() {
return date;
}
public void setDate(LocalDateTime date) {
this.date = date;
}
}
配置类似于下面这种:
<bean id="dateFoo" class="....DateFoo">
<property name="date">
<value>2022/3/19</value>
</property>
</bean>
但是,默认情况下,Spring容器找不到合适的PropertyEditor将字符串“2007/10/16”转换成对象所声明的LocalDateTime类型。所以,我们通过CustomEditorConfigurer将刚实现的DatePropertyEditor注册到容器。以告知容器按照DatePropertyEditor的形式进行String到LocalDateTime的转化。
如果使用的容器是BeanFactory的实现,比如xmlBeanFactory,就需要通过编码手动应用CustomEditorConfigure到容器,类似如下形式:
XmlBeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("..."));
//
CustomEditorConfigurer ceConfigurer = new CustomEditorConfigurer();
Map customerEditors = new HashMap();
customerEditors.put(java.time.LocalDateTime.class, new DatePropertyEditor());
ceConfigurer.setCustomEditors(customerEditors);
//
ceConfigurer.postProcessBeanFactory(beanFactory);
但如果使用的是ApplicationContext相应实现,因为ApplicationContext会自动识别BeanFactoryPostProcessor并应用,所以只需要在相应配置文件中配置一下
使用CustomEditorConfigurer注册自定义DatePropertyEditor到容器
<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
<property name="customEditors">
<map>
<entry key="java.util.Date">
<ref bean="datePropertyEditor"/>
</entry>
</map>
</property>
</bean>
<bean id="datePropertyEditor" class="...DatePropertyEditor">
<property name="datePattern">
<value>yyyy/MM/dd</value>
</property>
</bean>
Spring 2.0之前通常是通过CustomEditorConfigurer的customEditors属性来指定自定义的PropertyEditor。
2.0之后,比较提倡使用propertyEditorRegistrars属性来指定自定义的PropertyEditor。
不过,这样我们就需要再多做一步工作,就是给出一个PropertyEditorRegistrar的实现.
DatePropertyEditorRegistrar定义
public class DatePropertyEditorRegistrar implements PropertyEditorRegistrar {
private PropertyEditor propertyEditor;
@Override
public void registerCustomEditors(PropertyEditorRegistry propertyEditorRegistry) {
propertyEditorRegistry.registerCustomEditor(java.time.LocalDateTime.class,getPropertyEditor());
}
public PropertyEditor getPropertyEditor() {
return propertyEditor;
}
public void setPropertyEditor(PropertyEditor propertyEditor) {
this.propertyEditor = propertyEditor;
}
}
这样,2.0之后所提倡的注册自定义PropertyEditor的方式,如下所示:
通过CustomEditorConfigurer的propertyEditorRegistrars注册自定义PropertyEditor
要是还有其他扩展类型的PropertyEditor,可以在propertyEditorRegistrars的< List >中进行指定
本文篇幅已有3万多字,因此下面的内容将放到下一节进行讲解