mybatis源码学习–spring+mybatis注解方式为什么mybatis的dao接口不需要实现类

  • Post author:
  • Post category:其他


相信大家在刚开始学习mybatis注解方式,或者

spring

+mybatis注解方式的时候,一定会有一个疑问,为什么mybatis的dao接口只需要一个接口,不需要实现类,就可以正常使用,笔者最开始的时候也会有这种疑问,当时在网上查了很多资料,也问过公司比较年长的同事,但是并没有得到答案,后来通过自己看mybatis的源码的方式才明白其中道理,接下来我就对大家分享,为什么dao接口不需要实现类的原理,这篇文章的讲解主要分为两部分:



1.mybatis注解方式是怎样通过没有实现类的dao接口进行

数据库

操作



2.spring+mybatis注解方式是怎样在没有实现类的dao接口的情况下结合的

文章的结构是通过  总结+详细讲解  的方式来进行说明的,希望大家能和我一同进步,例子程序放在github上了,

mybatis-demo

环境:

mybatis   3.2.7

mybatis-spring 1.2.2

spring  4.1.6

总结:

1.mybatis注解方式通过没有实现类的dao接口进行数据库操作的原理,一句话概括,就是jdk proxy,就是jdk代理

2.spring+mybatis注解方式,也是没有实现类的,但是spring会默认返回MapperFactoryBean对象作为实现类的替换,但是这个只是被spring使用的,mybatis本身还是通过jdk代理来运行的。

详细讲解:

1.mybatis注解方式是怎样通过没有实现类的dao接口进行数据库操作



  1. /**



  2. *



  3. * 类UserMapper.java的实现描述:TODO 类实现描述



  4. * @author yuezhihua 2015年7月9日 上午11:18:30



  5. */





  6. public




    interface


    UserMapper {





  7. /**



  8. * 根据用户id查询用户角色



  9. * @param userId



  10. * @return



  11. */





  12. @Select


    (


    “select * from role_main a INNER JOIN user_role b ON a.id = b.role_id WHERE b.user_id=#{userId}”


    )



  13. public


    List<RolePO> getRolesByUserId(


    @Param


    (


    “userId”


    )Integer userId);





  14. /**



  15. * 根据用户id查询用户角色名



  16. * @param userId



  17. * @return



  18. */





  19. @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}”


    )



  20. public


    Set<String> getRoleNamesByUserId(


    @Param


    (


    “userId”


    )Integer userId);





  21. /**



  22. * 根据userid查询用户的所有权限



  23. * @param userId



  24. * @return



  25. */





  26. @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})”


    )



  27. public


    Set<String> getPermissionsByUserId(


    @Param


    (


    “userId”


    )Integer userId);




  28. /**



  29. * 通过用户名查询用户信息



  30. * @param username



  31. * @return



  32. */





  33. @Select


    (


    “select * from user_main where username=#{username}”


    )



  34. @Results


    ({



  35. @Result


    (property =


    “roleNames”


    , column =


    “id”


    , many =


    @Many


    (fetchType=FetchType.LAZY,select =


    “getRoleNamesByUserId”


    )),



  36. @Result


    (property =


    “permissionNames”


    , column =


    “id”


    , many =


    @Many


    (fetchType=FetchType.LAZY,select =


    “getPermissionsByUserId”


    ))


  37. })


  38. public


    UserPO getUserByUsername(


    @Param


    (


    “username”


    )String username);





  39. @Select


    (


    “select username from user_main”


    )



  40. public


    List<String> getRoleMain();



  41. }


测试

用例:



  1. /**



  2. *



  3. * 类SqlTemplateTest.java的实现描述:TODO 类实现描述



  4. * @author yuezhihua 2015年7月29日 下午2:07:44



  5. */





  6. @RunWith


    (SpringJUnit4ClassRunner.


    class


    )



  7. @ContextConfiguration


    (locations = {



  8. “classpath*:spring/demo-locator.xml”




  9. })


  10. public




    class


    SqlTemplateTest {




  11. @Autowired





  12. private


    SqlSessionTemplate sqlSessionTemplate;




  13. @Autowired





  14. private


    SqlSessionFactory sqlSessionFactory;




  15. /**



  16. * 初始化datasource



  17. */





  18. @Before





  19. public




    void


    init(){


  20. DataSource ds =

    null


    ;



  21. try


    {


  22. ds = BaseDataTest.createUnpooledDataSource(BaseDataTest.DERBY_PROPERTIES);

  23. BaseDataTest.runScript(ds,

    “com/mybatis/demo/databases/lazyloader/lazyloader-schema.sql”


    );


  24. BaseDataTest.runScript(ds,

    “com/mybatis/demo/databases/lazyloader/lazyloader-dataload.sql”


    );


  25. }

    catch


    (IOException e) {


  26. e.printStackTrace();

  27. }

    catch


    (SQLException e) {


  28. e.printStackTrace();

  29. }

  30. }




  31. /**



  32. * 测试mybatis自身的查询



  33. */





  34. @Test





  35. public




    void


    testMybatisSelect(){


  36. SqlSession session = sqlSessionFactory.openSession();

  37. UserMapper mapper = session.getMapper(UserMapper.

    class


    );


  38. UserPO userPo = mapper.getUserByUsername(

    “zhangsan”


    );


  39. System.out.println(

    “—–testMybatisSelect:”


    +userPo.getUsername());


  40. }





  41. /**



  42. * mybatis-spring : sqlSessionTemplate测试查询



  43. * java.lang.UnsupportedOperationException: Manual close is not allowed over a Spring managed SqlSession不要在意



  44. */





  45. @Test





  46. public




    void


    testSelect(){


  47. UserPO  result = sqlSessionTemplate.selectOne(

    “com.mybatis.demo.lazyload.mapper.UserMapper.getUserByUsername”


    ,


    “zhangsan”


    );


  48. System.out.println(result);

  49. }



  50. }




笔者这里不是纯使用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(相当于一个拦截器):



  1. public


    Object invoke(Object proxy, Method method, Object[] args)


    throws


    Throwable {



  2. if


    (Object.


    class


    .equals(method.getDeclaringClass())) {



  3. try


    {



  4. return


    method.invoke(


    this


    , args);


  5. }

    catch


    (Throwable t) {



  6. throw


    ExceptionUtil.unwrapThrowable(t);


  7. }

  8. }


  9. final


    MapperMethod mapperMethod = cachedMapperMethod(method);



  10. return


    mapperMethod.execute(sqlSession, args);


  11. }




这个方法中,首先会过滤object中的通用方法,遇到object方法会直接执行

但是如果是非通用方法,就会调用mappermethod.execute来代理执行方法,

mappermethod.execute



  1. public


    Object execute(SqlSession sqlSession, Object[] args) {


  2. Object result;


  3. if


    (SqlCommandType.INSERT == command.getType()) {


  4. Object param = method.convertArgsToSqlCommandParam(args);

  5. result = rowCountResult(sqlSession.insert(command.getName(), param));

  6. }

    else




    if


    (SqlCommandType.UPDATE == command.getType()) {


  7. Object param = method.convertArgsToSqlCommandParam(args);

  8. result = rowCountResult(sqlSession.update(command.getName(), param));

  9. }

    else




    if


    (SqlCommandType.DELETE == command.getType()) {


  10. Object param = method.convertArgsToSqlCommandParam(args);

  11. result = rowCountResult(sqlSession.delete(command.getName(), param));

  12. }

    else




    if


    (SqlCommandType.SELECT == command.getType()) {



  13. if


    (method.returnsVoid() && method.hasResultHandler()) {


  14. executeWithResultHandler(sqlSession, args);

  15. result =

    null


    ;


  16. }

    else




    if


    (method.returnsMany()) {


  17. result = executeForMany(sqlSession, args);

  18. }

    else




    if


    (method.returnsMap()) {


  19. result = executeForMap(sqlSession, args);

  20. }

    else


    {


  21. Object param = method.convertArgsToSqlCommandParam(args);

  22. result = sqlSession.selectOne(command.getName(), param);

  23. }

  24. }

    else


    {



  25. throw




    new


    BindingException(


    “Unknown execution method for: ”


    + command.getName());


  26. }


  27. if


    (result ==


    null


    && method.getReturnType().isPrimitive() && !method.returnsVoid()) {



  28. throw




    new


    BindingException(


    “Mapper method ‘”


    + command.getName()


  29. +

    ” attempted to return null from a method with a primitive return type (”


    + method.getReturnType() +


    “).”


    );


  30. }


  31. return


    result;


  32. }




这个execute方法会根据不同的注解@select,@update,@delete,@insert来分配不同的执行sql环境,进行操作数据库,其实这四个操作可以分为两类,一类是更新类型,返回更新的行数;一类是查询类型,返回查询的结果,这两部分的内部原理我会在其他的文章中进行详细解释。

所以,可以总结说,mybatis执行jdk代理的dao接口方法,跳转到mappermethod,execute方法来执行具体的数据库操作,并且返回结果;而且通过jdk代理的方法返回的代理对象,让人感觉和原接口对象一样,造成使用没有实现类的接口来执行的感觉

第二部分:

spring+mybatis注解方式是怎样在没有实现类的dao接口的情况下结合的

咱们先看一下spring是怎么管理mybatis的dao接口的吧。

我画了一个流程图


配置文件扫描所有mybatis的dao接口:



  1. <


    bean




    class


    =


    “org.mybatis.spring.mapper.MapperScannerConfigurer”


    >





  2. <


    property




    name


    =


    “basePackage”




    value


    =


    “com.mybatis.demo.*.mapper”




    />





  3. <!– 这里要用传beanName,不能传bean的ref,否则,会提前加载,用不到PropertyPlaceholder,切记 –>





  4. <


    property




    name


    =


    “sqlSessionFactoryBeanName”




    value


    =


    “demo_sqlSessionFactory”




    />





  5. </


    bean


    >






ClasspathMapperScanner.doScan



  1. /**



  2. * Calls the parent search that will search and register all the candidates.



  3. * Then the registered objects are post processed to set them as



  4. * MapperFactoryBeans



  5. */





  6. @Override





  7. public


    Set<BeanDefinitionHolder> doScan(String… basePackages) {


  8. Set<BeanDefinitionHolder> beanDefinitions =

    super


    .doScan(basePackages);




  9. if


    (beanDefinitions.isEmpty()) {


  10. logger.warn(

    “No MyBatis mapper was found in ‘”


    + Arrays.toString(basePackages) +


    “‘ package. Please check your configuration.”


    );


  11. }

    else


    {



  12. for


    (BeanDefinitionHolder holder : beanDefinitions) {


  13. GenericBeanDefinition definition = (GenericBeanDefinition) holder.getBeanDefinition();



  14. if


    (logger.isDebugEnabled()) {


  15. logger.debug(

    “Creating MapperFactoryBean with name ‘”


    + holder.getBeanName()


  16. +

    “‘ and ‘”


    + definition.getBeanClassName() +


    “‘ mapperInterface”


    );


  17. }



  18. // the mapper interface is the original class of the bean





  19. // but, the actual class of the bean is MapperFactoryBean




  20. <span style=

    “color:#ff6666;”


    >definition.getPropertyValues().add(


    “mapperInterface”


    , definition.getBeanClassName());


  21. definition.setBeanClass(MapperFactoryBean.

    class


    );</span>



  22. definition.getPropertyValues().add(

    “addToConfig”


    ,


    this


    .addToConfig);




  23. boolean


    explicitFactoryUsed =


    false


    ;



  24. if


    (StringUtils.hasText(


    this


    .sqlSessionFactoryBeanName)) {


  25. definition.getPropertyValues().add(

    “sqlSessionFactory”


    ,


    new


    RuntimeBeanReference(


    this


    .sqlSessionFactoryBeanName));


  26. explicitFactoryUsed =

    true


    ;


  27. }

    else




    if


    (


    this


    .sqlSessionFactory !=


    null


    ) {


  28. definition.getPropertyValues().add(

    “sqlSessionFactory”


    ,


    this


    .sqlSessionFactory);


  29. explicitFactoryUsed =

    true


    ;


  30. }



  31. if


    (StringUtils.hasText(


    this


    .sqlSessionTemplateBeanName)) {



  32. if


    (explicitFactoryUsed) {


  33. logger.warn(

    “Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.”


    );


  34. }

  35. definition.getPropertyValues().add(

    “sqlSessionTemplate”


    ,


    new


    RuntimeBeanReference(


    this


    .sqlSessionTemplateBeanName));


  36. explicitFactoryUsed =

    true


    ;


  37. }

    else




    if


    (


    this


    .sqlSessionTemplate !=


    null


    ) {



  38. if


    (explicitFactoryUsed) {


  39. logger.warn(

    “Cannot use both: sqlSessionTemplate and sqlSessionFactory together. sqlSessionFactory is ignored.”


    );


  40. }

  41. definition.getPropertyValues().add(

    “sqlSessionTemplate”


    ,


    this


    .sqlSessionTemplate);


  42. explicitFactoryUsed =

    true


    ;


  43. }



  44. if


    (!explicitFactoryUsed) {



  45. if


    (logger.isDebugEnabled()) {


  46. logger.debug(

    “Enabling autowire by type for MapperFactoryBean with name ‘”


    + holder.getBeanName() +


    “‘.”


    );


  47. }

  48. definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

  49. }

  50. }

  51. }



  52. return


    beanDefinitions;


  53. }




大家注意看红色部分,红色部分的意思就是对于mybatis的dao接口,spring是以MapperFactoryBean的方式来管理的,举个例子说

@autowired

private UserMapper userMapper;

这个userMapper返回的实例对象会是MapperFactoryBean,这个过程是由spring控制的,因为笔者对于spring原理没有深入研究过,笔者在这里不做说明。

可能大家好奇,为什么这里不能直接像第一部分一样,通过sqlsession.getMapper(…)的方式来获取dao接口对象呢,笔者在这里觉得,之所以出现MapperFactoryBean

这个中间对象,是因为SqlSessionTemplate,sqlsessionTemplate是mybatis-spring封装的用于方法执行mybatis方法的工具类,但是大家平时可能很少用到这个,

笔者在这里做了一个小测试利用,简单的看一下它的用法:



  1. /**



  2. *



  3. * 类SqlTemplateTest.java的实现描述:TODO 类实现描述



  4. * @author yuezhihua 2015年7月29日 下午2:07:44



  5. */





  6. @RunWith


    (SpringJUnit4ClassRunner.


    class


    )



  7. @ContextConfiguration


    (locations = {



  8. “classpath*:spring/demo-locator.xml”




  9. })


  10. public




    class


    SqlTemplateTest {




  11. @Autowired





  12. private


    SqlSessionTemplate sqlSessionTemplate;




  13. @Autowired





  14. private


    SqlSessionFactory sqlSessionFactory;




  15. /**



  16. * 初始化datasource



  17. */





  18. @Before





  19. public




    void


    init(){


  20. DataSource ds =

    null


    ;



  21. try


    {


  22. ds = BaseDataTest.createUnpooledDataSource(BaseDataTest.DERBY_PROPERTIES);

  23. BaseDataTest.runScript(ds,

    “com/mybatis/demo/databases/lazyloader/lazyloader-schema.sql”


    );


  24. BaseDataTest.runScript(ds,

    “com/mybatis/demo/databases/lazyloader/lazyloader-dataload.sql”


    );


  25. }

    catch


    (IOException e) {


  26. e.printStackTrace();

  27. }

    catch


    (SQLException e) {


  28. e.printStackTrace();

  29. }

  30. }




  31. /**



  32. * 测试mybatis自身的查询



  33. */





  34. @Test





  35. public




    void


    testMybatisSelect(){


  36. SqlSession session = sqlSessionFactory.openSession();

  37. UserMapper mapper = session.getMapper(UserMapper.

    class


    );


  38. System.out.println(

    “jdk proxy mapper : ”


    +mapper);


  39. UserPO userPo = mapper.getUserByUsername(

    “zhangsan”


    );


  40. System.out.println(

    “—–testMybatisSelect:”


    +userPo.getUsername());


  41. }





  42. /**



  43. * mybatis-spring : sqlSessionTemplate测试查询



  44. * java.lang.UnsupportedOperationException: Manual close is not allowed over a Spring managed SqlSession不要在意



  45. */





  46. @Test





  47. public




    void


    testSelect(){


  48. UserPO  result = sqlSessionTemplate.selectOne(

    “com.mybatis.demo.lazyload.mapper.UserMapper.getUserByUsername”


    ,


    “zhangsan”


    );


  49. System.out.println(result);

  50. }



  51. }




大家看上面testSelect这个测试用例,可以看到sqlsessiontemplate的基本使用方法

spring+mybatis注解方式获取dao接口对象的方法;:

MapperFactoryBean.getObject



  1. /**



  2. * {@inheritDoc}



  3. */





  4. public


    T getObject()


    throws


    Exception {



  5. return


    getSqlSession().getMapper(


    this


    .mapperInterface);


  6. }




这里边的getSqlSession其实就是sqlsessiontemplate

总结:对于第一部分可以说是返回了mybatis的dao接口的jdk代理对象,通过mapperproxy这个类似于拦截器一样的类跳转执行sql的,可以说是原生dao接口的一层代理对象;

那么对于第二部分来说,确实有三层代理对象:


所以,咱们在spring中使用

@autowired

private UserMapper userMapper;


来注入对象的时候,其实是经历了 cglib –> mapperfactorybean  –> sqlsessiontemplate  –> mapperproxy  –>  原生dao接口  的包装过程,才获取的

所以咱们在使用spring来调用没有实现类的mybatis的dao接口的时候,并不是像看起来那么简单,而是经过多层代理包装的一个代理对象,对方法的执行也跳转到mybatis框架中的mappermethod中了