diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleD/SomeConfigurationD.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleD/SomeConfigurationD.java new file mode 100644 index 000000000..282925b71 --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleD/SomeConfigurationD.java @@ -0,0 +1,34 @@ +/* + * Copyright 2018-2023 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 com.acme.myproject.moduleD; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.acme.myproject.moduleB.ServiceComponentB; +import com.acme.myproject.moduleE.ServiceComponentE; + +/** + * @author Oliver Drotbohm + */ +@Configuration +class SomeConfigurationD { + + @Bean + ServiceComponentE serviceComponentE() { + return null; + } +} diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleE/ServiceComponentE.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleE/ServiceComponentE.java new file mode 100644 index 000000000..d5bbc1c7a --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/moduleE/ServiceComponentE.java @@ -0,0 +1,24 @@ +/* + * Copyright 2018-2023 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 com.acme.myproject.moduleE; + +import org.springframework.stereotype.Component; + +/** + * @author Oliver Drotbohm + */ +@Component +public class ServiceComponentE {} diff --git a/spring-modulith-integration-test/src/test/java/com/acme/myproject/moduleD/ModuleDTest.java b/spring-modulith-integration-test/src/test/java/com/acme/myproject/moduleD/ModuleDTest.java new file mode 100644 index 000000000..b6714c2b4 --- /dev/null +++ b/spring-modulith-integration-test/src/test/java/com/acme/myproject/moduleD/ModuleDTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2023 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 com.acme.myproject.moduleD; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ConfigurableApplicationContext; + +import com.acme.myproject.NonVerifyingModuleTest; +import com.acme.myproject.moduleE.ServiceComponentE; + +/** + * @author Oliver Drotbohm + */ +@NonVerifyingModuleTest +class ModuleDTest { + + @Autowired ConfigurableApplicationContext context; + + @Test // GH-320 + void dropsManuallyDeclaredBeanOfNonIncludedModule() { + assertThat(context.getBeanNamesForType(ServiceComponentE.class)).isEmpty(); + } +} diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java index 7be119381..6f94c986c 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleContextCustomizerFactory.java @@ -24,13 +24,19 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.modulith.core.ApplicationModule; +import org.springframework.modulith.core.JavaPackage; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextAnnotationUtils; +import org.springframework.util.Assert; /** * @author Oliver Drotbohm @@ -53,7 +59,6 @@ public ContextCustomizer createContextCustomizer(Class testClass, static class ModuleContextCustomizer implements ContextCustomizer { private static final Logger LOGGER = LoggerFactory.getLogger(ModuleContextCustomizer.class); - private static final String BEAN_NAME = ModuleTestExecution.class.getName(); private final Supplier execution; @@ -73,7 +78,9 @@ public void customizeContext(ConfigurableApplicationContext context, MergedConte logModules(testExecution); var beanFactory = context.getBeanFactory(); - beanFactory.registerSingleton(BEAN_NAME, testExecution); + beanFactory.registerSingleton(ModuleTestExecution.class.getName(), testExecution); + beanFactory.registerSingleton(ModuleTestExecutionBeanDefinitionSelector.class.getName(), + new ModuleTestExecutionBeanDefinitionSelector(testExecution)); var events = new DefaultPublishedEvents(); beanFactory.registerSingleton(events.getClass().getName(), events); @@ -173,4 +180,78 @@ public int hashCode() { return Objects.hash(execution); } } + + /** + * A {@link BeanDefinitionRegistryPostProcessor} that selects + * {@link org.springframework.beans.factory.config.BeanDefinition}s that are either non-module beans (i.e. + * infrastructure) or beans living inside an {@link ApplicationModule} being part of the current + * {@link ModuleTestExecution}. + * + * @author Oliver Drotbohm + * @since 1.1 + */ + private static class ModuleTestExecutionBeanDefinitionSelector implements BeanDefinitionRegistryPostProcessor { + + private static final Logger LOGGER = LoggerFactory.getLogger(ModuleTestExecutionBeanDefinitionSelector.class); + + private final ModuleTestExecution execution; + + /** + * Creates a new {@link ModuleTestExecutionBeanDefinitionSelector} for the given {@link ModuleTestExecution}. + * + * @param execution must not be {@literal null}. + */ + private ModuleTestExecutionBeanDefinitionSelector(ModuleTestExecution execution) { + + Assert.notNull(execution, "ModuleTestExecution must not be null!"); + + this.execution = execution; + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry(org.springframework.beans.factory.support.BeanDefinitionRegistry) + */ + @Override + public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { + + if (!(registry instanceof ConfigurableListableBeanFactory factory)) { + return; + } + + var modules = execution.getModules(); + + for (String name : registry.getBeanDefinitionNames()) { + + var type = factory.getType(name, false); + var module = modules.getModuleByType(type); + + // Not a module type -> pass + if (module.isEmpty()) { + continue; + } + + var packagesIncludedInTestRun = execution.getBasePackages().toList(); + + // A type of a module bootstrapped -> pass + if (module.map(ApplicationModule::getBasePackage) + .map(JavaPackage::getName) + .filter(packagesIncludedInTestRun::contains).isPresent()) { + continue; + } + + LOGGER.trace("Dropping bean definition {} for type {} as it is not included in an application module to be bootstrapped!", name, type.getName()); + + // Remove bean definition from bootstrap + registry.removeBeanDefinition(name); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.beans.factory.config.BeanFactoryPostProcessor#postProcessBeanFactory(org.springframework.beans.factory.config.ConfigurableListableBeanFactory) + */ + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {} + } } diff --git a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java index 82b0ee5e4..05185ffcd 100644 --- a/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java +++ b/spring-modulith-test/src/main/java/org/springframework/modulith/test/ModuleTestExecution.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; import org.slf4j.Logger; @@ -227,7 +228,7 @@ private static Stream getExtraModules(ApplicationModuleTest a return Arrays.stream(annotation.extraIncludes()) // .map(modules::getModuleByName) // - .flatMap(it -> it.map(Stream::of).orElseGet(Stream::empty)); + .flatMap(Optional::stream); } private static record Key(String moduleBasePackage, ApplicationModuleTest annotation) {}