Java EE开发平台随手记5——Mybatis动态代理接口方式的原生用法

  为了说明后续的Mybatis扩展,插播一篇广告,先来简要说明一下Mybatis的一种原生用法,不过先声明:下面说的只是Mybatis的其中一种用法,如需要更深入了解Mybatis,请参考官方文档,或者研读源码。  

  我们知道,使用Mybatis的方式有很多种,从是否集成上分,可以单独使用,也可以和Spring集成使用;从使用方式上分,可以编写静态工具类,在静态工具中调用SqlSession,也可以直接注入SqlSession/ SqlSessionTemplate,还可以编写Dao接口,让mybatis自动生成代理子类(对于只编写接口,不写实现类就可以运行有疑问的朋友,可以先了解一下JDK中的动态代理技术,这里就不展开了);在sql脚本的编写上,可以使用xml——也就是通常所说的SqlMapper,可以使用注解,还可以使用Mybatis提供的API生成等等。

  根据新平台运行环境和之前同事的开发习惯,我们的选择组合是(姑且称之为动态代理接口方式或代理接口):集成Spring + Dao接口/自动动态代理子类 + SqlMapper配置。

  代理接口方式,首先需要解决的就是下面三个基本问题:

问题1:确定需要执行的sqlId

  从SqlSession接口的方法签名可以知道,所有的数据访问方法,都必须有一个参数“statement”,也就是我们通常所说的sqlId。那么,sqlId是怎么确定的?SqlMapper中那么多配置,Mybatis怎么知道调用哪一个?

问题2:确定需要执行的方法

  不管是用哪种方式,归根结底还是调用SqlSession接口中的方法,那么问题来了,SqlSession接口中那么多不同方法,怎么知道调用哪一个呢?比如Dao接口中的一个查询方法,是调用SqlSession的selectOne?还是selectList?还是有回调的select?还是分页查询呢?

问题3:确定执行SQL时的参数

  除了sqlId,还有执行SQL时的参数,也是需要解决的一个基本问题,如果Dao接口中有多个参数,传入了多个参数,又怎么组装成一个统一的参数呢?SQL运行时的参数也会影响到执行的方法,因为很多重载的方法就只能通过运行时参数来确定。

  下面,我们带着这三个问题,看一个简单的用户维护的例子,分析一下代理接口方式原生用法是怎么处理这三个问题的,然后,也思考一下Mybatis的处理有没有什么不足和缺陷,我们又怎么处理这些不足和缺陷?

  业务场景做了简化,用户模型只包括用户ID、用户名称和用户所属的机构,需要完成的操作有:查询用户列表、查询用户列表(分页查询)、查找用户对象、新增用户、修改用户以及删除用户。

(1)Dao接口

 1 @Repository("userDao")
 2 public interface IUserDao {
 3
 4     /**
 5      * 查询用户列表
 6      * @param user    用户查询参数
 7      * @return
 8      */
 9     public List<UserBean> dList(UserForm user);
10
11     /**
12      * 查询用户列表(分页查询)
13      * @param user    用户查询参数
14      * @param page  分页对象
15      * @return
16      */
17     public List<UserBean> dPageList(UserForm user, IPage page);
18
19     /**
20      * 查找单个用户
21      * @param userId 用户ID
22      * @return
23      */
24     public UserBean dFind(@Param("userId")String userId);
25
26     /**
27      * 新增单个用户
28      * @param user
29      * @return
30      */
31     public int dInsert(UserForm user);
32
33     /**
34      * 更新单个用户
35      * @param user
36      * @return
37      */
38     public int dUpdate(UserForm user);
39
40     /**
41      * 删除单个用户
42      * @param userId
43      * @return
44      */
45     public int dDelete(@Param("userId")String userId);
46 }

(2)SqlMapper配置

 1 <?xml version="1.0" encoding="UTF-8" ?>
 2 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 3 <mapper namespace="com.forms.beneform4j.webapp.systemmanage.user.dao.IUserDao">
 4
 5     <select id="dList" resultType="com.forms.beneform4j.webapp.systemmanage.user.bean.UserBean">
 6         SELECT U.USER_ID, U.USER_NAME, U.ORG_ID
 7           FROM BF_USER U
 8          <where>
 9              <if test="null != userId and ‘‘ != userId">
10                  AND U.USER_ID = #{userId, jdbcType=VARCHAR}
11              </if>
12              <if test="null != orgId and ‘‘ != orgId">
13                  AND U.ORG_ID = #{orgId, jdbcType=VARCHAR}
14              </if>
15              <if test="null != userName and ‘‘ != userName">
16                  AND U.USER_NAME LIKE ‘%‘||#{userName, jdbcType=VARCHAR}||‘%‘
17              </if>
18          </where>
19          ORDER BY U.ORG_ID, U.USER_ID
20     </select>
21
22     <select id="dPageList" resultType="com.forms.beneform4j.webapp.systemmanage.user.bean.UserBean">
23         SELECT U.USER_ID, U.USER_NAME, U.ORG_ID
24           FROM BF_USER U
25          <where>
26              <if test="null != userId and ‘‘ != userId">
27                  AND U.USER_ID = #{userId, jdbcType=VARCHAR}
28              </if>
29              <if test="null != orgId and ‘‘ != orgId">
30                  AND U.ORG_ID = #{orgId, jdbcType=VARCHAR}
31              </if>
32              <if test="null != userName and ‘‘ != userName">
33                  AND U.USER_NAME LIKE ‘%‘||#{userName, jdbcType=VARCHAR}||‘%‘
34              </if>
35          </where>
36          ORDER BY U.ORG_ID, U.USER_ID
37     </select>
38
39     <select id="dFind" resultType="com.forms.beneform4j.webapp.systemmanage.user.bean.UserBean">
40         SELECT U.USER_ID, U.USER_NAME, U.ORG_ID
41           FROM BF_USER U
42          WHERE U.USER_ID = #{userId, jdbcType=VARCHAR}
43     </select>
44
45     <insert id="dInsert" >
46         INSERT INTO BF_USER (USER_ID, USER_NAME, ORG_ID)
47         VALUES (#{userId,jdbcType=VARCHAR}, #{userName,jdbcType=VARCHAR}, #{orgId,jdbcType=VARCHAR})
48     </insert>
49     <update id="dUpdate" >
50         UPDATE BF_USER
51            SET USER_NAME = #{userName,jdbcType=VARCHAR},
52                ORG_ID = #{orgId,jdbcType=VARCHAR}
53          WHERE USER_ID = #{userId,jdbcType=VARCHAR}
54     </update>
55     <delete id="dDelete">
56         DELETE FROM BF_USER
57          WHERE USER_ID = #{userId, jdbcType=VARCHAR}
58     </delete>
59 </mapper>

  现在来看Mybatis是怎么解决上面的问题的。

  第一个问题:确定需要执行的sqlId

  Mybatis的解决方法很简单,就是将接口类加上方法作为sqlId,可以用下面一个公式来描述:

需要执行的SqlId
 = Dao接口所在的类名(全限定名)+ 点号(.)+ 方法名
 = SqlMapper配置文件中的命名空间 + 点号(.) + [select|insert|update|delete]元素的id属性

  有什么不足呢?很明显,以下几种情形都无法处理:

  1. 两个重载的方法(方法名相同,参数不同),需要执行不同的sqlId
  2. 两个不同名的方法,需要引用相同的sqlId配置(参数不同,比如查询列表和查找元素,就常常可以共用一个<select>配置)
  3. 当前Dao中的方法需要引用命名空间和全限定类名不一致的sqlId(比如有一个公共<select>配置,各个业务模块都有可能引用这段配置,当然,这种情况下并不建议dao直接引用<select>配置,而建议编写一个公共服务)
  4. 此外,对于我们前面博客中提到的批量执行,也是无法处理的

  其实,这里比较好一点的方法是添加一个类似下面的sqlId查找策略接口:

1 public interface ISqlIdLookupStrategy {
2
3     public String lookup(Method method);
4 }

  Mybatis可以实现一个默认查找策略,然后用户也可以自己实现这个查找策略,比如就可以利用这个接口实现sqlId的置换——是的,就是传说中的张大教主的乾坤大挪移。但是遗憾的是,Mybatis开发人员估计认为没有必要不按默认方法查找,所以直接在代码中写死了。更加遗憾的是,在我们自己的平台中,也因为种种评审,将这个接口给砍掉,理由是既然Mybatis没有提供,我们也就可以不提供(好有道理,我竟然无法反驳,所以做开发平台也有很多不爽的地方——这是后话,暂且不说了)。

  第二个问题:确定需要执行的方法

  确定了sqlId,Mybatis怎么确定需要执行的方法呢?

  首先想到的,可以根据sqlId读取到SqlMapper中的配置(不用担心每次都去解析xml,Mybatis会有缓存的),然后就可以知道是select|insert|update|delete中的哪一种,因此,可以先确定sql语句的类型。对于insert、update、delete,基本上也就确定了调用方法,但是对于select,因为SqlSession中的方法还是很多,还需要进一步区分。

  进而想到的是,可以根据dao接口方法的签名来区分,签名包括返回值和形参,比如返回值是void,并且形参中包含处理查询结果的回调接口ResultHandler,那么,就可以大致确定需要调用void select(String statement, ResultHandler handler)这族重载方法中的一种,进一步可以分析形参中是否包含分页参数RowBounds、是否包含执行参数从而选择重载方法中的其中一种;又比如可以根据返回值是否为集合(Collection)类型来区分是查询单笔(selectOne)还是查询列表(selectList)。

  如果上面两种方法还确定不了,Mybatis还有第三种方式:在dao接口方法上添加特定的注解,比如使用@Flush表示批量执行,使用@MapKey表示selectMap等。

  说了这么多,可能还不够清晰,那既然是程序员,那就用程序员的语言来沟通,把Mybatis的源码贴在这里:

 1 public Object execute(SqlSession sqlSession, Object[] args) {
 2     Object result;
 3     if (SqlCommandType.INSERT == command.getType()) {
 4       Object param = method.convertArgsToSqlCommandParam(args);
 5       result = rowCountResult(sqlSession.insert(command.getName(), param));
 6     } else if (SqlCommandType.UPDATE == command.getType()) {
 7       Object param = method.convertArgsToSqlCommandParam(args);
 8       result = rowCountResult(sqlSession.update(command.getName(), param));
 9     } else if (SqlCommandType.DELETE == command.getType()) {
10       Object param = method.convertArgsToSqlCommandParam(args);
11       result = rowCountResult(sqlSession.delete(command.getName(), param));
12     } else if (SqlCommandType.SELECT == command.getType()) {
13       if (method.returnsVoid() && method.hasResultHandler()) {
14         executeWithResultHandler(sqlSession, args);
15         result = null;
16       } else if (method.returnsMany()) {
17         result = executeForMany(sqlSession, args);
18       } else if (method.returnsMap()) {
19         result = executeForMap(sqlSession, args);
20       } else {
21         Object param = method.convertArgsToSqlCommandParam(args);
22         result = sqlSession.selectOne(command.getName(), param);
23       }
24     } else if (SqlCommandType.FLUSH == command.getType()) {
25         result = sqlSession.flushStatements();
26     } else {
27       throw new BindingException("Unknown execution method for: " + command.getName());
28     }
29     if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
30       throw new BindingException("Mapper method ‘" + command.getName()
31           + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
32     }
33     return result;
34   }

需要注意的是,上面的那些方法和步骤也并没有完全确定调用的方法,还需要根据调用时传入的实际参数类型进一步筛选。

  简单总结一下,确定调用方法的依据:

  1. SqlMapper中的元素名称,如select | insert | update | delete
  2. 接口方法签名中特定类型或特定类型的子类型,如分页参数类型RowBounds、结果回调处理器类型ResultHandler
  3. 接口方法的返回值,如是否集合Collection类型或数组类型
  4. 接口方法的注解,如@Flush、@MapKey等
  5. 调用接口时的运行时参数

说到运行时参数,也引出第三个问题的处理:

  第三个问题:确定执行SQL时的参数

  上面例子中有的使用@Param注解,有的只是表单对象,有的包含类型的参数类型(如IPage等),Mybatis是怎么包装这些参数的呢?

   首先有个规则:特殊类型参数不参与组装运行SQL时的参数,那什么是特殊类型参数?实际上就是有特殊意义的参数类型,在Mybatis原生用法中,只有两个,那就是分页对象类型RowBounds和结果回调处理类型ResultHandler,后面我们还会扩展这个特殊类型,添加IPage等。

除了特殊类型的参数之外,Mybatis按照如下的逻辑包装运行SQL时的参数:

  1. 如果实参为null或者除了特殊类型参数外没有形参,那就直接返回null
  2. 如果没有标注了@Param注解的形参,并且形参的个数只有1个(特殊类型参数之外),那么返回实参中的这个对应的参数对象,不做任何包装
  3. 其它情形,先创建一个Map结构,将含@Param注解的参数以该注解的值为键值,对应索引处的实际参数为值存入Map结构,其它没有@Param注解的参数,就使用"param"加上索引序号为键值,对应实际参数为值存入Map结构,最后再返回这个Map结构作为SqlSession接口方法的参数对象

  同样,把相关源码贴一下:

 1 public Object convertArgsToSqlCommandParam(Object[] args) {
 2     /**
 3      *  1.params 是一个类属性,属于Map类型,其中存储的是参数位置索引和@Param注解中value值的映射关系,但会忽略特殊类型的参数
 4      *  2.hasNamedParameters也是一个类属性,boolean类型,表示参数是否有@Param注解(只要任意一个参数有该注解就算有)
 5      */
 6     final int paramCount = params.size();
 7     if (args == null || paramCount == 0) {
 8         return null;
 9     } else if (!hasNamedParameters && paramCount == 1) {
10         return args[params.keySet().iterator().next().intValue()];
11     } else {
12         final Map<String, Object> param = new ParamMap<Object>();
13         int i = 0;
14         for (Map.Entry<Integer, String> entry : params.entrySet()) {
15             param.put(entry.getValue(), args[entry.getKey().intValue()]);
16             // issue #71, add param names as param1, param2...but ensure
17             // backward compatibility
18             final String genericParamName = "param" + String.valueOf(i + 1);
19             if (!param.containsKey(genericParamName)) {
20                 param.put(genericParamName, args[entry.getKey()]);
21             }
22             i++;
23         }
24         return param;
25     }
26 }

  对于执行参数的组装还不明确的朋友,可以自己跟踪源码调试分析一下。

  上面三个基本问题很重要,这是我们赖以使用和扩展的基础。广告插播先到这里,后面继续记录对Mybatis的扩展使用。

时间: 2024-10-14 01:09:32

Java EE开发平台随手记5——Mybatis动态代理接口方式的原生用法的相关文章

Java EE开发平台随手记6——Mybatis扩展4

这篇博客中来说一下对Mybatis动态代理接口方式的扩展,对于Mybatis动态代理接口不熟悉的朋友,可以参考前一篇博客,或者研读Mybatis源码. 扩展11:动态代理接口扩展 我们知道,真正在Mybatis动态代理接口方式背后起作用的是SqlSession接口,类似地,我们的动态代理接口扩展则是基于IDaoTemplate接口,同样的,也需要解决相同的三个基本问题: 问题1:确定需要执行的sqlId 原生用法是根据包名.接口名.方法名去查找,但我们推荐添加一个sqlId的查找策略接口: pu

Java EE开发平台随手记2——Mybatis扩展1

今天来记录一下对Mybatis的扩展,版本是3.3.0,是和Spring集成使用,mybatis-spring集成包的版本是1.2.3,如果使用maven,如下配置: <properties>元素下添加 1 <mybatis.version>3.3.0</mybatis.version> 2 <mybatis.spring.version>1.2.3</mybatis.spring.version> <dependencies>元素下

Java EE开发平台随手记1

过完春节以来,一直在负责搭建公司的新Java EE开发平台,所谓新平台,其实并不是什么新技术,不过是将目前业界较为流行的框架整合在一起,做一些简单的封装和扩展,让开发人员更加易用. 和之前负责具体的项目开发不同,不能只是功能实现就可以,还需要考虑更多的非功能性需求,比如性能.安全性.易用性.可维护性.易扩展性.兼容性等等,因此有很多在实际项目中觉得方便易用的功能不得不因种种原因而舍弃:另一方面,也常常会偶尔有一些新想法.新构想,但因缺乏实践论证,也往往没有加入进来:此外,对于平台中已经添加的那些

java快速开发平台可视化开发表单

XJR java快速开发平台,简单的理解就是:开发人员以某种编程语言或者某几种编程语言(比如:目前流行的多种web技术,包括springboot, JPA,Druid, Activiti,Lombok,swagger,poi,WebSocket,Jquery,BootStrap, maven,Jenkins 等等 )为基础,将各种需要的功能封装在不同的层中,具大家调用而开发出来的一个软件. 这个软件其实不是一个最终的软件产品,它是一个二次开发软件框架,用户可以在这个产品上进行各种各样的软件产品的

Java语言中反射动态代理接口的解释与演示

Java语言中反射动态代理接口的解释与演示 Java在JDK1.3的时候引入了动态代理机制.可以运用在框架编程与平台编程时候捕获事件.审核数据.日志等功能实现,首先看一下设计模式的UML图解: 当你调用一个接口API时候,实际实现类继承该接口,调用时候经过proxy实现. 在Java中动态代理实现的两个关键接口类与class类分别如下: java.lang.reflect.Proxy java.lang.reflect.InvocationHandler 我们下面就通过InvocationHan

Java动态代理实现方式一

Java代理设计模式(Proxy)的四种具体实现:静态代理和动态代理 实现方式一:静态代理 静态代理方式的优点 静态代理方式的缺点 Java动态代理实现方式一:InvocationHandler Java动态代理实现方式二:CGLIB 用CGLIB实现Java动态代理的局限性 面试问题:Java里的代理设计模式(Proxy Design Pattern)一共有几种实现方式?这个题目很像孔乙己问“茴香豆的茴字有哪几种写法? 所谓代理模式,是指客户端(Client)并不直接调用实际的对象(下图右下角

JAVA高速开发平台 - 开源 免费 - JEECG

JEECG 微云高速开发平台 当前最新版本号: 3.6.2(公布日期:20160315) 下载地址:http://git.oschina.net/jeecg/jeecg 前言: 随着 WEB UI 框架 ( EasyUI/Jquery UI/Ext/DWZ) 等的逐渐成熟,系统界面逐渐实现统一化,代码生成器也能够生成统一规范的界面! 代码生成+手工MERGE半智能开发将是新的趋势,单表数据模型和一对多数据模型的增删改查功能直接生成使用,可节省60%工作量,高速提高开发效率!! ! 简单介绍 JE

Java EE开发四大常用框架

我们对Java EE的框架有过很多介绍, 本文将对Java EE中常用的四个框架做一下系统的归纳,希望大家喜欢. Struts Struts是一个基于Sun Java EE平台的MVC框架,主要是采用Servlet和JSP技术来实现的. Struts框架可分为以下四个主要部分,其中三个就和MVC模式紧密相关: 1.模型 (Model),本质上来说在Struts中Model是一个Action类(这个会在后面详细讨论),开发者通过其实现商业逻辑,同时用户请求通过控制器(Controller)向Act

JAVA快速开发平台 - 开源 免费 - JEECG

JEECG 微云快速开发平台 当前最新版本: 3.6.2(发布日期:20160315) 下载地址:http://git.oschina.net/jeecg/jeecg 前言: 随着 WEB UI 框架 ( EasyUI/Jquery UI/Ext/DWZ) 等的逐渐成熟,系统界面逐渐实现统一化,代码生成器也可以生成统一规范的界面! 代码生成+手工MERGE半智能开发将是新的趋势,单表数据模型和一对多数据模型的增删改查功能直接生成使用,可节省60%工作量,快速提高开发效率!!! 简介 JEECG(