GH-522 - Support for package info types.

Introduce @PackageInfo annotation to allow marking a types as alternative to Java's native package-info.java so that annotation lookups on a JavaPackage will also find annotations placed on that type. Useful to declare package scoped metadata like @ApplicationModule and @NamedInterface for languages that do not support packages well enough (read: Kotlin).
This commit is contained in:
Oliver Drotbohm
2024-03-05 08:35:45 +01:00
parent f3c111f769
commit 5f7e6a2479
21 changed files with 461 additions and 66 deletions

View File

@@ -25,7 +25,8 @@ import java.lang.annotation.Target;
*
* @author Oliver Drotbohm
*/
@Target({ ElementType.PACKAGE, ElementType.ANNOTATION_TYPE })
@PackageInfo
@Target({ ElementType.PACKAGE, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationModule {

View File

@@ -0,0 +1,32 @@
/*
* 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 org.springframework.modulith;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* An annotation to mark a type as an alternative to Java's native {@code package-info.java}. Classes annotated like
* this will be considered, too, when looking up annotations on a package.
*
* @author Oliver Drotbohm
* @since 1.2
*/
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface PackageInfo {}

View File

@@ -18,7 +18,7 @@ package org.springframework.modulith.core;
import static com.tngtech.archunit.base.DescribedPredicate.*;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import static java.lang.System.*;
import static org.springframework.modulith.core.Types.*;
import static org.springframework.modulith.core.SyntacticSugar.*;
import static org.springframework.modulith.core.Types.JavaXTypes.*;
import static org.springframework.modulith.core.Types.SpringDataTypes.*;
import static org.springframework.modulith.core.Types.SpringTypes.*;
@@ -84,6 +84,8 @@ public class ApplicationModule {
*/
ApplicationModule(JavaPackage basePackage, boolean useFullyQualifiedModuleNames) {
Assert.notNull(basePackage, "Base package must not be null!");
this.basePackage = basePackage;
this.information = ApplicationModuleInformation.of(basePackage);
this.namedInterfaces = isOpen()

View File

@@ -15,18 +15,23 @@
*/
package org.springframework.modulith.core;
import java.lang.annotation.Annotation;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.function.Predicate;
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;
import com.tngtech.archunit.core.domain.JavaClass;
/**
* Abstraction for low-level module information. Used to support different annotations to configure metadata about a
* module.
@@ -43,13 +48,15 @@ interface ApplicationModuleInformation {
*/
public static ApplicationModuleInformation of(JavaPackage javaPackage) {
var lookup = AnnotationLookup.of(javaPackage, __ -> true);
if (ClassUtils.isPresent("org.jmolecules.ddd.annotation.Module",
ApplicationModuleInformation.class.getClassLoader())
&& JMoleculesModule.supports(javaPackage)) {
return new JMoleculesModule(javaPackage);
&& JMoleculesModule.supports(lookup)) {
return new JMoleculesModule(lookup);
}
return new SpringModulithModule(javaPackage);
return new SpringModulithModule(lookup);
}
/**
@@ -84,14 +91,14 @@ interface ApplicationModuleInformation {
*/
static class JMoleculesModule implements ApplicationModuleInformation {
private final Optional<org.jmolecules.ddd.annotation.Module> annotation;
private final Optional<Module> annotation;
public static boolean supports(JavaPackage javaPackage) {
return javaPackage.getAnnotation(org.jmolecules.ddd.annotation.Module.class).isPresent();
public static boolean supports(AnnotationLookup lookup) {
return lookup.lookup(Module.class).isPresent();
}
public JMoleculesModule(JavaPackage javaPackage) {
this.annotation = javaPackage.getAnnotation(org.jmolecules.ddd.annotation.Module.class);
public <A extends Annotation> JMoleculesModule(AnnotationLookup lookup) {
this.annotation = lookup.lookup(Module.class);
}
/*
@@ -102,11 +109,11 @@ interface ApplicationModuleInformation {
public Optional<String> getDisplayName() {
Supplier<Optional<String>> fallback = () -> annotation //
.map(org.jmolecules.ddd.annotation.Module::value) //
.map(Module::value) //
.filter(StringUtils::hasText);
return annotation //
.map(org.jmolecules.ddd.annotation.Module::name) //
.map(Module::name) //
.filter(StringUtils::hasText)
.or(fallback);
}
@@ -144,11 +151,11 @@ interface ApplicationModuleInformation {
*
* @param javaPackage must not be {@literal null}.
*/
public static boolean supports(JavaPackage javaPackage) {
public static boolean supports(AnnotationLookup lookup) {
Assert.notNull(javaPackage, "Java package must not be null!");
Assert.notNull(lookup, "Annotation lookup must not be null!");
return javaPackage.getAnnotation(ApplicationModule.class).isPresent();
return lookup.lookup(ApplicationModule.class).isPresent();
}
/**
@@ -156,8 +163,8 @@ interface ApplicationModuleInformation {
*
* @param javaPackage must not be {@literal null}.
*/
public SpringModulithModule(JavaPackage javaPackage) {
this.annotation = javaPackage.getAnnotation(ApplicationModule.class);
public SpringModulithModule(AnnotationLookup lookup) {
this.annotation = lookup.lookup(ApplicationModule.class);
}
/*
@@ -194,4 +201,21 @@ interface ApplicationModuleInformation {
return annotation.map(it -> it.type().equals(Type.OPEN)).orElse(false);
}
}
interface AnnotationLookup {
static AnnotationLookup of(JavaPackage javaPackage,
Predicate<JavaClass> typeSelector) {
return new AnnotationLookup() {
@Override
public <A extends Annotation> Optional<A> lookup(Class<A> annotation) {
return javaPackage.findAnnotation(annotation);
}
};
}
<A extends Annotation> Optional<A> lookup(Class<A> annotation);
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.modulith.core;
import static org.springframework.modulith.core.SyntacticSugar.*;
import static org.springframework.modulith.core.Types.JavaXTypes.*;
import java.util.ArrayList;
@@ -194,8 +195,8 @@ public abstract class ArchitecturallyEvidentType {
* Methods (meta-)annotated with @EventListener.
*/
private static final Predicate<JavaMethod> IS_ANNOTATED_EVENT_LISTENER = it -> //
Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(it) //
|| Types.isAnnotatedWith(SpringTypes.AT_TX_EVENT_LISTENER).test(it);
isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(it) //
|| isAnnotatedWith(SpringTypes.AT_TX_EVENT_LISTENER).test(it);
/**
* {@code ApplicationListener.onApplicationEvent(…)}
@@ -232,7 +233,7 @@ public abstract class ArchitecturallyEvidentType {
*/
@Override
public boolean isRepository() {
return Types.isAnnotatedWith(SpringTypes.AT_REPOSITORY).test(getType());
return isAnnotatedWith(SpringTypes.AT_REPOSITORY).test(getType());
}
/*
@@ -241,7 +242,7 @@ public abstract class ArchitecturallyEvidentType {
*/
@Override
public boolean isService() {
return Types.isAnnotatedWith(SpringTypes.AT_SERVICE).test(getType());
return isAnnotatedWith(SpringTypes.AT_SERVICE).test(getType());
}
/*
@@ -250,7 +251,7 @@ public abstract class ArchitecturallyEvidentType {
*/
@Override
public boolean isController() {
return Types.isAnnotatedWith(SpringTypes.AT_CONTROLLER).test(getType());
return isAnnotatedWith(SpringTypes.AT_CONTROLLER).test(getType());
}
/*
@@ -268,7 +269,7 @@ public abstract class ArchitecturallyEvidentType {
*/
@Override
public boolean isConfigurationProperties() {
return Types.isAnnotatedWith(SpringTypes.AT_CONFIGURATION_PROPERTIES).test(getType());
return isAnnotatedWith(SpringTypes.AT_CONFIGURATION_PROPERTIES).test(getType());
}
/*
@@ -365,15 +366,15 @@ public abstract class ArchitecturallyEvidentType {
*/
@Override
public boolean isController() {
return Types.isAnnotatedWith("org.springframework.data.rest.webmvc.BasePathAwareController")
return isAnnotatedWith("org.springframework.data.rest.webmvc.BasePathAwareController")
.test(getType());
}
}
static class JMoleculesArchitecturallyEvidentType extends ArchitecturallyEvidentType {
private static final Predicate<JavaMethod> IS_ANNOTATED_EVENT_LISTENER = Types
.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER)::test;
private static final Predicate<JavaMethod> IS_ANNOTATED_EVENT_LISTENER = isAnnotatedWith(
JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER)::test;
JMoleculesArchitecturallyEvidentType(JavaClass type) {
super(type);
@@ -388,7 +389,7 @@ public abstract class ArchitecturallyEvidentType {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Entity.class).test(type) || //
return isAnnotatedWith(org.jmolecules.ddd.annotation.Entity.class).test(type) || //
type.isAssignableTo(org.jmolecules.ddd.types.Entity.class);
}
@@ -401,7 +402,7 @@ public abstract class ArchitecturallyEvidentType {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.AggregateRoot.class).test(type) //
return isAnnotatedWith(org.jmolecules.ddd.annotation.AggregateRoot.class).test(type) //
|| type.isAssignableTo(org.jmolecules.ddd.types.AggregateRoot.class);
}
@@ -414,7 +415,7 @@ public abstract class ArchitecturallyEvidentType {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Repository.class).test(type)
return isAnnotatedWith(org.jmolecules.ddd.annotation.Repository.class).test(type)
|| type.isAssignableTo(org.jmolecules.ddd.types.Repository.class);
}
@@ -427,7 +428,7 @@ public abstract class ArchitecturallyEvidentType {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.Service.class).test(type);
return isAnnotatedWith(org.jmolecules.ddd.annotation.Service.class).test(type);
}
/*
@@ -448,7 +449,7 @@ public abstract class ArchitecturallyEvidentType {
JavaClass type = getType();
return Types.isAnnotatedWith(org.jmolecules.ddd.annotation.ValueObject.class).test(type)
return isAnnotatedWith(org.jmolecules.ddd.annotation.ValueObject.class).test(type)
|| type.isAssignableTo(org.jmolecules.ddd.types.ValueObject.class);
}
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.modulith.core;
import static org.springframework.modulith.core.SyntacticSugar.*;
import java.util.Arrays;
import java.util.Collection;
import java.util.function.Predicate;
@@ -125,8 +127,8 @@ public enum DependencyType {
}
static DependencyType forCodeUnit(JavaCodeUnit codeUnit) {
return Types.isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(codeUnit) //
|| Types.isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).test(codeUnit) //
return isAnnotatedWith(SpringTypes.AT_EVENT_LISTENER).test(codeUnit) //
|| isAnnotatedWith(JMoleculesTypes.AT_DOMAIN_EVENT_HANDLER).test(codeUnit) //
? EVENT_LISTENER
: DEFAULT;
}

View File

@@ -16,6 +16,9 @@
package org.springframework.modulith.core;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.*;
import static com.tngtech.archunit.core.domain.properties.HasModifiers.Predicates.*;
import static org.springframework.modulith.core.SyntacticSugar.*;
import java.lang.annotation.Annotation;
import java.util.Collection;
@@ -28,6 +31,7 @@ import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.modulith.PackageInfo;
import org.springframework.util.Assert;
import org.springframework.util.function.SingletonSupplier;
@@ -35,8 +39,6 @@ import com.tngtech.archunit.base.DescribedIterable;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaModifier;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
import com.tngtech.archunit.core.domain.properties.HasModifiers;
/**
* An abstraction of a Java package.
@@ -46,6 +48,9 @@ import com.tngtech.archunit.core.domain.properties.HasModifiers;
public class JavaPackage implements DescribedIterable<JavaClass> {
private static final String PACKAGE_INFO_NAME = "package-info";
private static final String MULTIPLE_TYPES_ANNOTATED_WITH = "Expected maximum of one type in package %s to be annotated with %s, but got %s!";
private static final DescribedPredicate<JavaClass> ARE_PACKAGE_INFOS = //
has(simpleName(PACKAGE_INFO_NAME)).or(is(metaAnnotatedWith(PackageInfo.class)));
private final String name;
private final Classes classes;
@@ -96,7 +101,7 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
Assert.notNull(type, "Type must not be null!");
return type.getSimpleName().equals(PACKAGE_INFO_NAME);
return ARE_PACKAGE_INFOS.test(type);
}
/**
@@ -152,8 +157,9 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
*/
public Classes getExposedClasses() {
return packageClasses.that(HasModifiers.Predicates.modifier(JavaModifier.PUBLIC)) //
.that(DescribedPredicate.not(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME)));
return packageClasses //
.that(doNotHave(simpleName(PACKAGE_INFO_NAME))) //
.that(have(modifier(JavaModifier.PUBLIC)));
}
/**
@@ -166,8 +172,7 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
Assert.notNull(annotation, "Annotation must not be null!");
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
.and(CanBeAnnotated.Predicates.annotatedWith(annotation))).stream() //
return packageClasses.that(ARE_PACKAGE_INFOS.and(are(metaAnnotatedWith(annotation)))).stream() //
.map(JavaClass::getPackageName) //
.distinct() //
.map(it -> of(classes, it));
@@ -228,13 +233,46 @@ public class JavaPackage implements DescribedIterable<JavaClass> {
*/
public <A extends Annotation> Optional<A> getAnnotation(Class<A> annotationType) {
return packageClasses.that(JavaClass.Predicates.simpleName(PACKAGE_INFO_NAME) //
.and(CanBeAnnotated.Predicates.annotatedWith(annotationType))) //
return packageClasses.that(have(simpleName(PACKAGE_INFO_NAME)) //
.and(are(metaAnnotatedWith(annotationType)))) //
.toOptional() //
.map(it -> it.reflect())
.map(it -> AnnotatedElementUtils.getMergedAnnotation(it, annotationType));
}
/**
* Finds the annotation of the given type declared on the package itself or any type located the direct package's
* types .
*
* @param <A> the type of the annotation.
* @param annotationType must not be {@literal null}.
* @param typeFilter must not be {@literal null}.
* @return will never be {@literal null}.
* @since 1.2
* @throws IllegalStateException in case multiple types in the current package are annotated with the given
* annotation.
*/
public <A extends Annotation> Optional<A> findAnnotation(Class<A> annotationType) {
return getAnnotation(annotationType)
.or(() -> {
var annotatedTypes = toSingle().packageClasses
.that(are(metaAnnotatedWith(PackageInfo.class).and(are(metaAnnotatedWith(annotationType)))))
.stream()
.map(it -> it.getAnnotationOfType(annotationType))
.toList();
if (annotatedTypes.size() > 1) {
throw new IllegalStateException(MULTIPLE_TYPES_ANNOTATED_WITH.formatted(name,
FormatableType.of(annotationType).getAbbreviatedFullName(), annotatedTypes));
}
return annotatedTypes.isEmpty() ? Optional.empty() : Optional.of(annotatedTypes.get(0));
});
}
/*
* (non-Javadoc)
* @see com.tngtech.archunit.base.HasDescription#getDescription()

View File

@@ -16,6 +16,7 @@
package org.springframework.modulith.core;
import static com.tngtech.archunit.base.DescribedPredicate.*;
import static org.springframework.modulith.core.SyntacticSugar.*;
import static org.springframework.modulith.core.Types.*;
import java.util.Iterator;
@@ -71,7 +72,7 @@ public class NamedInterface implements Iterable<JavaClass> {
*/
static List<NamedInterface> of(JavaPackage javaPackage) {
var names = javaPackage.getAnnotation(org.springframework.modulith.NamedInterface.class) //
var names = javaPackage.findAnnotation(org.springframework.modulith.NamedInterface.class) //
.map(it -> getDefaultedNames(it, javaPackage.getName())) //
.orElseThrow(() -> new IllegalArgumentException(
String.format("Couldn't find NamedInterface annotation on package %s!", javaPackage)));

View File

@@ -0,0 +1,59 @@
/*
* 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 org.springframework.modulith.core;
import java.lang.annotation.Annotation;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates;
/**
* Helper to make the composition of {@link DescribedPredicate}s more readable.
*
* @author Oliver Drotbohm
* @since 1.2
*/
class SyntacticSugar {
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<? extends Annotation> type) {
return isAnnotatedWith(type.getName());
}
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
return Predicates.metaAnnotatedWith(type);
}
static <T> DescribedPredicate<T> are(DescribedPredicate<T> predicate) {
return predicate;
}
static <T> DescribedPredicate<T> has(DescribedPredicate<T> predicate) {
return predicate;
}
static <T> DescribedPredicate<T> have(DescribedPredicate<T> predicate) {
return predicate;
}
static <T> DescribedPredicate<T> is(DescribedPredicate<T> predicate) {
return predicate;
}
static <T> DescribedPredicate<T> doNotHave(DescribedPredicate<T> predicate) {
return DescribedPredicate.not(predicate);
}
}

View File

@@ -16,18 +16,18 @@
package org.springframework.modulith.core;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.*;
import static org.springframework.modulith.core.SyntacticSugar.*;
import java.lang.annotation.Annotation;
import org.springframework.lang.Nullable;
import org.springframework.modulith.PackageInfo;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated;
import com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates;
/**
* @author Oliver Drotbohm
@@ -141,20 +141,11 @@ class Types {
}
static DescribedPredicate<JavaClass> isSpringDataRepository() {
return assignableTo(SpringDataTypes.REPOSITORY) //
return is(assignableTo(SpringDataTypes.REPOSITORY)) //
.or(isAnnotatedWith(SpringDataTypes.AT_REPOSITORY_DEFINITION));
}
}
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(Class<? extends Annotation> type) {
return isAnnotatedWith(type.getName());
}
static DescribedPredicate<CanBeAnnotated> isAnnotatedWith(String type) {
return Predicates.annotatedWith(type) //
.or(Predicates.metaAnnotatedWith(type));
}
/**
* Creates a new {@link DescribedPredicate} to match classes
*
@@ -170,7 +161,12 @@ class Types {
@Override
public boolean test(JavaClass t) {
return t.getPackage().isMetaAnnotatedWith(type);
var pkg = t.getPackage();
return pkg.isMetaAnnotatedWith(type)
|| pkg.getClasses().stream()
.anyMatch(it -> it.isMetaAnnotatedWith(PackageInfo.class) && it.isMetaAnnotatedWith(type));
}
};
}

View File

@@ -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.ontype;
/**
* @author Oliver Drotbohm
*/
public class Exposed {}

View File

@@ -0,0 +1,28 @@
/*
* 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.ontype;
import org.springframework.modulith.NamedInterface;
import org.springframework.modulith.PackageInfo;
/**
* @author Oliver Drotbohm
*/
@PackageInfo
@NamedInterface
class Marker {
}

View File

@@ -23,6 +23,7 @@ import example.ni.api.ApiType;
import example.ni.internal.AdditionalSpiType;
import example.ni.internal.DefaultedNamedInterfaceType;
import example.ni.internal.Internal;
import example.ni.ontype.Exposed;
import example.ni.spi.SpiType;
import example.ninvalid.InvalidDefaultNamedInterface;
@@ -46,13 +47,14 @@ class NamedInterfacesUnitTests {
var interfaces = NamedInterfaces.discoverNamedInterfaces(javaPackage);
assertThat(interfaces).map(NamedInterface::getName)
.containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal");
.containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal", "ontype");
assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class);
assertInterfaceContains(interfaces, "api", ApiType.class, AnnotatedNamedInterfaceType.class);
assertInterfaceContains(interfaces, "spi", SpiType.class, AdditionalSpiType.class);
assertInterfaceContains(interfaces, "kpi", AdditionalSpiType.class);
assertInterfaceContains(interfaces, "internal", DefaultedNamedInterfaceType.class);
assertInterfaceContains(interfaces, "ontype", Exposed.class);
}
@Test // GH-183
@@ -72,7 +74,7 @@ class NamedInterfacesUnitTests {
var interfaces = NamedInterfaces.forOpen(javaPackage);
assertThat(interfaces).map(NamedInterface::getName)
.containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal");
.containsExactlyInAnyOrder(NamedInterface.UNNAMED_NAME, "api", "spi", "kpi", "internal", "ontype");
assertInterfaceContains(interfaces, NamedInterface.UNNAMED_NAME, RootType.class, Internal.class);
}

View File

@@ -0,0 +1,24 @@
/*
* 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 pkg.multipleontype;
import org.springframework.modulith.ApplicationModule;
/**
* @author Oliver Drotbohm
*/
@ApplicationModule
class MarkerOne {}

View File

@@ -0,0 +1,24 @@
/*
* 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 pkg.multipleontype;
import org.springframework.modulith.ApplicationModule;
/**
* @author Oliver Drotbohm
*/
@ApplicationModule
class MarkerTwo {}

View File

@@ -0,0 +1,2 @@
@org.springframework.modulith.ApplicationModule(displayName = "onPackage")
package pkg.onpackage;

View File

@@ -0,0 +1,24 @@
/*
* 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 pkg.ontype;
import org.springframework.modulith.ApplicationModule;
/**
* @author Oliver Drotbohm
*/
@ApplicationModule(displayName = "onType")
public class Marker {}

View File

@@ -0,0 +1,24 @@
/*
* 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 pkg.ontype.nested;
import org.springframework.modulith.ApplicationModule;
/**
* @author Oliver Drotbohm
*/
@ApplicationModule(displayName = "onTypeNested")
public class Marker {}

View File

@@ -0,0 +1,45 @@
/*
* 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 org.springframework.modulith.core;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;
/**
* Unit tests for {@link ApplicationModuleInformation}.
*
* @author Oliver Drotbohm
*/
class ApplicationModuleInformationUnitTests {
static final JavaClasses PKG_CLASSES = new ClassFileImporter() //
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) //
.importPackages("pkg");
@Test // GH-522
void detectsApplicationInformationOnType() {
var pkg = JavaPackage.of(Classes.of(PKG_CLASSES), "pkg.ontype");
var information = ApplicationModuleInformation.of(pkg);
assertThat(information.getDisplayName()).hasValue("onType");
}
}

View File

@@ -18,6 +18,7 @@ package org.springframework.modulith.core;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.ApplicationModule;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
@@ -29,11 +30,15 @@ import com.tngtech.archunit.core.importer.ImportOption;
class JavaPackageUnitTests {
static final JavaClasses ALL_CLASSES = new ClassFileImporter() //
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) //
.importPackages("com.acme.myproject");
static final JavaClasses PKG_CLASSES = new ClassFileImporter() //
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS) //
.importPackages("pkg");
@Test
void testName() throws Exception {
void detectsDirectSubPackages() throws Exception {
Classes classes = Classes.of(ALL_CLASSES);
JavaPackage pkg = JavaPackage.of(classes, "com.acme.myproject.complex");
@@ -43,4 +48,35 @@ class JavaPackageUnitTests {
.extracting(JavaPackage::getLocalName) //
.contains("api", "internal", "spi");
}
@Test // GH-522
void findsAnnotationOnPackageInfo() {
var annotation = JavaPackage.of(Classes.of(PKG_CLASSES), "pkg.onpackage") //
.getAnnotation(ApplicationModule.class);
assertThat(annotation).hasValueSatisfying(it -> {
assertThat(it.displayName()).isEqualTo("onPackage");
});
}
@Test // GH-522
void findsAnnotationOnPackageType() {
var annotation = JavaPackage.of(Classes.of(PKG_CLASSES), "pkg.ontype") //
.findAnnotation(ApplicationModule.class);
assertThat(annotation).hasValueSatisfying(it -> {
assertThat(it.displayName()).isEqualTo("onType");
});
}
@Test // GH-522
void rejectsMultipleAnnotationsOnType() {
assertThatIllegalStateException().isThrownBy(() -> {
JavaPackage.of(Classes.of(PKG_CLASSES), "pkg.multipleontype") //
.findAnnotation(ApplicationModule.class);
});
}
}

View File

@@ -114,7 +114,8 @@ In a fully-modularized application, using open application modules usually hints
[[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.
A module can opt into declaring its allowed dependencies by using the `@ApplicationModule` annotation on the package, represented through the `package-info.java` file.
As, for example, Kotlin lacks support for that file, you can also use the annotation on a single type located in the application module's root package.
.Inventory explicitly configuring module dependencies
[tabs]
@@ -132,10 +133,12 @@ Kotlin::
+
[source, kotlin, role="secondary", chomp="none"]
----
@org.springframework.modulith.ApplicationModule(
allowedDependencies = "order"
)
package example.inventory
import org.springframework.modulith.ApplicationModule
@ApplicationModule(allowedDependencies = "order")
class ModuleMetadata {}
----
======
@@ -207,7 +210,7 @@ Note how each module is listed, the contained Spring components are identified,
By default and as described in xref:fundamentals.adoc#modules.advanced[Advanced Application Modules], an application module's base package is considered the API package and thus is the only package to allow incoming dependencies from other modules.
In case you would like to expose additional packages to other modules, you need to use __named interfaces__.
You achieve that by annotating the `package-info.java` file of those packages with `@NamedInterface`.
You achieve that by annotating the `package-info.java` file of those packages with `@NamedInterface` or a type explicitly annotated with `@org.springframework.modulith.PackageInfo`.
.A package arrangement to encapsulate an SPI named interface
[source, text, subs="macros, quotes"]
@@ -240,8 +243,14 @@ Kotlin::
+
[source, kotlin, role="secondary", chomp="none"]
----
@org.springframework.modulith.NamedInterface("spi")
package example.order.spi
import org.springframework.modulith.PackageInfo
import org.springframework.modulith.NamedInterface
@PackageInfo
@NamedInterface("spi")
class ModuleMetadata {}
----
======
The effect of that declaration is twofold: first, code in other application modules is allowed to refer to `SomeSpiInterface`.