前言
最近遇到了使用mysql的json類型字段的解析問題,之前的開發的時候,剛開始用的就是mybatis,爲了解決json字段的問題,有的同事是把json字段映射成Java裏的String,手動在業務代碼裏轉化,也有同事嘗試用typeHandler自動解析,可惜沒成功,最近我接受這部分代碼,花了一天的時間才完成自動解析的配置。
目的
最終的目的是希望json字段能自動映射成java對象。
基本情況說明
Java表對應的java實體
TeacherDO {
private Student student;
get(); // 省略
set(); // 省略
}
表:
create table teacher (
student json // 省略
)
tracher.xml:
<select resultType="teacher">
select student from teacher
</select>
<insert>
insert into teacher (student)
values(#{student)
</insert>
只寫了關鍵的內容,其它都忽略。
問題
如果在上述情況下使用,使用會報錯
org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.reflection.ReflectionException: Could not set property 'student' of 'class com.xxx.Student' with value 'xxxx' Cause: java.lang.IllegalArgumentException: argument type mismatch
at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:78)
at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:440)
at com.sun.proxy.$Proxy175.selectList(Unknown Source)
at org.mybatis.spring.SqlSessionTemplate.selectList(SqlSessionTemplate.java:223)
at org.apache.ibatis.binding.MapperMethod.executeForMany(MapperMethod.java:147)
at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:80)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:57)
at com.sun.proxy.$Proxy176.findBy(Unknown Source)
這個錯誤信息非常清晰,student字段的類型錯誤,無法匹配,原因也很明確,表中是json 字段,接收對象中student是對象Student。
開始解決
基於以上錯誤信息,我的第一想法是mybatis是不是還不支持json字段自動轉對象,我知道了官網的typeHandler的說明(官網地址),
從官網說明來看,實際是不支持自動轉化。
因此,開始考慮實現一個自定義的typeHandler來解決。
現在我需要決定需要創建幾個JSONTypeHandler,因爲自定義typeHandler一般都是繼承下面這個類:
public abstract class BaseTypeHandler<T> extends TypeReference<T> implements TypeHandler<T> {
/**
* @deprecated Since 3.5.0 - See https://github.com/mybatis/mybatis-3/issues/1203. This field will remove future.
*/
@Deprecated
protected Configuration configuration;
// 省略
}
自定義實現的時候需要決定自己的typeHandler要解決的類型是什麼,也就是泛型T。
有兩種實現方式:
第一種、指定具體的java類型:
public class StudentTypeHandler extends BaseTypeHandler<Student> {
// 省略
}
第二種、不指定具體的T,仍然使用泛型,通過配置javaType指定java類型
public class JsonTypeHandler<T extends Object> extends BaseTypeHandler<T> {
// 省略
}
考慮到未來可能有更多的json字段,因此決定使用第二種,完整的JsonTypeHandler :
package com.xxx.mybatis.handler;
import java.io.IOException;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.exceptions.PersistenceException;
import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedJdbcTypes;
/**
* Jackson 實現 JSON 字段類型處理器
*
* @author <a href="[email protected]">xinfeng</a>
* @date 2019/11/7 12:29
*/
@Slf4j
@MappedJdbcTypes(JdbcType.VARCHAR)
public class JacksonTypeHandler<T extends Object> extends BaseTypeHandler<T> {
private static ObjectMapper objectMapper;
private Class<T> type;
static {
objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
public CommonJacksonTypeHandler(Class<T> type) {
if (log.isTraceEnabled()) {
log.trace("JacksonTypeHandler(" + type + ")");
}
if (null == type) {
throw new PersistenceException("Type argument cannot be null");
}
this.type = type;
}
private T parse(String json) {
try {
if (json == null || json.length() == 0) {
return null;
}
return objectMapper.readValue(json, type);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String toJsonString(T obj) {
try {
return objectMapper.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
return parse(rs.getString(columnName));
}
@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
return parse(rs.getString(columnIndex));
}
@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
return parse(cs.getString(columnIndex));
}
@Override
public void setNonNullParameter(PreparedStatement ps, int columnIndex, T parameter, JdbcType jdbcType)
throws SQLException {
ps.setString(columnIndex, toJsonString(parameter));
}
}
這樣就實現了一個JsonTypeHandler,把對象轉化爲字符串(VARCHAR),用於解析json字段。
開始使用
基於以上分析決策,已經實現了typeHandler,現在開始使用。
因爲自定義的typeHandler指定的是java類型是泛型T,所以無法使用下面的配置:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
// 省略
<typeHandlers>
<typeHandler handler="com.xxx.JacksonTypeHandler"/>
</typeHandlers>
// 省略
</configuration>
爲什麼無法使用?
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
if (javaTypeClass != null) {
try {
Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
return (TypeHandler<T>) c.newInstance(javaTypeClass);
} catch (NoSuchMethodException ignored) {
// ignored
} catch (Exception e) {
throw new TypeException("Failed invoking constructor for handler " + typeHandlerClass, e);
}
}
try {
// 這一步會報錯
Constructor<?> c = typeHandlerClass.getConstructor();
return (TypeHandler<T>) c.newInstance();
} catch (Exception e) {
throw new TypeException("Unable to find a usable constructor for " + typeHandlerClass, e);
}
}
因爲使用的是泛型,所以mybatis反射通過構造方法實例化時會報錯,報錯原因是沒有具體的類型。
既然這種無法使用,只能在mapper.xml中使用。
<resultMap>
<result column="student" property="student"
typeHandler="com.xxx.Student"
javaType="com.xxx.JacksonTypeHandler"/>
</resultMap>
<select resultType="teacher">
select student from teacher
</select>
<insert>
insert into teacher (student)
values(#{student ,javaType=com.xxx.Student, typeHandler = com.xxx.JacksonTypeHandler)
)
</insert>
javaType用於指定,typeHandler的泛型T的具體類型,這樣查詢和插入就都能自動解析了。
優化
每個typeHandler的寫的時候名字都太長,能不能像alias對象一樣使用暱稱?
經過驗證,不行。
按照上述思路解決,還是無法解決問題,如何定位自己的問題
找到mybatis的DefaultResultSetHandler的 applyPropertyMappings 方法,這個方法用來遍歷解析查詢到的數據
private boolean applyPropertyMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
final List<String> mappedColumnNames = rsw.getMappedColumnNames(resultMap, columnPrefix);
boolean foundValues = false;
final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
for (ResultMapping propertyMapping : propertyMappings) {
String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
if (propertyMapping.getNestedResultMapId() != null) {
// the user added a column attribute to a nested result map, ignore it
column = null;
}
if (propertyMapping.isCompositeResult()
|| (column != null && mappedColumnNames.contains(column.toUpperCase(Locale.ENGLISH)))
|| propertyMapping.getResultSet() != null) {
// TODO 這一行是解析數據
Object value = getPropertyMappingValue(rsw.getResultSet(), metaObject, propertyMapping, lazyLoader, columnPrefix);
// issue #541 make property optional
final String property = propertyMapping.getProperty();
if (property == null) {
continue;
} else if (value == DEFERRED) {
foundValues = true;
continue;
}
if (value != null) {
foundValues = true;
}
if (value != null || (configuration.isCallSettersOnNulls() && !metaObject.getSetterType(property).isPrimitive())) {
// gcode issue #377, call setter on nulls (value is not 'found')
metaObject.setValue(property, value);
}
}
}
return foundValues;
}
getPropertyMappingValue方法:
private Object getPropertyMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
throws SQLException {
if (propertyMapping.getNestedQueryId() != null) {
return getNestedQueryMappingValue(rs, metaResultObject, propertyMapping, lazyLoader, columnPrefix);
} else if (propertyMapping.getResultSet() != null) {
addPendingChildRelation(rs, metaResultObject, propertyMapping); // TODO is that OK?
return DEFERRED;
} else {
// TODO 這一步可以確認自定義的typeHandler是不是正確的
final TypeHandler<?> typeHandler = propertyMapping.getTypeHandler();
final String column = prependPrefix(propertyMapping.getColumn(), columnPrefix);
// TODO 這一步用來調自定義的typeHandler的數據解析方法
return typeHandler.getResult(rs, column);
}
}
經過這兩步判斷一般都能判斷出自己的typeHandler爲什麼不能正常起作用。
總結
問題溯本歸源,總能定位具體的原因的,分析一下過程有助於解決同類問題。