使用過 mybatis 的人應該都知道,mybatis 有個特性就是對於 Mapper 類,只須要聲明接口就能夠了,而不須要寫具體的實現類,上層在使用 Mapper 接口時只須要直接注入 Mapper 接口就能夠正常工做,下面咱們就來具體剖析下 mybatis 是怎樣經過這個 Mapper 接口來自動生成 Mapper 實現類,而且註冊到 spring 容器中。java
applicationContext.xmlspring
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:config/mybatis-config.xml" /> <property name="dataSource" ref="dataSource" /> <property name="mapperLocations" value="classpath:mapping/*Mapper.xml" /> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!-- 掃描全部 RepositoryMapper 註解的類 --> <property name="annotationClass" value="com.jaf.framework.core.mapper.RepositoryMapper" /> <!-- 掃描全部 BaseMapper 接口實現類 --> <property name="markerInterface" value="com.jaf.framework.core.mapper.BaseMapper" /> <property name="basePackage" value="com.hbvtc.exam.mapper.*" /> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" /> </bean>
TeacherMapper.java & BaseMapper.javasql
@RepositoryMapper public interface TeacherMapper extends BaseMapper<Teacher> { } public interface BaseMapper<E extends BaseEntity<?>> { void insertEntity(E var1); void updateById(E var1); <T> void deleteById(T var1); <T> void deleteByIds(T[] var1); Page<E> pageQuery(Map<String, Object> var1); } @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepositoryMapper { }
這個 mapper 只有接口,沒有具體的實現類,而且這個 Mapper 接口是被 @RepositoryMapper
註解標識的,因此能夠被 mybatis 掃描到。apache
注意,這裏的 @RepositoryMapper
註解只是個普通的註解,並無被 spring @Component
註解標識,那麼 mybatis 是怎樣掃描到這些 mapper 接口,而且生成對應的實現類對象,而後註冊到 spring 容器中的呢?緩存
TeacherDaoImpl.javamybatis
@Repository("teacherDao") public class TeacherDaoImpl extends BaseDaoImpl<Teacher> implements TeacherDao { // 像使用普通的 spring bean 同樣注入 mapper 類 @Autowired private TeacherMapper teacherMapper; @Override protected BaseMapper<Teacher> getMapper() { return teacherMapper; } }
上層 dao 實現類中,對 mapper 的注入像普通的 bean 同樣。app
MapperScannerConfigurer
從上面的配置文件中能夠看到,mybatis 配置的類爲 org.mybatis.spring.mapper.MapperScannerConfigurer
,也正是這個類實現了 mybatis mapper 接口的掃描,而且註冊具體的實例到 spring 容器中。ide
先看下這個類的類繼承關係圖post
能夠看到這個類是實現了 BeanDefinitionRegistryPostProcessor
接口的,以前已經介紹過 BeanDefinitionRegistryPostProcessor
這個接口能夠在 spring 容器啓動過程當中對 Beandefinition 註冊作一些擴展。this
下面咱們直接來看 BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry
方法
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) { if (this.processPropertyPlaceHolders) { processPropertyPlaceHolders(); } // ClassPathBeanDefinitionScanner 從 ClassPathBeanDefinitionScanner 繼承,這個類主要負責 mybatis mapper 文件的掃描 // 掃描的基本規則就是根據以前配置文件中配置的 // basePackage: 在哪一個包下掃描 // markerInterface: 掃描哪一個接口,markerInterface 接口的全部實現類會被掃描到 // annotationClass: 或者是掃描被 annotationClass 註解的全部類 ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); scanner.setAddToConfig(this.addToConfig); scanner.setAnnotationClass(this.annotationClass); scanner.setMarkerInterface(this.markerInterface); scanner.setSqlSessionFactory(this.sqlSessionFactory); scanner.setSqlSessionTemplate(this.sqlSessionTemplate); scanner.setSqlSessionFactoryBeanName(this.sqlSessionFactoryBeanName); scanner.setSqlSessionTemplateBeanName(this.sqlSessionTemplateBeanName); scanner.setResourceLoader(this.applicationContext); scanner.setBeanNameGenerator(this.nameGenerator); scanner.registerFilters(); scanner.scan(StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS)); }
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#scan
public int scan(String... basePackages) { int beanCountAtScanStart = this.registry.getBeanDefinitionCount(); doScan(basePackages); // Register annotation config processors, if necessary. if (this.includeAnnotationConfig) { AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry); } return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart); }
#scan
方法是在父類(ClassPathBeanDefinitionScanner
)中定義的,其中最主要的是 #doScan
方法。
org.mybatis.spring.mapper.ClassPathMapperScanner#doScan
public Set<BeanDefinitionHolder> doScan(String... basePackages) { // 主要仍是經過 super.doScan 方法來掃描,返回全部符合條件的 BeanDefinitionHolder 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 { // mybatis 對返回的 Beandefinition 進行了進一步的處理 processBeanDefinitions(beanDefinitions); } return beanDefinitions; }
mybatis 的 ClassPathMapperScanner
類對父類中的 #doScan
方法作了重寫,但對於 Beandefinition 的掃描和註冊到 spring 容器中,主要仍是經過 super.doScan
方法來實現。
org.springframework.context.annotation.ClassPathBeanDefinitionScanner#doScan
方法
protected Set<BeanDefinitionHolder> doScan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>(); for (String basePackage : basePackages) { // 這個方法的具體邏輯就不展開了,感興趣的能夠本身跟進下源碼 // 主要過濾條件是經過 org.mybatis.spring.mapper.ClassPathMapperScanner#registerFilters 方法來完成註冊的 // 過濾條件包含配置文件中配置的 annotationClass 和 markerInterface // 另外這裏須要注意的一點是,這裏返回的 BeanDefinition 具體的實現類是 ScannedGenericBeanDefinition,而且 BeanDefinition 屬性 beanClass 對應的是接口類(正常狀況下應該是一個具體的實現類,由於接口是沒法實例化的) Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); candidate.setScope(scopeMetadata.getScopeName()); String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } if (candidate instanceof AnnotatedBeanDefinition) { // 處理其餘的一些註解(@Lazy, @Primary, @DependsOn...) AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } if (checkCandidate(beanName, candidate)) { BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); beanDefinitions.add(definitionHolder); // 向 spring 容器註冊 BeanDefinition registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; }
這個方法負責掃描 MapperScannerConfigurer
配置的 basePackage 下的全部符合 annotationClass 註解的接口/類,或者實現了 markerInterface 接口的接口/類,而且生成對應的 BeanDefinition,而後註冊到 spring 容器中。
到目前爲止咱們應該解決了 mybatis 是如何掃描到全部的 mapper 接口,而且註冊到 spring 容器中的問題,可是須要注意的是這裏返回的 BeanDefinition 裏面的 beanClass 屬性對應的可能接口類,一個接口是沒辦法被實例化的,也就是目前爲止掃描到的這些 BeanDefinition 在後續 spring 容器啓動過程當中沒辦法生成對應的 bean 實例。
咱們繼續回到上面的 org.mybatis.spring.mapper.ClassPathMapperScanner#doScan
這個方法,這個方法裏面還有調用了一個很重要的方法就是 org.mybatis.spring.mapper.ClassPathMapperScanner#processBeanDefinitions
,具體源代碼以下(刪除了部分日誌打印相關代碼):
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); // 這兩句代碼時關鍵,這裏將 beanClass 進行了從新設置,設置爲了 MapperFactoryBean,而且將原來接口 class 設置爲構造方法參數 definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59 definition.setBeanClass(this.mapperFactoryBean.getClass()); definition.getPropertyValues().add("addToConfig", this.addToConfig); // 設置 sqlSessionFactory,若是有配置 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; } // 設置 sqlSessionTemplate,若是有配置 if (StringUtils.hasText(this.sqlSessionTemplateBeanName)) { definition.getPropertyValues().add("sqlSessionTemplate", new RuntimeBeanReference(this.sqlSessionTemplateBeanName)); explicitFactoryUsed = true; } else if (this.sqlSessionTemplate != null) { definition.getPropertyValues().add("sqlSessionTemplate", this.sqlSessionTemplate); explicitFactoryUsed = true; } // 若是 sqlSessionFactory & sqlSessionTemplate 都沒有指定,那麼啓用類型自動注入 if (!explicitFactoryUsed) { definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } }
通過這一步的處理,如今 mybatis 掃描到的全部符合條件的 BeanDefinition 都是能夠在後續 spring 容器啓動過程當中,轉換成具體 bean 的了,也就是如今的 BeanDefinition 已是能夠正常使用的了。 下面咱們再深刻一步地看下這個 MapperFactoryBean
是如何生成具體的實現類的,由於咱們的代碼中並無對 Mapper 接口生成具體的實現類,查看 org.apache.ibatis.binding.MapperProxyFactory#newInstance
源碼以下:
public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); }
跟着調用鏈一直追蹤,最後會調用到 org.apache.ibatis.binding.MapperRegistry#getMapper
public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } protected T newInstance(MapperProxy<T> mapperProxy) { // mybatis 經過 jdk 的動態代理方式來爲全部的 mapper 接口生成一個代理實現類 // 使用 MapperProxy 做爲動態代理的 InvocationHandler return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); }
到這裏就已經很是明確了,mybatis 經過 jdk 的動態代理方式來爲全部的 mapper 接口生成一個代理實現類。
順便咱們來看下 org.apache.ibatis.binding.MapperProxy#invoke
方法
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 若是 mapper 是一個具體的實現類,那麼直接調用實現類的方法 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, args); } else if (isDefaultMethod(method)) { // default method 是指那些例如:toString, equals 方法等 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 當 mapper 接口中的一個方法被調用的時候,會進入到這裏,mybatis 會對這個方法生成一個 MapperMethod 對象,而且緩存這個對象 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); }
org.apache.ibatis.binding.MapperMethod#execute
// mapper 接口中的 select / update 等方法最終被執行的地方 public Object execute(SqlSession sqlSession, Object[] args) { Object result; switch (command.getType()) { case INSERT: { // 調用了 sqlSession 的 insert / update / delete ... Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.insert(command.getName(), param)); break; } case UPDATE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.update(command.getName(), param)); break; } case DELETE: { Object param = method.convertArgsToSqlCommandParam(args); result = rowCountResult(sqlSession.delete(command.getName(), param)); break; } case SELECT: 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 if (method.returnsCursor()) { result = executeForCursor(sqlSession, args); } else { Object param = method.convertArgsToSqlCommandParam(args); result = sqlSession.selectOne(command.getName(), param); } break; case FLUSH: result = sqlSession.flushStatements(); break; default: 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; }
至此 mybatis 對於 mapper 接口的掃描、如何註冊到 spring 容器中、如何對 mapper 接口生成動態代理實現類,以及動態代理類方法執行已經所有解析完了。
本文全部源代碼基於 mybatis:3.4.5 & mybatis-spring:1.3.1