mybatis源碼學習--spring+mybatis註解方式爲何mybatis的dao接口不須要實現類

  相信你們在剛開始學習mybatis註解方式,或者spring+mybatis註解方式的時候,必定會有一個疑問,爲何mybatis的dao接口只須要一個接口,不須要實現類,就能夠正常使用,筆者最開始的時候也會有這種疑問,當時在網上查了不少資料,也問過公司比較年長的同事,可是並無獲得答案,後來經過本身看mybatis的源碼的方式才明白其中道理,接下來我就對你們分享,爲何dao接口不須要實現類的原理,這篇文章的講解主要分爲兩部分:java

1.mybatis註解方式是怎樣經過沒有實現類的dao接口進行數據庫操做mysql

2.spring+mybatis註解方式是怎樣在沒有實現類的dao接口的狀況下結合的git

 

文章的結構是經過  總結+詳細講解  的方式來進行說明的,但願你們能和我一同進步,例子程序放在github上了,mybatis-demogithub

環境:spring

mybatis   3.2.7sql

mybatis-spring 1.2.2數據庫

spring  4.1.6apache

總結:編程

       1.mybatis註解方式經過沒有實現類的dao接口進行數據庫操做的原理,一句話歸納,就是jdk proxy,就是jdk代理session

         1)原理上:JDK動態動態代理的原理是根據 InvocationHandler 中的invoke()方法,由jdk爲你的接口手動生成了一個實現了對應接口的類,所以,你的接口能夠調用,這是理解mybatis接口沒有實現類能被調用的關鍵。
         2)功能上:能夠看出mybatis中的接口就是XML文件的描述,一方面這樣作的目的是和spring集成,將接口交給spring管理;另外一方面是爲了更加方便的管理XML文件(使用接口的package+interface做爲namespace,method做爲ID)

       

      2.spring+mybatis註解方式,也是沒有實現類的,可是spring會默認返回MapperFactoryBean對象做爲實現類的替換,可是這個只是被spring使用的,mybatis自己仍是經過jdk代理來運行的。

 

詳細講解:

 1.mybatis註解方式是怎樣經過沒有實現類的dao接口進行數據庫操做

/** 
 *  
 * 類UserMapper.java的實現描述:TODO 類實現描述  
 * @author yuezhihua 2015年7月9日 上午11:18:30 
 */  
public interface UserMapper {  
      
      
    /** 
     * 根據用戶id查詢用戶角色 
     * @param userId 
     * @return 
     */  
    @Select("select * from role_main a INNER JOIN user_role b ON a.id = b.role_id WHERE b.user_id=#{userId}")  
    public List<RolePO> getRolesByUserId(@Param("userId")Integer userId);  
      
      
    /** 
     * 根據用戶id查詢用戶角色名 
     * @param userId 
     * @return 
     */  
    @Select("select a.role_name from role_main a INNER JOIN user_role b ON a.id = b.role_id WHERE b.user_id=#{userId}")  
    public Set<String> getRoleNamesByUserId(@Param("userId")Integer userId);  
      
      
    /** 
     * 根據userid查詢用戶的全部權限 
     * @param userId 
     * @return 
     */  
    @Select("SELECT a.permission_name FROM permission_main a INNER JOIN role_permission b ON a.id=b.permission_id WHERE b.role_id IN (SELECT d.role_id from user_main c INNER JOIN user_role d ON c.id = d.user_id WHERE c.id=#{userId})")  
    public Set<String> getPermissionsByUserId(@Param("userId")Integer userId);  
      
    /** 
     * 經過用戶名查詢用戶信息 
     * @param username 
     * @return 
     */  
    @Select("select * from user_main where username=#{username}")  
    @Results({  
          @Result(property = "roleNames", column = "id", many = @Many(fetchType=FetchType.LAZY,select = "getRoleNamesByUserId")),  
          @Result(property = "permissionNames", column = "id", many = @Many(fetchType=FetchType.LAZY,select = "getPermissionsByUserId"))  
      })  
    public UserPO getUserByUsername(@Param("username")String username);  
      
      
    @Select("select username from user_main")  
    public List<String> getRoleMain();  
  
}  

測試用例:

 

/** 
 *  
 * 類SqlTemplateTest.java的實現描述:TODO 類實現描述  
 * @author yuezhihua 2015年7月29日 下午2:07:44 
 */  
@RunWith(SpringJUnit4ClassRunner.class)  
@ContextConfiguration(locations = {  
        "classpath*:spring/demo-locator.xml"  
})  
public class SqlTemplateTest {  
      
    @Autowired  
    private SqlSessionTemplate sqlSessionTemplate;  
      
    @Autowired  
    private SqlSessionFactory sqlSessionFactory;  
      
    /** 
     * 初始化datasource 
     */  
    @Before  
    public void init(){  
        DataSource ds = null;  
        try {  
            ds = BaseDataTest.createUnpooledDataSource(BaseDataTest.DERBY_PROPERTIES);  
            BaseDataTest.runScript(ds, "com/mybatis/demo/databases/lazyloader/lazyloader-schema.sql");  
            BaseDataTest.runScript(ds, "com/mybatis/demo/databases/lazyloader/lazyloader-dataload.sql");  
        } catch (IOException e) {  
            e.printStackTrace();  
        } catch (SQLException e) {  
            e.printStackTrace();  
        }  
    }  
      
      
    /** 
     * 測試mybatis自身的查詢 
     */  
    @Test  
    public void testMybatisSelect(){  
        SqlSession session = sqlSessionFactory.openSession();  
        UserMapper mapper = session.getMapper(UserMapper.class);  
        UserPO userPo = mapper.getUserByUsername("zhangsan");  
        System.out.println("-----testMybatisSelect:"+userPo.getUsername());  
    }  
      
      
      
    /** 
     * mybatis-spring : sqlSessionTemplate測試查詢 
     * java.lang.UnsupportedOperationException: Manual close is not allowed over a Spring managed SqlSession不要在乎 
     */  
    @Test  
    public void testSelect(){  
        UserPO  result = sqlSessionTemplate.selectOne("com.mybatis.demo.lazyload.mapper.UserMapper.getUserByUsername", "zhangsan");  
        System.out.println(result);  
    }  
      
  
}  

 

筆者這裏不是純使用mybatis,而是使用mybatis+spring,講解第一部分的時候,我仍是會用帶有spring的方式來給你們講解,你們注重看原理就好

 

第一部分的時候會用到測試用例;testMybatisSelect

你們能夠看到,測試用例裏邊獲取dao接口的方法時session.getMapper(UserMapper.class);那我們就看看sqlsession是怎麼樣獲取usermapper接口的,返回的這個usermaper接口又有什麼改變

獲取usermapper接口代理對象的時序圖

 

 返回的Usermapper編程了jdk代理對象,org.apache.ibatis.binding.MapperProxy@7e276594

雖然這裏打印信息顯示貌似mapperproxy是usermapper的實現類,可是筆者認爲,mapperproxy不能算是usermapper的實現類,由於筆者以爲實現類的概念是應該實現usermapper接口的,可是mapperproxy不是,mapperproxy只是usermapper執行方法以前的一個攔截器

因此session.getMapper(UserMapper.class)返回的實際上是usermapper的代理對象,並且usermapper中定義的方法執行都是經過mapperproxy的invoke方法代理執行的,

接下來咱們看看mapper.getUserByUsername("zhangsan");這行代碼的執行過程,經過usermapper一個方法的執行來說解mybatis是怎麼經過dao接口執行數據庫操做的

mapperproxy.invoke(至關於一個攔截器):

 

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);  
  }  

 

這個方法中,首先會過濾object中的通用方法,遇到object方法會直接執行

 

可是若是是非通用方法,就會調用mappermethod.execute來代理執行方法,

mappermethod.execute

 

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 {  
      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;  
  }  

 

這個execute方法會根據不一樣的註解@select,@update,@delete,@insert來分配不一樣的執行sql環境,進行操做數據庫,其實這四個操做能夠分爲兩類,一類是更新類型,返回更新的行數;一類是查詢類型,返回查詢的結果,這兩部分的內部原理我會在其餘的文章中進行詳細解釋。

 

因此,能夠總結說,mybatis執行jdk代理的dao接口方法,跳轉到mappermethod,execute方法來執行具體的數據庫操做,而且返回結果;並且經過jdk代理的方法返回的代理對象,讓人感受和原接口對象同樣,形成使用沒有實現類的接口來執行的感受

第二部分:

spring+mybatis註解方式是怎樣在沒有實現類的dao接口的狀況下結合的

我們先看一下spring是怎麼管理mybatis的dao接口的吧。

我畫了一個流程圖

 

 

 配置文件掃描全部mybatis的dao接口:

<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">  
        <property name="basePackage" value="com.mybatis.demo.*.mapper" />  
        <!-- 這裏要用傳beanName,不能傳bean的ref,不然,會提早加載,用不到PropertyPlaceholder,切記 -->  
        <property name="sqlSessionFactoryBeanName" value="demo_sqlSessionFactory" />  
    </bean>  

ClasspathMapperScanner.doScan

/** 
   * Calls the parent search that will search and register all the candidates. 
   * Then the registered objects are post processed to set them as 
   * MapperFactoryBeans 
   */  
  @Override  
  public Set<BeanDefinitionHolder> doScan(String... basePackages) {  
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);  
  
    if (beanDefinitions.isEmpty()) {  
      logger.warn("No MyBatis mapper was found in '" + Arrays.toString(basePackages) + "' package. Please check your configuration.");  
    } else {  
      for (BeanDefinitionHolder holder : beanDefinitions) {  
        GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();  
  
        if (logger.isDebugEnabled()) {  
          logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName()   
              + "' and '" + definition.getBeanClassName() + "' mapperInterface");  
        }  
  
        // the mapper interface is the original class of the bean  
        // but, the actual class of the bean is MapperFactoryBean  
        <span style="color:#ff6666;">definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());  
        definition.setBeanClass(MapperFactoryBean.class);</span>  
  
        definition.getPropertyValues().add("addToConfig", this.addToConfig);  
  
        boolean explicitFactoryUsed = false;  
        if (StringUtils.hasText(this.sqlSessionFactoryBeanName)) {  
          definition.getPropertyValues().add("sqlSessionFactory", new RuntimeBeanReference(this.sqlSessionFactoryBeanName));  
          explicitFactoryUsed = true;  
        } else if (this.sqlSessionFactory != null) {  
          definition.getPropertyValues().add("sqlSessionFactory", this.sqlSessionFactory);  
          explicitFactoryUsed = true;  
        }  
  
        if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) {  
          if (explicitFactoryUsed) {  
            logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");  
          }  
          definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName));  
          explicitFactoryUsed = true;  
        } else if (this.sqlSessionTemplate != null) {  
          if (explicitFactoryUsed) {  
            logger.warn("Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.");  
          }  
          definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate);  
          explicitFactoryUsed = true;  
        }  
  
        if (!explicitFactoryUsed) {  
          if (logger.isDebugEnabled()) {  
            logger.debug("Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");  
          }  
          definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);  
        }  
      }  
    }  
  
    return beanDefinitions;  
  }  

你們注意看紅色部分,紅色部分的意思就是對於mybatis的dao接口,spring是以MapperFactoryBean的方式來管理的,舉個例子說

 

@autowired

private UserMapper userMapper;

這個userMapper返回的實例對象會是MapperFactoryBean,這個過程是由spring控制的,由於筆者對於spring原理沒有深刻研究過,筆者在這裏不作說明。

可能你們好奇,爲何這裏不能直接像第一部分同樣,經過sqlsession.getMapper(...)的方式來獲取dao接口對象呢,筆者在這裏以爲,之因此出現MapperFactoryBean

這個中間對象,是由於SqlSessionTemplate,sqlsessionTemplate是mybatis-spring封裝的用於方法執行mybatis方法的工具類,可是你們平時可能不多用到這個,

筆者在這裏作了一個小測試利用,簡單的看一下它的用法:

/** 
 *  
 * 類SqlTemplateTest.java的實現描述:TODO 類實現描述  
 * @author yuezhihua 2015年7月29日 下午2:07:44 
 */  
@RunWith(SpringJUnit4ClassRunner.class)  
@ContextConfiguration(locations = {  
        "classpath*:spring/demo-locator.xml"  
})  
public class SqlTemplateTest {  
      
    @Autowired  
    private SqlSessionTemplate sqlSessionTemplate;  
      
    @Autowired  
    private SqlSessionFactory sqlSessionFactory;  
      
    /** 
     * 初始化datasource 
     */  
    @Before  
    public void init(){  
        DataSource ds = null;  
        try {  
            ds = BaseDataTest.createUnpooledDataSource(BaseDataTest.DERBY_PROPERTIES);  
            BaseDataTest.runScript(ds, "com/mybatis/demo/databases/lazyloader/lazyloader-schema.sql");  
            BaseDataTest.runScript(ds, "com/mybatis/demo/databases/lazyloader/lazyloader-dataload.sql");  
        } catch (IOException e) {  
            e.printStackTrace();  
        } catch (SQLException e) {  
            e.printStackTrace();  
        }  
    }  
      
      
    /** 
     * 測試mybatis自身的查詢 
     */  
    @Test  
    public void testMybatisSelect(){  
        SqlSession session = sqlSessionFactory.openSession();  
        UserMapper mapper = session.getMapper(UserMapper.class);  
        System.out.println("jdk proxy mapper : "+mapper);  
        UserPO userPo = mapper.getUserByUsername("zhangsan");  
        System.out.println("-----testMybatisSelect:"+userPo.getUsername());  
    }  
      
      
      
    /** 
     * mybatis-spring : sqlSessionTemplate測試查詢 
     * java.lang.UnsupportedOperationException: Manual close is not allowed over a Spring managed SqlSession不要在乎 
     */  
    @Test  
    public void testSelect(){  
        UserPO  result = sqlSessionTemplate.selectOne("com.mybatis.demo.lazyload.mapper.UserMapper.getUserByUsername", "zhangsan");  
        System.out.println(result);  
    }  
      
  
}  

你們看上面testSelect這個測試用例,能夠看到sqlsessiontemplate的基本使用方法

spring+mybatis註解方式獲取dao接口對象的方法;:

MapperFactoryBean.getObject

/** 
   * {@inheritDoc} 
   */  
  public T getObject() throws Exception {  
    return getSqlSession().getMapper(this.mapperInterface);  
  }  

這裏邊的getSqlSession其實就是sqlsessiontemplate

總結:對於第一部分能夠說是返回了mybatis的dao接口的jdk代理對象,經過mapperproxy這個相似於攔截器同樣的類跳轉執行sql的,能夠說是原生dao接口的一層代理對象;

那麼對於第二部分來講,確實有三層代理對象:

因此,我們在spring中使用

@autowired

private UserMapper userMapper;

來注入對象的時候,實際上是經歷了 cglib --> mapperfactorybean  --> sqlsessiontemplate  --> mapperproxy  -->  原生dao接口  的包裝過程,才獲取的

因此我們在使用spring來調用沒有實現類的mybatis的dao接口的時候,並非像看起來那麼簡單,而是通過多層代理包裝的一個代理對象,對方法的執行也跳轉到mybatis框架中的mappermethod中

相關文章
相關標籤/搜索