回顾-springboot自定义xml

在 Spring Boot 中,若需通过自定义 XML 配置文件 定义 Bean,并通过扫描(scan)机制加载这些 Bean,核心思路是:自定义 XML 命名空间解析器 (处理自定义标签)+ 注册自定义 BeanDefinitionParser (将 XML 配置转换为 BeanDefinition)+ 配置 Spring 扫描 XML 资源(让 Spring 识别自定义 XML 文件)。

以下是完整实现步骤,包含自定义 XML 标签、解析器、Spring Boot 集成扫描的全流程:

一、核心概念铺垫

  • 自定义 XML 标签 :例如 <my:user id="user1" name="张三" age="25"/>,而非 Spring 原生的 <bean/> 标签。
  • NamespaceHandler:绑定 XML 命名空间与解析器,告诉 Spring 如何处理自定义标签。
  • BeanDefinitionParser :解析自定义标签的属性,生成 Spring 可识别的 BeanDefinition(Bean 的元数据)。
  • 资源扫描:让 Spring Boot 扫描指定路径下的自定义 XML 文件,加载其中的 Bean。

二、完整实现步骤

步骤 1:创建项目结构(Maven/Gradle)

核心依赖(Spring Boot 基础依赖):

xml

复制代码
<!-- pom.xml 核心依赖 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.7.15</version> <!-- 按需选择版本 -->
    <relativePath/>
</parent>

<dependencies>
    <!-- Spring Boot 核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- 测试依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

步骤 2:定义目标 Bean 类(需被 XML 配置实例化)

创建普通 Java 类,作为自定义 XML 标签要实例化的 Bean:

java

运行

复制代码
// com.example.demo.bean.User.java
package com.example.demo.bean;

import lombok.Data;

@Data // Lombok 简化 getter/setter,也可手动编写
public class User {
    private String id;
    private String name;
    private Integer age;
}

步骤 3:自定义 BeanDefinitionParser(解析 XML 标签)

实现 BeanDefinitionParser 接口,将自定义 XML 标签的属性映射到 User 类的属性,并生成 BeanDefinition

java

运行

复制代码
// com.example.demo.parser.UserBeanDefinitionParser.java
package com.example.demo.parser;

import com.example.demo.bean.User;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser;
import org.w3c.dom.Element;

/**
 * 解析 <my:user> 标签,生成 User 的 BeanDefinition
 */
public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

    // 指定 Bean 的类型(User.class)
    @Override
    protected Class<?> getBeanClass(Element element) {
        return User.class;
    }

    // 解析 XML 标签的属性,设置到 BeanDefinition 中
    @Override
    protected void doParse(Element element, BeanDefinitionBuilder builder) {
        // 读取 XML 标签的 "id" 属性(可选,若不指定则 Spring 自动生成)
        String id = element.getAttribute("id");
        if (id != null && !id.isEmpty()) {
            builder.addPropertyValue("id", id);
        }

        // 读取 "name" 属性
        String name = element.getAttribute("name");
        if (name != null && !name.isEmpty()) {
            builder.addPropertyValue("name", name);
        }

        // 读取 "age" 属性(转换为 Integer 类型)
        String ageStr = element.getAttribute("age");
        if (ageStr != null && !ageStr.isEmpty()) {
            builder.addPropertyValue("age", Integer.parseInt(ageStr));
        }
    }
}
  • 继承 AbstractSingleBeanDefinitionParser(简化单个 Bean 的解析,无需手动处理 BeanDefinitionRegistry)。
  • getBeanClass:指定当前标签对应的 Bean 类型。
  • doParse:解析 XML 元素的属性,通过 BeanDefinitionBuilder 设置到 Bean 元数据中。

步骤 4:自定义 NamespaceHandler(绑定命名空间与解析器)

实现 NamespaceHandlerSupport,将自定义 XML 命名空间与 UserBeanDefinitionParser 绑定,告诉 Spring:"遇到 <my:user> 标签时,用这个解析器处理":

java

运行

复制代码
// com.example.demo.handler.MyNamespaceHandler.java
package com.example.demo.handler;

import com.example.demo.parser.UserBeanDefinitionParser;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

/**
 * 自定义命名空间处理器:绑定标签名与解析器
 */
public class MyNamespaceHandler extends NamespaceHandlerSupport {

    // 初始化时,将 "user" 标签与 UserBeanDefinitionParser 关联
    @Override
    public void init() {
        // 标签名 <my:user> 中的 "user" 对应解析器
        registerBeanDefinitionParser("user", new UserBeanDefinitionParser());
    }
}

步骤 5:配置 Spring 命名空间映射(关键!)

Spring 需通过两个配置文件识别自定义命名空间:spring.handlersspring.schemas(必须放在 src/main/resources/META-INF/ 目录下)。

5.1 创建 META-INF/spring.handlers

指定自定义命名空间的处理器类(格式:命名空间URI=处理器全类名):

properties

复制代码
# META-INF/spring.handlers
http://www.example.com/schema/my=com.example.demo.handler.MyNamespaceHandler
  • 命名空间 URI 可自定义(例如 http://www.example.com/schema/my),后续 XML 配置需引用此 URI。
5.2 创建 META-INF/spring.schemas(可选,用于 XSD 校验)

若需 XML 标签属性校验,可指定 XSD 文件路径(无校验需求可省略,但建议配置避免 IDE 报警):

properties

复制代码
# META-INF/spring.schemas
http://www.example.com/schema/my/my.xsd=META-INF/my.xsd
5.3 (可选)创建 META-INF/my.xsd(XSD 约束文件)

定义 XML 标签的属性约束(例如 name 为必填,age 为非负整数),让 IDE 提供语法提示和校验:

xsd

复制代码
<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://www.example.com/schema/my"
            xmlns:my="http://www.example.com/schema/my"
            elementFormDefault="qualified">

    <!-- 定义 <my:user> 标签 -->
    <xsd:element name="user">
        <xsd:complexType>
            <xsd:attribute name="id" type="xsd:string"/>
            <xsd:attribute name="name" type="xsd:string" use="required"/> <!-- 必填 -->
            <xsd:attribute name="age" type="xsd:positiveInteger"/> <!-- 正整数 -->
        </xsd:complexType>
    </xsd:element>
</xsd:schema>

步骤 6:创建自定义 XML 配置文件(定义 Bean)

src/main/resources/ 下创建 XML 文件(例如 my-beans.xml),使用自定义标签定义 Bean:

xml

复制代码
<!-- src/main/resources/my-beans.xml -->
<?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:my="http://www.example.com/schema/my" <!-- 引入自定义命名空间 -->
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.example.com/schema/my <!-- 自定义命名空间 URI -->
           http://www.example.com/schema/my/my.xsd"> <!-- 自定义 XSD 路径 -->

    <!-- 自定义标签:定义 User Bean -->
    <my:user id="user1" name="张三" age="25"/>
    <my:user id="user2" name="李四" age="30"/>

</beans>

步骤 7:Spring Boot 扫描 XML 配置文件

Spring Boot 默认扫描注解 Bean(如 @Component),需手动配置扫描 XML 文件,有两种方式:

方式 1:使用 @ImportResource 注解(直接指定 XML 文件)

在 Spring Boot 启动类上添加 @ImportResource,指定自定义 XML 文件路径:

java

运行

复制代码
// com.example.demo.DemoApplication.java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;

// 导入自定义 XML 配置文件(支持多个文件,用数组指定)
@ImportResource(locations = "classpath:my-beans.xml")
@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
方式 2:扫描指定目录下的所有 XML 文件(批量加载)

若有多个 XML 文件,可通过 ResourcePatternResolver 扫描目录(例如 classpath:xml/*.xml),需自定义配置类:

java

运行

复制代码
// com.example.demo.config.XmlScanConfig.java
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;

import java.io.IOException;

@Configuration
public class XmlScanConfig {

    // 扫描 classpath:xml/ 目录下的所有 XML 文件,注册 Bean
    @Bean
    public GenericXmlApplicationContext xmlApplicationContext() throws IOException {
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        // 扫描路径:classpath:xml/ 下的所有 .xml 文件
        String locationPattern = "classpath:xml/*.xml";
        GenericXmlApplicationContext context = new GenericXmlApplicationContext();
        context.load(resolver.getResources(locationPattern));
        context.refresh();
        return context;
    }
}
  • 此时需将 my-beans.xml 移动到 src/main/resources/xml/ 目录下。
  • 这种方式适合批量加载多个 XML 配置文件。

步骤 8:测试验证(是否成功加载 Bean)

编写测试类,验证 Spring 容器是否已加载 XML 中定义的 User Bean:

java

运行

复制代码
// com.example.demo.DemoApplicationTests.java
package com.example.demo;

import com.example.demo.bean.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    void testCustomXmlBean() {
        // 从容器中获取 XML 中定义的 Bean(id 为 user1)
        User user1 = applicationContext.getBean("user1", User.class);
        assertNotNull(user1);
        assertEquals("张三", user1.getName());
        assertEquals(25, user1.getAge());

        // 获取 user2
        User user2 = applicationContext.getBean("user2", User.class);
        assertNotNull(user2);
        assertEquals("李四", user2.getName());
        assertEquals(30, user2.getAge());

        System.out.println("user1: " + user1);
        System.out.println("user2: " + user2);
    }
}

运行测试,若控制台输出 user1user2 的信息,且断言通过,则说明自定义 XML 解析器和扫描机制生效!

三、关键注意事项

  1. 命名空间一致性spring.handlers 中的 URI、XML 配置中的 xmlns:my、XSD 中的 targetNamespace 必须完全一致。
  2. 资源路径正确 :XML 文件路径需符合 classpath: 前缀规则(例如 classpath:my-beans.xml 对应 src/main/resources/my-beans.xml)。
  3. 属性类型转换 :XML 标签属性默认是字符串,需在 doParse 中手动转换为 Bean 的属性类型(如 Integer.parseInt(ageStr))。
  4. Spring Boot 版本兼容:上述代码基于 Spring Boot 2.x,3.x 版本(Spring 6.x)用法一致,仅需调整依赖版本。

四、扩展场景

  • 多个自定义标签 :在 MyNamespaceHandler 中注册多个解析器(例如 registerBeanDefinitionParser("order", new OrderBeanDefinitionParser()))。
  • 复杂属性解析 :若 Bean 有嵌套属性(如 User 包含 Address),可自定义 CompositeBeanDefinitionParser 处理嵌套标签。
  • 注解与 XML 混合 :Spring Boot 支持同时扫描注解 Bean(@Component)和 XML 定义的 Bean,无需额外配置。

补充

上面的xml加载不太友好

方案一:自定义 BeanDefinitionRegistryPostProcessor(推荐)

BeanDefinitionRegistryPostProcessor 是 Spring 提供的扩展点,允许在容器初始化时动态注册 BeanDefinition ,且运行在容器刷新前,完全无需手动 refresh(),是最安全、最优雅的方式。

实现步骤

1. 自定义 XML 扫描注册器(核心)

java

运行

复制代码
// com.example.demo.config.XmlBeanDefinitionScannerConfig.java
package com.example.demo.config;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;

import java.io.IOException;

/**
 * 自动扫描 XML 文件,注册 BeanDefinition 到主容器(无 refresh,无硬编码)
 */
@Configuration
public class XmlBeanDefinitionScannerConfig implements BeanDefinitionRegistryPostProcessor {

    // 约定 XML 文件存放目录(可配置在 application.yml 中,更灵活)
    private static final String XML_SCAN_PATTERN = "classpath:xml/*.xml"; // 扫描 resources/xml/ 下所有 XML

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        // 1. 资源扫描器:自动发现指定目录的 XML 文件
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(registry); // 绑定主容器的注册中心

        try {
            // 2. 扫描所有符合规则的 XML 文件(无需硬编码具体文件名)
            Resource[] resources = resolver.getResources(XML_SCAN_PATTERN);
            if (resources.length == 0) {
                System.out.println("未找到需加载的 XML 配置文件");
                return;
            }

            // 3. 读取 XML 并注册 BeanDefinition(主容器会统一刷新,无需手动 refresh)
            reader.loadBeanDefinitions(resources);
            System.out.println("成功加载 " + resources.length + " 个 XML 配置文件:");
            for (Resource resource : resources) {
                System.out.println("- " + resource.getFilename());
            }

        } catch (IOException e) {
            throw new RuntimeException("扫描 XML 配置文件失败", e);
        }
    }

    // 父接口默认方法,无需实现
    @Override
    public void postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory beanFactory) throws BeansException {
    }
}
2. 配置 XML 文件存放目录(约定优于配置)

将所有自定义 XML 文件(如 my-beans.xml)放到 src/main/resources/xml/ 目录下(与 XML_SCAN_PATTERN 对应),无需在代码中指定具体文件名 ------Spring 会自动扫描该目录下所有 .xml 文件。

3. 移除原有的硬编码配置

删除启动类上的 @ImportResource 注解(无需手动指定 XML 文件),删除之前的 XmlScanConfig(避免重复操作)。

4. 测试验证

运行之前的测试类,依然能正常获取 user1user2 Bean,且控制台会打印加载的 XML 文件名(如 my-beans.xml),无 refresh() 操作。

方案一的优势

  • 无硬编码:支持批量扫描目录,新增 XML 文件无需修改代码;

  • 无刷新风险 :复用主容器的 BeanDefinitionRegistry,由 Spring 统一刷新容器;

  • 可配置化 :可将 XML_SCAN_PATTERN 抽到 application.yml 中,更灵活:

    yaml

    复制代码
    # application.yml
    xml:
      scan-pattern: classpath:xml/*.xml # 可动态调整扫描路径

    代码中读取配置:

    java

    运行

    复制代码
    @Value("${xml.scan-pattern}")
    private String xmlScanPattern;
    
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        Resource[] resources = resolver.getResources(xmlScanPattern); // 使用配置的路径
        // ... 其余逻辑不变
    }

方案二:自定义 ImportSelector(动态导入 XML 资源)

ImportSelector 是 Spring 注解驱动的扩展点,可动态返回需要导入的资源路径(如 XML 文件),支持扫描目录后动态导入,同样无需手动 refresh()

实现步骤

1. 自定义 XML 资源选择器

java

运行

复制代码
// com.example.demo.config.XmlResourceImportSelector.java
package com.example.demo.config;

import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.AnnotationMetadata;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
 * 动态扫描 XML 文件,返回资源路径给 Spring 导入
 */
public class XmlResourceImportSelector implements ImportSelector {

    private static final String XML_SCAN_PATTERN = "classpath:xml/*.xml";

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        List<String> resourcePaths = new ArrayList<>();

        try {
            Resource[] resources = resolver.getResources(XML_SCAN_PATTERN);
            for (Resource resource : resources) {
                // 转换为 Spring 可识别的资源路径(如 classpath:xml/my-beans.xml)
                resourcePaths.add("classpath:xml/" + resource.getFilename());
            }
            System.out.println("动态导入 " + resourcePaths.size() + " 个 XML 文件:" + resourcePaths);
            return resourcePaths.toArray(new String[0]);

        } catch (IOException e) {
            throw new RuntimeException("扫描 XML 资源失败", e);
        }
    }
}
2. 启动类上导入选择器

java

运行

复制代码
// com.example.demo.DemoApplication.java
package com.example.demo;

import com.example.demo.config.XmlResourceImportSelector;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

// 导入自定义选择器,动态加载 XML 文件
@Import(XmlResourceImportSelector.class)
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
3. 效果与方案一一致

Spring 会通过 XmlResourceImportSelector 返回的路径,自动加载 XML 文件,无需手动 refresh(),支持批量扫描目录。

方案二的优势

  • 更贴合 Spring 注解驱动的风格,代码侵入性低;
  • 无需实现复杂接口,仅需返回资源路径即可;
  • 同样支持配置化扫描路径(读取 application.yml 逻辑与方案一一致)。

为什么这两种方案没有风险?

  1. 不重复刷新容器 :两种方案都复用 Spring 主容器的初始化流程,BeanDefinition 注册完成后,由 Spring 统一执行 refresh()(仅执行一次),避免手动 refresh() 导致的 Bean 重复创建、生命周期异常等问题;
  2. 自动扫描无硬编码:基于目录扫描,新增 / 删除 XML 文件无需修改代码,符合 "约定优于配置";
  3. 兼容自定义 XML 解析器 :之前实现的 MyNamespaceHandlerUserBeanDefinitionParser 完全复用,无需修改 ------Spring 会自动通过 spring.handlers 识别自定义标签。

最终推荐方案

优先选择 方案一(BeanDefinitionRegistryPostProcessor),原因:

  • 功能更灵活:可在注册 BeanDefinition 前后添加自定义逻辑(如日志、属性校验);
  • 兼容性更好:支持复杂 XML 解析场景(如嵌套标签、条件注册);
  • 配置更直观:扫描路径可直接绑定配置文件,便于运维调整。
相关推荐
ZouZou老师2 小时前
C++设计模式之适配器模式:以家具生产为例
java·设计模式·适配器模式
曼巴UE52 小时前
UE5 C++ 动态多播
java·开发语言
VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue音乐管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
程序员鱼皮3 小时前
刚刚,IDEA 免费版发布!终于不用破解了
java·程序员·jetbrains
Hui Baby3 小时前
Nacos容灾俩种方案对比
java
曲莫终3 小时前
Java单元测试框架Junit5用法一览
java
成富4 小时前
Chat Agent UI,类似 ChatGPT 的聊天界面,Spring AI 应用的测试工具
java·人工智能·spring·ui·chatgpt
凌波粒4 小时前
Springboot基础教程(9)--Swagger2
java·spring boot·后端
2301_800256114 小时前
【第九章知识点总结1】9.1 Motivation and use cases 9.2 Conceptual model
java·前端·数据库