4599 2017-12-05 2020-06-25
前言:本篇分析Spring中对于XML中自定义标签的解析。
一、概述
在之前的章节中,我们提到了Spring中存在默认标签与自定义标签两种,而在上一章节中我们分析了Spring中对默认标签的解析过程,相信大家一定已经有所感悟。那么现在将开始新的历程,分析Spring中自定义标签的加载过程。
同样,我们还是先再次回顾一下,当完成从配置文件到Document的转换并提取对应的root后,就开始了所有元素的解析,而在这一过程中便开始了默认标签与自定义标签两种格式的区分,代码如下
/**
* Parse the elements at the root level in the document:
* "import", "alias", "bean".
* @param root the DOM root element of the document
*/
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
if (delegate.isDefaultNamespace(root)) {
NodeList nl = root.getChildNodes();
for (int i = 0; i < nl.getLength(); i++) {
Node node = nl.item(i);
if (node instanceof Element) {
Element ele = (Element) node;
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}
else {
// 这次走这里
delegate.parseCustomElement(ele);
}
}
}
}
else {
// 这次走这里
delegate.parseCustomElement(root);
}
}
在本篇中,所有的功能都是围绕其中的一行代码delegate.parseCustomElement(ele)
开展的。从上面的方法我们可以看出,当Spring拿到一个元素时,首先要做的是根据命名空间进行解析,如果是默认的命名空间,则使用parseDefaultElement(ele, delegate)方法进行解析,否则使用delegate.parseCustomElement(ele)方法。在分析自定义标签的解析过程前,我们先了解一下自定义标签的使用过程。
二、自定义标签使用
在很多情况下,我们需要为系统提供可配置化支持,简单的做法可以直接基于Spring标准的bean来配置,但配置较为复杂或者需要更多丰富控制的时候,会显得非常笨拙。一般的做法会用原生态的方式去解析定义好的XML文件,然后转化为配置对象。这种方式当然可以解决所有问题,但实现起来比较繁琐,特别是在配置非常复杂的时候,解析工作是一个不得不考虑的负担。Spring提供了可扩展的Schema的支持,这是一个不错的折中方案,扩展Spring自定义标签配置大致需要一下几个步骤(前提是把Spring的Core包加入项目中)。
- 创建一个需要扩展的组件。
- 定义一个XSD文件描述文件内容。
- 创建一个文件,实现BeanDefinitionParser接口,用来解析XSD文件中的定义和组件定义。
- 创建一个Handler文件,扩展自NamespaceHandlerSupport,目的是将组件注册到Spring容器。
- 编写spring.handlers和spring.schemas文件。
1、准备
1)首先我们创建一个普通的POJO,这个POJO并没有任何特别之处,只是用来接收配置文件。
public class User {
private String username;
private String email;
// 省略get/set方法
}
2)定义一个XSD文件描述组件内容。
<?xml version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.xiaokui.site/schema/user"
elementFormDefault="qualified">
<element name="user">
<complexType>
<attribute name="id" type="string"/>
<attribute name="username" type="string"/>
<attribute name="email" type="string"/>
</complexType>
</element>
</schema>
3)创建一个文件,实现BeanDefinitionParser接口,用来解析XSD文件中的定义和组件定义。
public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
@Override
protected Class getBeanClass(Element element) {
return User.class;
}
@Override
protected void doParse(Element element, BeanDefinitionBuilder bean) {
// 里面可以有非常复杂的逻辑,这里只是为了说明原理
String username = element.getAttribute("username");
String email = element.getAttribute("email");
if (StringUtils.hasText(username)) {
bean.addPropertyValue("username", username);
}
if (StringUtils.hasText(email)) {
bean.addPropertyValue("email", email);
}
}
}
4)创建一个Handler文件,扩展自NamespaceHandlerSupport,目的是将组件注册到Spring容器。
public class MyNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
}
}
5)编写spring.handlers和spring.schemas文件,默认位置是在工程的/META-INF文件夹,当然,你可以通过Spring的扩展或者修改源代码的方式改变路径。
# spring.handlers文件
http\://www.xiaokui.site/schema/user=xiaokui.test.TestCustomTag.MyNamespaceHandler
# spring.schemas文件
http\://www.xiaokui.site/schema/user.xsd=META-INF/user.xsd
到这里,自定义的配置就结束了,而Spring的加载自定义的大致流程是遇到自定义标签然后就去spring.handlers文件和spring.schemas文件中去找对应的handler和XSD,默认位置是/META-INF下,进而有找到对应的handler以及解析元素的Parser,从而完成了整个自定义元素的解析,也就是说自定义与Spring中默认的标准配置不同在于Spring将自定义标签的解析的工作委托给了用户去实现。
2、测试
1)创建测试配置文件,在配置文件中引入对应的命名空间以及XSD后,便可以直接使用自定义标签了。
<?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:myname="http://www.xiaokui.site/schema/user"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.xiaokui.site/schema/user http://www.xiaokui.site/schema/user.xsd">
<myname:user id="testBean" username="aaa" email="bbb"/>
</beans>
2)测试
public static void main(String[] args) throws IOException {
BeanFactory beanFactory = new XmlBeanFactory(new ClassPathResource("META-INF/xsd.xml"));
User user = (User) beanFactory.getBean("testBean");
System.out.println(user.username +" and " + user.email);
}
不出意外的话,你应该看到了aaa and bbb。在上面的例子中,我们实现了通过自定义标签为bean赋值,在Spring中自定义标签非常常用,例如我们熟知的事务标签:
# 如下面例子
<tx:annotation-driven>
<!-- 启动注解驱动的Spring MVC功能-->
<mvc:annotation-driven/>
<!--扫描所有包内的注解-->
<context:component-scan base-package="cn.ccsu.*"/>
二、自定义标签的解析
了解了自定义标签的使用后,我们带着强烈的好奇心来探究一下自定义标签的解析过程。
public BeanDefinition parseCustomElement(Element ele) {
return parseCustomElement(ele, null);
}
// containingBd为父类bean,对顶层元素的解析应设置为null
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
// 获取对应的命名空间
String namespaceUri = getNamespaceURI(ele);
// 根据命名空间找到对应的NamespaceHandler
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
// 调用自定义的NamespaceHandler进行解析
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
相信了解了自定义标签的使用方法后,或多或少会对自定义标签的实现过程有一个自己的想法。其实思路非常简单,无非是根据对应的bean获取对应的命名空间,根据命名空间解析对应的处理器,然后根据用户自定义的处理器进行解析。可是有些事情说起来简单做起来难,我们先看看如何获取命名空间吧。
1、获取标签的命名空间
标签的解析是从命名空间的提起开始的,无论是区分Spring中默认标签和自定义标签还是区分自定义标签中不同标签的处理器都是以标签所提供的命名空间为基础的,而至于如何提取对应元素的命名空间其实并不需要我们亲自去实现,在org.w3c.dom.Node中已经提供了方法供我们直接调用,如下
/**
* Get the namespace URI for the supplied node. The default implementation uses {@link Node#getNamespaceURI}.
* Subclasses may override the default implementation to provide a different namespace identification mechanism.
* @param node the node
*/
public String getNamespaceURI(Node node) {
return node.getNamespaceURI();
}
2、提取自定义标签处理器
有了命名空间,就可以进行NamespaceHandler的提取了,继续之前的parseCustomElement方法的跟踪,分析NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)
,在readerContext初始化的时候其属性namespaceHandlerResolver已经被初始化为了DefaultNamespaceHandlerResolver的实例,所以,这里调用的resolve方法其实调用的是DefaultNamespaceHandlerResolver类中的方法。其代码如下
/**
* Locate the {@link NamespaceHandler} for the supplied namespace URI
* from the configured mappings.
* @param namespaceUri the relevant namespace URI
* @return the located {@link NamespaceHandler}, or {@code null} if none found
*/
public NamespaceHandler resolve(String namespaceUri) {
// 获取所有已经配置的handler映射
Map<String, Object> handlerMappings = getHandlerMappings();
// 根据命名空间找到对应的信息
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
else if (handlerOrClassName instanceof NamespaceHandler) {
// 已经做过解析的情况,直接从缓存读取
return (NamespaceHandler) handlerOrClassName;
}
else {
// 没有做过解析,则返回的是类路径
String className = (String) handlerOrClassName;
try {
// 使用反射将类路径转化为类
Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
"] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
}
// 初始化类
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 调用自定义的NamespaceHandler的初始化方法
namespaceHandler.init();
// 记录在缓存中
handlerMappings.put(namespaceUri, namespaceHandler);
return namespaceHandler;
}
catch (ClassNotFoundException ex) {
throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
namespaceUri + "] not found", ex);
}
catch (LinkageError err) {
throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
namespaceUri + "]: problem with handler class file or dependent class", err);
}
}
}
上面的方法清晰地阐述了解析自定义NamespaceHandler的过程,通过之前的实例程序我们了解到如果要使用自定义标签,那么其中一项必不可少的操作就是在spring.handlers文件中配置命名空间与命名空间处理器的映射关系。只有这样,Spring才能根据映射关系找到匹配的处理器,而寻找匹配的处理器就是在上面方法中实现的,当获取到自定义的NamespaceHandler之后就可以进行处理器初始化并解析了。我们不妨在此回忆一下示例中对于命名空间处理器的内容。
public class MyNamespaceHandler extends NamespaceHandlerSupport {
@Override
public void init() {
registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
}
}
当得到自定义命名空间处理后就会马上执行namespaceHandler.init()来进行自定义BeanDefinitionParser的注册。在这里,你可以注册多个标签处理器,当前示例中只有支持<myname:user>
的写法,你也可以在这里注册多个解析器,如<myname:A>
、<myname:B>
等,使得myname的命名空间中可以支持多种标签的解析。
注册后,命名空间处理器就可以根据标签的不同来调用不同的解析器进行解析。那么,根据上面的方法与之前介绍过的例子,我们基本上可以判断getHandlerMappings方法的主要功能就是读取spring.handlers配置文件并将配置文件缓存在map中。代码如下
/**
* Load the specified NamespaceHandler mappings lazily.
*/
private Map<String, Object> getHandlerMappings() {
// 如果没有缓存则开始进行缓存
if (this.handlerMappings == null) {
synchronized (this) {
if (this.handlerMappings == null) {
try {
// this.handlerMappingLocation在构造方法中已经被初始化为:META-INF/spring.handlers
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Loaded NamespaceHandler mappings: " + mappings);
}
// 将Properties格式文件合并到Map格式的handlerMappings中
Map<String, Object> handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
this.handlerMappings = handlerMappings;
}
catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return this.handlerMappings;
}
同我们想象一样,借助了工具类PropertiesLoaderUtils对属性handlerMappingsLocation进行了配置文件的读取,handlerMappingsLocation被默认初始化为“META-INF/spring.handlers”。
3、标签解析
得到了解析器以及要分析的元素后,Spring就可以将解析工作委托给自定义解析器去解析了。代码如下
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
String namespaceUri = getNamespaceURI(ele);
NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
if (handler == null) {
error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
return null;
}
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
以之前提到的示例进行分析,此时的handler已经被实例化成为了我们自定义的MyNamespaceHandler了,而MyNamespaceHandler也已经完成了初始化的工作,但是在我们实现的自定义命名空间处理器中并没有实现parse方法,所以推断,这个方法是父类中的实现,查看父类NamespaceHandlerSupport中的parse方法。代码如下
/**
* Parses the supplied {@link Element} by delegating to the {@link BeanDefinitionParser} that is
* registered for that {@link Element}.
*/
public BeanDefinition parse(Element element, ParserContext parserContext) {
// 寻找解析器并进行解析操作
return findParserForElement(element, parserContext).parse(element, parserContext);
}
解析过程中首先是寻找元素对应的解析器,进而调用解析器中的parse方法,那么结合示例来讲,其实就是首先获取在MyNameSpaceHandler类中的init方法中注册的对应的UserBeanDefinitionParser实例,并调用parse方法进行进一步解析。
/**
* Locates the {@link BeanDefinitionParser} from the register implementations using
* the local name of the supplied {@link Element}.
*/
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
// 获取元素名称,也就是<myname:user>中的user,若在示例中,此时localName为user
String localName = parserContext.getDelegate().getLocalName(element);
// 根据user找到对应的解析器,也就是在registerBeanDefinitionParser("user", new User..)中注册的解析器
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
而对于parse方法的处理
public final BeanDefinition parse(Element element, ParserContext parserContext) {
AbstractBeanDefinition definition = parseInternal(element, parserContext);
if (definition != null && !parserContext.isNested()) {
try {
String id = resolveId(element, definition, parserContext);
if (!StringUtils.hasText(id)) {
parserContext.getReaderContext().error(
"Id is required for element '" + parserContext.getDelegate().getLocalName(element)
+ "' when used as a top-level tag", element);
}
String[] aliases = new String[0];
String name = element.getAttribute(NAME_ATTRIBUTE);
if (StringUtils.hasLength(name)) {
aliases = StringUtils.trimArrayElements(StringUtils.commaDelimitedListToStringArray(name));
}
// 将AbstractBeanDefinition转换为BeanDefinitionHolder并注册
BeanDefinitionHolder holder = new BeanDefinitionHolder(definition, id, aliases);
registerBeanDefinition(holder, parserContext.getRegistry());
if (shouldFireEvents()) {
// 需要通知监听器则进行处理
BeanComponentDefinition componentDefinition = new BeanComponentDefinition(holder);
postProcessComponentDefinition(componentDefinition);
parserContext.registerComponent(componentDefinition);
}
}
catch (BeanDefinitionStoreException ex) {
parserContext.getReaderContext().error(ex.getMessage(), element);
return null;
}
}
return definition;
}
虽然是对自定义配置文件的解析,但是,我们可以看到,在这个方法中大部分的代码是用来处理将解析后的AbstractBeanDefinition转换为BeanDefinitionHolder并注册的功能,而真正去做解析的事情委托给了方法parseInternal,正式这句代码调用了我们自定义的解析方法。
在parseInternal中并不是直接调用自定义的doParse方法,而是进行了一系列的数据准备,包括对beanClass、scope、lazyInit等属性的准备。代码如下
/**
* Creates a {@link BeanDefinitionBuilder} instance for the
* {@link #getBeanClass bean Class} and passes it to the
* {@link #doParse} strategy method.
* @param element the element that is to be parsed into a single BeanDefinition
* @param parserContext the object encapsulating the current state of the parsing process
* @return the BeanDefinition resulting from the parsing of the supplied {@link Element}
* @throws IllegalStateException if the bean {@link Class} returned from
* {@link #getBeanClass(org.w3c.dom.Element)} is {@code null}
* @see #doParse
*/
protected final AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
String parentName = getParentName(element);
if (parentName != null) {
builder.getRawBeanDefinition().setParentName(parentName);
}
// 获取自定义标签中的class,此时会调用自定义解析器如UserBeanDefinitionParser中的getBeanClass方法
Class<?> beanClass = getBeanClass(element);
if (beanClass != null) {
builder.getRawBeanDefinition().setBeanClass(beanClass);
}
else {
// 若子类没有重写getBeanClass方法,则尝试检查子类是否重写getBeanClassName方法
String beanClassName = getBeanClassName(element);
if (beanClassName != null) {
builder.getRawBeanDefinition().setBeanClassName(beanClassName);
}
}
builder.getRawBeanDefinition().setSource(parserContext.extractSource(element));
if (parserContext.isNested()) {
// 若存在父类则使用父类的scope属性
// Inner bean definition must receive same scope as containing bean.
builder.setScope(parserContext.getContainingBeanDefinition().getScope());
}
if (parserContext.isDefaultLazyInit()) {
// 配置延迟加载
// Default-lazy-init applies to custom bean definitions as well.
builder.setLazyInit(true);
}
// 调用子类重写的doParse方法进行解析
doParse(element, parserContext, builder);
return builder.getBeanDefinition();
}
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
doParse(element, builder);
}
回顾一下全部的自定义标签处理过程,虽然在实例中我们定义UserBeanDefinitionParser,但是在其中我们只是做了与自己业务逻辑相关的部分。不过我们没做但是并不代表没有,在这个处理过程中同样也是按照Spring中默认标签的处理方式进行,包括创建BeanDefinition以及进行相应默认属性的设置,对于这些工作Spring都默默地帮我们实现了,只是暴露出一些接口来供用户实现个性化的业务。
三、NamespaceHandlerSupport
这个类很关键,提供对自定义解析的支持,以后还会遇到的。包括但不限于如下示例
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/>
<mvc:annotation-driven/>
<!--扫描所有包内的注解-->
<context:component-scan base-package="xiaokui.*"/>
<!--省略大部分自定义标签-->
</beans>
另外附上类定义
/**
* Support class for implementing custom {@link NamespaceHandler NamespaceHandlers}.
* Parsing and decorating of individual {@link Node Nodes} is done via {@link BeanDefinitionParser}
* and {@link BeanDefinitionDecorator} strategy interfaces, respectively.
*
* <p>Provides the {@link #registerBeanDefinitionParser} and {@link #registerBeanDefinitionDecorator}
* methods for registering a {@link BeanDefinitionParser} or {@link BeanDefinitionDecorator}
* to handle a specific element.
*
* @author Rob Harrop
* @author Juergen Hoeller
* @since 2.0
* @see #registerBeanDefinitionParser(String, BeanDefinitionParser)
* @see #registerBeanDefinitionDecorator(String, BeanDefinitionDecorator)
*/
public abstract class NamespaceHandlerSupport implements NamespaceHandler {
// Spring中所有自定义标签的解析都是通过继承这个类然后重写init方法的,没有例外
}
/**
* Base interface used by the {@link DefaultBeanDefinitionDocumentReader}
* for handling custom namespaces in a Spring XML configuration file.
*
* <p>Implementations are expected to return implementations of the
* {@link BeanDefinitionParser} interface for custom top-level tags and
* implementations of the {@link BeanDefinitionDecorator} interface for
* custom nested tags.
*
* <p>The parser will call {@link #parse} when it encounters a custom tag
* directly under the {@code <beans>} tags and {@link #decorate} when
* it encounters a custom tag directly under a {@code <bean>} tag.
*
* <p>Developers writing their own custom element extensions typically will
* not implement this interface directly, but rather make use of the provided
* {@link NamespaceHandlerSupport} class.
*
* @author Rob Harrop
* @author Erik Wiersma
* @since 2.0
* @see DefaultBeanDefinitionDocumentReader
* @see NamespaceHandlerResolver
*/
public interface NamespaceHandler {
/**
* Invoked by the {@link DefaultBeanDefinitionDocumentReader} after
* construction but before any custom elements are parsed.
* @see NamespaceHandlerSupport#registerBeanDefinitionParser(String, BeanDefinitionParser)
*/
void init();
/**
* Parse the specified {@link Element} and register any resulting
* {@link BeanDefinition BeanDefinitions} with the
* {@link org.springframework.beans.factory.support.BeanDefinitionRegistry}
* that is embedded in the supplied {@link ParserContext}.
* <p>Implementations should return the primary {@code BeanDefinition}
* that results from the parse phase if they wish to be used nested
* inside (for example) a {@code <property>} tag.
* <p>Implementations may return {@code null} if they will
* <strong>not</strong> be used in a nested scenario.
* @param element the element that is to be parsed into one or more {@code BeanDefinitions}
* @param parserContext the object encapsulating the current state of the parsing process
* @return the primary {@code BeanDefinition} (can be {@code null} as explained above)
*/
@Nullable
BeanDefinition parse(Element element, ParserContext parserContext);
/**
* Parse the specified {@link Node} and decorate the supplied
* {@link BeanDefinitionHolder}, returning the decorated definition.
* <p>The {@link Node} may be either an {@link org.w3c.dom.Attr} or an
* {@link Element}, depending on whether a custom attribute or element
* is being parsed.
* <p>Implementations may choose to return a completely new definition,
* which will replace the original definition in the resulting
* {@link org.springframework.beans.factory.BeanFactory}.
* <p>The supplied {@link ParserContext} can be used to register any
* additional beans needed to support the main definition.
* @param source the source element or attribute that is to be parsed
* @param definition the current bean definition
* @param parserContext the object encapsulating the current state of the parsing process
* @return the decorated definition (to be registered in the BeanFactory),
* or simply the original bean definition if no decoration is required.
* A {@code null} value is strictly speaking invalid, but will be leniently
* treated like the case where the original bean definition gets returned.
*/
@Nullable
BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext);
}
通过本章的了解,相信读者对Spring中自定义标签的使用以及在解析自定义标签过程中Spring为我们做了哪些工作会有一个全面的了解。
到此为止我们已经完成了Spring中全部的解析工作,也就是说现在为止我们已经理解了Spring将bean从配置文件加载到内存中的全过程,而接下来的任务便是如何使用这些bean。
总访问次数: 294次, 一般般帅 创建于 2017-12-05, 最后更新于 2020-06-25
欢迎关注微信公众号,第一时间掌握最新动态!