相信你们在刚开始学习mybatis注解方式,或者spring+mybatis注解方式的时候,必定会有一个疑问,为何mybatis的dao接口只须要一个接口,不须要实现类,就能够正常使用,笔者最开始的时候也会有这种疑问,当时在网上查了不少资料,也问过公司比较年长的同事,可是并无获得答案,后来经过本身看mybatis的源码的方式才明白其中道理,接下来我就对你们分享,为何dao接口不须要实现类的原理,这篇文章的讲解主要分为两部分:java
1.mybatis注解方式是怎样经过没有实现类的dao接口进行数据库操做mysql
2.spring+mybatis注解方式是怎样在没有实现类的dao接口的状况下结合的git
文章的结构是经过 总结+详细讲解 的方式来进行说明的,但愿你们能和我一同进步,例子程序放在github上了,mybatis-demo。github
环境:spring
mybatis 3.2.7sql
mybatis-spring 1.2.2数据库
spring 4.1.6apache
总结:编程
1.mybatis注解方式经过没有实现类的dao接口进行数据库操做的原理,一句话归纳,就是jdk proxy,就是jdk代理session
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中了