Maven - Comment créer un projet multi-modules

Dans ce tutoriel, nous allons vous montrer comment utiliser Maven pour gérer un projet multi-module contenant quatre modules:
-
Module de mot de passe - Interface uniquement.
-
Module de mot de passe md5 - Implémentation du module de mot de passe, hachage de mot de passe MD5.
-
Module de mot de passe sha - Implémentation du module de mot de passe, hachage de mot de passe SHA.
-
Module Web - Une application Web MVC simple pour hacher une entrée avec l'algorithme MD5 ou SHA.
La dépendance du module.
$ password $ password <-- password-md5 $ password <-- password-sha $ web <-- (password-md5 | password-sha) <-- password
Quelques commandes pour construire un projet multi-module, par exemple:
$ mvn -pl password compile # compile password module only $ mvn -pl password-sha compile # compile password-sha module, also dependency - password $ mvn -pl web compile # compile web module only $ mvn -am -pl web compile # compile web module, also dependency - password-sha or password-md5, password $ mvn -pl web jetty:run # run web module with Jetty $ mvn compile # compile everything
Technologies utilisées:
-
Maven 3.5.3
-
JDK 8
-
Spring 5.1.0.RELEASE
-
thymeleaf 3.0.10.RELEASE
1. Structure du répertoire
Dans une mise en page de projet à plusieurs modules, un projet parent contient unpom.xml parent et chaque sous-module (sous-projets) contient également ses proprespom.xml

2. Maven POM
Examinons les fichiers POM multi-modules de Maven.
2.1 A Parent POM file, packaging is pom.
pom.xml
4.0.0 com.example.multi java-multi-modules pom 1.0 web password password-sha password-md5
2.2 In password module.
password/pom.xml
java-multi-modules com.example.multi 1.0 4.0.0 com.example.password password 1.0 jar
2.3 In password-md5 module.
password-md5/pom.xml
java-multi-modules com.example.multi 1.0 4.0.0 com.example.password password-md5 1.0 jar com.example.password password 1.0
2.4 In password-sha module.
password-sha/pom.xml
java-multi-modules com.example.multi 1.0 4.0.0 com.example.password password-sha 1.0 jar com.example.password password 1.0
2.4 In web module.
web/pom.xml
com.example.multi java-multi-modules 1.0 4.0.0 com.example web 1.0 war com.example.password password-sha 1.0
3. Projet parent

3.1 Parent pom, those properties and dependencies (JDK 8, JUnit 5, Spring 5) are shared by all sub modules.
pom.xml
4.0.0 com.example.multi java-multi-modules pom 1.0 UTF-8 1.8 1.8 5.3.1 5.1.0.RELEASE web password password-sha password-md5 org.springframework spring-context ${spring.version} org.junit.jupiter junit-jupiter-engine ${junit.version} test org.junit.jupiter junit-jupiter-params ${junit.version} test org.apache.maven.plugins maven-surefire-plugin 2.22.0 org.apache.maven.plugins maven-dependency-plugin 3.1.1
4. Module de mot de passe
4.1 A module containing only one interface.

4.2 POM file.
password/pom.xml
java-multi-modules com.example.multi 1.0 4.0.0 com.example.password password 1.0 jar
4.3 An interface.
PasswordService.java
package com.example.password;
public interface PasswordService {
String hash(String input);
String algorithm();
}
5. Module Password-md5
5.1 Password module implementation.

5.2 POM file.
password-md5/pom.xml
java-multi-modules com.example.multi 1.0 4.0.0 com.example.password password-md5 1.0 jar com.example.password password 1.0
5.3 MD5 hashing.
PasswordServiceImpl.java
package com.example.password;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@Service
public class PasswordServiceImpl implements PasswordService {
@Override
public String hash(String input) {
return md5(input);
}
@Override
public String algorithm() {
return "md5";
}
private String md5(String input) {
StringBuilder result = new StringBuilder();
MessageDigest md;
try {
md = MessageDigest.getInstance("MD5");
byte[] hashInBytes = md.digest(input.getBytes(StandardCharsets.UTF_8));
for (byte b : hashInBytes) {
result.append(String.format("%02x", b));
}
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(e);
}
return result.toString();
}
}
5.4 Unit test.
TestPasswordService.java
package com.example.password;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestPasswordService {
PasswordService passwordService;
@BeforeEach
void init() {
passwordService = new PasswordServiceImpl();
}
@DisplayName("md5 -> hex")
@ParameterizedTest
@CsvSource({
"123456, e10adc3949ba59abbe56e057f20f883e",
"hello world, 5eb63bbbe01eeed093cb22bb8f5acdc3"
})
void testMd5hex(String input, String expected) {
assertEquals(expected, passwordService.hash(input));
}
}
6. Module Password-sha
6.1 Password module implementation.

6.2 POM file.
password-sha/pom.xml
java-multi-modules com.example.multi 1.0 4.0.0 com.example.password password-sha 1.0 jar 1.11 commons-codec commons-codec ${commos.codec.version} com.example.password password 1.0
6.3 SHA hashing with the Apache common codec library
PasswordServiceImpl.java
package com.example.password;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.stereotype.Service;
@Service
public class PasswordServiceImpl implements PasswordService {
@Override
public String hash(String input) {
return DigestUtils.sha256Hex(input);
}
@Override
public String algorithm() {
return "sha256";
}
}
6.4 Unit test.
TestPasswordService.java
package com.example.password;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class TestPasswordService {
PasswordService passwordService;
@BeforeEach
void init() {
passwordService = new PasswordServiceImpl();
}
@DisplayName("sha256 -> hex")
@ParameterizedTest
@CsvSource({
"123456, 8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92",
"hello world, b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
})
void testSha256hex(String input, String expected) {
assertEquals(expected, passwordService.hash(input));
}
}
7. Module Web
7.1 A Spring MCV + thymeleaf web application, hash an input with a md5 or sha algorithm.

7.2 POM file.
web/pom.xml
com.example.multi java-multi-modules 1.0 4.0.0 com.example web 1.0 war 3.0.10.RELEASE com.example.password password-sha 1.0 org.springframework spring-webmvc ${spring.version} org.springframework spring-test ${spring.version} ch.qos.logback logback-classic 1.2.3 org.hamcrest hamcrest-library 1.3 test javax.servlet javax.servlet-api 3.1.0 provided org.thymeleaf thymeleaf ${thymeleaf.version} org.thymeleaf thymeleaf-spring5 ${thymeleaf.version} java-web-project org.eclipse.jetty jetty-maven-plugin 9.4.12.v20180830 org.apache.maven.plugins maven-war-plugin 3.2.2
7.3 Web module dependency
Terminal
$ mvn -pl web dependency:tree [INFO] com.example:web:war:1.0 [INFO] +- com.example.password:password-sha:jar:1.0:compile [INFO] | +- commons-codec:commons-codec:jar:1.11:compile [INFO] | \- com.example.password:password:jar:1.0:compile [INFO] +- org.springframework:spring-webmvc:jar:5.1.0.RELEASE:compile [INFO] | +- org.springframework:spring-aop:jar:5.1.0.RELEASE:compile [INFO] | +- org.springframework:spring-beans:jar:5.1.0.RELEASE:compile [INFO] | +- org.springframework:spring-core:jar:5.1.0.RELEASE:compile [INFO] | | \- org.springframework:spring-jcl:jar:5.1.0.RELEASE:compile [INFO] | +- org.springframework:spring-expression:jar:5.1.0.RELEASE:compile [INFO] | \- org.springframework:spring-web:jar:5.1.0.RELEASE:compile [INFO] +- org.springframework:spring-test:jar:5.1.0.RELEASE:compile [INFO] +- ch.qos.logback:logback-classic:jar:1.2.3:compile [INFO] | +- ch.qos.logback:logback-core:jar:1.2.3:compile [INFO] | \- org.slf4j:slf4j-api:jar:1.7.25:compile [INFO] +- org.hamcrest:hamcrest-library:jar:1.3:test [INFO] | \- org.hamcrest:hamcrest-core:jar:1.3:test [INFO] +- javax.servlet:javax.servlet-api:jar:3.1.0:provided [INFO] +- org.thymeleaf:thymeleaf:jar:3.0.10.RELEASE:compile [INFO] | +- ognl:ognl:jar:3.1.12:compile [INFO] | | \- org.javassist:javassist:jar:3.20.0-GA:compile [INFO] | +- org.attoparser:attoparser:jar:2.0.5.RELEASE:compile [INFO] | \- org.unbescape:unbescape:jar:1.1.6.RELEASE:compile [INFO] +- org.thymeleaf:thymeleaf-spring5:jar:3.0.10.RELEASE:compile [INFO] +- org.springframework:spring-context:jar:5.1.0.RELEASE:compile [INFO] +- org.junit.jupiter:junit-jupiter-engine:jar:5.3.1:test [INFO] | +- org.apiguardian:apiguardian-api:jar:1.0.0:test [INFO] | +- org.junit.platform:junit-platform-engine:jar:1.3.1:test [INFO] | | +- org.junit.platform:junit-platform-commons:jar:1.3.1:test [INFO] | | \- org.opentest4j:opentest4j:jar:1.1.1:test [INFO] | \- org.junit.jupiter:junit-jupiter-api:jar:5.3.1:test [INFO] \- org.junit.jupiter:junit-jupiter-params:jar:5.3.1:test [INFO] ------------------------------------------------------------------------
7.4 Spring stuff, integrate thymeleaf and configuration.
SpringConfig.java
package com.example.web.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
@EnableWebMvc
@Configuration
@ComponentScan({"com.example"})
public class SpringConfig implements WebMvcConfigurer {
@Autowired
private ApplicationContext applicationContext;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/resources/");
}
@Bean
public SpringResourceTemplateResolver templateResolver() {
SpringResourceTemplateResolver templateResolver = new SpringResourceTemplateResolver();
templateResolver.setApplicationContext(this.applicationContext);
templateResolver.setPrefix("/WEB-INF/views/");
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCacheable(true);
return templateResolver;
}
@Bean
public SpringTemplateEngine templateEngine() {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver());
templateEngine.setEnableSpringELCompiler(true);
return templateEngine;
}
@Bean
public ThymeleafViewResolver viewResolver() {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setTemplateEngine(templateEngine());
return viewResolver;
}
}
WebInitializer.java
package com.example.web;
import com.example.web.config.SpringConfig;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class>[] getRootConfigClasses() {
return null;
}
@Override
protected Class>[] getServletConfigClasses() {
return new Class[]{SpringConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
}
WelcomeController.java
package com.example.web.controller;
import com.example.password.PasswordService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class WelcomeController {
private final Logger logger = LoggerFactory.getLogger(WelcomeController.class);
@Autowired
private PasswordService passwordService;
@GetMapping("/")
public String welcome(@RequestParam(name = "query",
required = false, defaultValue = "123456") String query, Model model) {
logger.debug("Welcome to example.com... Query : {}", query);
model.addAttribute("query", query);
model.addAttribute("hash", passwordService.hash(query));
model.addAttribute("algorithm", passwordService.algorithm());
return "index";
}
}
7.5 Unit Test for Spring MVC.
TestWelcome.java
package com.example.web;
import com.example.web.config.SpringConfig;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringJUnitWebConfig(SpringConfig.class)
@DisplayName("Test Spring MVC default view")
public class TestWelcome {
private MockMvc mockMvc;
@Autowired
private WebApplicationContext webAppContext;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(webAppContext).build();
}
@Test
public void testDefault() throws Exception {
this.mockMvc.perform(
get("/"))
.andExpect(status().isOk())
.andExpect(view().name("index"))
.andExpect(model().attribute("query", "123456"));
//.andExpect(model().attribute("sha256", "8d969eef6ecad3c29a3a629280e686cf0c3f5d5a86aff3ca12020c923adc6c92"))
//.andExpect(model().attribute("md5", "e10adc3949ba59abbe56e057f20f883e"));
}
}
7.6 Thymeleaf view.
index.html
Terminé.
8. Demo
8.1 In parent project, uses the standard mvn compile to compile all the modules, Maven will decide the build order.
Terminal
$ mvn compile [INFO] Reactor Summary: [INFO] [INFO] java-multi-modules 1.0 ............................. SUCCESS [ 0.007 s] [INFO] password ........................................... SUCCESS [ 0.539 s] [INFO] password-sha ....................................... SUCCESS [ 0.038 s] [INFO] web ................................................ SUCCESS [ 0.080 s] [INFO] password-md5 1.0 ................................... SUCCESS [ 0.035 s] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.822 s [INFO] Finished at: 2018-10-23T15:32:21+08:00 [INFO] ------------------------------------------------------------------------
8.2 Install all the modules into the local repository.
Terminal
$ mvn install
8.3 Compiles the web module only (Maven will find the password module dependency from the local repository, that’s why we need the mvn install
Terminal
$ mvn -pl web compile
8.4 Alternatively, add -am to compile the web module and also its dependency modules (password-sha and password).
Terminal
$ mvn -am -pl web compile [INFO] Scanning for projects... [INFO] ------------------------------------------------------------------------ [INFO] Reactor Build Order: [INFO] [INFO] java-multi-modules [pom] [INFO] password [jar] [INFO] password-sha [jar] [INFO] web [war] ...... [INFO] Reactor Summary: [INFO] [INFO] java-multi-modules 1.0 ............................. SUCCESS [ 0.009 s] [INFO] password ........................................... SUCCESS [ 0.534 s] [INFO] password-sha ....................................... SUCCESS [ 0.036 s] [INFO] web 1.0 ............................................ SUCCESS [ 0.077 s] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 0.780 s [INFO] Finished at: 2018-10-23T15:36:16+08:00 [INFO] ------------------------------------------------------------------------
8.5 Test the web module with mvn -pl web jetty:run command.
Terminal
$ mvn install
$ mvn -pl web jetty:run
//...
[INFO] 1 Spring WebApplicationInitializers detected on classpath
[INFO] DefaultSessionIdManager workerName=node0
[INFO] No SessionScavenger set, using defaults
[INFO] node0 Scavenging every 600000ms
[INFO] Started ServerConnector@40a8a26f{HTTP/1.1,[http/1.1]}{0.0.0.0:8080}
[INFO] Started @6398ms
[INFO] Started Jetty Server


8.8 Update to md5 algorithm. Redémarrez le serveur Jetty.
web/pom.xml
com.example.password
password-md5
1.0

Télécharger le code source
$ git clone https://github.com/example/maven-examples.git
$ cd java-multi-modules
$ mvn install
$ mvn -pl web jetty:run