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的幫助類:
這個工具類分成四個部分:
- 實現ApplicationContextAware介面,注入ApplicationContext(BeanFactory)物件
- 表示式求值方法
- 對錶達式簡單求值(還可指定返回的目標型別)
- 指定root物件,對錶達式求值(還可指定返回的目標型別)
- 指定root物件和其它變數,對錶達式求值(還可指定返回的目標型別)
- 表示式設定方法
- 設定表示式的值
- 指定root物件,設定表示式的值
- 指定root物件和其它變數,設定表示式的值
- 變數管理方法
- 新增變數
- 移除變數
此外,還內建了一個保護變數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,好多地方還不熟悉,比如程式碼摺疊,但也算是一種新的嘗試。