diff --git a/pom.xml b/pom.xml index c20998c2..9792668f 100644 --- a/pom.xml +++ b/pom.xml @@ -96,6 +96,7 @@ limitations under the License. spring-modulith-integration-test + spring-modulith-example diff --git a/spring-modulith-example/pom.xml b/spring-modulith-example/pom.xml new file mode 100644 index 00000000..27526186 --- /dev/null +++ b/spring-modulith-example/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.0.0-SNAPSHOT + + + + spring-modulith-example + Spring Modulith - Example + 0.1.0-SNAPSHOT + + + 17 + + + + + + org.springframework.experimental + spring-modulith-bom + 0.1.0-SNAPSHOT + pom + import + + + org.jmolecules + jmolecules-bom + 2022.2.0-SNAPSHOT + pom + import + + + + + + + + org.springframework.boot + spring-boot-starter + + + + org.springframework.boot + spring-boot-starter-json + + + + org.springframework.experimental + spring-modulith-starter-jpa + + + + + + org.jmolecules + jmolecules-events + + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.h2database + h2 + runtime + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.experimental + spring-modulith-starter-test + test + + + + + + org.projectlombok + lombok + true + + + + org.springframework.boot + spring-boot-configuration-processor + true + + + + + + + spring-snapshot + https://repo.spring.io/snapshot + + false + + + + spring-milestone + https://repo.spring.io/milestone + + false + + + + + + + spring-snapshot + https://repo.spring.io/snapshot + + false + + + + spring-milestone + https://repo.spring.io/milestone + + false + + + + + diff --git a/spring-modulith-example/src/main/java/example/Application.java b/spring-modulith-example/src/main/java/example/Application.java new file mode 100644 index 00000000..826492cc --- /dev/null +++ b/spring-modulith-example/src/main/java/example/Application.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 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; + +import example.order.Order; +import example.order.OrderManagement; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; +import org.springframework.scheduling.annotation.EnableAsync; + +/** + * Spring Modulith example application + * + * @author Oliver Drotbohm + */ +@EnableAsync +@SpringBootApplication +@ConfigurationPropertiesScan +public class Application { + + public static void main(String... args) throws Exception { + + SpringApplication.run(Application.class, args) + .getBean(OrderManagement.class) + .complete(new Order()); + } +} diff --git a/spring-modulith-example/src/main/java/example/inventory/InventoryInternal.java b/spring-modulith-example/src/main/java/example/inventory/InventoryInternal.java new file mode 100644 index 00000000..6190b9c3 --- /dev/null +++ b/spring-modulith-example/src/main/java/example/inventory/InventoryInternal.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.inventory; + +import org.springframework.stereotype.Component; + +/** + * @author Oliver Drotbohm + */ +@Component +class InventoryInternal {} diff --git a/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java b/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java new file mode 100644 index 00000000..7c0e6973 --- /dev/null +++ b/spring-modulith-example/src/main/java/example/inventory/InventoryManagement.java @@ -0,0 +1,49 @@ +/* + * Copyright 2022 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.inventory; + +import example.order.OrderCompleted; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Service; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * @author Oliver Drotbohm + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class InventoryManagement { + + private final InventoryInternal dependency; + + @Async + @TransactionalEventListener + void on(OrderCompleted event) throws InterruptedException { + + var orderId = event.getOrderId(); + + LOG.info("Received order completion for {}.", orderId); + + // Simulate busy work + Thread.sleep(1000); + + LOG.info("Finished order completion for {}.", orderId); + } +} diff --git a/spring-modulith-example/src/main/java/example/inventory/InventorySettings.java b/spring-modulith-example/src/main/java/example/inventory/InventorySettings.java new file mode 100644 index 00000000..2bab8d95 --- /dev/null +++ b/spring-modulith-example/src/main/java/example/inventory/InventorySettings.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 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.inventory; + +import lombok.AccessLevel; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.ConstructorBinding; + +/** + * Some Spring Boot configuration properties exposed by the inventory. + * + * @author Oliver Drotbohm + */ +@Value +@RequiredArgsConstructor(access = AccessLevel.PACKAGE, onConstructor = @__(@ConstructorBinding)) +@ConfigurationProperties("example.inventory") +class InventorySettings { + + /** + * Some Javadoc. + */ + int stockThreshold; +} diff --git a/spring-modulith-example/src/main/java/example/order/Order.java b/spring-modulith-example/src/main/java/example/order/Order.java new file mode 100644 index 00000000..2e11429b --- /dev/null +++ b/spring-modulith-example/src/main/java/example/order/Order.java @@ -0,0 +1,34 @@ +/* + * Copyright 2022 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.order; + +import lombok.Value; + +import java.util.UUID; + +/** + * @author Oliver Drotbohm + */ +@Value +public class Order { + + private OrderIdentifier id = new OrderIdentifier(UUID.randomUUID()); + + @Value + public static class OrderIdentifier { + UUID id; + } +} diff --git a/spring-modulith-example/src/main/java/example/order/OrderCompleted.java b/spring-modulith-example/src/main/java/example/order/OrderCompleted.java new file mode 100644 index 00000000..661d2e7d --- /dev/null +++ b/spring-modulith-example/src/main/java/example/order/OrderCompleted.java @@ -0,0 +1,29 @@ +/* + * Copyright 2022 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.order; + +import example.order.Order.OrderIdentifier; +import lombok.Value; + +import org.jmolecules.event.types.DomainEvent; + +/** + * @author Oliver Drotbohm + */ +@Value +public class OrderCompleted implements DomainEvent { + OrderIdentifier orderId; +} diff --git a/spring-modulith-example/src/main/java/example/order/OrderManagement.java b/spring-modulith-example/src/main/java/example/order/OrderManagement.java new file mode 100644 index 00000000..9200dc1a --- /dev/null +++ b/spring-modulith-example/src/main/java/example/order/OrderManagement.java @@ -0,0 +1,39 @@ +/* + * Copyright 2022 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.order; + +import example.order.internal.OrderInternal; +import lombok.RequiredArgsConstructor; + +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author Oliver Drotbohm + */ +@Service +@RequiredArgsConstructor +public class OrderManagement { + + private final ApplicationEventPublisher events; + private final OrderInternal dependency; + + @Transactional + public void complete(Order order) { + events.publishEvent(new OrderCompleted(order.getId())); + } +} diff --git a/spring-modulith-example/src/main/java/example/order/internal/OrderInternal.java b/spring-modulith-example/src/main/java/example/order/internal/OrderInternal.java new file mode 100644 index 00000000..72640fab --- /dev/null +++ b/spring-modulith-example/src/main/java/example/order/internal/OrderInternal.java @@ -0,0 +1,24 @@ +/* + * Copyright 2022 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.order.internal; + +import org.springframework.stereotype.Component; + +/** + * @author Oliver Drotbohm + */ +@Component +public class OrderInternal {} diff --git a/spring-modulith-example/src/main/resources/application.properties b/spring-modulith-example/src/main/resources/application.properties new file mode 100644 index 00000000..f3767c76 --- /dev/null +++ b/spring-modulith-example/src/main/resources/application.properties @@ -0,0 +1,5 @@ +spring.jackson.constructor-detector=use-properties-based + +spring.jpa.show-sql=true + +logging.level.org.springframework.modulith=info \ No newline at end of file diff --git a/spring-modulith-example/src/main/resources/logback.xml b/spring-modulith-example/src/main/resources/logback.xml new file mode 100644 index 00000000..472b60e7 --- /dev/null +++ b/spring-modulith-example/src/main/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + %d %5p %40.40c:%4L - %m%n + + + + + + + + + + + \ No newline at end of file diff --git a/spring-modulith-example/src/test/java/example/ApplicationIntegrationTests.java b/spring-modulith-example/src/test/java/example/ApplicationIntegrationTests.java new file mode 100644 index 00000000..8ca81beb --- /dev/null +++ b/spring-modulith-example/src/test/java/example/ApplicationIntegrationTests.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 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; + +import example.order.Order; +import example.order.OrderManagement; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +/** + * Integration test for the overall application. + * + * @author Oliver Drotbohm + */ +@SpringBootTest +class ApplicationIntegrationTests { + + @Autowired OrderManagement orders; + + @Test + void completesOrder() throws Exception { + + orders.complete(new Order()); + + Thread.sleep(2000); + } +} diff --git a/spring-modulith-example/src/test/java/example/ModularityTests.java b/spring-modulith-example/src/test/java/example/ModularityTests.java new file mode 100644 index 00000000..0bb40cbc --- /dev/null +++ b/spring-modulith-example/src/test/java/example/ModularityTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2022 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; + +import org.junit.jupiter.api.Test; +import org.springframework.modulith.docs.Documenter; +import org.springframework.modulith.docs.Documenter.CanvasOptions; +import org.springframework.modulith.docs.Documenter.Options; +import org.springframework.modulith.model.ApplicationModules; + +/** + * Tests to verify the modular structure and generate documentation for the modules. + * + * @author Oliver Drotbohm + */ +class ModularityTests { + + ApplicationModules modules = ApplicationModules.of(Application.class); + + @Test + void verifiesModularStructure() { + modules.verify(); + } + + @Test + void createModuleDocumentation() { + + var canvasOptions = CanvasOptions.defaults(); + + new Documenter(modules) // + .writeDocumentation(Options.defaults(), canvasOptions); + } +} diff --git a/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java b/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java new file mode 100644 index 00000000..c93b8895 --- /dev/null +++ b/spring-modulith-example/src/test/java/example/order/EventPublicationRegistryTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2022 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.order; + +import static org.assertj.core.api.Assertions.*; + +import example.order.EventPublicationRegistryTests.FailingAsyncTransactionalEventListener; +import lombok.RequiredArgsConstructor; + +import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Import; +import org.springframework.modulith.events.EventPublicationRegistry; +import org.springframework.modulith.test.ApplicationModuleTest; +import org.springframework.scheduling.annotation.Async; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.transaction.event.TransactionalEventListener; + +/** + * A show case for how the Spring Modulith application event publication registry keeps track of incomplete publications + * for failing transactional event listeners + * + * @author Oliver Drotbohm + */ +@ApplicationModuleTest +@RequiredArgsConstructor +@Import(FailingAsyncTransactionalEventListener.class) +@DirtiesContext +class EventPublicationRegistryTests { + + private final OrderManagement orders; + private final EventPublicationRegistry registry; + + @Test + void leavesPublicationIncompleteForFailingListener() throws Exception { + + var order = new Order(); + + orders.complete(order); + + Thread.sleep(40); + + assertThat(registry.findIncompletePublications()).hasSize(1); + } + + static class FailingAsyncTransactionalEventListener { + + @Async + @TransactionalEventListener + void foo(OrderCompleted event) { + throw new IllegalStateException(); + } + } +} diff --git a/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java b/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java new file mode 100644 index 00000000..8d520151 --- /dev/null +++ b/spring-modulith-example/src/test/java/example/order/OrderIntegrationTests.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 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.order; + +import static org.assertj.core.api.Assertions.*; + +import lombok.RequiredArgsConstructor; + +import org.junit.jupiter.api.Test; +import org.springframework.modulith.test.ApplicationModuleTest; +import org.springframework.modulith.test.PublishedEvents; + +/** + * @author Oliver Drotbohm + */ +@ApplicationModuleTest +@RequiredArgsConstructor +class OrderIntegrationTests { + + private final OrderManagement orders; + + @Test + void publishesOrderCompletion(PublishedEvents events) { + + var reference = new Order(); + + orders.complete(reference); + + assertThat(events.ofType(OrderCompleted.class)) + .hasSize(1) + .element(0) + .extracting(OrderCompleted::getOrderId).isEqualTo(reference.getId()); + } +}