Main Tutorials

Maven – How to create a multi module project

In this tutorial, we will show you how to use Maven to manage a Multi-module project containing four modules :

  1. Password module – Interface only.
  2. Password md5 module – Password module implementation, MD5 password hashing.
  3. Password sha module – Password module implementation, SHA password hashing.
  4. Web module – A simple MVC web app to hash an input with either MD5 or SHA algorithm.

The module dependency.


$ password 

$ password <-- password-md5

$ password <-- password-sha

$ web <-- (password-md5 | password-sha) <-- password

Some commands to a build a multi-module project, for examples :


$ 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 used :

  1. Maven 3.5.3
  2. JDK 8
  3. Spring 5.1.0.RELEASE
  4. thymeleaf 3.0.10.RELEASE

1. Directory Structure

In a multi-module project layout, a parent project contains a parent pom.xml and each sub modules (sub projects) also containing own pom.xml

2. Maven POM

Let review the Maven multi-module POM files.

2.1 A Parent POM file, packaging is pom.

pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

	<!-- parent pom -->
    <groupId>com.mkyong.multi</groupId>
    <artifactId>java-multi-modules</artifactId>
    <packaging>pom</packaging>
	<version>1.0</version>

	<!-- sub modules -->
	<modules>
        <module>web</module>
        <module>password</module>
        <module>password-sha</module>
        <module>password-md5</module>
    </modules>
	
</project>

2.2 In password module.

password/pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">

	<!-- parent pom -->
    <parent>
        <artifactId>java-multi-modules</artifactId>
        <groupId>com.mkyong.multi</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

	<!-- password info -->
    <groupId>com.mkyong.password</groupId>
    <artifactId>password</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>
	
</project>

2.3 In password-md5 module.

password-md5/pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">
		 
    <parent>
        <artifactId>java-multi-modules</artifactId>
        <groupId>com.mkyong.multi</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

	<!-- password-md5 info -->
    <groupId>com.mkyong.password</groupId>
    <artifactId>password-md5</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

</project>

2.4 In password-sha module.

password-sha/pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">
		 
    <parent>
        <artifactId>java-multi-modules</artifactId>
        <groupId>com.mkyong.multi</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

	<!-- password-sha info -->
    <groupId>com.mkyong.password</groupId>
    <artifactId>password-sha</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

</project>

2.4 In web module.

web/pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <!-- this is a parent pom -->
    <parent>
        <groupId>com.mkyong.multi</groupId>
        <artifactId>java-multi-modules</artifactId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <!-- web project info -->
    <groupId>com.mkyong</groupId>
    <artifactId>web</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <dependencies>

		<!-- md5 or sha hashing -->
		
        <!-- md5 hash
        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password-md5</artifactId>
            <version>1.0</version>
        </dependency>
        -->

        <!-- sha -->
        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password-sha</artifactId>
            <version>1.0</version>
        </dependency>

    </dependencies>

</project>

3. Parent Project

3.1 Parent pom, those properties and dependencies (JDK 8, JUnit 5, Spring 5) are shared by all sub modules.

pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mkyong.multi</groupId>
    <artifactId>java-multi-modules</artifactId>
    <packaging>pom</packaging>
    <version>1.0</version>

    <properties>
        <!-- https://maven.apache.org/general.html#encoding-warning -->
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <junit.version>5.3.1</junit.version>
        <spring.version>5.1.0.RELEASE</spring.version>
    </properties>

    <modules>
        <module>web</module>
        <module>password</module>
        <module>password-sha</module>
        <module>password-md5</module>
    </modules>

    <dependencies>

        <!-- Spring DI for all modules -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- unit test -->
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>
		
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>${junit.version}</version>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.0</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.1.1</version>
            </plugin>
        </plugins>
    </build>
</project>

4. Password Module

4.1 A module containing only one interface.

4.2 POM file.

password/pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>java-multi-modules</artifactId>
        <groupId>com.mkyong.multi</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mkyong.password</groupId>
    <artifactId>password</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

</project>

4.3 An interface.

PasswordService.java

package com.mkyong.password;

public interface PasswordService {

    String hash(String input);

    String algorithm();

}

5. Password-md5 Module

5.1 Password module implementation.

5.2 POM file.

password-md5/pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>java-multi-modules</artifactId>
        <groupId>com.mkyong.multi</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <!-- password-md5 info -->
    <groupId>com.mkyong.password</groupId>
    <artifactId>password-md5</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <dependencies>
        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password</artifactId>
            <version>1.0</version>
        </dependency>
    </dependencies>

</project>

5.3 MD5 hashing.

PasswordServiceImpl.java

package com.mkyong.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.mkyong.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. Password-sha Module

6.1 Password module implementation.

6.2 POM file.

password-sha/pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>java-multi-modules</artifactId>
        <groupId>com.mkyong.multi</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.mkyong.password</groupId>
    <artifactId>password-sha</artifactId>
    <version>1.0</version>
    <packaging>jar</packaging>

    <properties>
        <commos.codec.version>1.11</commos.codec.version>
    </properties>

    <dependencies>

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
            <version>${commos.codec.version}</version>
        </dependency>

        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password</artifactId>
            <version>1.0</version>
        </dependency>

    </dependencies>

</project>

6.3 SHA hashing with the Apache common codec library

PasswordServiceImpl.java

package com.mkyong.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.mkyong.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. Web Module

7.1 A Spring MCV + thymeleaf web application, hash an input with a md5 or sha algorithm.

7.2 POM file.

web/pom.xml

<?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 
		 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <!-- this is a parent pom -->
    <parent>
        <groupId>com.mkyong.multi</groupId>
        <artifactId>java-multi-modules</artifactId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <!-- web project info -->
    <groupId>com.mkyong</groupId>
    <artifactId>web</artifactId>
    <version>1.0</version>
    <packaging>war</packaging>

    <properties>
        <thymeleaf.version>3.0.10.RELEASE</thymeleaf.version>
    </properties>

    <dependencies>

        <!-- md5 hash
        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password-md5</artifactId>
            <version>1.0</version>
        </dependency>
        -->

        <!-- sha -->
        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password-sha</artifactId>
            <version>1.0</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>${spring.version}</version>
        </dependency>

        <!-- logging , spring 5 no more bridge, thanks spring-jcl -->
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.2.3</version>
        </dependency>

        <!-- need this for unit test -->
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>hamcrest-library</artifactId>
            <version>1.3</version>
            <scope>test</scope>
        </dependency>

        <!-- servlet api -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>

        <!-- thymeleaf view -->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf</artifactId>
            <version>${thymeleaf.version}</version>
        </dependency>

        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>${thymeleaf.version}</version>
        </dependency>

    </dependencies>

    <build>
        <finalName>java-web-project</finalName>
        <plugins>

            <plugin>
                <groupId>org.eclipse.jetty</groupId>
                <artifactId>jetty-maven-plugin</artifactId>
                <version>9.4.12.v20180830</version>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-war-plugin</artifactId>
                <version>3.2.2</version>
            </plugin>

        </plugins>
    </build>

</project>

7.3 Web module dependency

Terminal

$ mvn -pl web dependency:tree

[INFO] com.mkyong:web:war:1.0
[INFO] +- com.mkyong.password:password-sha:jar:1.0:compile
[INFO] |  +- commons-codec:commons-codec:jar:1.11:compile
[INFO] |  \- com.mkyong.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.mkyong.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.mkyong"})
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.mkyong.web;

import com.mkyong.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.mkyong.web.controller;

import com.mkyong.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 mkyong.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.mkyong.web;

import com.mkyong.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

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<h1 th:text="'Input : ' + ${query}"/>
<h1 th:text="'Algorithm : ' + ${algorithm}"/>
<h3 th:text="${hash}" />
</body>
</html>

Done.

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.6 http://localhost:8080

8.7 http://localhost:8080/?query=mkyong

8.8 Update to md5 algorithm. Restart the Jetty server.

web/pom.xml

		<!-- md5 hash -->
        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password-md5</artifactId>
            <version>1.0</version>
        </dependency>
        
        <!-- sha
        <dependency>
            <groupId>com.mkyong.password</groupId>
            <artifactId>password-sha</artifactId>
            <version>1.0</version>
        </dependency>
        -->

8.9 http://localhost:8080/?query=mkyong

Download Source Code

$ git clone https://github.com/mkyong/maven-examples.git
$ cd java-multi-modules
$ mvn install
$ mvn -pl web jetty:run

References

  1. Tutorial: Thymeleaf + Spring
  2. Serving Web Content with Spring MVC
  3. A Multi-Module Project
  4. Spring IO – Creating a Multi Module Project
  5. Multi-module project builds with Maven and Gradle
  6. Maven – How to create a Java project
  7. Maven – How to create a Java web application project

About Author

author image
Founder of Mkyong.com, love Java and open source stuff. Follow him on Twitter. If you like my tutorials, consider make a donation to these charities.

Comments

Subscribe
Notify of
11 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Mazen Khalil
4 years ago

Great efforts! But you didn’t mention about the best practices for releasing such kind of projects and dealing with the repository. as the whole project with its submodules are located in single repository, how do you mange releases of different sub module versions (e.g. a bug in password-sha module will result to password-sha:2.0 module). Considering big project having concurrent sprints and different teams involved. In another words, how do you commit that to git repository and when do you mark it as tagged?

Naresh
4 years ago
Reply to  Mazen Khalil

The scenario which you are discussing is closely related to microservices. But this example’s aim is just to showcase multi-module maven projects.

Ghasem
4 years ago

I think you don’t need to declare explicitly the password module as a dependency in the password-sha and the password-md5 pom files.
If you declare modules in the parent pom respectively (from top to bottom), maven provides upper modules for bottom side modules.
Also I think you should declare the web module as a last module in the parent pom module no the first.

elo
4 years ago

hi, I have issues with the install, I have this error:
——————————————————————————-
Test set: com.mkyong.web.TestWelcome
——————————————————————————-
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 6.754 s <<< FAILURE! – in com.mkyong.web.TestWelcome
testDefault Time elapsed: 4.125 s <<< ERROR!
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "ServletContext resource [/WEB-INF/views/index.html]")
at com.mkyong.web.TestWelcome.testDefault(TestWelcome.java:37)
Caused by: org.thymeleaf.exceptions.TemplateInputException: An error happened during template parsing (template: "ServletContext resource [/WEB-INF/views/index.html]")
at com.mkyong.web.TestWelcome.testDefault(TestWelcome.java:37)
Caused by: java.io.FileNotFoundException: Could not open ServletContext resource [/WEB-INF/views/index.html]
at com.mkyong.web.TestWelcome.testDefault(TestWelcome.java:37)

I took the files from git, and indeed there is no WEB-INF directory in your project, no web.xml, no index.html (although I did see that one in the steps above).
Can you please help?

thank you

bumbleeexxx
4 years ago
Reply to  elo

yes it is missing and i wonder why nobody answers your question. You should only create the file index.html under the directory java-multi-modules/web/src/main/webapp/WEB-INF/views/index.html

you can copy the content of the file from above

Alex Ablood
4 years ago
Reply to  elo

Same here, did you figure this out? That .html whatever cause maven to fail build.

Jack
1 year ago

What is the point in specifying he package versions and also dependency’s versions in the submodules?

Niraj Patel
1 year ago

This is great. Thanks !

Stepan
3 years ago

Hello!Can i built a Jar file in root folder which will be contain all submodules and when i run it the app will start?

Ezequiel
3 years ago

Spring Boot, application.properties and multi module

Should all modules must contain the “application.properties” file?

I am leaving my file only in “application.properties”, however the application log is no longer being created in the given directory. I see the information now only on the Eclipse console. Do you know how I can solve it?

Jianfei
4 years ago

It’s really a good example !
I did every thing you showed here, but I still cannot use the interface from the dependency module.
You could check this question if you are interested : https://stackoverflow.com/questions/57866901/cannot-use-interface-or-class-from-another-local-dependency-module

Thanks again for your example!