在Java springboot+mybatis项目里,通过sql查询user表,mapper蹭有一个方法 List<Map<String, Object>> queryData(String sql); 将user表的数据查询出来。
queryData方法的返回类型是一个List<Map<String, Object>> 类型,Map中的key是user表中的字段名,value是字段对应的值。
现在发现user表中 price 的值为 0是,通过 queryData 查询出来pricede 值变成了科学计数法。
怎么解决让他显示正常的数字而不是科学计数法。
mysql user表里有一个 price字段,类型是 decimal(12,30)。
user表的结构如下:
price 字段的类型是 decimal(25, 13)。比如 更新数据为 0:update user set price = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`userName` varchar(30) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '姓名',
`age` int(11) NULL DEFAULT NULL COMMENT '年龄',
`userAccount` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户账号',
`userPassword` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户账号密码',
`phone` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '电话',
`email` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',
`userRole` int(11) NOT NULL DEFAULT 0 COMMENT '用户角色: 0-普通用户 1-管理员用户',
`avatarUrl` varchar(512) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`userStatus` int(11) NOT NULL DEFAULT 0 COMMENT '状态:0-正常',
`gender` tinyint(4) NULL DEFAULT NULL COMMENT '性别:1-男,0-女',
`createTime` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0) COMMENT '更新时间',
`isDelete` tinyint(4) NOT NULL DEFAULT 1 COMMENT '逻辑删除:1,表示未删除 0,表示已删除',
`tags` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标签列表',
`price` decimal(25, 13) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 31 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
public interface UserMapper {
List<Map<String, Object>> queryData(@param("sql") String sql);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mhd.UserMapper">
<select id="queryData" resultType="java.util.Map">
${sql}
</select>
</mapper>
@SpringBootTest
public class Test01 {
@Autowired
private UserMapper userMapper;
@Test
void testBigDecimal() {
String sql = "select price from user";
List<Map<String, Object>> mapList = userMapper.queryData(sql);
System.out.println("查询结果:");
}
}
在测试类中 mapList 查询出来的数据 price 变成了科学计数法 “0E-13”。
当 MySQL 的 DECIMAL(25, 13)
类型值(如 0)被 MyBatis 映射为 BigDecimal
类型时,在转换为字符串时会默认使用科学计数法表示(如 0E-30
)。这是因为 BigDecimal 的 toString()
方法在值较小时会自动转为科学计数法。
当数据库中 price 的值为 0时,并且是DECIMAL(25, 13)类型,此时数据库中定义的小数位比较长,当price在表里存储的值为 0 时,实际上存储的是0.0000000000000(13位小数)。
在Java中,当使用JDBC从结果集获取这个值时,会得到一个BigDecimal对象。而BigDecimal的toString()方法在遇到小数位数很多且值为0的情况下,会使用科学计数法表示,即0E-15。
解释:
BigDecimal的toString()方法在以下情况会使用科学计数法:
- 如果值很小(绝对值小于0.001)或者很大(绝对值大于等于10^7)且小数部分有有效数字,但这里0是一个特殊情况。
- 对于0,如果它的标度(scale)是负数,则表示为0,如果标度为正数,则可能用科学计数法表示。例如,标度为15,则0会表示为0E-15。
修改 SQL 查询语句,使用 MySQL 的 CAST()
或 FORMAT()
函数强制转换为字符串:
<!-- Mapper.xml -->
<select id="queryData" resultType="map">
SELECT
${otherColumns},
CAST(price AS CHAR) AS price <!-- 关键转换 -->
FROM user
<!-- 或使用 FORMAT(price, 30) 保留小数 -->
</select>
创建专用类型处理器,重写 BigDecimal 的转换逻辑:
import org.apache.ibatis.type.*;
import java.math.BigDecimal;
import java.sql.*;
// 使用注解声明处理的类型(非必须但推荐)
@MappedTypes(BigDecimal.class)
@MappedJdbcTypes(JdbcType.DECIMAL)
public class PlainBigDecimalTypeHandler extends BaseTypeHandler<BigDecimal> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, BigDecimal parameter, JdbcType jdbcType)
throws SQLException {
ps.setBigDecimal(i, parameter);
}
@Override
public BigDecimal getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return rs.getBigDecimal(columnName);
}
// 其他必要方法...
// 重点:覆盖结果转换逻辑
@Override
public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
BigDecimal value = cs.getBigDecimal(columnIndex);
return value != null ? value.toPlainString() : null; // 使用 toPlainString()
}
}
注册处理器:在mybatis的配置文件里注册自定义的处理器
<!-- mybatis-config.xml -->
<typeHandlers>
<typeHandler handler="com.example.PlainBigDecimalTypeHandler"/>
</typeHandlers>
在 Service 层对查询结果进行格式化:
public List<Map<String, Object>> queryData(String sql) {
List<Map<String, Object>> result = mapper.queryData(sql);
// 遍历结果集格式化数值
result.forEach(map -> map.replaceAll((key, value) -> {
if ("price".equalsIgnoreCase(key) && value instanceof BigDecimal) {
return ((BigDecimal) value).stripTrailingZeros().toPlainString();
}
return value;
}));
return result;
}
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
SQL转换 | 简单直接,无代码侵入 | 需修改所有相关查询 | 少量固定查询 |
类型处理器 | 一劳永逸,自动处理所有映射 | 需注册全局处理器 | 项目中有大量DECIMAL字段 |
结果集处理 | 不修改SQL/映射,灵活通用 | 每次查询都需遍历结果 | 动态SQL/通用查询方法 |
精确值场景(如金融系统):
// 使用 setScale 控制精度
BigDecimal value = rs.getBigDecimal("price");
return value.setScale(30, RoundingMode.HALF_UP).stripTrailingZeros();
避免科学计数法的关键方法:
// 正确方式
bigDecimal.toPlainString();
// 错误方式(会产生科学计数法)
bigDecimal.toString();
全局配置建议(在应用启动时):
// 禁用科学计数法(影响所有BigDecimal)
System.setProperty("bigdecimal.formatter", "plain");
转换前(科学计数法):
{"price": 0E-30}
转换后(普通数字):
{"price": "0.000000000000000000000000000000"}
选择最适合你业务场景的方案即可彻底解决此问题。
对于通用查询方法,推荐使用方案3;
对于固定字段映射,方案2更优雅。
mybatis-config.xml
文件,可以通过以下三种方式注册自定义类型处理器(TypeHandler)实现全局生效:@Configuration
配置类注册(推荐)import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.type.TypeHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisConfig {
/**
* 注册自定义类型处理器
*/
@Bean
public ConfigurationCustomizer mybatisConfigurationCustomizer() {
return configuration -> {
// 注册单个处理器
configuration.getTypeHandlerRegistry().register(PlainBigDecimalTypeHandler.class);
// 或注册多个处理器
configuration.getTypeHandlerRegistry().register(new YourDateTypeHandler());
};
}
}
application.properties
自动扫描注册# application.properties
# 指定自定义TypeHandler所在的包(多个包用逗号分隔)
mybatis.type-handlers-package=com.example.handlers
或者 yml
# 指定自定义TypeHandler所在的包(多个包用逗号分隔)
mybatis:
type-handlers-package: com.example.handlers
TypeHandler
接口的类并注册。@MapperScan
注解注册在启动类或配置类中指定:
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(
basePackages = "com.example.mapper",
typeHandlers = {PlainBigDecimalTypeHandler.class, YourDateTypeHandler.class}
)
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
import org.apache.ibatis.type.*;
import java.math.BigDecimal;
import java.sql.*;
// 使用注解声明处理的类型(非必须但推荐)
@MappedTypes(BigDecimal.class)
@MappedJdbcTypes(JdbcType.DECIMAL)
public class PlainBigDecimalTypeHandler extends BaseTypeHandler<BigDecimal> {
@Override
public void setNonNullParameter(PreparedStatement ps, int i, BigDecimal parameter, JdbcType jdbcType)
throws SQLException {
ps.setBigDecimal(i, parameter);
}
@Override
public BigDecimal getNullableResult(ResultSet rs, String columnName)
throws SQLException {
return rs.getBigDecimal(columnName);
}
// 其他必要方法...
// 重点:覆盖结果转换逻辑
@Override
public Object getResult(CallableStatement cs, int columnIndex) throws SQLException {
BigDecimal value = cs.getBigDecimal(columnIndex);
return value != null ? value.toPlainString() : null; // 使用 toPlainString()
}
}
@Configuration
public class MyBatisConfig {
@Bean
public ConfigurationCustomizer typeHandlerRegistry() {
return configuration ->
configuration.getTypeHandlerRegistry().register(PlainBigDecimalTypeHandler.class);
}
}
@SpringBootTest
class UserMapperTest {
@Autowired
private UserMapper userMapper;
@Test
void testDecimalFormat() {
List<Map<String, Object>> result = userMapper.queryData("SELECT * FROM user");
result.forEach(row -> {
Object price = row.get("sale_price");
System.out.println(price.getClass() + ": " + price);
// 输出:java.math.BigDecimal: 0 (不再是0E-15)
});
}
}
处理器作用范围:
全局注册后,所有 BigDecimal 类型的字段都会使用该处理器
如需局部覆盖,可在 Mapper XML 中单独指定:
<result column="price" property="price"
typeHandler="com.example.AnotherTypeHandler"/>
处理精度问题:
// 在处理器中保留原始精度
private BigDecimal format(BigDecimal value) {
if (value == null) return null;
return value.setScale(15, RoundingMode.HALF_UP) // 保持15位小数
.stripTrailingZeros(); // 去除尾部零
}
处理 NULL 值:
rs.wasNull()
可检测数据库 NULL 值调试技巧:
@PostConstruct
public void logRegisteredHandlers() {
// 打印所有注册的处理器
SqlSessionFactory factory = ... // 注入SqlSessionFactory
factory.getConfiguration()
.getTypeHandlerRegistry()
.getTypeHandlers()
.forEach(handler ->
System.out.println("Registered: " + handler.getClass()));
}
注册方式 | 优点 | 适用场景 |
---|---|---|
@Configuration 配置类 | 灵活,可编程控制注册逻辑 | 需要条件注册处理器的场景 |
mybatis.type-handlers-package | 配置简单,零代码 | 处理器集中存放的简单项目 |
@MapperScan 注解 | 与Mapper扫描整合 | 小型项目快速集成 |
重要:避免重复注册!如果同时使用多种方式,可能导致处理器被注册多次,建议选择一种统一方式管理。
通过以上任一方式,都能让自定义类型处理器在 Spring Boot + MyBatis 项目中全局生效,解决科学计数法显示问题。