mysql8.0.17以下驱动导致mybatis blob映射String乱码问题分析与解决
mysql8.0.17以下驱动导致blob映射String乱码问题分析与解决
- 一、问题复现
- 二、问题深究
- 三、解决方法
- 方法1
- 方法2
一、问题复现
1、docker安装mysql8.0,并创建测试数据库及测试数据表
CREATE DATABASE `test` DEFAULT CHARACTER SET utf8mb4;
use test;
CREATE TABLE `test_content` (`id` int NOT NULL,`content` varchar(255) DEFAULT NULL,`b_content` blob,`lb_content` longblob,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2、使用mysql8.0.16驱动、mybatis、spring-boot搭建测试项目
2.1 实体类
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class TestContent {private int id;private String content;private String bContent;private String lbContent;
}
2.2 mapper
@Mapper
public interface TestContentMapper {@Select("select * from test_content where id = #{id}")TestContent selectOne(@Param("id") int id);@Select("select * from test_content where id = #{id}")Map<String, Object> selectOneMap(@Param("id") int id);@Update("update test_content set content=#{p.content}, b_content=#{p.bContent}, lb_content=#{p.lbContent} where id=#{p.id}")int update(@Param("p") TestContent p);@Insert("insert into test_content(id, content, b_content, lb_content) values (#{p.id}, #{p.content}, #{p.bContent}, #{p.lbContent})")int insert(@Param("p") TestContent p);}
2.3 配置文件
spring:datasource:type: com.zaxxer.hikari.HikariDataSourceurl: jdbc:mysql:///test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTCusername: rootpassword: YOUR_PASSWORDdriver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration:map-underscore-to-camel-case: true
2.4 单元测试类
@SpringBootTest(classes = Application.class)
public class TestBlobConvert {@Resourceprivate TestContentMapper testContentMapper;@Testpublic void testBlobConvert() {int insert = testContentMapper.insert(TestContent.builder().id(1).content("红红火火恍恍惚惚aaa").bContent("红红火火恍恍惚惚aaa").lbContent("红红火火恍恍惚惚aaa").build());System.out.println("insert = " + insert);Map<String, Object> map = testContentMapper.selectOneMap(1);map.forEach((key, value) -> System.out.println("key = " + key+ ", value = " + value));TestContent testContent = testContentMapper.selectOne(1);System.out.println("testContent = " + testContent);testContent.setContent(testContent.getContent() + "q");int update = testContentMapper.update(testContent);System.out.println("update = " + update);TestContent testContent1 = testContentMapper.selectOne(1);System.out.println("testContent1 = " + testContent1);}
}
执行单元测试发现复现成功;控制台打印结果为:
insert = 1
key = b_content, value = [B@b5c6a30
key = lb_content, value = [B@3bfae028
key = id, value = 1
key = content, value = 红红火火恍恍惚惚aaa
testContent = TestContent(id=1, content=红红火火恍恍惚惚aaa, bContent=红红ç«ç«æææƒšæƒšaaa, lbContent=红红ç«ç«æææƒšæƒšaaa)
update = 1
testContent1 = TestContent(id=1, content=红红火火恍恍惚惚aaaq, bContent=红红ç«ç«æÂÂæÂÂ惚惚aaa, lbContent=红红ç«ç«æÂÂæÂÂ惚惚aaa)
二、问题深究
起初想法是直接自定义typeHandler一把梭,后来决定debug看一看发现,在驱动com.mysql.cj.jdbc.result.ResultSetImpl中public String getString(int columnIndex)时乱码就已经产生;
继续往下com.mysql.cj.protocol.a.result.TextBufferRow#getValue;
然后是com.mysql.cj.protocol.result.AbstractResultsetRow#decodeAndCreateReturnValue;
再然后com.mysql.cj.protocol.a.MysqlTextValueDecoder#decodeByteArray;
最后com.mysql.cj.result.StringValueFactory#createFromBytes中
public String createFromBytes(byte[] bytes, int offset, int length, Field f) {return StringUtils.toString(bytes, offset, length, f.getEncoding());
}
一看这个f.getEncoding()是ISO-8859-1
随后看了下8.0.17版本这个位置的代码,做了处理,使用url上的characterEncoding编码进行解码
public String createFromBytes(byte[] bytes, int offset, int length, Field f) {return StringUtils.toString(bytes, offset, length,f.getCollationIndex() == CharsetMapping.MYSQL_COLLATION_INDEX_binary ? this.pset.getStringProperty(PropertyKey.characterEncoding).getValue(): f.getEncoding());}
驱动改成8.0.17后清理数据执行一下看看
insert = 1
key = b_content, value = [B@5042e3d0
key = lb_content, value = [B@1c34365c
key = id, value = 1
key = content, value = 红红火火恍恍惚惚aaa
testContent = TestContent(id=1, content=红红火火恍恍惚惚aaa, bContent=红红火火恍恍惚惚aaa, lbContent=红红火火恍恍惚惚aaa)
update = 1
testContent1 = TestContent(id=1, content=红红火火恍恍惚惚aaaq, bContent=红红火火恍恍惚惚aaa, lbContent=红红火火恍恍惚惚aaa)
好的,这样我们就有了一种不用动代码的解决方法
三、解决方法
方法1
升级mysql驱动至8.0.17或以上版本
方法2
驱动不想或不能升级,那自定义typehandler是一个好的解决方法,在mybatis中debug发现,数据字段为blob、longblob、tinyblob时,JDBCType为JdbcType.LONGVARBINARY
以下是自定义TypeHandler
@MappedJdbcTypes(JdbcType.LONGVARBINARY)
@MappedTypes(String.class)
public class BlobToStringTypeHandler extends BaseTypeHandler<String> {@Overridepublic void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {ps.setBytes(i, parameter.getBytes(StandardCharsets.UTF_8));}@Overridepublic String getNullableResult(ResultSet rs, String columnName) throws SQLException {return getStringFromBlob(rs.getBlob(columnName));}@Overridepublic String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {return getStringFromBlob(rs.getBlob(columnIndex));}@Overridepublic String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {return getStringFromBlob(cs.getBlob(columnIndex));}private String getStringFromBlob(Blob blob) throws SQLException {if (blob == null) return "";byte[] bytes = blob.getBytes(1, (int) blob.length());return new String(bytes, StandardCharsets.UTF_8);}
}
自定义后需要增加配置使其全局生效
mybatis:type-handlers-package: typeHandler所在包名
如果不想改配置文件,想通过代码的方式全局生效可以改一下sqlSessionFactory
@Configuration
public class MybatisConfig implements InitializingBean {@Autowiredprivate SqlSessionFactory sqlSessionFactory;@Overridepublic void afterPropertiesSet() {sqlSessionFactory.getConfiguration().getTypeHandlerRegistry().register("typeHandler所在包名");}
}
当然也可以在某个字段上单独生效,在查询的方法上增加
@Results(@Result(column = "b_content", property = "bContent",jdbcType = JdbcType.LONGVARBINARY, javaType = String.class,typeHandler = BlobToStringTypeHandler.class)
)
来点涩话
花点时间找找是否有更好的方法,结果会让你惊喜