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.
Create the parent directory:
mkdir my-java-projects cd my-java-projectsGenerate the Maven project using an archetype: Maven archetypes are project templates. We’ll use the
maven-archetype-quickstartas a basic starting point.mvn archetype:generate -DgroupId=com.expert.java.calculator -DartifactId=simple-calculator -DarchetypeArtifactId=maven-archetype-quickstart -DarchetypeVersion=1.4 -DinteractiveMode=falsegroupId: 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-quickstartcreates a simple project with aMainclass 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-calculatorwithinmy-java-projects.Navigate into the new project and inspect
pom.xml:cd simple-calculator lsYou should see
pom.xmland thesrcdirectory. Open thepom.xmlfile. 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.xmluses older Java and JUnit versions. We need to update this to reflect current best practices as of 2025-12-04.Update
pom.xmlfor Java 24, JUnit 5, and Logging:We’ll modify the
<properties>section to specify Java 24, update themaven-compiler-pluginto 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, andreleaseare all set to24for the latest Java LTS version as of 2025. - JUnit 5: Replaced JUnit 4 dependency with
junit-jupiter-apiandjunit-jupiter-enginefor JUnit 5.junit-jupiter-paramsis also added, which is useful for parameterized tests. Thescopeistest, 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 forlogback-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.compilerArgswith-Xlint:allenables comprehensive compiler warnings, a good practice for catching potential issues early.maven-surefire-plugin: Updated version and added a dependency onjunit-platform-surefire-providerto 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 includeaddClasspathandmainClassin the JAR’s manifest. This makes the generated JAR executable directly usingjava -jar. We’ve specifiedcom.expert.java.calculator.Appas 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.
Remove the old
App.javaandAppTest.java: The archetype generated a basicApp.javaandAppTest.javafor 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.javaCreate 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.javapackage 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 ourgroupId.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. Usingstatic finalensures it’s initialized once and is thread-safe.logger.info(),logger.debug(),logger.error(): Examples of different logging levels.infois for general operational messages,debugfor detailed diagnostic information (often disabled in production), anderrorfor critical issues.{}: Placeholder syntax for logging arguments, which is efficient and prevents string concatenation if the log level is disabled.try-catchblock: Demonstrates basic error handling. While simple, it’s crucial to wrap potentially failing operations and log errors appropriately.
Create a basic
Calculatorclass: 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.javapackage 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. }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 usesRollingFileAppenderto 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%nis a common, readable format. - Root Logger: The default logger for all messages. Here, it’s set to
INFOlevel, meaningDEBUGmessages will not be printed by default. It uses bothCONSOLEandFILEappenders. - Specific Logger: We define a specific logger for our application’s package (
com.expert.java.calculator) and set its level toDEBUG. This allows us to see detailed debug messages for our own code while keeping external library logs at a higher level (e.g.,INFOorWARN) to avoid excessive verbosity.additivity="false"prevents messages from also being sent to the root logger, ensuring our custom package logger has full control.
- Appenders: Define where log messages go.
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.
Create the
CalculatorTestclass:File:
src/test/java/com/expert/java/calculator/CalculatorTest.javapackage 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. Themessageis 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 theCsvSourcebecomes 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.
Run the tests: Navigate to the
simple-calculatordirectory in your terminal and run the Maven test command:mvn testYou 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
Calculatorclass 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:
- 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. - Logging: Our
logback.xmlis 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). TheINFOroot level with aDEBUGlevel for our specific package is a common strategy to balance verbosity and detail. - 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.
- Security (Dependency Vulnerabilities): While not explicitly configured in
pom.xmlyet, 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. - Executable JAR: The
maven-jar-pluginconfiguration allows us to create an executable JAR. After runningmvn clean package, you’ll findsimple-calculator-1.0-SNAPSHOT.jarin thetarget/directory. You can run it directly:This will execute thejava -jar target/simple-calculator-1.0-SNAPSHOT.jarmainmethod in ourApp.java, and you’ll see the log messages printed to the console and written tologs/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 simpleaddmethod.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 theCalculatorclass, 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
- “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
bindirectory is in your PATH.
- “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.xmlis configured for Java 24. - Solution: Ensure you have JDK 24 installed and that your
JAVA_HOMEenvironment variable points to the JDK 24 installation. You can check your active Java version withjava -version. If you need to switch JDKs, consider using a tool like SDKMAN! or manually updatingJAVA_HOME.
- Issue: Your Java Development Kit (JDK) version is older than Java 24, but your
- “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 installfrom 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 modifyingpom.xml(usually there’s a “Reload Maven Project” option).
- Tests not running or “No tests were found” for JUnit 5:
- Issue: The
maven-surefire-pluginmight not be correctly configured to detect JUnit 5 tests. - Solution: Double-check the
maven-surefire-pluginconfiguration in yourpom.xml, especially the<dependencies>section forjunit-platform-surefire-providerand its version, which should match your JUnit Jupiter version. Ensure your test classes and methods are correctly annotated with@Test,@ParameterizedTest, etc.
- Issue: The
Testing & Verification
To verify that everything in this chapter is correctly set up and working:
Clean and Install the Project: Navigate to the
simple-calculatordirectory in your terminal:cd my-java-projects/simple-calculator mvn clean install- Expected Behavior: The build should succeed (
BUILD SUCCESS). Maven will compile yourApp.javaandCalculator.java, download all specified dependencies, runCalculatorTest.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.jarexists and that the test report (target/surefire-reports/) shows all tests passed.
- Expected Behavior: The build should succeed (
Run the Executable JAR:
java -jar target/simple-calculator-1.0-SNAPSHOT.jar- Expected Behavior: You should see the
INFOlevel log messages fromApp.javaprinted to your console, including “Application started.” and “Welcome to the Simple Calculator!”. - Verification: Check for the
logs/directory in yoursimple-calculatorproject root. Inside, you should findsimple-calculator.logcontaining the same log messages, but potentially withDEBUGmessages if you configuredcom.expert.java.calculatortoDEBUGlevel inlogback.xml.
- Expected Behavior: You should see the
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.xmlto use Java 24, manage dependencies (JUnit 5, SLF4J/Logback), and customize build plugins. - Implement basic application code (
App.javaandCalculator.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.