Java 11+开发必看:String.lines()空行丢失问题的5种应对方案

第一章:Java 11 String.lines() 空行丢失问题的背景与影响

在 Java 11 中,String.lines() 方法作为字符串处理的重要增强功能被引入,旨在简化将多行字符串按行分割的操作。该方法返回一个 Stream<String>,便于开发者对每一行进行流式处理。然而,在实际使用中,开发者逐渐发现一个关键行为:当原始字符串中包含连续换行符(即空行)时,lines() 并不会保留这些空行对应的空字符串元素。

问题表现

考虑如下包含空行的多行字符串:
String text = "First line\n\nThird line";
text.lines().forEach(System.out::println);
上述代码输出为:
  • First line
  • Third line
中间的空行并未出现在输出中,这与使用传统 split("\n") 方法的行为不一致,容易引发数据解析偏差。

根本原因分析

String.lines() 内部基于 Matcher 和正则表达式匹配行终止符(如 \n、\r\n 等),其设计逻辑是“以行终止符为分隔符”,但忽略了两个终止符之间可能存在的空内容。因此,连续换行被视为多个分隔符,而非产生空字段。

潜在影响

该行为可能导致以下问题:
  • 文本行号映射错误,影响日志或配置文件解析
  • 数据结构化过程中丢失格式信息
  • 与预期的 CSV 或文档格式处理逻辑不符
方法输入 "A\n\nC"输出结果
split("\n")["A", "", "C"]保留空行
lines()["A", "C"]丢失空行
此差异凸显了在迁移旧代码或处理结构化文本时需谨慎评估 lines() 的适用性。

第二章:String.lines() 方法的底层机制与空行处理逻辑

2.1 Java 11 中 String.lines() 的设计原理与规范

Java 11 引入的 String.lines() 方法旨在简化多行字符串的流式处理。该方法返回一个 Stream<String>,按照行终止符(如 \n、\r\n 等)将原字符串分割为独立的行。
核心设计原则
该方法遵循“惰性求值”与“不可变性”原则:不修改原始字符串,且返回的流仅在终端操作触发时才进行实际分割,提升性能。
行为规范与边界处理
  • 空字符串返回空流
  • 末尾无换行符仍视为有效行
  • 支持跨平台换行符(\n, \r, \r\n)自动识别
String text = "Hello\nWorld\r\nJava";
text.lines().forEach(System.out::println);
上述代码输出三行内容。内部通过 Scanner 和正则表达式 \R(通用行终结符)实现兼容性分割,确保跨系统一致性。

2.2 换行符类型对 lines() 分割结果的影响分析

在文本处理中,`lines()` 方法常用于按行分割字符串。然而,不同操作系统使用的换行符标准不一,直接影响分割结果。
常见换行符类型
  • \n:Unix/Linux 和 macOS(现代)系统使用
  • \r\n:Windows 系统使用
  • \r:旧版 macOS(经典)系统使用
代码示例与行为对比
package main

import (
    "fmt"
    "strings"
)

func main() {
    text := "line1\nline2\r\nline3"
    lines := strings.Split(text, "\n")
    for _, line := range lines {
        fmt.Printf("'%s'\n", strings.TrimRight(line, "\r"))
    }
}
上述代码通过 `Split` 按 `\n` 分割,并使用 `TrimRight` 清除残留的 `\r` 字符,确保跨平台一致性。若忽略 `\r` 处理,可能导致行尾出现异常字符。
推荐处理策略
换行符来源系统建议处理方式
\nLinux/macOS直接 Split
\r\nWindows预处理替换为 \n 或 Trim \r
\r旧 macOS替换为 \n 后处理

2.3 空行在流式分割中被过滤的根本原因探究

在流式数据处理中,空行常被视为无意义的分隔符或噪声数据。许多流式分割工具默认启用空行过滤机制,以提升处理效率并减少冗余输出。
常见过滤逻辑实现
scanner := bufio.NewScanner(input)
for scanner.Scan() {
    line := scanner.Text()
    if strings.TrimSpace(line) == "" {
        continue // 跳过空行
    }
    process(line)
}
上述代码展示了典型的空行过滤逻辑:通过 strings.TrimSpace 判断行内容是否为空,若为空则跳过处理。
设计动机分析
  • 避免触发下游系统对空输入的异常处理
  • 减少日志体积和网络传输开销
  • 提升解析器对有效数据的吞吐能力
该机制虽提升了整体稳定性,但在特定场景下可能导致数据丢失,需谨慎配置。

2.4 使用 Stream 调试工具验证 lines() 输出行为

在处理流式数据时,lines() 方法常用于按行提取文本内容。为确保其输出行为符合预期,可借助调试工具对流的每个阶段进行观测。
调试步骤
  • 启用日志记录中间流状态
  • 使用断点捕获 lines() 的实时输出
  • 验证换行符处理的一致性
Files.lines(Paths.get("data.log"))
     .peek(line -> System.out.println("读取行: " + line))
     .filter(s -> s.contains("ERROR"))
     .count();
上述代码中,peek() 插入调试信息,观察每行输入;filter() 后续操作可验证数据是否被正确筛选。通过此方式,能精准定位 lines() 在复杂流中的行为表现。

2.5 与其他字符串分割方式的对比实验(split vs lines)

在处理多行文本时,splitlines 是两种常见的字符串分割方式。它们在行为和性能上存在显著差异。
方法对比
  • split("\n"):基于换行符显式分割,保留空行;适用于自定义分隔符场景。
  • lines():惰性逐行读取,自动去除行尾空白;适合大文件流式处理。
性能测试代码
package main

import (
    "strings"
    "bufio"
    "io"
)

func splitLines(s string) []string {
    return strings.Split(s, "\n") // 显式分割
}

func scanLines(s string) []string {
    var lines []string
    scanner := bufio.NewScanner(strings.NewReader(s))
    for scanner.Scan() {
        lines = append(lines, scanner.Text()) // 惰性读取每行
    }
    return lines
}
上述代码展示了两种实现方式:`Split` 立即返回所有子串,适合小文本;`Scanner` 则通过迭代器模式降低内存占用,更适合处理大规模数据流。

第三章:常见业务场景中的空行语义重要性

3.1 配置文件解析中空行作为段落分隔符的应用

在配置文件解析过程中,空行常被用作逻辑段落的分隔符,以提升可读性并辅助结构化处理。通过识别连续的非空行组,解析器可将配置划分为独立区块,便于后续映射为数据结构。
典型配置格式示例

[server]
host = 127.0.0.1
port = 8080

[database]
url = postgres://localhost:5432/app
timeout = 30
上述配置中,空行将 [server][database] 段落分离,增强视觉区分。
解析逻辑实现
  • 逐行读取配置内容
  • 跳过纯空行或仅含空白字符的行
  • 以非空行构成一个段落单元
  • 遇空行时触发当前段落提交
该机制广泛应用于 INI、YAML 等格式的轻量级解析器中,是构建清晰配置层级的基础手段。

3.2 文本协议(如HTTP、CSV)中空行的结构意义

在文本协议中,空行不仅是视觉分隔符,更承担着关键的语法功能。以HTTP协议为例,空行用于分隔请求头与消息体,标志着头部信息的结束。
HTTP报文中的空行示例
GET /index.html HTTP/1.1
Host: example.com
User-Agent: curl/7.68.0

<!-- 空行后为消息体 -->
Hello World
该空行(即头部末尾的\r\n\r\n)是协议解析的关键信号,服务器据此判断头部终止位置,确保后续数据被正确视为实体内容。
CSV中的空行语义差异
与HTTP不同,CSV文件中的空行通常表示数据缺失或记录分隔异常。解析器可能将其忽略或视为无效行,影响数据完整性。
  • HTTP:空行 = 头部与主体的分界符
  • CSV:空行 = 潜在数据错误或分隔冗余

3.3 日志文件分析时空行承载的时间段标识作用

在日志分析中,空行常被用作逻辑分隔符,用以标识不同时间段或事件周期的边界。特别是在批处理日志或定时任务输出中,连续的日志条目可能属于同一执行周期,而空行使周期间界限清晰可辨。
空行作为时间边界信号
当系统按固定间隔生成日志时,空行的出现往往意味着前一时间段的数据输出结束。例如:
2023-10-01 08:00:01 INFO  Starting batch job...
2023-10-01 08:00:05 DEBUG Processed 100 records
2023-10-01 08:00:06 INFO  Job completed successfully

2023-10-01 09:00:01 INFO  Starting next batch job...
上述日志中,空行分隔了发生在 08:00 和 09:00 的两次作业执行,便于通过脚本按时间段切分分析单元。
解析策略与实现逻辑
可借助正则匹配或流式读取识别空行边界:
  • 逐行读取日志,记录时间戳上下文
  • 检测到空行时,触发当前时间段数据聚合
  • 初始化新周期的缓冲区,避免跨时段混淆
该机制提升了日志解析的语义准确性,尤其适用于无显式结构标记的文本日志场景。

第四章:五种应对空行丢失的实践解决方案

4.1 方案一:基于正则表达式的自定义行分割工具

在处理结构复杂或非标准换行符分隔的文本数据时,传统的按 `\n` 分割方式往往无法满足需求。为此,可构建基于正则表达式的自定义行分割工具,灵活识别多样化的行边界。
核心设计思路
通过正则表达式匹配多种换行模式(如 `\r\n`、`\n`、连续空格+换行等),并支持用户自定义分隔符规则,提升文本解析的鲁棒性。
// RegexLineSplitter 使用正则表达式进行行分割
func RegexLineSplitter(text string) []string {
    // 匹配常见换行符及前后空白字符
    re := regexp.MustCompile(`\s*\r?\n\s*`)
    return re.Split(strings.TrimSpace(text), -1)
}
上述代码中,`regexp.MustCompile` 编译正则表达式 `\s*\r?\n\s*`,可忽略行尾和行首的空白字符;`Split` 方法将原始文本切分为字符串切片,`-1` 表示不限制返回数量。
适用场景对比
场景传统分割正则分割
纯 \n 换行✅ 有效✅ 有效
混合 \r\n❌ 错乱✅ 正确处理
多余空格换行❌ 保留冗余✅ 自动清理

4.2 方案二:利用 BufferedReader 按原始格式逐行读取

在处理大文本文件时,BufferedReader 提供了高效且低内存消耗的逐行读取能力。它通过内置缓冲机制减少 I/O 操作次数,适合保持原始数据格式不变的场景。
核心优势
  • 支持按行读取,保留原始换行与编码格式
  • 内存占用低,适用于大文件处理
  • InputStreamReader 配合可指定字符集
代码实现示例
BufferedReader reader = new BufferedReader(new InputStreamReader(
    new FileInputStream("data.log"), StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
    System.out.println(line); // 处理每行数据
}
reader.close();
上述代码中,readLine() 方法逐行读取内容,返回 null 表示文件结束。使用 StandardCharsets.UTF_8 明确指定编码,避免乱码问题。缓冲区默认大小为 8KB,可通过构造函数调整。

4.3 方案三:结合 Pattern.splitAsStream 保留空行内容

在处理包含空行的文本时,传统的字符串分割方法往往会忽略空白内容,导致信息丢失。通过 Java 8 提供的 Pattern.splitAsStream 方法,可以更精细地控制分割行为。
核心实现逻辑
利用正则表达式模式匹配换行符,并将结果转换为流式处理,确保空行作为独立元素被保留。
Pattern pattern = Pattern.compile("\n");
String input = "line1\n\nline3";
pattern.splitAsStream(input)
       .forEach(line -> System.out.println("[" + line + "]"));
上述代码中,splitAsStream 将输入按换行符拆分为流元素,包括空字符串在内的每一部分都会被输出。参数 "\n" 明确指定分隔符,避免平台差异问题。
优势对比
  • 精确保留原始结构中的空行
  • 支持延迟加载,适合大文本处理
  • 与 Stream API 集成,便于后续过滤或映射操作

4.4 方案四:使用第三方库(Apache Commons IO)的安全替代

在处理文件操作时,直接使用 Java 原生 I/O 容易引发资源泄漏或异常处理不完整。Apache Commons IO 提供了更高层次的抽象,简化了常见任务并增强了安全性。
核心优势
  • 自动关闭资源,避免流未关闭问题
  • 封装重复代码,提升开发效率
  • 经过广泛测试,稳定性强
典型应用示例
FileUtils.copyFile(source, dest);
该方法封装了输入输出流管理,内部自动处理异常和资源释放。参数 sourcedest 分别为源文件与目标文件的 File 对象,无需手动编写 try-with-resources 块。
依赖引入
构建工具依赖配置
Maven<groupId>commons-io</groupId>

第五章:综合选型建议与未来版本兼容性展望

技术栈选型的权衡策略
在微服务架构中,选择 gRPC 还是 REST 需结合团队技术储备和长期维护成本。若系统对性能敏感,gRPC 的二进制序列化和 HTTP/2 支持更具优势:

// 示例:gRPC 服务定义中的 proto 文件片段
service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);
}
// 使用 Protocol Buffers 可提升跨语言兼容性,尤其适合异构环境
依赖管理与语义化版本控制
采用语义化版本(SemVer)可有效规避升级带来的破坏性变更。以下为常见依赖管理实践:
  • 锁定主版本号以避免非预期更新,如使用 ^1.2.3 仅允许补丁和次版本升级
  • 定期审查依赖项的安全漏洞,推荐集成 Dependabot 或 Renovate
  • 构建时引入版本校验脚本,确保生产环境一致性
向前兼容的 API 设计原则
为保障未来版本平滑演进,API 应遵循扩展而不破坏的原则。例如,在 JSON 响应中新增字段时,客户端需具备容错能力:
版本新增字段兼容策略
v1.0基础用户信息返回
v1.1metadata.timezone客户端忽略未知字段
长期支持版本的迁移路径规划
当框架进入 EOL(End-of-Life)阶段,应提前制定迁移方案。以 Node.js 为例,LTS 版本切换周期明确,建议:
  1. 评估当前运行时版本的支持截止时间
  2. 在测试环境中验证新版本的模块兼容性
  3. 通过灰度发布逐步切换线上实例
,说了别考虑handleSplitPlaceholderInRun,只想要当前分支如何处理,想保留前端传的值,有换行格式,在后端需要保留,不用考虑另一个分支,当前代码实现不了。打断点进去在设置文本那里,参数为:US20250708094332 【业务逻辑】【机关17+】人岗提名模块页面机关17+字段名称恢复为“备注10” 1.5 人天 US20250827428997 【业务逻辑】【机关17+】人岗行权模块评议支撑页面机关17+字段名称恢复为“备注10” 1.5 人天 US20250103938962 【确认规范性】确认规范性详情页各数据统计维度增加筛选过滤功能 1.5 人天 US20250827430348 【业务逻辑】【评议支撑】批准权行权主体非当前环节评议数据在会前准备、启动会前准备6步里统计显示和操作数据流转 US20250827415976 【业务逻辑】【评议、AT评议中】去掉系统原页面的分类管理相关按钮 0.5 人天 US20240816139643 【离职员工】员工离职后自动终止在途委托关系并给员工所属最小部门和委托接收部门人岗匹配HR发送应用号知会 US20240813973188 【离职员工】提名申请单流转过程中页面不显示离职员工功能验证 dfsfisafkhkhllll sfsf kk 。 注意,当前参数在打断点那里并没有占位符\n,只是肉眼看数据格式有换行 如何修改代码,以下都是源码 public static void dataConversion(String templateUrl, String filePath, Map<String, Object> dataMap) throws Exception { File template = new File(FilenameUtils.normalize(templateUrl)); File file = FileUtils.getFile(filePath); try (InputStream inputStream = FileUtils.openInputStream(template); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); FileOutputStream os = FileUtils.openOutputStream(file)) { XWPFDocument doc = new XWPFDocument(inputStream); // 简单处理替换页眉和页尾占位{xx} replaceHeaderAndFooter(doc, dataMap); // 处理文档段落 replacesCommonData(doc, dataMap); // 处理文档表格 replacesTableData(doc, dataMap); doc.write(byteStream); byte[] replace = byteStream.toByteArray(); if (!file.exists()) { boolean bb = file.createNewFile(); if (!bb) { logger.error(“dataConversion error”); } } os.write(replace, 0, replace.length); os.flush(); } } private static void replacesCommonData(XWPFDocument doc, Map<String, Object> params) { if (params == null) { params = new HashMap<>(); } List paragraphs = doc.getParagraphs(); if (CollectionUtils.isEmpty(paragraphs)) { return; } Pattern pattern = Pattern.compile(REG_FOREACH_PARAGRAPH); int tempCount = paragraphs.size(); XWPFParagraph tempParagraphs; for (int tempIndex = 0; tempIndex < tempCount; tempIndex++) { tempParagraphs = paragraphs.get(tempIndex + paragraphs.size() - tempCount); // 动态段落处理会增加新的段落,跳过新增的段落 String paragraphText = tempParagraphs.getText(); Matcher matcher = pattern.matcher(paragraphText); if (!matcher.find()) { replaceInParagraph(tempParagraphs, params); // 静态段落直接替换 } } } private static void replaceInParagraph(XWPFParagraph xwpfParagraph, Map<String, Object> parametersMap) { if (xwpfParagraph == null) { return; } if (parametersMap == null) { parametersMap = new HashMap<>(); } List runs = xwpfParagraph.getRuns(); String paragraphText = xwpfParagraph.getText(); if (CollectionUtils.isEmpty(runs) || paragraphText == null) { return; } Pattern pattern = Pattern.compile(REG_PLACEHOLDER); Matcher matcher = pattern.matcher(paragraphText); if (matcher.find()) { // 查找到有标签才执行替换 int beginRunIndex = xwpfParagraph.searchText(PREFIX, new PositionInParagraph()).getBeginRun(); // 标签开始run位置 int endRunIndex = xwpfParagraph.searchText(SUFFIX, new PositionInParagraph()).getEndRun(); // 结束标签 StringBuffer key = new StringBuffer(); if (beginRunIndex == endRunIndex) { // {xx}在一个run标签内 handleWholePlaceholderInRun(xwpfParagraph, parametersMap, runs, beginRunIndex, key); } else { // {xx}被分成多个run handleSplitPlaceholderInRun(xwpfParagraph, parametersMap, runs, beginRunIndex, endRunIndex, key); } replaceInParagraph(xwpfParagraph, parametersMap); } } private static void handleWholePlaceholderInRun(XWPFParagraph xwpfParagraph, Map<String, Object> parametersMap, List runs, int beginRunIndex, StringBuffer key) { XWPFRun beginRun = runs.get(beginRunIndex); String beginRunText = beginRun.text(); int beginIndex = beginRunText.indexOf(PREFIX); int endIndex = beginRunText.indexOf(SUFFIX); int length = beginRunText.length(); if (beginIndex == 0 && endIndex == length - 1) { // 该run标签只有{xx} XWPFRun insertNewRun = xwpfParagraph.insertNewRun(beginRunIndex); insertNewRun.getCTR().setRPr(beginRun.getCTR().getRPr()); // 设置文本 key.append(beginRunText.substring(1, endIndex)); String textString = getValueByKey(key.toString(), parametersMap); if (textString.contains(HardCodedConstants.LINE_FEED) || textString.contains(“\n”)) { String[] lines = textString.split(HardCodedConstants.LINE_FEED); for (int i = 0; i < lines.length; i++) { insertNewRun.setText(lines[i]); insertNewRun.addBreak(); } } else { insertNewRun.setText(textString); } xwpfParagraph.removeRun(beginRunIndex + 1); } else { // 该run标签为xx{xx}xx,替换key后,还需要加上原始key前后的文本 XWPFRun insertNewRun = xwpfParagraph.insertNewRun(beginRunIndex); insertNewRun.getCTR().setRPr(beginRun.getCTR().getRPr()); // 设置文本 key.append(beginRunText.substring(beginRunText.indexOf(PREFIX) + 1, beginRunText.indexOf(SUFFIX))); String textString = beginRunText.substring(0, beginIndex) + getValueByKey(key.toString(), parametersMap) + beginRunText.substring(endIndex + 1); if (textString.contains(HardCodedConstants.LINE_FEED)) { String[] lines = textString.split(HardCodedConstants.LINE_FEED); for (int i = 0; i < lines.length; i++) { insertNewRun.setText(lines[i]); insertNewRun.addBreak(); } } else { insertNewRun.setText(textString); } xwpfParagraph.removeRun(beginRunIndex + 1); } }
09-10
package Util; import java.sql.*; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; public class SQLFileExecutor { /** * 执行SQL文件,支持存储过程和触发器 */ public static void executeSQLFile(String sqlFilePath) { Connection conn = null; Statement stmt = null; try { conn = DatabaseUtil.getConnection(); stmt = conn.createStatement(); // 读取SQL文件 InputStream inputStream = SQLFileExecutor.class.getClassLoader().getResourceAsStream(sqlFilePath); if (inputStream == null) { throw new RuntimeException("SQL文件未找到: " + sqlFilePath); } BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); StringBuilder sqlBuilder = new StringBuilder(); String line; int sqlCount = 0; System.out.println("开始读取SQL文件: " + sqlFilePath); // 读取文件内容 while ((line = reader.readLine()) != null) { // 跳过注释和空行 line = line.trim(); if (line.isEmpty() || line.startsWith("--") || line.startsWith("#")) { continue; } sqlBuilder.append(line).append("\n"); // 如果行以分号结束,表示一个完整的SQL语句 if (line.endsWith(";")) { String sql = sqlBuilder.toString().trim(); if (!sql.isEmpty()) { sqlCount++; try { // 移除末尾的分号 if (sql.endsWith(";")) { sql = sql.substring(0, sql.length() - 1); } // 检查是否是存储过程或触发器 if (isProcedureOrTrigger(sql)) { System.out.println("创建程序对象 [" + sqlCount + "]: " + getObjectName(sql)); executeWithDelimiter(conn, sql); } else { System.out.println("执行SQL [" + sqlCount + "]: " + (sql.length() > 100 ? sql.substring(0, 100) + "..." : sql)); stmt.execute(sql); } System.out.println("SQL执行成功"); } catch (SQLException e) { handleSQLException(e, sql); } } sqlBuilder.setLength(0); // 清空StringBuilder } } reader.close(); System.out.println("SQL文件执行完成: " + sqlFilePath + ",共执行 " + sqlCount + " 条SQL语句"); } catch (Exception e) { System.err.println("执行SQL文件时出错: " + e.getMessage()); e.printStackTrace(); } finally { DatabaseUtil.closeResources(conn, stmt, null); } } /** * 使用自定义分隔符执行存储过程/触发器 */ private static void executeWithDelimiter(Connection conn, String sql) throws SQLException { // 临时改变分隔符来执行存储过程 try (Statement stmt = conn.createStatement()) { // 设置分隔符为 // stmt.execute("DELIMITER //"); stmt.execute(sql + " //"); // 恢复分隔符为 ; stmt.execute("DELIMITER ;"); } } /** * 专门处理存储过程和触发器文件的方法 */ public static void executeProcedureFile(String sqlFilePath) { Connection conn = null; try { conn = DatabaseUtil.getConnection(); // 读取整个文件内容 String fileContent = readEntireFile(sqlFilePath); if (fileContent == null) { throw new RuntimeException("无法读取SQL文件: " + sqlFilePath); } System.out.println("开始执行存储过程文件: " + sqlFilePath); // 分割SQL语句(处理DELIMITER命令) List<String> sqlStatements = parseSQLStatements(fileContent); int successCount = 0; int totalCount = sqlStatements.size(); for (int i = 0; i < sqlStatements.size(); i++) { String sql = sqlStatements.get(i).trim(); if (sql.isEmpty()) continue; try { System.out.println("执行语句 [" + (i + 1) + "/" + totalCount + "]: " + (sql.length() > 100 ? sql.substring(0, 100) + "..." : sql)); try (Statement stmt = conn.createStatement()) { stmt.execute(sql); successCount++; System.out.println("语句执行成功"); } } catch (SQLException e) { System.err.println("语句执行失败: " + e.getMessage()); // 如果是对象已存在的错误,继续执行 if (e.getErrorCode() == 1304 || e.getErrorCode() == 1350) { // PROCEDURE/TRIGGER already exists System.out.println("程序对象已存在,继续执行"); successCount++; } else { logSQLException(e); } } } System.out.println("存储过程文件执行完成: " + sqlFilePath + ",成功 " + successCount + "/" + totalCount + " 条语句"); } catch (Exception e) { System.err.println("执行存储过程文件时出错: " + e.getMessage()); e.printStackTrace(); } finally { DatabaseUtil.closeResources(conn, null, null); } } /** * 读取整个文件内容 */ private static String readEntireFile(String sqlFilePath) { try { InputStream inputStream = SQLFileExecutor.class.getClassLoader().getResourceAsStream(sqlFilePath); if (inputStream == null) { return null; } BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); StringBuilder content = new StringBuilder(); String line; while ((line = reader.readLine()) != null) { content.append(line).append("\n"); } reader.close(); return content.toString(); } catch (Exception e) { System.err.println("读取文件失败: " + e.getMessage()); return null; } } /** * 解析SQL语句,处理DELIMITER命令 */ private static List<String> parseSQLStatements(String fileContent) { List<String> statements = new ArrayList<>(); String[] lines = fileContent.split("\n"); StringBuilder currentStatement = new StringBuilder(); String delimiter = ";"; boolean inDelimiterCommand = false; for (String line : lines) { line = line.trim(); // 跳过注释 if (line.startsWith("--") || line.startsWith("#") || line.isEmpty()) { continue; } // 处理DELIMITER命令 if (line.toUpperCase().startsWith("DELIMITER ")) { if (!currentStatement.toString().trim().isEmpty()) { statements.add(currentStatement.toString()); currentStatement.setLength(0); } delimiter = line.substring(10).trim(); System.out.println("设置分隔符为: " + delimiter); inDelimiterCommand = true; continue; } currentStatement.append(line).append("\n"); // 检查是否以当前分隔符结束 if (line.endsWith(delimiter) && !inDelimiterCommand) { String statement = currentStatement.toString().trim(); if (!statement.isEmpty()) { // 移除末尾的分隔符 if (statement.endsWith(delimiter)) { statement = statement.substring(0, statement.length() - delimiter.length()); } statements.add(statement); } currentStatement.setLength(0); } inDelimiterCommand = false; } // 添加最后一个语句(如果没有以分隔符结束) String lastStatement = currentStatement.toString().trim(); if (!lastStatement.isEmpty()) { statements.add(lastStatement); } return statements; } /** * 检查是否为存储过程或触发器SQL */ private static boolean isProcedureOrTrigger(String sql) { String upperSql = sql.toUpperCase(); return upperSql.contains("CREATE PROCEDURE") || upperSql.contains("CREATE FUNCTION") || upperSql.contains("CREATE TRIGGER") || upperSql.contains("DROP PROCEDURE") || upperSql.contains("DROP FUNCTION") || upperSql.contains("DROP TRIGGER"); } /** * 从SQL中提取对象名称 */ private static String getObjectName(String sql) { String[] parts = sql.split("\\s+"); for (int i = 0; i < parts.length; i++) { if (parts[i].equalsIgnoreCase("PROCEDURE") || parts[i].equalsIgnoreCase("FUNCTION") || parts[i].equalsIgnoreCase("TRIGGER")) { if (i + 1 < parts.length) { return parts[i + 1].replace("`", "").split("\\(")[0]; } } } return "未知对象"; } /** * 处理SQL异常 */ private static void handleSQLException(SQLException e, String sql) { System.err.println("SQL执行失败: " + e.getMessage()); System.err.println("失败SQL: " + (sql.length() > 200 ? sql.substring(0, 200) + "..." : sql)); // 如果是对象已存在的错误,可以继续执行 int errorCode = e.getErrorCode(); if (errorCode == 1050 || errorCode == 1051 || errorCode == 1060 || errorCode == 1061 || errorCode == 1062 || errorCode == 1217 || errorCode == 1304 || errorCode == 1350) { System.out.println("数据库对象已存在,继续执行后续SQL"); } else { logSQLException(e); } } // 保留其他原有方法(executeSQL, executeQuery, executeUpdate, executeBatch, checkTableExists等) /** * 执行单条SQL语句 */ public static boolean executeSQL(String sql) { Connection conn = null; Statement stmt = null; try { conn = DatabaseUtil.getConnection(); stmt = conn.createStatement(); return stmt.execute(sql); } catch (SQLException e) { System.err.println("执行SQL语句失败: " + sql); logSQLException(e); return false; } finally { DatabaseUtil.closeResources(conn, stmt, null); } } /** * 执行查询SQL语句 */ public static ResultSet executeQuery(String sql) { Connection conn = null; Statement stmt = null; try { conn = DatabaseUtil.getConnection(); stmt = conn.createStatement(); return stmt.executeQuery(sql); } catch (SQLException e) { System.err.println("执行查询SQL失败: " + sql); logSQLException(e); return null; } // 注意:调用者需要手动关闭ResultSet和Statement } /** * 执行更新SQL语句(INSERT, UPDATE, DELETE) */ public static int executeUpdate(String sql) { Connection conn = null; Statement stmt = null; try { conn = DatabaseUtil.getConnection(); stmt = conn.createStatement(); return stmt.executeUpdate(sql); } catch (SQLException e) { System.err.println("执行更新SQL失败: " + sql); logSQLException(e); return -1; } finally { DatabaseUtil.closeResources(conn, stmt, null); } } /** * 批量执行SQL语句 */ public static int[] executeBatch(String[] sqlArray) { Connection conn = null; Statement stmt = null; try { conn = DatabaseUtil.getConnection(); stmt = conn.createStatement(); for (String sql : sqlArray) { stmt.addBatch(sql); } return stmt.executeBatch(); } catch (SQLException e) { System.err.println("批量执行SQL失败"); logSQLException(e); return null; } finally { DatabaseUtil.closeResources(conn, stmt, null); } } /** * 检查表是否存在 */ public static boolean checkTableExists(String tableName) { Connection conn = null; Statement stmt = null; ResultSet rs = null; try { conn = DatabaseUtil.getConnection(); // 先切换到目标数据库 conn.setCatalog("Library_Management"); stmt = conn.createStatement(); rs = stmt.executeQuery("SHOW TABLES LIKE '" + tableName + "'"); return rs.next(); } catch (Exception e) { System.err.println("检查表存在性时出错: " + e.getMessage()); return false; } finally { DatabaseUtil.closeResources(conn, stmt, rs); } } /** * 检查存储过程是否存在 */ public static boolean checkProcedureExists(String procedureName) { Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = DatabaseUtil.getConnection(); String sql = "SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES " + "WHERE ROUTINE_SCHEMA = ? AND ROUTINE_NAME = ? AND ROUTINE_TYPE = 'PROCEDURE'"; stmt = conn.prepareStatement(sql); stmt.setString(1, "Library_management"); stmt.setString(2, procedureName); rs = stmt.executeQuery(); return rs.next(); } catch (Exception e) { System.err.println("检查存储过程存在性时出错: " + e.getMessage()); return false; } finally { DatabaseUtil.closeResources(conn, stmt, rs); } } /** * 检查触发器是否存在 */ public static boolean checkTriggerExists(String triggerName) { Connection conn = null; PreparedStatement stmt = null; ResultSet rs = null; try { conn = DatabaseUtil.getConnection(); String sql = "SELECT TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS " + "WHERE TRIGGER_SCHEMA = ? AND TRIGGER_NAME = ?"; stmt = conn.prepareStatement(sql); stmt.setString(1, "Library_management"); stmt.setString(2, triggerName); rs = stmt.executeQuery(); return rs.next(); } catch (Exception e) { System.err.println("检查触发器存在性时出错: " + e.getMessage()); return false; } finally { DatabaseUtil.closeResources(conn, stmt, rs); } } /** * 记录SQL异常详细信息 */ private static void logSQLException(SQLException e) { System.err.println("SQL错误: " + e.getMessage()); System.err.println("SQL状态: " + e.getSQLState()); System.err.println("错误代码: " + e.getErrorCode()); } } 优化代码要求可以实现sql语句中的创建存储过程和触发器语句 a.sql文件 -- 创建借阅图书的存储过程(MySQL 8.0 格式规范) DROP PROCEDURE IF EXISTS BorrowBooks; DELIMITER // -- 分隔符后无空格,单独一行 CREATE PROCEDURE BorrowBooks( IN p_reader_id INT, -- 传:读者ID IN p_book_ids TEXT, -- 传:图书ID列表,逗号分隔,如 '1,2,3,4' IN p_borrow_date DATETIME, -- 可选:借阅日期(NULL则使用当前时间) OUT p_result VARCHAR(500) -- 输出:操作结果 ) BEGIN -- 声明变量 DECLARE v_book_status ENUM('可借','借出','维修中','下架','遗失'); DECLARE v_reader_status ENUM('正常','异常','冻结','注销'); DECLARE v_current_borrow_count INT; DECLARE v_max_borrow_count INT; DECLARE v_due_date DATETIME; DECLARE v_actual_borrow_date DATETIME; DECLARE v_book_id INT; -- 用于循环处理图书ID的变量 DECLARE v_book_ids_temp TEXT; DECLARE v_current_book_id TEXT; DECLARE v_comma_pos INT; DECLARE v_success_count INT DEFAULT 0; DECLARE v_fail_count INT DEFAULT 0; DECLARE v_error_messages TEXT DEFAULT ''; DECLARE v_total_books INT DEFAULT 0; DECLARE v_remaining_quota INT DEFAULT 0; -- 剩余借阅额度 DECLARE v_can_borrow_count INT DEFAULT 0; -- 实际可借数量 DECLARE v_processed_count INT DEFAULT 0; -- 已处理图书数量 -- 异常处理 DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; GET DIAGNOSTICS CONDITION 1 @sqlstate = RETURNED_SQLSTATE, @errno = MYSQL_ERRNO, @text = MESSAGE_TEXT; SET p_result = CONCAT('错误: ', @text); END; -- 开始事务 START TRANSACTION; -- 确定实际借阅日期:如果传入NULL则使用当前时间 SET v_actual_borrow_date = COALESCE(p_borrow_date, NOW()); -- 1. 锁定并查询读者信息 SELECT status, current_borrow_count, max_borrow_count INTO v_reader_status, v_current_borrow_count, v_max_borrow_count FROM readers WHERE reader_id = p_reader_id FOR UPDATE; -- 检查读者是否存在 IF v_reader_status IS NULL THEN SET p_result = '借阅失败: 读者不存在'; ROLLBACK; ELSE -- 检查读者状态 IF v_reader_status != '正常' THEN SET p_result = CONCAT('借阅失败: 读者账户状态为"', v_reader_status, '"'); ROLLBACK; ELSE -- 计算剩余借阅额度和要借阅的图书总数 SET v_remaining_quota = v_max_borrow_count - v_current_borrow_count; SET v_total_books = (LENGTH(p_book_ids) - LENGTH(REPLACE(p_book_ids, ',', '')) + 1); -- 如果剩余额度为0,直接返回 IF v_remaining_quota <= 0 THEN SET p_result = CONCAT('借阅失败: 借阅数量已达上限。当前借阅:', v_current_borrow_count, '/最大借阅:', v_max_borrow_count); ROLLBACK; ELSE -- 计算实际可借数量(取剩余额度和请求数量的较小值) SET v_can_borrow_count = LEAST(v_remaining_quota, v_total_books); -- 初始化循环变量 SET v_book_ids_temp = p_book_ids; SET v_success_count = 0; SET v_processed_count = 0; -- 循环处理每个图书ID(最多处理v_can_borrow_count本) WHILE LENGTH(v_book_ids_temp) > 0 AND v_processed_count < v_total_books DO -- 获取第一个图书ID SET v_comma_pos = LOCATE(',', v_book_ids_temp); IF v_comma_pos = 0 THEN SET v_current_book_id = v_book_ids_temp; SET v_book_ids_temp = ''; ELSE SET v_current_book_id = SUBSTRING(v_book_ids_temp, 1, v_comma_pos - 1); SET v_book_ids_temp = SUBSTRING(v_book_ids_temp, v_comma_pos + 1); END IF; -- 转换为整数并去除可能的空格 SET v_book_id = CAST(TRIM(v_current_book_id) AS UNSIGNED); SET v_processed_count = v_processed_count + 1; -- 如果已经达到可借数量上限,记录额度不足 IF v_success_count >= v_can_borrow_count THEN SET v_fail_count = v_fail_count + 1; SET v_error_messages = CONCAT(v_error_messages, ' 图书', v_book_id, '因借阅额度不足未能借阅;'); ELSE -- 锁定并查询当前图书状态 SELECT status INTO v_book_status FROM books WHERE book_id = v_book_id FOR UPDATE; -- 检查图书是否存在和状态 IF v_book_status IS NULL THEN SET v_fail_count = v_fail_count + 1; SET v_error_messages = CONCAT(v_error_messages, ' 图书', v_book_id, '不存在;'); ELSEIF v_book_status != '可借' THEN SET v_fail_count = v_fail_count + 1; SET v_error_messages = CONCAT(v_error_messages, ' 图书', v_book_id, '状态为"', v_book_status, '";'); ELSE -- 计算应还日期(基于实际借阅日期) SET v_due_date = DATE_FORMAT( DATE_ADD(v_actual_borrow_date, INTERVAL 30 DAY), '%Y-%m-%d 18:00:00' ); -- 插入借阅记录 INSERT INTO borrow_records (reader_id, book_id, borrow_date, due_date) VALUES (p_reader_id, v_book_id, v_actual_borrow_date, v_due_date); -- 更新图书状态 UPDATE books SET status = '借出' WHERE book_id = v_book_id; SET v_success_count = v_success_count + 1; END IF; END IF; END WHILE; -- 更新读者借阅数量(只增加成功借阅的数量) UPDATE readers SET current_borrow_count = current_borrow_count + v_success_count WHERE reader_id = p_reader_id; -- 提交事务 COMMIT; -- 构造结果信息 IF v_fail_count = 0 THEN SET p_result = CONCAT('借阅成功: 成功借阅', v_success_count, '本书,应还日期:', DATE_FORMAT(v_due_date, '%Y-%m-%d')); ELSE SET p_result = CONCAT('部分成功: 成功借阅', v_success_count, '本,失败', v_fail_count, '本。失败原因:', v_error_messages); END IF; END IF; END IF; END IF; END // -- 关键:END和 //在同一行,//后无空格(避免工具类无法识别) DELIMITER ; -- 切回默认分隔符,单独一行 可以考虑使用成熟的SQL解析库 <dependency> <groupId>com.github.jsqlparser</groupId> <artifactId>jsqlparser</artifactId> <version>4.5</version> </dependency>
10-07
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值