Mybatis中SqlMapper配置的擴展與應用(3)


隔了兩周,首先回顧一下,在Mybatis中的SqlMapper配置文件中引入的幾個擴展機制:

1.引入SQL配置函數,簡化配置、屏蔽DB底層差異性
2.引入自定義命名空間,允許自定義語句級元素、腳本級元素
3.引入表達式配置,擴充SqlMapper配置的表達能力

前面兩條已經舉過例子,現在來看看怎么使用表達式配置。說到表達式語言,最為富麗堂皇的自然就是OGNL,但這也正是Mybatis內部訪問數據的固有方式,所以也輪不到我們在這里來擴充了(事實上Mybatis的參數設置並不能使用完全的OGNL)。那么,除了OGNL,還有哪些表達式語言呢?別忘了,我們的前提是Spring環境,自然,SpEL表達式也就走入我們的視野,因此這篇文章就重點記錄在SqlMapper中使用SpEL表達式

四、在Mybatis中的SqlMapper使用SpEL表達式

1.SpEL工具類

SpEL就是Spring提供的EL表達式,雖然到Spring3才開始推出,但已經是Spring的一個基礎核心模塊了,地位已經差不多等同於IoC和AOP了。SpEL和OGNL類似,也有表達式、上下文環境、root對象等概念,但和OGNL不同的是,SpEL還提供了訪問Spring中bean的能力——這是非常強悍的,試問一個Spring應用有多少類不是Spring管理的呢?具體的SpEL語法細節可以參考Spring的官方文檔。
SpEL目前主要應用於Spring的配置,使用起來非常方便,但是在Java類中使用則比較繁瑣,稍微實用一點的例子都需要創建解析器實例、創建執行環境、解析表達式、對表達式求值等步驟,如果需要訪問Spring的Bean,還要設置BeanFactoryResolver等,因此,為了簡化SpEL在Java中的應用,我編寫了一個SpEL的幫助類:

這個工具類分成四個部分:

  1. 實現ApplicationContextAware接口,注入ApplicationContext(BeanFactory)對象
  2. 表達式求值方法
    • 對表達式簡單求值(還可指定返回的目標類型)
    • 指定root對象,對表達式求值(還可指定返回的目標類型)
    • 指定root對象和其它變量,對表達式求值(還可指定返回的目標類型)
  3. 表達式設置方法
    • 設置表達式的值
    • 指定root對象,設置表達式的值
    • 指定root對象和其它變量,設置表達式的值
  4. 變量管理方法
    • 添加變量
    • 移除變量

此外,還內置了一個保護變量Tool。
編寫一個測試類驗證一下:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={
	"classpath:applicationContext.xml"	
})
@Component  // 該測試類本身作為一個Spring管理的bean,便於后面的測試
public class SpringHelpTest {
	
	public String getBeanValue(String arg){//bean的一個方法
		return "beanValue:"+arg;
	}

	@Test
	public void testSpelHelp(){
		// 准備root對象 {key1 : 'root-value1', key2 : 'root-value2'}
		Root root = new Root("root-value1", "root-value2");
		// 准備一般變量
		Map<String, Object> vars = new HashMap<String, Object>();
		vars.put("var1", "value1");
		vars.put("var2", "value2");
		// 直接計算簡單表達式
		Object rs = SpringHelp.evaluate("1+2");
		Assert.assertEquals(3, rs);
		// 按指定類型計算簡單表達式
		rs = SpringHelp.evaluate("1+2", String.class);
		Assert.assertEquals("3", rs);
		// 訪問root對象的屬性
		rs = SpringHelp.evaluate(root, "key1");
		Assert.assertEquals("root-value1", rs);
		// 訪問一般變量
		rs = SpringHelp.evaluate(root, "#var2", vars);
		Assert.assertEquals("value2", rs);
		// 訪問root對象
		rs = SpringHelp.evaluate(root, "#root", vars);
		Assert.assertTrue(rs == root);
		// 訪問Spring管理的bean,同時傳入的參數又是root對象的屬性
		rs = SpringHelp.evaluate(root, "@springHelpTest.getBeanValue(key2)", vars);
		Assert.assertEquals("beanValue:root-value2", rs);
		// 設置root對象的屬性
		SpringHelp.setValue(root, "key1", "new-root-value1");
		rs = SpringHelp.evaluate(root, "key1");
		Assert.assertEquals("new-root-value1", rs);
		//訪問工具類,其中Tool.DATE.getDate()的作用是獲取當前日期
		rs = SpringHelp.evaluate("#Tool.DATE.getDate()");
		Assert.assertEquals(Tool.DATE.getDate(), rs);
	}
	
	public class Root{
		String key1;
		String key2;
		Root(String key1, String key2){
			this.key1 = key1;
			this.key2 = key2;
		}
		// 省略getter/setter方法
	}
}

有了這個靜態幫助類,在Java中使用SpEL就方便很多了。

2.編寫表達式處理器

利用SpEL幫助類,編寫表達式處理器IExpressionHandler的實現,具體邏輯參看代碼中的注釋

public class SpelExpressionHandler implements IExpressionHandler {
	
	/**
	 * 直接返回true,也就是說不做進一步判斷,支持所有的${(exp)}、#{(exp)}內的表達式
	 * 由於支持所有表達式,實際上起到了一種攔截作用,所以需要注意,注冊該實現時必須最低優先級
	 */
	@Override
	public boolean isSupport(String expression) {
		return true;
	}

	/**
	 * 對SqlMapper配置中的表達式求值
	 */
	@Override
	public Object eval(String expression, Object parameter, String databaseId) {
		/**
		 * 如果以spel:為前綴,則將mybatis包裝后的參數、數據庫id以及表達式自身一起封裝一個新的root對象
		 * 因此在exp表達式中可以通過params.paramName、databaseId等形式訪問
		 */
		if(expression.toLowerCase().startsWith("spel:")){
			expression = expression.substring(5);
			Root root = new Root(parameter, databaseId, expression);
			return SpringHelp.evaluate(root, expression);
		}
		/**
		 * 否則將databaseId作為一個特殊名稱的變量
		 * 因此在exp表達式中可以通過paramName、#databaseId等形式訪問
		 */
		else{
			Map<String, Object> vars = new HashMap<String, Object>();
			vars.put("databaseId", databaseId);
			return SpringHelp.evaluate(parameter, expression, vars);
		}
	}
	
	public class Root {

		private final Object params;
		private final String databaseId;
		private final String expression;

		public Root(Object params, String databaseId, String expression) {
			this.params = params;
			this.databaseId = databaseId;
			this.expression = expression;
		}

		// 省略getter/setter方法
	}
}

3.注冊表達式處理器

如上面的注釋,注冊的時候需要注意一點,優先級要最低,以避免所有表達式都被攔截,導致其它的處理器不生效。
保證優先級最低,有一種方法,就是實現Spring中的Order接口,並且將該實現類的order值設置為最大,然后按Order排序;另外一種方法,就是干脆另起爐灶,單獨一個屬性保存默認處理器,只有其它處理器都不支持的時候才使用默認處理器,請看下面的代碼:

/**
 * 表達式處理器
 */
private static final Set<IExpressionHandler> expressions = new LinkedHashSet<IExpressionHandler>();
/**
 * 默認表達式處理器
 */
private static final IExpressionHandler defaultExpressionHandler = new SpelExpressionHandler();
/**
 * 獲取表達式處理器
 * @param node
 * @return
 */
public static IExpressionHandler getExpressionHandler(String expression){
	for(IExpressionHandler handler : expressions){
		if(handler.isSupport(expression)){
			return handler;
		}
	}
	return defaultExpressionHandler;
}

4.修改SqlMapper中配置

<?xml version="1.0" encoding="UTF-8" ?>
<mapper xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns="http://dysd.org/schema/sqlmapper"
	xmlns:e="http://dysd.org/schema/sqlmapper-extend"
	xsi:schemaLocation="http://dysd.org/schema/sqlmapper http://dysd.org/schema/sqlmapper.xsd
		http://dysd.org/schema/sqlmapper-extend http://dysd.org/schema/sqlmapper-extend.xsd"
	namespace="org.dysd.dao.mybatis.mapper.IExampleDao">
	
	<select id="selectString" resultType="string">
		select PARAM_NAME, ${(@spelBean.param(paramName))} AS TEST_SPEL
		  from BF_PARAM_ENUM_DEF
		 where PARAM_NAME $like{#{(spel:@spelBean.root(#root,params.paramName)), jdbcType=VARCHAR}}
		 order by SEQNO
	</select>
</mapper>

5.編寫配置中的bean

@Component("spelBean")
public class SpelBean {

	public String param(String paramName){
		// 測試的是${()},所以返回結果中添加單引號
		return "'PARAM-"+paramName+"'";
	}
	
	public String root(SpelExpressionHandler.Root root,String paramName){
		// 測試spel:為前綴的表達式,所以可以直接訪問SpelExpressionHandler.Root對象
		return "ROOT-"+root.getDatabaseId()+"-"+paramName;
	}
}

6.編寫Dao接口

@Repository
public interface IExampleDao {
	
	public String selectString(@Param("paramName")String paramName);
}

7.編寫JUnit測試類

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={
	"classpath:spring/applicationContext.xml"	
})
@Service
public class ExampleDaoTest{

	@Resource
	private IExampleDao dao;
	
	@Test
	public void testSelectString(){
		try {
			String a = dao.selectString("DISPLAY_AREA");
			Assert.assertEquals("顯示區域", a);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

8.執行測試

20161119 19:00:44,298 [main]-[DEBUG] ==>  Preparing: select PARAM_NAME, 'PARAM-DISPLAY_AREA' AS TEST_SPEL from BF_PARAM_ENUM_DEF where PARAM_NAME LIKE CONCAT('%',?,'%') order by SEQNO 
20161119 19:00:48,001 [main]-[DEBUG] ==> Parameters: ROOT-MySQL-DISPLAY_AREA(String)

可以看到,無論是${(exp)}還是#{(exp)},其中的exp都已經得到正確的解析了。

在SqlMapper中可以調用Spring的Bean,大大豐富了SqlMapper的表達能力,但是對於${(exp)}這種情形,由於是字符串的簡單替換,也存在SQL注入的風險,因此一般只使用#{(exp)}。

題外話:
1.SqlMapper的擴展與應用系列算是暫告一段落,有朋友希望我能提供實際的案例,我利用這兩周的業余時間整理了一下,在GitHub和OSChina同步上傳了這個項目,有興趣的朋友可以看一下,也希望可以多提一點建議給我。因為是maven項目,希望實際運行的朋友最好搭建一個nexus私服,然后git下載,導入至Eclipse中,修改數據庫配置即可。
具體地址:
GitHub:https://github.com/linjisong/dysd
OSChina:https://git.oschina.net/linjisong/dysd
2.在博客園中首次使用Markdown,好多地方還不熟悉,比如代碼折疊,但也算是一種新的嘗試。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM