diff --git a/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java b/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java index e09bb078..07b9cd32 100644 --- a/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java +++ b/spring-modulith-api/src/main/java/org/springframework/modulith/ApplicationModule.java @@ -52,4 +52,37 @@ public @interface ApplicationModule { * @see NamedInterface */ String[] allowedDependencies() default { OPEN_TOKEN }; + + /** + * Declares the {@link Type} of the {@link ApplicationModule} + * + * @return will never be {@literal null}. + * @since 1.2 + */ + Type type() default Type.CLOSED; + + /** + * The type of an application module + * + * @author Oliver Drotbohm + * @since 1.2 + */ + enum Type { + + /** + * A closed application module exposes an API to other modules, but also allows to hide internals. Access to those + * internals from other modules is sanctioned. Also, closed application modules must not be part of dependency + * cycles. + * + * @see NamedInterface + */ + CLOSED, + + /** + * An open application module does not hide its internals, which means that access to those from other modules is + * not sanctioned. They are also excluded from the cycle detection algorithm. All types contained in an open module + * are part of the unnamed named interface. + */ + OPEN; + } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java index f0bc91e9..d17b23e2 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java @@ -86,7 +86,9 @@ public class ApplicationModule { this.basePackage = basePackage; this.information = ApplicationModuleInformation.of(basePackage); - this.namedInterfaces = NamedInterfaces.discoverNamedInterfaces(basePackage); + this.namedInterfaces = isOpen() + ? NamedInterfaces.forOpen(basePackage) + : NamedInterfaces.discoverNamedInterfaces(basePackage); this.useFullyQualifiedModuleNames = useFullyQualifiedModuleNames; this.springBeans = SingletonSupplier.of(() -> filterSpringBeans(basePackage)); @@ -320,7 +322,7 @@ public class ApplicationModule { /** * Returns whether the module is considered a root one, i.e., it is an artificial one created for each base package * configured. - * + * * @return whether the module is considered a root one. * @since 1.1 */ @@ -350,7 +352,13 @@ public class ApplicationModule { public String toString(@Nullable ApplicationModules modules) { - var builder = new StringBuilder("# ").append(getDisplayName()).append("\n"); + var builder = new StringBuilder("# ").append(getDisplayName()); + + if (isOpen()) { + builder.append(" (open)"); + } + + builder.append("\n"); builder.append("> Logical name: ").append(getName()).append('\n'); builder.append("> Base package: ").append(basePackage.getName()).append('\n'); @@ -454,6 +462,16 @@ public class ApplicationModule { || packageName.startsWith(basePackageName + "."); } + /** + * Returns whether the module is considered open. + * + * @see org.springframework.modulith.ApplicationModule.Type + * @since 1.2 + */ + boolean isOpen() { + return information.isOpen(); + } + /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) @@ -964,8 +982,8 @@ public class ApplicationModule { var originModule = getExistingModuleOf(source, modules); var targetModule = getExistingModuleOf(target, modules); - DeclaredDependencies declaredDependencies = originModule.getDeclaredDependencies(modules); - Violations violations = Violations.NONE; + var declaredDependencies = originModule.getDeclaredDependencies(modules); + var violations = Violations.NONE; // Check explicitly defined allowed targets if (!declaredDependencies.isAllowedDependency(target)) { @@ -979,6 +997,10 @@ public class ApplicationModule { // No explicitly allowed dependencies - check for general access + if (targetModule.isOpen()) { + return violations; + } + if (!targetModule.isExposed(target)) { var violationText = "Module '%s' depends on non-exposed type %s within module '%s'!" diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleInformation.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleInformation.java index 1773fe93..72012bbf 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleInformation.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModuleInformation.java @@ -21,8 +21,8 @@ import java.util.Optional; import java.util.function.Supplier; import java.util.stream.Stream; -import org.jmolecules.ddd.annotation.Module; import org.springframework.modulith.ApplicationModule; +import org.springframework.modulith.ApplicationModule.Type; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -68,6 +68,14 @@ interface ApplicationModuleInformation { */ List getDeclaredDependencies(); + /** + * Returns whether the module is considered open. + * + * @see org.springframework.modulith.ApplicationModule.Type + * @since 1.2 + */ + boolean isOpen(); + /** * An {@link ApplicationModuleInformation} for the jMolecules {@link Module} annotation. * @@ -111,6 +119,15 @@ interface ApplicationModuleInformation { public List getDeclaredDependencies() { return List.of(ApplicationModule.OPEN_TOKEN); } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.core.ApplicationModuleInformation#isOpenModule() + */ + @Override + public boolean isOpen() { + return false; + } } /** @@ -167,5 +184,14 @@ interface ApplicationModuleInformation { .orElse(Stream.of(ApplicationModule.OPEN_TOKEN)) // .toList(); } + + /* + * (non-Javadoc) + * @see org.springframework.modulith.core.ApplicationModuleInformation#isOpenModule() + */ + @Override + public boolean isOpen() { + return annotation.map(it -> it.type().equals(Type.OPEN)).orElse(false); + } } } diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java index 3b657e70..b36cca73 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java @@ -24,6 +24,7 @@ import static java.util.stream.Collectors.*; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -810,6 +811,7 @@ public class ApplicationModules implements Iterable { public SliceIdentifier getIdentifierOf(JavaClass javaClass) { return getModuleByType(javaClass) + .filter(Predicate.not(ApplicationModule::isOpen)) .map(ApplicationModule::getName) .map(SliceIdentifier::of) .orElse(SliceIdentifier.ignore()); diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java index d82ba013..f2104482 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterface.java @@ -44,6 +44,8 @@ public class NamedInterface implements Iterable { static final String UNNAMED_NAME = "<>"; private static final DescribedPredicate ANNOTATED_NAMED_INTERFACE = // isAnnotatedWith(org.springframework.modulith.NamedInterface.class); + private static final DescribedPredicate ANNOTATED_NAMED_INTERFACE_PACKAGE = // + residesInPackageAnnotatedWith(org.springframework.modulith.NamedInterface.class); private final String name; private final Classes classes; @@ -98,22 +100,26 @@ public class NamedInterface implements Iterable { * @param javaPackage must not be {@literal null}. * @return will never be {@literal null}. */ - static NamedInterface unnamed(JavaPackage javaPackage) { + static NamedInterface unnamed(JavaPackage javaPackage, boolean flatten) { - var basePackageClasses = javaPackage.toSingle().getExposedClasses(); + var basePackageClasses = (flatten ? javaPackage.toSingle() : javaPackage).getExposedClasses(); - // Types that declare the annotation but no explicit name - var withDefaultedNamedInterface = basePackageClasses.stream() - .filter(ANNOTATED_NAMED_INTERFACE) - .filter(NamedInterface::withDefaultedNamedInterface) - .toList(); + if (flatten) { - // Illegal in the base package - Assert.state(withDefaultedNamedInterface.isEmpty(), - () -> "Cannot use named interface defaulting for type(s) %s located in base package!" - .formatted(FormatableType.format(withDefaultedNamedInterface))); + // Types that declare the annotation but no explicit name + var withDefaultedNamedInterface = basePackageClasses.stream() + .filter(ANNOTATED_NAMED_INTERFACE) + .filter(NamedInterface::withDefaultedNamedInterface) + .toList(); - return new NamedInterface(UNNAMED_NAME, basePackageClasses.that(not(ANNOTATED_NAMED_INTERFACE))); + // Illegal in the base package + Assert.state(withDefaultedNamedInterface.isEmpty(), + () -> "Cannot use named interface defaulting for type(s) %s located in base package!" + .formatted(FormatableType.format(withDefaultedNamedInterface))); + } + + return new NamedInterface(UNNAMED_NAME, basePackageClasses + .that(not(ANNOTATED_NAMED_INTERFACE_PACKAGE).and(not(ANNOTATED_NAMED_INTERFACE)))); } /** diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java index 42a8d2d7..bd43ef6c 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/NamedInterfaces.java @@ -63,7 +63,7 @@ public class NamedInterfaces implements Iterable { */ static NamedInterfaces discoverNamedInterfaces(JavaPackage basePackage) { - return NamedInterfaces.of(NamedInterface.unnamed(basePackage)) + return NamedInterfaces.of(NamedInterface.unnamed(basePackage, true)) .and(ofAnnotatedPackages(basePackage)) .and(ofAnnotatedTypes(basePackage)); } @@ -94,6 +94,21 @@ public class NamedInterfaces implements Iterable { .collect(Collectors.collectingAndThen(Collectors.toList(), NamedInterfaces::of)); } + /** + * Creates a new {@link NamedInterface} consisting of the unnamed one containing all classes in the given + * {@link JavaPackage}. + * + * @param basePackage must not be {@literal null}. + * @return will never be {@literal null}. + * @since 1.2 + */ + static NamedInterfaces forOpen(JavaPackage basePackage) { + + return NamedInterfaces.of(NamedInterface.unnamed(basePackage, false)) + .and(ofAnnotatedPackages(basePackage)) + .and(ofAnnotatedTypes(basePackage)); + } + /** * Returns whether at least one explicit {@link NamedInterface} is declared. * diff --git a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java index 7b037a7e..1c9a1a90 100644 --- a/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java +++ b/spring-modulith-core/src/main/java/org/springframework/modulith/core/Types.java @@ -20,6 +20,7 @@ import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*; import java.lang.annotation.Annotation; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import com.tngtech.archunit.base.DescribedPredicate; @@ -145,7 +146,7 @@ class Types { } } - static DescribedPredicate isAnnotatedWith(Class type) { + static DescribedPredicate isAnnotatedWith(Class type) { return isAnnotatedWith(type.getName()); } @@ -153,4 +154,24 @@ class Types { return Predicates.annotatedWith(type) // .or(Predicates.metaAnnotatedWith(type)); } + + /** + * Creates a new {@link DescribedPredicate} to match classes + * + * @param type must not be {@literal null}. + * @return will never be {@literal null}. + * @since 1.2 + */ + static DescribedPredicate residesInPackageAnnotatedWith(Class type) { + + Assert.notNull(type, "Annotation type must not be null!"); + + return new DescribedPredicate("resides in a package annotated with", type) { + + @Override + public boolean test(JavaClass t) { + return t.getPackage().isMetaAnnotatedWith(type); + } + }; + } } diff --git a/spring-modulith-core/src/test/java/example/ni/internal/Internal.java b/spring-modulith-core/src/test/java/example/ni/internal/Internal.java new file mode 100644 index 00000000..de400054 --- /dev/null +++ b/spring-modulith-core/src/test/java/example/ni/internal/Internal.java @@ -0,0 +1,21 @@ +/* + * Copyright 2024 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 example.ni.internal; + +/** + * @author Oliver Drotbohm + */ +public interface Internal {} diff --git a/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java b/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java index 24438d8e..7ecd2337 100644 --- a/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java +++ b/spring-modulith-core/src/test/java/org/springframework/modulith/core/NamedInterfacesUnitTests.java @@ -22,6 +22,7 @@ import example.ni.RootType; import example.ni.api.ApiType; import example.ni.internal.AdditionalSpiType; import example.ni.internal.DefaultedNamedInterfaceType; +import example.ni.internal.Internal; import example.ni.spi.SpiType; import example.ninvalid.InvalidDefaultNamedInterface; @@ -64,6 +65,18 @@ class NamedInterfacesUnitTests { .withMessageContaining(InvalidDefaultNamedInterface.class.getSimpleName()); } + @Test // GH-284 + void detectsOpenNamedInterface() { + + var javaPackage = TestUtils.getPackage(RootType.class); + var interfaces = NamedInterfaces.forOpen(javaPackage); + + assertThat(interfaces).map(NamedInterface::getName) + .containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal"); + + assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class, Internal.class); + } + private static void assertInterfaceContains(NamedInterfaces interfaces, String name, Class... types) { var classNames = Arrays.stream(types).map(Class::getName).toArray(String[]::new); diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/open/internal/Internal.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/open/internal/Internal.java new file mode 100644 index 00000000..d0ac4558 --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/open/internal/Internal.java @@ -0,0 +1,29 @@ +/* + * Copyright 2024 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.open.internal; + +import org.springframework.stereotype.Component; + +import com.acme.myproject.openclient.ClientToInternal; + +/** + * @author Oliver Drotbohm + */ +@Component +public class Internal { + + ClientToInternal clientToInternal; +} diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/open/package-info.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/open/package-info.java new file mode 100644 index 00000000..f95b1646 --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/open/package-info.java @@ -0,0 +1,5 @@ +@ApplicationModule(type = Type.OPEN) +package com.acme.myproject.open; + +import org.springframework.modulith.ApplicationModule; +import org.springframework.modulith.ApplicationModule.Type; diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/openclient/ClientToInternal.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/openclient/ClientToInternal.java new file mode 100644 index 00000000..0bb13e7a --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/openclient/ClientToInternal.java @@ -0,0 +1,26 @@ +/* + * Copyright 2024 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.openclient; + +import com.acme.myproject.open.internal.Internal; + +/** + * @author Oliver Drotbohm + */ +public class ClientToInternal { + + Internal internal; +} diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/opendisallowedclient/DisallowedClientOfInternal.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/opendisallowedclient/DisallowedClientOfInternal.java new file mode 100644 index 00000000..9685c3d2 --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/opendisallowedclient/DisallowedClientOfInternal.java @@ -0,0 +1,25 @@ +/* + * Copyright 2024 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.opendisallowedclient; + +import com.acme.myproject.open.internal.Internal; + +/** + * @author Oliver Drotbohm + */ +public class DisallowedClientOfInternal { + Internal internal; +} diff --git a/spring-modulith-integration-test/src/main/java/com/acme/myproject/opendisallowedclient/package-info.java b/spring-modulith-integration-test/src/main/java/com/acme/myproject/opendisallowedclient/package-info.java new file mode 100644 index 00000000..578beef7 --- /dev/null +++ b/spring-modulith-integration-test/src/main/java/com/acme/myproject/opendisallowedclient/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.modulith.ApplicationModule(allowedDependencies = "moduleA") +package com.acme.myproject.opendisallowedclient; diff --git a/spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java b/spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java index 7183e611..0cfbc8ee 100644 --- a/spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java +++ b/spring-modulith-integration-test/src/test/java/com/acme/myproject/ModulithTest.java @@ -61,7 +61,7 @@ class ModulithTest { assertThatExceptionOfType(Violations.class).isThrownBy(() -> { ApplicationModules - .of(Application.class, DEFAULT_EXCLUSIONS.or(Filters.withoutModule("invalid"))) + .of(Application.class, DEFAULT_EXCLUSIONS.or(Filters.withoutModules("invalid", "opendisallowedclient"))) .verify(); }).satisfies(it -> { diff --git a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java index dbc78ba5..4cde963e 100644 --- a/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java +++ b/spring-modulith-integration-test/src/test/java/org/springframework/modulith/core/ApplicationModulesIntegrationTest.java @@ -211,6 +211,29 @@ class ApplicationModulesIntegrationTest { }); } + @Test // GH-284 + void detectsOpenModule() { + + assertThat(modules.getModuleByName("open")).hasValueSatisfying(it -> { + assertThat(it.isOpen()).isTrue(); + }); + + var detectViolations = modules.detectViolations().getMessages(); + + assertThat(detectViolations) + .isNotEmpty() + + // No invalid references to internals from unrestricted module + .noneMatch(it -> it.matches("Module 'openclient' depends on non-exposed type .* within module 'open'")) + + // Invalid reference to internals from restricted module + .anyMatch(it -> it.contains("Module 'opendisallowedclient' depends on module 'open'")) + + // No cycle detection + .anyMatch(it -> it.contains("Cycle detected: Slice cycleA")) + .noneMatch(it -> it.contains("Cycle detected: Slice open")); + } + private static void verifyNamedInterfaces(NamedInterfaces interfaces, String name, Class... types) { Stream.of(types).forEach(type -> { diff --git a/spring-modulith-integration-test/src/test/resources/application.properties b/spring-modulith-integration-test/src/test/resources/application.properties new file mode 100644 index 00000000..33406d21 --- /dev/null +++ b/spring-modulith-integration-test/src/test/resources/application.properties @@ -0,0 +1 @@ +spring.main.banner-mode=off diff --git a/spring-modulith-integration-test/src/test/resources/logback.xml b/spring-modulith-integration-test/src/test/resources/logback.xml index 509f2604..d9c6c989 100644 --- a/spring-modulith-integration-test/src/test/resources/logback.xml +++ b/spring-modulith-integration-test/src/test/resources/logback.xml @@ -9,7 +9,7 @@ - + diff --git a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc index 0ecd7429..b7664d77 100644 --- a/src/docs/antora/modules/ROOT/pages/fundamentals.adoc +++ b/src/docs/antora/modules/ROOT/pages/fundamentals.adoc @@ -73,6 +73,45 @@ Note, how `SomethingOrderInternal` is a public type, likely because `OrderManage This unfortunately means that it can also be referred to from other packages such as the `inventory` one. In this case, the Java compiler is not of much use to prevent these illegal references. +[[modules.advanced.open]] +==== Open Application Modules + +The arrangement described xref:fundamentals.adoc#modules.advanced[above] are considered closed as they only expose types to other modules that are actively selected for exposure. +When applying Spring Modulith to legacy applications, hiding all types located in nested packages from other modules might be inadequate or require marking all those packages for exposure, too. + +To turn an application module into an open one, use the `@ApplicationModule` annotation on the `package-info.java` type. + +.Declaring an Application Modules as Open +[tabs] +====== +Java:: ++ +[source, java, role="primary", chomp="none"] +---- +@org.springframework.modulith.ApplicationModule( + type = Type.OPEN +) +package example.inventory; +---- +Kotlin:: ++ +[source, kotlin, role="secondary", chomp="none"] +---- +@org.springframework.modulith.ApplicationModule( + type = Type.OPEN +) +package example.inventory +---- +====== + +Declaring an application module as open will cause the following changes to the verification: + +* Access to application module internal types from other modules is generally allowed. +* All types, also ones residing in sub-packages of the application module base package are added to the xref:fundamentals.adoc#modules.named-interfaces[unnamed named interface], unless explicitly assigned to a named interface. + +NOTE: This feature is intended to be primarily used with code bases of existing projects gradually moving to the Spring Modulith recommended packaging structure. +In a fully-modularized application, using open application modules usually hints at sub-optimal modularization and packaging structures. + [[modules.explicit-dependencies]] === Explicit Application Module Dependencies A module can opt into declaring its allowed dependencies by using the `@ApplicationModule` annotation on the `package-info.java` type.