Mybatis系列12:责任链模式在Mybatis的分页插件中的应用

1.责任链模式是什么

定义
将能够处理同一类请求的对象连成一条链,所提交的请求沿着链传递,链上的对象逐个判断是否有能力处理该请求,如果能则处理,如果不能则传递给下一个对象。
开发中常用的场景: Java中的异常机制,一个try可以对应多个catch,当一个不匹配自动跳到第二个catch。 Js中事件的冒泡和捕获机制。Java语言中,事件的处理采用观察者模式。 servlet开发中,过滤器的链式处理。 strtus2中,拦截器的调用是典型的责任链模式。
这里的特征是每个链上的类有一个指向自己下一个流程的引用
例如:一个请假活动,根据天数需要不同的人来审批。

/**
 * 封装请假的基本信息
 */
public class LeaveRequest {
    private String empName;
    private int leaveDays;
    private String reason;

    public LeaveRequest(String empName, int leaveDays, String reason) {
        this.empName = empName;
        this.leaveDays = leaveDays;
        this.reason = reason;
    }

    public String getEmpName() {
        return empName;
    }

    public void setEmpName(String empName) {
        this.empName = empName;
    }

    public int getLeaveDays() {
        return leaveDays;
    }

    public void setLeaveDays(int leaveDays) {
        this.leaveDays = leaveDays;
    }

    public String getReason() {
        return reason;
    }

    public void setReason(String reason) {
        this.reason = reason;
    }
}

定义一个领导的抽象类

public abstract class Leader {
    protected String name;
    protected Leader nextLeader;

    public Leader(String name) {
        this.name = name;
    }

    //设置责任链上的后继对象
    public void setNextLeader(Leader nextLeader) {
        this.nextLeader = nextLeader;
    }

    /**
     * 处理请求的核心业务方法
     */
    public abstract void handleRequest(LeaveRequest request);

}

之后定义子类经理,主管和总经理

public class Manager extends Leader {
    public Manager(String name) {
        super(name);
    }

    @Override
    public void handleRequest(LeaveRequest request) {
        if (request.getLeaveDays() < 10) {
            System.out.println("Manager Tom 请假" + request.getLeaveDays() + "原因是:" + request.getReason());
        } else {
            if (this.nextLeader != null) {
                this.nextLeader.handleRequest(request);
            }
        }
    }
}


public class Director extends Leader {
    public Director(String name) {
        super(name);
    }

    @Override
    public void handleRequest(LeaveRequest request) {
        if (request.getLeaveDays() < 3) {
            System.out.println(" Derecor Tom 请假" + request.getLeaveDays() + "原因是:" + request.getReason());
        } else {
            if (this.nextLeader != null) {
                this.nextLeader.handleRequest(request);
            }
        }
    }
}



public class GeneralManager extends Leader {
    public GeneralManager(String name) {
        super(name);
    }

    @Override
    public void handleRequest(LeaveRequest request) {
        if (request.getLeaveDays() < 20) {
            System.out.println("GernalManager Tom 请假" + request.getLeaveDays() + "原因是:" + request.getReason());
        } else {
            if (this.nextLeader != null) {
                this.nextLeader.handleRequest(request);
            }
        }
    }
}

调用的时候指定链上的关系,然后调用最链上的第一个点,接着就开始自动向后执行了:

public static void main(String[] args) {
        Leader b = new Director("张三");
        Leader a = new Manager("李四");
        Leader c = new GeneralManager("王五");

        a.setNextLeader(b);
        b.setNextLeader(c);

        LeaveRequest leaveRequest = new LeaveRequest("Tom", 10, "回家探亲");
        a.handleRequest(leaveRequest);
    }

2.Interceptor

插件是一种常见的扩展方式,大多数开源框架也都支持用户通过添加自定义插件的方式来扩展或改变框架原有的功能。 Mybatis 中也提供了插件的功能,虽然叫插件,但是实际上是通过拦截器( Interceptor)实现的。在 MyBatis 的插件模块中涉及责任链模式和 JDK 动态代理, JDK动态代理等模式。
Interceptor
MyBatis 允许用户使用自定义拦截器对 SQL 语句执行过程中的某一点进行拦截 。 默认情况下,MyBatis 允许拦截器拦截 Executor 的方法 、 ParameterHandler 的方法 、 ResultSetHandler 的方法以及 StatementHandler 的方法 。 具体可拦截的方法如下 :

  • Executor 中 的 update ()方法、 query()方法 、 flushStatements ()方法 、 commit()方法 、 rollback()方法、 getTransaction()方法 、 close()方法、 isClosed()方法 。
  • ParameterHandler 中的 getParameterObject()方法 、 setParameters ()方法 。
  • ResultS etHandler 中的 handleResultSets()方法 、 handleOu飞putParameters()方法 。
  • StatementHandler 中的 prepare()方法、 parameterize()方法 、 batch()方法、 update()方法 、query()方法 。
    MyBatis 中使用的拦截器都需要实现 Interceptor 接口 。 Interceptor 接口是 MyBatis 插件模块的核心,其定义如下 :
public interface Interceptor {
//执行拦截逻辑的方法	
  Object intercept(Invocation invocation) throws Throwable;
///决定是否触发 intercept ()方法
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
//根据配置初始化 Interceptor 对象
  default void setProperties(Properties properties) {
    // NOP
  }

}

用户自定义的拦截器除了继承 Interceptor 接口,还需要使用@Intercepts 和@S ignature 两个注解进行标识。@Intercepts 注解中指定了一个@Signature 注解列表,每个@Signature 注解中都标识了该插件需要拦截的方法信息,其中@Signature 注解的可pe 属性指定需要拦截的类型,method 属性指定需要拦截的方法 , args 属性指定了被拦截方法的参数列表。通过这三个属性值,@Signature 注解就可 以表示一个方法签名, 唯一确定一个方法。如下示例所示,该拦截器需要拦截 Executor 接口的两个方法 , 分 别 是 query(MappedStatement, Object, RowBounds,ResultHandler)方法和 close(boolean)方法。
例子:

@Intercepts({

  @Signature(type = Executor.class, method = "query", args = {
    MappedStatement.class, Object.class, RowBounds.class,
    ResultHandler.class}),
  @Signature(type = Executor.class, method = "close", args = {boolean.class})
})
public class ExamplePlugin implements Interceptor {

  private int testProp; //省略该属性的 getter/setter 方法

  @Override
  public Object intercept(Invocation invocation) throws Throwable {
    return null;
  }

  @Override
  public Object plugin(Object target) {
    return null;
  }

  @Override
  public void setProperties(Properties properties) {

  }
}

然后在配置文件中添加plugin标签。在 MyBatis 初始化时,会通过XMLConfigBuilder抖uginElement()方法解析 mybatis-config .xml 配置文件中 定义的<plugin>节点 ,得到相应的 Interceptor 对象 以及配置的相应属性, 之后会调用 Interceptor.setProperties(properties)方法完成对 Interceptor 对象 的 初始 化配 置 , 最后将 Interceptor 对 象添 加到Configuration. interceptorChain 字段中保存。

3. Mybatis如何使用拦截器的

MyBatis 的拦截器如何 对 Executor 、ParameterHandler 、ResultSettHandler 、StatementHandler 进拦截。在 MyBatis 中使用的这 四类的对象 , 都是通过 Configuration .new*()系列方法创建的 。 如果配置了用户自定义拦截器,则会在该系列方法中 , 通过 InterceptorChain .pluginAll()方法为目标对象创建代理对象 ,所以通过Configuration . new * ()系列方法得到的对象实际是一个代理对象。下面以 Configuration.newExecutor() 方法为 例 进行分析 , Configuration 中的newParameterHandler()方法、 newResultSetHandler()方法、 newStatementHandler()方法原理类似。
newExecutor的实现:

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      // 默认 SimpleExecutor
      executor = new SimpleExecutor(this, transaction);
    }
    // 二级缓存开关,settings 中的 cacheEnabled 默认是 true
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
    // 植入插件的逻辑,至此,四大对象已经全部拦截完毕
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

InterceptorChain 中使用 interceptors 字段( ArrayList<Interceptor>类型)记录了mybatis-config.xml 文件中配置的 拦截器。在 InterceptorChain . pluginAll () 方法中会遍历该interceptors 集合 ,并调用 其中每个元素 的 plugin ()方法创建代理对象

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

如果自定义拦截器的 plugin()方法,可以考虑使用 MyBatis 提供的 Plugin 工具类实现,它实现了 InvocationHandler 接口,并提供了一个 wrap()静态方法用于创建代理对象。 Plugin.wrap()
方法的具体实现如下:

  public static Object wrap(Object target, Interceptor interceptor) {
  //获取用户自定义 I口terceptor 中 @ Signature i主解的信息, getSignatureMap ()方法负责处理@Signature i主解
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //获取 目标类型实现的接口,拦截器可以拦截的 4 类对象都实现了相应的接口,这也是能使用 JDK 动态代理的方式创建代理对象的基础
    if (interfaces.length > 0) {
    //使用 JDK 动态代理的方式创建代理对象
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

在 Plugin.invoke()方法中,会将当前调用的方法与 signatureMap 集合中记录的方法信息进行比较,如果当前调用 的方法是需要被拦截的方法,则调用其 intercept()方法进行处理,如果不能被拦截则直接调用 target 的相应方法。 Plugin .invoke()方法的具体实现如下:

  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
    //获取当前方法所在类或接口中,可被当前 Interceptor 拦截的方法
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
     // 如果当前调用的方法需妥被拦截,则调用工nterceptor . intercept ()方法进行拦截处理
      if (methods != null && methods.contains(method)) {
        return interceptor.intercept(new Invocation(target, method, args));
      }
      return method.invoke(target, args);
    } catch (Exception e) {
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }

Interceptor.intercept()方法 的参数是 Invocation 对象 , 其中封装了目标对象、目标方法以及调用目标方法的参数,并提供了 proceed()方法调用目标方法,如下所示。所以在 Interceptor.intercept()方法中执行完拦截处理之后 ,如果需要调用目标方法,则通过 Invocation.proceed()方法实现。

  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    return method.invoke(target, args);
  }

这样我们明白了如何构造链路,并且如何使用的。

4.分页插件

现在我们看一个具体的实现,分页插件是如何做的。
MyBatis 本身可以通过 RowRounds 方式进行分页,但是在前面分析 DefaultResultSetHandler 时 已经发现,它并没有转换成分页 相关的 SQL 语句,例如 MySQL 数据库中的 limit 语句,而是通过调用ResuItSet.absolute()方法或循环调用 ResultSet.next()方法定位到指定的记录行。当一个表中的数据量比较大时,这种分页方式依然会查询全表数据,导致性能问题 。我们可以在映射配置文件编写带有 limit 关键字 以及分页参数的 select 语句来实现物理分页,避免上述性能问题 。 但是,对于己有系统来说,用这种方式添加分页功能会造成大量代码修改。
为解决这个问题,可以考虑使用插件的方式实现分页功能。用户可 以添加自定义拦截器并在其中拦截 Executor.query(MappedStatemen, Object, RowBounds, ResultHandler, CacheKey,BoundSql)方法或 Executor.query(MappedStatemen, Object, RowBounds, ResultHandler)方法。在拦截的 Executorquery()方法中,可以通过 RowBounds 参数获取所 需记录的起止位置, 通过BoundSql 参数获取待执行的 SQL 语句,这样就可以在 SQL 语句中合适的位置添加“"limit,offset, length "片段,实现分页功能 。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

纵横千里,捭阖四方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值