在使用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();
-
-
-
- IUser iUser = sqlSession.getMapper(IUser.class);
- User user = iUser.find(1L);
第五:测试接口
- @Test
- public void testFind(){
- User user = UserDao.find(1L);
- }
第二部分:面向接口编程原理简单剖析
第一步:获取接口对象的代理对象
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 {
-
- 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) {
-
- 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) {
-
- }
- if (boundType != null) {
- if (!configuration.hasMapper(boundType)) {
-
-
-
- configuration.addLoadedResource("namespace:" + namespace);
-
- 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 {
-
- knownMappers.put(type, new MapperProxyFactory<T>(type));
-
-
-
- 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 {
- return method.invoke(this, args);
- } catch (Throwable t) {
- throw ExceptionUtil.unwrapThrowable(t);
- }
- }
-
- final MapperMethod mapperMethod = cachedMapperMethod(method);
- return mapperMethod.execute(sqlSession, args);
- }
-
- 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;
- private final MethodSignature method;
-
- public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
- this.command = new SqlCommand(config, mapperInterface, method);
- this.method = new MethodSignature(config, method);
- }
-
- public Object execute(SqlSession sqlSession, Object[] args) {
- Object result;
-
- 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);
-
- 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帮助我们完成的任务。