Welcome to Chapter 2 of our journey! In the previous chapter, we laid the groundwork by ensuring our development environment was properly set up with the latest Java Development Kit (JDK). With our tools in place, it’s time to elevate our project management capabilities.

This chapter will guide you through setting up a robust, production-ready Java project using Apache Maven. Maven is an indispensable build automation tool used predominantly for Java projects. It standardizes project structures, manages dependencies, and automates the build process, from compilation and testing to packaging and deployment. By the end of this chapter, you will have a fully configured Maven project, complete with proper directory structure, dependency management, and a foundational setup for logging and testing, ready for us to start building our “Simple Calculator” application in the next chapter.

The expected outcome is a well-structured Maven project that adheres to industry best practices, making it easy to manage dependencies, compile code, run tests, and package our application. This foundation is critical for any real-world application, ensuring scalability, maintainability, and collaboration.

Planning & Design: Maven Project Structure

Before diving into the implementation, let’s briefly outline the standard Maven project structure we’ll be establishing. Adhering to this convention is a cornerstone of Maven’s “convention over configuration” principle, making projects immediately understandable to other Java developers.

The core structure will be as follows:

my-java-projects/
├── pom.xml
└── src/
    ├── main/
    │   ├── java/        # Contains application source code (.java files)
    │   └── resources/   # Contains application resources (e.g., configuration files, static assets)
    └── test/
        ├── java/        # Contains test source code (.java files for unit/integration tests)
        └── resources/   # Contains test resources

The pom.xml (Project Object Model) file is the heart of a Maven project. It defines project coordinates, dependencies, build plugins, and various other configurations. We will carefully craft this file to meet our project’s needs, including specifying the Java version, integrating logging, and setting up our testing framework.

Step-by-Step Implementation

Let’s get started by creating our Maven project and configuring its essential components.

a) Setup/Configuration: Initializing the Maven Project

First, ensure you have Maven installed and added to your system’s PATH. You can verify this by opening your terminal or command prompt and typing:

mvn -v

You should see output similar to this (version numbers might differ, but ensure it’s Maven 3.x or newer):

Apache Maven 3.9.6 (XXXXXXX; 2023-12-18T11:51:25-06:00)
Maven home: /path/to/apache-maven-3.9.6
Java version: 24.0.2, vendor: Oracle Corporation, runtime: /path/to/jdk-24.0.2
Default locale: en_US, platform encoding: UTF-8
OS name: "mac os x", version: "14.4.1", arch: "aarch64", family: "mac"

Now, let’s create a new directory for our entire set of projects and then generate our first Maven project inside it. We’ll name our main project simple-calculator.

  1. Create the parent directory:

    mkdir my-java-projects
    cd my-java-projects
    
  2. Generate the Maven project using an archetype: Maven archetypes are project templates. We’ll use the maven-archetype-quickstart as a basic starting point.

    mvn archetype:generate -DgroupId=com.expert.java.calculator -DartifactId=simple-calculator -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=false
    
    • groupId: A unique identifier for your project, usually following a reverse domain name pattern.
    • artifactId: The name of the project.
    • archetypeArtifactId: The template to use. maven-archetype-quickstart creates a simple project with a Main class and a basic JUnit test.
    • archetypeVersion: The version of the archetype.
    • interactiveMode=false: Skips interactive prompts.

    After running this command, you’ll see a new directory simple-calculator within my-java-projects.

  3. Navigate into the new project and inspect pom.xml:

    cd simple-calculator
    ls
    

    You should see pom.xml and the src directory. Open the pom.xml file. It will look something like this:

    <!-- simple-calculator/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.expert.java.calculator</groupId>
      <artifactId>simple-calculator</artifactId>
      <version>1.0-SNAPSHOT</version>
    
      <name>simple-calculator</name>
      <!-- FIXME change it to the project's website -->
      <url>http://www.example.com</url>
    
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.7</maven.compiler.source>
        <maven.compiler.target>1.7</maven.compiler.target>
      </properties>
    
      <dependencies>
        <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.11</version>
          <scope>test</scope>
        </dependency>
      </dependencies>
    
      <build>
        <pluginManagement><!-- lock down plugins versions to avoid using Maven defaults (may be moved to parent pom) -->
          <plugins>
            <!-- clean lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#clean_Lifecycle -->
            <plugin>
              <artifactId>maven-clean-plugin</artifactId>
              <version>3.1.0</version>
            </plugin>
            <!-- default lifecycle, jar packaging: see https://maven.apache.org/ref/current/maven-core/default-bindings.html#Plugin_bindings_for_jar_packaging -->
            <plugin>
              <artifactId>maven-resources-plugin</artifactId>
              <version>3.0.2</version>
            </plugin>
            <plugin>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>3.8.0</version>
            </plugin>
            <plugin>
              <artifactId>maven-surefire-plugin</artifactId>
              <version>2.22.1</version>
            </plugin>
            <plugin>
              <artifactId>maven-jar-plugin</artifactId>
              <version>3.0.2</version>
            </plugin>
            <plugin>
              <artifactId>maven-install-plugin</artifactId>
              <version>2.5.2</version>
            </plugin>
            <plugin>
              <artifactId>maven-deploy-plugin</artifactId>
              <version>2.8.2</version>
            </plugin>
            <!-- site lifecycle, see https://maven.apache.org/ref/current/maven-core/lifecycles.html#site_Lifecycle -->
            <plugin>
              <artifactId>maven-site-plugin</artifactId>
              <version>3.7.1</version>
            </plugin>
            <plugin>
              <artifactId>maven-project-info-reports-plugin</artifactId>
              <version>3.0.0</version>
            </plugin>
          </plugins>
        </pluginManagement>
      </build>
    </project>
    

    The generated pom.xml uses older Java and JUnit versions. We need to update this to reflect current best practices as of 2025-12-04.

  4. Update pom.xml for Java 24, JUnit 5, and Logging:

    We’ll modify the <properties> section to specify Java 24, update the maven-compiler-plugin to support it, replace JUnit 4 with JUnit 5, and add a robust logging framework (SLF4J with Logback). We’ll also update the versions of other core plugins for stability and security.

    <!-- simple-calculator/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.expert.java.calculator</groupId>
      <artifactId>simple-calculator</artifactId>
      <version>1.0-SNAPSHOT</version>
      <packaging>jar</packaging> <!-- Explicitly declare packaging type -->
    
      <name>Simple Calculator Application</name>
      <description>A basic console-based calculator as part of a learning series.</description>
      <url>https://www.example.com/simple-calculator</url> <!-- Update with your project's URL -->
    
      <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>24</maven.compiler.source> <!-- Target Java 24 -->
        <maven.compiler.target>24</maven.compiler.target> <!-- Compile for Java 24 -->
        <junit.jupiter.version>5.10.2</junit.jupiter.version> <!-- Latest JUnit 5 as of 2025 -->
        <slf4j.version>2.0.12</slf4j.version> <!-- Latest SLF4J as of 2025 -->
        <logback.version>1.5.6</logback.version> <!-- Latest Logback as of 2025 -->
        <maven.compiler.plugin.version>3.13.0</maven.compiler.plugin.version>
        <maven.surefire.plugin.version>3.2.5</maven.surefire.plugin.version>
        <maven.jar.plugin.version>3.4.1</maven.jar.plugin.version>
      </properties>
    
      <dependencies>
        <!-- JUnit 5 for testing -->
        <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter-api</artifactId>
          <version>${junit.jupiter.version}</version>
          <scope>test</scope>
        </dependency>
        <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter-engine</artifactId>
          <version>${junit.jupiter.version}</version>
          <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-params</artifactId>
            <version>${junit.jupiter.version}</version>
            <scope>test</scope>
        </dependency>
    
        <!-- SLF4J API -->
        <dependency>
          <groupId>org.slf4j</groupId>
          <artifactId>slf4j-api</artifactId>
          <version>${slf4j.version}</version>
        </dependency>
        <!-- Logback Classic for SLF4J implementation -->
        <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>${logback.version}</version>
          <scope>runtime</scope> <!-- Logback is only needed at runtime -->
        </dependency>
        <!-- Logback Core for Logback Classic -->
        <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-core</artifactId>
          <version>${logback.version}</version>
          <scope>runtime</scope>
        </dependency>
      </dependencies>
    
      <build>
        <pluginManagement>
          <plugins>
            <plugin>
              <artifactId>maven-clean-plugin</artifactId>
              <version>3.3.2</version>
            </plugin>
            <plugin>
              <artifactId>maven-resources-plugin</artifactId>
              <version>3.3.1</version>
            </plugin>
            <plugin>
              <artifactId>maven-compiler-plugin</artifactId>
              <version>${maven.compiler.plugin.version}</version>
              <configuration>
                <source>${maven.compiler.source}</source>
                <target>${maven.compiler.target}</target>
                <release>${maven.compiler.source}</release> <!-- For modern Java versions -->
                <compilerArgs>
                    <arg>-Xlint:all</arg> <!-- Enable all recommended warnings -->
                </compilerArgs>
              </configuration>
            </plugin>
            <plugin>
              <artifactId>maven-surefire-plugin</artifactId>
              <version>${maven.surefire.plugin.version}</version>
              <configuration>
                <!-- Required for JUnit 5 to run tests -->
                <argLine>@{argLine}</argLine>
              </configuration>
              <dependencies>
                <dependency>
                  <groupId>org.junit.platform</groupId>
                  <artifactId>junit-platform-surefire-provider</artifactId>
                  <version>1.10.2</version> <!-- Match with JUnit 5.10.2 -->
                </dependency>
              </dependencies>
            </plugin>
            <plugin>
              <artifactId>maven-jar-plugin</artifactId>
              <version>${maven.jar.plugin.version}</version>
              <configuration>
                <archive>
                  <manifest>
                    <addClasspath>true</addClasspath>
                    <mainClass>com.expert.java.calculator.App</mainClass> <!-- Will be our main class -->
                  </manifest>
                </archive>
              </configuration>
            </plugin>
            <plugin>
              <artifactId>maven-install-plugin</artifactId>
              <version>3.1.1</version>
            </plugin>
            <plugin>
              <artifactId>maven-deploy-plugin</artifactId>
              <version>3.1.1</version>
            </plugin>
            <plugin>
              <artifactId>maven-site-plugin</artifactId>
              <version>3.12.1</version>
            </plugin>
            <plugin>
              <artifactId>maven-project-info-reports-plugin</artifactId>
              <version>3.5.0</version>
            </plugin>
          </plugins>
        </pluginManagement>
      </build>
    </project>
    

    Explanation of Changes:

    • <packaging>jar</packaging>: Explicitly states our project will be packaged as a JAR.
    • <properties>: Centralized version management for Java, JUnit, SLF4J, Logback, and key Maven plugins. This makes updates easier and ensures consistency.
    • Java Version: maven.compiler.source, maven.compiler.target, and release are all set to 24 for the latest Java LTS version as of 2025.
    • JUnit 5: Replaced JUnit 4 dependency with junit-jupiter-api and junit-jupiter-engine for JUnit 5. junit-jupiter-params is also added, which is useful for parameterized tests. The scope is test, meaning these dependencies are only available during the test compilation and execution phases.
    • SLF4J + Logback:
      • slf4j-api: The API for logging. Your code will interact with this.
      • logback-classic: The concrete implementation of SLF4J.
      • logback-core: A core dependency for logback-classic.
      • scope=runtime: Logback implementation is only needed when the application runs, not during compilation of the main code.
    • maven-compiler-plugin: Updated version and added <release> tag which is the recommended way to configure Java versions for modern JDKs. compilerArgs with -Xlint:all enables comprehensive compiler warnings, a good practice for catching potential issues early.
    • maven-surefire-plugin: Updated version and added a dependency on junit-platform-surefire-provider to ensure Maven’s Surefire plugin can discover and run JUnit 5 tests. The <argLine>@{argLine}</argLine> is a common configuration for Surefire when using advanced test runners.
    • maven-jar-plugin: Configured to include addClasspath and mainClass in the JAR’s manifest. This makes the generated JAR executable directly using java -jar. We’ve specified com.expert.java.calculator.App as our main class, which we’ll create shortly.
    • Plugin Versions: All other plugin versions were updated to their latest stable releases as of December 2025, ensuring compatibility and access to the newest features/bug fixes.

b) Core Implementation: Basic Application and Logging Setup

Now that our pom.xml is configured, let’s set up our basic application structure and integrate logging.

  1. Remove the old App.java and AppTest.java: The archetype generated a basic App.java and AppTest.java for JUnit 4. We’ll replace these with our own.

    rm src/main/java/com/expert/java/calculator/App.java
    rm src/test/java/com/expert/java/calculator/AppTest.java
    
  2. Create the main application class: This will be our entry point. We’ll use SLF4J for logging.

    File: src/main/java/com/expert/java/calculator/App.java

    package com.expert.java.calculator;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /**
     * Main application class for the Simple Calculator.
     * This class serves as the entry point for the application.
     */
    public class App {
    
        // Initialize a logger for this class
        private static final Logger logger = LoggerFactory.getLogger(App.class);
    
        /**
         * The main method, which is the entry point of the application.
         *
         * @param args Command line arguments (not used in this basic example).
         */
        public static void main(String[] args) {
            // Log a simple message to confirm setup
            logger.info("Application started.");
            logger.debug("Debug logging is enabled."); // This will only show if debug level is configured
    
            // In future chapters, this is where we'll instantiate our Calculator and run its logic.
            logger.info("Welcome to the Simple Calculator!");
    
            try {
                // Simulate some work or a potential error
                int result = 10 / 2; // Simple operation
                logger.info("Calculation successful: 10 / 2 = {}", result);
            } catch (ArithmeticException e) {
                // Basic error handling with logging
                logger.error("An arithmetic error occurred: {}", e.getMessage(), e);
            } catch (Exception e) {
                // Catching other unexpected exceptions
                logger.error("An unexpected error occurred: {}", e.getMessage(), e);
            }
    
            logger.info("Application finished.");
        }
    }
    

    Explanation:

    • package com.expert.java.calculator;: Declares the package for our class, matching our groupId.
    • import org.slf4j.Logger; import org.slf4j.LoggerFactory;: Imports the necessary SLF4J classes.
    • private static final Logger logger = LoggerFactory.getLogger(App.class);: This is the standard way to get a logger instance. Using static final ensures it’s initialized once and is thread-safe.
    • logger.info(), logger.debug(), logger.error(): Examples of different logging levels. info is for general operational messages, debug for detailed diagnostic information (often disabled in production), and error for critical issues.
    • {}: Placeholder syntax for logging arguments, which is efficient and prevents string concatenation if the log level is disabled.
    • try-catch block: Demonstrates basic error handling. While simple, it’s crucial to wrap potentially failing operations and log errors appropriately.
  3. Create a basic Calculator class: For now, this class will be minimal, just to demonstrate how new components are added to the project. We’ll expand on its functionality in the next chapter.

    File: src/main/java/com/expert/java/calculator/Calculator.java

    package com.expert.java.calculator;
    
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    /**
     * A simple utility class to perform basic arithmetic operations.
     * This class will be expanded in future chapters.
     */
    public class Calculator {
    
        private static final Logger logger = LoggerFactory.getLogger(Calculator.class);
    
        /**
         * Adds two numbers.
         *
         * @param a The first number.
         * @param b The second number.
         * @return The sum of a and b.
         */
        public int add(int a, int b) {
            logger.debug("Adding {} and {}", a, b);
            int sum = a + b;
            logger.debug("Result of addition: {}", sum);
            return sum;
        }
    
        // Other arithmetic methods (subtract, multiply, divide) will be added here later.
    }
    
  4. Configure Logback: For Logback to work, it needs a configuration file. By default, if no configuration is found, Logback will print to the console with a basic setup. However, for production-ready applications, explicit configuration is essential.

    File: src/main/resources/logback.xml

    <!-- src/main/resources/logback.xml -->
    <?xml version="1.0" encoding="UTF-8"?>
    <configuration>
    
        <!-- Console Appender -->
        <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
            <encoder>
                <!-- Pattern: timestamp [thread] level logger_name - message%n -->
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>
    
        <!-- File Appender -->
        <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <file>logs/simple-calculator.log</file>
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!-- Rollover daily -->
                <fileNamePattern>logs/simple-calculator.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <!-- Or whenever the file size reaches 10MB -->
                    <maxFileSize>10MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
                <!-- Keep 30 days of history -->
                <maxHistory>30</maxHistory>
            </rollingPolicy>
            <encoder>
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
            </encoder>
        </appender>
    
        <!-- Root Logger configuration -->
        <root level="INFO"> <!-- Default logging level for the application -->
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="FILE" />
        </root>
    
        <!-- Specific logger for our application package at DEBUG level -->
        <logger name="com.expert.java.calculator" level="DEBUG" additivity="false">
            <appender-ref ref="CONSOLE" />
            <appender-ref ref="FILE" />
        </logger>
    
        <!-- Example of how to suppress chatty logs from external libraries -->
        <logger name="org.apache.maven" level="WARN"/>
        <logger name="org.springframework" level="INFO"/> <!-- If Spring were used -->
    
    </configuration>
    

    Explanation of logback.xml:

    • Appenders: Define where log messages go.
      • CONSOLE: Prints logs to the standard output.
      • FILE: Prints logs to a file (logs/simple-calculator.log). It uses RollingFileAppender to rotate logs daily (fileNamePattern) and/or when they reach 10MB (maxFileSize), keeping up to 30 days of history (maxHistory). This is a crucial production best practice to prevent log files from growing indefinitely.
    • Encoders: Define the format of the log messages. The pattern %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n is a common, readable format.
    • Root Logger: The default logger for all messages. Here, it’s set to INFO level, meaning DEBUG messages will not be printed by default. It uses both CONSOLE and FILE appenders.
    • Specific Logger: We define a specific logger for our application’s package (com.expert.java.calculator) and set its level to DEBUG. This allows us to see detailed debug messages for our own code while keeping external library logs at a higher level (e.g., INFO or WARN) to avoid excessive verbosity. additivity="false" prevents messages from also being sent to the root logger, ensuring our custom package logger has full control.

c) Testing This Component: Setting up Unit Tests

With our pom.xml and core classes in place, let’s create a unit test for our Calculator class using JUnit 5.

  1. Create the CalculatorTest class:

    File: src/test/java/com/expert/java/calculator/CalculatorTest.java

    package com.expert.java.calculator;
    
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    import org.junit.jupiter.params.ParameterizedTest;
    import org.junit.jupiter.params.provider.CsvSource;
    
    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertNotNull;
    
    /**
     * Unit tests for the Calculator class.
     * Demonstrates basic JUnit 5 usage including parameterized tests.
     */
    class CalculatorTest { // Note: No 'public' modifier needed for test classes in JUnit 5
    
        private Calculator calculator; // Instance of the Calculator class to be tested
    
        /**
         * Setup method executed before each test method.
         * Initializes the Calculator instance.
         */
        @BeforeEach
        void setUp() {
            calculator = new Calculator();
            assertNotNull(calculator, "Calculator instance should not be null after setup.");
        }
    
        /**
         * Test case for the add method with a simple scenario.
         */
        @Test
        @DisplayName("Should correctly add two positive numbers")
        void testAddPositiveNumbers() {
            int result = calculator.add(5, 3);
            assertEquals(8, result, "5 + 3 should equal 8"); // Expected, Actual, Message
        }
    
        /**
         * Test case for the add method with negative numbers.
         */
        @Test
        @DisplayName("Should correctly add negative numbers")
        void testAddNegativeNumbers() {
            int result = calculator.add(-5, -3);
            assertEquals(-8, result, "-5 + -3 should equal -8");
        }
    
        /**
         * Test case for the add method with mixed positive and negative numbers.
         */
        @Test
        @DisplayName("Should correctly add positive and negative numbers")
        void testAddMixedNumbers() {
            int result = calculator.add(10, -7);
            assertEquals(3, result, "10 + -7 should equal 3");
        }
    
        /**
         * Parameterized test for the add method using CsvSource.
         * This allows running the same test logic with multiple sets of inputs.
         *
         * @param a The first number
         * @param b The second number
         * @param expectedSum The expected sum
         */
        @ParameterizedTest(name = "{0} + {1} = {2}")
        @CsvSource({
                "1, 1, 2",
                "0, 0, 0",
                "100, 200, 300",
                "-10, 5, -5",
                "7, -15, -8"
        })
        @DisplayName("Should correctly add numbers from CSV source")
        void testAddWithCsvSource(int a, int b, int expectedSum) {
            int actualSum = calculator.add(a, b);
            assertEquals(expectedSum, actualSum, () -> String.format("%d + %d should be %d", a, b, expectedSum));
        }
    }
    

    Explanation of CalculatorTest.java:

    • import org.junit.jupiter.api.*: Imports necessary JUnit 5 annotations.
    • @BeforeEach: Marks a method to be executed before each test method. Useful for common setup.
    • @Test: Marks a regular test method.
    • @DisplayName: Provides a more readable name for the test in reports.
    • assertEquals(expected, actual, message): An assertion method to check if two values are equal. The message is displayed if the assertion fails.
    • @ParameterizedTest: Marks a test method that can be run multiple times with different arguments.
    • @CsvSource: Provides test data as comma-separated values. Each row in the CsvSource becomes a separate invocation of the test method.
    • Lambda for assertion message: () -> String.format(...) is a good practice for assertion messages in parameterized tests, as the message is only constructed if the test fails, saving resources.
  2. Run the tests: Navigate to the simple-calculator directory in your terminal and run the Maven test command:

    mvn test
    

    You should see output similar to this, indicating that your tests passed successfully:

    [INFO] -------------------------------------------------------
    [INFO] T E S T S
    [INFO] -------------------------------------------------------
    [INFO] Running com.expert.java.calculator.CalculatorTest
    [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: X.XXX s - Success
    [INFO]
    [INFO] Results:
    [INFO]
    [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0
    [INFO]
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    

    This confirms that our Maven setup for testing is working correctly with JUnit 5 and our Calculator class can be tested.

Production Considerations

Building production-ready applications means thinking beyond just functional code. Here’s how our Maven setup addresses some production concerns:

  1. Dependency Management: Maven centralizes all dependencies in pom.xml. This ensures that everyone working on the project uses the exact same versions of libraries, preventing “it works on my machine” issues. The use of <properties> for version numbers further streamlines this.
  2. Logging: Our logback.xml is designed for production. It directs logs to both console (for immediate feedback during development/testing) and a rolling file (essential for persistent logging in production). The INFO root level with a DEBUG level for our specific package is a common strategy to balance verbosity and detail.
  3. Build Optimization: For faster builds, especially in CI/CD pipelines, you might skip tests or package only without running tests.
    • mvn clean install -DskipTests: Builds the project, packages it, and installs it to your local Maven repository, but skips running unit tests.
    • mvn clean package: Builds and packages the project without installing it locally.
  4. Security (Dependency Vulnerabilities): While not explicitly configured in pom.xml yet, a critical production practice is to regularly scan your dependencies for known vulnerabilities. Tools like OWASP Dependency-Check (which can be integrated as a Maven plugin) or commercial solutions are essential. We will consider adding this to our CI/CD pipeline in later chapters.
  5. Executable JAR: The maven-jar-plugin configuration allows us to create an executable JAR. After running mvn clean package, you’ll find simple-calculator-1.0-SNAPSHOT.jar in the target/ directory. You can run it directly:
    java -jar target/simple-calculator-1.0-SNAPSHOT.jar
    
    This will execute the main method in our App.java, and you’ll see the log messages printed to the console and written to logs/simple-calculator.log.

Code Review Checkpoint

At this point, you should have the following files and directories:

my-java-projects/
└── simple-calculator/
    ├── pom.xml
    ├── src/
    │   ├── main/
    │   │   ├── java/
    │   │   │   └── com/
    │   │   │       └── expert/
    │   │   │           └── java/
    │   │   │               └── calculator/
    │   │   │                   ├── App.java
    │   │   │                   └── Calculator.java
    │   │   └── resources/
    │   │       └── logback.xml
    │   └── test/
    │       └── java/
    │           └── com/
    │               └── expert/
    │                   └── java/
    │                       └── calculator/
    │                           └── CalculatorTest.java
    └── target/             # Generated after 'mvn clean install'
        ├── classes/
        ├── generated-sources/
        ├── maven-archiver/
        ├── maven-status/
        ├── surefire-reports/
        ├── simple-calculator-1.0-SNAPSHOT.jar
        └── ...
  • pom.xml: Defines project metadata, dependencies (JUnit 5, SLF4J/Logback), Java 24 compilation, and plugin configurations for building and testing.
  • App.java: Our application’s entry point, demonstrating basic logging using SLF4J.
  • Calculator.java: A placeholder class for our calculator logic, with a simple add method.
  • logback.xml: Configures Logback for console and rolling file output, with specific log levels for different packages.
  • CalculatorTest.java: Contains JUnit 5 unit tests for the Calculator class, including parameterized tests.

This structure forms the backbone of any professional Java application, providing a clear separation of concerns, automated build processes, and robust dependency management.

Common Issues & Solutions

  1. “mvn: command not found”:
    • Issue: Maven is not installed or not added to your system’s PATH environment variable.
    • Solution: Revisit Chapter 1 or the Maven installation guide (e.g., https://maven.apache.org/install.html) and ensure Maven is correctly installed and its bin directory is in your PATH.
  2. “Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:X.Y.Z:compile (default-compile) on project simple-calculator: Fatal error compiling: invalid target release: 24”:
    • Issue: Your Java Development Kit (JDK) version is older than Java 24, but your pom.xml is configured for Java 24.
    • Solution: Ensure you have JDK 24 installed and that your JAVA_HOME environment variable points to the JDK 24 installation. You can check your active Java version with java -version. If you need to switch JDKs, consider using a tool like SDKMAN! or manually updating JAVA_HOME.
  3. “Cannot find symbol Logger” or similar compilation errors after adding logging/JUnit:
    • Issue: Maven hasn’t downloaded the new dependencies, or your IDE hasn’t refreshed its project configuration.
    • Solution: Run mvn clean install from your project’s root directory (simple-calculator/). This command will download all declared dependencies, compile your code, run tests, and package your application. If using an IDE (like IntelliJ IDEA or Eclipse), ensure you refresh your Maven project after modifying pom.xml (usually there’s a “Reload Maven Project” option).
  4. Tests not running or “No tests were found” for JUnit 5:
    • Issue: The maven-surefire-plugin might not be correctly configured to detect JUnit 5 tests.
    • Solution: Double-check the maven-surefire-plugin configuration in your pom.xml, especially the <dependencies> section for junit-platform-surefire-provider and its version, which should match your JUnit Jupiter version. Ensure your test classes and methods are correctly annotated with @Test, @ParameterizedTest, etc.

Testing & Verification

To verify that everything in this chapter is correctly set up and working:

  1. Clean and Install the Project: Navigate to the simple-calculator directory in your terminal:

    cd my-java-projects/simple-calculator
    mvn clean install
    
    • Expected Behavior: The build should succeed (BUILD SUCCESS). Maven will compile your App.java and Calculator.java, download all specified dependencies, run CalculatorTest.java (you should see “Tests run: 5, Failures: 0, Errors: 0, Skipped: 0”), and package your application into an executable JAR.
    • Verification: Confirm that target/simple-calculator-1.0-SNAPSHOT.jar exists and that the test report (target/surefire-reports/) shows all tests passed.
  2. Run the Executable JAR:

    java -jar target/simple-calculator-1.0-SNAPSHOT.jar
    
    • Expected Behavior: You should see the INFO level log messages from App.java printed to your console, including “Application started.” and “Welcome to the Simple Calculator!”.
    • Verification: Check for the logs/ directory in your simple-calculator project root. Inside, you should find simple-calculator.log containing the same log messages, but potentially with DEBUG messages if you configured com.expert.java.calculator to DEBUG level in logback.xml.

If both these steps are successful, congratulations! You have a fully configured, production-ready Maven project with logging and testing capabilities.

Summary & Next Steps

In this chapter, we’ve accomplished a significant milestone: establishing a solid foundation for our Java projects using Apache Maven. We learned how to:

  • Initialize a new Maven project with mvn archetype:generate.
  • Configure the pom.xml to use Java 24, manage dependencies (JUnit 5, SLF4J/Logback), and customize build plugins.
  • Implement basic application code (App.java and Calculator.java) with integrated logging.
  • Set up comprehensive Logback configuration for console and rolling file appenders.
  • Write and execute unit tests using JUnit 5, including parameterized tests.
  • Understand critical production considerations like dependency management, logging, and executable JAR creation.

This robust Maven setup will serve as the backbone for all subsequent projects in this series. In the next chapter, “Chapter 3: Building the Simple Calculator - Core Logic,” we will dive into implementing the full functionality of our first application, the Simple Calculator, leveraging the project structure and tools we’ve set up today. We’ll add methods for subtraction, multiplication, and division, along with user input handling.