目录
一、背景
二、排查过程
2.1 Springboot的自动配置
2.2 Spring获取资源
2.3 AntPathMatcher,spring默认的路径匹配器
三、解决
四、小结
最近在新项目中在用springboot结合mybatis,一开始采取全注解模式,即,sql语句也写到接口注解上。后来,考虑到复杂sql语句在注解上编写的可读性和可维护性,决定将sql语句转移到mybatis常用的xml映射器上。
原mapper接口写法如下:
@Mapper
public interface AuditBillVMapper {
@Select("select t.* from audit_bill_v t where t.task_user_no=#{userNo}")
List getByTaskUserNo(String userNo);
}
加入sql映射器xml文件后写法如下:
AuditBillVMapper.class
@Mapper
public interface AuditBillVMapper {
List getByTaskUserNo(String userNo);
}
AuditBillVMapper.xml
然而,在xml到mapper接口的映射上遇到了问题:sql语句映射器的xml文件跟mapper接口class怎么文件对应呢?mybatis官方文档有关于mybatis找到这些xml映射器的xml配置,例如(省略其他配置):
但是问题来了:
1.作为一个基于springboot的项目,这些xml配置文件无法忍受;
2.每次添加sql映射xml文件时,都需要在这个mybatis-config.xml添加xml文件资源路径,不易于维护。
有关mybatis的介绍和入门这里就不阐述了,官方有较为详细的中文文档。链接:mybatis入门
一个基于springboot框架的项目,首先想到的当然是自动配置。显然springboot对于mybatis也是有自动配置的,然后我找到了自动配置类:
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的(不了解可跳转:mybatis入门)。因而我在MybatisAutoConfiguration.class中找到这个方法sqlSessionFactory:
@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());
}
//mapperLocations,显然这就是配置mapper路径的地方
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
return factory.getObject();
}
这个方法里面便是springboot对mybatis的一些默认自动配置,我们现在里面找到factory.setMapperLocations,从字面上看这应是设置mapper路径的地方,因而跟进SqlSessionFactoryBean.setMapperLocations看到源码上的注释如下:
SqlSessionFactoryBean.class
/**
* 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".
*/
//官方注释说的大致如此:
//设置即将写进SqlSessionFactory的MyBatis的mapper文件的路径。
//这将替代mybatis配置文件中指定的""实体
//此属性基于spring的抽象资源模式(Resource),也允许像这样指定资源模式:“classpath*:sqlmap/*-mapper.xml”
public void setMapperLocations(Resource[] mapperLocations) {
this.mapperLocations = mapperLocations;
}
到了这里,我们基本可以肯定就是SqlSessionFactoryBean.mapperLocations就是我们需要配置的地方,所以我们回到自动配置方法里:
MybatisAutoConfiguration.class
private final MybatisProperties properties;
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
为了了解到this.properties.resolveMapperLocations()是什么,我们跟进到MybatisProperties.class这个类里。部分代码如下:
/**
* Configuration properties for MyBatis.
*
* @author Eddú Meléndez
* @author Kazuki Shimizu
*/
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
/**
* Location of MyBatis xml config file.
*/
private String configLocation;
/**
* Locations of MyBatis mapper files.
*/
private String[] mapperLocations;
/**
* Packages to search type aliases. (Package delimiters are ",; \t\n")
*/
private String typeAliasesPackage;
/**
* Packages to search for type handlers. (Package delimiters are ",; \t\n")
*/
private String typeHandlersPackage;
/**
* Indicates whether perform presence check of the MyBatis xml config file.
*/
private boolean checkConfigLocation = false;
}
显然,这个类的属性就是springboot的默认配置文件application.properties对应mybatis配置的属性了。例如需要配置mybatis的typeAliasesPackage属性,可以在application.properties中添加mybatis.typeAliasesPackage=com.demo.domain。
接着,回到我们需要了解的resolveMapperLocations()方法:
public Resource[] resolveMapperLocations() {
ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
List resources = new ArrayList();
//this.mapperLocations,这个就是配置mapper地址路径的属性
if (this.mapperLocations != null) {
for (String mapperLocation : this.mapperLocations) {
try {
//这就是我们要找resources,所以它是根据mapperLocation的地址找到xml文件并返回Resources对象
Resource[] mappers = resourceResolver.getResources(mapperLocation);
resources.addAll(Arrays.asList(mappers));
} catch (IOException e) {
// ignore
}
}
}
return resources.toArray(new Resource[resources.size()]);
}
这时熟悉spring和springboot及其URL地址配置的朋友应该想到怎么配置了。mapperLocations为String数组,所以我们可以在application.properties添加多个mybatis.mapperLocations=路径/aaa.xml,mybatis.mapperLocations=路径/bbb.xml等等,或者根据在SqlSessionFactoryBean.setMapperLocations上看到的注释,我们也可以尝试这样配置:mybatis.mapperLocations=classpath*:sqlmap/*-mapper.xml。
想知道如何在application.properties怎样配置mybatis的映射器xml路径的朋友在这里可以结束了,可以跳转到解决。
但是!如果我们想在深入了解spring是怎样把路径和资源加载进去及资源路径的匹配原则,那么,我们可以提出两个问题:
下面便对这两个问题继续跟进。
我们接着看源码,现在来到PathMatchingResourcePatternResolver类的getResources方法:
@Override
public Resource[] getResources(String locationPattern) throws IOException {
Assert.notNull(locationPattern, "Location pattern must not be null");
if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
// a class path resource (multiple resources for same name possible)
if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
// a class path resource pattern
return findPathMatchingResources(locationPattern);
}
else {
// all class path resources with the given name
return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
}
}
else {
// Generally only look for a pattern after a prefix here,
// and on Tomcat only after the "*/" separator for its "war:" protocol.
int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
locationPattern.indexOf(':') + 1);
if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
// a file pattern
return findPathMatchingResources(locationPattern);
}
else {
// a single resource with the given name
return new Resource[] {getResourceLoader().getResource(locationPattern)};
}
}
}
上述代码有三个关键点:
1.根据地址前缀是否为CLASSPATH_ALL_URL_PREFIX(classparh*:)区分了获取资源方式(如,在我本测试项目中,以classpath*: 为前缀的路径地址,查找路径为:本项目所在文件夹路径\target\classes\匹配路径后缀 和 本项目所在文件夹路径\target\test-classes\匹配路径后缀);
2.根据方法getPathMatcher().isPattern(“路径地址”)来判断去除前缀(如classpath*:)后的路径地址是否为表达式模式(如/aa/*.txt,为表达式,/aa/a.txt则非表达式模式)。
3.如何查找所希望匹配的资源,可以跟踪findPathMatchingResources(“去除前缀后的路径地址”)方法。
下面以路径:classpath*: com/demo/aa/*.txt 简述查找资源步骤:
1.路径classpath*: com/demo/aa/*.txt 以classpath*: 开头,所以locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)返回true;
2.getPathMatcher().isPattern(“com/demo/aa/*.txt ”),判断路径地址是否为表达式模式,此处返回true;
3.进入findPathMatchingResources(“com/demo/aa/*.txt”) ,先找到当前路径中非匹配路径的最上级目录(如:/aa/**/*.txt,则找到/aa目录),然后从非匹配路径的最上级目录获取到其目录下的所有文件(如找到com/demo/aa/123.txt,com/demo/aa/abc,com/demo/aa/111.jpg),再根据getPathMatcher().match()来将找到的文件路径与com/demo/aa/*.txt比较,符合表达式则返回该资源,因而上述例子中返回com/demo/aa/123.txt。
上述步骤中多次出现getPathMatcher()中的方法,查看源码发现:
private PathMatcher pathMatcher = new AntPathMatcher();
/**
* Return the PathMatcher that this resource pattern resolver uses.
*/
public PathMatcher getPathMatcher() {
return this.pathMatcher;
}
可以看到该resolver中使用的默认路径匹配器为AntPathMatcher,这类就是路径表达式跟文件路径匹配的语法实现类了,即上述第3步骤时 123.txt 与 *.txt 匹配的语法。有关路径匹配器的内容交给下一节AntPathMatcher,srping默认的路径匹配器
Spring获取资源时序图:
由于AntPathMatcher实现了PathMatcher接口,我们先来看看PathMatcher:
/**
* Strategy interface for {@code String}-based path matching.
* Used by {@link org.springframework.core.io.support.PathMatchingResourcePatternResolver},
* {@link org.springframework.web.servlet.handler.AbstractUrlHandlerMapping},
* and {@link org.springframework.web.servlet.mvc.WebContentInterceptor}.
*
The default implementation is {@link AntPathMatcher}, supporting the
* Ant-style pattern syntax.
*/
public interface PathMatcher {
//省略其他代码
}
接口上的注释很容易理解:
这是一个基于String的策略接口。用于PathMatchingResourcePatternResolver,AbstractUrlHandlerMapping,和WebContentInterceptor。该接口的默认实现类为支持Ant-style模式的语法的AntPathMatcher类。
至此,我们可以了解到有关spring的路径匹配中,可以说都是基于Ant风格的匹配原则。有关AntPathMatcher的源码这里就不解读了,下面给出具体匹配原则和例子:
匹配原则:
符号 | 匹配原则 | 举例和说明 |
---|---|---|
? | 匹配一个字符 | /app/p?ttern 匹配(Matches) /app/pattern 和 /app/pXttern,但是不包括/app/pttern |
* | 匹配0个或多个字符 |
/app/*.x 匹配(Matches)所有在app路径下的.x文件 |
** | 匹配0个或多个目录 | /app/**/dir/file. 匹配(Matches) /app/dir/file.jsp, /app/foo/dir/file.html,/app/foo/bar/dir/file.pdf /**/*.jsp 匹配(Matches)任何的.jsp 文件 |
参考了:spring工具类AntPathMatcher
......
由于最近没有时间,目前先写到这里,解决方案和总结很快不上(其实解决方案应该已经一目了然了)