Skip to content

Commit 16bc4b3

Browse files
committed
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
1 parent 3c52dbd commit 16bc4b3

File tree

4 files changed

+222
-7
lines changed

4 files changed

+222
-7
lines changed

spring-modulith-docs/pom.xml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,32 @@
5353
<optional>true</optional>
5454
</dependency>
5555

56+
<!-- jMolecules architecture abstractions -->
57+
58+
<dependency>
59+
<groupId>org.jmolecules</groupId>
60+
<artifactId>jmolecules-cqrs-architecture</artifactId>
61+
<optional>true</optional>
62+
</dependency>
63+
64+
<dependency>
65+
<groupId>org.jmolecules</groupId>
66+
<artifactId>jmolecules-hexagonal-architecture</artifactId>
67+
<optional>true</optional>
68+
</dependency>
69+
70+
<dependency>
71+
<groupId>org.jmolecules</groupId>
72+
<artifactId>jmolecules-layered-architecture</artifactId>
73+
<optional>true</optional>
74+
</dependency>
75+
76+
<dependency>
77+
<groupId>org.jmolecules</groupId>
78+
<artifactId>jmolecules-onion-architecture</artifactId>
79+
<optional>true</optional>
80+
</dependency>
81+
5682
<dependency>
5783
<groupId>org.springframework.boot</groupId>
5884
<artifactId>spring-boot-starter-test</artifactId>

spring-modulith-docs/src/main/java/org/springframework/modulith/docs/Documenter.java

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.io.IOException;
2323
import java.io.StringWriter;
2424
import java.io.Writer;
25+
import java.lang.annotation.Annotation;
2526
import java.nio.file.Files;
2627
import java.nio.file.Path;
2728
import java.nio.file.Paths;
@@ -42,6 +43,8 @@
4243
import org.springframework.modulith.core.DependencyDepth;
4344
import org.springframework.modulith.core.DependencyType;
4445
import org.springframework.modulith.core.SpringBean;
46+
import org.springframework.modulith.docs.Groupings.JMoleculesGroupings;
47+
import org.springframework.modulith.docs.Groupings.SpringGroupings;
4548
import org.springframework.util.Assert;
4649
import org.springframework.util.LinkedMultiValueMap;
4750
import org.springframework.util.MultiValueMap;
@@ -833,21 +836,39 @@ public static class CanvasOptions {
833836
this.hideEmptyLines = hideEmptyLines;
834837
}
835838

839+
/**
840+
* Creates a default {@link CanvasOptions} instance configuring component {@link Groupings} for jMolecules (if on
841+
* the classpath) and Spring Framework. Use {@link #withoutDefaultGroupings()} if you prefer to register component
842+
* {@link Grouping}s yourself.
843+
*
844+
* @return will never be {@literal null}.
845+
* @see #withoutDefaultGroupings()
846+
* @see Groupings
847+
*/
836848
public static CanvasOptions defaults() {
837849

838850
return withoutDefaultGroupings()
839-
.groupingBy("Controllers", bean -> bean.toArchitecturallyEvidentType().isController()) //
840-
.groupingBy("Services", bean -> bean.toArchitecturallyEvidentType().isService()) //
841-
.groupingBy("Repositories", bean -> bean.toArchitecturallyEvidentType().isRepository()) //
842-
.groupingBy("Event listeners", bean -> bean.toArchitecturallyEvidentType().isEventListener()) //
843-
.groupingBy("Configuration properties",
844-
bean -> bean.toArchitecturallyEvidentType().isConfigurationProperties());
851+
.groupingBy(JMoleculesGroupings.getGroupings())
852+
.groupingBy(SpringGroupings.getGroupings());
845853
}
846854

855+
/**
856+
* Creates a {@link CanvasOptions} instance that does not register any default component {@link Grouping}s.
857+
*
858+
* @return will never be {@literal null}.
859+
* @see #defaults()
860+
* @see Groupings
861+
*/
847862
public static CanvasOptions withoutDefaultGroupings() {
848863
return new CanvasOptions(new ArrayList<>(), null, null, true, true);
849864
}
850865

866+
/**
867+
* Creates a new {@link CanvasOptions} with the given {@link Grouping}s added.
868+
*
869+
* @param groupings must not be {@literal null}.
870+
* @return will never be {@literal null}.
871+
*/
851872
public CanvasOptions groupingBy(Grouping... groupings) {
852873

853874
var result = new ArrayList<>(groupers);
@@ -1075,6 +1096,10 @@ public static Predicate<SpringBean> subtypeOf(Class<?> type) {
10751096
.and(bean -> !bean.getType().isEquivalentTo(type));
10761097
}
10771098

1099+
public static Predicate<SpringBean> isAnnotatedWith(Class<? extends Annotation> type) {
1100+
return bean -> bean.getType().isAnnotatedWith(type);
1101+
}
1102+
10781103
/**
10791104
* Returns the name of the {@link Grouping}.
10801105
*
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.modulith.docs;
17+
18+
import static org.springframework.modulith.docs.Documenter.CanvasOptions.Grouping.*;
19+
20+
import java.lang.annotation.Annotation;
21+
import java.util.ArrayList;
22+
import java.util.function.Predicate;
23+
24+
import org.jmolecules.architecture.cqrs.Command;
25+
import org.jmolecules.architecture.cqrs.CommandDispatcher;
26+
import org.jmolecules.architecture.cqrs.CommandHandler;
27+
import org.jmolecules.architecture.cqrs.QueryModel;
28+
import org.jmolecules.architecture.hexagonal.Adapter;
29+
import org.jmolecules.architecture.hexagonal.Application;
30+
import org.jmolecules.architecture.hexagonal.Port;
31+
import org.jmolecules.architecture.hexagonal.PrimaryAdapter;
32+
import org.jmolecules.architecture.hexagonal.PrimaryPort;
33+
import org.jmolecules.architecture.hexagonal.SecondaryAdapter;
34+
import org.jmolecules.architecture.hexagonal.SecondaryPort;
35+
import org.jmolecules.architecture.layered.ApplicationLayer;
36+
import org.jmolecules.architecture.layered.DomainLayer;
37+
import org.jmolecules.architecture.layered.InfrastructureLayer;
38+
import org.jmolecules.architecture.layered.InterfaceLayer;
39+
import org.jmolecules.architecture.onion.classical.ApplicationServiceRing;
40+
import org.jmolecules.architecture.onion.classical.DomainModelRing;
41+
import org.jmolecules.architecture.onion.classical.DomainServiceRing;
42+
import org.jmolecules.architecture.onion.simplified.ApplicationRing;
43+
import org.jmolecules.architecture.onion.simplified.DomainRing;
44+
import org.jmolecules.architecture.onion.simplified.InfrastructureRing;
45+
import org.springframework.modulith.core.SpringBean;
46+
import org.springframework.modulith.docs.Documenter.CanvasOptions.Grouping;
47+
import org.springframework.util.ClassUtils;
48+
49+
/**
50+
* A collection of {@link Grouping}s.
51+
*
52+
* @author Oliver Drotbohm
53+
*/
54+
public class Groupings {
55+
56+
/**
57+
* Spring Framework-related {@link Grouping}s.
58+
*
59+
* @author Oliver Drotbohm
60+
*/
61+
public static class SpringGroupings {
62+
63+
/**
64+
* Returns Spring Framework-related {@link Grouping}s.
65+
*
66+
* @return will never be {@literal null}.
67+
*/
68+
public static Grouping[] getGroupings() {
69+
70+
return new Grouping[] {
71+
of("Controllers", bean -> bean.toArchitecturallyEvidentType().isController()),
72+
of("Services", bean -> bean.toArchitecturallyEvidentType().isService()),
73+
of("Repositories", bean -> bean.toArchitecturallyEvidentType().isRepository()),
74+
of("Event listeners", bean -> bean.toArchitecturallyEvidentType().isEventListener()),
75+
of("Configuration properties",
76+
bean -> bean.toArchitecturallyEvidentType().isConfigurationProperties())
77+
};
78+
}
79+
}
80+
81+
/**
82+
* jMolecules-related {@link Grouping}s.
83+
*
84+
* @author Oliver Drotbohm
85+
*/
86+
public static class JMoleculesGroupings {
87+
88+
private static final boolean JMOLECULES_CQRS_PRESENT = ClassUtils
89+
.isPresent("org.jmolecules.architecture.cqrs.Command", JMoleculesGroupings.class.getClassLoader());
90+
91+
private static final boolean JMOLECULES_HEXAGONAL_PRESENT = ClassUtils
92+
.isPresent("org.jmolecules.architecture.hexagonal.Port", JMoleculesGroupings.class.getClassLoader());
93+
94+
private static final boolean JMOLECULES_LAYERS_PRESENT = ClassUtils
95+
.isPresent("org.jmolecules.architecture.layered", JMoleculesGroupings.class.getClassLoader());
96+
97+
private static final boolean JMOLECULES_ONION_PRESENT = ClassUtils
98+
.isPresent("org.jmolecules.architecture.onion.classical.ApplicationRing",
99+
JMoleculesGroupings.class.getClassLoader());
100+
101+
public static Grouping[] getGroupings() {
102+
103+
var groupings = new ArrayList<Grouping>();
104+
105+
if (JMOLECULES_CQRS_PRESENT) {
106+
107+
groupings.add(of("Commands", packageOrTypeAnnotatedWith(Command.class)));
108+
groupings.add(of("Command dispatchers", packageOrTypeAnnotatedWith(CommandDispatcher.class)));
109+
groupings.add(of("Command handlers", packageOrTypeAnnotatedWith(CommandHandler.class)));
110+
groupings.add(of("Query models", packageOrTypeAnnotatedWith(QueryModel.class)));
111+
}
112+
113+
if (JMOLECULES_HEXAGONAL_PRESENT) {
114+
115+
groupings.add(of("Primary ports", packageOrTypeAnnotatedWith(PrimaryPort.class)));
116+
groupings.add(of("Secondary ports", packageOrTypeAnnotatedWith(SecondaryPort.class)));
117+
groupings.add(of("Ports", packageOrTypeAnnotatedWith(Port.class)));
118+
119+
groupings.add(of("Application", packageOrTypeAnnotatedWith(Application.class)));
120+
121+
groupings.add(of("Primary adapters", packageOrTypeAnnotatedWith(PrimaryAdapter.class)));
122+
groupings.add(of("Secondary adapters", packageOrTypeAnnotatedWith(SecondaryAdapter.class)));
123+
groupings.add(of("Adapters", packageOrTypeAnnotatedWith(Adapter.class)));
124+
}
125+
126+
if (JMOLECULES_LAYERS_PRESENT) {
127+
128+
groupings.add(of("Application layer", packageOrTypeAnnotatedWith(ApplicationLayer.class)));
129+
groupings.add(of("Domain layer", packageOrTypeAnnotatedWith(DomainLayer.class)));
130+
groupings.add(of("Infrastructure layer", packageOrTypeAnnotatedWith(InfrastructureLayer.class)));
131+
groupings.add(of("Interface layer", packageOrTypeAnnotatedWith(InterfaceLayer.class)));
132+
}
133+
134+
if (JMOLECULES_ONION_PRESENT) {
135+
136+
groupings.add(of("Application ring", packageOrTypeAnnotatedWith(ApplicationRing.class)));
137+
groupings.add(of("Domain ring", packageOrTypeAnnotatedWith(DomainRing.class)));
138+
groupings.add(of("Infrastructure ring", packageOrTypeAnnotatedWith(InfrastructureRing.class)));
139+
140+
groupings.add(of("Application service ring", packageOrTypeAnnotatedWith(ApplicationServiceRing.class)));
141+
groupings.add(of("Domain service ring", packageOrTypeAnnotatedWith(DomainServiceRing.class)));
142+
groupings.add(of("Domain model ring", packageOrTypeAnnotatedWith(DomainModelRing.class)));
143+
groupings.add(of("Infrastructure ring",
144+
packageOrTypeAnnotatedWith(
145+
org.jmolecules.architecture.onion.classical.InfrastructureRing.class)));
146+
}
147+
148+
return groupings.toArray(Grouping[]::new);
149+
}
150+
}
151+
152+
private static Predicate<SpringBean> packageOrTypeAnnotatedWith(Class<? extends Annotation> annotation) {
153+
154+
return bean -> {
155+
156+
var type = bean.getType();
157+
var pkg = type.getPackage();
158+
159+
return type.isAnnotatedWith(annotation) || type.isMetaAnnotatedWith(annotation) //
160+
|| pkg.isAnnotatedWith(annotation) || pkg.isMetaAnnotatedWith(annotation);
161+
};
162+
}
163+
}

src/docs/asciidoc/60-documentation.adoc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@ _Others_
245245
It consists of the following sections:
246246

247247
* __The application module's base package.__
248-
* __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>>.
248+
* __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>>.
249+
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.
249250
* __Exposed aggregate roots__ -- Any entities that we find repositories for or explicitly declared as aggregate via jMolecules.
250251
* __Application events published by the module__ -- Those event types need to be demarcated using jMolecules `@DomainEvent` or implement its `DomainEvent` interface.
251252
* __Application events listened to by the module__ -- Derived from methods annotated with Spring's `@EventListener`, `@TransactionalEventListener`, jMolecules' `@DomainEventHandler` or beans implementing `ApplicationListener`.

0 commit comments

Comments
 (0)