Description

I’m encountering an issue with component scanning after upgrading from Spring 6.1.16 to 6.2.11. The problem occurs only on Windows, while the same codebase works correctly on Linux.

After the upgrade, beans that should be automatically detected and registered in the Spring context through component scanning are no longer visible. As a result, the application fails to start with the following error:

No qualifying bean of type [...]

When I explicitly define the affected beans in a @Configuration class, they are successfully loaded on Windows as well. This suggests that the issue is limited to the component scanning mechanism.

The issue persists in version 6.12.2 as well

Additional Context

From the 6.2 release notes:

Classpath scanning comes with internal JAR caching now, which is beneficial for repeated component scan attempts. However, this can cause regressions if such caching is unexpected. As of 6.2.6, you may revert to the previous behavior through setUseCaches(false) on PathMatchingResourcePatternResolver.

Applying this setting in my project resolves the issue — component scanning starts working again on Windows.

Comment From: jhoeller

Looks like certain scan attempts simply do not find any matching classes there, not registering the corresponding beans for that reason. The actual problem is certainly in the component scanner - in particular given that setUseCaches(false) helps.

What kind of component-scan instructions do you have in your configuration, and how many? Are their base-package specifications overlapping (for example different subpackages of the same top-level package) or pointing to separate packages in the same jar file?

If you have a suspicion about a particular scan attempt that does not find matching classes, you could try to debug it: stepping through the corresponding code in PathMatchingResourcePatternResolver and checking what kind of cached entry paths it is trying to match the given path against. Maybe there is just a minor mismatch in path separators or path prefixes (which could easily be Windows-specific) in your scenario that we need to take into account in the matching algorithm.

Alternatively, you could try to extract a minimal repro project that fails to find matching classes on Windows.

Comment From: ansokpl

What kind of component-scan instructions do you have in your configuration, and how many? Are their base-package specifications overlapping (for example different subpackages of the same top-level package) or pointing to separate packages in the same jar file?

Initialization is handled as follows. This is the only component scan defined within this package:

package com.aaa.bbb.ccc.ddd;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.aaa.bbb.ccc.ddd")
class SpringInitializer {
}

We use component scanning sparingly across our codebase. We never scan both com.aaa.bbb and com.aaa.bbb.ccc. We usually scan com.aaa.bbb.ccc and com.adva.bbb.ccc1

If you have a suspicion about a particular scan attempt that does not find matching classes, you could try to debug it: stepping through the corresponding code in PathMatchingResourcePatternResolver and checking what kind of cached entry paths it is trying to match the given path against. Maybe there is just a minor mismatch in path separators or path prefixes (which could easily be Windows-specific) in your scenario that we need to take into account in the matching algorithm.

I will try that. Alternatively, you could try to extract a minimal repro project that fails to find matching classes on Windows.

Comment From: jhoeller

Ok so this sounds like you are scanning several subpackages of the same root package, assumably in the same jar.

Within PathMatchingResourcePatternResolver, this involves the rootDirCache (used in the findPathMatchingResources method) as well as the jarEntriesCache (used in doFindPathMatchingJarResources method). You could put breakpoints into those methods and see what they end up doing against those caches.

Since we have a file system matching problem here, rootDirCache and its base directory matching in findPathMatchingResources might have a bug against Windows-style paths. For multiple independent scan attempts against different base packages in the same jar file, it would rather be doFindPathMatchingJarResources to blame.

Comment From: ansokpl

Thanks for the helpful hint! During debugging, I discovered that the issue isn’t caused by differences in path handling between Windows and Linux, but rather by the Wrapper.jar used in our test Windows environment.

The Wrapper.jar contains a manifest file with a Class-Path entry that lists absolute paths to other JAR files. When I used the same JAR on Linux, the issue appeared there as well, confirming that the problem is related to those absolute paths rather than the operating system.

Example manifest snippet:

Manifest-Version: 1.0
Main-Class: com.example.Main
Class-Path: /home/ansok/spring/libs/core.jar /home/ansok/spring/libs/utils.jar /home/ansok/spring/libs/config.jar

Further debugging led me to the following piece of code responsible for reading classpath entries from the JAR’s manifest:

private Set<ClassPathManifestEntry> getClassPathManifestEntriesFromJar(File jar) throws IOException {
    URL base = jar.toURI().toURL();
    File parent = jar.getAbsoluteFile().getParentFile();
    try (JarFile jarFile = new JarFile(jar)) {
        Manifest manifest = jarFile.getManifest();
        Attributes attributes = (manifest != null ? manifest.getMainAttributes() : null);
        String classPath = (attributes != null ? attributes.getValue(Name.CLASS_PATH) : null);
        Set<ClassPathManifestEntry> manifestEntries = new LinkedHashSet<>();

        if (StringUtils.hasLength(classPath)) {
            StringTokenizer tokenizer = new StringTokenizer(classPath);
            while (tokenizer.hasMoreTokens()) {
                String path = tokenizer.nextToken();
                if (path.indexOf(':') >= 0 && !"file".equalsIgnoreCase(new URL(base, path).getProtocol())) {
                    // See jdk.internal.loader.URLClassPath.JarLoader.tryResolveFile(URL, String)
                    continue;
                }
                File candidate = new File(parent, path);
                if (candidate.isFile() && candidate.getCanonicalPath().contains(parent.getCanonicalPath())) {
                    manifestEntries.add(ClassPathManifestEntry.of(candidate, this.useCaches));
                }
            }
        }

        return Collections.unmodifiableSet(manifestEntries);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Failed to load manifest entries from jar file '" + jar + "': " + ex);
        }
        return Collections.emptySet();
    }
}

In debug mode, I observed the following values being used in this line: File candidate = new File(parent, path); where

parent = "/home/ansok/wrapper"
path   = "file:/home/ansok/spring/libs/core.jar"

As a result, candidate.isFile() returns false, and the JAR is not added to manifestEntries.