The process of mapper generating agent in mybatis

catalogue

  1. Generation process of mapper agent in mybatis
  2. Generation process of mapper proxy when integrating with Spring
  3. Generation process of mapper proxy when integrating with SpringBoot

Generation process of mapper agent in mybatis

Build agent factory

Starting from the entry point, step by step, first, the build() method in the SqlSessionFactoryBuilder class loads the configuration file

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      // ... omitted
    }
  }

Read the configuration file as an XMLConfigBuilder object, call the parse() method to parse the file and enter it into parse()

  public Configuration parse() {
     // ... omitted
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

You can see that the specific parsing process is carried out in the parseConfiguration method.

  private void parseConfiguration(XNode root) {
    try {
       // ... omitted
      //Parse mapper
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

Here, let's focus on the mapperElement(root.evalNode("mappers") method that parses the mapper and enters it into the method,

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          //   Load in package form, and load all class files under package
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            // Load through Mapper.xml
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            // Load through Mapper.xml
            ErrorContext.instance().resource(url);
            InputStream inputStream = Resources.getUrlAsStream(url);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            // Load through a single class file
            Class<?> mapperInterface = Resources.classForName(mapperClass);
            configuration.addMapper(mapperInterface);
          } else {
            throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
          }
        }
      }
    }
  }

The whole mapperElement() method is the process of loading mapper. You can see loading mapper
There are two forms: through class files and through xml files.
The process of building mapper agent starts from this. Analyze it step by step.
Take a look at the loading process of XML files. mybatis reads the mapper related configuration as an XMLMapperBuilder object, parses it through the parse() method, and enters this method

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // Load xml file
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      // Load mapper class file
      bindMapperForNamespace();
    }
   // ... omitted
  }

The parse() method does two main things, loading xml files and loading class files.
Take a look at the process of loading xml

  private void configurationElement(XNode context) {
    try {
      // Gets the namespace of the xml file
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      // Save and get the namespace of the xml file
      builderAssistant.setCurrentNamespace(namespace);
       // ... omitted
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

This article is to analyze the generation process of mapper agent, so the specific details of loading xml will not be analyzed in detail. What we should pay attention to here is to read the value of namespace tag in XML file and set the value to builder assistant object
Now let's go back to the process of loading the class file. Go to the bindMapperForNamespace() method

  private void bindMapperForNamespace() {
    // Gets the namespace value set in the xml file
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        // Load class
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          // Add to configuration
          configuration.addMapper(boundType);
        }
      }
    }
  }

bindMapperForNamespace() loads the corresponding mapper interface through the namespace value set in the xml file, and finally adds it to the configuration through configuration.addMapper().

Do you remember loading mapper just mentioned
There are two forms: through class files and through xml files. Directly call configuration.addMapper() through the class file to load the mapper interface into the configuration.

Configuration is the global configuration class of mybatis. All mybatis related information is saved in configuration.
Continue to the addMapper method of Configuration

  public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

Configuration adds the corresponding mapper interface to mapperRegistry, and then enters mapperRegistry.addMapper() method

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
        // ... omitted
      try {
        knownMappers.put(type, new MapperProxyFactory<T>(type));
         // ... omitted
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

This method first determines whether it is an interface. If it is an interface, add the mapper interface to knownMappers.
Take a look at the definition of knownMappers

  private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<Class<?>, MapperProxyFactory<?>>();

knownMappers is a HashMap that stores all mapper interfaces and corresponding mapper proxy factories.

So far, the mapper has been loaded, but the mapper's proxy object has not been generated, but the corresponding proxy factory has been generated.

Generate and use proxy objects

mybatis does not generate proxy objects when the mapper interface is loaded, but when it is called.
Start with the entrance

sqlSession.getMapper(XXX.class)

sqlSession defaults to DefaultSqlSession. Enter the getMapper() method of DefaultSqlSession

  @Override
  public <T> T getMapper(Class<T> type) {
    return configuration.<T>getMapper(type, this);
  }

Continue to getMapper in Configuration

 public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    return mapperRegistry.getMapper(type, sqlSession);
  }

Continue to mapperRegistry.getMapper()

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    // ... omitted
    }
    try {
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

Obtain the proxy factory class MapperProxyFactory of the corresponding mapper interface from knownMappers, and then obtain the real proxy object through MapperProxyFactory.
Enter the newInstance() method of MapperProxyFactory

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
  
   protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

First, the MapperProxy class is generated, and then the real Proxy class is generated through Proxy.
Take a look at the MapperProxy class

public class MapperProxy<T> implements InvocationHandler, Serializable {
  //  ... omitted
}

MapperProxy implements the InvocationHandler interface, and the specific processing logic of the mapper interface is processed in this class.

So far, the proxy object is really generated.

Generation process of mapper proxy when integrating with Spring

Mybatis Spring jar s are required when integrating mybatis with Spring.

Spring registers the mapper proxy class

Since it is integrated with Spring, you need to configure it and hand over mybatis to Spring for management.
xml file configuration for spring

    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="driverClassName"/>
        <property name="url" value="url"/>
        <property name="username" value="username"/>
        <property name="password" value="password"/>
    </bean>

    <!--sqlSessionFactory-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <!--binding mybatis configuration file-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!--register Mapper.xm Mapper-->
        <property name="mapperLocations" value="classpath:cn/ycl/mapper/*.xml"/>
    </bean>

    <!--Register all mapper-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!--basePackage Property is the package path of the mapper interface file.-->
        <!--You can set more than one package path using semicolons or commas as as separators-->
        <property name="basePackage" value="cn/ycl/mapper"/>
        <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
    </bean>

To hand over mybatis to Spring, you only need to configure three bean s
1. Database related dataSource
2. sqlSessionFactory of mybatis
3. Delegate mapper to the tool class mappercannerconfigurer of Spring
The process of generating mapper agent is mainly in mappercannerconfigurer. Please see the definition of mappercannerconfigurer

public class MapperScannerConfigurer
    implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware {
    // ... omitted
}

The key point is that mappercannerconfigurer implements BeanDefinitionRegistryPostProcessor, which is an extension point reserved by Spring and can register custom beans in Spring.

Mappercannerconfigurer implements the postProcessBeanDefinitionRegistry() method of BeanDefinitionRegistryPostProcessor, in which mapper is registered

  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    // ... omitted
    
    // Instantiate ClassPathMapperScanner and configure the relevant properties of the scanner
    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.setMapperFactoryBeanClass(this.mapperFactoryBeanClass);
    if (StringUtils.hasText(lazyInitialization)) {
      scanner.setLazyInitialization(Boolean.valueOf(lazyInitialization));
    }
    if (StringUtils.hasText(defaultScope)) {
      scanner.setDefaultScope(defaultScope);
    }
    // Register scan rules
    scanner.registerFilters();
    // Scan and register all mapper s
    scanner.scan(
        StringUtils.tokenizeToStringArray(this.basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS));
  }

The main logic of postProcessBeanDefinitionRegistry() is to define a ClassPathMapperScanner object, then call registerFilters() to register the scan rules, and finally call scan() method.

When defining mappercannerconfigurerbean in xml, you can set an annotationClass attribute whose value is an annotation class. When registerFilters() is called, registerFilters() will add a class that only scans the classes with annotationClass annotation. There is no setting here, and all interfaces will be scanned. This field is used when spring boot integrates mybatis

Take a look at the definition of the ClassPathMapperScanner class

public class ClassPathMapperScanner extends ClassPathBeanDefinitionScanner {
    // ... omitted
}

Classpathmappercanner inherits the ClassPathBeanDefinitionScanner, which is defined in Spring and a Spring tool to scan all bean definitions from the specified package.

Take a look at the scan() method of classpathmappercanner

  public Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Set<BeanDefinitionHolder> beanDefinitions = super.doScan(basePackages);

    if (beanDefinitions.isEmpty()) {
        // ... omitted
    } else {
      processBeanDefinitions(beanDefinitions);
    }

    return beanDefinitions;
  }

All mapper s have been scanned through super.doScan(basePackages). Continue with the processBeanDefinitions() method

  private void processBeanDefinitions(Set<BeanDefinitionHolder> beanDefinitions) {
    AbstractBeanDefinition definition;
    BeanDefinitionRegistry registry = getRegistry();
    // Traverse all bean s scanned
    for (BeanDefinitionHolder holder : beanDefinitions) {
      definition = (AbstractBeanDefinition) holder.getBeanDefinition();
      boolean scopedProxy = false;
      if (ScopedProxyFactoryBean.class.getName().equals(definition.getBeanClassName())) {
        definition = (AbstractBeanDefinition) Optional
            .ofNullable(((RootBeanDefinition) definition).getDecoratedDefinition())
            .map(BeanDefinitionHolder::getBeanDefinition).orElseThrow(() -> new IllegalStateException(
                "The target bean definition of scoped proxy bean not found. Root bean definition[" + holder + "]"));
        scopedProxy = true;
      }
      String beanClassName = definition.getBeanClassName();
      LOGGER.debug(() -> "Creating MapperFactoryBean with name '" + holder.getBeanName() + "' and '" + beanClassName
          + "' mapperInterface");

 
      // Add a constructor with the interface type as the input parameter of the constructor
      definition.getConstructorArgumentValues().addGenericArgumentValue(beanClassName); 

      // Convert the type of the bean to maperfactorybean
      definition.setBeanClass(this.mapperFactoryBeanClass);

      // Add addToConfig attribute
      definition.getPropertyValues().add("addToConfig", this.addToConfig);

  
      definition.setAttribute(FACTORY_BEAN_OBJECT_TYPE, beanClassName);

      boolean explicitFactoryUsed = false;
      // Add sqlSessionFactory property
      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;
      }

      // Add sqlSessionTemplate attribute
      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) {
        LOGGER.debug(() -> "Enabling autowire by type for MapperFactoryBean with name '" + holder.getBeanName() + "'.");
        definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
      }

      definition.setLazyInit(lazyInitialization);

      if (scopedProxy) {
        continue;
      }

      if (ConfigurableBeanFactory.SCOPE_SINGLETON.equals(definition.getScope()) && defaultScope != null) {
        definition.setScope(defaultScope);
      }

      if (!definition.isSingleton()) {
        BeanDefinitionHolder proxyHolder = ScopedProxyUtils.createScopedProxy(holder, registry, true);
        if (registry.containsBeanDefinition(proxyHolder.getBeanName())) {
          registry.removeBeanDefinition(proxyHolder.getBeanName());
        }
        registry.registerBeanDefinition(proxyHolder.getBeanName(), proxyHolder.getBeanDefinition());
      }

    }
  }

This method is relatively long, but not complex. The main logic is to modify the type of the scanned bean to the MapperFactoryBean type, and add a constructor with the interface type as the input parameter, that is, Spring generates the mapper through the FactoryBean when it obtains the mapper. Finally, register in Spring by calling the registry. Registerbeandefinition () method.

Take a look at the MapperFactoryBean definition provided by mybatis

public class MapperFactoryBean<T> extends SqlSessionDaoSupport implements FactoryBean<T> {
}

MapperFactoryBean implements FactoryBean, which is a factory Bean that can produce objects provided by Spring

MapperFactoryBean also inherits SqlSessionDaoSupport, SqlSessionDaoSupport inherits DaoSupport, and DaoSupport implements InitializingBean. InitializingBean is used to call the afterpropertieset() method of InitializingBean first when Spring initializes the bean object.

The checkDaoConfig() method is called in afterPropertiesSet() of DaoSupport.

    public final void afterPropertiesSet() throws IllegalArgumentException, BeanInitializationException {
        this.checkDaoConfig();

        try {
            this.initDao();
        } catch (Exception var2) {
            throw new BeanInitializationException("Initialization of DAO failed", var2);
        }
    }

The implementation logic of the checkDaoConfig() method is in MapperFactoryBean

 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) {
        // .. Omitted
      } finally {
        ErrorContext.instance().reset();
      }
    }
  }

OK, here we go back to mybatis. As mentioned earlier, the configuration.addMapper() method only generates the corresponding proxy factory.

In the whole process above, the mapper is registered as a Spring bean, and the mapper is set to the configuration in mybatis. Therefore, when using, you can not only use Spring to automatically inject the set, but also use sqlSession in mybatis to obtain the mapper's proxy object

Spring generates proxy objects

All mapper corresponding beans in Spring are MapperFactoryBean corresponding to mapper. When obtaining mapper bean s, they are generated through the getObject() method of MapperFactoryBean

  public T getObject() throws Exception {
    return getSqlSession().getMapper(this.mapperInterface);
  }

MapperFactoryBean obtains the sqlsession first, and then obtains the proxy object through getMapper(). Here we return to the process of generating proxy objects by mybatis.

Generation process of mapper proxy when integrating with SpringBoot

Mybatis Spring boot starter jar is required for integration between mybatis and Spring. Mybatis Spring boot starter depends on mybatis Spring boot autoconfigure jar, while mybatis Spring boot autoconfigure jar depends on mybatis Spring jar. Therefore, in the end, mybatis integrates with Spring

According to the principle of spring boot automatic loading, directly look at the META-INF/spring.factories file under mybatis spring boot autoconfigurejar

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

SpringBoot will automatically load the MybatisAutoConfiguration class. Look at this class directly. MybatisAutoConfiguration defines various bean s required by mybtis.

    //Generate SqlSessionFactory
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        // ... omitted
    }
    
    //Generate SqlSessionTemplate
    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
     // ... omitted
    }
    
    //Scan mapper
     @Configuration
    @Import({MybatisAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
    @ConditionalOnMissingBean({MapperFactoryBean.class, MapperScannerConfigurer.class})
    public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
        public MapperScannerRegistrarNotFoundConfiguration() {
        }

        public void afterPropertiesSet() {
            MybatisAutoConfiguration.logger.debug("Not found configuration for registering mapper bean using @MapperScan, MapperFactoryBean and MapperScannerConfigurer.");
        }
    }
     //Scan mapper
     public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar {
        private BeanFactory beanFactory;

        public AutoConfiguredMapperScannerRegistrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            if (!AutoConfigurationPackages.has(this.beanFactory)) {
                MybatisAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
            } else {
                MybatisAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
                List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
                if (MybatisAutoConfiguration.logger.isDebugEnabled()) {
                    packages.forEach((pkg) -> {
                        MybatisAutoConfiguration.logger.debug("Using auto-configuration base package '{}'", pkg);
                    });
                }
                //Generate mappercannerconfigurer 
                BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
                builder.addPropertyValue("processPropertyPlaceHolders", true);
                // Register scan rules
                builder.addPropertyValue("annotationClass", Mapper.class);
                builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(packages));
                BeanWrapper beanWrapper = new BeanWrapperImpl(MapperScannerConfigurer.class);
                Stream.of(beanWrapper.getPropertyDescriptors()).filter((x) -> {
                    return x.getName().equals("lazyInitialization");
                }).findAny().ifPresent((x) -> {
                    builder.addPropertyValue("lazyInitialization", "${mybatis.lazy-initialization:false}");
                });
                registry.registerBeanDefinition(MapperScannerConfigurer.class.getName(), builder.getBeanDefinition());
            }
        }

        public void setBeanFactory(BeanFactory beanFactory) {
            this.beanFactory = beanFactory;
        }
    }
    

Tags: Java

Posted on Wed, 22 Sep 2021 00:01:17 -0400 by zrueda