java-Mybatis自定义JsonObjectTypeHandler动态解析数据库JSON类型数据

java-Mybatis自定义JsonObjectTypeHandler动态解析数据库JSON类型数据

环境

  • jdk 1.8
  • springboot 1.5.6
  • PostgreSQL 14.5
  • mybatis 3.53
  • postgresql 42.2.1

引言

主流数据库对Json数据类型都有了支持,但是Mybatis中并没有很好地支持,必须自己编写TypeHandler进行处理。最近用pg库时遇到了json类型数据的查询解析问题,也查了不少资料启发很大,但是没有找到满足我需求的相关资料。所以对代码进行了跟踪,分析了Mybaits的PGProvider默认数据类型转换部分的代码逻辑,解决了相关的通用查询json解析问题。
遇到的问题是,服务查询的表名、字段名都不确定,所以不能提前生成一堆实体模型。只能通过表名+字段名动态生成SQL进行动态查询,为了使查询结果通用,我们使用了List>类型接受Mybatis查询结果,List中的每个元素表示一行数据,Map为字段和值的对应关系,最后服务报文以Json类型返回。

handler的作用?

当使用默认的查询转换Json类型数据时,会输出以下格式内容:

{
    "type":"json",
    "value":"{\"\aa":\"11\",\"bb\":\"22\"}"
}

这种格式必然有个Handler与之对应,找到这个Handler并替换掉应该就能解决我们的问题;

定位Handler的获取逻辑

一路单步跟下去,发现几个底层转换方法需要认真分析下:

//解析妹一行的结果值
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#getRowValue(org.apache.ibatis.executor.resultset.ResultSetWrapper, org.apache.ibatis.mapping.ResultMap, java.lang.String)
//创建字段和java类型的映射关系
org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createAutomaticMappings()
//获取jdbcType类型使用的handler对象
org.apache.ibatis.executor.resultset.ResultSetWrapper#getTypeHandler(Class propertyType, String columnName)

getTypeHandler()方法

//其中的一条命令是使用java类型+jdbc类型在typeHandlerRegistry中查找handler
handler = typeHandlerRegistry.getTypeHandler(propertyType, jdbcType);
//getTypeHandler的实现不多贴出来瞧瞧
//org.apache.ibatis.type.TypeHandlerRegistry#getTypeHandler(java.lang.reflect.Type, org.apache.ibatis.type.JdbcType)
private  TypeHandler getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
    return null;
}
Map> jdbcHandlerMap = getJdbcHandlerMap(type); //通过JDBC类型获取到了三个实现
TypeHandler handler = null;
if (jdbcHandlerMap != null) {
    handler = jdbcHandlerMap.get(jdbcType);
    if (handler == null) {
    handler = jdbcHandlerMap.get(null);
    }
    if (handler == null) {
    // #591
    handler = pickSoleHandler(jdbcHandlerMap);
    }
}
// type drives generics here
return (TypeHandler) handler;
}
//上面方法中的jdbcHandlerMap内容为:
{null=class java.lang.Object, OTHER=class java.lang.Object, ARRAY=class java.lang.Object}
//由于没有对应的handler,所以最总new了一个默认的handler:handler = new ObjectTypeHandler();
//org.apache.ibatis.type.ObjectTypeHandler
//以上为handler的查找定位,接下来的代码是比较有意思的,有关数据库中的数据是如何转为java对象的。(题外话了)
//org.postgresql.jdbc.PgResultSet#internalGetObject
//org.postgresql.jdbc.PgResultSet#getString(int)  --获取一个字符
public String getString(int columnIndex) throws SQLException {
    ...
    Encoding encoding = connection.getEncoding();
    try {
      /*
      1.this_row表示一行的数据,使用byte[][]类型的二维数据表示,第一维是字段索引,第二维是字段值,知道jdbcType就可以将不同类型转为Java类型了
      2.byte的取值范围是[-128, 127],是不是就限制了每个pg的每个表最多只能有0~127个索引共128个字段呢???
      */
      return trimString(columnIndex, encoding.decode(this_row[columnIndex - 1]));
    } catch (IOException ioe) {
      ...
    }
}

通过以上定位发现所有的handler都是间接从configuration的TypehandlerRegisty对象的typeHandlerMap私有属性中获得。
看看typehandlerMap的赋值方法,发现调用复制到这个是register(…)方法,数了下居然有12个重载-_-!!!
不过针对这个私有变脸赋值的方法只有一个,就是:

  private void register(Type javaType, JdbcType jdbcType, TypeHandler handler) {
    if (javaType != null) {
      Map> map = typeHandlerMap.get(javaType);
      if (map == null || map == NULL_TYPE_HANDLER_MAP) {
        map = new HashMap<>();
        typeHandlerMap.put(javaType, map);
      }
      map.put(jdbcType, handler);
    }
    allTypeHandlersMap.put(handler.getClass(), handler);
  }

通过跟踪handler对象的获取过程,知道3个关键的参数:

  1. db中的json用jdbcType.OTHER表示
  2. db中的json用java.Object表示
  3. 就是我们需要实现的自定handler了,继承自BaseTypeHandler<>抽象类

自定handler的注册

对于注册我用的方法是,通过@autowired获得到了已注册的所有SqlSessionFactory,遍历SqlSessionFactory为每个实例注册handler


@Configuration
public class MybatisConfig {
    @Autowired
    private List sqlSessionFactoryList;
    @PostConstruct
    public void addSqlInterceptor() {
        SchemaParamsterInterceptor interceptor = new SchemaParamsterInterceptor();
        for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
            ...
            //注册json对象处理器
            //registryJsonObjectTypeHandler(sqlSessionFactory);
        }
    }
    private void registryJsonObjectTypeHandler(SqlSessionFactory sqlSessionFactory ){
        org.apache.ibatis.session.Configuration  config = sqlSessionFactory.getConfiguration();
        //以下两个注册方法针对的私有对象不同,一个是jdbcTypeHandlerMap另一个是typeHandlerMap
        config.getTypeHandlerRegistry().register(JdbcType.OTHER, new JsonObjectTypeHandler());
        config.getTypeHandlerRegistry().register(Object.class,JdbcType.OTHER, new JsonObjectTypeHandler());
    }
}

JsonObjectTypeHandler的实现

既然要实现自定的Typehandler,自然要继承BaseTypeHandler基类,其中的T为泛型类型,下面讨论下T的确定过程。

T为com.alibaba.fastjson.JSONObject

要返回json字符串,用到又是com.alibaba.fastjson,第一个想法就是使用JSONObject类型了;
数据库中存的为{“a”:“1”,“b”:“2”},查询下非常完美正常解析了。
但是如果db中存的是数组内?如:[{“a”:“1”,“b”:“2”},…] 或 [“a”,“b”,…],这时自定义的转换失败。
fastjson中有个类型专门的处理数组,对象是com.alibaba.fastjson.JSONArray。

T为com.alibaba.fastjson.JSONArray

这时数组类型处理的没问题了,但是把[] 去掉。逻辑又异常了!!!!
{“a”:“1”,“b”:“2”},[{“a”:“1”,“b”:“2”},…] 这两类json如何共存呢?
JSONObject和JSONArray,是个包含关系,只需要将JSONObjet加个[]就可以变为JSONArray类型。
虽然能解决问题,但是和db中存的数据发生了天壤之别,这个方案也不可取。

T为java.lang.Object

为什么要拘泥于实现类呢?JSONObject和JSONArray也是有共同点的,顶层必然继承自Object对象。
且json字符串数组和对象区别也比较好区分,如果是中扩招([)开头的字符串就是数组了。所以通过判断是否为数据,
使用fastjson的两个不同转换方法去做转换,通过Object去接收转换后的对象实例,完美解决了数组和对象的问题。

总结

配置并使用自定义的TypeHandler比较容易,继承个基类实现几个方法就OK了。不过总有一种猜的成分在里面,不知道他是如何工作如何实现的,
甚至不清楚我自定义的handler是否成功,是否注册到了需要生效的位置。如果不通过分析代码分析流程自己永远只能在“黑盒”之外用一用。
遇到问题解决问题,是很好的学些过程,在时间和进度允许的的情况下,可以钻下“牛角尖”。

你可能感兴趣的:(mybatis,java,数据库)