GH-192 - Add default component groupings for jMolecules architecture abstractions.

We now register default groupings for the architectural abstractions [0] in case they are available on the classpath but still fall back to the standard Spring Framework ones if not. In other words, if you e.g. use the jmolecules-hexagonal-architecture ones, types and packages annotated with @Port will cause the affected types to appear under a "Ports" section in the "Spring components" row in the Application Module Canvas.

[0] https://github.com/xmolecules/jmolecules#available-libraries-1
This commit is contained in:
Oliver Drotbohm
2023-04-19 17:31:25 +02:00
parent 3c52dbd0b3
commit 16bc4b3a60
4 changed files with 222 additions and 7 deletions

View File

@@ -53,6 +53,32 @@
<optional>true</optional>
</dependency>
<!-- jMolecules architecture abstractions -->
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-cqrs-architecture</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-hexagonal-architecture</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-layered-architecture</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.jmolecules</groupId>
<artifactId>jmolecules-onion-architecture</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>

View File

@@ -22,6 +22,7 @@ import java.io.FileWriter;
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.annotation.Annotation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
@@ -42,6 +43,8 @@ import org.springframework.modulith.core.ApplicationModules;
import org.springframework.modulith.core.DependencyDepth;
import org.springframework.modulith.core.DependencyType;
import org.springframework.modulith.core.SpringBean;
import org.springframework.modulith.docs.Groupings.JMoleculesGroupings;
import org.springframework.modulith.docs.Groupings.SpringGroupings;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
@@ -833,21 +836,39 @@ public class Documenter {
this.hideEmptyLines = hideEmptyLines;
}
/**
* Creates a default {@link CanvasOptions} instance configuring component {@link Groupings} for jMolecules (if on
* the classpath) and Spring Framework. Use {@link #withoutDefaultGroupings()} if you prefer to register component
* {@link Grouping}s yourself.
*
* @return will never be {@literal null}.
* @see #withoutDefaultGroupings()
* @see Groupings
*/
public static CanvasOptions defaults() {
return withoutDefaultGroupings()
.groupingBy("Controllers", bean -> bean.toArchitecturallyEvidentType().isController()) //
.groupingBy("Services", bean -> bean.toArchitecturallyEvidentType().isService()) //
.groupingBy("Repositories", bean -> bean.toArchitecturallyEvidentType().isRepository()) //
.groupingBy("Event listeners", bean -> bean.toArchitecturallyEvidentType().isEventListener()) //
.groupingBy("Configuration properties",
bean -> bean.toArchitecturallyEvidentType().isConfigurationProperties());
.groupingBy(JMoleculesGroupings.getGroupings())
.groupingBy(SpringGroupings.getGroupings());
}
/**
* Creates a {@link CanvasOptions} instance that does not register any default component {@link Grouping}s.
*
* @return will never be {@literal null}.
* @see #defaults()
* @see Groupings
*/
public static CanvasOptions withoutDefaultGroupings() {
return new CanvasOptions(new ArrayList<>(), null, null, true, true);
}
/**
* Creates a new {@link CanvasOptions} with the given {@link Grouping}s added.
*
* @param groupings must not be {@literal null}.
* @return will never be {@literal null}.
*/
public CanvasOptions groupingBy(Grouping... groupings) {
var result = new ArrayList<>(groupers);
@@ -1075,6 +1096,10 @@ public class Documenter {
.and(bean -> !bean.getType().isEquivalentTo(type));
}
public static Predicate<SpringBean> isAnnotatedWith(Class<? extends Annotation> type) {
return bean -> bean.getType().isAnnotatedWith(type);
}
/**
* Returns the name of the {@link Grouping}.
*

View File

@@ -0,0 +1,163 @@
/*
* 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 org.springframework.modulith.docs;
import static org.springframework.modulith.docs.Documenter.CanvasOptions.Grouping.*;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.function.Predicate;
import org.jmolecules.architecture.cqrs.Command;
import org.jmolecules.architecture.cqrs.CommandDispatcher;
import org.jmolecules.architecture.cqrs.CommandHandler;
import org.jmolecules.architecture.cqrs.QueryModel;
import org.jmolecules.architecture.hexagonal.Adapter;
import org.jmolecules.architecture.hexagonal.Application;
import org.jmolecules.architecture.hexagonal.Port;
import org.jmolecules.architecture.hexagonal.PrimaryAdapter;
import org.jmolecules.architecture.hexagonal.PrimaryPort;
import org.jmolecules.architecture.hexagonal.SecondaryAdapter;
import org.jmolecules.architecture.hexagonal.SecondaryPort;
import org.jmolecules.architecture.layered.ApplicationLayer;
import org.jmolecules.architecture.layered.DomainLayer;
import org.jmolecules.architecture.layered.InfrastructureLayer;
import org.jmolecules.architecture.layered.InterfaceLayer;
import org.jmolecules.architecture.onion.classical.ApplicationServiceRing;
import org.jmolecules.architecture.onion.classical.DomainModelRing;
import org.jmolecules.architecture.onion.classical.DomainServiceRing;
import org.jmolecules.architecture.onion.simplified.ApplicationRing;
import org.jmolecules.architecture.onion.simplified.DomainRing;
import org.jmolecules.architecture.onion.simplified.InfrastructureRing;
import org.springframework.modulith.core.SpringBean;
import org.springframework.modulith.docs.Documenter.CanvasOptions.Grouping;
import org.springframework.util.ClassUtils;
/**
* A collection of {@link Grouping}s.
*
* @author Oliver Drotbohm
*/
public class Groupings {
/**
* Spring Framework-related {@link Grouping}s.
*
* @author Oliver Drotbohm
*/
public static class SpringGroupings {
/**
* Returns Spring Framework-related {@link Grouping}s.
*
* @return will never be {@literal null}.
*/
public static Grouping[] getGroupings() {
return new Grouping[] {
of("Controllers", bean -> bean.toArchitecturallyEvidentType().isController()),
of("Services", bean -> bean.toArchitecturallyEvidentType().isService()),
of("Repositories", bean -> bean.toArchitecturallyEvidentType().isRepository()),
of("Event listeners", bean -> bean.toArchitecturallyEvidentType().isEventListener()),
of("Configuration properties",
bean -> bean.toArchitecturallyEvidentType().isConfigurationProperties())
};
}
}
/**
* jMolecules-related {@link Grouping}s.
*
* @author Oliver Drotbohm
*/
public static class JMoleculesGroupings {
private static final boolean JMOLECULES_CQRS_PRESENT = ClassUtils
.isPresent("org.jmolecules.architecture.cqrs.Command", JMoleculesGroupings.class.getClassLoader());
private static final boolean JMOLECULES_HEXAGONAL_PRESENT = ClassUtils
.isPresent("org.jmolecules.architecture.hexagonal.Port", JMoleculesGroupings.class.getClassLoader());
private static final boolean JMOLECULES_LAYERS_PRESENT = ClassUtils
.isPresent("org.jmolecules.architecture.layered", JMoleculesGroupings.class.getClassLoader());
private static final boolean JMOLECULES_ONION_PRESENT = ClassUtils
.isPresent("org.jmolecules.architecture.onion.classical.ApplicationRing",
JMoleculesGroupings.class.getClassLoader());
public static Grouping[] getGroupings() {
var groupings = new ArrayList<Grouping>();
if (JMOLECULES_CQRS_PRESENT) {
groupings.add(of("Commands", packageOrTypeAnnotatedWith(Command.class)));
groupings.add(of("Command dispatchers", packageOrTypeAnnotatedWith(CommandDispatcher.class)));
groupings.add(of("Command handlers", packageOrTypeAnnotatedWith(CommandHandler.class)));
groupings.add(of("Query models", packageOrTypeAnnotatedWith(QueryModel.class)));
}
if (JMOLECULES_HEXAGONAL_PRESENT) {
groupings.add(of("Primary ports", packageOrTypeAnnotatedWith(PrimaryPort.class)));
groupings.add(of("Secondary ports", packageOrTypeAnnotatedWith(SecondaryPort.class)));
groupings.add(of("Ports", packageOrTypeAnnotatedWith(Port.class)));
groupings.add(of("Application", packageOrTypeAnnotatedWith(Application.class)));
groupings.add(of("Primary adapters", packageOrTypeAnnotatedWith(PrimaryAdapter.class)));
groupings.add(of("Secondary adapters", packageOrTypeAnnotatedWith(SecondaryAdapter.class)));
groupings.add(of("Adapters", packageOrTypeAnnotatedWith(Adapter.class)));
}
if (JMOLECULES_LAYERS_PRESENT) {
groupings.add(of("Application layer", packageOrTypeAnnotatedWith(ApplicationLayer.class)));
groupings.add(of("Domain layer", packageOrTypeAnnotatedWith(DomainLayer.class)));
groupings.add(of("Infrastructure layer", packageOrTypeAnnotatedWith(InfrastructureLayer.class)));
groupings.add(of("Interface layer", packageOrTypeAnnotatedWith(InterfaceLayer.class)));
}
if (JMOLECULES_ONION_PRESENT) {
groupings.add(of("Application ring", packageOrTypeAnnotatedWith(ApplicationRing.class)));
groupings.add(of("Domain ring", packageOrTypeAnnotatedWith(DomainRing.class)));
groupings.add(of("Infrastructure ring", packageOrTypeAnnotatedWith(InfrastructureRing.class)));
groupings.add(of("Application service ring", packageOrTypeAnnotatedWith(ApplicationServiceRing.class)));
groupings.add(of("Domain service ring", packageOrTypeAnnotatedWith(DomainServiceRing.class)));
groupings.add(of("Domain model ring", packageOrTypeAnnotatedWith(DomainModelRing.class)));
groupings.add(of("Infrastructure ring",
packageOrTypeAnnotatedWith(
org.jmolecules.architecture.onion.classical.InfrastructureRing.class)));
}
return groupings.toArray(Grouping[]::new);
}
}
private static Predicate<SpringBean> packageOrTypeAnnotatedWith(Class<? extends Annotation> annotation) {
return bean -> {
var type = bean.getType();
var pkg = type.getPackage();
return type.isAnnotatedWith(annotation) || type.isMetaAnnotatedWith(annotation) //
|| pkg.isAnnotatedWith(annotation) || pkg.isMetaAnnotatedWith(annotation);
};
}
}

View File

@@ -245,7 +245,8 @@ _Others_
It consists of the following sections:
* __The application module's base package.__
* __The Spring beans exposed by the application module, grouped by stereotype.__ -- In other words beans that are located in either the API package or any <<fundamentals.modules.named-interfaces, named interface package>>.
* __The Spring beans exposed by the application module, grouped by stereotype.__ -- In other words, beans that are located in either the API package or any <<fundamentals.modules.named-interfaces, named interface package>>.
This will detect component stereotypes defined by https://github.com/xmolecules/jmolecules/tree/main/jmolecules-architecture[jMolecules architecture abstractions], but also standard Spring stereotype annotations.
* __Exposed aggregate roots__ -- Any entities that we find repositories for or explicitly declared as aggregate via jMolecules.
* __Application events published by the module__ -- Those event types need to be demarcated using jMolecules `@DomainEvent` or implement its `DomainEvent` interface.
* __Application events listened to by the module__ -- Derived from methods annotated with Spring's `@EventListener`, `@TransactionalEventListener`, jMolecules' `@DomainEventHandler` or beans implementing `ApplicationListener`.