基于 JAXB 注解方式解析 XML 文件与批量导入实现
本文以实际的项目需求为例,分享如何基于 JAXB 实现 XML 文件的解析,并将解析后的数据导入数据库
本文主要使用的 javax.xml
是 Java 自带的包,通常用来处理 XML 和 Java 对象之间的转换。具体来说,这些类属于 JAXB(Java Architecture for XML Binding),用于将 Java 对象序列化为 XML,或者将 XML 反序列化为 Java 对象。
需要注意:在 Java 9 及以后,javax.xml.bind
被迁移到了模块化系统,并且从 JDK 11 开始 移除了 默认支持。如果你使用的 JDK 是 11 或以上版本,需要通过添加外部依赖来使用这些类,比如引入 javax.xml.bind
的实现库,例如:
Maven 依赖:
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
或者将 com.sun.xml.bind
实现的库加入项目中:
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.1</version>
</dependency>
如果使用的是 JDK 8,则这些类是默认包含在标准库中的,无需额外配置
一、XML 文件格式及对应的 JavaBean 设计
假设有以下 XML 文件内容:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<Record>
<RecordID>2921725428</RecordID>
<RecordLastUpdateDate>20240204173652</RecordLastUpdateDate>
<RecordTitle>String类型</RecordTitle>
<Publication>
<PublicationID>2026366</PublicationID>
<Title>出版社题名</Title>
</Publication>
<AlphaPubDate>1976</AlphaPubDate>
<NumericPubDate>19760101</NumericPubDate>
<Contributor>
<ContribRole>Author</ContribRole>
<LastName>József</LastName>
<FirstName>Terjéki</FirstName>
<OriginalForm>József, Terjéki</OriginalForm>
</Contributor>
<Contributor>
<ContribRole>Advisor</ContribRole>
<LastName>Lajos</LastName>
<FirstName>Pintér</FirstName>
<OriginalForm>Lajos, Pintér</OriginalForm>
</Contributor>
<Contributor>
<ContribRole>Advisor</ContribRole>
<LastName>László</LastName>
<FirstName>Hatvani</FirstName>
<OriginalForm>László, Hatvani</OriginalForm>
</Contributor>
<Products>
<Product>
<ProductID>1006523</ProductID>
<HasFullText>true</HasFullText>
</Product>
<Product>
<ProductID>1007587</ProductID>
<HasFullText>false</HasFullText>
</Product>
<Product>
<ProductID>1007945</ProductID>
<HasFullText>true</HasFullText>
</Product>
</Products>
<Terms>
<ClassTerm>
<ClassTermType>DissSubject</ClassTermType>
<ClassCode>0642</ClassCode>
<ClassExpansion>Theoretical Mathematics</ClassExpansion>
</ClassTerm>
<ClassTerm>
<ClassTermType>DissSubject</ClassTermType>
<ClassCode>0405</ClassCode>
<ClassExpansion>Mathematics</ClassExpansion>
</ClassTerm>
<GenSubjTerm>
<GenSubjValue>Theoretical mathematics</GenSubjValue>
</GenSubjTerm>
<GenSubjTerm>
<GenSubjValue>Applied mathematics</GenSubjValue>
</GenSubjTerm>
<FlexTerm>
<FlexTermName>DissPaperKwd</FlexTermName>
<FlexTermValue>Differential equation</FlexTermValue>
</FlexTerm>
<FlexTerm>
<FlexTermName>DissPaperKwd</FlexTermName>
<FlexTermValue>Non-equilibrium function </FlexTermValue>
</FlexTerm>
</Terms>
</Record>
1. Record
类
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.cxstar.test.LocalDateAdapter;
import com.cxstar.test.LocalDateTimeAdapter;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.xml.bind.annotation.*;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Data
@XmlRootElement(name = "Record") // 定义 XML 根元素的名称为 <Record>
@XmlAccessorType(XmlAccessType.FIELD) // 指定通过字段映射到 XML
public class Record {
/**
* 映射为 XML 节点 <RecordID>
* 示例:
* <RecordID>2921725428</RecordID>
*/
@XmlElement(name = "RecordID")
private Long recordId;
/**
* 映射为 XML 节点 <RecordLastUpdateDate>
* 使用 LocalDateTimeAdapter 自定义适配器处理 LocalDateTime 类型
* 示例:
* <RecordLastUpdateDate>20240204173652</RecordLastUpdateDate>
*/
@XmlElement(name = "RecordLastUpdateDate")
@XmlJavaTypeAdapter(LocalDateTimeAdapter.class)
private LocalDateTime recordLastUpdateDate;
/**
* 映射为 XML 节点 <RecordTitle>
* 示例:
* <RecordTitle>String类型</RecordTitle>
*/
@XmlElement(name = "RecordTitle")
private String recordTitle;
/**
* 映射为 XML 节点 <AlphaPubDate>
* 示例:
* <AlphaPubDate>1976</AlphaPubDate>
*/
@XmlElement(name = "AlphaPubDate")
private Integer alphaPubDate;
/**
* 映射为 XML 节点 <NumericPubDate>
* 使用 LocalDateAdapter 自定义适配器处理 LocalDate 类型
* 示例:
* <NumericPubDate>19760101</NumericPubDate>
*/
@XmlElement(name = "NumericPubDate")
@XmlJavaTypeAdapter(LocalDateAdapter.class)
private LocalDate numericPubDate;
/**
* 映射为 XML 节点 <Publication>
* 该字段对应 RecordPublicationBean 类型
* 示例:
* <Publication>
* <PublicationID>2026366</PublicationID>
* <Title>出版社题名</Title>
* </Publication>
*/
@XmlElement(name = "Publication")
private RecordPublicationBean publications;
/**
* 映射为多个 XML 节点 <Contributor>
* 示例:
* <Contributor>
* <ContribRole>Author</ContribRole>
* <LastName>József</LastName>
* <FirstName>Terjéki</FirstName>
* <OriginalForm>József, Terjéki</OriginalForm>
* </Contributor>
* <Contributor>
* <ContribRole>Advisor</ContribRole>
* <LastName>Lajos</LastName>
* <FirstName>Pintér</FirstName>
* <OriginalForm>Lajos, Pintér</OriginalForm>
* </Contributor>
*/
@XmlElement(name = "Contributor")
private List<RecordContributorBean> contributors;
/**
* 映射为 XML 节点 <Products>,并包含多个 <Product> 子节点
* 示例:
* <Products>
* <Product>
* <ProductID>1006523</ProductID>
* <HasFullText>true</HasFullText>
* </Product>
* <Product>
* <ProductID>1007587</ProductID>
* <HasFullText>false</HasFullText>
* </Product>
* </Products>
*/
@XmlElementWrapper(name = "Products")
@XmlElement(name = "Product") // 必须指定子节点的名称为 <Product>
private List<RecordProductBean> products;
/**
* 映射为 XML 节点 <Terms>,包含多种类型的子节点
* 子节点类型: <ClassTerm>, <GenSubjTerm>, <FlexTerm>
* 示例:
* <Terms>
* <ClassTerm>
* <ClassTermType>DissSubject</ClassTermType>
* <ClassCode>0642</ClassCode>
* <ClassExpansion>Theoretical Mathematics</ClassExpansion>
* </ClassTerm>
* <GenSubjTerm>
* <GenSubjValue>Theoretical mathematics</GenSubjValue>
* </GenSubjTerm>
* <FlexTerm>
* <FlexTermName>DissPaperKwd</FlexTermName>
* <FlexTermValue>Differential equation</FlexTermValue>
* </FlexTerm>
* </Terms>
*/
@TableField(exist = false) // MyBatis-Plus 标注,该字段不对应数据库表字段
@XmlElementWrapper(name = "Terms")
@XmlElements({
@XmlElement(name = "ClassTerm", type = RecordTermsBean.class), // 映射为 <ClassTerm>
@XmlElement(name = "GenSubjTerm", type = RecordTermsBean.class), // 映射为 <GenSubjTerm>
@XmlElement(name = "FlexTerm", type = RecordTermsBean.class) // 映射为 <FlexTerm>
})
private List<RecordTermsBean> terms;
}
1.1 类级注解
- @XmlRootElement(name = “Record”)
定义了当前类在生成 XML 文档时的根元素名称为Record
,即生成的 XML 中的最外层节点将被命名为<Record>
- @XmlAccessorType(XmlAccessType.FIELD)
指定了将类中的字段(field
)直接映射为 XML 的节点或属性,而不是使用类的方法(如 getter 和 setter)
1.2 字段级注解
-
@XmlElement(name = “RecordID”)
将字段recordId
映射为 XML 节点<RecordID>
-
@XmlJavaTypeAdapter(LocalDateTimeAdapter.class)
指定在处理recordLastUpdateDate
字段时使用自定义适配器LocalDateTimeAdapter
,用于在序列化和反序列化 XML 时格式化LocalDateTime
类型示例适配器
LocalDateTimeAdapter
package com.test.util; import javax.xml.bind.annotation.adapters.XmlAdapter; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; public class LocalDateTimeAdapter extends XmlAdapter<String, LocalDateTime> { private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); @Override public LocalDateTime unmarshal(String v) throws Exception { return (v == null || v.isEmpty()) ? null : LocalDateTime.parse(v, formatter); } @Override public String marshal(LocalDateTime v) throws Exception { return (v == null) ? null : v.format(formatter); } }
-
@XmlElementWrapper
用于将一个集合字段包裹在外层节点内,其功能是为集合字段生成一个外层的包装节点
例如,当products
是一个集合时:@XmlElementWrapper(name = "Products") @XmlElement(name = "Product") private List<RecordProductBean> products;
- @XmlElementWrapper(name = “Products”):生成一个
<Products>
包装节点作为集合的外层。 - @XmlElement(name = “Product”):指定集合中的每个元素用
<Product>
节点表示。
示例 XML
<Products> <Product> <ProductID>1006523</ProductID> <HasFullText>true</HasFullText> </Product> <Product> <ProductID>1007587</ProductID> <HasFullText>false</HasFullText> </Product> </Products>
适用场景
在 XML 表现中,集合字段需要一个外层节点包裹时,使用
@XmlElementWrapper
比直接使用@XmlElement
更符合语义,且更易于生成符合要求的 XML - @XmlElementWrapper(name = “Products”):生成一个
-
@XmlElements
当集合中的元素有多种类型时,@XmlElements
可以指定每种类型的对应节点名称。例如:@XmlElementWrapper(name = "Terms") @XmlElements({ @XmlElement(name = "ClassTerm", type = RecordTermsBean.class), @XmlElement(name = "GenSubjTerm", type = RecordTermsBean.class), @XmlElement(name = "FlexTerm", type = RecordTermsBean.class) }) private List<RecordTermsBean> terms;
- @XmlElement(name = “ClassTerm”, type = RecordTermsBean.class):为
RecordTermsBean
类型的元素生成<ClassTerm>
节点 - @XmlElementWrapper(name = “Terms”):将集合包装在
<Terms>
节点内
示例 XML
<Terms> <ClassTerm> <ClassTermType>DissSubject</ClassTermType> <ClassCode>0642</ClassCode> <ClassExpansion>Theoretical Mathematics</ClassExpansion> </ClassTerm> <GenSubjTerm> <GenSubjValue>Theoretical mathematics</GenSubjValue> </GenSubjTerm> </Terms>
- @XmlElement(name = “ClassTerm”, type = RecordTermsBean.class):为
2. 子实体类设计
RecordPublicationBean
:表示出版物信息RecordContributorBean
:表示贡献者信息RecordProductBean
:表示产品信息RecordTermsBean
:表示记录术语信息
示例:
package com.test.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
@Data
@XmlRootElement(name = "Publication")
@XmlAccessorType(XmlAccessType.FIELD)
public class RecordPublicatio implements com.cxstar.common.entity.Bean {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
private Long id;
/**
* 关联的记录ID
*/
private Long recordId;
/**
* 出版物ID
*/
@XmlElement(name = "PublicationID")
private Long publicationId;
/**
* 出版物标题
*/
@XmlElement(name = "Title")
private String title;
}
二、基于 JAXB 的 XML 解析工具
为简化 XML 字符串与 Java 对象的转换,创建了一个通用的 XML 工具类 XmlUtil
:
package com.test.util;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import java.io.StringReader;
/**
* @author zhouquan
* @date 2024年11月27日 14:07
*/
public class XmlUtil {
/**
* 将 XML 字符串转换为指定的 POJO 对象
*
* @param clazz 需要转换的类
* @param xmlStr XML 数据
* @return 转换后的对象
* @throws JAXBException 如果 XML 解析失败
*/
public static <T> T xmlStrToObject(Class<T> clazz, String xmlStr) throws JAXBException {
// 校验输入参数
if (clazz == null || xmlStr == null || xmlStr.trim().isEmpty()) {
throw new IllegalArgumentException("输入的类或 XML 字符串不能为空");
}
// 创建 JAXB 上下文和反序列化器
JAXBContext context = JAXBContext.newInstance(clazz);
Unmarshaller unmarshaller = context.createUnmarshaller();
// 使用 try-with-resources 自动关闭资源
try (StringReader reader = new StringReader(xmlStr)) {
T result = (T) unmarshaller.unmarshal(reader);
return result;
}
}
}
三、批量导入 XML 文件的实现
以下为批量解析并导入 XML 文件的核心方法:
@Override
public String importBatchXML(MultipartFile[] files) {
if (files == null || files.length == 0) {
log.warn("上传的文件列表为空");
return "上传文件为空,请重新上传";
}
int successCount = 0;
int totalFiles = files.length;
for (MultipartFile file : files) {
String fileName = file.getOriginalFilename();
// 校验文件是否为 XML 格式
if (fileName == null || !fileName.toLowerCase().endsWith(".xml")) {
log.warn("文件 {} 不是有效的 XML 文件,已跳过", fileName);
continue;
}
try (BufferedReader br = new BufferedReader(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8))) {
// 读取 XML 文件内容
StringBuilder buffer = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
buffer.append(line);
}
// XML 转换为 Java 对象
Record record = XmlUtil.xmlStrToObject(Record.class, buffer.toString());
// 保存到数据库
recordService.save(recordBean);
successCount++;
} catch (Exception e) {
log.error("解析文件 {} 失败", fileName, e);
}
}
return String.format("总文件数:%d,成功导入数:%d", totalFiles, successCount);
}