首页
关于这个博客
Search
1
Java 实现Google 账号单点登录(OAuth 2.0)全流程解析
1,428 阅读
2
Spring AI 无法获取大模型深度思考内容?解决方案来了
525 阅读
3
EasyExcel 实战:导出带图片的 Excel 完整方案
283 阅读
4
服务器遭遇 XMRig 挖矿程序入侵排查与清理全记录
266 阅读
5
微信小程序实现页面返回前确认弹窗:兼容左上角返回与右滑返回
219 阅读
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
登录
Search
标签搜索
Spring AI
java虚拟机
JVM
Spring AI Alibaba
Java
保姆级教程
SpringBoot
Spring
WebFlux
MCP
大模型
Agent Skills
Nginx
Agent
Ubuntu
Mysql
Apache POI
自定义starter
Mybatis
响应式编程
Luca Ju
累计撰写
51
篇文章
累计收到
2
条评论
首页
栏目
Java 核心
框架与中间件
数据库技术
开发工具与效率
问题排查与踩坑记录
程序员成长与思考
前端
页面
关于这个博客
搜索到
51
篇与
的结果
2026-01-13
玩转 MyBatis-Plus 多数据源配置,Spring Boot 项目快速实现多库操作
在实际开发中,我们经常会遇到一个 Spring Boot 项目需要操作多个数据库的场景,比如从 Oracle 拉取数据同步到 MySQL。如果手动管理多个数据源的连接和切换,不仅开发效率低,还容易出现数据源混淆的问题。而 MyBatis-Plus 提供的 dynamic-datasource-spring-boot-starter 依赖,能够帮助我们快速实现多数据源的配置和动态切换,极大简化多库操作的开发流程。本文就来详细讲解 Spring Boot 整合 MyBatis-Plus 多数据源的具体实现步骤。一、核心依赖引入首先,我们需要在项目的 pom.xml 文件中引入 MyBatis-Plus 多数据源的核心依赖。这里以 3.1.0 版本为例(可根据项目实际情况选择兼容版本):<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>4.2.0</version> </dependency>注意:如果项目中已经引入了 MyBatis-Plus 的核心依赖,无需重复引入,该 starter 已包含相关依赖。二、多数据源配置(application.yml)接下来,在 Spring Boot 的核心配置文件 application.yml 中配置多个数据源的连接信息。本文以 1 个 MySQL 数据源 + 2 个 Oracle 数据源为例,配置如下:spring: datasource: dynamic: # 设置默认的数据源,默认数据源的 key 需与下方 datasource 中的配置一致 primary: mysql-ocr # 严格模式:默认 true,未匹配到指定数据源时抛异常,false 则使用默认数据源 strict: true # 配置多个数据源,key 自定义(建议与业务相关,便于区分) datasource: # MySQL 数据源:ocr 业务库 mysql-ocr: url: jdbc:mysql://ip:port/ocr?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: username password: password driver-class-name: com.mysql.cj.jdbc.Driver # Oracle 数据源1:orcl 业务库 oracle-orcl: url: jdbc:oracle:thin:@ip:port:helowin username: username password: password driver-class-name: oracle.jdbc.OracleDriver # Oracle 数据源2:cbxx 业务库 oracle-cbxx: url: jdbc:oracle:thin:@ip:port:helowin username: username password: password driver-class-name: oracle.jdbc.OracleDriver配置参数说明参数作用primary指定默认数据源,当不指定数据源时,默认使用该配置的数据源strict严格模式开关,开启后若调用不存在的数据源会抛出异常,关闭则默认使用主数据源datasource多数据源的具体配置节点,每个子节点对应一个数据源,key 为自定义的数据源名称三、Mapper 层目录结构与数据源注解配置为了更清晰地管理不同数据源对应的 Mapper 接口,我们可以按照数据源划分包结构,同时通过 @DS 注解指定 Mapper 对应的数据源。1. Mapper 层目录结构推荐按照数据源名称创建独立的包,将不同数据源的 Mapper 接口分类存放,便于后期维护:src/main/java/com/xxx/mapper ├── cbxx // oracle-cbxx 数据源对应的 Mapper 包 │ └── VOcrCbxxMapper.java ├── ocr // mysql-ocr 数据源对应的 Mapper 包 │ └── OcrCmMapper.java └── orcl // oracle-orcl 数据源对应的 Mapper 包 └── VOcrMeterMapper.java2. @DS 注解指定数据源@DS 注解是 MyBatis-Plus 多数据源的核心注解,用于指定当前 Mapper 接口或方法对应的数据源,支持类级别和方法级别,遵循就近原则(方法上的注解优先级高于类上的注解)。在 Mapper 接口上添加 @DS 注解,指定该接口下所有方法都使用对应的数据源,也可以在方法上单独添加 @DS 注解,优先级更高:import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.dynamic.datasource.annotation.DS; import com.xxx.entity.VOcrCbxx; import org.springframework.stereotype.Repository; /** * oracle-cbxx 数据源对应的 Mapper 接口 * @DS 注解指定数据源名称,与 application.yml 中配置的 key 一致 */ @DS("oracle-cbxx") @Mapper public interface VOcrCbxxMapper extends BaseMapper<VOcrCbxx> { /** * 自定义查询方法:根据日期查询数据 * 该方法默认使用类上指定的 oracle-cbxx 数据源 */ default List<VOcrCbxx> selectByBqcbr(String date) { QueryWrapper<VOcrCbxx> wrapper = new QueryWrapper<>(); // 适配 Oracle 日期函数,匹配年月日 wrapper.apply("TRUNC(bqcbr) = TO_DATE({0}, 'YYYY-MM-DD')", date); return selectList(wrapper); } /** * 自定义方法:使用 oracle-orcl 数据源查询 * 方法级别 @DS 注解优先级高于类级别 */ @DS("oracle-orcl") List<VOcrCbxx> selectFromOrclByCondition(String condition); }四、核心使用说明注解优先级:方法上的 @DS 注解 > 类上的 @DS 注解 > 全局默认数据源。事务支持:多数据源下的事务需要使用 @DSTransactional 注解(而非 Spring 原生的 @Transactional),该注解能保证同一数据源内的事务一致性;跨数据源事务需结合分布式事务方案(如 TCC)。避免数据源混用:建议严格按照包结构划分 Mapper,避免不同数据源的 Mapper 混杂,降低维护成本。动态切换数据源:除了通过 @DS 注解静态指定数据源,还可以通过 DynamicDataSourceContextHolder 类手动切换数据源,适用于动态选择数据源的业务场景:// 手动切换到 mysql-ocr 数据源 DynamicDataSourceContextHolder.push("mysql-ocr"); // 执行数据库操作 ocrCmMapper.selectById(1L); // 清空当前数据源上下文 DynamicDataSourceContextHolder.clear();五、总结通过 MyBatis-Plus 的 dynamic-datasource-spring-boot-starter,我们可以在 Spring Boot 项目中零侵入式地实现多数据源配置,核心步骤总结如下:引入多数据源核心依赖;在 application.yml 中配置多数据源连接信息,指定默认数据源;按数据源划分 Mapper 包结构,通过 @DS 注解指定 Mapper/方法对应的数据源;业务层直接注入 Mapper 接口使用,无需关心数据源切换细节。这种配置方式简洁高效,极大降低了多数据源开发的复杂度,非常适合需要操作多个异构数据库的业务场景。
2026年01月13日
38 阅读
0 评论
2 点赞
2025-11-21
EasyExcel 实战:导出带图片的 Excel 完整方案
在实际开发中,可能会遇到导出 Excel 时需要包含图片的场景。EasyExcel 作为阿里开源的高效 Excel 处理工具,虽然原生支持图片导出,但在图片格式适配、单元格样式调整等细节上需要自定义扩展。本文将详细讲解如何基于 EasyExcel 实现带图片的 Excel 导出,包含完整代码示例和关键细节说明。一、最终效果展示导出的 Excel 中,图片将自适应单元格大小,无图片时显示"无图"提示,整体格式整洁规范:二、实现步骤详解1. 引入依赖首先在 pom.xml 中添加 EasyExcel 核心依赖(本文使用 4.0.3 版本,适配大部分场景):<dependency> <groupId>com.alibaba</groupId> <artifactId>easyexcel</artifactId> <version>4.0.3</version> </dependency>2. 定义导出实体类创建 RecordExcelVO 作为 Excel 导出的数据载体,核心关注图片字段的配置:package com.retail.ocr.model.vo; import com.alibaba.excel.annotation.ExcelProperty; import com.alibaba.excel.annotation.write.style.ColumnWidth; import com.retail.ocr.excel.converter.ImageConverter; import lombok.Data; /** * Excel 导出实体(识别记录) * * @author jucunqi * @since 2025/11/20 */ @Data public class RecordExcelVO { @ColumnWidth(20) // 列宽设置 @ExcelProperty("手机号") // Excel 表头名称 private String phoneNumber; @ColumnWidth(25) @ExcelProperty("识别时间") private String createTime; @ColumnWidth(20) @ExcelProperty("识别读数") private String number1; @ColumnWidth(20) @ExcelProperty("识别条码") private String number2; // 图片字段:使用自定义转换器处理 byte[] 类型 @ExcelProperty(value = "识别照片", converter = ImageConverter.class) private byte[] imageBytes; @ColumnWidth(20) @ExcelProperty("标记内容") private String markDesc; @ColumnWidth(20) @ExcelProperty("备注") private String remark; }关键说明:图片字段类型为 byte[]:便于存储图片二进制数据(可从文件、数据库 BLOB 字段、网络图片转换获取)。@ExcelProperty(converter = ImageConverter.class):指定自定义转换器,将 byte[] 转换为 Excel 可识别的图片格式。@ColumnWidth:统一设置列宽,优化 Excel 显示效果。3. 实现图片转换器(核心)自定义 ImageConverter 实现 Converter<byte[]> 接口,负责将图片二进制数据转换为 EasyExcel 支持的 ImageData 格式:package com.retail.ocr.excel.converter; import com.alibaba.excel.converters.Converter; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.GlobalConfiguration; import com.alibaba.excel.metadata.data.ImageData; import com.alibaba.excel.metadata.data.ReadCellData; import com.alibaba.excel.metadata.data.WriteCellData; import com.alibaba.excel.metadata.property.ExcelContentProperty; import com.alibaba.excel.util.ListUtils; import lombok.extern.slf4j.Slf4j; import org.springframework.util.CollectionUtils; import java.util.List; @Slf4j public class ImageConverter implements Converter<byte[]> { @Override public Class<?> supportJavaTypeKey() { return List.class; } @Override public CellDataTypeEnum supportExcelTypeKey() { return CellDataTypeEnum.EMPTY; } @Override public byte[] convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { return null; } @Override public WriteCellData<?> convertToExcelData(byte[] value, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) throws Exception { // 这里进行对数据实体类的byte[]进行处理 List<ImageData> data = ListUtils.newArrayList(); ImageData imageData; try { imageData = new ImageData(); imageData.setImage(value); data.add(imageData); } catch (Exception e) { log.error("导出临时记录图片异常:", e); } WriteCellData<?> cellData = new WriteCellData<>(); if (!CollectionUtils.isEmpty(data)) { // 图片返回图片列表 cellData.setImageDataList(data); cellData.setType(CellDataTypeEnum.EMPTY); } else { // 没有图片使用汉字表示 cellData.setStringValue("无图"); cellData.setType(CellDataTypeEnum.STRING); } return cellData; } }核心逻辑:校验图片二进制数据有效性,避免空指针异常。有图片时:封装 ImageData 列表,指定 Excel 单元格类型为 IMAGE。无图片时:设置文本"无图",单元格类型为 STRING,提升用户体验。4. 自定义图片写入处理器由于 EasyExcel 原生图片导出不支持自适应单元格、多图片排列等需求,需自定义 CellWriteHandler 处理图片的位置、大小和样式:package com.retail.ocr.excel.handler; import com.alibaba.excel.enums.CellDataTypeEnum; import com.alibaba.excel.metadata.Head; import com.alibaba.excel.metadata.data.ImageData; import com.alibaba.excel.metadata.data.WriteCellData; import com.alibaba.excel.write.handler.CellWriteHandler; import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; import com.alibaba.excel.write.metadata.holder.WriteTableHolder; import org.apache.commons.collections4.CollectionUtils; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.*; import org.apache.poi.util.Units; import org.apache.poi.xssf.usermodel.XSSFDrawing; import org.apache.poi.xssf.usermodel.XSSFPicture; import org.apache.poi.xssf.usermodel.XSSFShape; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicReference; public class ImageWriteHandler implements CellWriteHandler { /** * 已经处理的Cell */ private final CopyOnWriteArrayList<String> REPEATS = new CopyOnWriteArrayList<>(); /** * 单元格的图片最大张数(每列的单元格图片张数不确定,单元格宽度需按照张数最多的长度来设置) */ private final AtomicReference<Integer> MAX_IMAGE_SIZE = new AtomicReference<>(0); /** * 标记手动添加的图片,用于排除EasyExcel自动添加的图片 */ private final CopyOnWriteArrayList<Integer> CREATE_PIC_INDEX = new CopyOnWriteArrayList<>(); @Override public void beforeCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Head head, Integer columnIndex, Integer relativeRowIndex, Boolean isHead) { } @Override public void afterCellCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { } @Override public void afterCellDataConverted(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, WriteCellData<?> cellData, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 在 数据转换成功后 不是头就把类型设置成空 if (isHead) { return; } // 将要插入图片的单元格的type设置为空,下面再填充图片 if (CollectionUtils.isNotEmpty(cellData.getImageDataList())) { cellData.setType(CellDataTypeEnum.EMPTY); } } @Override public void afterCellDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) { // 在 单元格写入完毕后 ,自己填充图片 if (isHead || CollectionUtils.isEmpty(cellDataList)) { return; } boolean listFlag = false; Sheet sheet = cell.getSheet(); // 此处为ExcelUrlConverterUtil的返回值 List<ImageData> imageDataList = cellDataList.get(0).getImageDataList(); if (CollectionUtils.isNotEmpty(imageDataList)) { listFlag = true; } if (!listFlag && imageDataList == null) { return; } String key = cell.getRowIndex() + "_" + cell.getColumnIndex(); if (REPEATS.contains(key)) { return; } REPEATS.add(key); if (imageDataList.size() > MAX_IMAGE_SIZE.get()) { MAX_IMAGE_SIZE.set(imageDataList.size()); } // 默认要导出的图片大小为60*60px,60px的行高大约是900,60px列宽大概是248*8 sheet.getRow(cell.getRowIndex()).setHeight((short) 900); sheet.setColumnWidth(cell.getColumnIndex(), listFlag ? 240 * 8 * MAX_IMAGE_SIZE.get() : 240 * 8); if (listFlag) { for (int i = 0; i < imageDataList.size(); i++) { ImageData imageData = imageDataList.get(i); if (imageData == null) { continue; } byte[] image = imageData.getImage(); this.insertImage(sheet, cell, image, i); } } else { this.insertImage(sheet, cell, imageDataList.get(0).getImage(), 0); } // 清除EasyExcel自动添加的没有格式的图片 XSSFDrawing drawingPatriarch = (XSSFDrawing) sheet.getDrawingPatriarch(); List<XSSFShape> shapes = drawingPatriarch.getShapes(); for (int i = 0; i < shapes.size(); i++) { XSSFShape shape = shapes.get(i); if (shape instanceof XSSFPicture && !CREATE_PIC_INDEX.contains(i)) { CREATE_PIC_INDEX.add(i); XSSFPicture picture = (XSSFPicture) shape; picture.resize(0); } } } /** * 重新插入一个图片 * * @param sheet Excel页面 * @param cell 表格元素 * @param pictureData 图片数据 * @param i 图片顺序 */ private void insertImage(Sheet sheet, Cell cell, byte[] pictureData, int i) { int picWidth = Units.pixelToEMU(60); int index = sheet.getWorkbook().addPicture(pictureData, HSSFWorkbook.PICTURE_TYPE_PNG); CREATE_PIC_INDEX.add(index); Drawing<?> drawing = sheet.getDrawingPatriarch(); if (drawing == null) { drawing = sheet.createDrawingPatriarch(); } CreationHelper helper = sheet.getWorkbook().getCreationHelper(); ClientAnchor anchor = helper.createClientAnchor(); // 设置图片坐标 anchor.setDx1(picWidth * i); anchor.setDx2(picWidth + picWidth * i); anchor.setDy1(0); anchor.setDy2(0); // 设置图片位置 int columnIndex = cell.getColumnIndex(); anchor.setCol1(columnIndex); anchor.setCol2(columnIndex); int rowIndex = cell.getRowIndex(); anchor.setRow1(rowIndex); anchor.setRow2(rowIndex + 1); // ClientAnchor.AnchorType里有多种类型可选,从网上看的之前是不移动,直接悬浮在单元格上了,现在这个是随着单元格移动 anchor.setAnchorType(ClientAnchor.AnchorType.MOVE_AND_RESIZE); drawing.createPicture(anchor, index); } }核心功能:自适应行列宽:根据图片数量动态调整列宽,固定行高适配 60*60px 图片。多图片横向排列:支持单元格内多图片横向显示,间距 10px,避免重叠。图片随单元格移动:设置锚点类型为 MOVE_AND_RESIZE,图片会随单元格位置变化而同步移动。5. 业务层导出实现最后在 Controller 中编写导出接口,整合上述组件完成 Excel 导出:@GetMapping("/recognizeRecordExport") public void recognizeRecordExport(RecognizeRecordPageReqVo reqVo, HttpServletResponse response) throws IOException { // 获取数据逻辑省略 List<RecordExcelVO> excelVOList = getRecordExcelVOS(reqVo); String filename = "识别数据.xlsx"; // 输出 Excel EasyExcel.write(response.getOutputStream(), RecordExcelVO.class) .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 .registerWriteHandler(new ImageWriteHandler()) .sheet(filename).doWrite(excelVOList); // 设置 header 和 contentType。写在最后的原因是,避免报错时,响应 contentType 已经被修改了 response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(filename)); response.setContentType("application/vnd.ms-excel;charset=UTF-8"); }关键注意事项:中文文件名乱码:使用 URLEncoder.encode 编码文件名,并指定 filename*=UTF-8'' 格式,兼容主流浏览器。响应头设置:contentType 对于 xlsx 格式需设置为 application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,xls 格式需改为 application/vnd.ms-excel。流关闭策略:设置 autoCloseStream(false),避免 EasyExcel 提前关闭响应流导致下载失败。以上就是使用EasyExcel导出带图片文件的所有内容了,希望对大家有所帮助~
2025年11月21日
283 阅读
0 评论
3 点赞
2025-11-17
Java 高效替换 Word 文档内容(模板填充方案)
使用java操作Excel大家应该非常熟悉,但是操作word可能会稍微少一点。本文将提供一套基于 Apache POI 的完整解决方案,支持普通段落+表格单元格的占位符替换,逻辑简洁、可直接复用。一、核心思路模板设计:在 Word 文档中,用 {{占位符名}} 标记需要替换的内容(如 {{name}}、{{date}});技术选型:使用 Apache POI(poi-ooxml)解析 .docx 文档,遍历段落和表格,替换占位符;核心优势:无需依赖第三方付费组件,支持复杂文档结构,兼容主流 Word 版本。二、实现步骤1. 环境准备(添加 Maven 依赖)核心依赖 poi-ooxml 用于处理 Office Open XML 格式(.docx),兼容 5.2.0 及以上版本(推荐使用最新稳定版):<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>5.2.0</version> <!-- 你可以使用最新版本 --> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>5.2.0</version> </dependency>2. 编写工具类(完整可运行代码)工具类封装了「读取模板、替换占位符、保存文件」全流程,支持普通段落和表格单元格的替换,还保留了原文本格式(字体、大小、颜色):package com.water.ocrimagerecognize.util; import org.apache.poi.xwpf.usermodel.*; import java.io.*; import java.util.HashMap; import java.util.List; import java.util.Map; public class WordTemplateExporter { public static void main(String[] args) { // 输出文件路径 String outputPath = "output.docx"; try { Map<String,String> contractData = new HashMap<>(); // 占位符替换 contractData.put("date", "2025"); contractData.put("s15", "FF"); contractData.put("s16", "增效降本"); contractData.put("name", "张三"); contractData.put("luca", "lucaju"); contractData.put("s1", "game"); // 加载资源文件夹resources/tem文件夹下的合同模板 //不在resources获取模板文件需要修改此处,通过这个方法只能获取resources中的资源文件 InputStream templateInputStream = new FileInputStream("/Users/lucaju/Documents/文档/水务/word/template.docx"); XWPFDocument document = new XWPFDocument(templateInputStream); // 遍历文档中的段落 for (XWPFParagraph paragraph : document.getParagraphs()) { if (paragraph == null || paragraph.getRuns().size() == 0){ continue; } //替换占位符 change(paragraph,contractData); } // 遍历文档中的表格 for (XWPFTable table : document.getTables()) { for (XWPFTableRow row : table.getRows()) { for (XWPFTableCell cell : row.getTableCells()) { // 遍历单元格中的段落 for (XWPFParagraph cellParagraph : cell.getParagraphs()) { //替换占位符 change(cellParagraph,contractData); } } } } // 遍历文档中的图片 for (XWPFPictureData picture : document.getAllPictures()) { // 图片不会被修改,直接跳过 //需要修改图片在此处 System.out.println("图片: " + picture.getFileName()); } //该路径为相对路径,默认创建保存位置为项目文件所在盘符的根目录下 // 保存填充后的合同(将毫秒数添加到文件名防止命名冲突) String filePath = "contract_" + System.currentTimeMillis() + ".docx"; //检查目录是否存在,不存在则创建 File directory = new File(filePath); // 使用 FileOutputStream 保存填充后的合同 try (FileOutputStream out = new FileOutputStream(filePath)) { document.write(out); // 将内容写入文件 } System.out.println("输出完成"); } catch (Exception e) { e.printStackTrace(); } } //替换占位符方法 public static void change(XWPFParagraph paragraph, Map<String, String> contractData){ for (XWPFRun run : paragraph.getRuns()) { // 获取当前 run 的文本 String runText = run.getText(0); if (runText == null){ continue; } StringBuilder newText = new StringBuilder(runText); // 替换占位符 for (Map.Entry<String, String> entry : contractData.entrySet()) { //此处的占位符“{{”和“}}”可以任意更改,合同模板文件随着替换即可。 String placeholder = "{{" + entry.getKey() + "}}"; int startIndex = newText.indexOf(placeholder); while (startIndex != -1) { newText.replace(startIndex, startIndex + placeholder.length(), entry.getValue()); startIndex = newText.indexOf(placeholder, startIndex + entry.getValue().length()); } } // 更新替换后的文本 run.setText(newText.toString(), 0); } } /** * 替换 Word 文件中的占位符(支持普通段落 + 表格单元格) * * @param document Word 文档对象 * @param placeholder 占位符,例如 {{name}} * @param replacement 替换后的值,例如 "张三" */ private static void replacePlaceholder(XWPFDocument document, String placeholder, String replacement) { // 1. 替换普通段落中的占位符(原有逻辑保留) for (XWPFParagraph paragraph : document.getParagraphs()) { replaceRunInParagraph(paragraph, placeholder, replacement); } // 2. 替换表格中的占位符(新增核心逻辑) for (XWPFTable table : document.getTables()) { // 遍历所有表格 for (XWPFTableRow row : table.getRows()) { // 遍历表格的所有行 for (XWPFTableCell cell : row.getTableCells()) { // 遍历行的所有单元格 for (XWPFParagraph paragraph : cell.getParagraphs()) { // 遍历单元格内的所有段落 replaceRunInParagraph(paragraph, placeholder, replacement); // 替换段落中的占位符 } } } } } /** * 替换单个段落中所有 Run 里的占位符(抽取通用逻辑,避免重复代码) */ private static void replaceRunInParagraph(XWPFParagraph para, String placeholder, String replacement) { List<XWPFRun> runs = para.getRuns(); int runCount = runs.size(); // 情况1:段落只有1个 Run(直接替换) if (runCount == 1) { XWPFRun run = runs.get(0); String text = run.getText(0); if (text != null && text.contains(placeholder)) { run.setText(text.replace(placeholder, replacement), 0); } return; } // 情况2:段落有多个 Run(合并文本后替换) StringBuilder mergedText = new StringBuilder(); // 第一步:合并所有 Run 的文本 for (XWPFRun run : runs) { String text = run.getText(0); if (text != null) { mergedText.append(text); } } // 检查合并后的文本是否包含占位符 String finalText = mergedText.toString(); if (!finalText.contains(placeholder)) { return; // 不包含则无需处理 } // 第二步:替换占位符 finalText = finalText.replace(placeholder, replacement); // 第三步:清空原有所有 Run(兼容低版本 POI 的写法) // 注意:要倒序删除,避免索引错乱 for (int i = runCount - 1; i >= 0; i--) { para.removeRun(i); } // 第四步:创建新的 Run,写入替换后的文本 XWPFRun newRun = para.createRun(); newRun.setText(finalText, 0); // (可选)复制原有文本的格式(如字体、大小、颜色) if (runCount > 0) { XWPFRun originalFirstRun = runs.get(0); // 取第一个 Run 的格式 newRun.setFontFamily(originalFirstRun.getFontFamily()); newRun.setFontSize(originalFirstRun.getFontSize()); newRun.setColor(originalFirstRun.getColor()); } } }3. 模板编辑规范占位符格式:统一使用 {{占位符名}}(如 {{name}}、{{date}}),可自定义分隔符(需同步修改代码中 placeholder 的拼接逻辑);关键注意点:占位符必须作为一个整体输入(直接复制粘贴 {{name}},或一次性输入完成);避免输入一半保存、再续输的操作(会导致占位符被拆分成多个 Run,替换失败);模板文件需保存为 .docx 格式(不支持 .doc 旧格式,如需兼容可先转成 .docx)。三、使用示例编辑模板 template.docx,内容如下:姓名:{{name}} 日期:{{date}} 项目:{{s1}} 目标:{{s16}}运行 main 方法,传入替换数据;生成的文件中,占位符会被自动替换为对应值:姓名:张三 日期:2025年11月 项目:game 目标:增效降本这套方案轻量化、无额外依赖,适合合同生成、报表导出、通知书批量制作等场景,可直接集成到 Spring Boot、SSM 等主流 Java 项目中。
2025年11月17日
174 阅读
0 评论
1 点赞
2025-10-16
Spring Boot 整合 Milvus 向量数据库:CRUD封装
本文围绕 Spring Boot 集成 Milvus 向量数据库展开,详细介绍了二次封装的完整方案:首先通过配置类实现 Milvus 连接池与连接参数管理,再以@CollectionName和@PrimaryKey注解建立实体与 Milvus Collection 的映射,搭配驼峰转下划线命名策略解决字段名适配问题;核心封装泛型MilvusBaseService
类,实现新增、主键查询、分页查询等通用 CRUD 方法,并针对 Milvus 无原生更新的特性,采用 “先删除后插入” 策略实现更新功能;同时提供 Lambda 查询构造器简化条件查询,辅以工具类处理主键解析等通用逻辑。文中还给出业务层 Service、Controller 的使用示例与完整配置文件,附项目 GitHub 地址,帮助开发者快速落地 Milvus 相关业务。
2025年10月16日
107 阅读
0 评论
4 点赞
2025-09-30
Java 实战:基于 WebSocket 获取大模型流式输出并转为 Flux
本文详细讲解 Java 如何基于 ReactorNettyWebSocketClient 实现 WebSocket 流式输出获取,附带完整代码演示将 WebSocket 消息转为 Flux 响应式流的全流程,包含连接建立、异常处理、会话管理等关键逻辑,助力开发者快速解决大模型流式对接需求,适配前端 HTTP 流式交互场景。
2025年09月30日
105 阅读
0 评论
2 点赞
1
...
3
4
5
...
11