gradle 적용

template 삭제
This commit is contained in:
Hyojin Ahn 2026-06-08 12:05:14 -04:00
parent e1d17b7af7
commit b3f8c84d15
37 changed files with 548 additions and 2206 deletions

View File

@ -1,57 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/classes" path="target/generated-sources/openapi/src/main/java">
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<classpathentry kind="src" output="bin/test" path="src/test/java">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" path="target/generated-sources/annotations">
<attributes>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="target/generated-test-sources/test-annotations">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

View File

@ -1,92 +0,0 @@
<factorypath>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter/3.5.4/spring-boot-starter-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot/3.5.4/spring-boot-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-context/6.2.9/spring-context-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-autoconfigure/3.5.4/spring-boot-autoconfigure-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-logging/3.5.4/spring-boot-starter-logging-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-classic/1.5.18/logback-classic-1.5.18.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/ch/qos/logback/logback-core/1.5.18/logback-core-1.5.18.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-to-slf4j/2.24.3/log4j-to-slf4j-2.24.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/logging/log4j/log4j-api/2.24.3/log4j-api-2.24.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/jul-to-slf4j/2.0.17/jul-to-slf4j-2.0.17.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/annotation/jakarta.annotation-api/2.1.1/jakarta.annotation-api-2.1.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-core/6.2.9/spring-core-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jcl/6.2.9/spring-jcl-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/yaml/snakeyaml/2.4/snakeyaml-2.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-web/3.5.4/spring-boot-starter-web-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-json/3.5.4/spring-boot-starter-json-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.19.2/jackson-datatype-jdk8-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.19.2/jackson-datatype-jsr310-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/module/jackson-module-parameter-names/2.19.2/jackson-module-parameter-names-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-tomcat/3.5.4/spring-boot-starter-tomcat-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-core/10.1.43/tomcat-embed-core-10.1.43.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-el/10.1.43/tomcat-embed-el-10.1.43.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/tomcat/embed/tomcat-embed-websocket/10.1.43/tomcat-embed-websocket-10.1.43.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-web/6.2.9/spring-web-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-beans/6.2.9/spring-beans-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-webmvc/6.2.9/spring-webmvc-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aop/6.2.9/spring-aop-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-expression/6.2.9/spring-expression-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-data-jpa/3.5.4/spring-boot-starter-data-jpa-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-jdbc/3.5.4/spring-boot-starter-jdbc-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/zaxxer/HikariCP/6.3.1/HikariCP-6.3.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-jdbc/6.2.9/spring-jdbc-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/orm/hibernate-core/6.6.22.Final/hibernate-core-6.6.22.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/persistence/jakarta.persistence-api/3.1.0/jakarta.persistence-api-3.1.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/transaction/jakarta.transaction-api/2.0.1/jakarta.transaction-api-2.0.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jboss/logging/jboss-logging/3.6.1.Final/jboss-logging-3.6.1.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hibernate/common/hibernate-commons-annotations/7.0.3.Final/hibernate-commons-annotations-7.0.3.Final.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/smallrye/jandex/3.2.0/jandex-3.2.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/classmate/1.7.0/classmate-1.7.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/net/bytebuddy/byte-buddy/1.17.6/byte-buddy-1.17.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/jaxb-runtime/4.0.5/jaxb-runtime-4.0.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/jaxb-core/4.0.5/jaxb-core-4.0.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/eclipse/angus/angus-activation/2.0.2/angus-activation-2.0.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/glassfish/jaxb/txw2/4.0.5/txw2-4.0.5.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/sun/istack/istack-commons-runtime/4.1.2/istack-commons-runtime-4.1.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/inject/jakarta.inject-api/2.0.1/jakarta.inject-api-2.0.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/antlr/antlr4-runtime/4.13.0/antlr4-runtime-4.13.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-jpa/3.5.2/spring-data-jpa-3.5.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/data/spring-data-commons/3.5.2/spring-data-commons-3.5.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-orm/6.2.9/spring-orm-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-tx/6.2.9/spring-tx-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/slf4j/slf4j-api/2.0.17/slf4j-api-2.0.17.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/spring-aspects/6.2.9/spring-aspects-6.2.9.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/aspectj/aspectjweaver/1.9.24/aspectjweaver-1.9.24.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-starter-actuator/3.5.4/spring-boot-starter-actuator-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-actuator-autoconfigure/3.5.4/spring-boot-actuator-autoconfigure-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springframework/boot/spring-boot-actuator/3.5.4/spring-boot-actuator-3.5.4.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-observation/1.15.2/micrometer-observation-1.15.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-commons/1.15.2/micrometer-commons-1.15.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-jakarta9/1.15.2/micrometer-jakarta9-1.15.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/micrometer/micrometer-core/1.15.2/micrometer-core-1.15.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/hdrhistogram/HdrHistogram/2.2.2/HdrHistogram-2.2.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/latencyutils/LatencyUtils/2.0.3/LatencyUtils-2.0.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/openapitools/jackson-databind-nullable/0.2.6/jackson-databind-nullable-0.2.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-databind/2.19.2/jackson-databind-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-annotations/2.19.2/jackson-annotations-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/core/jackson-core/2.19.2/jackson-core-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springdoc/springdoc-openapi-starter-webmvc-ui/2.8.6/springdoc-openapi-starter-webmvc-ui-2.8.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springdoc/springdoc-openapi-starter-webmvc-api/2.8.6/springdoc-openapi-starter-webmvc-api-2.8.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/springdoc/springdoc-openapi-starter-common/2.8.6/springdoc-openapi-starter-common-2.8.6.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/swagger/core/v3/swagger-core-jakarta/2.2.29/swagger-core-jakarta-2.2.29.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/swagger/core/v3/swagger-annotations-jakarta/2.2.29/swagger-annotations-jakarta-2.2.29.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/io/swagger/core/v3/swagger-models-jakarta/2.2.29/swagger-models-jakarta-2.2.29.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/validation/jakarta.validation-api/3.0.2/jakarta.validation-api-3.0.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/fasterxml/jackson/dataformat/jackson-dataformat-yaml/2.19.2/jackson-dataformat-yaml-2.19.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/webjars/swagger-ui/5.20.1/swagger-ui-5.20.1.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/webjars/webjars-locator-lite/1.1.0/webjars-locator-lite-1.1.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/h2database/h2/2.3.232/h2-2.3.232.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/guava/guava/33.4.8-jre/guava-33.4.8-jre.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/guava/failureaccess/1.0.3/failureaccess-1.0.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/jspecify/jspecify/1.0.0/jspecify-1.0.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/errorprone/error_prone_annotations/2.36.0/error_prone_annotations-2.36.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/com/google/j2objc/j2objc-annotations/3.0.0/j2objc-annotations-3.0.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/projectlombok/lombok/1.18.38/lombok-1.18.38.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/modelmapper/modelmapper/3.2.2/modelmapper-3.2.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/xml/bind/jakarta.xml.bind-api/4.0.2/jakarta.xml.bind-api-4.0.2.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/jakarta/activation/jakarta.activation-api/2.1.3/jakarta.activation-api-2.1.3.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="VARJAR" id="M2_REPO/org/apache/commons/commons-lang3/3.17.0/commons-lang3-3.17.0.jar" enabled="true" runInBatchMode="false"/>
<factorypathentry kind="PLUGIN" id="org.eclipse.jst.ws.annotations.core" enabled="false" runInBatchMode="false"/>
</factorypath>

9
.gitignore vendored
View File

@ -8,3 +8,12 @@ target/
!.mvn/wrapper/maven-wrapper.jar
/.allure
/allure-results
### Gradle ###
.gradle
### Eclipse output ###
bin/
### Logs ###
logs/

View File

@ -11,18 +11,18 @@
</arguments>
</buildCommand>
<buildCommand>
<name>org.springframework.ide.eclipse.boot.validation.springbootbuilder</name>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<name>org.springframework.ide.eclipse.boot.validation.springbootbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

View File

@ -0,0 +1,13 @@
arguments=
auto.sync=false
build.scans.enabled=false
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
connection.project.dir=
eclipse.preferences.version=1
gradle.user.home=
java.home=
jvm.arguments=
offline.mode=false
override.workspace.settings=false
show.console.view=false
show.executions.view=false

View File

@ -3,5 +3,4 @@ encoding//src/main/java=UTF-8
encoding//src/main/resources=UTF-8
encoding//src/test/java=UTF-8
encoding//src/test/resources=UTF-8
encoding//target/generated-sources/openapi/src/main/java=UTF-8
encoding/<project>=UTF-8

View File

@ -1,4 +0,0 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

576
README.md
View File

@ -1,576 +0,0 @@
> This documentation is also available in an enhanced form at
> [Layered Architecture Template](https://kamilmazurek.pl/layered-architecture-template) page.
# Layered Architecture Template
This repository contains a Spring Boot microservice template that follows a modern REST-based Layered Architecture approach. Designed with simplicity and clarity in mind, it provides a solid foundation for building Java applications that are easy to understand, extend, and maintain. The template separates concerns across common layers such as controllers, services, and repositories, and delivers a cleanly structured REST API ready for real-world use.
Core benefits:
* **Simplicity and Familiarity**: Widely adopted, this pattern is easy to understand and implement, especially for traditional enterprise teams.
* **Separation of Responsibilities**: The architecture organizes code into layers like controller, service, and repository, each handling its role clearly.
* **Maintainability**: Encapsulation of responsibilities within layers makes the application easier to debug, extend, and refactor over time.
* **Testability**: With clearly defined boundaries between layers, unit and integration testing become more straightforward and effective.
* **Scalability for Simple Use Cases**: Good fit for CRUD or moderate business logic apps, as layers support growth without much complexity.
It was designed to be minimalistic, organized, and flexible to change.
## Quickstart
You can quickly get started with the Layered Architecture Template by following these steps:
1. Confirm that a JDK is installed to build and run the app. Temurin, based on OpenJDK, is available at [adoptium.net](https://adoptium.net/).
2. Obtain the source code by cloning the repository using Git, or downloading it as a ZIP archive.
If you downloaded the ZIP file, extract it and navigate to the `layered-architecture-template` directory.
3. Launch the application with the development profile to preload sample data:
```shell
mvnw spring-boot:run -Pdev
```
4. Check if the app is running by sending a GET request to the following URL. You can also paste it directly into your browser:
```console
http://localhost:8080/items/1
```
You should see a response containing the following item:
```json
[
{
"id": 1,
"name":"Item A"
}
]
```
5. Customize the code as needed, then rebuild and relaunch the project to see your changes in action 🚀.
## Table of contents
* [Motivation](#motivation)
* [Architecture Overview](#architecture-overview)
* [When to Use Layered Architecture](#when-to-use-layered-architecture)
* [Technology Stack](#technology-stack)
* [How It Works](#how-it-works)
* [Build and Deployment](#build-and-deployment)
* [REST API Overview](#rest-api-overview)
* [Swagger and OpenAPI Endpoints](#swagger-and-openapi-endpoints)
* [Production-ready Features](#production-ready-features)
* [Testing Strategy](#testing-strategy)
* [Additional Resources](#additional-resources)
* [Author](#author)
* [Disclaimer](#disclaimer)
## Motivation
Starting new projects often involves repetitive setup work to establish a solid codebase.
This template aims to streamline that process by providing a clear, Layered Architecture based microservice example that can be reused and adapted quickly.
While Maven Archetypes offer automation, the effort required to maintain a flexible archetype felt too demanding.
Instead, this template balances simplicity and practicality to help developers get started faster.
## Architecture Overview
Layered Architecture is a classic software design pattern that organizes code into distinct layers, each with a specific responsibility.
This separation helps manage complexity, improve maintainability, and promote clear boundaries between different parts of an application.
The classic layers are:
* **Presentation Layer**: Responsible for handling user interactions and input/output operations.
* **Business Layer**: Contains the core application logic and rules.
* **Persistence Layer**: Manages data storage and retrieval from databases.
* **Database**: The actual data storage infrastructure where data physically resides.
In modern REST-based applications built with Spring Boot, these map naturally to:
* **API Layer**: Exposes REST endpoints and handles HTTP requests/responses (equivalent to Presentation).
* **Service Layer**: Implements business logic and orchestrates operations (equivalent to Business Logic).
* **Repository Layer**: Interfaces with the database, handling CRUD operations (equivalent to Persistence).
* **Database Layer**: Stores the application data.
Each layer communicates only with the one directly below it, ensuring clear boundaries and straightforward data flow.
This diagram illustrates the REST-based layer concept implemented in this project, while also showing the classic counterparts:
![Concept diagram](readme-images/layered-architecture-template-concept-diagram.png)
<p align="center">
<i>Layered Architecture Template concept diagram</i>
</p>
Layered Architecture remains a popular pattern for enterprise applications thanks to its clear separation of concerns and ease of understanding.
Spring Boot's convention-over-configuration approach and built-in support for RESTful services, data access, and testing tools make it particularly well-suited for implementing this pattern.
Together, they provide a solid foundation that helps teams quickly build and maintain scalable applications without unnecessary complexity.
Consequently, this repository provides a template implementation of a microservice following the Layered Architecture pattern, developed in Java with Spring Boot. It consists of:
* **API Layer**
* REST controllers handling HTTP requests
* **Service Layer**
* Business logic and service operations
* **Repository Layer**
* Data access and persistence interface
* **Supporting Components**
* Swagger for API documentation
* OpenAPI specifications
* Spring Boot Actuator for monitoring and management
* **Database Layer**
* In-memory H2 database for development and testing
Please keep in mind this project serves as a basic template, providing core support for HTTP request handling and database interactions.
It's flexible by design, allowing you to add features or integrations as your requirements evolve.
## When to Use Layered Architecture
Layered Architecture organizes your application into distinct layers, each with a specific responsibility such as presentation, business logic, and data access.
This clear separation simplifies development, improves maintainability, and helps enforce separation of concerns across your codebase.
This approach is particularly effective for projects with well-defined and stable requirements, where a straightforward division between layers can improve team collaboration and speed up delivery.
It works well for applications that primarily follow a request-response model, such as typical REST APIs, and where concerns like UI, service logic, and database access can be cleanly separated.
For smaller or less complex projects, Layered Architecture offers a familiar and easy-to-understand structure that helps avoid unnecessary complexity.
It also suits teams that prefer conventional architectural patterns or need quick onboarding of new developers.
However, for systems requiring high flexibility or integration with multiple external interfaces, more decoupled architectures like Hexagonal or Event-Driven might provide better adaptability.
In such cases, you might be interested in the [Hexagonal Architecture Template](https://kamilmazurek.pl/hexagonal-architecture-template).
Layered Architecture remains a solid choice when the focus is on clear organization, testability, and incremental development.
Ultimately, use Layered Architecture when your project benefits from a clear hierarchical structure that promotes simplicity, maintainability, and team alignment, especially when the application is expected to evolve steadily within a defined scope.
## Technology Stack
Layered Architecture Template is built using Java and Spring Boot, which naturally support modular design and clear separation between layers.
It uses the in-memory H2 database for quick prototyping and testing, but thanks to Spring Data, switching to another database later is simple and straightforward.
OpenAPI is used to clearly define the RESTful APIs exposed by the API layer, helping maintain a clean separation between layers and simplifying client generation.
This aligns well with the layered architecture's goal of separating presentation from business logic.
Testing is an integral part of the stack, with unit tests focusing on individual service and repository layers, and integration tests verifying end-to-end flow across layers.
Maven Surefire and Failsafe plugins ensure smooth test execution during builds.
Heres an overview of the technology stack:
- **Language & Framework**
- **Java 21**: Modern Java version powering the core application logic.
- **Spring Boot**: Simplifies building modular, RESTful Java applications.
- **API & Data**
- **OpenAPI**: Defines clear REST API specs and supports client generation.
- **ModelMapper**: Helps map data between layers smoothly.
- **H2 database**: Lightweight in-memory database for development and testing.
- **Testing**
- **JUnit**: Core framework for unit testing Java code.
- **REST Assured**: Integration testing for REST APIs.
- **Mockito**: Mocks dependencies to isolate components during tests.
- **Allure Report**: Generates detailed and user-friendly test reports.
- **Build & Deployment**
- **Apache Maven**: Manages builds and dependencies efficiently.
- **Docker**: Packages the app into containers for consistent deployment.
This stack was picked to support the layered design, focusing on clear separation, ease of testing, and flexibility to grow with the application's needs.
It provides a solid foundation for building maintainable, modular, and production-ready microservices.
## How It Works
This implementation follows Layered Architecture principles by organizing the application into distinct layers with clear responsibilities.
The key layers here are the presentation layer (controller), service layer (business logic), and data access layer (repository).
To illustrate how these layers interact, let's walk through a typical Read use case, starting with a `GET` request handled by the controller.
The `ItemsController` serves as the entry point for incoming HTTP requests. It receives the GET request, delegates processing to the service layer, and returns the appropriate HTTP response:
```java
@RestController
@AllArgsConstructor
public class ItemsController implements ItemsApi {
private final ItemsService service;
@Override
public ResponseEntity<ItemDTO> getItem(Long id) {
return service.getItem(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
(...)
}
```
The `ItemsService` contains the core business logic. It processes requests from the controller, applies any necessary rules or transformations, and interacts with the data access layer to fetch or modify data.
This separation keeps business rules centralized and reusable. It's also the place where more complex domain behavior or custom logic can be introduced as needed.
Since this example only covers a simple read operation, the service currently just retrieves data from the repository, maps between the entity, domain object, and DTO, and returns it to the controller.
```java
@Service
@AllArgsConstructor
public class ItemsService {
private final ItemsRepository repository;
private final ModelMapper mapper;
(...)
public Optional<ItemDTO> getItem(Long id) {
return repository.findById(id).map(this::toDomainObject).map(this::toDTO);
}
(...)
}
```
Data persistence and retrieval are handled by the `ItemsRepository`, which communicates with the database using Spring Data's `JpaRepository`.
This layer abstracts database operations and keeps the service layer decoupled from storage-specific implementation details.
For development and testing, the application uses an H2 in-memory database, which simplifies setup and allows for fast, isolated tests without the need for a full database installation.
```java
@Repository
public interface ItemsRepository extends JpaRepository<ItemEntity, Long> {
@Query("select max(item.id) from ItemEntity item")
Long findMaxID();
}
```
Once the data is retrieved, it flows back through the service to the controller, which formats it into an HTTP response sent to the client.
This clear layering helps maintain a strong separation of concerns, making the application easier to develop, test, and maintain.
By adhering to these layered principles, the application remains organized and scalable, allowing effective work on individual layers without tightly coupling components.
This results in cleaner code and a more maintainable system over time.
## Build and Deployment
This template is built with Spring Boot, which works well with the layered approach by clearly separating presentation, service, and data access responsibilities.
The build process is managed using Apache Maven, which handles dependency management, compilation, testing, and packaging.
To perform a full build, including compiling source code, running unit and integration tests, and installing the jar into your local Maven repo, run:
```shell
mvnw clean install
```
To run the application locally during development, you can use the following Maven command, which starts the Spring Boot application without requiring a separate packaging step:
```shell
mvnw spring-boot:run
```
Alternatively, you can package the application into an executable jar file and run it directly using the Java command. First, build the package with:
```shell
mvnw clean package
```
Then, start the application by executing the jar:
```shell
java -jar target/layered-architecture-template-1.0.0-SNAPSHOT.jar
```
The project includes a Dockerfile to simplify containerized deployment. To build a Docker image and run the application inside a container, use the following commands:
```shell
mvnw clean package
docker build -t template/layered-architecture-template .
docker run -p 8080:8080 template/layered-architecture-template
```
For easier development and testing, the project provides a special profile that preloads sample data. You can launch the application with this profile enabled by running:
```shell
mvnw spring-boot:run -Pdev
```
This setup offers a simple and flexible way to build, run, and deploy the application in various environments. Whether youre working locally, running tests, or preparing for production, these commands cover the essential steps for an efficient development workflow.
## REST API Overview
> **Note:** You can test all the endpoints below using Swagger UI, available at http://localhost:8080/swagger-ui/index.html.
The API is defined in [api.yaml](src/main/resources/api.yaml) and is intentionally kept simple, as this project is designed to serve as a template.
It supports the standard HTTP methods: `POST`, `GET`, `PUT`, and `DELETE`, providing basic CRUD operations for managing items, including create, read, update (or more precisely, upsert), and delete.
Each request is routed through the applications layered structure, allowing clear separation between the controller, service, and data access layers.
* `POST /items`: Creates a new item.
* `GET /items`: Retrieves all items.
* `GET /items/{itemId}`: Retrieves a single item by its ID.
* `PUT /items/{itemId}`: Creates or updates an item with the specified ID.
* `DELETE /items/{itemId}`: Deletes an item by its ID.
By default, the application runs on port `8080`. Once running, items can be retrieved by sending a `GET` request to the following endpoint:
```console
http://localhost:8080/items
```
The response will contain all items currently stored in the database. If no items are present, an empty array is returned:
```json
[]
```
New items can be added to the database using the `POST` method. For example, the following curl command can be used on Linux to create an item named "Item A":
```console
curl -i -X POST http://localhost:8080/items \
-H "Content-Type: application/json" \
-d '{"name":"Item A"}'
```
Items can also be added or updated using the `PUT` method. For example, the following curl command adds or updates an item with the ID `1`:
```console
curl -i -X PUT http://localhost:8080/items/1 \
-H "Content-Type: application/json" \
-d '{"id":1, "name":"Item A"}'
```
After adding the item, you can verify it by retrieving the list of items. A `GET` request to `/items` should now return the newly added entry:
```console
http://localhost:8080/items
```
```json
[
{
"id": 1,
"name":"Item A"
}
]
```
You can also fetch a specific item by its ID using the `/items/{id}` endpoint. For example, sending `GET /items/1` request:
```console
http://localhost:8080/items/1
```
```json
{
"id": 1,
"name":"Item A"
}
```
When the application is started with the `dev` profile enabled, it automatically loads a set of sample data into the database to facilitate easier development and testing.
As a result, sending a `GET` request to the `/items` endpoint will return a predefined list of items like the following:
```console
http://localhost:8080/items
```
```json
[
{
"id":1,
"name":"Item A"
},
{
"id":2,
"name":"Item B"
},
{
"id":3,
"name":"Item C"
}
]
```
Items can be removed from the database using the `DELETE` method.
For instance, to delete the item with ID `1`, you can execute the following curl command on a Linux terminal.
This will send a request to the server to remove the specified item:
```console
curl -i -X DELETE http://localhost:8080/items/1
```
As another option, you can perform all these operations such as GET, POST, PUT and DELETE through the Swagger user interface.
Simply navigate to http://localhost:8080/swagger-ui/index.html to explore and interact with the API endpoints in an easy and interactive way.
The REST API works well with the layered architecture by offering a clear and consistent way to interact with the application's core functionality.
This setup improves modularity and flexibility, letting each layer evolve independently while keeping communication smooth through well-defined API endpoints.
## Swagger and OpenAPI Endpoints
The application includes Swagger UI and an OpenAPI `/api-docs` endpoint that provide interactive API documentation.
These can be accessed locally at the following URLs:
* http://localhost:8080/swagger-ui/index.html
* http://localhost:8080/api-docs
Swagger UI offers a visual and interactive way to browse the API endpoints exposed by the applications presentation layer (more precisely, the controller in this template).
It simplifies sending `HTTP` requests such as `GET`, `POST`, `PUT`, and `DELETE`, which correspond to CRUD operations handled through the underlying service and data layers.
This makes Swagger a convenient tool for manual testing, debugging, and exploring how the API interacts with the different layers of the application.
For example, you can easily check what data is returned for a `GET /items` request:
![Swagger UI](readme-images/sample-swagger-view.png)
<p align="center">
<i>Sample Swagger UI view. For more details about Swagger, visit </i>
<a href="https://swagger.io"><i>https://swagger.io</i></a>
</p>
The OpenAPI `/api-docs` endpoint provides a machine-readable JSON specification of the API. This standardized format allows easy integration with various development tools, documentation generators, and client code generators, helping to maintain clear contracts between layers and teams. Sharing this specification fosters better collaboration and ensures that the API remains consistent with the layered architecture principles guiding the project.
These tools help reinforce the separation of concerns by clearly exposing the API endpoints managed by the presentation layer while hiding the complexities of the underlying service and data layers.
By maintaining this clear contract through Swagger and OpenAPI, you ensure that each layer can evolve independently without breaking the overall system architecture.
## Production-ready Features
The application uses Spring Boot Actuator, a library that adds production-ready features to Spring Boot applications. It provides capabilities like monitoring and health checks, which are enabled through the included configuration.
These features allow you to observe the health and status of the application across its layers, from the presentation layer down to the data layer, helping ensure that each part of the layered architecture is functioning properly.
Two important actuator endpoints configured in this template are:
* `/actuator` which lists all exposed actuator endpoints: http://localhost:8080/actuator/
* `/actuator/health` which shows the current health status of the application: http://localhost:8080/actuator/health
You can find the list of available actuator endpoints by accessing the `/actuator` endpoint in your running application.
This list can be customized by modifying the `management.endpoints.web.exposure.include` property in [application.yaml](src/main/resources/application.yaml).
For example, to enable the `beans` endpoint, add it to the `management.endpoints.web.exposure.include` list like this:
```yaml
management:
endpoints:
web:
exposure:
include: health, beans
```
For more details, visit the Spring Boot Actuator documentation: https://docs.spring.io/spring-boot/reference/actuator/endpoints.html
You can verify the health status of the application by sending a request to the `/actuator/health` endpoint.
The response will include the current state of the application, such as:
```console
http://localhost:8080/actuator/health
```
```json
{
"status": "UP"
}
```
By default, the application provides only basic health status information for security reasons.
If more detailed information is needed, such as disk space usage or database connectivity, this can be enabled by updating the [application.yaml](src/main/resources/application.yaml) configuration file as shown below:
```yaml
management:
endpoint:
health:
show-details: "always"
```
Applying this change results in more detailed information at the `/actuator/health` endpoint.
It also enables additional endpoints like `/actuator/health/db`, which provide details about the database:
```console
http://localhost:8080/actuator/health/db
```
```json
{
"status": "UP",
"details": {
"database": "H2",
"validationQuery": "isValid()"
}
}
```
These features help you monitor and maintain the application effectively, providing valuable insights into its health and performance across all layers of the architecture.
Proper use of actuator endpoints can improve reliability and simplify troubleshooting in both development and production environments.
**Important:** In production environments, actuator endpoints should be secured to prevent unauthorized access. It is recommended to restrict access using authentication and authorization mechanisms. Be cautious when enabling detailed health information or sensitive endpoints.
## Testing Strategy
This project includes a structured testing setup that combines unit tests and integration tests, helping ensure both individual components and their interactions behave as expected.
Test execution is handled using Mavens Surefire and Failsafe plugins, which are configured out of the box.
Testing is built around well-established tools and libraries:
* JUnit: Used for writing test cases and defining assertions.
* Mockito: Helps simulate dependencies in unit testing.
* REST Assured: Simplifies testing REST endpoints during integration testing.
This layered approach to testing aligns with the project's architecture and encourages maintainable, focused tests at every level.
There are two main categories of tests included in this project:
* Unit tests (`*Test.java`), which verify individual classes or methods in isolation. These run using the Maven Surefire Plugin.
* Integration tests (`*IntegrationTest.java`), used to verify how components interact. These tests run with the Maven Failsafe Plugin.
Here is a simple example of a JUnit unit test, `ItemsControllerTest`, which tests the `ItemsController` behavior.
It uses Mockito to mock the behavior of `ItemsService`, then requests an item using the controller and validates the response:
```java
class ItemsControllerTest {
@Test
void shouldGetItem() {
//given item
var item = new ItemDTO().id(1L).name("Item A");
//and service
var service = mock(ItemsService.class);
when(service.getItem(1L)).thenReturn(Optional.of(item));
//and controller
var controller = new ItemsController(service);
//when item is requested
var response = controller.getItem(1L);
//then response containing expected item is returned
assertEquals(item, response.getBody());
//and OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in retrieving the data
verify(service).getItem(1L);
}
(...)
}
```
Following that, here's an example of an integration test, `ItemsControllerIntegrationTest`, which verifies how multiple components work together to handle requests and responses.
This is also a JUnit test, but this one runs with `@SpringBootTest`, so it starts an H2 database and Spring context, including controllers, database connections and more. It uses REST-assured to perform requests and validate responses, testing the actual behavior across multiple layers:
```java
class ItemsControllerIntegrationTest extends AbstractIntegrationTest {
private final ObjectWriter objectWriter = new ObjectMapper().writer();
@Test
void shouldGetItem() throws JsonProcessingException {
when()
.get("/items/1")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(new ItemDTO().id(1L).name("Item A"))));
}
(...)
}
```
Unit tests, which verify individual components in isolation, can be executed using the Maven Surefire Plugin by running the following command:
```console
mvnw clean test
```
Integration tests, which validate how multiple components work together within the application, can be run using the Maven Failsafe Plugin:
```console
mvnw clean integration-test
```
Note that this command also executes unit tests as part of the build lifecycle.
Both unit and integration tests are executed automatically during the standard Maven build process:
```console
mvnw clean install
```
This helps ensure that the application behaves correctly across individual components as well as across layers, from the service logic to the data access, before packaging or deployment.
In addition, the project is configured to work with Allure Report, which provides a visual representation of test execution results.
You can generate and open the report in your browser by running the following commands:
```console
mvnw clean integration-test
mvnw allure:serve
```
The report provides a clear overview of test results, including which tests passed or failed, how long they took to run, and the overall coverage.
A sample view of the generated report is shown below:
![Allure Report](readme-images/sample-allure-report.png)
<p align="center">
<i>Sample Allure Report. For more information, visit</i>
<a href="https://allurereport.org/"><i>https://allurereport.org/</i></a>
</p>
This testing setup supports the layered architecture by ensuring that each level, from isolated service logic to fully integrated REST interactions, is thoroughly verified.
It helps maintain confidence that every layer of the application behaves reliably both on its own and in coordination with others.
## Additional resources
* [Layered Architecture, Baeldung](https://www.baeldung.com/cs/layered-architecture)
* [Multitier architecture, Wikipedia](https://en.wikipedia.org/wiki/Multitier_architecture)
* [Repository Pattern with Layered Architecture, Medium](https://medium.com/@leadcoder/repository-pattern-with-layered-architecture-35f7b9182ebf)
* [Layered Architecture Template on LibHunt](https://www.libhunt.com/r/layered-architecture-template)
## Author
This project was created by [Kamil Mazurek](https://kamilmazurek.pl), a Software Engineer based in Warsaw, Poland.
You can also find me on my [LinkedIn profile](https://www.linkedin.com/in/kamil-mazurek). Thanks for visiting 🙂
## Disclaimer
THIS SOFTWARE AND ANY ACCOMPANYING DOCUMENTATION (INCLUDING, BUT NOT LIMITED TO, THE README.MD FILE) ARE PROVIDED
FOR EDUCATIONAL PURPOSES ONLY.
THE SOFTWARE AND DOCUMENTATION ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE,
THE DOCUMENTATION, OR THE USE OR OTHER DEALINGS IN THE SOFTWARE OR DOCUMENTATION.
Spring Boot is a trademark of Broadcom Inc. and/or its subsidiaries.
Oracle, Java, MySQL, and NetSuite are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners.

148
build.gradle Normal file
View File

@ -0,0 +1,148 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.5.4'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.openapi.generator' version '7.9.0'
id 'io.qameta.allure' version '2.12.0'
}
group = 'template'
version = '1.0.0-SNAPSHOT'
description = 'Layered Architecture Template'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
// The repo contains two @SpringBootApplication classes (the leftover
// 'template.Application' and the real 'com.goi.integration.IntegrationServiceApplication').
// Pin the main class so bootJar/bootRun are unambiguous.
springBoot {
mainClass = 'com.goi.integration.IntegrationServiceApplication'
}
ext {
jacksonDatabindNullableVersion = '0.2.6'
springdocVersion = '2.8.6'
modelmapperVersion = '3.2.2'
allureVersion = '2.29.0'
guavaVersion = '33.4.8-jre'
}
// Equivalent of the Maven 'default' / 'dev' profiles, which only set the
// 'activeProfile' property that is filtered into application.yaml.
// Override with: ./gradlew bootRun -PactiveProfile=dev
def activeProfile = project.findProperty('activeProfile') ?: 'default'
repositories {
mavenCentral()
}
dependencies {
// spring
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// api
implementation "org.openapitools:jackson-databind-nullable:${jacksonDatabindNullableVersion}"
implementation "org.springdoc:springdoc-openapi-starter-webmvc-ui:${springdocVersion}"
// persistence
runtimeOnly 'com.h2database:h2'
// util
implementation "com.google.guava:guava:${guavaVersion}"
implementation "org.modelmapper:modelmapper:${modelmapperVersion}"
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured'
testImplementation "io.qameta.allure:allure-junit5:${allureVersion}"
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
}
// ---------------------------------------------------------------------------
// OpenAPI code generation (replaces openapi-generator-maven-plugin)
// ---------------------------------------------------------------------------
def openApiOutputDir = layout.buildDirectory.dir('generated/openapi')
openApiGenerate {
generatorName = 'spring'
inputSpec = layout.projectDirectory.file('src/main/resources/api.yaml').asFile.toString()
outputDir = openApiOutputDir.get().asFile.toString()
apiPackage = 'template.api'
modelPackage = 'template.api.model'
configOptions = [
interfaceOnly : 'true',
useSpringBoot3: 'true',
useTags : 'true'
]
// keep the generated output limited to interfaces + models
generateApiTests = false
generateApiDocumentation = false
generateModelTests = false
generateModelDocumentation = false
}
sourceSets {
main {
java {
srcDir openApiOutputDir.map { it.dir('src/main/java') }
}
}
}
tasks.named('compileJava') {
dependsOn tasks.named('openApiGenerate')
}
// ---------------------------------------------------------------------------
// Resource filtering: replaces Maven's @activeProfile@ token in application.yaml
// ---------------------------------------------------------------------------
tasks.named('processResources') {
inputs.property('activeProfile', activeProfile)
filesMatching(['**/application*.yaml', '**/application*.yml', '**/application*.properties']) {
filter(org.apache.tools.ant.filters.ReplaceTokens, tokens: [activeProfile: activeProfile.toString()])
}
}
// ---------------------------------------------------------------------------
// Tests: 'test' runs unit tests, 'integrationTest' runs *IntegrationTest
// (replaces the Maven surefire/failsafe split)
// ---------------------------------------------------------------------------
tasks.named('test', Test) {
useJUnitPlatform()
exclude '**/*IntegrationTest.*'
}
tasks.register('integrationTest', Test) {
description = 'Runs integration tests (*IntegrationTest).'
group = 'verification'
useJUnitPlatform()
include '**/*IntegrationTest.*'
testClassesDirs = sourceSets.test.output.classesDirs
classpath = sourceSets.test.runtimeClasspath
shouldRunAfter tasks.named('test')
}
tasks.named('check') {
dependsOn tasks.named('integrationTest')
}
// ---------------------------------------------------------------------------
// Allure reporting (replaces allure-maven)
// ---------------------------------------------------------------------------
allure {
version = allureVersion
adapter {
autoconfigure = true
aspectjWeaver = true
}
}

View File

@ -1,5 +1,5 @@
FROM openjdk:21-jdk-slim
RUN addgroup --system template-group && adduser --system --ingroup template-group template-user
USER template-user:template-group
COPY target/layered-architecture-template*.jar layered-architecture-template.jar
COPY build/libs/layered-architecture-template-*.jar layered-architecture-template.jar
ENTRYPOINT ["java","-jar","/layered-architecture-template.jar"]

8
gradle.properties Normal file
View File

@ -0,0 +1,8 @@
# Build settings
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
# Spring profile filtered into application.yaml (Maven 'default'/'dev' profile equivalent)
# Override on the command line, e.g. ./gradlew bootRun -PactiveProfile=dev
activeProfile=default

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

249
gradlew vendored Normal file
View File

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original 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.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

92
gradlew.bat vendored Normal file
View File

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

259
mvnw vendored
View File

@ -1,259 +0,0 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you 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
#
# http://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.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.2
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
if [ -n "${JAVA_HOME-}" ]; then
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

149
mvnw.cmd vendored
View File

@ -1,149 +0,0 @@
<# : batch portion
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM http://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.3.2
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
if ($env:MAVEN_USER_HOME) {
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
}
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

176
pom.xml
View File

@ -1,176 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/>
</parent>
<groupId>template</groupId>
<artifactId>layered-architecture-template</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>layered-architecture-template</name>
<description>Layered Architecture Template</description>
<properties>
<java.version>17</java.version>
<jackson-databind-nullable.version>0.2.6</jackson-databind-nullable.version>
<openapi-generator-maven-plugin.version>7.9.0</openapi-generator-maven-plugin.version>
<springdoc-openapi-starter-webmvc-ui.version>2.8.6</springdoc-openapi-starter-webmvc-ui.version>
<modelmapper.version>3.2.2</modelmapper.version>
<allure.version>2.29.0</allure.version>
<guava.version>33.4.8-jre</guava.version>
</properties>
<profiles>
<profile>
<id>default</id>
<properties>
<activeProfile>default</activeProfile>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>dev</id>
<properties>
<activeProfile>dev</activeProfile>
</properties>
</profile>
</profiles>
<dependencies>
<!-- spring -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- api -->
<dependency>
<groupId>org.openapitools</groupId>
<artifactId>jackson-databind-nullable</artifactId>
<version>${jackson-databind-nullable.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc-openapi-starter-webmvc-ui.version}</version>
</dependency>
<!-- persistence -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
<!-- util -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>${modelmapper.version}</version>
</dependency>
<!-- test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-junit5</artifactId>
<version>${allure.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${openapi-generator-maven-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>
${project.basedir}/src/main/resources/api.yaml
</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>template.api</apiPackage>
<modelPackage>template.api.model</modelPackage>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useTags>true</useTags>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
<!-- test -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/*IntegrationTest.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<executions>
<execution>
<configuration>
<includes>
<include>**/*IntegrationTest.java</include>
</includes>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.qameta.allure</groupId>
<artifactId>allure-maven</artifactId>
<version>2.15.2</version>
<configuration>
<reportVersion>${allure.version}</reportVersion>
</configuration>
</plugin>
</plugins>
</build>
</project>

1
settings.gradle Normal file
View File

@ -0,0 +1 @@
rootProject.name = 'integration-service'

View File

@ -1,17 +0,0 @@
package template;
import io.swagger.v3.oas.annotations.OpenAPIDefinition;
import io.swagger.v3.oas.annotations.info.Info;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import static org.springframework.boot.SpringApplication.run;
@SpringBootApplication
@OpenAPIDefinition(info = @Info(title = "Items API"))
public class Application {
public static void main(String[] args) {
run(Application.class, args);
}
}

View File

@ -1,63 +0,0 @@
package template.api;
import lombok.AllArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import template.api.model.ItemDTO;
import template.service.ItemsService;
import java.util.List;
import java.util.Objects;
@RestController
@AllArgsConstructor
public class ItemsController implements ItemsApi {
private final ItemsService service;
@Override
public ResponseEntity<ItemDTO> getItem(Long id) {
return service.getItem(id).map(ResponseEntity::ok).orElse(ResponseEntity.notFound().build());
}
@Override
public ResponseEntity<List<ItemDTO>> getItems() {
return ResponseEntity.ok(service.getItems().stream().toList());
}
@Override
public ResponseEntity<Void> postItem(ItemDTO itemDTO) {
if (itemDTO.getId() != null) {
return ResponseEntity.badRequest().build();
}
service.postItem(itemDTO);
return ResponseEntity.ok().build();
}
@Override
public ResponseEntity<Void> putItem(Long itemId, ItemDTO itemDTO) {
if (!hasValidId(itemId, itemDTO)) {
return ResponseEntity.badRequest().build();
}
service.putItem(itemId, itemDTO);
return ResponseEntity.ok().build();
}
@Override
public ResponseEntity<Void> deleteItem(Long id) {
if (service.getItem(id).isEmpty()) {
return ResponseEntity.notFound().build();
}
service.deleteItem(id);
return ResponseEntity.ok().build();
}
private boolean hasValidId(Long itemId, ItemDTO itemDTO) {
return itemDTO.getId() == null || Objects.equals(itemId, itemDTO.getId());
}
}

View File

@ -1,15 +0,0 @@
package template.config;
import org.modelmapper.ModelMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class TemplateConfig {
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
}

View File

@ -1,13 +0,0 @@
package template.exception;
import static java.lang.String.format;
public final class ItemIdAlreadySetException extends RuntimeException {
public static final String MESSAGE = "Item ID must be null when creating a new item. Expected null so the service can assign a new ID, but received: %s.";
public ItemIdAlreadySetException(Long id) {
super(format(MESSAGE, id));
}
}

View File

@ -1,24 +0,0 @@
package template.repository;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "item")
public class ItemEntity {
@Id
private Long id;
private String name;
}

View File

@ -1,13 +0,0 @@
package template.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
@Repository
public interface ItemsRepository extends JpaRepository<ItemEntity, Long> {
@Query("select max(item.id) from ItemEntity item")
Long findMaxID();
}

View File

@ -1,18 +0,0 @@
package template.service;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Item {
private Long id;
private String name;
}

View File

@ -1,76 +0,0 @@
package template.service;
import com.google.common.annotations.VisibleForTesting;
import lombok.AllArgsConstructor;
import org.modelmapper.ModelMapper;
import org.springframework.stereotype.Service;
import template.api.model.ItemDTO;
import template.exception.ItemIdAlreadySetException;
import template.repository.ItemEntity;
import template.repository.ItemsRepository;
import java.util.List;
import java.util.Optional;
@Service
@AllArgsConstructor
public class ItemsService {
private final ItemsRepository repository;
private final ModelMapper mapper;
public List<ItemDTO> getItems() {
return repository.findAll().stream().map(this::toDomainObject).map(this::toDTO).toList();
}
public Optional<ItemDTO> getItem(Long id) {
return repository.findById(id).map(this::toDomainObject).map(this::toDTO);
}
public void postItem(ItemDTO itemDTO) {
var item = toDomainObject(itemDTO);
if (item.getId() != null) {
throw new ItemIdAlreadySetException(item.getId());
}
var itemEntity = toEntity(item);
var maxID = repository.findMaxID();
itemEntity.setId(maxID + 1);
repository.save(itemEntity);
}
public void putItem(Long itemId, ItemDTO itemDTO) {
var item = toDomainObject(itemDTO);
item.setId(itemId);
repository.save(toEntity(item));
}
public void deleteItem(Long id) {
repository.deleteById(id);
}
@VisibleForTesting
ItemDTO toDTO(Item item) {
return mapper.map(item, ItemDTO.class);
}
@VisibleForTesting
Item toDomainObject(ItemDTO itemDTO) {
return mapper.map(itemDTO, Item.class);
}
@VisibleForTesting
Item toDomainObject(ItemEntity itemEntity) {
return mapper.map(itemEntity, Item.class);
}
@VisibleForTesting
ItemEntity toEntity(Item item) {
return mapper.map(item, ItemEntity.class);
}
}

View File

@ -1,6 +1,6 @@
spring:
profiles:
active: @activeProfile@
active: ${ACTIVE_PROFILE:default}
datasource:
url: jdbc:h2:mem:layered
jpa:

View File

@ -1,21 +0,0 @@
package template;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.server.LocalServerPort;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringBootTest(webEnvironment = RANDOM_PORT)
public abstract class AbstractIntegrationTest {
@LocalServerPort
private int port;
@BeforeEach
void setUp() {
RestAssured.port = this.port;
}
}

View File

@ -1,11 +0,0 @@
package template;
import org.junit.jupiter.api.Test;
class ApplicationIntegrationTest extends AbstractIntegrationTest {
@Test
void shouldStartContext() {
}
}

View File

@ -1,163 +0,0 @@
package template.api;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import org.junit.jupiter.api.Test;
import template.AbstractIntegrationTest;
import template.api.model.ItemDTO;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.emptyString;
import static org.hamcrest.Matchers.equalTo;
class ItemsControllerIntegrationTest extends AbstractIntegrationTest {
private final ObjectWriter objectWriter = new ObjectMapper().writer();
@Test
void shouldGetItem() throws JsonProcessingException {
when()
.get("/items/1")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(new ItemDTO().id(1L).name("Item A"))));
}
@Test
void shouldNotFindItem() {
when()
.get("/items/4")
.then()
.statusCode(404)
.assertThat()
.body(emptyString());
}
@Test
void shouldCreateItemByPostRequest() throws JsonProcessingException {
//given item
var item = new ItemDTO().name("Item D");
//when POST request with item is sent
given()
.contentType("application/json")
.body(item)
.when()
.post("/items")
.then()
.statusCode(200);
//then item can be retrieved by ID
var expectedItem = new ItemDTO().id(4L).name("Item D");
when()
.get("/items/4")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(expectedItem)));
//cleanup
when()
.delete("/items/4")
.then()
.statusCode(200);
}
@Test
void shouldNotAcceptPostRequestWhenItemHasID() {
given()
.contentType("application/json")
.body(new ItemDTO().id(4L).name("Item D"))
.when()
.post("/items")
.then()
.statusCode(400);
}
@Test
void shouldInsertItemByPutRequest() throws JsonProcessingException {
//given item
var item = new ItemDTO().id(4L).name("Item D");
//when PUT request with item is sent
given()
.contentType("application/json")
.body(item)
.when()
.put("/items/4")
.then()
.statusCode(200);
//then item can be retrieved by ID
when()
.get("/items/4")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(item)));
//cleanup
when()
.delete("/items/4")
.then()
.statusCode(200);
}
@Test
void shouldNotAcceptPutRequestWhenItemHasAmbiguousID() {
given()
.contentType("application/json")
.body(new ItemDTO().id(5L).name("Item E"))
.when()
.put("/items/6")
.then()
.statusCode(400);
}
@Test
void shouldDeleteItem() throws JsonProcessingException {
//given item
var item = new ItemDTO().id(4L).name("Item D");
//and item has been put
given()
.contentType("application/json")
.body(item)
.when()
.put("/items/4")
.then()
.statusCode(200);
//and item can be retrieved by ID
when()
.get("/items/4")
.then()
.statusCode(200)
.assertThat()
.body(equalTo(objectWriter.writeValueAsString(item)));
//when item is deleted
when()
.delete("/items/4")
.then()
.statusCode(200);
//then item can no longer be retrieved by ID
when()
.get("/items/4")
.then()
.statusCode(404);
}
@Test
void shouldNotFindItemToDelete() {
when()
.delete("/items/4")
.then()
.statusCode(404);
}
}

View File

@ -1,198 +0,0 @@
package template.api;
import org.junit.jupiter.api.Test;
import template.api.model.ItemDTO;
import template.service.ItemsService;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
import static org.springframework.http.HttpStatus.NOT_FOUND;
import static org.springframework.http.HttpStatus.OK;
import static template.util.TestItems.createTestItemDTOs;
class ItemsControllerTest {
@Test
void shouldGetItem() {
//given item
var item = new ItemDTO().id(1L).name("Item A");
//and service
var service = mock(ItemsService.class);
when(service.getItem(1L)).thenReturn(Optional.of(item));
//and controller
var controller = new ItemsController(service);
//when item is requested
var response = controller.getItem(1L);
//then response containing expected item is returned
assertEquals(item, response.getBody());
//and OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in retrieving the data
verify(service).getItem(1L);
}
@Test
void shouldNotFindItem() {
//given service
var service = mock(ItemsService.class);
when(service.getItem(1L)).thenReturn(Optional.empty());
//and controller
var controller = new ItemsController(service);
//when item is requested
var response = controller.getItem(1L);
//then response contains no item
assertNull(response.getBody());
//and Not Found status is returned
assertEquals(NOT_FOUND, response.getStatusCode());
//and service was involved in retrieving the data
verify(service).getItem(1L);
}
@Test
void shouldGetItems() {
//given service
var service = mock(ItemsService.class);
when(service.getItems()).thenReturn(createTestItemDTOs());
//and controller
var controller = new ItemsController(service);
//when items are requested
var response = controller.getItems();
//then response containing expected items is returned
assertEquals(createTestItemDTOs(), response.getBody());
//and OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in retrieving the data
verify(service).getItems();
}
@Test
void shouldPostItem() {
//given item
var item = new ItemDTO().name("Item A");
//and service
var service = mock(ItemsService.class);
//and controller
var controller = new ItemsController(service);
//when POST request with item is handled
var response = controller.postItem(item);
//then OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in saving the data
verify(service).postItem(item);
}
@Test
void shouldNotAcceptPostRequestWhenItemHasID() {
//given item
var item = new ItemDTO().id(1L).name("Item A");
//and service
var service = mock(ItemsService.class);
//and controller
var controller = new ItemsController(service);
//when POST request with item containing ID is received
var response = controller.postItem(item);
//then Bad Request status is returned
assertEquals(BAD_REQUEST, response.getStatusCode());
//and service was not involved in saving the data
verify(service, never()).postItem(any());
}
@Test
void shouldPutItem() {
//given item
var item = new ItemDTO().name("Item A");
//and service
var service = mock(ItemsService.class);
//and controller
var controller = new ItemsController(service);
//when item is put
var response = controller.putItem(1L, item);
//then OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in saving data
verify(service).putItem(1L, item);
}
@Test
void shouldDeleteItem() {
//given item
var item = new ItemDTO().id(1L).name("Item A");
//and service
var service = mock(ItemsService.class);
when(service.getItem(item.getId())).thenReturn(Optional.of(item));
//and controller
var controller = new ItemsController(service);
//when DELETE request is handled
var response = controller.deleteItem(item.getId());
//then OK status is returned
assertEquals(OK, response.getStatusCode());
//and service was involved in deleting the data
verify(service).deleteItem(item.getId());
}
@Test
void shouldNotFindItemToDelete() {
//given service
var service = mock(ItemsService.class);
//and controller
var controller = new ItemsController(service);
//and item id
var itemId = 1L;
//when DELETE request is handled
var response = controller.deleteItem(itemId);
//then Not Found status is returned
assertEquals(NOT_FOUND, response.getStatusCode());
//and service was not involved in deleting the data
verify(service, never()).deleteItem(any());
}
}

View File

@ -1,29 +0,0 @@
package template.misc;
import org.junit.jupiter.api.Test;
import template.AbstractIntegrationTest;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.containsString;
class ActuatorIntegrationTest extends AbstractIntegrationTest {
@Test
void shouldReturnResponseFromActuatorEndpoint() {
when()
.get("/actuator")
.then()
.statusCode(200)
.body(containsString("/actuator/health"));
}
@Test
void shouldReturnResponseFromHealthEndpoint() {
when()
.get("/actuator/health")
.then()
.statusCode(200)
.body(containsString("{\"status\":\"UP\"}"));
}
}

View File

@ -1,20 +0,0 @@
package template.misc;
import org.junit.jupiter.api.Test;
import template.AbstractIntegrationTest;
import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.containsString;
class OpenApiIntegrationTest extends AbstractIntegrationTest {
@Test
void shouldReturnResponseFromOpenApiEndpoint() {
when()
.get("/api-docs")
.then()
.statusCode(200)
.body(containsString("Items API"));
}
}

View File

@ -1,18 +0,0 @@
package template.misc;
import org.junit.jupiter.api.Test;
import template.AbstractIntegrationTest;
import static io.restassured.RestAssured.when;
class SwaggerIntegrationTest extends AbstractIntegrationTest {
@Test
void shouldReturnResponseFromSwaggerEndpoint() {
when()
.get("/swagger-ui/index.html")
.then()
.statusCode(200);
}
}

View File

@ -1,169 +0,0 @@
package template.service;
import org.junit.jupiter.api.Test;
import org.modelmapper.ModelMapper;
import template.api.model.ItemDTO;
import template.exception.ItemIdAlreadySetException;
import template.repository.ItemEntity;
import template.repository.ItemsRepository;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static template.util.TestItems.createTestItemDTOs;
import static template.util.TestItems.createTestItemEntities;
class ItemsServiceTest {
@Test
void shouldGetItem() {
//given entity
var entity = ItemEntity.builder().id(1L).name("Item A").build();
//and repository
var repository = mock(ItemsRepository.class);
when(repository.findById(entity.getId())).thenReturn(Optional.of(entity));
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is requested
var result = service.getItem(entity.getId());
//then expected item is returned
var item = service.toDomainObject(entity);
assertEquals(Optional.of(service.toDTO(item)), result);
//and repository was queried for data
verify(repository).findById(entity.getId());
}
@Test
void shouldNotFindItem() {
//given repository
var repository = mock(ItemsRepository.class);
when(repository.findById(1L)).thenReturn(Optional.empty());
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is requested
var result = service.getItem(1L);
//then no items are returned
assertTrue(result.isEmpty());
//and repository was queried for data
verify(repository).findById(1L);
}
@Test
void shouldGetItems() {
//given repository
var repository = mock(ItemsRepository.class);
when(repository.findAll()).thenReturn(createTestItemEntities());
//and service
var service = new ItemsService(repository, new ModelMapper());
//when items are requested
var items = service.getItems();
//then items are returned
assertEquals(createTestItemDTOs(), items);
//and repository was involved in retrieving the data
verify(repository).findAll();
}
@Test
void shouldCreateItem() {
//given DTO
var dto = new ItemDTO().name("Item A");
//and repository
var repository = mock(ItemsRepository.class);
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is created
service.postItem(dto);
//then item is saved in repository
var item = service.toDomainObject(dto);
var expectedEntity = service.toEntity(item);
expectedEntity.setId(1L);
verify(repository).save(expectedEntity);
}
@Test
void shouldNotCreateItem() {
//given DTO
var dto = new ItemDTO().name("Item A").id(1L);
//and repository
var repository = mock(ItemsRepository.class);
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is created
var exception = assertThrows(ItemIdAlreadySetException.class, () -> service.postItem(dto));
//then exception is thrown
var expectedMessage = "Item ID must be null when creating a new item. Expected null so the service can assign a new ID, but received: 1.";
assertEquals(expectedMessage, exception.getMessage());
//and item has not been saved in repository
verify(repository, never()).save(any());
}
@Test
void shouldPutItem() {
//given DTO
var dto = new ItemDTO().name("Item A");
//and repository
var repository = mock(ItemsRepository.class);
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is put
service.putItem(1L, dto);
//then item has been saved in repository with proper ID
var item = service.toDomainObject(dto);
var expectedEntity = service.toEntity(item);
expectedEntity.setId(1L);
verify(repository).save(expectedEntity);
}
@Test
void shouldDeleteItem() {
//given entity
var entity = ItemEntity.builder().id(1L).name("Item A").build();
//and repository
var repository = mock(ItemsRepository.class);
when(repository.findById(entity.getId())).thenReturn(Optional.of(entity));
//and service
var service = new ItemsService(repository, new ModelMapper());
//when item is deleted
service.deleteItem(entity.getId());
//then item is deleted from repository
verify(repository).deleteById(entity.getId());
}
}

View File

@ -1,35 +0,0 @@
package template.util;
import template.api.model.ItemDTO;
import template.service.Item;
import template.repository.ItemEntity;
import java.util.List;
public class TestItems {
public static List<ItemDTO> createTestItemDTOs() {
var itemA = new ItemDTO().id(1L).name("Item A");
var itemB = new ItemDTO().id(2L).name("Item B");
var itemC = new ItemDTO().id(3L).name("Item C");
return List.of(itemA, itemB, itemC);
}
public static List<Item> createTestItems() {
var itemA = Item.builder().id(1L).name("Item A").build();
var itemB = Item.builder().id(2L).name("Item B").build();
var itemC = Item.builder().id(3L).name("Item C").build();
return List.of(itemA, itemB, itemC);
}
public static List<ItemEntity> createTestItemEntities() {
var itemA = ItemEntity.builder().id(1L).name("Item A").build();
var itemB = ItemEntity.builder().id(2L).name("Item B").build();
var itemC = ItemEntity.builder().id(3L).name("Item C").build();
return List.of(itemA, itemB, itemC);
}
}