问题来源
看到工程里的单测Base类有类似这样的用法:
@ContextConfiguration({"classpath*:spring/applicationContext*.xml"})
@RunWith(SpringJUnit4ClassRunner.class)
public class SpringTestBootStrap implements ApplicationContextAware {
....
}
而工程目录下,test模块与main模块都有spring/applicationContext.xml文件,因此瞬间感到很疑惑,启动的时候到底加载哪个目录的文件呢?还是都加载?如图:
问题分析
按照Spring的设计,用SpringJUnit4ClassRunner
加载配置与单独用ClassPathXMLApplicationContext
来加载配置文件,其原理是一致的,因此想到用如下单测来测试:
TestConfig为test模块的class, 依赖了main模块的AppConfig, 分别配置在两个模块的applicationContext.xml文件里,如果能正常的做依赖注入,那么说明两个文件都加载到了。
public class SpringStandardTest {
@Test
public void testStart() {
ClassPathXmlApplicationContext context =
new ClassPathXmlApplicationContext("classpath*:spring/applicationContext*.xml");
TestConfig bean = context.getBean(TestConfig.class);
Assert.assertNotNull(bean);
}
}
@Component
public class TestConfig {
@Autowired
private AppConfig appConfig;
}
先说结论
通过分析源码,知晓了如果指定的是classpath*, 则Spring会加载工程类路径下所有的资源,包括依赖的二方模块jar与三方jar文件。
IDEA启动单测时会默认指定-classpath,表示需要加载的class和jar. 多个用冒号分隔。
java -classpath <目录和 zip/jar 文件的类搜索路径>
用 : 分隔的目录, JAR 档案
和 ZIP 档案列表, 用于搜索类文件。
IDEA单测启动示例:
/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/bin/java
-agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52388,suspend=y,server=n
-ea -Didea.test.cyclic.buffer.size=1048576
-javaagent:/Users/xiele/Library/Caches/IntelliJIdea2019.3/captureAgent/debugger-agent.jar -Dfile.encoding=UTF-8
-classpath "/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar:/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit5-rt.jar
:/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit-rt.jar
:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/charsets.jar
:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/deploy
:/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/tools.jar
:/Users/xiele/IdeaProjects/HelloThere/springboot-demo/target/test-classes
:/Users/xiele/IdeaProjects/HelloThere/springboot-demo/target/classes
:/Users/xiele/.m2/repository/org/springframework/boot/spring-boot-starter-web/2.3.0.RELEASE/spring-boot-starter-web-2.3.0.RELEASE.jar
:/Users/xiele/.m2/repository/org/springframework/boot/spring-boot-starter/2.3.0.RELEASE/spring-boot-starter-2.3.0.RELEASE.jar
:/Users/xiele/.m2/repository/org/springframework/boot/spring-boot/2.3.0.RELEASE/spring-boot-2.3.0.RELEASE.jar
:/Users/xiele/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.3.0.RELEASE/spring-boot-autoconfigure-2.3.0.RELEASE.jar
com.intellij.rt.junit.JUnitStarter
-ideVersion5 -junit4
com.example.springboot.springbootdemo.SpringStandardTest,testStart
可以看到,本地单测时,默认把当前工程里的target/classes 与target/test-classes里的资源都加入到classpath里的搜索路径里,因此JVM可以通过ClassLoader可以加载到。
当前配置文件为classpath*开头时,内部通过 Enumeration方法返回当前所有满足配置文件前缀目录下的资源。
如在当前示例下,查询「spring/」这个目录下的资源,返回:test-classes与classes下的资源。
而已classpath开头则只是简单执行:用当前的classloader加载启动的入口目录。
classpath*
classpath: Prefix: There is special support for retrieving multiple class path resources with the same name, via the “classpath:” prefix. For example, “classpath*:META-INF/beans.xml” will find all “beans.xml” files in the class path, be it in “classes” directories or in JAR files. This is particularly useful for autodetecting config files of the same name at the same location within each jar file. Internally, this happens via a ClassLoader.getResources() call, and is completely portable.
The “classpath:” prefix can also be combined with a PathMatcher pattern in the rest of the location path, for example “classpath:META-INF/*-beans.xml”. In this case, the resolution strategy is fairly simple: a ClassLoader.getResources() call is used on the last non-wildcard path segment to get all the matching resources in the class loader hierarchy, and then off each resource the same PathMatcher resolution strategy described above is used for the wildcard subpath.
classpath*走ClassLoader的返回多个URL的getResource,默认查找所有的ClassPath搜索路径。
/**
* Finds all the resources with the given name. A resource is some data
* (images, audio, text, etc) that can be accessed by class code in a way
* that is independent of the location of the code.
*
* <p>The name of a resource is a <tt>/</tt>-separated path name that
* identifies the resource.
*
* <p> The search order is described in the documentation for {@link
* #getResource(String)}. </p>
*
* @apiNote When overriding this method it is recommended that an
* implementation ensures that any delegation is consistent with the {@link
* #getResource(java.lang.String) getResource(String)} method. This should
* ensure that the first element returned by the Enumeration's
* {@code nextElement} method is the same resource that the
* {@code getResource(String)} method would return.
*
* @param name
* The resource name
*
* @return An enumeration of {@link java.net.URL <tt>URL</tt>} objects for
* the resource. If no resources could be found, the enumeration
* will be empty. Resources that the class loader doesn't have
* access to will not be in the enumeration.
*
* @throws IOException
* If I/O errors occur
*
* @see #findResources(String)
*
* @since 1.2
*/
public Enumeration<URL> getResources(String name) throws IOException {
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
tmp[0] = getBootstrapResources(name);
}
tmp[1] = findResources(name);
return new CompoundEnumeration<>(tmp);
}
classpath
In the simple case, if the specified location path does not start with the “classpath*:” prefix, and does not contain a PathMatcher pattern, this resolver will simply return a single resource via a getResource() call on the underlying ResourceLoader. Examples are real URLs such as “file:C:/context.xml”, pseudo-URLs such as “classpath:/context.xml”, and simple unprefixed paths such as “/WEB-INF/context.xml”. The latter will resolve in a fashion specific to the underlying ResourceLoader (e.g. ServletContextResource for a WebApplicationContext).
Ant-style Patterns:
When the path location contains an Ant-style pattern, e.g.:
/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml
classpath查找会走ClassLoader的返回单个URL的getResource, 按照classpath 指定的路径顺序开始查找,如果找到则直接返回,不在继续查找(例如如果test-classes找到了, 则直接返回)
/**
* Finds the resource with the given name. A resource is some data
* (images, audio, text, etc) that can be accessed by class code in a way
* that is independent of the location of the code.
*
* <p> The name of a resource is a '<tt>/</tt>'-separated path name that
* identifies the resource.
*
* <p> This method will first search the parent class loader for the
* resource; if the parent is <tt>null</tt> the path of the class loader
* built-in to the virtual machine is searched. That failing, this method
* will invoke {@link #findResource(String)} to find the resource. </p>
*
* @apiNote When overriding this method it is recommended that an
* implementation ensures that any delegation is consistent with the {@link
* #getResources(java.lang.String) getResources(String)} method.
*
* @param name
* The resource name
*
* @return A <tt>URL</tt> object for reading the resource, or
* <tt>null</tt> if the resource could not be found or the invoker
* doesn't have adequate privileges to get the resource.
*
* @since 1.1
*/
public URL getResource(String name) {
URL url;
if (parent != null) {
url = parent.getResource(name);
} else {
url = getBootstrapResource(name);
}
if (url == null) {
url = findResource(name);
}
return url;
}
测试
例如对于Maven工程,在main/resources目录下有个一个application.yml文件,在test/resources下也有一个applicaiton.yml文件。通过如下代码测试:
public static void main(String[] args) throws IOException {
ClassPathResource resource = new ClassPathResource("application.yml");
System.out.println(resource.getClassLoader());
System.out.println(IOUtils.toString(resource.getInputStream(), "utf-8"));
}
在Test目录下运行时,IDEA指定的classpath顺序为:
target/test-classes: target/classes. 所以默认先搜索target/test-classes下的application.yml文件。
如果交换顺序,则先搜索target/classes下的application.yml文件。