相信你們在剛開始學習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中了