Maven Multi-modules, Maven repositories and Docker registries
On a project I’m currently working on we’re developing Maven multi-module projects. However, the challenge that we had was that only one of these modules needs to be deployed to a Maven repository. The other module only must be built into a Docker image and pushed to a Docker registry.
Maven Multi-modules
On this project we’re developing microservices that expose REST interfaces using Spring Boot. Because a lot of clients for these microservices are also being built as Spring Boot microservices, there was some discussion on if/how the Data Transfer Objects (DTOs) should be shared across these applications. Lots of articles (and probably even more opinions) about how/why sharing DTO classes should (not) be done are already available, so in this article we will not focus on this discussion, but purely on the technical implementation.
The decision was made that we were going to use a Maven multi-module application containing the following modules:
- service: This module contains the Spring Boot implementation of the microservice. This module will be built into a Docker image and pushed to a Docker registry.
- client: This module contains a simple client interface for interaction with an (externally) running instance of the service module. This module should be built and deployed to a Maven repository so it can be included as a dependency in other Spring Boot projects.
- dto: This module only contains the DTO model classes. This module will only serve to make these classes available in the service and client modules. This module will only be built in the multi-module, but not published anywhere.
This has the following advantages for us:
- Still independently deployable and loosely coupled: As long as a service doesn’t introduce a breaking change, older versions of client dependencies keep working. Only if a client application actually wants to use new features from the service, it needs to update the version of the client dependency.
- Reduced code duplication; both the services and clients don’t have to specify the DTO objects themselves.
- Because the service module also exposes an OpenAPI specification of the dto module, it’s not mandatory to use to client module. Non-Spring Boot applications (like .NET backends, Angular frontends, etc.) can just use this OpenAPI specification to construct their own client implementation.
Deploying (only) the client module to a Maven repository
For deploying the client module we’re just going to use the Maven Release Plugin. We configure this plugin on the parent pom of our project, so we can just perform the following command to create the desired Git tags and artifacts in our Maven repository:
$ mvn release:clean release:prepare release:perform
However, because we specified the plugin in the parent pom, this would mean that all modules will be published. This is not what we want. The service module doesn’t need to end up in the Maven repository, because we will immediately wrap it in a Docker image in the next chapter. Also, we don’t want the dto module to become available as a separate artifact in Maven.
Not deploying certain artifacts in a Maven multi-module is very easy. We can just set the maven.deploy.skip property to true for both the service and dto modules.
However, because the client module does use the dto module as a dependency, applications that will include the client would need the dto module to be available to load it as a transitive dependency. Luckily we can resolve this by using the Maven Shade Plugin. By configuring this plugin in the following way on the client module, we basically tell Maven to take everything from the dto module, and repackage all of its contents within the client JAR, making it a “fat JAR”:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<artifactSet>
<includes>
<include>nl.mikeheeren:dto</include>
</includes>
</artifactSet>
</configuration>
</execution>
</executions>
</plugin>
Pushing (only) the service module to a Docker registry
After the Maven release command finished, the client JAR has been published to the Maven repository, but the other modules are built as well. However, those JARs are now only available in the target folders from the build system. But now that those are available as well, we can create a simple Dockerfile which will take the built service-<VERSION>.jar file and put it in an OpenJDK image:
FROM openjdk:17-alpine
COPY /target/service-*.jar service.jar
CMD [ "java", "-jar", "service.jar"]
If we now execute the following commands on the same system the Maven release plugin was ran, the Docker image will be created and published to the Docker registry as well:
$ docker build -t service service
$ docker tag service ${REGISTRY_URL}:${REGISTRY_PORT}/service:latest
$ docker push ${REGISTRY_URL}:${REGISTRY_PORT}/service:latest
When we want to run this image, the only thing we have to do now is starting the Docker container:
$ docker run -p 8080:8080 ${REGISTRY_URL}:${REGISTRY_PORT}/service:latest
Using the client module in another application
Now that we have a running instance of our service available in a Docker container, it’s time to use the client module from another application. By adding the client dependency, we get all the desired functionality:
<dependency>
<groupId>nl.mikeheeren</groupId>
<artifactId>client</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
One of the classes that will become available via this dependency, is the BlogService. We can just create an instance and fire requests against it. For example, here is a method that will fetch all existing blogs, store a new one and return the full list of all blogs (transformed to the Publications DTO from the consumer application) in the response:
@RestController
@RequestMapping("publications")
public class PublicationController {
private final BlogService blogService;
public PublicationController(@Value("${blog.service.url}") String blogServiceUrl) {
blogService = BlogService.create(blogServiceUrl);
}
@PostMapping(path = "publish")
public ResponseEntity<List<Publication>> publish(@RequestBody Publication publication) {
if (!publication.getType().equalsIgnoreCase("BLOG")) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Unsupported publication type");
}
var allBlogs = blogService.getBlogs();
var saved = blogService.saveBlog(toBlog(publication));
allBlogs.add(saved);
return ResponseEntity.ok(allBlogs.stream()
.map(PublicationController::toPublication)
.toList()
);
}
private static Blog toBlog(Publication publication) {
// ...
}
private static Publication toPublication(Blog blog) {
// ...
}
}
And if another application wants to use our service, but it’s not a Spring Boot application (like a .NET backend or an Angular frontend for example)? Well, just use the OpenAPI documentation and write your own client logic. Nothing forces other consumers to use the client module!
Presumed that the service application is running at the default port 8080, the OpenAPI documentation can be found at:
http://localhost:8080/swagger-ui.html
Example application sources
All example sources can be found in the following Bitbucket repository:
https://bitbucket.org/whitehorsesbv/spring-boot-service-client-modules
It contains all the service, client and dto modules, as well as the example for using the client module in another application (other-application-example).
Tip: If you want to do some testing with Maven repositories but don’t have access to one, just start a sonatype/nexus3 Docker container!
Geen reacties
Geef jouw mening
Reactie plaatsenReactie toevoegen