问题来源

看到工程里的单测Base类有类似这样的用法:

  1. @ContextConfiguration({"classpath*:spring/applicationContext*.xml"})
  2. @RunWith(SpringJUnit4ClassRunner.class)
  3. public class SpringTestBootStrap implements ApplicationContextAware {
  4. ....
  5. }

而工程目录下,test模块与main模块都有spring/applicationContext.xml文件,因此瞬间感到很疑惑,启动的时候到底加载哪个目录的文件呢?还是都加载?如图:
image.png

问题分析

按照Spring的设计,用SpringJUnit4ClassRunner加载配置与单独用ClassPathXMLApplicationContext 来加载配置文件,其原理是一致的,因此想到用如下单测来测试:
TestConfig为test模块的class, 依赖了main模块的AppConfig, 分别配置在两个模块的applicationContext.xml文件里,如果能正常的做依赖注入,那么说明两个文件都加载到了。

  1. public class SpringStandardTest {
  2. @Test
  3. public void testStart() {
  4. ClassPathXmlApplicationContext context =
  5. new ClassPathXmlApplicationContext("classpath*:spring/applicationContext*.xml");
  6. TestConfig bean = context.getBean(TestConfig.class);
  7. Assert.assertNotNull(bean);
  8. }
  9. }
  10. @Component
  11. public class TestConfig {
  12. @Autowired
  13. private AppConfig appConfig;
  14. }

先说结论

通过分析源码,知晓了如果指定的是classpath*, 则Spring会加载工程类路径下所有的资源,包括依赖的二方模块jar与三方jar文件。

IDEA启动单测时会默认指定-classpath,表示需要加载的class和jar. 多个用冒号分隔。

  1. java -classpath <目录和 zip/jar 文件的类搜索路径>
  2. : 分隔的目录, JAR 档案
  3. ZIP 档案列表, 用于搜索类文件。

IDEA单测启动示例:

  1. /Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/bin/java
  2. -agentlib:jdwp=transport=dt_socket,address=127.0.0.1:52388,suspend=y,server=n
  3. -ea -Didea.test.cyclic.buffer.size=1048576
  4. -javaagent:/Users/xiele/Library/Caches/IntelliJIdea2019.3/captureAgent/debugger-agent.jar -Dfile.encoding=UTF-8
  5. -classpath "/Applications/IntelliJ IDEA.app/Contents/lib/idea_rt.jar:/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit5-rt.jar
  6. :/Applications/IntelliJ IDEA.app/Contents/plugins/junit/lib/junit-rt.jar
  7. :/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/charsets.jar
  8. :/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/jre/lib/deploy
  9. :/Library/Java/JavaVirtualMachines/jdk1.8.0_251.jdk/Contents/Home/lib/tools.jar
  10. :/Users/xiele/IdeaProjects/HelloThere/springboot-demo/target/test-classes
  11. :/Users/xiele/IdeaProjects/HelloThere/springboot-demo/target/classes
  12. :/Users/xiele/.m2/repository/org/springframework/boot/spring-boot-starter-web/2.3.0.RELEASE/spring-boot-starter-web-2.3.0.RELEASE.jar
  13. :/Users/xiele/.m2/repository/org/springframework/boot/spring-boot-starter/2.3.0.RELEASE/spring-boot-starter-2.3.0.RELEASE.jar
  14. :/Users/xiele/.m2/repository/org/springframework/boot/spring-boot/2.3.0.RELEASE/spring-boot-2.3.0.RELEASE.jar
  15. :/Users/xiele/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.3.0.RELEASE/spring-boot-autoconfigure-2.3.0.RELEASE.jar
  16. com.intellij.rt.junit.JUnitStarter
  17. -ideVersion5 -junit4
  18. com.example.springboot.springbootdemo.SpringStandardTest,testStart

可以看到,本地单测时,默认把当前工程里的target/classes 与target/test-classes里的资源都加入到classpath里的搜索路径里,因此JVM可以通过ClassLoader可以加载到。

当前配置文件为classpath*开头时,内部通过 Enumeration getResources(String name)
方法返回当前所有满足配置文件前缀目录下的资源。
如在当前示例下,查询「spring/」这个目录下的资源,返回:test-classes与classes下的资源。
image.png

而已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搜索路径。

  1. /**
  2. * Finds all the resources with the given name. A resource is some data
  3. * (images, audio, text, etc) that can be accessed by class code in a way
  4. * that is independent of the location of the code.
  5. *
  6. * <p>The name of a resource is a <tt>/</tt>-separated path name that
  7. * identifies the resource.
  8. *
  9. * <p> The search order is described in the documentation for {@link
  10. * #getResource(String)}. </p>
  11. *
  12. * @apiNote When overriding this method it is recommended that an
  13. * implementation ensures that any delegation is consistent with the {@link
  14. * #getResource(java.lang.String) getResource(String)} method. This should
  15. * ensure that the first element returned by the Enumeration's
  16. * {@code nextElement} method is the same resource that the
  17. * {@code getResource(String)} method would return.
  18. *
  19. * @param name
  20. * The resource name
  21. *
  22. * @return An enumeration of {@link java.net.URL <tt>URL</tt>} objects for
  23. * the resource. If no resources could be found, the enumeration
  24. * will be empty. Resources that the class loader doesn't have
  25. * access to will not be in the enumeration.
  26. *
  27. * @throws IOException
  28. * If I/O errors occur
  29. *
  30. * @see #findResources(String)
  31. *
  32. * @since 1.2
  33. */
  34. public Enumeration<URL> getResources(String name) throws IOException {
  35. @SuppressWarnings("unchecked")
  36. Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
  37. if (parent != null) {
  38. tmp[0] = parent.getResources(name);
  39. } else {
  40. tmp[0] = getBootstrapResources(name);
  41. }
  42. tmp[1] = findResources(name);
  43. return new CompoundEnumeration<>(tmp);
  44. }

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找到了, 则直接返回)

  1. /**
  2. * Finds the resource with the given name. A resource is some data
  3. * (images, audio, text, etc) that can be accessed by class code in a way
  4. * that is independent of the location of the code.
  5. *
  6. * <p> The name of a resource is a '<tt>/</tt>'-separated path name that
  7. * identifies the resource.
  8. *
  9. * <p> This method will first search the parent class loader for the
  10. * resource; if the parent is <tt>null</tt> the path of the class loader
  11. * built-in to the virtual machine is searched. That failing, this method
  12. * will invoke {@link #findResource(String)} to find the resource. </p>
  13. *
  14. * @apiNote When overriding this method it is recommended that an
  15. * implementation ensures that any delegation is consistent with the {@link
  16. * #getResources(java.lang.String) getResources(String)} method.
  17. *
  18. * @param name
  19. * The resource name
  20. *
  21. * @return A <tt>URL</tt> object for reading the resource, or
  22. * <tt>null</tt> if the resource could not be found or the invoker
  23. * doesn't have adequate privileges to get the resource.
  24. *
  25. * @since 1.1
  26. */
  27. public URL getResource(String name) {
  28. URL url;
  29. if (parent != null) {
  30. url = parent.getResource(name);
  31. } else {
  32. url = getBootstrapResource(name);
  33. }
  34. if (url == null) {
  35. url = findResource(name);
  36. }
  37. return url;
  38. }

测试

例如对于Maven工程,在main/resources目录下有个一个application.yml文件,在test/resources下也有一个applicaiton.yml文件。通过如下代码测试:

  1. public static void main(String[] args) throws IOException {
  2. ClassPathResource resource = new ClassPathResource("application.yml");
  3. System.out.println(resource.getClassLoader());
  4. System.out.println(IOUtils.toString(resource.getInputStream(), "utf-8"));
  5. }

在Test目录下运行时,IDEA指定的classpath顺序为:
target/test-classes: target/classes. 所以默认先搜索target/test-classes下的application.yml文件。

如果交换顺序,则先搜索target/classes下的application.yml文件。