乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 MyBatis概述与快速上手

    • 本章定位
    • MyBatis简介
    • 环境搭建
    • 第一个MyBatis程序
    • SqlSessionFactoryBuilder与openSession重载
    • SqlSessionFactory与SqlSession
    • SqlSession核心方法
    • 不使用 XML 构建 SqlSessionFactory
    • Mapper接口与映射方式
    • Java API 目录结构
  • 第2章 全局配置文件详解

    • 本章定位
    • properties
    • settings
    • typeAliases
    • typeHandlers
    • objectFactory
    • plugins
    • environments
    • transactionManager
    • dataSource
    • databaseIdProvider
    • mappers
    • 日志配置
  • 第3章 SQL映射文件基础

    • 本章定位
    • select
    • insert
    • update
    • delete
    • 参数传递与占位符
    • 主键生成策略
    • resultType
    • resultMap
    • 自动映射详解
    • sql片段
    • SQL 语句构建器
  • 第4章 动态SQL

    • 本章定位
    • if
    • choose、when、otherwise
    • where
    • set
    • foreach
    • trim
    • bind
    • script 元素:在注解映射器中启用动态 SQL
    • _databaseId 与动态 SQL 的多数据库支持
    • 动态 SQL 中插入脚本语言
  • 第5章 结果映射与关联查询

    • 本章定位
    • resultMap详解
    • association
    • collection
    • discriminator
    • N+1查询问题
    • 延迟加载
  • 第6章 MyBatis注解开发

    • 本章定位
    • @Select
    • @Insert
    • @Update
    • @Delete
    • @Param
    • @Options
    • @SelectKey
    • @Results
    • @Result
    • @One
    • @Many
    • @SelectProvider
  • 第7章 缓存与性能优化

    • 本章定位
    • 一级缓存
    • 二级缓存
    • 缓存配置详解
    • 自定义缓存
    • Executor执行器类型
    • 分页插件

动态 SQL 中插入脚本语言

概述

MyBatis 从 3.2 版本开始支持可插拔脚本语言(Pluggable Scripting Language),允许开发者用自定义的脚本语言驱动来编写动态 SQL,替代默认的 OGNL 表达式。你可以接入 Velocity、FreeMarker 甚至 Groovy 作为 SQL 模板引擎,让动态 SQL 的编写方式彻底改变。

飞翔科技的技术总监大翔在一个报表系统中引入了 Velocity 引擎,让原本嵌套 5 层的 <if> <choose> 标签变成了一段干净的可读模板。

LanguageDriver 机制

MyBatis 的动态 SQL 处理核心在 LanguageDriver 接口,它负责两件事:

  1. 创建 SqlSource:把 XML 中的 SQL 脚本(或注解中的 SQL 字符串)解析成可执行的 SqlSource 对象
  2. 创建 ParameterHandler:处理 SQL 参数绑定
public interface LanguageDriver {

    // 从 XML 节点创建 SqlSource(XML 映射文件方式)
    SqlSource createSqlSource(Configuration configuration,
        XNode script, Class<?> parameterType);

    // 从字符串创建 SqlSource(注解方式)
    SqlSource createSqlSource(Configuration configuration,
        String script, Class<?> parameterType);

    // 创建参数处理器
    ParameterHandler createParameterHandler(
        MappedStatement mappedStatement,
        Object parameterObject, BoundSql boundSql);
}

MyBatis 核心组件交互流程

自定义 LanguageDriver 实现

下面实现一个 简单模板驱动,支持 ${param} 占位符替换(仅用于演示原理,生产请用 MyBatis-Velocity 正式项目):

package com.feixiang.mybatis.scripting;

import org.apache.ibatis.builder.xml.XMLMapperEntityResolver;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 简易模板 LanguageDriver — 演示原理
 * 支持 ${paramName} 占位符替换为 MyBatis 的 #{paramName} 预编译参数
 */
public class SimpleTemplateDriver extends XMLLanguageDriver {

    // 匹配 ${变量名} —— 非贪婪
    private static final Pattern TEMPLATE_PATTERN =
        Pattern.compile("\\$\\{(\\w+)\\}");

    @Override
    public SqlSource createSqlSource(Configuration configuration,
                                      XNode script,
                                      Class<?> parameterType) {

        // 1. 先让 XMLLanguageDriver 按标准 XML 方式处理
        //    (支持 <if> <where> <foreach> 等动态标签)
        SqlSource original = super.createSqlSource(
            configuration, script, parameterType);

        // 2. 包装一层:在 getBoundSql 时把 ${xxx} 替换为 #{xxx}
        return new SqlSource() {
            @Override
            public BoundSql getBoundSql(Object parameterObject) {
                BoundSql boundSql = original.getBoundSql(parameterObject);
                String sql = boundSql.getSql();

                // 提取 ${xxx} 中的变量名
                List<String> paramNames = new ArrayList<>();
                Matcher matcher = TEMPLATE_PATTERN.matcher(sql);
                while (matcher.find()) {
                    paramNames.add(matcher.group(1));
                }

                // 构建新的 ParameterMapping 列表
                List<ParameterMapping> newMappings =
                    new ArrayList<>(boundSql.getParameterMappings());

                for (String name : paramNames) {
                    ParameterMapping mapping =
                        new ParameterMapping.Builder(
                            configuration, name, Object.class).build();
                    newMappings.add(mapping);
                }

                // 把 ${xxx} 替换为 #{xxx} 作为 MyBatis 标准占位符
                String newSql = TEMPLATE_PATTERN.matcher(sql)
                    .replaceAll("#{$1}");

                return new BoundSql(configuration, newSql,
                    newMappings, parameterObject);
            }
        };
    }

    @Override
    public SqlSource createSqlSource(Configuration configuration,
                                      String script,
                                      Class<?> parameterType) {
        // 注解模式:直接处理字符串
        String processed = TEMPLATE_PATTERN.matcher(script)
            .replaceAll("#{$1}");
        return new XMLLanguageDriver().createSqlSource(
            configuration, processed, parameterType);
    }

    @Override
    public ParameterHandler createParameterHandler(
            MappedStatement mappedStatement,
            Object parameterObject, BoundSql boundSql) {
        // 使用默认的参数处理器即可
        return new DefaultParameterHandler(
            mappedStatement, parameterObject, boundSql);
    }
}

注册自定义 LanguageDriver

在 mybatis-config.xml 中注册别名并设为默认:

<configuration>
    <typeAliases>
        <typeAlias type="com.feixiang.mybatis.scripting.SimpleTemplateDriver"
                   alias="simpleTemplate"/>
    </typeAliases>

    <settings>
        <!-- 设为全局默认脚本语言 -->
        <setting name="defaultScriptingLanguage" value="simpleTemplate"/>
    </settings>
</configuration>

三种指定方式

对比总览

指定方式作用范围配置位置优先级
全局默认所有 Mapper 的所有 SQL 语句mybatis-config.xml 的 <settings>最低
语句级 lang 属性单个 <select>/<insert>/<update>/<delete>Mapper XML 中中
@Lang 注解单个 Mapper 方法Mapper 接口上最高

方式一:全局默认

<settings>
    <setting name="defaultScriptingLanguage" value="simpleTemplate"/>
</settings>

所有未显式指定语言的 SQL 语句都使用 SimpleTemplateDriver。

方式二:语句级 lang 属性

<mapper namespace="com.feixiang.mapper.EmployeeMapper">

    <!-- 仅此语句使用 simpleTemplate -->
    <select id="findByCondition" lang="simpleTemplate">
        SELECT * FROM t_employee
        WHERE 1=1
        <if test="name != null">
            AND name = ${name}
        </if>
        <if test="deptId != null">
            AND dept_id = ${deptId}
        </if>
    </select>

    <!-- 未指定 lang,使用全局默认 -->
    <select id="findById" resultType="Employee">
        SELECT * FROM t_employee WHERE id = #{id}
    </select>

</mapper>

方式三:@Lang 注解

public interface EmployeeMapper {

    // 注解指定,优先级最高
    @Lang(SimpleTemplateDriver.class)
    @Select("SELECT * FROM t_employee WHERE name = ${name}")
    Employee findByName(@Param("name") String name);

    // 未指定,使用全局默认
    @Select("SELECT * FROM t_employee WHERE id = #{id}")
    Employee findById(int id);
}

飞翔科技实战:报表系统的模板化 SQL

场景

孔蓝负责的报表模块需要根据 10 多个筛选条件动态生成 SQL,XML 中 <if> 标签嵌套了 5 层,代码审查时被大翔打回:

大翔:"这种嵌套深度没人能维护。引入 Velocity 脚本语言,把 SQL 模板化。"

改造前(XML 地狱)

<select id="buildReport" resultType="map">
    SELECT * FROM t_report
    <where>
        <if test="startDate != null">
            AND create_time >= #{startDate}
        </if>
        <if test="endDate != null">
            AND create_time &lt;= #{endDate}
        </if>
        <choose>
            <when test="deptId != null">
                AND dept_id = #{deptId}
            </when>
            <otherwise>
                <if test="region != null">
                    AND region = #{region}
                </if>
            </otherwise>
        </choose>
        <!-- ... 还有 7 层嵌套 ... -->
    </where>
</select>

改造后(Velocity 模板)

<select id="buildReport" resultType="map" lang="velocity">
    SELECT * FROM t_report
    WHERE 1=1
    #if($_parameter.startDate)
        AND create_time >= @{startDate}
    #end
    #if($_parameter.endDate)
        AND create_time &lt;= @{endDate}
    #end
    #if($_parameter.deptId)
        AND dept_id = @{deptId}
    #elseif($_parameter.region)
        AND region = @{region}
    #end
</select>

SQL 模板现在是一个平坦结构,黄俪(测试)一眼就能看懂所有分支逻辑。

MyBatis-Velocity 项目

官方提供了 Velocity 集成:mybatis-velocity。使用方法:

  1. 添加 Maven 依赖:
<dependency>
    <groupId>org.mybatis.scripting</groupId>
    <artifactId>mybatis-velocity</artifactId>
    <version>2.1.2</version>
</dependency>
  1. 注册别名:
<typeAliases>
    <typeAlias type="org.mybatis.scripting.velocity.VelocityLanguageDriver"
               alias="velocity"/>
</typeAliases>
  1. 使用 lang="velocity" 即可。

易错场景提醒

1. 混淆 ${} 和 #{}

在自定义驱动中,${xxx} 和 #{xxx} 的行为由 LanguageDriver 决定。
如果没有正确处理,${xxx} 可能变成字符串拼接(SQL 注入风险),
而不是 MyBatis 的预编译占位符。务必在驱动中将变量转为 ParameterMapping。

2. 全局改默认语言影响所有语句

如果全局设置 defaultScriptingLanguage=velocity,但 XML 中有
对 OGNL 表达式的依赖(如 <if test="...">),可能会解析失败。
建议:只在需要的语句上用 lang 属性单独指定。

3. @Lang 注解的优先级陷阱

// 即使全局默认是 velocity,这个方法也被 @Lang 锁定为 XMLLanguageDriver
@Lang(XMLLanguageDriver.class)
@Select("SELECT * FROM t WHERE name = #{name}")
Employee findByName(String name);

4. 自定义驱动忘记处理参数映射

LanguageDriver.createSqlSource 返回的 SqlSource 必须正确生成
ParameterMapping 列表,否则 MyBatis 不知道有哪些参数需要设置,
运行时会抛出 ParameterNotFoundException。

5. 生产环境慎用自研驱动

自研 LanguageDriver 需要覆盖各种边界情况(动态标签嵌套、foreach、bind 等)。
官方推荐的 mybatis-velocity 项目经过了充分测试,生产环境优先选用。

面试考点

  1. MyBatis 的脚本语言机制由哪个接口定义?

    • LanguageDriver,包含 createSqlSource 和 createParameterHandler 两个核心方法。
  2. 如何为 MyBatis 替换默认的 OGNL 脚本语言?

    • 实现 LanguageDriver 接口(或继承 XMLLanguageDriver),注册类型别名,通过 defaultScriptingLanguage 设置全局默认,或通过 lang 属性 / @Lang 注解局部指定。
  3. 三种指定脚本语言的方式优先级如何?

    • @Lang 注解 > lang 属性 > 全局 defaultScriptingLanguage。
  4. MyBatis 官方支持的替代脚本语言是什么?

    • Apache Velocity(mybatis-velocity 项目)。也有社区支持的 FreeMarker 驱动。
  5. 自定义脚本驱动最需要注意什么?

    • 正确处理参数映射(ParameterMapping),防止 ${} 退化为字符串拼接导致 SQL 注入。
上一页
_databaseId 与动态 SQL 的多数据库支持