【Java EE】插件

插件

有機會在四大對象調度時插入自定義的代碼來執行一些特殊的要求以滿足特殊的場景需求,這便是MyBatis的插件技術。

插件接口

在MyBatis中使用插件,就必須實現接口Interceptor,它的定義和各個方法的含義爲:

public interface Interceptor{
	Object intercept(Invocation invocation) throws Throwable;
	Object plugin(Object target);
	void setProperties(Properties properties);
}	

在接口中,定義了3個方法:
-intercept方法:它將直接覆蓋攔截對象原有的方法,是插件的核心方法。intercept裏面有個參數Invocation對象,通過它可以反射調度原來對象的方法。
plugin方法:target是被攔截對象,它的作用的給被攔截對象生成一個代理對象,並返回它。在MyBatis中,它提供了org.apache.ibatis.plugin.Plugin中的wrap靜態(static)方法生成代理對象,一般情況下都會使用它來生成代理對象。
setProperties方法:允許在plugin元素中配置所需參數,方法在插件初始化時被調用了一次,然後把插件對象存入到配置中,以便後面再取出。
這是插件的骨架,這樣的模式被稱爲模板(template)模式

插件的初始化

插件的初始化是在MyBatis初始化時完成的。在解析配置文件時,在MyBatis的上下文初始化過程中,就開始讀入插件節點和配置的參數,同時使用反射技術生成對應的插件實例,然後調用插件方法中的setProperties方法,設置我們配置的參數,將插件實例保存到配置對象中,以便讀取和使用它。所以插件的實例對象是一開始就被初始化的,而不是用到時才初始化。使用時,直接拿出來用就可以了。
完成初始化的插件保存在List對象裏邊等待將其取出使用。

插件的代理和反射設計

插件用的是責任鏈模式,MyBatis的責任鏈是由interceptorChain去定義的。創建執行器時用到過如下代碼:

executor = (Executor) interceptorChain.pluginAll(executor)

其中pluginAll()方法的實現如下:

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

plugin方法是生成代理對象的方法,它是從Configuration對象中取出插件的。有多少個攔截器就生成多少個代理對象。每一個插件都可以攔截到真實的對象。
MyBatis中提供了一個常用的工具類,可以用來生成代理對象,它便是Plugin類。Plugin類實現了InvocationHandler接口,採用的JDK的動態代理。這個類有兩個十分重要的方法:wrap()方法和invoke()方法。其中wrap方法生成這個對象的動態代理對象。如果使用這個類爲插件生成代理對象,那麼代理對象在調用方法時就會進入到invoke方法中。在invoke方法中,如果存在簽名的攔截方法,插件的intercept方法就會在這裏調用,然後返回結果。如果不存在簽名方法,那麼將直接反射調度要執行的方法。
這裏MyBatis把被代理對象、反射方法及其參數都傳遞給了Invocation類的構造方法,用以生成一個Invocation類對象,Invocation類中有一個proceed()方法,這個就是通過反射的方式調度被代理對象的真實方法的。

常用的工具類——MetaObject

MyBatis的工具類——MetaObject,可以有效讀取或者修改一些重要對象的屬性。在MyBatis中,四大對象提供的public設置參數的方法很少,難以通過其自身得到相關的屬性信息,但是有了MetaObject這個工具類就可以通過其他的技術手段來讀取或者修改這些重要對象的屬性。
工具類MetaObject有3個方法:

  • MetaObject forObject(Object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory)方法用於包裝對象。這個方法已經不再使用了,而是用MyBatis提供的SystemMetaObject.forObject(Object obj)。
  • Object getValue(String name) 方法用於獲取對象屬性值,支持OGNL。
  • void setValue(String name, Object value)方法用於修改對象屬性值,支持OGNL。
    MyBatis對象,包括四大對象大量使用了這個類進行包裝,因此可以通過它來給四大對象的某些屬性賦值從而滿足我們的需要。

插件的開發過程和實例

確定需要攔截的簽名

MyBatis允許攔截四大對象中的任意一個對象,而通過Plugin源碼可知需要先註冊簽名才能使用插件。因此首先要確定需要攔截的對象,才能進一步確定需要配置什麼樣的簽名,進而完成攔截的方法邏輯。

1. 確定需要攔截的對象

首先要根據功能來確定需要攔截什麼對象。

  • Executor是執行SQL的全過程,包括組裝參數、組裝結果集返回和執行SQL過程,都可以攔截。一般用的不算太多。根據是否啓動緩存參數,決定它是否使用CachingExecutor進行封裝,這是攔截執行器時需要注意的地方。
  • StatementHandler是執行SQL的過程,是最常用的攔截對象。
  • ParameterHandler主要攔截執行SQL的參數組裝,可以重寫組裝參數規則。
  • ResultSetHandler用於攔截執行結果的組裝,可以重寫組裝結果的規則。
    要攔截的是StatementHandler對象,應該在預編譯SQL之前修改SQL,使得結果返回數量被限制。

2. 攔截方法和參數

確定了需要攔截什麼對象,接下來就要確定需要攔截什麼方法及方法的參數,這些都是在理解了MyBatis四大對象運作的基礎上才能確定的。
查詢的過程是通過Executor調度StatementHandler來完成的。調度StatementHandler的prepare方法預編譯SQL,於是要攔截的方法便是prepare方法,在此之前完成SQL的重新編寫。 先看StatementHandler接口的定義:

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}

以上的任何方法都可以攔截。從接口定義而言,prepare方法有一個參數Connection對象,因此,以如下代碼設計攔截器:

@Intercepts({
	@Signature(type=StatementHandler.class,
		method="prepare",
		args={Connection.class, Integer}
	)
})
public class MyPlugin implements Interceptor{
	......
}

其中,@Intercepts說明它是一個攔截器。@Signature是註冊攔截器簽名的地方,只有簽名滿足條件才能攔截,type可以是四大對象中的一個,這裏是StatementHandler。method代表要攔截四大對象的某一種接口方法,而args則表示該方法的參數。要根據攔截對象的方法參數進行設置。

實現攔截方法

import java.sql.Connection;
import java.util.Properties;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.log4j.Logger;

@Intercepts({
		@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class MyPlugin implements Interceptor {
	private Logger log = Logger.getLogger(MyPlugin.class);
	private Properties props = null;

	/**
	 * 插件方法,它將代替StatementHandler的prepare方法
	 *
	 * @param invocation
	 *            入參
	 * @return 返回預編譯後的PreparedStatement.
	 * @throws Throwable
	 *             異常.
	 */
	@Override
	public Object intercept(Invocation invocation) throws Throwable {
		StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
		// 進行綁定
		MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
		Object object = null;
		/*
		 * 分離代理對象鏈(由於目標類可能被多個攔截器[插件]攔截, 從而形成多次代理,通過循環可以分離出最原始的目標類)
		 */
		while (metaStatementHandler.hasGetter("h")) {
			object = metaStatementHandler.getValue("h");
			metaStatementHandler = SystemMetaObject.forObject(object);
		}
		statementHandler = (StatementHandler) object;
		String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
		Long parameterObject = (Long) metaStatementHandler.getValue("delegate.boundSql.parameterObject");
		log.info("執行的SQL:【" + sql + "】");
		log.info("參數:【" + parameterObject + "】");
		log.info("before ......");
		// 如果當前代理的是一個非代理對象,那麼它就回調用真實攔截對象的方法,
		// 如果不是,那麼它會調度下個插件代理對象的invoke方法
		Object obj = invocation.proceed();
		log.info("after ......");
		return obj;
	}

	/**
	 * 生成代理對象
	 *
	 * @param target
	 *            -- 被攔截對象.
	 * @return 代理對象
	 */
	@Override
	public Object plugin(Object target) {
		// 採用系統默認的Plugin.wrap方法生成
		return Plugin.wrap(target, this);
	}

	/**
	 * 設置參數,MyBatis初始化時,就會生成插件實例,並且調用這個方法.
	 *
	 * @param props
	 *            配置參數
	 */
	@Override
	public void setProperties(Properties props) {
		this.props = props;
		log.info("dbType = " + this.props.get("dbType"));
	}
}

插件首先分離代理對象,然後通過MetaObject獲取了執行的SQL參數,並且在反射方法之前和之後分別打印before和after,這就意味着使用可以在方法前或方法後執行特殊的代碼,以滿足特殊要求。

配置和運行

最後要在MyBatis配置文件裏配置才能夠使用插件。注意plugins元素的配置順序,配錯順序系統會報錯。

<plugins>
	<plugin interceptor="com.ssm.chapter8.plugin.MyPlugin">
		<property name="dbType" value="mysql" />
	</plugin>
</plugins>

使用MyBatis執行一條SQL

<select id="getRole" parameterType="long" resultType="com.ssm.chapter8.pojo.Role">
	select id, role_name as roleName, note from t_role where id = #{id}
</select>

插件實例——分頁插件

首先定義一個分頁參數的POJO,通過它可以設置分頁的各種參數。

public class PageParams {
	// 當前頁碼
	private Integer page;
	// 每頁限制條數
	private Integer pageSize;
	// 是否啓動插件,如果不啓動,則不作分頁
	private Boolean useFlag;
	// 是否檢測頁碼的有效性,如果爲true,而頁碼大於最大頁數,則拋出異常
	private Boolean checkFlag;
	// 是否清除最後order by後面的語句
	private Boolean cleanOrderBy;
	// 總條數,插件會回填這個值
	private Integer total;
	// 總頁數,插件會回填這個值.
	private Integer totalPage;
	/*setter and getter*/
}

在MyBatis中傳遞參數可以是單個參數,也可以是多個,或者使用Map。有了這些規則,爲了使用方便,定義只要滿足下列條件之一,就可以啓用分頁參數(PageParams)。

  • 傳遞單個PageParams或者其子對象
  • map中存在一個值爲PageParams或者其子對象的參數
  • 在MyBatis中傳遞多個參數,但其中之一爲PageParams或者其子對象
  • 傳遞多個POJO參數,這個POJO有一個屬性爲PageParams或者其子對象,且提供了setter和getter方法。
    爲了在編譯SQL之前修改SQL,需要增加分頁參數並計算出查詢總條數。依據MyBatis運行原理,選擇攔截StatementHandler的prepare方法,攔截方法簽名:
@Intercepts({
	@Signature(type=StatementHandler.class,
		method="prepare",
		args={Connection.class, Integer.class}
	)
})

在插件中有3個方法需要自己完成,其中plugin方法中,我們使用的是Plugin類的靜態方法wrap生產代理對象。當Parameters的useFlag屬性設置爲false時,也就是禁用此分類參數時,就沒有必要生成代理對象了。

@Intercepts({
		@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class, Integer.class }) })
public class PagePlugin implements Interceptor {
	/**
	 * 插件默認參數,可配置默認值.
	 */
	private Integer defaultPage; // 默認頁碼
	private Integer defaultPageSize;// 默認每頁條數
	private Boolean defaultUseFlag; // 默認是否啓用插件
	private Boolean defaultCheckFlag; // 默認是否檢測頁碼參數
	private Boolean defaultCleanOrderBy; // 默認是否清除最後一個order by 後的語句

	@Override
	public Object intercept(Invocation invocation) throws Throwable {
	...
	}
	@Override
	public Object plugin(Object target) {
		// 生成代理對象.
		return Plugin.wrap(target, this);
	}

	/**
	 * 設置插件配置參數。
	 *
	 * @param props
	 *            配置參數
	 */
	@Override
	public void setProperties(Properties props) {
		// 從配置中獲取參數
		String strDefaultPage = props.getProperty("default.page", "1");
		String strDefaultPageSize = props.getProperty("default.pageSize", "50");
		String strDefaultUseFlag = props.getProperty("default.useFlag", "false");
		String strDefaultCheckFlag = props.getProperty("default.checkFlag", "false");
		String StringDefaultCleanOrderBy = props.getProperty("default.cleanOrderBy", "false");
		// 設置默認參數.
		this.defaultPage = Integer.parseInt(strDefaultPage);
		this.defaultPageSize = Integer.parseInt(strDefaultPageSize);
		this.defaultUseFlag = Boolean.parseBoolean(strDefaultUseFlag);
		this.defaultCheckFlag = Boolean.parseBoolean(strDefaultCheckFlag);
		this.defaultCleanOrderBy = Boolean.parseBoolean(StringDefaultCleanOrderBy);
	}
}

plugin方法使用了MyBatis提供生成代理對象的方法來生成,只要符合簽名的規則,StatementHandler在運行時就會進入到intercept方法裏。setProperties方法用於給插件設置默認值。intercept方法如下:

@Override
	public Object intercept(Invocation invocation) throws Throwable {
		StatementHandler stmtHandler = (StatementHandler) getUnProxyObject(invocation.getTarget());
		MetaObject metaStatementHandler = SystemMetaObject.forObject(stmtHandler);
		String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
		MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
		// 不是select語句
		if (!checkSelect(sql)) {
			return invocation.proceed();
		}
		BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
		Object parameterObject = boundSql.getParameterObject();
		PageParams pageParams = getPageParamsForParamObj(parameterObject);
		if (pageParams == null) { // 無法獲取分頁參數,不進行分頁
			return invocation.proceed();
		}

		// 獲取配置中是否啓用分頁功能
		Boolean useFlag = pageParams.getUseFlag() == null ? this.defaultUseFlag : pageParams.getUseFlag();
		if (!useFlag) { // 不使用分頁插件
			return invocation.proceed();
		}
		// 獲取相關配置的參數
		Integer pageNum = pageParams.getPage() == null ? defaultPage : pageParams.getPage();
		Integer pageSize = pageParams.getPageSize() == null ? defaultPageSize : pageParams.getPageSize();
		Boolean checkFlag = pageParams.getCheckFlag() == null ? defaultCheckFlag : pageParams.getCheckFlag();
		Boolean cleanOrderBy = pageParams.getCleanOrderBy() == null ? defaultCleanOrderBy
				: pageParams.getCleanOrderBy();
		// 計算總條數
		int total = getTotal(invocation, metaStatementHandler, boundSql, cleanOrderBy);
		// 回填總條數到分頁參數
		pageParams.setTotal(total);
		// 計算總頁數.
		int totalPage = total % pageSize == 0 ? total / pageSize : total / pageSize + 1;
		// 回填總頁數到分頁參數
		pageParams.setTotalPage(totalPage);
		// 檢查當前頁碼的有效性
		checkPage(checkFlag, pageNum, totalPage);
		// 修改sql
		return preparedSQL(invocation, metaStatementHandler, boundSql, pageNum, pageSize);
	}

首先從責任鏈中分離出最原始的StatementHandler對象,使用的方法是getUnProxyObject,代碼如下:

/**
	 * 從代理對象中分離出真實對象
	 * 
	 * @param ivt
	 *            --Invocation
	 * @return 非代理StatementHandler對象
	 */
	private Object getUnProxyObject(Object target) {
		MetaObject metaStatementHandler = SystemMetaObject.forObject(target);
		// 分離代理對象鏈(由於目標類可能被多個攔截器攔截,從而形成多次代理,通過循環可以分離出最原始的目標類)
		Object object = null;
		
		// 可以分離出最原始的的目標類)  
        while (metaStatementHandler.hasGetter("h")) {  
            object = metaStatementHandler.getValue("h");  
            metaStatementHandler = SystemMetaObject.forObject(object);  
        }  
        
		if (object == null) {
			return target;
		}
		return object;
	}

通過這個方法就可以把JDK動態代理鏈上原始的StatementHandler分離出來,然後通過MetaObject對象進行綁定,這樣就可以爲後續通過它分離出當前執行的SQL和參數做準備,由於攔截了所有的SQL,對於非查詢(select)語句是不需要攔截的,所以需要一個判斷是否是查詢語句的方法checkSelect,代碼如下:

/**
	 * 判斷是否sql語句
	 *
	 * @param sql
	 *            --當前執行SQL
	 * @return 是否查詢語句
	 */
	private boolean checkSelect(String sql) {
		String trimSql = sql.trim();
		int idx = trimSql.toLowerCase().indexOf("select");
		return idx == 0;
	}

如果是查詢語句,則攔截這條SQL進入下一步——分離出分頁參數PageParams,代碼如下:

/**
	 * * 分離出分頁參數
	 * 
	 * @param parameterObject
	 *            --執行參數
	 * @return 分頁參數
	 * @throws Exception
	 */
	public PageParams getPageParamsForParamObj(Object parameterObject) throws Exception {
		PageParams pageParams = null;
		if (parameterObject == null) {
			return null;
		}
		// 處理map參數,多個匿名參數和@Param註解參數,都是map
		if (parameterObject instanceof Map) {
			@SuppressWarnings("unchecked")
			Map<String, Object> paramMap = (Map<String, Object>) parameterObject;
			Set<String> keySet = paramMap.keySet();
			Iterator<String> iterator = keySet.iterator();
			while (iterator.hasNext()) {
				String key = iterator.next();
				Object value = paramMap.get(key);
				if (value instanceof PageParams) {
					return (PageParams) value;
				}
			}
		} else if (parameterObject instanceof PageParams) { // 參數是或者繼承PageParams
			return (PageParams) parameterObject;
		} else { // 從POJO屬性嘗試讀取分頁參數
			Field[] fields = parameterObject.getClass().getDeclaredFields();
			// 嘗試從POJO中獲得類型爲PageParams的屬性
			for (Field field : fields) {
				if (field.getType() == PageParams.class) {
					PropertyDescriptor pd = new PropertyDescriptor(field.getName(), parameterObject.getClass());
					Method method = pd.getReadMethod();
					return (PageParams) method.invoke(parameterObject);
				}
			}
		}
		return pageParams;
	}

計算出這條SQL能返回多少條記錄,這是插件的難點之一。首先,修改爲總數的SQL。其次,要爲總數SQL設置參數,代碼如下:

/**
	 * 獲取總條數.
	 * 
	 * @param ivt
	 *            Invocation 入參
	 * @param metaStatementHandler
	 *            statementHandler
	 * @param boundSql
	 *            sql
	 * @param cleanOrderBy
	 *            是否清除order by語句
	 * @return sql查詢總數.
	 * @throws Throwable
	 *             異常.
	 */
	private int getTotal(Invocation ivt, MetaObject metaStatementHandler, BoundSql boundSql, Boolean cleanOrderBy)
			throws Throwable {
		// 獲取當前的mappedStatement
		MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
		// 配置對象
		Configuration cfg = mappedStatement.getConfiguration();
		// 當前需要執行的SQL
		String sql = (String) metaStatementHandler.getValue("delegate.boundSql.sql");
		// 去掉最後的order by語句
		if (cleanOrderBy) {
			sql = this.cleanOrderByForSql(sql);
		}
		// 改寫爲統計總數的SQL
		String countSql = "select count(*) as total from (" + sql + ") $_paging";
		// 獲取攔截方法參數,根據插件簽名,知道是Connection對象
		Connection connection = (Connection) ivt.getArgs()[0];
		PreparedStatement ps = null;
		int total = 0;
		try {
			// 預編譯統計總數SQL
			ps = connection.prepareStatement(countSql);
			// 構建統計總數BoundSql
			BoundSql countBoundSql = new BoundSql(cfg, countSql, boundSql.getParameterMappings(),
					boundSql.getParameterObject());
			// 構建MyBatis的ParameterHandler用來設置總數Sql的參數
			ParameterHandler handler = new DefaultParameterHandler(mappedStatement, boundSql.getParameterObject(),
					countBoundSql);
			// 設置總數SQL參數
			handler.setParameters(ps);
			// 執行查詢.
			ResultSet rs = ps.executeQuery();
			while (rs.next()) {
				total = rs.getInt("total");
			}
		} finally {
			// 這裏不能關閉Connection,否則後續的SQL就沒法繼續了
			if (ps != null) {
				ps.close();
			}
		}
		return total;
	}

	private String cleanOrderByForSql(String sql) {
		StringBuilder sb = new StringBuilder(sql);
		String newSql = sql.toLowerCase();
		// 如果沒有order語句,直接返回
		if (newSql.indexOf("order") == -1) {
			return sql;
		}
		int idx = newSql.lastIndexOf("order");
		return sb.substring(0, idx).toString();
	}

首先,從BoundSql中分離出SQL,通過分頁參數判斷是否需要去掉order by語句,如果需要,則刪除,這樣是爲了避免影響執行性能。然後通過StatementHandler的參數獲取數據庫連接資源,使用JDBC獲取總數,但是難點在於如何給總數SQL預編譯參數。原本SQL設置參數是通過ParameterHandler進行處理的,因此首先使用總數的SQL創建BoundSql和ParameterHandler對象,然後就可以通過總數的ParameterHandler對象設置總數SQL的參數,這樣就能夠執行總數SQL,求出這條SQL所能查詢出的總數,然後將其返回。
求出了總數,就可以通過每頁條數的限制求出總頁數,並與總數一起回填到分頁總數中,然後判斷當前頁面是否合法,這需要判斷一個分頁參數的屬性——checkFlag,當true時纔回去判斷:

/**
	 * 檢查當前頁碼的有效性
	 *
	 * @param checkFlag
	 *            檢測標誌
	 * @param pageNum
	 *            當前頁碼
	 * @param pageTotal
	 *            最大頁碼
	 * @throws Throwable
	 */
	private void checkPage(Boolean checkFlag, Integer pageNum, Integer pageTotal) throws Throwable {
		if (checkFlag) {
			// 檢查頁碼page是否合法
			if (pageNum > pageTotal) {
				throw new Exception("查詢失敗,查詢頁碼【" + pageNum + "】大於總頁數【" + pageTotal + "】!!");
			}
		}
	}

查詢當前頁就需要修改原有的SQL,並且加入SQL的分頁參數,這樣就容易查詢到分頁數據:

/**
	 * 預編譯改寫後的SQL,並設置分頁參數
	 *
	 * @param invocation
	 *            入參
	 * @param metaStatementHandler
	 *            MetaObject綁定的StatementHandler
	 * @param boundSql
	 *            boundSql對象
	 * @param pageNum
	 *            當前頁
	 * @param pageSize
	 *            最大頁
	 * @throws IllegalAccessException
	 *             異常
	 * @throws InvocationTargetException
	 *             異常
	 */
	private Object preparedSQL(Invocation invocation, MetaObject metaStatementHandler, BoundSql boundSql, int pageNum,
			int pageSize) throws Exception {
		// 獲取當前需要執行的SQL
		String sql = boundSql.getSql();
		String newSql = "select * from (" + sql + ") $_paging_table limit ?, ?";
		// 修改當前需要執行的SQL
		metaStatementHandler.setValue("delegate.boundSql.sql", newSql);
		// 執行編譯,相當於StatementHandler執行了prepared()方法,這個時候,就剩下兩個分頁參數沒有設置
		Object statementObj = invocation.proceed();
		// 設置兩個分頁參數
		this.preparePageDataParams((PreparedStatement) statementObj, pageNum, pageSize);
		return statementObj;
	}

	/**
	 * 使用PreparedStatement預編譯兩個分頁參數,如果數據庫的規則不一樣,需要改寫設置的參數規則
	 *
	 * @throws SQLException
	 * @throws NotSupportedException
	 *
	 */
	private void preparePageDataParams(PreparedStatement ps, int pageNum, int pageSize) throws Exception {
		// prepared()方法編譯SQL,由於MyBatis上下文沒有分頁參數的信息,所以這裏需要設置這兩個參數
		// 獲取需要設置的參數個數,由於參數是最後的兩個,所以很容易得到其位置
		int idx = ps.getParameterMetaData().getParameterCount();
		// 最後兩個是我們的分頁參數
		ps.setInt(idx - 1, (pageNum - 1) * pageSize);// 開始行
		ps.setInt(idx, pageSize); // 限制條數
	}

取出原有的SQL,改寫爲分頁SQL。分頁SQL存在兩個參數:偏移量和限制條數。它們實際上調用了StatementHandler的prepare方法,這個方法的作用就是預編譯SQL的參數。由於加入了兩個新參數,所以調用了這個方法,並沒有把它們預編譯在內,因此這裏的preparePageDataParams方法就是爲了完成這個任務,在執行完preparedSQL後,就完成了查詢總條數、總頁數和分頁SQL的編譯,最後返回已編譯好的PreparedStatement對象,這樣就完成了分頁插件的邏輯。
MyBatis配置如下:

<plugins>
		<plugin interceptor="com.ssm.chapter8.plugin.PagePlugin">
			<property name="default.page" value="1" />
			<property name="default.pageSize" value="20" />
			<property name="default.useFlag" value="true" />
			<property name="default.checkFlag" value="false" />
			<property name="default.cleanOrderBy" value="false" />
		</plugin>
	</plugins>

映射器的配置如下:

<select id="findRole" parameterType="string" resultType="com.ssm.chapter8.pojo.Role">
        select id, role_name as roleName, note from t_role
        <where>
            <if test = "roleName != null">
                role_name like concat('%', #{roleName}, '%')
            </if>
        </where>
</select>

總結

注意以下6點:

  • 插件是MyBatis最強大的組件。也是最危險的組件,能不用盡量少用它。
  • 插件生成的是層層代理對象的責任鏈模式,通過反射方法運行,性能不高,所以減少插件就能減少代理,提高系統性能
  • 插件的基礎是SqlSession下的四大對象和它們的寫作,需要對四大對象的方法有較深入的理解,才能明確攔截什麼對象、攔截什麼方法及其參數,從而確定插件的簽名
  • 在插件中往往需要讀取和修改MyBatis映射器中的對象屬性
  • 插件的代碼編寫要考慮全面,特別是多個插件層層代理時,注意其執行順序及保證前後邏輯的正確性
  • 大部分插件都應該儘量少改動MyBatis的底層內容,才能減少錯誤的發生。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章