zoukankan      html  css  js  c++  java
  • 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,好多地方还不熟悉,比如代码折叠,但也算是一种新的尝试。

  • 相关阅读:
    How much training data do you need?
    什么样的论文容易接收发表?​
    How to Write and Publish a Scientific Paper: 7th Edition(科技论文写作与发表教程)(11.04更新)
    如何起草你的第一篇科研论文——应该做&避免做
    Writing the first draft of your science paper — some dos and don’ts
    matlab学习笔记 bsxfun函数
    乘法器的Verilog HDL实现
    重构:改善既有代码的设计
    【Leetcode】Length of Last Word
    Saving HDU hdu
  • 原文地址:https://www.cnblogs.com/linjisong/p/6080834.html
Copyright © 2011-2022 走看看