In-depth analysis of spring source code spring integration mybatis principle analysis and spring extension point analysis

In-depth analysis of spring source code spring integration mybatis principle analysis and spring extension point analysis

Preface

I recently watched a video about how to interview. It said that you should not just write that you have read the source code of XX, but reflect it in the project. Even if you don t have it in your own project, you can say that you have seen it in other people s projects. Some features of a certain framework, so I am ready to try it myself to learn the essence of an excellent framework, I manually integrated spring and mybatis, and the view to experience the excellence of mybatis.

begin

To start integrating spring and mybatis, naturally, we must first build a maven project. When I integrate spring and mybatis, I also integrated log4j to facilitate log viewing, and integrated Ali s druid as the mysql database connection pool, because I just want to integrate Spring and mybatis and found the principle of mybatis based on spring extension points and integration, there is no need to use web projects, so I only introduced the spring-context package. The project pom file is as follows

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.ww</groupId> <artifactId>mybatis-spring</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.6</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-context --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.3.25.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.47</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.3.25.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis-spring --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>compile</scope> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> </dependencies> </project> Copy code

Since I don t like to use xml, I used pure annotations for the construction this time. In the resource directory of the project, I created the application.properties file

spring.datasource.username=root spring.datasource.password=123456 spring.datasource.url=jdbc:mysql://localhost:3306/testmybatis spring.datasource.driver=com.mysql.jdbc.Driver Copy code

Write configuration classes to obtain database information configured in application.properties

@Configuration @PropertySource("classpath:application.properties") public class PropertiesConfig { @Value("${spring.datasource.url}") public String url; @Value("${spring.datasource.username}") public String username; @Value("${spring.datasource.password}") public String password; @Value("${spring.datasource.driver}") public String driver; public String getUrl() { return url; } public String getUsername() { return username; } public String getPassword() { return password; } public String getDriver() { return driver; } } Copy code

The mapper file is as follows

public interface UserMapper { @Select("select id,name,height,weight from user where id=#{id}") public User selectUser(Integer id); } Copy code

Entity class

@Data public class User { private int id; private String name; private String height; private String weight; } Copy code

Correspondingly, I built a database in mysql, the table name is user

service class

@Service public class UserService{ @Autowired UserMapper mapper; public User getUser(int id) { //At first log4j did not output the log. After checking the official website, you can print the log by adding this sentence org.apache.ibatis.logging.LogFactory.useLog4JLogging(); return mapper.selectUser(id); } } Copy code

The main configuration class for spring to start

@Configuration @ComponentScan("com.ww") @MapperScan("com.ww.mapper") @PropertySource("classpath:application.properties") public class MybatisConfig { //These are examples from the official website of mybatis-spring, just follow the changes @Bean public DataSource dataSource(PropertiesConfig config) { DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(config.getDriver()); dataSource.setUrl(config.getUrl()); dataSource.setPassword(config.getPassword()); dataSource.setUsername(config.getUsername()); return dataSource; } //These are examples from the official website of mybatis-spring, just follow the changes @Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); return factoryBean.getObject(); } } Copy code

At this point, the project is completed, and then we run the project to observe the operation process after mybatis integrates spring, and what is the difference between mybatis integrated spring and unintegrated spring

First of all, we see a line of @MapperScan annotation on the main configuration class, which means that the mapper is scanned into the spring container and the mapper is handed over to spring for management. Then I want to know when the mapper was scanned and injected by spring, we clicked into this Annotation, I saw that this annotation is a combined annotation, and there is such a line of annotation that caught my attention

@Import(MapperScannerRegistrar.class) Copy the code

What is the meaning of this annotation? Import annotation means to import resources, then let's see what kind of resource it imports, and look at the name of the class. I guess this may be a registrar for a mapper scanner, so I clicked it in. Look

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware { private ResourceLoader resourceLoader; /** * {@inheritDoc} */ @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { AnnotationAttributes annoAttrs = AnnotationAttributes.fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName())); 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(); //Call the doScan method in ClassPathMapperScanner to scan the mapper and assemble it into a beanDefinition scanner.doScan(StringUtils.toStringArray(basePackages)); } /** * {@inheritDoc} */ @Override public void setResourceLoader(ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } } Copy code

You can see that this class implements the ImportBeanDefinitionRegistrar interface, which leads to the first extension point of spring. The ImportBeanDefinitionRegistrar interface can be used to dynamically register beans, and it can support our own code to be packaged into a BeanDefinition object. Here, as a third-party framework, mybatis has no way to use @Component or @Service to indicate that this is a bean that needs to be injected like a first-party component, so it can only extend this interface to dynamically inject beans.

Let s look at the registerBeanDefinitions method again. The meaning of this method is to register bd (for convenience, the following beanDefinitions are referred to as bd). We see that the method finally calls the doScan() method of ClassPathMapperScanner. Let s take a look at the ClassPathMapperScanner class.

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner { ... } Copy code

This class belongs to the spring-mybatis package and inherits the Spring ClassPathBeanDefinitionScanner class

@Override public Set<BeanDefinitionHolder> doScan(String... basePackages) { //Call spring's doScan method to scan bd 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 { //Execute bd processing after scanning processBeanDefinitions(beanDefinitions); } return beanDefinitions; } Copy code

Because spring executes componentScan first, when I put a breakpoint in the doScan method of ClassPathBeanDefinitionScanner, I will also enter this breakpoint when I scan my own beans first. This part is skipped because componentScan is not analyzed this time. Skip this breakpoint, and output on the console at this time

Registering bean definition for @Bean method com.ww.config.MybatisConfig.dataSource() Registering bean definition for @Bean method com.ww.config.MybatisConfig.sqlSessionFactory() Copy code

It means that my own bean has been registered, and then I should enter the mapper scan. I put a breakpoint on the first line of the registerBeanDefinitions() method in the MapperScannerRegistrar class, and the breakpoint jumped in and executed it all the way. After finishing the doScan method of ClassPathBeanDefinitionScanner, the console outputs a sentence:

Identified candidate component class: file [E :/mycode/gitclone/mybatis-spring/target/classes/com/ww/mapper/UserMapper.class] duplicated code

The candidate component class is confirmed, that is to say, spring has scanned the mapper, and spring has registered bd, and then execute it, it will enter the processBeanDefinitions() method

//Process the registered bd private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) { GenericBeanDefinition definition; for (BeanDefinitionHolder holder: beanDefinitions) { 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.getConstructorArgumentValues().addGenericArgumentValue(definition.getBeanClassName());//issue #59 //Add the mapperFactoryBean class to bd, it can be seen that the beanClass corresponding to a mapper is mapperFactoryBean, which is one of the core classes of mybatis, which will be explained in detail 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() + "'."); } //The automatic injection type is byType definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); } } } Copy code

After this method is processed, the entire bd is established, and then mybatis starts the initialization process. First of all, let s look at the MapperFactoryBean class that was added to the bd.

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> { private Class<T> mapperInterface; private boolean addToConfig = true; public MapperFactoryBean() { //intentionally empty } public MapperFactoryBean(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } /** * {@inheritDoc} */ @Override protected void checkDaoConfig() { super.checkDaoConfig(); notNull(this.mapperInterface, "Property'mapperInterface' is required"); Configuration configuration = getSqlSession().getConfiguration(); if (this.addToConfig && !configuration.hasMapper(this.mapperInterface)) { try { configuration.addMapper(this.mapperInterface); } catch (Exception e) { logger.error("Error while adding the mapper'" + this.mapperInterface + "'to configuration.", e); throw new IllegalArgumentException(e); } finally { ErrorContext.instance().reset(); } } } /** * {@inheritDoc} */ @Override public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); } /** * {@inheritDoc} */ @Override public Class<T> getObjectType() { return this.mapperInterface; } /** * {@inheritDoc} */ @Override public boolean isSingleton() { return true; } //------------- mutators -------------- /** * Sets the mapper interface of the MyBatis mapper * * @param mapperInterface class of the interface */ public void setMapperInterface(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } /** * Return the mapper interface of the MyBatis mapper * * @return class of the interface */ public Class<T> getMapperInterface() { return mapperInterface; } /** * If addToConfig is false the mapper will not be added to MyBatis. This means * it must have been included in mybatis-config.xml. * <p/> * If it is true, the mapper will be added to MyBatis in the case it is not already * registered. * <p/> * By default addToCofig is true. * * @param addToConfig */ public void setAddToConfig(boolean addToConfig) { this.addToConfig = addToConfig; } /** * Return the flag for addition into MyBatis config. * * @return true if the mapper will be added to MyBatis in the case it is not already * registered. */ public boolean isAddToConfig() { return addToConfig; } } Copy code

MapperFactoryBean extends Spring's FactoryBean interface. FactoryBean is an extension point of Spring. The function of FactoryBean allows us to customize the Bean creation process.

//Returned object instance T getObject() throws Exception; //Bean type Class<?> getObjectType(); //true is a singleton, false is a non-singleton. In Spring5.0, this method takes advantage of the new features of JDK1.8 and becomes the default method, returning true boolean isSingleton(); Copy code

At the same time, MapperFactory also inherits the SqlSessionDaoSupport class, and the SqlSessionDaoSupport class inherits the Spring DaoSupport class. It uses the extension point of InitializingBean in Spring to operate the bean after the property setting. Let's see what the SqlSessionDaoSupport class and DaoSupport class do

public abstract class SqlSessionDaoSupport extends DaoSupport { private SqlSession sqlSession; private boolean externalSqlSession; //Set sqlSessionFactory, because mapperScan finally executed definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE), so this set method will be automatically injected into the spring container public void setSqlSessionFactory(SqlSessionFactory sqlSessionFactory) { if (!this.externalSqlSession) { this.sqlSession = new SqlSessionTemplate(sqlSessionFactory); } } //Set sqlSessionTemplate, one of the core classes of spring-mybatis, instead of the DefaultSqlSession class in ordinary mybatis. This class controls sqlSession and contains an internal class to execute dynamic proxy. At the same time, the sqlSessionTemplate class is thread-safe and can be used as a single in spring Example bean use public void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) { this.sqlSession = sqlSessionTemplate; this.externalSqlSession = true; } /** * Users should use this method to get a SqlSession to call its statement methods * This is SqlSession is managed by spring. Users should not commit/rollback/close it * because it will be automatically done. * * @return Spring managed thread safe SqlSession */ public SqlSession getSqlSession() { return this.sqlSession; } /** * {@inheritDoc} */ @Override protected void checkDaoConfig() { notNull(this.sqlSession, "Property'sqlSessionFactory' or'sqlSessionTemplate' are required"); } } //To implement the InitializingBean interface, the afterPropertiesSet() method will be executed, and the checkDaoConfig() method in the MapperFactoryBean class will be executed after mybatis integrates spring public abstract class DaoSupport implements InitializingBean { /** Logger available to subclasses */ protected final Log logger = LogFactory.getLog(getClass()); @Override public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException { //Let abstract subclasses check their configuration. checkDaoConfig(); //Let concrete implementations initialize themselves. try { initDao(); } catch (Exception ex) { throw new BeanInitializationException("Initialization of DAO failed", ex); } } /** * Abstract subclasses must override this to check their configuration. * <p>Implementors should be marked as {@code final} if concrete subclasses * are not supposed to override this template method themselves. * @throws IllegalArgumentException in case of illegal configuration */ protected abstract void checkDaoConfig() throws IllegalArgumentException; /** * Concrete subclasses can override this for custom initialization behavior. * Gets called after population of this instance's bean properties. * @throws Exception if DAO initialization fails * (will be rethrown as a BeanInitializationException) * @see org.springframework.beans.factory.BeanInitializationException */ protected void initDao() throws Exception { } } Copy code

Hit the breakpoint on the checkDaoConfig() method in the MapperFactoryBean class and continue execution. We see that configuration.addMapper(this.mapperInterface) is executed, click in, and we see that the org.apache.ibatis.session.Configuration has been jumped into. AddMapper method in the class

//mapper register, add and get the actual class of mapper protected final MapperRegistry mapperRegistry = new MapperRegistry(this); public <T> void addMapper(Class<T> type) { mapperRegistry.addMapper(type); } Copy code

So I enter the first floor

//mapper is added to the MapperProxyFactory class private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>(); public <T> void addMapper(Class<T> type) { if (type.isInterface()) { if (hasMapper(type)) { throw new BindingException("Type "+ type +" is already known to the MapperRegistry."); } boolean loadCompleted = false; try { knownMappers.put(type, new MapperProxyFactory<T>(type)); //After adding to the map, immediately parse the annotations in the mapper, the purpose is to get the annotation sql statement in the mapper //It's important that the type is added before the parser is run //otherwise the binding may automatically be attempted by the //mapper parser. If the type is already known, it won't try. MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); parser.parse(); loadCompleted = true; } finally { if (!loadCompleted) { knownMappers.remove(type); } } } } public class MapperProxyFactory<T> { private final Class<T> mapperInterface; private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>(); public MapperProxyFactory(Class<T> mapperInterface) { this.mapperInterface = mapperInterface; } public Class<T> getMapperInterface() { return mapperInterface; } public Map<Method, MapperMethod> getMethodCache() { return methodCache; } //Dynamic proxy callback method @SuppressWarnings("unchecked") protected T newInstance(MapperProxy<T> mapperProxy) { return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] {mapperInterface }, mapperProxy); } //The instantiation method of the mapper proxy class public T newInstance(SqlSession sqlSession) { final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache); return newInstance(mapperProxy); } } Copy code

Continue to execute, and pay attention to the output of the console, and see the console output a Finished creating instance of bean'userMapper'. At this point, the instantiation of the mapper is complete. Now that the instantiation is complete, the instantiated mapper must be returned. , Because the beanClass put in bd is mapperFactoryBean, so the mapper instance should be obtained from the getObject method of mapperFactoryBean, look at the code

//org.mybatis.spring.mapper.MapperFactoryBean#getObject @Override public T getObject() throws Exception { return getSqlSession().getMapper(this.mapperInterface); } //org.mybatis.spring.SqlSessionTemplate#getMapper //sqlSessionTemplate as sqlSession @Override public <T> T getMapper(Class<T> type) { return getConfiguration().getMapper(type, this); } //org.apache.ibatis.session.Configuration#getMapper public <T> T getMapper(Class<T> type, SqlSession sqlSession) { return mapperRegistry.getMapper(type, sqlSession); } //org.apache.ibatis.binding.MapperRegistry#getMapper public <T> T getMapper(Class<T> type, SqlSession sqlSession) { final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type); if (mapperProxyFactory == null) { throw new BindingException("Type "+ type +" is not known to the MapperRegistry."); } try { //As mentioned before, what is returned here is a proxy object, that is, the mapper that has been dynamically proxyed return mapperProxyFactory.newInstance(sqlSession); } catch (Exception e) { throw new BindingException("Error getting mapper instance. Cause: "+ e, e); } } Copy code

We observe the changes of variables and find that the finally returned object is proxied by MapperProxy. So far, the mapper has been managed by spring, and spring will use proxy classes to perform various operations in the mapper through dynamic proxy technology.

summary

After integrating spring and mybatis with pure annotations by ourselves, we have seen many extensions of mybatis based on spring, and also saw many extension points of spring, such as ImportBeanDefinitionRegistrar, InitializingBean, FactoryBean, and summarize the use of mybatis in the process of integrating spring. Several key classes and the basic functions of these classes

  • ClassPathMapperScanner: inherits Spring's ClassPathBeanDefinitionScanner, which is used to scan the mapper in the classPath and process the beanDefinition after the scan. The mapperFactoryBean is added to the beanDefinition in the processBeanDefinitions method of this class
  • MapperFactoryBean: inherits the SqlSessionDaoSupport of mybatis-spring, and implements the FactoryBean interface of Spring. The role is to customize the bean. At the same time, it is responsible for adding the mapper after the initialization of mybatis and obtaining the proxy object of the mapper.
  • MapperRegistry: mapper registrar, add and get the actual class of mapper
  • MapperProxy: The mapper proxy class, call the invoke method to execute the proxy method proxy operation in the actual mapper and cache the method in the mapper
  • MapperMethod: mapper method class, including target method object and sql command object, the main function is to execute the sql statement of the target method
  • SqlSessionTemplate: Mybatis integrates the core class of spring. This class controls sqlSession and contains an internal class to execute dynamic proxy