作者:@Neoest
时间:2025-12-10
关键词:NumberFormatException、Excel导入、POI、数据加密、类型转换、DecimalFormat
1. 问题现场
业务需求:从Excel导入用户数据,其中数量列是数字类型 ,比如 1.00。入库前需要加密 ,读取时需要解密 并转成 Integer。
java
// 导入时:Excel单元格值 1.00
Double cellValue = 1.00; // POI读取后
String encryptStr = encrypt(cellValue.toString()); // 加密成"MS4uMDA="存库
// 读取时:解密后转换
String decryptStr = decrypt(encryptStr); // 解密得到"1.00"
Integer count = Integer.valueOf(decryptStr); // 当场翻车!
控制台炸裂:
java.lang.NumberFormatException: For input string: "1.00"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
at java.lang.Integer.parseInt(Integer.java:580)
at java.lang.Integer.valueOf(Integer.java:740)
at com.example.service.UserService.getUserCount(UserService.java:123)
2. 先定位根因
| 环节 | 期望 | 实际 | 问题 |
|---|---|---|---|
| Excel单元格 | 整数 1 |
数字 1.00 |
POI读取为Double |
| 转字符串 | "1" |
"1.00" |
toString()保留小数 |
| 加密存储 | 无 | 密文 | 无 |
| 解密后 | "1" |
"1.00" |
带小数点 |
Integer.parseInt() |
合法整数 | 含.非法 |
崩溃 |
核心原因 :Integer 只认纯整数格式,"1.00" 对它来说和 "abc" 没区别。Excel的数字格式和Java的整数类型之间存在语义鸿沟。
3. 四种修复姿势(针对Excel场景)
✅ 方案 A:导入时强制转整数(源头根治)
java
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
public class ExcelImporter {
public static String parseCellToIntegerString(Cell cell) {
if (cell == null) return "0";
if (cell.getCellType() == CellType.NUMERIC) {
// 关键:直接取整,丢弃小数部分
return String.valueOf((int) cell.getNumericCellValue());
} else if (cell.getCellType() == CellType.STRING) {
return cell.getStringCellValue().trim();
}
return "0";
}
}
// 使用
String cleanStr = parseCellToIntegerString(cell); // "1"
String encryptStr = encrypt(cleanStr); // 存库
- 优点:源头干净,入库的就是纯整数
- 缺点 :会丢失精度,不适合需要保留小数的场景
✅ 方案 B:用 DecimalFormat 格式化(推荐)
java
import java.text.DecimalFormat;
public class NumberEncryptor {
private static final DecimalFormat INTEGER_FORMAT = new DecimalFormat("#");
public static String formatBeforeEncrypt(Double num) {
if (num == null) return "0";
// 1.00 → "1", 1.50 → "2"(四舍五入)
return INTEGER_FORMAT.format(num);
}
}
// 导入流程
Double cellValue = cell.getNumericCellValue(); // 1.00
String formatted = NumberEncryptor.formatBeforeEncrypt(cellValue); // "1"
String encryptStr = encrypt(formatted); // 存库
| Excel值 | 格式化后 | 说明 |
|---|---|---|
1.00 |
"1" |
去小数 |
1.50 |
"2" |
四舍五入 |
0.00 |
"0" |
正常处理 |
- 优点:控制精确,支持四舍五入
- 缺点:需要引入格式化类
✅ 方案 C:解密后做兼容性转换(后置容错)
java
public class SafeNumberParser {
/**
* 兼容带小数的数字字符串
*/
public static Integer parseLenient(String str, Integer defaultValue) {
if (StringUtils.isBlank(str)) return defaultValue;
try {
return Integer.valueOf(str.trim());
} catch (NumberFormatException e) {
// 尝试按浮点数解析
try {
return Double.valueOf(str.trim()).intValue();
} catch (NumberFormatException e2) {
return defaultValue;
}
}
}
}
// 读取流程
String decryptStr = decrypt(encryptStr); // "1.00"
Integer count = SafeNumberParser.parseLenient(decryptStr, 0); // 成功返回 1
- 优点:兼容历史脏数据,不需要改入库逻辑
- 缺点: 性能略差,存在隐式转换风险
✅ 方案 D:全链路使用 BigDecimal(企业级方案)
java
import java.math.BigDecimal;
// 导入时
BigDecimal cellDecimal = BigDecimal.valueOf(cell.getNumericCellValue());
String encryptStr = encrypt(cellDecimal.toPlainString()); // "1.00"
// 读取时
String decryptStr = decrypt(encryptStr); // "1.00"
Integer count = new BigDecimal(decryptStr).intValue(); // 安全转换
流程图:
Excel(1.00) → POI(Double) → BigDecimal → toPlainString() → 加密 → 数据库
数据库 → 解密 → "1.00" → BigDecimal → intValue() → Integer(1)
- 优点:精度无损,金融级安全,避免浮点误差
- 缺点 :代码稍重,需要理解
BigDecimalAPI
4. 工程级最佳实践
建议一:导入时统一清洗
java
@UtilityClass
public class ExcelDataCleaner {
public String cleanInteger(Cell cell) {
if (cell == null) return "0";
switch (cell.getCellType()) {
case NUMERIC:
// Excel数字统一转整数字符串
return String.valueOf((long) cell.getNumericCellValue());
case STRING:
String str = cell.getStringCellValue().trim();
// 字符串也清洗掉小数点
return str.contains(".") ? str.substring(0, str.indexOf(".")) : str;
default:
return "0";
}
}
}
建议二:封装加密解密工具类
java
public class SecurityNumberHandler {
public static String encryptNumber(Double value) {
// 先格式化再加密,确保格式统一
String plain = new DecimalFormat("#").format(value);
return AESUtil.encrypt(plain);
}
public static Integer decryptToInteger(String cipher) {
String plain = AESUtil.decrypt(cipher);
return new BigDecimal(plain).intValue(); // 安全转换
}
}
建议三:数据库字段设计反思
如果业务允许,建议:
- 不加密纯数字字段,用明文整数存储
- 如需加密,考虑使用数据库层加密(如MySQL的AES_ENCRYPT)
- 避免应用层加密导致类型信息丢失
5. 小结
| 方案 | 适用场景 | 侵入性 | 精度保留 | 推荐度 |
|---|---|---|---|---|
| 导入时强制转整 | 纯整数业务 | 低 | ❌ 否 | ⭐⭐⭐ |
DecimalFormat |
需要四舍五入 | 中 | ⚠️ 部分 | ⭐⭐⭐⭐ |
| 后置容错转换 | 历史数据兼容 | 低 | ✅ 是 | ⭐⭐⭐ |
BigDecimal全链路 |
企业级/金融级 | 高 | ✅ 是 | ⭐⭐⭐⭐⭐ |
6. 一键复制版补丁(推荐方案 D)
java
// 导入依赖:poi-ooxml 5.x
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import java.math.BigDecimal;
public final class ExcelNumberHandler {
private ExcelNumberHandler() {}
/**
* Excel单元格转加密字符串(整数)
*/
public static String cellToEncryptedInteger(Cell cell) {
if (cell == null || cell.getCellType() == CellType.BLANK) {
return encrypt("0");
}
BigDecimal decimal;
if (cell.getCellType() == CellType.NUMERIC) {
decimal = BigDecimal.valueOf(cell.getNumericCellValue());
} else {
String str = cell.getStringCellValue().trim();
decimal = new BigDecimal(str);
}
// 去除小数部分
return encrypt(decimal.toPlainString());
}
/**
* 解密字符串转Integer
*/
public static Integer decryptToInteger(String cipher) {
String plain = decrypt(cipher);
return new BigDecimal(plain).intValue();
}
// 模拟加密方法
private static String encrypt(String plain) {
return Base64.getEncoder().encodeToString(plain.getBytes());
}
// 模拟解密方法
private static String decrypt(String cipher) {
return new String(Base64.getDecoder().decode(cipher));
}
}
7. 踩坑复盘
教训 :数据在跨系统/跨格式 流转时,类型信息极易丢失 。Excel的
1.00≠ Java的"1"≠ 数据库的1。
** checklist **:
- Excel导入时明确数据类型(数字/文本/日期)
- 转换字符串前做格式化 ,而非直接
toString() - 加密字段在解密后必须进行合法性校验
- 生产环境务必打印原始值和转换值便于排查
8. 参考资料
- POI官方文档:Cell类型处理
- 《阿里巴巴Java开发手册》:第5章 日期与时间规约
- Java官方文档:BigDecimal vs Double
如果本文帮到你,记得点赞收藏!评论区一起交流更多Excel导入的坑位~