MyBatis注解支持SQL片断/宏

对于喜欢使用注解而不是xml的同学来说,通过<sql>这种xml的方式将常用的sql片断抽取成公用的sql会非常痛苦,我们想使用无xml配置的方法,有没有办法通过注解实现呢?办法就是使用mybatis插件

希望达到如下使用方式的目的(省略无关代码):

@Macro(name = "columns", content = "id, name, cel_phone")
public interface UserMapper {
   @Select("select @columns from user where id = #{id}")
   User queryById(@Param("id")int id);
}

其中,注解@Macro用于定义宏,即用于对SQL做文本替换,上面的代码中,@Macro(name = "columns", content = "id, name, cel_phone")表示的意思是定义了一个名为columns的宏,当SQL中出现@columns时,将其替换成id, name, cel_phone,下面是@Macro的定义:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 用于在mybatis的映射接口上定义宏,适用于纯注解的使用方式。
 * <p>
 * 一个映射接口上可以标记多个@Macro,jdk8支持多个注解实例
 *
 * @author gaohang
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Macro {

  /**
   * 宏的名称
   */
  String name();

  /**
   * 用于做替换的文本内容,使用数组可以避免因内容太长导致字符串非常难以阅读,拆分成多个string即可
   */
  String[] content();
}

最后聊聊此插件如何实现,基本原理:

  • 识别SQL中的@符号,取出@符号后面的第一个标识符,比如@columns,则取出columns,columns是正在使用的宏的名称
  • 将通过宏名称找到映射接口上的@Macro声明,取出其中的content
  • 将取出的content替换原始sql中的@以及宏名称

结合mybatis的实现,我们可以看到MappedStatement对象有一个getSqlSource方法,返回SqlSource对象,而SqlSource对象的getBoundSql方法返回BoundSql,我们需要处理的正是BoundSql的getSql方法返回的sql字符串,这个时候,最容易想到的方式是使用代理模式,将SqlSource和BoundSql对象做代理,当调用到boundSql.getSql()时,处理sql中所使用到的宏,这里需要注意以下几点:

  • SqlSource是一个接口,且其中只有一个方法getBoundSql,使用静态代理或者匿名内部类即可
  • BoundSql是一个具体类,想要代理此类的对象,需要使用cglib
  • MappedStatement对象是全局唯一的,不要直接修改此类的对象的属性,当需要修改时,选择复制一个并为需要修改的字段赋上新的值

废话不多说了,代码实现如下:

import com.google.common.collect.Sets;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;

import java.util.List;
import java.util.Objects;
import java.util.Properties;
import java.util.Set;

/**
 * 处理{@link Macro}注解,对SQL做字符串替换,将SQL语句中的@xxx替换成{@link Macro#name()}为xxx的注解中的{@link Macro#content()},例如:
 *
 * <pre>
 * {@code
 * @Macro(name = "columns", content = "id, name, cel_phone")
 * public interface UserMapper {
 *    @Select("select @columns from user where id = #{id})
 *    User queryById(@Param("id")int id);
 * }
 *
 * }
 * </pre>
 * <p>
 * 注意,宏的使用必须要跟着逗号或空白字符,或者是SQL的结尾,否则识别不了
 *
 * @author gaohang
 */
@Intercepts({
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
    @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class MacroInterceptor implements Interceptor {
  private static final Logger logger = LoggerFactory.getLogger(MacroInterceptor.class);

  /**
   * 记录不需要处理{@link Macro}的映射接口
   */
  private final Set<String> namespaceNotContainsMacro = Sets.newConcurrentHashSet();

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    final Object[] args = invocation.getArgs();
    final MappedStatement ms = (MappedStatement) args[0];
    //这里的sqlSource是一个代理对象
    final SqlSource sqlSource = new SqlSourceDelegate(ms);
    //复制MapedStatement对象
    final MappedStatement mappedStatement = MappedStatementUtils.copyMappedStatement(ms, sqlSource);
    args[0] = mappedStatement;
    return invocation.proceed();
  }

  @Override
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  @Override
  public void setProperties(Properties properties) {
  }

  /**
   * 对{@link SqlSource}的代理,{@link #getBoundSql(Object)}方法会返回一个{@link BoundSql}
   * 对象的cglib代理,在代理对象中处理宏的替换
   */
  private final class SqlSourceDelegate implements SqlSource {
    private final MappedStatement ms;

    private SqlSourceDelegate(MappedStatement ms) {
      this.ms = ms;
    }

    @Override
    public BoundSql getBoundSql(Object parameterObject) {
      //获取原始的BoundSql
      final BoundSql boundSql = ms.getSqlSource().getBoundSql(parameterObject);
      final String namespace = getNamespace(ms);
      if (namespace == null) {
        return boundSql;
      }
      //拦截一次
      if (namespaceNotContainsMacro.contains(namespace)) {
        return boundSql;
      }

      try {
        final Class<?> mapperInterface = Class.forName(namespace);
        final Macro[] macros = mapperInterface.getAnnotationsByType(Macro.class);
        //没有定义Macro,则返回原始的boundSql
        if (macros == null || macros.length == 0) {
          namespaceNotContainsMacro.add(namespace);
          return boundSql;
        }
        //对BoundSql做代理,处理getSql()返回的sql语句
        return proxyBoundSql(ms.getConfiguration(), boundSql, macros);
      } catch (ClassNotFoundException e) {
        logger.debug("load namespace class failed, maybe namespace {} is not a class", namespace, e);
        namespaceNotContainsMacro.add(namespace);
      }

      return boundSql;
    }
  }

  /**
   * 创建{@link BoundSql}的代理对象
   */
  private BoundSql proxyBoundSql(Configuration configuration, BoundSql boundSql, Macro[] macros) {
    return getProxy(configuration, boundSql, (proxy, method, args, methodProxy) -> {
      if (!Objects.equals("getSql", method.getName())) {
        return methodProxy.invoke(boundSql, args);
      }
      //处理宏
      return replaceMacroContent(macros, boundSql.getSql());
    });
  }

  private BoundSql getProxy(Configuration configuration, BoundSql target, MethodInterceptor methodInterceptor) {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(target.getClass());
    enhancer.setCallback(methodInterceptor);
    final Class<?>[] parameterTypes = {Configuration.class, String.class, List.class, Object.class};
    final Object[] params = new Object[]{
        configuration, target.getSql(), target.getParameterMappings(), target.getParameterObject()
    };
    return (BoundSql) enhancer.create(parameterTypes, params);
  }

  /**
   * 对sql做宏的替换
   */
  private String replaceMacroContent(Macro[] macros, String sql) {
    //识别sql字符串中的@符号,并取出@符号之后的宏名
    int lastAppended = 0;
    final StringBuilder sqlAppender = new StringBuilder();
    for (int i = sql.length(); lastAppended < i; ) {
      final int macroPlaceStart = sql.indexOf('@', lastAppended);
      if (macroPlaceStart < 0) {
        //没有了,结束
        break;
      }
      final int macroNameStart = macroPlaceStart + 1;
      if (macroNameStart == i) {
        break;
      }
      int k = macroNameStart;
      for (; k < i; k++) {
        final char ch = sql.charAt(k);
        //@macroName后面必须要跟着逗号或者空白字符,或者是SQL的结尾
        if (Character.isWhitespace(ch) || ch == ',') {
          break;
        }
      }
      sqlAppender.append(sql, lastAppended, macroPlaceStart);
      //(macroPlaceStart, k)是宏结束
      final String macroName = sql.substring(macroNameStart, k);
      for (Macro macro : macros) {
        if (Objects.equals(macro.name(), macroName)) {
          final String[] contents = macro.content();
          for (String content : contents) {
            sqlAppender.append(content).append(' ');
          }
        }
      }
      lastAppended = k;
    }
    if (sqlAppender.length() == 0) {
      return sql;
    }
    if (lastAppended < sql.length()) {
      sqlAppender.append(sql.substring(lastAppended));
    }
    return sqlAppender.toString();
  }

  private String getNamespace(MappedStatement ms) {
    final String statementId = ms.getId();
    final int i = statementId.lastIndexOf('.');
    if (i <= 0) {
      return null;
    }
    return statementId.substring(0, i);
  }

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354