If not combined with the Spring framework, a typical way to use MyBatis is as follows:
public class UserDaoTest { private SqlSessionFactory sqlSessionFactory; @Before public void setUp() throws Exception{ ClassPathResource resource = new ClassPathResource("mybatis-config.xml"); InputStream inputStream = resource.getInputStream(); sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); } @Test public void selectUserTest(){ String id = "{0003CCCA-AEA9-4A1E-A3CC-06D884BA3906}"; SqlSession sqlSession = sqlSessionFactory.openSession(); CbondissuerMapper cbondissuerMapper = sqlSession.getMapper(CbondissuerMapper.class); Cbondissuer cbondissuer = cbondissuerMapper.selectByPrimaryKey(id); System.out.println(cbondissuer); sqlSession.close(); } }
We need SqlSessionFactory first, and then obtain SqlSession through the openSession method of SqlSessionFactory. Get the dynamic proxy class (MapperProxy) of the interface we defined through SqlSession. When we integrate the Spring framework, we use MyBatis in a simple "unimaginable" way:
@Autowore private CbondissuerMapper cbondissuerMapper;
It's very Spring like. Just inject it directly. How is this realized? How does Spring omit the slightly complicated template code above? My first instinct is that Spring did the initialization of Mybatis when it started, and then got all the dynamic proxy implementation classes of Mapper interface at one time and put them into the Spring container for management.
Now let's test your conjecture.
Analysis on the principle of MyBatis integrating Spring
The following is a simple analysis based on mybatis autoconfiguration in Spring Boot:
//The final function of SqlSessionFactoryBean is to parse MyBati configuration file, generate configuration object, generate DefaultSqlSessionFactory, and join Spring's container management. It can be seen that SqlSessionFactoryBean has the same function as sqlsessionfactorbeanbuilder. @Bean @ConditionalOnMissingBean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factory = new SqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setVfs(SpringBootVFS.class); if (StringUtils.hasText(this.properties.getConfigLocation())) { factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation())); } Configuration configuration = this.properties.getConfiguration(); if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) { configuration = new Configuration(); } if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) { for (ConfigurationCustomizer customizer : this.configurationCustomizers) { customizer.customize(configuration); } } factory.setConfiguration(configuration); if (this.properties.getConfigurationProperties() != null) { factory.setConfigurationProperties(this.properties.getConfigurationProperties()); } if (!ObjectUtils.isEmpty(this.interceptors)) { factory.setPlugins(this.interceptors); } if (this.databaseIdProvider != null) { factory.setDatabaseIdProvider(this.databaseIdProvider); } if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) { factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage()); } if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) { factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage()); } if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) { factory.setMapperLocations(this.properties.resolveMapperLocations()); } return factory.getObject(); } //Create the Bean SqlSessionTemplate, which dynamically proxies DefaultSqlSession, so the DefaultSqlSession is called finally. Next, we will focus on the analysis of SqlSessionTemplate @Bean @ConditionalOnMissingBean public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) { ExecutorType executorType = this.properties.getExecutorType(); if (executorType != null) { return new SqlSessionTemplate(sqlSessionFactory, executorType); } else { return new SqlSessionTemplate(sqlSessionFactory); } }
The following is the source code of SqlSessionTemplate. Due to the large number of source code, only the key parts are posted:
public class SqlSessionTemplate implements SqlSession, DisposableBean { private final SqlSessionFactory sqlSessionFactory; private final ExecutorType executorType; //SqlSessionTemplate dynamically proxies DefaultSqlSession. All calls to sqlSessionProxy will pass through the proxy object private final SqlSession sqlSessionProxy; private final PersistenceExceptionTranslator exceptionTranslator; .... public SqlSessionTemplate(SqlSessionFactory sqlSessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { notNull(sqlSessionFactory, "Property 'sqlSessionFactory' is required"); notNull(executorType, "Property 'executorType' is required"); this.sqlSessionFactory = sqlSessionFactory; this.executorType = executorType; this.exceptionTranslator = exceptionTranslator; this.sqlSessionProxy = (SqlSession) newProxyInstance( SqlSessionFactory.class.getClassLoader(), new Class[] { SqlSession.class }, new SqlSessionInterceptor()); } .... //CRUD calls to sqlSessionProxy will be called here first private class SqlSessionInterceptor implements InvocationHandler { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { //This step is equivalent to calling the opensession method of sqlSessionFactory to obtain SqlSession SqlSession sqlSession = getSqlSession( SqlSessionTemplate.this.sqlSessionFactory, SqlSessionTemplate.this.executorType, SqlSessionTemplate.this.exceptionTranslator); try { //Call CRUD method of DefaultSqlSession Object result = method.invoke(sqlSession, args); if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) { sqlSession.commit(true); } return result; } catch (Throwable t) { Throwable unwrapped = unwrapThrowable(t); if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) { // release the connection to avoid a deadlock if the translator is no loaded. See issue #22 closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); sqlSession = null; Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped); if (translated != null) { unwrapped = translated; } } throw unwrapped; } finally { if (sqlSession != null) { //Finally, force SqlSession to close closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory); } } } } }
So far, the whole process of getting SqlSessionFactory, getting SqlSession, executing CRUD, and closing SqlSession in Spring has been analyzed. There is also a question about when the Mapper interface implementation classes are generated dynamically.
MapperScan's Secret
We know MapperScan is used to scan Mapper interface, so we naturally want to explore MapperScan.
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented //The secret is in MapperScannerRegistrar @Import(MapperScannerRegistrar.class) public @interface MapperScan { String[] value() default {}; //Set up scanned packages String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class; Class<? extends Annotation> annotationClass() default Annotation.class; Class<?> markerInterface() default Class.class; String sqlSessionTemplateRef() default ""; String sqlSessionFactoryRef() default ""; //Specify a custom MapperFactoryBean Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class; }
Here's what MapperScannerRegistrar does:
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName())); //Create a ClassPathMapperScanner and set the properties according to the configuration in @ MapperScan ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry); // this check is needed in Spring 3.1 if (resourceLoader != null) { scanner.setResourceLoader(resourceLoader); } Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass"); if (!Annotation.class.equals(annotationClass)) { scanner.setAnnotationClass(annotationClass); } Class<?> markerInterface = annoAttrs.getClass("markerInterface"); if (!Class.class.equals(markerInterface)) { scanner.setMarkerInterface(markerInterface); } Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator"); if (!BeanNameGenerator.class.equals(generatorClass)) { scanner.setBeanNameGenerator(BeanUtils.instantiateClass(generatorClass)); } Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean"); if (!MapperFactoryBean.class.equals(mapperFactoryBeanClass)) { scanner.setMapperFactoryBean(BeanUtils.instantiateClass(mapperFactoryBeanClass)); } scanner.setSqlSessionTemplateBeanName(annoAttrs.getString("sqlSessionTemplateRef")); scanner.setSqlSessionFactoryBeanName(annoAttrs.getString("sqlSessionFactoryRef")); List<String> basePackages = new ArrayList<String>(); for (String pkg : annoAttrs.getStringArray("value")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } for (String pkg : annoAttrs.getStringArray("basePackages")) { if (StringUtils.hasText(pkg)) { basePackages.add(pkg); } } for (Class<?> clazz : annoAttrs.getClassArray("basePackageClasses")) { basePackages.add(ClassUtils.getPackageName(clazz)); } scanner.registerFilters(); //Start scanning scanner.doScan(StringUtils.toStringArray(basePackages)); }
The above creates a ClassPathMapperScanner, and sets properties according to the configuration in @ MapperScan to start scanning. Let's look at ClassPathMapperScanner.
private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; //Further processing of the scanned BeanDefinition for (BeanDefinitionHolder holder : beanDefinitions) { definition = (GenericBeanDefinition) holder.getBeanDefinition(); if (logger.isDebugEnabled()) { logger.debug("Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + definition.getBeanClassName() + "' mapperInterface"); } definition.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName()); // issue #59 //Here is the key point. Set the bean class of the Mapper interface scanned to MapperFactoryBrean //Mapperfactorybean is a factory Bean used to generate MapperProxy; //When Spring automatically injects Mapper, it will automatically call the getObject method of the factory Bean, generate MapperProxy and put it into the Spring container. definition.setBeanClass(this.mapperFactoryBean.getClass()); 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); } } }
The process of Spring automatically generating Mapper interface implementation class is also analyzed.
Brief summary
Through the above analysis, it confirms the conjecture at the beginning: Spring did the initialization of Mybatis when it started, and then obtained all the dynamic proxy implementation classes of Mapper interface at one time and put them into the Spring container for management. The general process is as follows:
- Configure SqlSessionFactoryBean. The final function of this Bean is to create DefaultSqlSessionFactory and add DefaultSqlSessionFactory to Spring container management;
- Create SqlSessionTemplate, which represents DefaultSqlSession in MyBatis. Calling any CRUD method through SqlSessionTemplate will experience openSession, calling CRUD method of DefaultSqlSession, and closing Session;
The process of Mapper interface generation is as follows:
- @MapperScan annotation scans all Mapper interfaces to generate corresponding BeanDefinition;
- ClassPathMapperScanner's processing method further configures these beandefinitions. The final step is to set the Beanclass property of these beandefinitions to MapperFactoryBean (this is a factory Bean, which is specially used to generate Mapper's dynamic proxy implementation MapperProxy, and a Dao interface will correspond to a MapperFactoryBean);
- When Spring detects that Mapper needs to be injected automatically, it will call the getObject method of MapperFactoryBean to generate the corresponding MapperProxy and bring the object into Spring management.
The above is about the integration of mybatis into Spring. We found that its essence is the same as the traditional use of mybatis, except for some customized configuration through Spring.
The analysis is a bit messy. Let's do it first~