Spring的容器具可以具有父子关系。具体含义就是:如果两个容器具有父子关系,那么在查找bean时,会先从子容器中查找,如果没有就查找父容器。但是父容器不能查找子容器的bean。
这个特性有什么意义?
比如,现在有一个应用,确实需要隔离不同的bean在不同的容器中,所以需要创建多个spring容器。但是,这些容器的bean可能都需要一些基础的bean,比方说数据访问层的bean,那总不能每一个容器都加载一遍吧,所以,spring推荐的做法就是把基础的需要共享的bean放在父容器中,这样所有的子容器就能访问这些bean了。
但是实际当中,并没有怎么见到这样的场景。
最最常见的应该就是springmvc中的父子容器了吧。
具体的在这里探讨过:javascript:void(0)
这里就不再深入springmvc的父子容器了,而是重点关注下spring框架对父子容器的支持,以及给出一些使用的例子。
spring的容器有两大类,基础容器BeanFactory以及高级容器 ApplicationContext,里面都有父子容器的支持。先看BeanFactory相关的。
基础容器BeanFactory
BeanFactory的第一级继承接口HierarchicalBeanFactory,首先定义了BeanFactory是可以有父容器的:
public interface HierarchicalBeanFactory extends BeanFactory {
/**
* Return the parent bean factory, or {@code null} if there is none.
*/
BeanFactory getParentBeanFactory();
/**
* Return whether the local bean factory contains a bean of the given name,
* ignoring beans defined in ancestor contexts.
* <p>This is an alternative to {@code containsBean}, ignoring a bean
* of the given name from an ancestor bean factory.
* @param name the name of the bean to query
* @return whether a bean with the given name is defined in the local factory
* @see BeanFactory#containsBean
*/
boolean containsLocalBean(String name);
}
不过,这一层接口没有提供设置父容器的方法。
设置方法是在更下一级接口:
public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, SingletonBeanRegistry {
/**
* Set the parent of this bean factory.
* <p>Note that the parent cannot be changed: It should only be set outside
* a constructor if it isn't available at the time of factory instantiation.
* @param parentBeanFactory the parent BeanFactory
* @throws IllegalStateException if this factory is already associated with
* a parent BeanFactory
* @see #getParentBeanFactory()
*/
void setParentBeanFactory(BeanFactory parentBeanFactory) throws IllegalStateException;
}
ConfigurableBeanFactory这个接口当然不只有这一个方法,该接口提供了若干可以对BeanFactory进行配置的方法,例如常见的addPostProcessor方法。
一般的Spring基础容器都会实现这些接口。
至此,spring父子容器的行为已经被定义在接口中。
下面看下,在子容器中对bean的检索。
实现在AbstractBeanFactory中的doGetBean方法:
@SuppressWarnings("unchecked")
protected <T> T doGetBean(
final String name, final Class<T> requiredType, final Object[] args, boolean typeCheckOnly)
throws BeansException {
BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// Not found -> check parent.
String nameToLookup = originalBeanName(name);
if (args != null) {
// Delegation to parent with explicit args.
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else {
// No args -> delegate to standard getBean method.
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
}
// 省略
}
该方法,会判断,如果当前容器不包含该bean的定义,同时父容器不为空,那么就递归调用父容器的getBean方法检索bean。
逻辑简单清晰。
下面看一个例子:
public static void main(String args[]) {
DefaultListableBeanFactory factory1 = new DefaultListableBeanFactory();
Resource resource1 = new ClassPathResource("base.xml");
BeanDefinitionReader reader1 = new XmlBeanDefinitionReader(factory1);
reader1.loadBeanDefinitions(resource1);
DefaultListableBeanFactory factory2 = new DefaultListableBeanFactory();
factory2.setParentBeanFactory(factory1);
System.out.println(factory2.getBean("cd1"));
}
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id = "cd1" class="spring.xml.config.CD3"></bean>
</beans>
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id = "cd2" class="spring.xml.config.CD3"></bean>
</beans>
这里定义了两个基础容器,factory1和factory2,其中1是2的父容器,cd1这个bean只在父容器中定义,正常来说,factory2里面没有cd1这个bean,直接get会报错,但是由于设置了父容器,所以仍然可以get到这个bean。输出:
spring.xml.config.CD3@3498ed
Process finished with exit code 0
这是一个简单的基础父子容器的例子。下面看下ApplicationContext高级中对父子容器的支持。
高级容器ApplicationContext
首先明确下高级容器和低级容器的关系。高级容器可以当做低级容器使用,是因为继承了低级容器的接口:
public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory,
MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
}
高级容器实现时,会在内部持有一个低级容器,所以,通过调用一个高级容器的实例的低级容器接口还是会代理到内部的低级容器,比方说getBean方法,具体在:
AbstractApplicationContext的getBean方法:
@Override
public Object getBean(String name) throws BeansException {
assertBeanFactoryActive();
return getBeanFactory().getBean(name);
}
在ApplicationContext接口里,定义了getParent方法:
/**
* Return the parent context, or {@code null} if there is no parent
* and this is the root of the context hierarchy.
* @return the parent context, or {@code null} if there is no parent
*/
ApplicationContext getParent();
这与低级容器的getParentFactory不同,这里返回的是一个ApplicationContext。
在下一级的继承接口ConfigurationApplicationContext里定义了set方法:
/**
* Set the parent of this application context.
* <p>Note that the parent shouldn't be changed: It should only be set outside
* a constructor if it isn't available when an object of this class is created,
* for example in case of WebApplicationContext setup.
* @param parent the parent context
* @see org.springframework.web.context.ConfigurableWebApplicationContext
*/
void setParent(ApplicationContext parent);
所以,对于高级容器,也可以通过上面的方法设置读取父子容器。
下面看一个例子:
public static void main(String args[]) {
ApplicationContext c1 = new ClassPathXmlApplicationContext("base.xml");
ApplicationContext c2 = new ClassPathXmlApplicationContext("sub.xml");
((ClassPathXmlApplicationContext) c2).setParent(c1);
System.out.println(c2.getBean("cd1"));
}
运行结果:
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'cd1' is defined
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:638)
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1159)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:282)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:195)
at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:973)
at spring.xml.config.Main2.main(Main2.java:17)
什么情况???居然报错了???
按照低级容器的方式使用父子容器,居然行不通。
但是如果这么改一下就可以了:
public static void main(String args[]) {
ApplicationContext c1 = new ClassPathXmlApplicationContext("base.xml");
ApplicationContext c2 = new ClassPathXmlApplicationContext("sub.xml");
((ClassPathXmlApplicationContext) c2).setParent(c1);
((ClassPathXmlApplicationContext) c2).refresh();
System.out.println(c2.getBean("cd1"));
}
只是在setParent之后,手动调用了一次refresh方法。
结果:
spring.xml.config.CD3@589838eb
Process finished with exit code 0
这次就没问题了。
也就是说,setParent必须在fresh之前调用,否则无法访问到父容器中的bean。
为什么???
从前面看到,doGetBean时,会调用低级容器的getParentBeanFactory方法来查找父容器。前面访问失败,必然是因为没有设置内部低级容器的父容器。注意,这里有两个父子容器的关系,一个是高级容器之间的,一个是低级容器之间的。
doGetBean要求低级容器之间具备父子容器关系。
但是,加上fresh之后就好了,说明refresh方法内部一定设置了低级容器的父容器:
protected DefaultListableBeanFactory createBeanFactory() {
return new DefaultListableBeanFactory(getInternalParentBeanFactory());
}
protected BeanFactory getInternalParentBeanFactory() {
return (getParent() instanceof ConfigurableApplicationContext) ?
((ConfigurableApplicationContext) getParent()).getBeanFactory() : getParent();
}
public AbstractAutowireCapableBeanFactory(BeanFactory parentBeanFactory) {
this();
setParentBeanFactory(parentBeanFactory);
}
在refresh里有这几个方法的调用,可以看到,是通过高级容器的getParent方法拿到高级容器的父容器,再调用其getBeanFactory拿到里面的低级容器,设置低级容器的父容器。
比较绕,总之,要想利用父子容器关系,必须设置低级容器的父子关系,否则访问不到父容器的bean。
而这一步是在refresh方法中设置的,当然前提是先设置高级容器的父子关系。
那么其实,上面的例子,还可以这么改:
public static void main(String args[]) {
ApplicationContext c1 = new ClassPathXmlApplicationContext("base.xml");
ApplicationContext c2 = new ClassPathXmlApplicationContext("sub.xml");
// ((ClassPathXmlApplicationContext) c2).setParent(c1);
// ((ClassPathXmlApplicationContext) c2).refresh();
((ClassPathXmlApplicationContext) c2).getBeanFactory().setParentBeanFactory(c1);
System.out.println(c2.getBean("cd1"));
}
直接拿到里面的低级容器,然后设置父容器。这样绕过了高级容器的父子关系。
其实高级容器在构造器上提供了直接设置高级容器父容器的方法,所以这样也能设置成功,因为构造器里也会调用fresh。所以,也可以这么改:
public static void main(String args[]) {
ApplicationContext c1 = new ClassPathXmlApplicationContext("base.xml");
ApplicationContext c2 = new ClassPathXmlApplicationContext(new String[]{"sub.xml"}, c1);
// ((ClassPathXmlApplicationContext) c2).setParent(c1);
// ((ClassPathXmlApplicationContext) c2).refresh();
// ((ClassPathXmlApplicationContext) c2).getBeanFactory().setParentBeanFactory(c1);
System.out.println(c2.getBean("cd1"));
}
这样也是可行的。
好了,至此介绍了spring中父子容器在低级容器和高级容器中的使用以及原理。