1. 程式人生 > >MyBatis攔截器的使用和設計原理

MyBatis攔截器的使用和設計原理

使用攔截器

Web開發中我們經常會碰到分頁操作,一個專案中或許有多處使用到分頁,這時如果Java後臺使用MyBatis作為持久層,我們就可以使用MyBatis的攔截器功能來完成整個專案中多處的分頁操作,減少程式碼的冗餘。

攔截器程式碼

//攔截StatementHandler中引數型別為Connection的prepare方法
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
public class PageInterceptor implements
Interceptor {
private String test; // 獲取xml中配置的屬性 @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler)invocation.getTarget(); //通過MetaObject優雅訪問物件的屬性,這裡是訪問statementHandler的屬性 MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, new
DefaultReflectorFactory()); //先攔截到RoutingStatementHandler,裡面有個StatementHandler型別的delegate變數,其實現類是BaseStatementHandler,然後就到BaseStatementHandler的成員變數mappedStatement MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement"); // 配置檔案中SQL語句的ID
String id = mappedStatement.getId(); if(id.matches(".+ByPage$")) { //需要攔截的ID(正則匹配) BoundSql boundSql = statementHandler.getBoundSql(); // 原始的SQL語句 String sql = boundSql.getSql(); // 查詢總條數的SQL語句 String countSql = "select count(*) from (" + sql + ")a"; //執行總條數SQL語句的查詢 Connection connection = (Connection)invocation.getArgs()[0]; PreparedStatement countStatement = connection.prepareStatement(countSql); ////獲取引數資訊即where語句的條件資訊,注意上面拿到的sql中引數還是用?代替的 ParameterHandler parameterHandler = (ParameterHandler)metaObject.getValue("delegate.parameterHandler"); parameterHandler.setParameters(countStatement); ResultSet rs = countStatement.executeQuery(); Map<?,?> parameter = (Map<?,?>)boundSql.getParameterObject(); Page page = (Page)parameter.get("page"); if(rs.next()) { page.setTotalNumber(rs.getInt(1)); } // 改造後帶分頁查詢的SQL語句 String pageSql = sql + " limit " + page.getDbIndex() + "," + page.getDbNumber(); metaObject.setValue("delegate.boundSql.sql", pageSql); } return invocation.proceed(); } @Override public Object plugin(Object target) { System.out.println(this.test); return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { this.test = properties.getProperty("test"); // TODO Auto-generated method stub } }

MetaObject是Mybatis提供的一個用於方便、優雅訪問物件屬性的物件,通過它可以簡化程式碼、不需要try/catch各種reflect異常,同時它支援對JavaBean、Collection、Map三種類型物件的操作。獲取MetaObject物件需要使用靜態方法MetaObject.forObject,並且需要指定ObjectFactory , ObjectWrapperFactory , ReflectorFactory(3.3.0之前不需要)。

配置攔截器

<plugins>
    <plugin interceptor="com.chm.inteceptor.PageInterceptor">
        <property name="test" value="abc"/>
    </plugin>
</plugins>

這裡配置test的值為abc,那麼上面攔截器中test的就會被賦值為abc。

查詢SQL

<select id="queryMessageListByPage" parameterType="java.util.Map" resultMap="MessageResult">
 select <include refid="columns"/> from MESSAGE
  <where>
    <if test="message.command != null and !&quot;&quot;.equals(message.command.trim())">
    and COMMAND=#{message.command}
   </if>
   <if test="message.description != null and !&quot;&quot;.equals(message.description.trim())">
    and DESCRIPTION like '%' #{message.description} '%'
   </if>
  </where>
  order by ID
</select>

<sql id="columns">ID,COMMAND,DESCRIPTION,CONTENT</sql>

呼叫示例

/**
 * 根據查詢條件分頁查詢訊息列表
 */
public List<Message> queryMessageListByPage(String command,String description,Page page) {
    Map<String,Object> parameter = new HashMap<String, Object>();
    // 組織訊息物件
    Message message = new Message();
    message.setCommand(command);
    message.setDescription(description);
    parameter.put("message", message);
    parameter.put("page", page);
    MessageDao messageDao = new MessageDao();
    // 分頁查詢並返回結果
    return messageDao.queryMessageListByPage(parameter);
}

攔截器設計原理

攔截器的實現都是基於代理的設計模式實現的,簡單的說就是要創造一個目標類的代理類,在代理類中執行目標類的方法並在方法之前執行攔截器程式碼。 首先,先不管mybatis的原始碼是怎麼設計的,先假設一下自己要做一個攔截器應該怎麼做。下面我們就利用JDK的動態代理自己設計一個簡單的攔截器。

將被攔截的目標介面:

public interface Target {  
    public void execute();  
} 

目標介面的一個實現類:

 public class TargetImpl implements Target {  
    public void execute() {  
        System.out.println("Execute");  
    }  
}  

利用JDK的動態代理實現攔截器:

public class TargetProxy implements InvocationHandler {  
    private Object target;  
    private TargetProxy(Object target) {  
        this.target = target;  
    }  

    //生成一個目標物件的代理物件  
    public static Object bind(Object target) {  
        return Proxy.newProxyInstance(target.getClass() .getClassLoader(),   
                target.getClass().getInterfaces(),  
                       new TargetProxy(target));  
    }  

    //在執行目標物件方法前加上自己的攔截邏輯  
    public Object invoke(Object proxy, Method method,  
                             Object[] args) throws Throwable {  
        System.out.println("Begin");  
        return method.invoke(target, args);  
    }  
}  

客戶端呼叫:

public class Client {  
public static void main(String[] args) {  

    //沒有被攔截之前  
    Target target = new TargetImpl();  
    target.execute(); //Execute  

    //攔截後  
    target = (Target)TargetProxy.bind(target);  
    target.execute();   
    //Begin  
    //Execute  
}  

上面的設計有幾個非常明顯的不足。首先,攔截邏輯被寫死在代理物件中:

public Object invoke(Object proxy, Method method,  
                   Object[] args) throws Throwable {  
   //攔截邏輯被寫死在代理物件中,導致客戶端無法靈活的設定自己的攔截邏輯  
   System.out.println("Begin");  
   return method.invoke(target, args);  
}  

我們可以將攔截邏輯封裝到一個類中,客戶端在呼叫TargetProxy的bind()方法的時候將攔截邏輯一起當成引數傳入:
定義一個攔截邏輯封裝的介面Interceptor,這才是真正的攔截器介面。

public interface Interceptor {  
    public void intercept();  
}  

那麼我們的代理類就可以改成:

public class TargetProxy implements InvocationHandler {  

private Object target;  
private Interceptor interceptor;  

private TargetProxy(Object target, Interceptor interceptor) {  
    this.target = target;  
    this.interceptor = interceptor;  
}  

//將攔截邏輯封裝到攔截器中,有客戶端生成目標類的代理類的時候一起傳入,這樣客戶端就可以設定不同的攔截邏輯。  
public static Object bind(Object target, Interceptor interceptor) {  
    return Proxy.newProxyInstance(target.getClass().getClassLoader(),   
                       target.getClass().getInterfaces(),  
                       new TargetProxy(target, interceptor));  
}  

public Object invoke(Object proxy, Method method,   
                      Object[] args) throws Throwable {  
    //執行客戶端定義的攔截邏輯  
    interceptor.intercept();  
    return method.invoke(target, args);  
}  

客戶端呼叫程式碼:

/客戶端可以定義各種攔截邏輯  
Interceptor interceptor = new Interceptor() {  
    public void intercept() {  
        System.out.println("Go Go Go!!!");  
    }  
};  
target = (Target)TargetProxy.bind(target, interceptor);  
target.execute();  

當然,很多時候我們的攔截器中需要判斷當前方法需不需要攔截,或者獲取當前被攔截的方法引數等。我們可以將被攔截的目標方法物件,引數資訊傳給攔截器。
攔截器介面改成:

public interface Interceptor {  
    public void intercept(Method method, Object[] args);  
}  

在代理類執行的時候可以將當前方法和引數傳給攔截,即TargetProxy的invoke方法改為:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
    interceptor.intercept(method, args);
    return method.invoke(target, args); 
}  

在Java設計原則中有一個叫做迪米特法則,大概的意思就是一個類對其他類知道得越少越好。其實就是減少類與類之間的耦合強度。這是從類成員的角度去思考的。 什麼叫越少越好,什麼是最少?最少就是不知道。 所以我們是不是可以這麼理解,一個類所要了解的類應該越少越好呢?

對於上面的例子,Interceptor介面中需要使用intercept方法傳過去Method類,那麼也需要了解它。那麼既然Interceptor都需要使用Method,還不如將Method的執行也放到Interceptor中,不再讓TargetProxy類對其瞭解。Method的執行需要target物件,所以也需要將target物件給Interceptor。將Method,target和args封裝到一個物件Invocation中,將Invocation傳給Interceptor。

  • Invocation.java
public class Invocation {
    private Object target;
    private Method method;
    private Object[] args;

    public Invocation(Object target, Method method, Object[] args) {
        this.target = target;
        this.method = method;
        this.args = args;
    }

    //將自己成員變數的操作儘量放到自己內部,不需要Interceptor獲得自己的成員變數再去操作它們,
    //除非這樣的操作需要Interceptor的其他支援。然而這裡不需要。
    public Object proceed() throws InvocationTargetException, IllegalAccessException {
        return method.invoke(target, args);
    }

    public Object getTarget() {
        return target;
    }

    public void setTarget(Object target) {
        this.target = target;
    }

    public Method getMethod() {
        return method;
    }

    public void setMethod(Method method) {
        this.method = method;
    }

    public Object[] getArgs() {
        return args;
    }

    public void setArgs(Object[] args) {
        this.args = args;
    }
}
  • TargetProxy.java
public class TargetProxy implements InvocationHandler {

    private Object target;
    private Interceptor interceptor;

    private TargetProxy(Object target, Interceptor interceptor) {
        this.target = target;
        this.interceptor = interceptor;
    }

    //將攔截邏輯封裝到攔截器中,有客戶端生成目標類的代理類的時候一起傳入,這樣客戶端就可以設定不同的攔截邏輯。
    public static Object bind(Object target, Interceptor interceptor) {
        return Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TargetProxy(target, interceptor));
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return interceptor.intercept(new Invocation(target, method, args));
    }
}
  • 測試類
public class Client {
    public static void main(String[] args) {
        Interceptor interceptor = invocation -> {
            System.out.println("Go Go Go!!!");
            return invocation.proceed();
        };

        Target target = (Target) TargetProxy.bind(new TargetImpl(), interceptor);
        target.execute();

    }
}

根據迪米特法則來講,其實客戶端根本不需要了解TargetProxy類。將繫結邏輯放到攔截器內部,客戶端只需要和攔截器打交道就可以了。

攔截器介面變為:

public interface Inteceptor {
    Object intercept(Invocation invocation)throws Throwable;
    Object register(Object target);
}

攔截器實現類

public class InteceptorImpl implements Inteceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("Go Go Go!!!");
        return invocation.proceed();
    }

    @Override
    public Object register(Object target) {
        return TargetProxy.bind(target, this);
    }
}

測試類

public class Client {
    public static void main(String[] args) {
        Inteceptor interceptor = new InteceptorImpl();
        Target target = (Target) interceptor.register(new TargetImpl());
        target.execute();
    }
}

好了,通過一系列調整,設計已經挺好了,不過上面的攔截器還是有一個很大的不足, 那就是攔截器會攔截目標物件的所有方法,然而這往往是不需要的,我們經常需要攔截器攔截目標物件的指定方法。 我們利用在Interceptor上加註解解決。

假設目標物件介面有多個方法:

public interface Target {
    public void execute1();
    public void execute2();
}

首先簡單的定義一個註解:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MethodName {
    public String value();
}

在攔截器的實現類加上該註解:

@MethodName("execute1")
public class InteceptorImpl implements Inteceptor {
    ...
}

在TargetProxy中判斷interceptor的註解,看是否實行攔截:

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        MethodName methodName = this.interceptor.getClass().getAnnotation(MethodName.class);
        if (methodName == null)
            throw new NullPointerException("xxxx");

        //如果註解上的方法名和該方法名一樣,才攔截
        String name = methodName.value();
        if (name.equals(method.getName()))
            return interceptor.intercept(new Invocation(target,    method, args));

        return method.invoke(this.target, args);
    }

OK,上面的一系列過程其實都是mybatis的攔截器程式碼結構,上面的TargetProxy其實就是mybatis的Plugin類。Interceptor和Invocation幾乎一樣。只是mybatis的Interceptor支援的註解更加複雜。

MyBatis的Plugin類,看到裡面的兩個方法,現在就不那麼陌生了。

public class Plugin implements InvocationHandler {

  private Object target;
  private Interceptor interceptor;
  private Map<Class<?>, Set<Method>> signatureMap;
  ...

  public static Object wrap(Object target, Interceptor interceptor) {
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      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);
    }
  }

  ...
}

mybatis最終是通過將自定義的Interceptor配置到xml檔案中:

<!-- 自定義處理Map返回結果的攔截器 -->  
 <plugins>  
     <plugin interceptor="com.chm.inteceptor.PageInterceptor">
 </plugins>