使用mybatis動態加載外部sql

背景

不知道你們公司內部有沒有這樣的困惑, 很多部門經常會要求你們部門提供接口, 查詢一些數據, 接口基本沒有業務邏輯, 一條sql足以, 但是爲了這個sql就不得不開發一個接口, 費時費力. 很多人也想過解決, 比如經常見到的, 會寫一個包含很多字段的SQL, 然後通過不同的入參拼接不同的sql(mybatis中的). 這種方式簡單粗暴, 只能查詢固定表, 如果換一個表的數據, 還是要重新寫, 而且返回無用大量字段.

思路

怎麼解決? 說說我和小夥伴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地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章