Spring是如何解析自定义标签的(类SPI)
Spring SPI
Spring借鉴了Java SPI思想来解析各种标签,我们称之为Spring SPI。
Spring SPI沿用了Java SPI的设计思想,但在实现上和Java SPI及Dubbo SPI也存在差异,Spring通过spring.handlers和spring.factories两种方式实现SPI机制,可以在不修改Spring源码的前提下,做到对Spring框架的扩展开发。
Java SPI从/META-INF/services目录加载服务提供接口配置,而Spring默认从META-INF/spring.handlers和META-INF/spring.factories目录加载配置,其中META-INF/spring.handlers的路径可以通过创建实例时重新指定,而META-INF/spring.factories固定不可变。
DefaultNamespaceHandlerResolver
类似于Java SPI的ServiceLoader,负责解析spring.handlers配置文件,生成namespaceUri和NamespaceHandler名称的映射,并实例化NamespaceHandler。
spring.handlers
自定义标签配置文件。Spring在2.0时便引入了spring.handlers,通过配置spring.handlers文件实现自定义标签并使用自定义标签解析类进行解析实现动态扩展,内容配置如:
http\://www.springframework.org/schema/context=org.springframework.context.config.ContextNamespaceHandler
http\://www.springframework.org/schema/jee=org.springframework.ejb.config.JeeNamespaceHandler
http\://www.springframework.org/schema/lang=org.springframework.scripting.config.LangNamespaceHandler
http\://www.springframework.org/schema/task=org.springframework.scheduling.config.TaskNamespaceHandler
http\://www.springframework.org/schema/cache=org.springframework.cache.config.CacheNamespaceHandler
spring.handlers实现的SPI是以namespaceUri作为key,NamespaceHandler作为value,建立映射关系,在解析标签时通过namespaceUri获取相应的NamespaceHandler来解析。
SpringFactoriesLoader
类似于Java SPI的ServiceLoader,负责解析spring.factories,并将指定接口的所有实现类实例化后返回。
spring.factories
Spring在3.2时引入spring.factories,加强版的SPI配置文件,为Spring的SPI机制的实现提供支撑,内容配置如:
org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory
spring.factories实现的SPI是以接口的全限定名作为key,接口实现类作为value,多个实现类用逗号隔开,最终返回的结果是该接口所有实现类的实例集合。
spring.handlers
代码演练
创建NameSpaceHandler
MysqlDataBaseHandler
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Element;
// 继承抽象类
public class MysqlDataBaseHandler extends NamespaceHandlerSupport {
@Override
public void init() {
}
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
System.out.println("MysqlDataBaseHandler!!!");
return null;
}
}
OracleDataBaseHandler
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
import org.springframework.beans.factory.xml.ParserContext;
import org.w3c.dom.Element;
public class OracleDataBaseHandler extends NamespaceHandlerSupport {
@Override
public void init() {
}
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
System.out.println("OracleDataBaseHandler!!!");
return null;
}
}
在项目META-INF/目录下创建spring.handlers文件
#一个namespaceUri对应一个handler
http\://www.mysql.org/schema/mysql=com.harvey.demo.handler.MysqlDataBaseHandler
http\://www.oracle.org/schema/oracle=com.harvey.demo.handler.OracleDataBaseHandler
运行代码
import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;
import org.springframework.beans.factory.xml.NamespaceHandler;
public class SpringSpiTest {
public static void main(String args[]){
// spring中提供的默认namespace URI解析器
DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver();
// 此处假设nameSpaceUri已从xml文件中解析出来,正常流程是在项目启动的时候会解析xml文件,获取到对应的自定义标签
// 然后根据自定义标签取得对应的nameSpaceUri
String mysqlNameSpaceUri = "http://www.mysql.org/schema/mysql";
NamespaceHandler handler = resolver.resolve(mysqlNameSpaceUri);
// 验证自定义NamespaceHandler,这里参数传null,实际使用中传具体的Element
handler.parse(null, null);
String oracleNameSpaceUri = "http://www.oracle.org/schema/oracle";
handler = resolver.resolve(oracleNameSpaceUri);
handler.parse(null, null);
}
}
上述代码通过解析spring.handlers实现对自定义标签的动态解析,以namespaceUri作为key获取具体的NameSpaceHandler实现类,这里有别于Java SPI,其中:
DefaultNamespaceHandlerResolver是NamespaceHandlerResolver接口的默认实现类,用于解析自定义标签。
- DefaultNamespaceHandlerResolver.resolve(String namespaceUri)方法以namespaceUri作为参数,默认加载各jar包中的META-INF/spring.handlers配置文件,通过解析spring.handlers文件建立namespaceUri和NameSpaceHandler的映射。
- 加载配置文件的默认路径是META-INF/spring.handlers,但可以使用DefaultNamespaceHandlerResolver(ClassLoader, String)构造方法修改,DefaultNamespaceHandlerResolver有多个重载方法。
- DefaultNamespaceHandlerResolver.resolve(String namespaceUri)方法主要被BeanDefinitionParserDelegate的parseCustomElement()和decorateIfRequired()方法中调用,所以spring.handlers SPI机制主要用在bean的扫描和解析过程中。
源码分析
org.springframework.context.support.AbstractApplicationContext#refresh
org.springframework.context.support.AbstractApplicationContext#obtainFreshBeanFactory
org.springframework.context.support.AbstractRefreshableApplicationContext#refreshBeanFactory
org.springframework.web.context.support.XmlWebApplicationContext#loadBeanDefinitions(org.springframework.beans.factory.support.DefaultListableBeanFactory)
org.springframework.web.context.support.XmlWebApplicationContext#loadBeanDefinitions(org.springframework.beans.factory.xml.XmlBeanDefinitionReader)
org.springframework.beans.factory.support.AbstractBeanDefinitionReader#loadBeanDefinitions(java.lang.String)
org.springframework.beans.factory.xml.XmlBeanDefinitionReader#loadBeanDefinitions(org.springframework.core.io.Resource)
org.springframework.beans.factory.xml.XmlBeanDefinitionReader#doLoadBeanDefinitions
org.springframework.beans.factory.xml.XmlBeanDefinitionReader#registerBeanDefinitions
org.springframework.beans.factory.xml.XmlBeanDefinitionReader#createReaderContext
org.springframework.beans.factory.xml.XmlBeanDefinitionReader#getNamespaceHandlerResolver
org.springframework.beans.factory.xml.XmlBeanDefinitionReader#createDefaultNamespaceHandlerResolver
最后一步,创建DefaultNamespaceHandlerResolver实例:
protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());
return new DefaultNamespaceHandlerResolver(cl);
}
DefaultNamespaceHandlerResolver
DefaultNamespaceHandlerResolver.resolve()方法本身是根据namespaceUri获取对应的namespaceHandler对标签进行解析,核心源码:
public NamespaceHandler resolve(String namespaceUri) {
// 1、核心逻辑之一:获取namespaceUri和namespaceHandler映射关系
Map<String, Object> handlerMappings = getHandlerMappings();
// 根据namespaceUri参数取对应的namespaceHandler全限定类名or NamespaceHandler实例
Object handlerOrClassName = handlerMappings.get(namespaceUri);
if (handlerOrClassName == null) {
return null;
}
// 2、handlerOrClassName是已初始化过的实例则直接返回
else if (handlerOrClassName instanceof NamespaceHandler) {
return (NamespaceHandler) handlerOrClassName;
}else {
String className = (String) handlerOrClassName;
try {
///3、使用反射根据namespaceHandler全限定类名加载实现类
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");
}
// 3.1、初始化namespaceHandler实例
NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
// 3.2、 初始化,不同的namespaceHandler实现类初始化方法逻辑有差异
namespaceHandler.init();
// 4、将初始化好的实例放入内存缓存中,下次解析到相同namespaceUri标签时直接返回,避免再次初始化
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);
}
}
}
第1步:源码中getHandlerMappings()是比较核心的一个方法,通过懒加载的方式解析spring.handlers并返回namespaceUri和NamespaceHandler的映射关系。
第2步:根据namespaceUri返回对应的NamespaceHandler全限定名或者具体的实例(是名称还是实例取决于是否被初始化过,若是初始化过的实例会直接返回)
第3步:是NamespaceHandler实现类的全限定名,通过上述源码中的第3步,使用反射进行初始化。
第4步:将初始化后的实例放到handlerMappings内存缓存中,这也是第2步为什么可能是NamespaceHandler类型的原因。
看完resolve方法的源码,再看下resolve方法在Spring中调用场景,大致可以了解spring.handlers的使用场景:
可以看到resolve()主要用在标签解析过程中,主要被在BeanDefinitionParserDelegate的parseCustomElement和decorateIfRequired方法中调用。
resolve()源码中核心逻辑之一便是调用的getHandlerMappings(),在getHandlerMappings()中实现对各个jar包中的META-INF/spring.handlers文件的解析,如:
private Map<String, Object> getHandlerMappings() {
Map<String, Object> handlerMappings = this.handlerMappings;
// 使用线程安全的解析逻辑,避免在并发场景下重复的解析,没必要重复解析
// 这里在同步代码块的内外对handlerMappings == null作两次判断很有必要,采用懒汉式初始化
if (handlerMappings == null) {
synchronized (this) {
handlerMappings = this.handlerMappings;
// duble check
if (handlerMappings == null) {
if (logger.isDebugEnabled()) {
logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
}
try {
// 加载handlerMappingsLocation目录文件,handlerMappingsLocation路径值可变,默认是META-INF/spring.handlers
Properties mappings =
PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
if (logger.isDebugEnabled()) {
logger.debug("Loaded NamespaceHandler mappings: " + mappings);
}
// 初始化内存缓存
handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
// 将加载到的属性合并到handlerMappings中
CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
// 赋值内存缓存
this.handlerMappings = handlerMappings;
}catch (IOException ex) {
throw new IllegalStateException(
"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
}
}
}
}
return handlerMappings;
}
源码中this.handlerMappings是一个Map类型的内存缓存,存放解析到的namespaceUri以及NameSpaceHandler实例。
getHandlerMappings()方法体中的实现使用了线程安全方式,增加了同步逻辑。
Spring基于spring.handlers实现SPI逻辑相对比较简单,但应用却比较灵活,对自定义标签的支持很方便,在不修改Spring源码的前提下轻松实现接入,如Dubbo中定义的各种Dubbo标签便是很好的利用了spring.handlers。
Spring提供如此灵活的功能,那是如何应用的呢?下面简单了解下parseCustomElement()。
BeanDefinitionParserDelegate.parseCustomElement()
public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
// 获取标签的namespaceUri
String namespaceUri = getNamespaceURI(ele);
// 首先获得DefaultNamespaceHandlerResolver实例在再以namespaceUri作为参数调用resolve方法解析取得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中的parse方法开始解析标签
return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
}
parseCustomElement作为解析标签的中间方法,再看下parseCustomElement的调用情况:
在parseBeanDefinitions()中被调用,再看下parseBeanDefinitions的源码:
protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
// spring内部定义的标签为默认标签,即非spring内部定义的标签都不是默认的namespace
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;
// root子标签也做此判断
if (delegate.isDefaultNamespace(ele)) {
parseDefaultElement(ele, delegate);
}else{
// 子标签非spring默认标签(即自定义标签)也走parseCustomElement来解析
delegate.parseCustomElement(ele);
}
}
}
}else {
// 非spring的默认标签(即自定义的标签)走parseCustomElement来解析
delegate.parseCustomElement(root);
}
}
到此就很清晰了,调用前判断是否为Spring默认标签,不是默认标签调用parseCustomElement来解析,最后调用resolve方法。
spring.factories
代码演练
创建DataBaseSPI接口
public interface DataBaseSPI {
void dataBaseOperation();
}
创建DataBaseSPI接口的实现类
MysqlDataBaseImpl
public class MysqlDataBaseImpl implements DataBaseSPI {
@Override
public void dataBaseOperation() {
System.out.println("Mysql database test!!!!");
}
}
OracleDataBaseImpl
public class OracleDataBaseImpl implements DataBaseSPI {
@Override
public void dataBaseOperation() {
System.out.println("Oracle database test!!!!");
}
}
在项目META-INF/目录下创建spring.factories文件
#key是接口的全限定名,value是接口的实现类
spring.spi.factories.DataBaseSPI = spring.spi.factories.impl.MysqlDataBaseImpl,spring.spi.factories.impl.OracleDataBaseImpl
运行代码
public class SpringSpiTest {
public static void main(String args[]){
// 调用SpringFactoriesLoader.loadFactories方法加载DataBaseSPI接口所有实现类的实例
List<DataBaseSPI> spis= SpringFactoriesLoader.loadFactories(DataBaseSPI.class, Thread.currentThread().getContextClassLoader());
// 遍历DataBaseSPI接口实现类实例
for(DataBaseSPI spi : spis){
spi.dataBaseOperation();
}
}
}
从上述的示例代码中可以看出spring.facotries方式实现的SPI和Java SPI很相似,都是先获取指定接口类型的实现类,然后遍历访问所有的实现。但也存在一定的差异:
(1)配置上
Java SPI是一个服务提供接口对应一个配置文件,配置文件中存放当前接口的所有实现类,多个服务提供接口对应多个配置文件,所有配置都在services目录下;
Spring factories SPI是一个spring.factories配置文件存放多个接口及对应的实现类,以接口全限定名作为key,实现类作为value来配置,多个实现类用逗号隔开,仅spring.factories一个配置文件。
(2)实现上
Java SPI使用了懒加载模式,即在调用ServiceLoader.load()时仅是返回了ServiceLoader实例,尚未解析接口对应的配置文件,在使用时即循环遍历时才正式解析返回服务提供接口的实现类实例;
Spring factories SPI在调用SpringFactoriesLoader.loadFactories()时便已解析spring.facotries文件返回接口实现类的实例。
源码分析
SpringFactoriesLoader.loadFactories()
public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
Assert.notNull(factoryClass, "'factoryClass' must not be null");
ClassLoader classLoaderToUse = classLoader;
// 1.确定类加载器
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
// 2.核心逻辑之一:解析各jar包中META-INF/spring.factories文件中factoryClass的实现类全限定名
List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
}
List<T> result = new ArrayList<T>(factoryNames.size());
// 3.遍历实现类的全限定名并进行实例化
for (String factoryName : factoryNames) {
result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
}
// 排序
AnnotationAwareOrderComparator.sort(result);
// 4.返回实例化后的结果集
return result;
}
源码中loadFactoryNames() 是另外一个比较核心的方法,解析spring.factories文件中指定接口的实现类的全限定名。
SpringFactoriesLoader#loadFactoryNames()
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
// 1.接口全限定名
String factoryClassName = factoryClass.getName();
try {
// 2.加载META-INF/spring.factories文件路径(分布在各个不同jar包里,所以这里会是多个文件路径,枚举返回)
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
// 3.遍历枚举集合,逐个解析spring.factories文件
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String propertyValue = properties.getProperty(factoryClassName);
// 4.spring.factories文件中一个接口的实现类有多个时会用逗号隔开,这里拆开获取实现类全限定名
for (String factoryName : StringUtils.commaDelimitedListToStringArray(propertyValue)) {
result.add(factoryName.trim());
}
}
return result;
}catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
源码中第2步获取所有jar包中META-INF/spring.factories文件路径,以枚举值返回。
源码中第3步开始遍历spring.factories文件路径,逐个加载解析,整合factoryClass类型的实现类名称。
获取到实现类的全限定名集合后,便根据实现类的名称逐个实例化,继续看下instantiateFactory()方法的源码。
SpringFactoriesLoader#instantiateFactory()
private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
try {
// 1.使用classLoader类加载器加载instanceClassName类
Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
if (!factoryClass.isAssignableFrom(instanceClass)) {
throw new IllegalArgumentException(
"Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
}
// 2.instanceClassName类中的构造方法
Constructor<?> constructor = instanceClass.getDeclaredConstructor();
ReflectionUtils.makeAccessible(constructor);
// 3.实例化
return (T) constructor.newInstance();
}
catch (Throwable ex) {
throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
}
}
实例化方法是私有型(private)静态方法,这个有别于loadFactories和loadFactoryNames。
实例化逻辑整体使用了反射实现,比较通用的实现方式。
Spring在3.2便已引入spring.factories,那spring.factories在Spring框架中又是如何使用的呢?先看下loadFactories方法的调用情况:
从调用情况看Spring自3.2引入spring.factories SPI后并没有真正的利用起来,使用的地方比较少,然而真正把spring.factories发扬光大的,是在Spring Boot中。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 使用C#创建一个MCP客户端
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 按钮权限的设计及实现