background

In a microservice project, you need to make the database configuration part a common component to the required sub-service dependencies. The database common component contains all the data source configuration, but the sub-service can choose to use some data sources and maintain mapper itself. So the basePackages on each data source are different in different sub-services, which requires the value of basePackages to be read in the configuration file via placeholder configuration.

The solution

Therefore, I customized @mapperscan and MapperScannerRegister, mainly modifying the logic that reads basePackages in @MappersCan. The code is as follows:

for (String pkg : annoAttrs.getStringArray("basePackages")) {
    if (StringUtils.hasText(pkg)) {
        String value = parsePlaceHolder(pkg);
        if (StringUtils.hasText(value)) {
            List<String> values = Arrays.asList(value.split(","));
            for(String base : values) { basePackages.add(base); }}}}private String parsePlaceHolder(String pro) {if(StringUtils. HasText (pro) && pro. The contains (PropertySourcesPlaceholderConfigurer. DEFAULT_PLACEHOLDER_PREFIX)) {String  value = environment.getProperty(pro.substring(2, pro.length() - 1));if (null= = {LOGGER. Warn (value)"The value of property '{}' is null"Pro); }returnvalue; }return pro;
}
Copy the code

Configuration on the data source:

@Configuration
@MapperScan(basePackages = {"${mybatis.mapperScan.basePackages.order}"}, sqlSessionTemplateRef = "orderSqlSessionTemplate")
public class OrderDataSourceConfig {... }Copy the code

Add configuration to application.yml:

mybatis:
  mapperScan:
    basePackages:
      order: com.abc.bc.order.mapper.order
Copy the code

Problem finding and analysis

Now try the launcher service, and sure enough, it fails…

[the 00:03:38 2020-12-28, 450] [ERROR] [the main] [org. Springframework. Boot. SpringApplication: 821] Application run failed java.lang.IllegalArgumentException: At least one base package must be specified at org.springframework.util.Assert.notEmpty(Assert.java:372) at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.doScan(ClassPathBeanDefinitionScanner.java:272) at  org.mybatis.spring.mapper.ClassPathMapperScanner.doScan(ClassPathMapperScanner.java:181) at org.springframework.context.annotation.ClassPathBeanDefinitionScanner.scan(ClassPathBeanDefinitionScanner.java:253) at org.mybatis.spring.mapper.MapperScannerConfigurer.postProcessBeanDefinitionRegistry(MapperScannerConfigurer.java:356) at  org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostPr ocessorRegistrationDelegate.java:275) at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegis trationDelegate.java:125) at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContex t.java:705) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:531) at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationConte xt.java:140) at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:742) at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:389) at org.springframework.boot.SpringApplication.run(SpringApplication.java:311) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1213) at org.springframework.boot.SpringApplication.run(SpringApplication.java:1202)Copy the code

Through the debug source code analysis, found that this is the Spring ClassPathBeanDefinitionScanner scanner sweep the yard basePackages package and register beanDefinitions times wrong, because we don’t use all of the data source configuration, However, other data source configurations also specify basePackages, and we do not configure their value, resulting in doScan(String… BasePackages) does not pass the non-null check. It is possible that the project has many data sources, and the subservice only needs to use one of them, so it only needs to configure the mapper that it needs. So the final idea is that even though the subservice introduces all the data sources, the database common component should support basePackages that don’t let Spring scan their configuration if they don’t need them. Then change MapperScannerRegister. If basePackages is empty, Spring does not manage it. The code is as follows:

/** * If basePackages are not configured, the bean is not registered */
if (basePackages.isEmpty()) {
    return;
}
Copy the code

Repack and start. Everything’s fine.

Complete source code

Here is the full code for MapperScannerRegister:

import org.mybatis.spring.mapper.MapperFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.EnvironmentAware;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class MapperScannerRegistrar implements ImportBeanDefinitionRegistrar.ResourceLoaderAware.EnvironmentAware {

    private static final Logger LOGGER = LoggerFactory.getLogger(MapperScannerRegistrar.class);

    private Environment environment;

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        // NOP
    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AnnotationAttributes mapperScanAttrs = AnnotationAttributes
                .fromMap(importingClassMetadata.getAnnotationAttributes(MapperScan.class.getName()));
        if(mapperScanAttrs ! =null) {
            registerBeanDefinitions(mapperScanAttrs, registry, generateBaseBeanName(importingClassMetadata, 0)); }}void registerBeanDefinitions(AnnotationAttributes annoAttrs, BeanDefinitionRegistry registry, String beanName) {

        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
        builder.addPropertyValue("processPropertyPlaceHolders".true);

        Class<? extends Annotation> annotationClass = annoAttrs.getClass("annotationClass");
        if(! Annotation.class.equals(annotationClass)) { builder.addPropertyValue("annotationClass", annotationClass); } Class<? > markerInterface = annoAttrs.getClass("markerInterface");
        if(! Class.class.equals(markerInterface)) { builder.addPropertyValue("markerInterface", markerInterface);
        }

        Class<? extends BeanNameGenerator> generatorClass = annoAttrs.getClass("nameGenerator");
        if(! BeanNameGenerator.class.equals(generatorClass)) { builder.addPropertyValue("nameGenerator", BeanUtils.instantiateClass(generatorClass));
        }

        Class<? extends MapperFactoryBean> mapperFactoryBeanClass = annoAttrs.getClass("factoryBean");
        if(! MapperFactoryBean.class.equals(mapperFactoryBeanClass)) { builder.addPropertyValue("mapperFactoryBeanClass", mapperFactoryBeanClass);
        }

        String sqlSessionTemplateRef = annoAttrs.getString("sqlSessionTemplateRef");
        if (StringUtils.hasText(sqlSessionTemplateRef)) {
            builder.addPropertyValue("sqlSessionTemplateBeanName", annoAttrs.getString("sqlSessionTemplateRef"));
        }

        String sqlSessionFactoryRef = annoAttrs.getString("sqlSessionFactoryRef");
        if (StringUtils.hasText(sqlSessionFactoryRef)) {
            builder.addPropertyValue("sqlSessionFactoryBeanName", annoAttrs.getString("sqlSessionFactoryRef"));
        }

        List<String> basePackages = new ArrayList<>();
        basePackages.addAll(
                Arrays.stream(annoAttrs.getStringArray("value")).filter(StringUtils::hasText).collect(Collectors.toList()));

        /** ** change point */
        for (String pkg : annoAttrs.getStringArray("basePackages")) {
            if (StringUtils.hasText(pkg)) {
                String value = parsePlaceHolder(pkg);
                if (StringUtils.hasText(value)) {
                    List<String> values = Arrays.asList(value.split(","));
                    for (String base : values) {
                        basePackages.add(base);
                    }
                }
            }
        }

        basePackages.addAll(Arrays.stream(annoAttrs.getClassArray("basePackageClasses")).map(ClassUtils::getPackageName)
                .collect(Collectors.toList()));

        /** * If basePackages are not configured, the bean is not registered */
        if (basePackages.isEmpty()) {
            return;
        }

        String lazyInitialization = annoAttrs.getString("lazyInitialization");
        if (StringUtils.hasText(lazyInitialization)) {
            builder.addPropertyValue("lazyInitialization", lazyInitialization);
        }

        builder.addPropertyValue("basePackage", StringUtils.collectionToCommaDelimitedString(basePackages));

        registry.registerBeanDefinition(beanName, builder.getBeanDefinition());

    }

    private static String generateBaseBeanName(AnnotationMetadata importingClassMetadata, int index) {
        return importingClassMetadata.getClassName() + "#" + org.mybatis.spring.annotation.MapperScannerRegistrar.class.getSimpleName() + "#" + index;
    }

    private String parsePlaceHolder(String pro) {
        if (StringUtils.hasText(pro) && pro.contains(PropertySourcesPlaceholderConfigurer.DEFAULT_PLACEHOLDER_PREFIX)) {
            String value = environment.getProperty(pro.substring(2, pro.length() - 1));

            if (null == value) {
                LOGGER.warn("The value of property '{}' is null", pro);
            }

            return value;
        }

        returnpro; }}Copy the code