前情提要

在上一次的需求中,为了实现两个JSON字段的映射,我们自定义了一个AbstractJsonTypeHandler类,然后为每一个实体类定义了各自的TypeHandler。详情可以看上一篇关于TypeHandler的博客:在Mybatis Plus中使用TypeHandler映射PostgreSQL内的JSON字段。那一个实现虽然能完成需求,但是实在不优雅。对于写代码有强迫症的我,自然需要优化一下方案。

FastjsonTypeHandler

简介

FastjsonTypeHandler是MybatisPlus提供的默认Handler之一,它是继承了AbstractJsonTypeHandler<Object>。可以实现各种实体类到JSON字符串的映射。总体的设计思想跟我之前想的很类似,这依旧是使用了模板方法的设计模式。继承AbstractJsonTypeHandler<Object>的TypeHandler还有JacksonTypeHandler。

源码解析

我们直接看源码,首先是AbstractJsonTypeHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public abstract class AbstractJsonTypeHandler<T> extends BaseTypeHandler<T> {

@Override
public void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
ps.setString(i, toJson(parameter));
}

@Override
public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
final String json = rs.getString(columnName);
return StringUtils.isBlank(json) ? null : parse(json);
}

@Override
public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
final String json = rs.getString(columnIndex);
return StringUtils.isBlank(json) ? null : parse(json);
}

@Override
public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
final String json = cs.getString(columnIndex);
return StringUtils.isBlank(json) ? null : parse(json);
}

protected abstract T parse(String json);

protected abstract String toJson(T obj);
}

可以看到,它把通用逻辑给定义成了模板方法,留出了parsetoJson方法供子类实现。

再查看FastjsonTypeHandler是如何实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@MappedTypes({Object.class})
@MappedJdbcTypes({JdbcType.VARCHAR})
public class FastjsonTypeHandler extends AbstractJsonTypeHandler<Object> {
private static final Logger log = LoggerFactory.getLogger(FastjsonTypeHandler.class);
private final Class<?> type;

public FastjsonTypeHandler(Class<?> type) {
if (log.isTraceEnabled()) {
log.trace("FastjsonTypeHandler(" + type + ")");
}

Assert.notNull(type, "Type argument cannot be null", new Object[0]);
this.type = type;
}

protected Object parse(String json) {
return JSON.parseObject(json, this.type);
}

protected String toJson(Object obj) {
return JSON.toJSONString(obj, new SerializerFeature[]{SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteNullStringAsEmpty});
}
}

我们需要关注的就是 type 变量,可以观察到它由 public FastjsonTypeHandler(Class<?> type) 这个构造方法传入。实例化TypeHandler的操作是在Mybatis内部进行的。以下是对应的源码:

1
2
3
4
5
6
7
8
public <T> TypeHandler<T> getInstance(Class<?> javaTypeClass, Class<?> typeHandlerClass) {
if (javaTypeClass != null) {
Constructor<?> c = typeHandlerClass.getConstructor(Class.class);
return (TypeHandler<T>) c.newInstance(javaTypeClass);
}
Constructor<?> c = typeHandlerClass.getConstructor();
return (TypeHandler<T>) c.newInstance();
}

可以看到,当javaTypeClass被指定时,会通过反射的方式获得参数列表为Class.class的构造方法,然后传入javaTypeClass进行实例化返回。若为空则调用无参构造方法。

那么FastjsonTypeHandler是如何注册的呢?

Mybatis那些自带的TypeHandler都是通过register方法注册的,register方法有大量的重载方法,可以供不同的入参完成注册。它们之间的调用关系可以用下图表示:

image-20230806143941665.png

但是在MybatisPlus中,它是这样实例化并指定该字段的TypeHandler的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ResultMapping getResultMapping(final Configuration configuration) {
ResultMapping.Builder builder = new ResultMapping.Builder(configuration, property,
StringUtils.getTargetColumn(column), propertyType);
TypeHandlerRegistry registry = configuration.getTypeHandlerRegistry();
if (jdbcType != null && jdbcType != JdbcType.UNDEFINED) {
builder.jdbcType(jdbcType);
}
if (typeHandler != null && typeHandler != UnknownTypeHandler.class) {
TypeHandler<?> typeHandler = registry.getMappingTypeHandler(this.typeHandler);
if (typeHandler == null) {
typeHandler = registry.getInstance(propertyType, this.typeHandler);
// todo 这会有影响 registry.register(typeHandler);
}
builder.typeHandler(typeHandler);
}
return builder.build();
}

这段代码的意思是:

  • 第8行 :如果指定了这个变量的typeHandler类型,就进入处理逻辑

  • 第9行:尝试从已注册的typeHandler的map中获取这个typeHandler类型的实例

  • 第10~11行:如果这个类型的typeHandler还没被注册,就调用 registry.getInstance(propertyType, this.typeHandler) 。propertyType就是该字段的类型。之后指定实例化的FastjsonTypeHandler作为该字段的typeHandler。

注意:MybatisPlus并不会帮我们注册TypeHandler,而只是实例化+指定。

问题

那么现在的问题就在于,这里的propertyType是否保留了泛型呢?很遗憾答案是否定的,这里的propertyType已经仅剩下了List

image-20230806231140557.png
这样的话它依旧是无法正确的处理集合类型。无奈只能继续使用数组了……。

修改

autoResultMap

要使用FastjsonTypeHandler需要在表实体类上标注 @TableName(value = "problem",autoResultMap = true) 。不然上述的 getResultMapping 方法就不会被调用,也就无法实例化、指定typehandler了。

typeHandler

在对应的字段上添加 @TableField(typeHandler = FastjsonTypeHandler.class) 指定TypeHandler的类型,这也是上述 getResultMapping 方法里 typeHandler = registry.getInstance(propertyType, this.typeHandler);this.typeHandler的入参。

修改后的实体类为:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "problem",autoResultMap = true)
public class Problem {

@TableField(typeHandler = FastjsonTypeHandler.class)
private Test[] test;

@TableField(typeHandler = FastjsonTypeHandler.class)
private Solution[] solution;

}

我之前自定的TypeHandler和注册操作的Bean也可以删除了。