本文以“项目报告与附件 XML 生成工具”为主题,分享如何在生产环境中使用 JAXB 与 Lombok 快速构建 Java 对象模型,并将后台业务数据转换成层次分明、易于扩展的 XML 文档。
为什么要这样做?
可读性:层次分明,任何人一眼就能找到“报告编号”“附件列表”“每个附件的属性”。
可扩展性:未来要加“上传人”“审核状态”……只需在模型里多写几个字段即可。
兼容性:XML 是标准,大部分系统都能轻松解析与交互。
一、起因与需求
在大型后端系统中,往往存在“报表+附件”或“档案+文档”这样的场景,需要把数据库或服务返回的文件树结构导出成标准 XML,供上游系统或第三方存储解析。
业务侧数据:List
目标 XML:
二、模型设计
借助 JAXB(Java XML Binding)进行序列化,利用 Lombok 省略样板代码,使模型定义简洁明了。
XML对象:ReportXml
@XmlRootElement(name = "report")
@XmlAccessorType(XmlAccessType.FIELD)
@Data
public class ReportXml {
/** 报表标题,对应根节点的 title 属性 */
@XmlAttribute(name = "title")
private String title;
/** 报告编号节点 */
@XmlElement(name = "ReportID")
private ReportID reportID;
/** 附件容器节点 */
@XmlElement(name = "Attachments")
private Attachments attachments;
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public static class ReportID {
@XmlAttribute(name = "title")
private String title; // 如 "报告编号"
@XmlValue
private String value; // 编号内容
}
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public static class Attachments {
@XmlAttribute(name = "title")
private String title; // 如 "所有附件"
@XmlElement(name = "Category")
private List
}
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public static class Category {
@XmlAttribute(name = "title")
private String title; // 分类名称
@XmlElement(name = "Attachment")
private List
}
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public static class Attachment {
@XmlAttribute(name = "title")
private String title; // 文件名(带扩展名)
@XmlElement(name = "SEQ")
private TagWithTitle seq; // 序号
@XmlElement(name = "PATH")
private TagWithTitle path; // 存储路径
@XmlElement(name = "FORMAT")
private TagWithTitle format; // 格式
@XmlElement(name = "NAME")
private TagWithTitle name; // 文件名(不含扩展名)
@XmlElement(name = "SIZE")
private TagWithTitle size; // 文件大小
@XmlElement(name = "CREATED")
private TagWithTitle created; // 创建时间
}
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public static class TagWithTitle {
@XmlAttribute(name = "title")
private String title; // 节点描述,如“序号”“存储路径”
@XmlValue
private String value; // 节点值
}
}
文件对象:FileInfo
/**
* 业务层通用文件信息对象
*/
@Data
public class FileInfo {
/**
* 序号
*/
private int pieceNo;
/**
* 文件在磁盘或存储服务上的完整路径
*/
private String filePath;
/**
* 文件格式(如 "PDF"、"JPG" 等)
*/
private String format;
/**
* 计算机端不带扩展名的文件名
*/
private String computerFileName;
/**
* 带单位的文件大小字符串(如 "2.4MB")
*/
private String size;
/**
* 文件创建或生成时间字符串(格式 "yyyy-MM-dd HH:mm:ss")
*/
private String createTime;
/**
* 原始文件名(带扩展名)
*/
private String originalFilename;
}
三、核心代码
/**
* 生成报告 XML 文件
*
* @param fileTreeList 项目文件树列表
* @param outFilePath 输出文件路径
* @param reportId 报告编号
* @return 文件路径
*/
private static String createReportXmlFile(List
// 1. 根与编号
ReportXml xml = new ReportXml();
xml.setTitle("项目报告与附件");
ReportXml.ReportID rid = new ReportXml.ReportID();
rid.setTitle("报告编号");
rid.setValue(reportId);
xml.setReportID(rid);
// 2. 附件容器
ReportXml.Attachments atts = new ReportXml.Attachments();
atts.setTitle("所有附件");
// 3. 分类和附件
List
for (Map
ReportXml.Category cat = new ReportXml.Category();
cat.setTitle((String) catMap.get("name"));
List
List
int seq = 1;
for (FileInfo info : files) {
ReportXml.Attachment att = new ReportXml.Attachment();
att.setTitle(info.getOriginalFilename());
att.setSeq(newTag("序号", String.valueOf(seq++)));
att.setPath(newTag("存储路径", info.getFilePath()));
String fmt = info.getFilePath() .substring( info.getFilePath().lastIndexOf('.') + 1);
att.setFormat(newTag("格式", fmt.toUpperCase()));
String nameOnly = info.getOriginalFilename() .replace("." + fmt, "");
att.setName(newTag("文件名", nameOnly));
att.setSize(newTag("文件大小", info.getSize()));
att.setCreated(newTag("创建时间", info.getCreateTime()));
atList.add(att);
}
cat.setAttachments(atList);
categories.add(cat);
}
atts.setCategories(categories);
xml.setAttachments(atts);
// 4. 写文件
try {
JAXBContext ctx = JAXBContext.newInstance(ReportXml.class);
Marshaller m = ctx.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
File out = new File(outFilePath);
if (out.getParentFile() != null) out.getParentFile().mkdirs();
m.marshal(xml, out);
return out.getAbsolutePath();
} catch (Exception e) {
throw new RuntimeException("生成报告 XML 失败", e);
}
}
/**
* 快速构造带 title 的节点
*
* @param title 节点标题
* @param value 节点值
* @return null
*/
private static ReportXml.TagWithTitle newTag(String title, String value) {
ReportXml.TagWithTitle tag = new ReportXml.TagWithTitle();
tag.setTitle(title);
tag.setValue(value);
return tag;
}
四、测试与运行案例
XmlUtil工具类
public class XmlUtil {
public static void main(String[] args) {
// 1. 准备测试数据:两类分类,每类下若干附件
List
// 分类一:项目评审材料
Map
category1.put("name", "项目评审材料");
List
list1.add(createInfo("设计方案.pdf", "/data/reports/设计方案.pdf", 1));
list1.add(createInfo("预算表.xlsx", "/data/reports/预算表.xlsx", 2));
category1.put("fileList", list1);
// 分类二:审批文件
Map
category2.put("name", "审批文件");
List
list2.add(createInfo("审批意见.docx", "/data/reports/审批意见.docx", 1));
category2.put("fileList", list2);
fileTreeList.add(category1);
fileTreeList.add(category2);
// 2. 指定输出路径和报告编号
String outPath = "output/report_with_attachments.xml";
String reportId = "RPT-20250509-001";
// 3. 调用生成方法
String generated = createReportXmlFile(fileTreeList, outPath, reportId);
System.out.println("XML 已生成,路径:" + generated);
}
/**
* 快速创建FileInfo 对象
*/
private static FileInfo createInfo(String originalFilename, String path, int seq) {
FileInfo info = new FileInfo();
info.setOriginalFilename(originalFilename);
info.setFilePath(path);
// 自动根据路径提取格式
String fmt = path.substring(path.lastIndexOf('.') + 1).toUpperCase();
info.setFormat(fmt);
// 文件名(无扩展名)
info.setComputerFileName(originalFilename.replace("." + fmt, ""));
// 模拟大小
info.setSize(seq * 1.5 + "MB");
// 当前时间
String now = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") .format(new Date());
info.setCreateTime(now);
info.setPieceNo(seq);
return info;
}
}
生成样例