1. 前言

在使用 Spring 框架(非 SpringBoot)时,经常会遇到各种在 XML 文件中配置 Bean 的场景。一些框架会自定义 XML 元素,例如 Dubbo 框架中配置 <dubbo:service ...>。这些元素是怎么定义的呢?

今天会讲到:

  • 创建 XSD 文件,来描述 XML 元素
  • 实现 NamespaceHandlerSupport 接口,初始化命名空间
  • 实现 BeanDefinitionParser 接口,定义 Bean 解析类

用 SpringBoot 的同学会说,这和我定义一个注解,然后定义注解解析器有什么区别,能实现同样的功能。的确是,但了解一下同样也可以扩展一下知识点。

尤其当我们在阅读一些框架源码时,经常会在 SPI 机制中遇到。前面讲到 Dubbo 的例子,Dubbo 框架 dubbo-config-spring 模块中,就是一个很好的例子,最后再讲解。

2. 示例

NamespaceHandlerSupport 是 Spring 框架中用于扩展 XML 配置解析的一种机制。它主要用于自定义 XML 配置的解析,以便在 Spring 的应用上下文中支持新的 XML 命名空间。

在 Spring 中,默认支持的 XML 元素和属性可能无法满足某些复杂应用的需求,因此可以通过扩展 NamespaceHandlerSupport 来自定义解析逻辑,从而支持特定的配置需求。

使用步骤:

  • 定义 XSD 文件:首先需要定义一个 XML Schema 文件(XSD),用于描述自定义 XML 元素和属性。这是为了让 XML 配置文件具有结构化和验证的能力。
  • 实现 NamespaceHandlerSupport:创建一个类继承自 NamespaceHandlerSupport,并实现其中的方法,用于注册自定义的解析器。
  • 实现 BeanDefinitionParser:为每个自定义的 XML 元素创建一个解析器类,实现 BeanDefinitionParser 接口,以定义如何将 XML 元素解析为 Spring 的 BeanDefinition
  • 注册自定义命名空间处理器:在 META-INF/spring.handlers 文件中注册自定义的 NamespaceHandler,并在 META-INF/spring.schemas 中注册 XSD 文件的位置。

假设我们需要定义一个简单的 <custom:task> 元素,来配置一个自定义任务。

2.1. 定义方

2.1.1. 定义 Task 类

确保 Task 类具有与 XML 元素相对应的属性和方法:

@Data // Getters and Setters
public class Task {
    private String name;
    private int interval;

    public void execute() {
        System.out.println("Executing task: " + name + " every " + interval + " milliseconds.");
    }
}

2.1.2. 定义 xsd 文件

创建一个 custom.xsd 文件:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns="http://www.example.com/schema/custom"
           targetNamespace="http://www.example.com/schema/custom"
           elementFormDefault="qualified">

    <xs:element name="task">
        <xs:complexType>
            <xs:attribute name="name" type="xs:string" use="required"/>
            <xs:attribute name="interval" type="xs:int" use="required"/>
        </xs:complexType>
    </xs:element>

</xs:schema>

2.1.3. 实现 BeanDefinitionParser

在 TaskBeanDefinitionParser 中,确保 getBeanClass 方法返回 Task.class,并在 doParse 方法中解析 XML 元素的属性:

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.w3c.dom.Element;

public class TaskBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    @Override
    protected Class<?> getBeanClass(Element element) {
        return Task.class; // 确保返回的是 Task 类
    }

    @Override
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
        String name = element.getAttribute("name");
        int interval = Integer.parseInt(element.getAttribute("interval"));
        
        // 设置 Task 类的属性
        builder.addPropertyValue("name", name);
        builder.addPropertyValue("interval", interval);
    }
}

2.1.4. 实现 NamespaceHandlerSupport

创建一个 CustomNamespaceHandler 类:

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class CustomNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("task", new TaskBeanDefinitionParser());
    }
}

2.1.5. 注册命名空间处理器

META-INF/spring.handlers 中添加:

http\://www.example.com/schema/custom=com.example.CustomNamespaceHandler

在 META-INF/spring.schemas 中添加:

http\://www.example.com/schema/custom/custom.xsd=classpath:custom.xsd

2.2. 使用方

2.2.1. 使用自定义命名空间

在 Spring 配置文件中使用自定义命名空间:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:custom="http://www.example.com/schema/custom"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.example.com/schema/custom classpath:custom.xsd">

    <custom:task name="myTask" interval="5000"/>
</beans>

2.2.2. 使用 Bean

上述配置已经在 Spring 环境中注册了 Bean,那么直接可以使用 myTask Bean 了。

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TaskRunner {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        
        // 获取 Task Bean
        Task task = (Task) context.getBean("myTask");
        
        // 调用 Task 的方法
        task.execute();
    }
}

3. 示例(包含SPI)

这种自定义 XML 命名空间和解析器的机制,常用于 SPI(Service Provider Interface)机制中。

SPI 机制允许在运行时动态加载和使用第三方服务提供者,而自定义 XML 命名空间和解析器可以提供一种灵活的方式来配置和管理这些服务提供者。

1、SPI 机制概述

SPI 机制通常用于以下场景:

  • 插件式架构:允许在运行时动态加载和配置插件。
  • 服务发现:在运行时发现并加载可用的服务实现。
  • 扩展性:允许在不修改现有代码的情况下添加新的功能或服务实现。
2、如何结合 SPI 和自定义 XML 命名空间
  1. 定义 SPI 接口:定义一个 SPI 接口,让服务提供者实现该接口。
  2. 定义 XML 配置:使用自定义 XML 命名空间来配置 SPI 服务提供者。
  3. 注册命名空间处理器:通过 NamespaceHandlerSupport 注册自定义命名空间处理器。
  4. 加载和使用 SPI 服务提供者:在运行时通过 SPI 机制加载并使用服务提供者。
3、分成三方
  • 定义方

    • 定义 SPI 接口
    • 提供自定义命名空间的支持(XSD 文件、解析器)。
  • SPI服务提供方

    • 实现 SPI 接口
    • 并通过 META-INF/services 进行注册。
  • 使用方

    • 通过配置文件,使用方可以灵活地设置 TaskService 的属性值。
    • 在 Spring 配置文件中使用自定义命名空间标签来配置和使用服务。

3.1. 定义方

3.1.1. 定义 SPI 接口

之前的区别点

原本 Bean 对应的是 Task 类,现在换成了一个 TaskService 接口。接口的实现类,由服务实现方提供。

如下定义一个 TaskService 接口:

public interface TaskService {
    void executeTask();
    void setName(String name);
    void setInterval(int interval);
}

3.1.2. 定义 XSD 文件

创建一个 custom.xsd 文件来描述 XML 元素:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns="http://www.example.com/schema/custom"
           targetNamespace="http://www.example.com/schema/custom"
           elementFormDefault="qualified">

    <xs:element name="task">
        <xs:complexType>
            <xs:attribute name="name" type="xs:string" use="required"/>
            <xs:attribute name="interval" type="xs:int" use="required"/>
        </xs:complexType>
    </xs:element>

</xs:schema>

3.1.3. 实现 NamespaceHandlerSupport

创建一个 CustomNamespaceHandler 类:

import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class CustomNamespaceHandler extends NamespaceHandlerSupport {
    @Override
    public void init() {
        registerBeanDefinitionParser("task", new TaskBeanDefinitionParser());
    }
}

3.1.4. 实现 BeanDefinitionParser

创建一个 TaskBeanDefinitionParser 类:

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.w3c.dom.Element;

public class TaskBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
    @Override
    protected Class<?> getBeanClass(Element element) {
        return TaskServiceImpl.class; // TaskServiceImpl 是 TaskService 的实现类
    }

    @Override
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
        String name = element.getAttribute("name");
        int interval = Integer.parseInt(element.getAttribute("interval"));

        builder.addPropertyValue("name", name);
        builder.addPropertyValue("interval", interval);
    }
}

3.1.5. 注册命名空间处理器

META-INF/spring.handlers 中注册命名空间处理器:

http\://www.example.com/schema/custom=com.example.CustomNamespaceHandler

META-INF/spring.schemas 中注册 XSD 文件的位置:

http\://www.example.com/schema/custom/custom.xsd=classpath:custom.xsd

3.2. SPI服务提供方

3.2.1. 定义 SPI 服务提供者

创建一个实现 TaskService 接口的实现类 DefaultTaskService

public class DefaultTaskService implements TaskService {
    private String name;
    private int interval;

    @Override
    public void executeTask() {
        System.out.println("Executing task: " + name + " every " + interval + " milliseconds.");
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void setInterval(int interval) {
        this.interval = interval;
    }
}

3.2.2. 配置 SPI

在 src/main/resources/META-INF/services 目录下创建一个名为 com.example.TaskService 的文件,文件内容是实现类的全限定名:

com.example.DefaultTaskService

3.3. 使用方

使用方在其 Spring 配置文件中使用自定义命名空间来配置和使用 TaskService。

3.2.1. Spring XML 配置

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:custom="http://www.example.com/schema/custom"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.example.com/schema/custom classpath:custom.xsd">

    <!-- 使用自定义命名空间标签来配置 TaskService 的属性 -->
    <custom:task name="myCustomTask" interval="10000"/>
</beans>

3.3.3. 使用 TaskService Bean

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class TaskRunner {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");

        // 获取 TaskService Bean
        TaskService taskService = (TaskService) context.getBean(TaskService.class);

        // 调用 TaskService 的方法
        taskService.executeTask();
    }
}

4. dubbo-config-spring

这样就很好理解 dubbo-config-spring 的项目结构。阅读代码时先看下列配置,查看命名空间处理器:

META-INF/spring.handlers 中注册命名空间处理器:

http\://code.alibabatech.com/schema/dubbo=com.alibaba.dubbo.config.spring.schema.DubboNamespaceHandler

META-INF/spring.schemas 中注册 XSD 文件的位置:

http\://code.alibabatech.com/schema/dubbo/dubbo.xsd=META-INF/dubbo.xsd

可以通过 dubbo.xsd 文件查看 XML 配置约束。

另外在 DubboNamespaceHandler中定义了 Dubbo 各个 Bean 的解析器:

public class DubboNamespaceHandler extends NamespaceHandlerSupport {
    public DubboNamespaceHandler() {
    }

    public void init() {
        this.registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        this.registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig.class, true));
        this.registerBeanDefinitionParser("registry", new DubboBeanDefinitionParser(RegistryConfig.class, true));
        this.registerBeanDefinitionParser("monitor", new DubboBeanDefinitionParser(MonitorConfig.class, true));
        this.registerBeanDefinitionParser("provider", new DubboBeanDefinitionParser(ProviderConfig.class, true));
        this.registerBeanDefinitionParser("consumer", new DubboBeanDefinitionParser(ConsumerConfig.class, true));
        this.registerBeanDefinitionParser("protocol", new DubboBeanDefinitionParser(ProtocolConfig.class, true));
        this.registerBeanDefinitionParser("service", new DubboBeanDefinitionParser(ServiceBean.class, true));
        this.registerBeanDefinitionParser("reference", new DubboBeanDefinitionParser(ReferenceBean.class, false));
        this.registerBeanDefinitionParser("annotation", new DubboBeanDefinitionParser(AnnotationBean.class, true));
    }

    static {
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }
}

可以看到 Dubbo 的各种类型 Bean 都是通过 DubboBeanDefinitionParser 来解析的。

5. SpringBoot 实现

SPI的例子中,如果不用 XML,SpringBoot 可以怎么实现呢?

5.1. 定义方

1、定义 SPI 接口

在接口中直接定义需要的配置参数,或者通过构造函数注入。

public interface TaskService {
    void executeTask();
}

public class ConfigurableTaskService implements TaskService {

    private final TaskConfiguration config;

    public ConfigurableTaskService(TaskConfiguration config) {
        this.config = config;
    }

    @Override
    public void executeTask() {
        System.out.println("Executing task: " + config.getName() + " every " + config.getInterval() + " milliseconds.");
    }
}
2、定义配置属性接口

定义一个配置接口,使得配置需求明确。

public interface TaskConfiguration {
    String getName();
    int getInterval();
}
3、提供默认的配置属性实现
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "task")
public class DefaultTaskProperties implements TaskConfiguration {
    private String name;
    private int interval;

    // Getters and Setters
    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int getInterval() {
        return interval;
    }

    public void setInterval(int interval) {
        this.interval = interval;
    }
}

5.2. 服务提供方

提供方只需实现 TaskService 接口,并确保 TaskConfiguration 被注入。
如果有多个提供方,具体使用哪个,可以通过扫描路径或者 @ConditionOnxxx实现。

import org.springframework.stereotype.Service;

@Service
public class DefaultTaskService extends ConfigurableTaskService {

    public DefaultTaskService(TaskConfiguration config) {
        super(config);
    }
}

5.3. 使用方

1、配置文件设置属性值。

application.yml

task:
  name: myCustomTask
  interval: 10000
2、启动类

启动类启用配置类,并且在运行起来后,执行 TaskService Bean 方法。

@SpringBootApplication
@EnableConfigurationProperties(DefaultTaskProperties.class)
public class TaskApplication implements CommandLineRunner {

    private final TaskService taskService;

    public TaskApplication(TaskService taskService) {
        this.taskService = taskService;
    }

    public static void main(String[] args) {
        SpringApplication.run(TaskApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        taskService.executeTask();
    }
}

NamespaceHandlerSupport 提供了一种强大且灵活的方式来扩展 Spring 的 XML 配置能力,允许开发者创建自定义的 XML 配置选项。

虽然 Spring Boot 提倡使用注解和自动配置,但在某些复杂场景下,特别是需要与现有 XML 配置集成时,自定义命名空间仍然是一个非常有用的工具。


KerryWu
641 声望159 粉丝

保持饥饿