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记录下来,接收参数类型记录下来,以及返回类型记录下来。
<mapper namespace="com.gusi.demo.idao.IUser">

  <resultMap type="com.gusi.demo.pojo.User" id="UserResult">
    <id column="id" jdbcType="INTEGER" property="id"/>
    <result column="username" jdbcType="VARCHAR" property="username"/>
    <result column="password" jdbcType="VARCHAR" property="password.encrypted"/>
    <result column="administrator" jdbcType="BOOLEAN" property="administrator"/>
  </resultMap>

  <select id="find" parameterType="long" resultMap="UserResult">
    SELECT * FROM user WHERE id = #{id:INTEGER}
  </select>
</mapper>
第三:给上面的每一个sql语句在接口类IUser.java中添加一个接口方法(上面只有一条sql语句,所以只添加一个接口方法)
接口方法的返回类型就为上面记录的返回类型:com.gusi.demo.pojo.User类型,当然这个地方也支持java基本类型和String类型
接口方法的名称就为上面记录sql语句的id:find,这个id在同一个namespace下是唯一的
接口方法的请求参数就为上面记录的参数类型:long,当然这个地方是支持JavaBean类型的参数类型
package com.gusi.demo.idao;
public interface IUser{
    public com.gusi.demo.pojo.User find(long id);//这就是对应的接口方法之一
}
第四:修改UserDao中对数据库访问的方法
SqlSession sqlSession = sqlSessionFactory.getSqlSession();//获得一个sqlSession
//以前代码写法如下:
//User user = sqlSession.selectOne("User.find",1L);
//改为面向接口编程:
IUser iUser = sqlSession.getMapper(IUser.class);//通过sqlSession获取对应注册接口
User user = iUser.find(1L);//直接调运接口方法就可以获得对应的User对象
第五:测试接口
@Test
public void testFind(){
    User user = UserDao.find(1L);//其实是测试IUser.find(long id)方法
}

第二部分:面向接口编程原理简单剖析
    通过上面的步骤我们很容易就实现的面向接口编程,我们不仅规避了上面提到的几种隐患,同时还使我们的代码更统一,便于管理。但是问题又来了,我们绝对没有给那个接口写任何实现类,怎么掉接口的方法就能成功执行到指定的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:
  @SuppressWarnings("unchecked")
  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
 //获得代理对象的工厂类
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
 //去获得代理对象,调运源码片段2
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }
源码片段2:
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
 //获得了一个正真的接口对象的代理对象(java动态代理对象),就是IUser接口对象
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }
  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
源码片段3:
  private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
  //在读取配置文件的过程中,将namespace对应的接口类(IUser.java)加入的map中,调运源码片段4
          configuration.addMapper(boundType);
        }
      }
    }
  }
源码片段4:
  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
  //根据接口类构建一个接口类代理对象放入到map中,以便源代码片段1中获取
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }
整个过程梳理就是:在构建sqlSession的时候,会读取配置文件,配置文件中包含映射文件,所以先将映射文件namespace对应的接口对象解析出来,然后会提前将每一个映射文件对应的接口代理工厂对象(namespace对应的接口对象的代理工厂对象)加入到一个map中,然后sqlSession.getMapper方法会在map中获得对应的接口的代理工厂,最终通过相应的工厂获得相应的接口的代理对象(IUser对象)。

第二步:执行代理对象的invoke方法
iUser.find(1L);这句代码其实是通过获得代理对象调运代理对象的invoke方法。
源码片段5:
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
      try {//这段if内的代码是不会执行的,因为接口没有实现类,所以不能调运实现类的方法。
        return method.invoke(this, args);
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
    }
  //下面两句才是真真有效的
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    return mapperMethod.execute(sqlSession, args);//去执行对应的sql语句
  }
  //该方法会将源码片段6中对象的command属性和method属性赋值以便下一步执行sql语句
  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }
源码片段6:
public class MapperMethod {

  private final SqlCommand command;//sql的命令存在该对象中,包含sqlId和sql语句的类型是增删改查哪种
  private final MethodSignature method;

  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    this.command = new SqlCommand(config, mapperInterface, method);//通过接口类和方法名字,可以得到namespace.sqlId的值
    this.method = new MethodSignature(config, method);
  }
 //执行sql语句
  public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
 //根据sqlCommand选择执行哪种sql语句
    if (SqlCommandType.INSERT == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
    } else if (SqlCommandType.UPDATE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
    } else if (SqlCommandType.DELETE == command.getType()) {
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
    } else if (SqlCommandType.SELECT == command.getType()) {
      if (method.returnsVoid() && method.hasResultHandler()) {
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        result = executeForMap(sqlSession, args);
      } else {
        Object param = method.convertArgsToSqlCommandParam(args);
 //如果是查询,就调运sqlSession的真真的查询方法
        result = sqlSession.selectOne(command.getName(), param);
      }
    } else if (SqlCommandType.FLUSH == command.getType()) {
        result = sqlSession.flushStatements();
    } else {
      throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }
}
在invoke方法中通过反射获得映射文件中的对应namespace对应的sqlId,然后在执行配置的sql语句完成查询操作。因为整个过程中,接口对象没有实现类,所以在代理中其实是不会执行接口实现类的接口方法,而是巧妙的通过各种反射得到namespace.sqlId以及请求参数,执行真真的和数据库交互相关sqlSession.selectOne(),sqlSession.selectList(),sqlSession.insert()等各种方法。所以我们看似接口没有实现类,但是调运接口方法却完成了数据库的交互操作,这都是Mybatis帮助我们完成的任务。


评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值