使用mybatis动态加载外部sql

编程

思路

怎么解决? 说说我和小伙伴D的思路:

回顾下需求场景, 提供无业务逻辑, 只返回sql查询结果的接口. 也就是说, 如果有这样一个接口, 可以每次执行我写的sql, 那问题就解决了, 所以我们的目标就是: 把sql写到一个地方(DB), 然后接口获取sql, 并执行返回执行结果.

实现

我和D开始觉得并不难, 将sql存到DB, 然后读取, 利用mybatis执行. 但是在执行这步就卡住了, 如果是简单的sql, 比如

select * from user where name = ? and age = ?

的确可以实现, 比如使用mybatis提供的@SelectProvider注解, 在方法selectUserSql中拼接参数, 然后执行.

@SelectProvider(value = UserService.class, method = "selectUserSql")

List<User> selectDyn(SQL sql, Map<String, Object> parameterMap);

但是如果稍微复杂一点, 比如name非必填, 那这的处理想想就头大(开始还想着要不要自己实现一套解析工具)...

和D商量, 既然mybatis已经有一套完整的sql解析工具, 我们直接拿来用就好了, 既省去了自己开发的工作量, 又可靠(是不是瞧不起我! 嗯~).

mybatis加载解析过程概述

说干就干, 从看mybatis源码着手, 发现了点门道. 一般使用mybatis代码如下

// 配置文件以流的形式加载到内存

InputStream inputStreamXML = Resources.getResourceAsStream("mybatis-config.xml");

// 构造工厂

SqlSessionFactory sqlSessionFactoryXML = new SqlSessionFactoryBuilder().build(inputStreamXML);

// sqlSession

SqlSession sqlSessionXML = sqlSessionFactoryXML.openSession();

// 获取对应Mapper

UserMapper userMapper = sqlSessionXML.getMapper(UserMapper.class);

// 执行

System.out.println("xml : " + userMapper.queryById(1));

看着代码我们从加载配置文件唠起, 首先我们测试代码的配置信息如下

<configuration>

<environments default="development">

<environment id="development">

<transactionManager type="JDBC"/>

<dataSource type="POOLED">

<property name="driver" value="com.mysql.cj.jdbc.Driver"/>

<property name="url" value="jdbc:mysql://127.0.0.1:3306/xxx"/>

<property name="username" value="xxx"/>

<property name="password" value="xxxxxx"/>

</dataSource>

</environment>

</environments>

<mappers>

<mapper resource="UserMapper.xml"/>

</mappers>

</configuration>

流程大概这样, 用于配置参数太多, 通过工厂的builder创建工厂类, 先构造一个解析配置文件的工具, 然后一点点解析, 将解析结果放到configuration对象中, 然后使用该对象构造工厂对象.

由于我们的目标是动态载入sql, 所以我们重点看下Mapper的解析

解析分为两类, 一个是package标签, 一个是Mapper标签, 这里是Mapper标签. Mapper标签下又分为三种resource, url, class(就是加载方式不一样), 接下来会加载Mapper标签指定的文件信息, 也就是UserMapper.xml, 内容如下:

<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.togo.repository.UserMapper">

<resultMap type="com.togo.entity.User" id="UserMap">

<result property="id" column="id" jdbcType="INTEGER"/>

<result property="xx" column="xx" jdbcType="VARCHAR"/>

<result property="appid" column="appid" jdbcType="VARCHAR"/>

<result property="nickname" column="nickname" jdbcType="VARCHAR"/>

<result property="passtest" column="passtest" jdbcType="INTEGER"/>

</resultMap>

<select id="queryById" resultMap="UserMap">

select

id, xx, appid, nickname, passtest

from wx.user

<where>

<if test="id != null">

and id = #{id}

</if>

</where>

</select>

</mapper>

跟解析配置文件的套路一致, 也是挨个标签的解析, 因为我们最初就是打算直接使用mybatis的解析工具, 所以不是很关心它是如何实现的, 我们只要知道怎么载入Mapper就可以了, 在这里出现了关键代码

org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement下

if (resource != null && url == null && mapperClass == null) {

ErrorContext.instance().resource(resource);

InputStream inputStream = Resources.getResourceAsStream(resource);

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());

mapperParser.parse();

}

这里我们完全可以拿出来加载我们的mapper,

// mapper就是xml中的字符串

InputStream inputStream = new ByteArrayInputStream(mapper.getBytes());

Configuration configuration = sqlSessionFactoryXML.getConfiguration();

ErrorContext.instance().resource("resource");

XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, "resource", configuration.getSqlFragments());

mapperParser.parse();

debug中发现已经加载到configuration对象中了~

执行

加载完成后就是执行, 我们在看下正常的执行代码

SqlSession sqlSessionXML = sqlSessionFactoryXML.openSession();

UserMapper userMapper = sqlSessionXML.getMapper(UserMapper.class);

System.out.println("xml : " + userMapper.queryById(1));

额...这个UserMapper怎么得到? 我们只是加载了一段字符串, 当然没有可以执行方法的Mapper类了, 那是不是说只要我们有一个这样的类就可以了! 那么就动态生成一个吧~

我们这里使用的是asm, 配合idea插件使用简单.

dependency>

<groupId>org.ow2.asm</groupId>

<artifactId>asm</artifactId>

<version>7.0</version>

</dependency>

准备生成的类

public interface TestMapper {

Map<String, Object> queryById(Integer id);

}

生成代码

public class MyClassLoader extends ClassLoader {

public static byte[] dump() throws Exception {

ClassWriter cw = new ClassWriter(0);

FieldVisitor fv;

MethodVisitor mv;

AnnotationVisitor av0;

cw.visit(52, ACC_PUBLIC + ACC_ABSTRACT + ACC_INTERFACE, "com/togo/asm/TestMapper", null, "java/lang/Object", null);

cw.visitSource("TestMapper.java", null);

{

mv = cw.visitMethod(ACC_PUBLIC + ACC_ABSTRACT, "queryById", "(Ljava/lang/Integer;)Ljava/util/Map;", "(Ljava/lang/Integer;)Ljava/util/Map<Ljava/lang/String;Ljava/lang/Object;>;", null);

mv.visitEnd();

}

cw.visitEnd();

return cw.toByteArray();

}

public Class<?> defineClass(String name, byte[] b) {

// ClassLoader是个抽象类,而ClassLoader.defineClass 方法是protected的

// 所以我们需要定义一个子类将这个方法暴露出来

return super.defineClass(name, b, 0, b.length);

}

}

执行!!!

// 生成二进制字节码

byte[] bytes = MyClassLoader.dump();

// 使用自定义的ClassLoader

MyClassLoader cl = new MyClassLoader();

// 加载我们生成的 HelloWorld 类

Class<?> clazz = cl.defineClass("com.togo.asm.TestMapper", bytes);

// 将生成的类对象加载到configuration中

configuration.addMapper(clazz);

Method query = clazz.getMethod("queryById", Integer.class);

// 这里就是通过类对象从configuration中获取对应的Mapper

Object testMapper = sqlSessionXML.getMapper(clazz);

Object result = query.invoke(testMapper, 1);

System.out.println("dyn : " + result);

总结

本篇通过mybatis实现了动态加载执行外部sql的功能, 这里只是为大家提供一个实现思路, 在应用到项目前还有很多细节需要深入研究. 加油加油~

demo地址

以上是 使用mybatis动态加载外部sql 的全部内容, 来源链接: utcz.com/z/516048.html

回到顶部