Mybatis 面向接口编程

 在使用Mybatis的时候,我们通过sqlSession的各种方法和数据交互,比如查询我们是通过sqlSession.selectList("Namespace.sqlId",paramObj),对于插入数据以及修改和删除数据也是同样的通过sqlSession的方法操作,传入配置文件中sql语句对应的唯一id以及动态拼装sql的参数。然后返回的结果是泛型类型,也就是任意的Object类型。距离来说就是下面这条java代码:Object result = sqlSession.selsectOne("Namespace.sqlId",paramObj);但是这样写java代码其实是有隐患的,具体共有三个隐患:
第一是String类型的"namespace.sqlId"容易出错,在Java代码中或这Mybatis映射文件需要完全一致。不仅要namespace相同,还要sql的id也完全相同。
第二是请求参数是任意Object类型,可以是java基本类型,也可以是JavaBean类型,但是在Mybatis映射文件中接收的parameterType的类型是确定的,如果两个不类型不一致势必会导致出错。
第三是返回类型为泛型的任意类型,其实返回的类型也是在Mybatis映射文件中通过resultMap或者resultType限定,如果我们在java代码中接收返回的数据类型和映射文件中不同,肯定也会出问题。

第一部分:实现面向接口编程
那么如何避免上面说的三个隐患呢 ?我们可以使用Mybatis提供的面向接口编程,具体的操作方法如下:
第一:编写一个接口,(IUser.java)
接口暂时为空接口,接口文件包路径为:com.gusi.demo.idao.IUser
第二:修改映射文件,(User.xml)
将namespace属性值改为上面定义接口的类的全名称:com.gusi.demo.idao.IUser。然后将每个sql语句的id记录下来,接收参数类型记录下来,以及返回类型记录下来。
[html]  view plain  copy
  1. <mapper namespace="com.gusi.demo.idao.IUser">  
  2.   
  3.   <resultMap type="com.gusi.demo.pojo.User" id="UserResult">  
  4.     <id column="id" jdbcType="INTEGER" property="id"/>  
  5.     <result column="username" jdbcType="VARCHAR" property="username"/>  
  6.     <result column="password" jdbcType="VARCHAR" property="password.encrypted"/>  
  7.     <result column="administrator" jdbcType="BOOLEAN" property="administrator"/>  
  8.   </resultMap>  
  9.   
  10.   <select id="find" parameterType="long" resultMap="UserResult">  
  11.     SELECT * FROM user WHERE id = #{id:INTEGER}  
  12.   </select>  
  13. </mapper>  
第三:给上面的每一个sql语句在接口类IUser.java中添加一个接口方法(上面只有一条sql语句,所以只添加一个接口方法)
接口方法的返回类型就为上面记录的返回类型:com.gusi.demo.pojo.User类型,当然这个地方也支持java基本类型和String类型
接口方法的名称就为上面记录sql语句的id:find,这个id在同一个namespace下是唯一的
接口方法的请求参数就为上面记录的参数类型:long,当然这个地方是支持JavaBean类型的参数类型
[java]  view plain  copy
  1. package com.gusi.demo.idao;  
  2. public interface IUser{  
  3.     public com.gusi.demo.pojo.User find(long id);//这就是对应的接口方法之一  
  4. }  
第四:修改UserDao中对数据库访问的方法
[java]  view plain  copy
  1. SqlSession sqlSession = sqlSessionFactory.getSqlSession();//获得一个sqlSession  
  2. //以前代码写法如下:  
  3. //User user = sqlSession.selectOne("User.find",1L);  
  4. //改为面向接口编程:  
  5. IUser iUser = sqlSession.getMapper(IUser.class);//通过sqlSession获取对应注册接口  
  6. User user = iUser.find(1L);//直接调运接口方法就可以获得对应的User对象  
第五:测试接口
[java]  view plain  copy
  1. @Test  
  2. public void testFind(){  
  3.     User user = UserDao.find(1L);//其实是测试IUser.find(long id)方法  
  4. }  

第二部分:面向接口编程原理简单剖析
    通过上面的步骤我们很容易就实现的面向接口编程,我们不仅规避了上面提到的几种隐患,同时还使我们的代码更统一,便于管理。但是问题又来了,我们绝对没有给那个接口写任何实现类,怎么掉接口的方法就能成功执行到指定的sql语句然后返回合理的结果了。Mybatis到底是怎么实现的呢?要知道这个问题,只能通过看源码咯。在看源码之前,我们首先得了解java泛型以及java的动态代理,java泛型可参考: http://blog.csdn.net/dyy_gusi/article/details/46414721java动态代理可参考: http://blog.csdn.net/dyy_gusi/article/details/46414605

第一步:获取接口对象的代理对象
IUser iUser = sqlSession.getMapper(Iuser.class);这句代码其实是去获取一个IUser接口对象的代理对象。
源码片段1:
[java]  view plain  copy
  1.  @SuppressWarnings("unchecked")  
  2.  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {  
  3. //获得代理对象的工厂类  
  4.    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);  
  5.    if (mapperProxyFactory == null) {  
  6.      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");  
  7.    }  
  8.    try {  
  9. //去获得代理对象,调运源码片段2  
  10.      return mapperProxyFactory.newInstance(sqlSession);  
  11.    } catch (Exception e) {  
  12.      throw new BindingException("Error getting mapper instance. Cause: " + e, e);  
  13.    }  
  14.  }  
源码片段2:
[java]  view plain  copy
  1.  @SuppressWarnings("unchecked")  
  2.  protected T newInstance(MapperProxy<T> mapperProxy) {  
  3. //获得了一个正真的接口对象的代理对象(java动态代理对象),就是IUser接口对象  
  4.    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);  
  5.  }  
  6.  public T newInstance(SqlSession sqlSession) {  
  7.    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);  
  8.    return newInstance(mapperProxy);  
  9.  }  
源码片段3:
[java]  view plain  copy
  1. private void bindMapperForNamespace() {  
  2.   String namespace = builderAssistant.getCurrentNamespace();  
  3.   if (namespace != null) {  
  4.     Class<?> boundType = null;  
  5.     try {  
  6.       boundType = Resources.classForName(namespace);  
  7.     } catch (ClassNotFoundException e) {  
  8.       //ignore, bound type is not required  
  9.     }  
  10.     if (boundType != null) {  
  11.       if (!configuration.hasMapper(boundType)) {  
  12.         // Spring may not know the real resource name so we set a flag  
  13.         // to prevent loading again this resource from the mapper interface  
  14.         // look at MapperAnnotationBuilder#loadXmlResource  
  15.         configuration.addLoadedResource("namespace:" + namespace);  
  16. //在读取配置文件的过程中,将namespace对应的接口类(IUser.java)加入的map中,调运源码片段4  
  17.         configuration.addMapper(boundType);  
  18.       }  
  19.     }  
  20.   }  
  21. }  
源码片段4:
[java]  view plain  copy
  1. public <T> void addMapper(Class<T> type) {  
  2.   if (type.isInterface()) {  
  3.     if (hasMapper(type)) {  
  4.       throw new BindingException("Type " + type + " is already known to the MapperRegistry.");  
  5.     }  
  6.     boolean loadCompleted = false;  
  7.     try {  
  8. //根据接口类构建一个接口类代理对象放入到map中,以便源代码片段1中获取  
  9.       knownMappers.put(type, new MapperProxyFactory<T>(type));  
  10.       // It's important that the type is added before the parser is run  
  11.       // otherwise the binding may automatically be attempted by the  
  12.       // mapper parser. If the type is already known, it won't try.  
  13.       MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);  
  14.       parser.parse();  
  15.       loadCompleted = true;  
  16.     } finally {  
  17.       if (!loadCompleted) {  
  18.         knownMappers.remove(type);  
  19.       }  
  20.     }  
  21.   }  
  22. }  
整个过程梳理就是:在构建sqlSession的时候,会读取配置文件,配置文件中包含映射文件,所以先将映射文件namespace对应的接口对象解析出来,然后会提前将每一个映射文件对应的接口代理工厂对象(namespace对应的接口对象的代理工厂对象)加入到一个map中,然后sqlSession.getMapper方法会在map中获得对应的接口的代理工厂,最终通过相应的工厂获得相应的接口的代理对象(IUser对象)。

第二步:执行代理对象的invoke方法
iUser.find(1L);这句代码其实是通过获得代理对象调运代理对象的invoke方法。
源码片段5:
[java]  view plain  copy
  1. @Override  
  2. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {  
  3.   if (Object.class.equals(method.getDeclaringClass())) {  
  4.     try {//这段if内的代码是不会执行的,因为接口没有实现类,所以不能调运实现类的方法。  
  5.       return method.invoke(this, args);  
  6.     } catch (Throwable t) {  
  7.       throw ExceptionUtil.unwrapThrowable(t);  
  8.     }  
  9.   }  
  10. //下面两句才是真真有效的  
  11.   final MapperMethod mapperMethod = cachedMapperMethod(method);  
  12.   return mapperMethod.execute(sqlSession, args);//去执行对应的sql语句  
  13. }  
  14. //该方法会将源码片段6中对象的command属性和method属性赋值以便下一步执行sql语句  
  15. private MapperMethod cachedMapperMethod(Method method) {  
  16.   MapperMethod mapperMethod = methodCache.get(method);  
  17.   if (mapperMethod == null) {  
  18.     mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());  
  19.     methodCache.put(method, mapperMethod);  
  20.   }  
  21.   return mapperMethod;  
  22. }  
源码片段6:
[java]  view plain  copy
  1. public class MapperMethod {  
  2.   
  3.   private final SqlCommand command;//sql的命令存在该对象中,包含sqlId和sql语句的类型是增删改查哪种  
  4.   private final MethodSignature method;  
  5.   
  6.   public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {  
  7.     this.command = new SqlCommand(config, mapperInterface, method);//通过接口类和方法名字,可以得到namespace.sqlId的值  
  8.     this.method = new MethodSignature(config, method);  
  9.   }  
  10.  //执行sql语句  
  11.   public Object execute(SqlSession sqlSession, Object[] args) {  
  12.     Object result;  
  13.  //根据sqlCommand选择执行哪种sql语句  
  14.     if (SqlCommandType.INSERT == command.getType()) {  
  15.       Object param = method.convertArgsToSqlCommandParam(args);  
  16.       result = rowCountResult(sqlSession.insert(command.getName(), param));  
  17.     } else if (SqlCommandType.UPDATE == command.getType()) {  
  18.       Object param = method.convertArgsToSqlCommandParam(args);  
  19.       result = rowCountResult(sqlSession.update(command.getName(), param));  
  20.     } else if (SqlCommandType.DELETE == command.getType()) {  
  21.       Object param = method.convertArgsToSqlCommandParam(args);  
  22.       result = rowCountResult(sqlSession.delete(command.getName(), param));  
  23.     } else if (SqlCommandType.SELECT == command.getType()) {  
  24.       if (method.returnsVoid() && method.hasResultHandler()) {  
  25.         executeWithResultHandler(sqlSession, args);  
  26.         result = null;  
  27.       } else if (method.returnsMany()) {  
  28.         result = executeForMany(sqlSession, args);  
  29.       } else if (method.returnsMap()) {  
  30.         result = executeForMap(sqlSession, args);  
  31.       } else {  
  32.         Object param = method.convertArgsToSqlCommandParam(args);  
  33.  //如果是查询,就调运sqlSession的真真的查询方法  
  34.         result = sqlSession.selectOne(command.getName(), param);  
  35.       }  
  36.     } else if (SqlCommandType.FLUSH == command.getType()) {  
  37.         result = sqlSession.flushStatements();  
  38.     } else {  
  39.       throw new BindingException("Unknown execution method for: " + command.getName());  
  40.     }  
  41.     if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {  
  42.       throw new BindingException("Mapper method '" + command.getName()  
  43.           + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");  
  44.     }  
  45.     return result;  
  46.   }  
  47. }  
在invoke方法中通过反射获得映射文件中的对应namespace对应的sqlId,然后在执行配置的sql语句完成查询操作。因为整个过程中,接口对象没有实现类,所以在代理中其实是不会执行接口实现类的接口方法,而是巧妙的通过各种反射得到namespace.sqlId以及请求参数,执行真真的和数据库交互相关sqlSession.selectOne(),sqlSession.selectList(),sqlSession.insert()等各种方法。所以我们看似接口没有实现类,但是调运接口方法却完成了数据库的交互操作,这都是Mybatis帮助我们完成的任务。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值