在使用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的类型是确定的,如果两个不类型不一致势必会导致出错。
第二是请求参数是任意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/46414721;
java动态代理可参考:
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帮助我们完成的任务。