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接口进行数据库操作

/**
 * 
 * 类UserMapper.java的实现描述:TODO 类实现描述 
 */
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 类实现描述  
     */  
    @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
[java] view plain copy

    /** 
       * 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  
            definition.getPropertyValues().add("mapperInterface", definition.getBeanClassName());  
            definition.setBeanClass(MapperFactoryBean.class); 
      
            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;  
      }  
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中了

猜你喜欢

转载自blog.csdn.net/qq_30186661/article/details/80483867