您的位置:首页 > 编程语言

Mybatis 面向接口编程

2016-07-25 16:17 405 查看
 在使用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

<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类型的参数类型

[java] view
plain copy

package com.gusi.demo.idao;  

public interface IUser{  

    public com.gusi.demo.pojo.User find(long id);//这就是对应的接口方法之一  

}  

第四:修改UserDao中对数据库访问的方法

[java] view
plain copy

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对象  

第五:测试接口

[java] view
plain copy

@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/46414721;java动态代理可参考:http://blog.csdn.net/dyy_gusi/article/details/46414605

第一步:获取接口对象的代理对象

IUser iUser = sqlSession.getMapper(Iuser.class);这句代码其实是去获取一个IUser接口对象的代理对象。

源码片段1:

[java] view
plain copy

 @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:

[java] view
plain copy

 @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:

[java] view
plain copy

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:

[java] view
plain copy

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:

[java] view
plain copy

@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:

[java] view
plain copy

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帮助我们完成的任务。

项目源码:https://github.com/yoting/demoMybatis
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: