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<Map>类型接受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中查找handlerhandler = 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<JdbcType, TypeHandler> jdbcHandlerMap = getJdbcHandlerMap(type); //通过JDBC类型获取到了三个实现TypeHandler handler = null;if (jdbcHandlerMap != null) {handler = jdbcHandlerMap.get(jdbcType);if (handler == null) {handler = jdbcHandlerMap.get(null);}if (handler == null) {// #591handler = pickSoleHandler(jdbcHandlerMap);}}// type drives generics herereturn (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<JdbcType, TypeHandler> 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

@Configurationpublic class MybatisConfig {@Autowiredprivate List sqlSessionFactoryList;@PostConstructpublic void addSqlInterceptor() {SchemaParamsterInterceptor interceptor = new SchemaParamsterInterceptor();for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {...//注册json对象处理器//registryJsonObjectTypeHandler(sqlSessionFactory);}}private void registryJsonObjectTypeHandler(SqlSessionFactory sqlSessionFactory ){org.apache.ibatis.session.Configurationconfig = sqlSessionFactory.getConfiguration();//以下两个注册方法针对的私有对象不同,一个是jdbcTypeHandlerMap另一个是typeHandlerMapconfig.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是否成功,是否注册到了需要生效的位置。如果不通过分析代码分析流程自己永远只能在“黑盒”之外用一用。
遇到问题解决问题,是很好的学些过程,在时间和进度允许的的情况下,可以钻下“牛角尖”。