diff --git a/spring-modulith-runtime/pom.xml b/spring-modulith-runtime/pom.xml index a7dc20ef..cec44d4c 100644 --- a/spring-modulith-runtime/pom.xml +++ b/spring-modulith-runtime/pom.xml @@ -33,6 +33,16 @@ jgrapht-core 1.4.0 + + + com.jayway.jsonpath + json-path + + + + net.minidev + json-smart + org.springframework.boot @@ -60,5 +70,46 @@ + + + + + org.apache.maven.plugins + maven-shade-plugin + + + package + + shade + + + + + + com.jayway.jsonpath:* + net.minidev:* + + + + + + com.jayway.jsonpath + org.springframework.modulith.runtime.jsonpath + + + net.minidev + org.springframework.modulith.runtime.minidev + + + + ${project.build.directory}/dependency-reduced-pom.xml + true + + + + + + + diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/ApplicationModuleInitializerInvoker.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/ApplicationModuleInitializerInvoker.java new file mode 100644 index 00000000..a4c89acf --- /dev/null +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/ApplicationModuleInitializerInvoker.java @@ -0,0 +1,28 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.runtime.autoconfigure; + +import java.util.stream.Stream; + +import org.springframework.modulith.ApplicationModuleInitializer; + +/** + * @author Oliver Drotbohm + */ +interface ApplicationModuleInitializerInvoker { + + void invokeInitializers(Stream initializers); +} diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/DefaultApplicationModuleInitializerInvoker.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/DefaultApplicationModuleInitializerInvoker.java new file mode 100644 index 00000000..e4fbfb51 --- /dev/null +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/DefaultApplicationModuleInitializerInvoker.java @@ -0,0 +1,53 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.runtime.autoconfigure; + +import java.util.function.Supplier; +import java.util.stream.Stream; + +import org.springframework.modulith.ApplicationModuleInitializer; +import org.springframework.modulith.core.ApplicationModules; + +/** + * @author Oliver Drotbohm + */ +class DefaultApplicationModuleInitializerInvoker implements ApplicationModuleInitializerInvoker { + + private final Supplier modules; + + /** + * @param modules + */ + DefaultApplicationModuleInitializerInvoker(Supplier modules) { + this.modules = modules; + } + + /* + * + * (non-Javadoc) + * @see org.springframework.modulith.runtime.autoconfigure.ApplicationModuleInitializerInvoker#invokeInitializers(java.util.stream.Stream) + */ + @Override + public void invokeInitializers(Stream initializers) { + + var modules = this.modules.get(); + + initializers + .sorted(modules.getComparator()) // + .map(it -> LoggingApplicationModuleInitializerAdapter.of(it, modules)) + .forEach(ApplicationModuleInitializer::initialize); + } +} diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/LoggingApplicationModuleInitializerAdapter.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/LoggingApplicationModuleInitializerAdapter.java new file mode 100644 index 00000000..6bce4f5c --- /dev/null +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/LoggingApplicationModuleInitializerAdapter.java @@ -0,0 +1,97 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.runtime.autoconfigure; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.aop.support.AopUtils; +import org.springframework.modulith.ApplicationModuleInitializer; +import org.springframework.modulith.core.ApplicationModules; +import org.springframework.modulith.core.FormattableType; +import org.springframework.util.Assert; + +class LoggingApplicationModuleInitializerAdapter implements ApplicationModuleInitializer { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggingApplicationModuleInitializerAdapter.class); + + private final ApplicationModuleInitializer delegate; + private final String label; + + /** + * Creates a new {@link LoggingApplicationModuleInitializerAdapter} for the given {@link ApplicationModuleInitializer} + * and identifier. + * + * @param delegate must not be {@literal null}. + * @param label must not be {@literal null}. + */ + private LoggingApplicationModuleInitializerAdapter(ApplicationModuleInitializer delegate, String label) { + + Assert.notNull(delegate, "ApplicationModuleInitializer must not be null!"); + Assert.hasText(label, "Label must not be null or empty!"); + + this.delegate = delegate; + this.label = label; + } + + public static ApplicationModuleInitializer of(ApplicationModuleInitializer initializer, + ApplicationModules modules) { + + if (!LoggerFactory.getLogger(initializer.getClass()).isDebugEnabled()) { + return initializer; + } + + var listenerType = AopUtils.getTargetClass(initializer); + var formattable = FormattableType.of(listenerType); + + var formattedListenerType = modules.getModuleByType(listenerType) + .map(formattable::getAbbreviatedFullName) + .orElseGet(formattable::getAbbreviatedFullName); + + return new LoggingApplicationModuleInitializerAdapter(initializer, formattedListenerType); + } + + public static ApplicationModuleInitializer of(ApplicationModuleInitializer initializer) { + + if (!LoggerFactory.getLogger(initializer.getClass()).isDebugEnabled()) { + return initializer; + } + + var listenerType = AopUtils.getTargetClass(initializer); + var abbreviatedPackage = Stream.of(listenerType.getPackageName().split("\\.")) + .map(it -> it.substring(0, 1)) + .collect(Collectors.joining(".")); + + return new LoggingApplicationModuleInitializerAdapter(initializer, + abbreviatedPackage + "." + listenerType.getSimpleName()); + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.ApplicationModuleInitializer#initialize() + */ + @Override + public void initialize() { + + LOGGER.debug("Initializing {}.", label); + + delegate.initialize(); + + LOGGER.debug("Initializing {} done.", label); + } +} diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvoker.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvoker.java new file mode 100644 index 00000000..7f42c095 --- /dev/null +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvoker.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.runtime.autoconfigure; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.springframework.core.io.Resource; +import org.springframework.modulith.ApplicationModuleInitializer; +import org.springframework.util.Assert; + +import com.jayway.jsonpath.JsonPath; + +class PrecomputedApplicationModuleInitializerInvoker implements ApplicationModuleInitializerInvoker { + + private List plan; + + public PrecomputedApplicationModuleInitializerInvoker(Resource metadata) { + + Assert.isTrue(metadata.exists(), () -> "Resource %s does not exist!".formatted(metadata.getDescription())); + + try (var stream = metadata.getInputStream()) { + + this.plan = JsonPath.parse(stream).> read("$..initializers[*]"); + + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.runtime.autoconfigure.ApplicationModuleInitializerInvoker#invokeInitializers(java.util.stream.Stream) + */ + @Override + public void invokeInitializers(Stream initializers) { + + var map = initializers + .collect(Collectors.toMap(it -> it.getClass().getName(), Function.identity())); + + plan.stream() + .map(map::get) + .map(Optional::ofNullable) + .flatMap(Optional::stream) + .map(LoggingApplicationModuleInitializerAdapter::of) + .forEach(ApplicationModuleInitializer::initialize); + } +} diff --git a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java index d757ff60..e6c378bc 100644 --- a/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java +++ b/spring-modulith-runtime/src/main/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfiguration.java @@ -17,8 +17,8 @@ package org.springframework.modulith.runtime.autoconfigure; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -28,7 +28,9 @@ import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Role; +import org.springframework.core.io.Resource; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; @@ -36,10 +38,9 @@ import org.springframework.modulith.ApplicationModuleInitializer; import org.springframework.modulith.core.ApplicationModule; import org.springframework.modulith.core.ApplicationModules; import org.springframework.modulith.core.ApplicationModulesFactory; -import org.springframework.modulith.core.FormattableType; +import org.springframework.modulith.core.util.ApplicationModulesExporter; import org.springframework.modulith.runtime.ApplicationModulesRuntime; import org.springframework.modulith.runtime.ApplicationRuntime; -import org.springframework.util.Assert; import org.springframework.util.function.ThrowingSupplier; /** @@ -51,10 +52,10 @@ import org.springframework.util.function.ThrowingSupplier; @AutoConfiguration class SpringModulithRuntimeAutoConfiguration { - private static final Logger LOGGER = LoggerFactory.getLogger(SpringModulithRuntimeAutoConfiguration.class); private static final AsyncTaskExecutor EXECUTOR = new SimpleAsyncTaskExecutor(); @Bean + @Lazy @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnMissingBean(ApplicationRuntime.class) static ApplicationRuntime modulithsApplicationRuntime(ApplicationContext context) { @@ -62,6 +63,7 @@ class SpringModulithRuntimeAutoConfiguration { } @Bean + @Lazy @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnMissingBean static ApplicationModulesRuntime modulesRuntime(ApplicationRuntime runtime) { @@ -77,64 +79,20 @@ class SpringModulithRuntimeAutoConfiguration { @Role(BeanDefinition.ROLE_INFRASTRUCTURE) @ConditionalOnBean(ApplicationModuleInitializer.class) static ApplicationListener applicationModuleInitializingListener( - ObjectProvider runtime, - ObjectProvider initializers) { - - return event -> { - - var modules = runtime.getObject().get(); - - initializers.stream() // - .sorted(modules.getComparator()) // - .map(it -> LOGGER.isDebugEnabled() ? new LoggingApplicationModuleInitializerAdapter(it, modules) : it) - .forEach(ApplicationModuleInitializer::initialize); - }; + ApplicationModuleInitializerInvoker invoker, ObjectProvider initializers) { + return __ -> invoker.invokeInitializers(initializers.stream()); } - private static class LoggingApplicationModuleInitializerAdapter implements ApplicationModuleInitializer { + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + @ConditionalOnBean(ApplicationModuleInitializer.class) + static ApplicationModuleInitializerInvoker applicationModuleInitializerInvoker( + @Value("classpath:" + ApplicationModulesExporter.DEFAULT_LOCATION) Resource metadata, + ObjectProvider runtime) { - private static final Logger LOGGER = LoggerFactory.getLogger(LoggingApplicationModuleInitializerAdapter.class); - - private final ApplicationModuleInitializer delegate; - private final ApplicationModules modules; - - /** - * Creates a new {@link LoggingApplicationModuleInitializerAdapter} for the given - * {@link ApplicationModuleInitializer} and {@link ApplicationModule}. - * - * @param delegate must not be {@literal null}. - * @param modules must not be {@literal null}. - */ - public LoggingApplicationModuleInitializerAdapter(ApplicationModuleInitializer delegate, - ApplicationModules modules) { - - Assert.notNull(delegate, "ApplicationModuleInitializer must not be null!"); - Assert.notNull(modules, "ApplicationModules must not be null!"); - - this.delegate = delegate; - this.modules = modules; - } - - /* - * (non-Javadoc) - * @see org.springframework.modulith.ApplicationModuleInitializer#initialize() - */ - @Override - public void initialize() { - - var listenerType = AopUtils.getTargetClass(delegate); - var formattable = FormattableType.of(listenerType); - - var formattedListenerType = modules.getModuleByType(listenerType) - .map(formattable::getAbbreviatedFullName) - .orElseGet(formattable::getAbbreviatedFullName); - - LOGGER.debug("Initializing {}.", formattedListenerType); - - delegate.initialize(); - - LOGGER.debug("Initializing {} done.", formattedListenerType); - } + return metadata.exists() + ? new PrecomputedApplicationModuleInitializerInvoker(metadata) + : new DefaultApplicationModuleInitializerInvoker(runtime.getIfAvailable()); } private static class ApplicationModulesBootstrap { @@ -165,7 +123,7 @@ class SpringModulithRuntimeAutoConfiguration { LOGGER.debug("Detected {} application modules: {}", // numberOfModules, // - result.stream().map(ApplicationModule::getName).toList()); + result.stream().map(ApplicationModule::getIdentifier).toList()); } return result; diff --git a/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvokerUnitTests.java b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvokerUnitTests.java new file mode 100644 index 00000000..00f32284 --- /dev/null +++ b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/PrecomputedApplicationModuleInitializerInvokerUnitTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.modulith.runtime.autoconfigure; + +import static org.assertj.core.api.Assertions.*; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.springframework.core.io.ClassPathResource; +import org.springframework.modulith.ApplicationModuleInitializer; + +/** + * @author Oliver Drotbohm + */ +class PrecomputedApplicationModuleInitializerInvokerUnitTests { + + @Test + void ordersInitializersBasedOnPrecomputedMetadata() throws IOException { + + var resource = new ClassPathResource("application-modules.json"); + var invoker = new PrecomputedApplicationModuleInitializerInvoker(resource); + + var second = new SecondApplicationModuleInitializer(); + var first = new FirstApplicationModuleInitializer(); + + invoker.invokeInitializers(Stream.of(second, first)); + + assertThat(first.invoked).isBefore(second.invoked); + } + + static class FirstApplicationModuleInitializer implements ApplicationModuleInitializer { + + LocalDateTime invoked; + + @Override + public void initialize() { + invoked = LocalDateTime.now(); + } + } + + static class SecondApplicationModuleInitializer implements ApplicationModuleInitializer { + + LocalDateTime invoked; + + @Override + public void initialize() { + invoked = LocalDateTime.now(); + } + } +} diff --git a/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java index 405542cb..8cbd1414 100644 --- a/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java +++ b/spring-modulith-runtime/src/test/java/org/springframework/modulith/runtime/autoconfigure/SpringModulithRuntimeAutoConfigurationIntegrationTests.java @@ -16,11 +16,22 @@ package org.springframework.modulith.runtime.autoconfigure; import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.HashMap; +import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeanInstantiationException; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.event.ApplicationStartedEvent; import org.springframework.boot.test.context.FilteredClassLoader; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.modulith.ApplicationModuleInitializer; @@ -70,15 +81,69 @@ class SpringModulithRuntimeAutoConfigurationIntegrationTests { @Test // GH-375 void registersInitializingListenerIfInitializersPresent() { + var tracker = new InitializationTracker(); + var beanName = "applicationModuleInitializingListener"; var runner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(SpringModulithRuntimeAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(SpringModulithRuntimeAutoConfiguration.class)) + .withBean(InitializationTracker.class, () -> tracker); // No initializer -> no listener - runner.run(context -> assertThat(context).doesNotHaveBean(beanName)); + runner.run(context -> { + + assertThat(context).doesNotHaveBean(beanName); + tracker.assertBeanNotInitialized(ApplicationModulesRuntime.class); + }); // Initializer -> listener runner.withBean(ApplicationModuleInitializer.class, () -> () -> {}) - .run(context -> assertThat(context).hasBean(beanName)); + .withBean(DummyApplication.class) + .run(context -> { + + context.publishEvent(mock(ApplicationStartedEvent.class)); + assertThat(context).hasBean(beanName); + }); } + + static class InitializationTracker implements BeanPostProcessor, BeanFactoryAware { + + private final Map> initializedBeans = new HashMap<>(); + private ListableBeanFactory beanFactory; + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = (ListableBeanFactory) beanFactory; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.BeanPostProcessor#postProcessBeforeInitialization(java.lang.Object, java.lang.String) + */ + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + + initializedBeans.put(beanName, AopUtils.getTargetClass(bean)); + + return bean; + } + + public void assertBeanInitialized(Class type) { + assertThat(initializedBeans).containsValue(type); + } + + public void assertBeanNotInitialized(Class type) { + + var names = beanFactory.getBeanNamesForType(type); + + assertThat(names).isNotEmpty(); + assertThat(initializedBeans).doesNotContainKeys(names); + } + } + + @SpringBootApplication + static class DummyApplication {} } diff --git a/spring-modulith-runtime/src/test/resources/application-modules.json b/spring-modulith-runtime/src/test/resources/application-modules.json new file mode 100644 index 00000000..2d00a237 --- /dev/null +++ b/spring-modulith-runtime/src/test/resources/application-modules.json @@ -0,0 +1,8 @@ +{ + "a" : { + "initializers" : [ "org.springframework.modulith.runtime.autoconfigure.PrecomputedApplicationModuleInitializerInvokerUnitTests$FirstApplicationModuleInitializer" ] + }, + "b" : { + "initializers" : [ "org.springframework.modulith.runtime.autoconfigure.PrecomputedApplicationModuleInitializerInvokerUnitTests$SecondApplicationModuleInitializer" ] + } +}