【问题标题】:@ComponentScan does not detect classes in module path@ComponentScan 未检测到模块路径中的类
【发布时间】:2019-12-03 20:44:44
【问题描述】:

我正在使用 Java 11 和 Spring 开发 JavaFX 应用程序。应用程序模块使用 jlink 与自定义 JRE 捆绑在一起,它只允许将命名模块包含在捆绑包中。由于 Spring 不提供命名模块,而是依赖自动模块来实现 Java 9 模块系统支持,因此我使用 moditect 将模块描述符 (module-info.java) 添加到 Spring JAR。

编译、jlinking 和执行应用程序没有任何问题。但是,尽管我的 AppConfig 类使用 @ComponentScan 注释,但 Spring 没有检测到我的应用程序的任何使用 @Component 注释的类:

@Configuration
@ComponentScan
public class AppConfig {
}

Main 中,我基于AppConfig 创建一个AnnotationConfigApplicationContext 并打印所有注册的bean 以及类路径上可用的资源:

public class Main extends Application {

    private ConfigurableApplicationContext applicationContext;

    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void init() {
        applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
    }

    @Override
    public void start(Stage mainWindow) throws IOException {
        System.out.println("Printing beans: " + applicationContext.getBeanDefinitionNames().length);
        for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
            System.out.println(beanDefinitionName);
        }

        PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
        System.out.println(Arrays.toString(resolver.getResources("classpath*:com/myapp/**/*.class")));
    }

    @Override
    public void stop() {
        applicationContext.stop();
    }
}

如果我使用 IntelliJ 运行应用程序,PathMatchingResourcePatternResolver 会在类路径上找到我的所有类(我猜是因为 IntelliJ 使用类路径而不是模块路径运行应用程序)。因此,通过组件扫描检测所有组件并创建相应的 bean:

Printing beans: 8
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalPersistenceAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
appConfig
com.myapp.services.UserServiceImpl
com.myapp.services.BookingServiceImpl
[file [/Users/user/myapp/target/classes/com/myapp/AppConfig.class], file [/Users/user/myapp/target/classes/com/myapp/Main.class], file [/Users/user/myapp/target/classes/com/myapp/services/UserService.class], file [/Users/user/myapp/target/classes/com/myapp/services/UserServiceImpl.class], file [/Users/user/myapp/target/classes/com/myapp/services/BookingService.class], file [/Users/user/myapp/target/classes/com/myapp/services/BookingServiceImpl.class]]

但如果我通过 jlink'ed 包运行应用程序,即在使用模块路径的自定义 JRE 上,Spring 无法检测到我的任何类:

Printing beans: 5
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
appConfig
[]

PathMatchingResourcePatternResolver 找不到任何类(因为现在所有内容都位于模块路径上),并且组件扫描没有实例化一个 bean。

如果我手动将组件类导入AppConfig,bean 会正确创建并通过@Autowired 注入:

@Configuration
@Import({
        com.myapp.service.UserServiceImpl.class,
        com.myapp.service.BookingServiceImpl.class
})
public class AppConfig {
}

为什么 Spring 在使用 @Import 时能够创建 bean,但无法通过 @ComponentScan 检测到它们?如何通过@ComponentScan 解析我的组件?

【问题讨论】:

  • 你好@IggyBlob,你在这方面有什么进展吗?我几乎在同一个洞里......
  • @Mumrah81 我最终设法通过使用这个 Gradle 插件解决了这个问题:github.com/beryx/badass-runtime-plugin。它使用以下步骤为非模块化应用程序创建自定义运行时映像:1)创建经典(Java-9 之前的样式)分发(即,还包含非模块化 Spring 依赖项的胖 JAR)2)使用 jlink创建仅包含应用程序所需模块的自定义 JRE 3) 将 pre-Java-9 样式的发行版合并到自定义 JRE 中,并使用适当的 JVM 设置标志(例如 --module-path)调整启动脚本。

标签: java spring java-11 java-module jlink


【解决方案1】:

除了问题的 cmets 中 @IggyBlob 提供的解决方案之外,另一种可能的解决方案是修补 PathMatchingResourcePatternResolver 以将资源搜索到模块路径中,至少直到发布完全符合模块的 Spring 版本。

一种可能的实现方式:

public class PathMatchingResourcePatternResolverJigsaw extends PathMatchingResourcePatternResolver {

    public PathMatchingResourcePatternResolverJigsaw() {
    }

    public PathMatchingResourcePatternResolverJigsaw(ResourceLoader resourceLoader) {
        super(resourceLoader);
    }

    public PathMatchingResourcePatternResolverJigsaw(ClassLoader classLoader) {
        super(classLoader);
    }

    public List<Resource> getResourcesFromModules(String locationPattern) throws IOException {
        String pattern = locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length());
        List<Resource> list = new ArrayList<>();
        ModuleLayer.boot().configuration().modules().stream()
         .map(ResolvedModule::reference)
         .forEach(mref -> {
             try (ModuleReader reader = mref.open()) {
                 list.addAll(reader.list()
                    .filter(p -> getPathMatcher().match(pattern, p))
                    .map(p -> {
                        try {
                            return convertClassLoaderURL(reader.find(p).get().toURL());
                        } catch (Exception e) {
                            return null;
                        }
                    })
                    .collect(Collectors.toList()));
            } catch (IOException ioe) {
                 throw new UncheckedIOException(ioe);
            }
         });
        return list;
    }
    @Override
    public Resource[] getResources(String locationPattern) throws IOException {
        boolean addModSearch = true;

        Resource[] result = super.getResources(locationPattern);
        if (addModSearch && locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
            List<Resource> list = getResourcesFromModules(locationPattern);
            list.addAll(Arrays.asList(result));
            result = list.toArray(new Resource[0]);
        }

        return result;
    }
}

并使用以下命令初始化您的 spring 应用程序上下文:

private ConfigurableApplicationContext context;

    @Override
    public void init() throws Exception {
        ApplicationContextInitializer<GenericApplicationContext> initializer = new ApplicationContextInitializer<GenericApplicationContext>() {
            @Override
            public void initialize(GenericApplicationContext genericApplicationContext) {
                genericApplicationContext.setResourceLoader(new PathMatchingResourcePatternResolverJigsaw());
            }
        };

        this.context = new SpringApplicationBuilder().sources(MyApplication.class)
                .initializers(initializer)
                .build().run(new String[0]);
    }

这只是一个丑陋的解决方法,因此请谨慎使用

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-11-11
    • 2013-04-25
    • 1970-01-01
    • 2021-10-02
    • 1970-01-01
    • 1970-01-01
    • 2015-09-19
    • 2014-05-21
    相关资源
    最近更新 更多