spring-bootmybatisspring-mybatis

Configure mapper-locations in spring-mybatis with classpath:*


So I want this to work

@Bean
@ConfigurationProperties("datasource.mybatis-factory")
public SqlSessionFactoryBean sqlSessionFactoryBean() {
  SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  sqlSessionFactoryBean.setDataSource(dataSource());
  return sqlSessionFactoryBean;
}

with property (among others)

datasource.mybatis-factory.mapper-locations=classpath*:sqlmap/*.xml

However, it fails even though the files are there:

Caused by: java.io.FileNotFoundException: class path resource [classpath*:sqlmap/*.xml] cannot be opened because it does not exist

Looking at setMapperLocations() I didn't do anything wrong, they clearly want me to use classpath*:...:

/**
 * Set locations of MyBatis mapper files that are going to be merged into the {@code SqlSessionFactory} configuration
 * at runtime.
 *
 * This is an alternative to specifying "<sqlmapper>" entries in an MyBatis config file. This property being
 * based on Spring's resource abstraction also allows for specifying resource patterns here: e.g.
 * "classpath*:sqlmap/*-mapper.xml".
 *
 * @param mapperLocations
 *          location of MyBatis mapper files
 */
public void setMapperLocations(Resource... mapperLocations) {
  this.mapperLocations = mapperLocations;
}

Looking further down the code there's just this:

    for (Resource mapperLocation : this.mapperLocations) {
      if (mapperLocation == null) {
        continue;
      }
      try {
        XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
            targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
        xmlMapperBuilder.parse();

There is no code that would convert the classpath*:sqlmap/*.xml into openable resources or at least I don't see it. Or what am I missing here?

Work around:
What I have now and is working (note that I don't use datasource.mybatis-factory.mapper-locations as that would again overwrite what I set):

@Bean
@ConfigurationProperties("datasource.mybatis-factory")
public SqlSessionFactoryBean sqlSessionFactoryBean(
    @Value("${datasource.mybatis-factory.mapper-location-pattern}") String mapperLocations) {
  SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
  sqlSessionFactoryBean.setDataSource(dataSource());
  sqlSessionFactoryBean.setMapperLocations(findMapperLocations(mapperLocations));
  return sqlSessionFactoryBean;
}

private Resource[] findMapperLocations(String resourcePaths) {
  PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
  return Stream.of(resourcePaths.split(","))
      .map(LambdaExceptionUtilities.rethrowFunction(patternResolver::getResources))
      .flatMap(Stream::of)
      .toArray(Resource[]::new);
}

with property

datasource.mybatis-factory.mapper-location-pattern=classpath*:sqlmap/*.xml

So: what is missing here to make it work without the work around? How do XMLs on the classpath find the way into MyBatis? Maybe something Spring-Bootish missing?


Solution

  • I ran into the same issue recently. I believe this is what what you're looking for:

    @Bean
    @ConfigurationProperties("datasource.mybatis-factory")
    public SqlSessionFactoryBean sqlSessionFactoryBean(
        @Value("${datasource.mybatis-factory.mapper-location-pattern}") String mapperLocations) {
      SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
      sqlSessionFactoryBean.setDataSource(dataSource());
      sqlSessionFactoryBean.setMapperLocations(
          new PathMatchingResourcePatternResolver().getResources("classpath*:sqlmap/*.xml")
      );
      return sqlSessionFactoryBean;
    }
    

    Basically what you need is this line of code in your @Bean definition above:

    sqlSessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath*:sqlmap/*.xml")   );
    Note: the method name is getResources (the plural) and not getResource

    Feel free to replace the hard coded value of classpath*:sqlmap/*.xml with the @Value("datasource.mybatis-factory.mapper-location-pattern") injected value instead.

    Because you're using MyBatis with Spring, the issue here is not so much a MyBatis issue, as much as it is a Spring issue. More specifically, the wildcard feature that you want to use to load multiple resources, namely, classpath*:sqlmap/*.xml is specific to Spring and not MyBatis.

    I know, that the way it's documented in the MyBatis-Spring docs may lead you to believe that it's a MyBatis feature that let's you do this type of wildcard Resource loading, but it's not. Here's the relevant part of the MyBatis-Spring doc (source: https://mybatis.org/spring/factorybean.html#properties):

    The mapperLocations property takes a list of resource locations. This property can be used to specify the location of MyBatis XML mapper files. The value can contain Ant-style patterns to load all files in a directory or to recursively search all paths from a base location.

    However, sadly the docs only provide a Spring example based on XML and not Java configuration. If you read the Java Docs docs for SqlSessionFactoryBean, you'll find the following (source: https://mybatis.org/spring/apidocs/org/mybatis/spring/SqlSessionFactoryBean.html#setMapperLocations(org.springframework.core.io.Resource...)):

    public void setMapperLocations(org.springframework.core.io.Resource... mapperLocations)
    
    Set locations of MyBatis mapper files that are going to be merged into the
    SqlSessionFactory configuration at runtime.
    
    This is an alternative to specifying "<sqlmapper>" entries in an MyBatis config file.
    This property being based on Spring's resource abstraction also allows for
    specifying resource patterns here: e.g. "classpath*:sqlmap/*-mapper.xml".
    
    Parameters:
        mapperLocations - location of MyBatis mapper files
    

    So, the setMapperLocations method needs one or more org.springframework.core.io.Resource object(s). So, using Spring ClassPathResource will not work here because ClassPathResource expects only a single resource. What you need to use instead is Spring's PathMatchingResourcePatternResolver class. See: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/support/PathMatchingResourcePatternResolver.html

    You may also find this Stack Overflow post useful: How to use wildcards when searching for resources in Java-based Spring configuration?

    I hope this helps!