TungDaDev's Blog

maven dependencies

Maven dependencies.png
Published on
/8 mins read/

Trong các hệ thống phân tán và kiến trúc microservices hiện đại, cấu hình build không đơn thuần chỉ là công cụ đóng gói phần mềm. Nó là bản thiết kế thể hiện sự gọn gàng, tính nhất quán và tính dễ bảo trì của cả một dự án. Quản lý tồi, bạn sẽ rơi vào "Dependency Hell". Quản lý tốt, hệ thống sẽ tự động vận hành trơn tru từ môi trường local cho đến production.

Bài viết này sẽ đào sâu vào nghệ thuật làm chủ Maven, từ việc tổ chức cấu trúc POM tinh giản, quản lý phiên bản tập trung bằng BOM, cho đến cách xây dựng kiến trúc Multi-module tiêu chuẩn.

# pom structure

Một file pom.xml được tổ chức tốt cần phản ánh rõ sự phân tách trách nhiệm (Separation of Concerns). Thay vì nhồi nhét mọi thứ, chúng ta sử dụng cơ chế kế thừa (Parent) và gom nhóm thuộc tính (Properties).

<project>
   <!-- Coordinates -->
   <groupId>vn.com.vpbank.internal</groupId>
   <artifactId>my-service</artifactId>
   <version>1.0.0-SNAPSHOT</version>
   <packaging>jar</packaging>
 
   <!-- Parent (inherits config) -->
   <parent>
       <groupId>org.springframework.boot</groupId>
       <artifactId>spring-boot-starter-parent</artifactId>
       <version>3.2.5</version>
   </parent>
 
   <!-- Properties (centralize versions) -->
   <properties>
       <java.version>21</java.version>
       <mapstruct.version>1.5.5.Final</mapstruct.version>
   </properties>
 
   <!-- Dependency Management (BOM imports) -->
   <dependencyManagement>
       <dependencies>
           <dependency>
               <groupId>org.springframework.cloud</groupId>
               <artifactId>spring-cloud-dependencies</artifactId>
               <version>2023.0.1</version>
               <type>pom</type>
               <scope>import</scope>
           </dependency>
       </dependencies>
   </dependencyManagement>
 
   <!-- Actual Dependencies -->
   <dependencies>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
           <!-- version inherited from parent -->
       </dependency>
   </dependencies>
</project>

# dependency scopes

Việc hiểu đúng Scope giúp giữ cho dung lượng file JAR/WAR cuối cùng ở mức tối giản nhất, giảm thiểu thời gian khởi động và rủi ro bảo mật.

ScopeCompileTestRuntimePackaged
compile (default)
provided
runtime
test
system

Ví dụ thực tế:

<!-- Examples -->
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <scope>provided</scope> <!-- annotation processor, not in final jar -->
</dependency>
 
<dependency>
   <groupId>org.postgresql</groupId>
   <artifactId>postgresql</artifactId>
   <scope>runtime</scope> <!-- only needed at runtime via JDBC -->
</dependency>
 
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>

# bom (bill of materials)

Trong các dự án lớn hoặc kiến trúc Microservices, việc hardcode version ở từng module là một "Anti-pattern". BOM giải quyết bài toán này bằng cách đóng vai trò là một "Single Source of Truth" cho toàn bộ phiên bản thư viện.

Tại file Parent hoặc Shared BOM:

<!-- In parent or shared BOM -->
<dependencyManagement>
   <dependencies>
       <!-- Spring Cloud BOM -->
       <dependency>
           <groupId>org.springframework.cloud</groupId>
           <artifactId>spring-cloud-dependencies</artifactId>
           <version>${spring-cloud.version}</version>
           <type>pom</type>
           <scope>import</scope>
       </dependency>
 
       <!-- Internal libraries BOM -->
       <dependency>
           <groupId>vn.com.vpbank.internal</groupId>
           <artifactId>csp-common</artifactId>
           <version>${csp-common.version}</version>
       </dependency>
   </dependencies>
</dependencyManagement>

Tại các Child Module: (Mã nguồn trở nên gọn gàng, không còn sự hiện diện của thẻ <version>)

<!-- In child module — no version needed -->
<dependencies>
   <dependency>
       <groupId>org.springframework.cloud</groupId>
       <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
       <!-- version resolved from BOM -->
   </dependency>
</dependencies>

# dependency resolution

Khi hai thư viện kéo theo (transitive dependencies) cùng một thư viện khác nhưng khác phiên bản, Maven áp dụng quy tắc "Nearest wins" (Khoảng cách gần nhất tới gốc POM sẽ thắng).

Để ép buộc hệ thống dùng đúng phiên bản mong muốn hoặc loại bỏ các thư viện rác, chúng ta dùng cơ chế Exclusions:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>

# useful commands

# Hiển thị cây thư viện để truy vết nguồn gốc dependency
./mvnw dependency:tree
 
# Lọc nhanh các xung đột
./mvnw dependency:tree -Dverbose | grep "omitted for conflict"
 
# Phát hiện các dependency đang được khai báo nhưng không dùng tới
./mvnw dependency:analyze
 
# Show effective POM (resolved inheritance)
./mvnw help:effective-pom
 
# Show effective settings
./mvnw help:effective-settings

# profiles

<profiles>
   <!-- Dev profile (default) -->
   <profile>
       <id>dev</id>
       <activation>
           <activeByDefault>true</activeByDefault>
       </activation>
       <properties>
           <packaging.type>jar</packaging.type>
           <spring.profiles.active>dev</spring.profiles.active>
       </properties>
   </profile>
 
   <!-- Production profile -->
   <profile>
       <id>prod</id>
       <properties>
           <packaging.type>war</packaging.type>
           <spring.profiles.active>prod</spring.profiles.active>
       </properties>
       <dependencies>
           <dependency>
               <groupId>org.springframework.boot</groupId>
               <artifactId>spring-boot-starter-tomcat</artifactId>
               <scope>provided</scope>
           </dependency>
       </dependencies>
   </profile>
 
   <!-- Skip tests profile -->
   <profile>
       <id>fast</id>
       <properties>
           <maven.test.skip>true</maven.test.skip>
           <checkstyle.skip>true</checkstyle.skip>
       </properties>
   </profile>
</profiles>
# Activate profile
./mvnw clean package -Pprod
./mvnw clean package -Pdev,fast
 
# Property override
./mvnw clean package -DskipTests

# multi-module project

Với các hệ thống tuân theo Clean Architecture hoặc Domain-Driven Design, việc chia tách mã nguồn thành các module độc lập (Core, Infrastructure, Application) là bắt buộc. Maven Multi-module giữ cho cấu trúc này liên kết chặt chẽ nhưng độc lập về mặt biên dịch.

Parent POM: Quản lý danh sách các module con.

<modules>
    <module>core</module>
    <module>application</module>
    <module>infrastructure</module>
</modules>

# build commands

# Build all modules
./mvnw clean install
 
# Build specific module + dependencies
./mvnw clean install -pl application -am
 
# Build specific module only (no deps)
./mvnw clean install -pl bpm-cluster
 
# Skip modules
./mvnw clean install -pl !integration-tests

Flags:

  • -pl (projects list): specify modules to build
  • -am (also make): build required dependencies
  • -amd (also make dependents): build modules that depend on specified
  • -rf (resume from): restart build from specific module

# build lifecycle

# default lifecycle phases

Maven thực thi theo tính tuyến tính. Gọi một phase sẽ kích hoạt tất cả các phase trước đó: validate → compile → test → package (tạo JAR) → verify (chạy Integration test & Checkstyle) → install (đưa vào local repo) → deploy (đưa lên Nexus).

PhaseAction
validateCheck POM correctness
compileCompile source code
testRun unit tests
packageCreate JAR/WAR
verifyRun integration tests, quality checks
installInstall to local .m2 repository
deployUpload to remote Nexus repository
# Each phase runs all preceding phases
./mvnw package    # → validate → compile → test → package
./mvnw verify     # → ... → package → verify (JaCoCo, Fortify)
./mvnw install    # → ... → verify → install

# plugin configuration

<build>
   <plugins>
       <!-- Compiler -->
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <configuration>
               <release>21</release>
               <compilerArgs>
                   <arg>--enable-preview</arg>
               </compilerArgs>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.projectlombok</groupId>
                       <artifactId>lombok</artifactId>
                       <version>${lombok.version}</version>
                   </path>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>${mapstruct.version}</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
 
       <!-- Spring Boot packaging -->
       <plugin>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-maven-plugin</artifactId>
           <configuration>
               <excludes>
                   <exclude>
                       <groupId>org.projectlombok</groupId>
                       <artifactId>lombok</artifactId>
                   </exclude>
               </excludes>
           </configuration>
       </plugin>
 
       <!-- JaCoCo coverage -->
       <plugin>
           <groupId>org.jacoco</groupId>
           <artifactId>jacoco-maven-plugin</artifactId>
           <executions>
               <execution>
                   <goals><goal>prepare-agent</goal></goals>
               </execution>
               <execution>
                   <id>check</id>
                   <phase>verify</phase>
                   <goals><goal>check</goal></goals>
                   <configuration>
                       <rules>
                           <rule>
                               <limits>
                                   <limit>
                                       <counter>LINE</counter>
                                       <minimum>0.80</minimum>
                                   </limit>
                               </limits>
                           </rule>
                       </rules>
                   </configuration>
               </execution>
           </executions>
       </plugin>
   </plugins>
</build>

# private repository (nexus)

# settings.xml

<settings>
   <servers>
       <server>
           <id>nexus-releases</id>
           <username>${env.NEXUS_USER}</username>
           <password>${env.NEXUS_PASS}</password>
       </server>
       <server>
           <id>nexus-snapshots</id>
           <username>${env.NEXUS_USER}</username>
           <password>${env.NEXUS_PASS}</password>
       </server>
   </servers>
 
   <mirrors>
       <mirror>
           <id>nexus</id>
           <mirrorOf>*</mirrorOf>
           <url>https://nexus.company.com/repository/maven-public/</url>
       </mirror>
   </mirrors>
</settings>

# deploy to nexus

<distributionManagement>
   <repository>
       <id>nexus-releases</id>
       <url>https://nexus.company.com/repository/maven-releases/</url>
   </repository>
   <snapshotRepository>
       <id>nexus-snapshots</id>
       <url>https://nexus.company.com/repository/maven-snapshots/</url>
   </snapshotRepository>
</distributionManagement>
./mvnw deploy -s settings.xml

# resource filtering

<build>
   <resources>
       <resource>
           <directory>src/main/resources</directory>
           <filtering>true</filtering> <!-- replace ${...} placeholders -->
           <includes>
               <include>application.yml</include>
           </includes>
       </resource>
       <resource>
           <directory>src/main/resources</directory>
           <filtering>false</filtering> <!-- don't corrupt binary files -->
           <excludes>
               <exclude>application.yml</exclude>
           </excludes>
       </resource>
   </resources>
</build>

# common pitfalls

PitfallProblemFix
Version in child when parent managesOverrides BOM, divergesRemove version from child
Missing <type>pom</type> on BOM importImports as regular depAdd <type>pom</type><scope>import</scope>
compile scope for test utilitiesPollutes production classpathUse test scope
Snapshot dependency in releaseNon-reproducible buildPin to release versions
Annotation processor order wrongLombok + MapStruct conflictLombok first, then MapStruct
Missing -am flagModule build fails (no deps)Use -pl module -am
mvn install without cleanStale classes in targetAlways clean install

# quick reference

# Common workflows
./mvnw clean package -Pdev              # Build for dev
./mvnw clean package -Pprod             # Build WAR for production
./mvnw clean verify -s settings.xml     # Build + coverage check
./mvnw clean install -pl core -am       # Build core + its deps
./mvnw dependency:tree                  # Show dep tree
./mvnw versions:display-dependency-updates  # Check for updates
./mvnw spring-boot:run                  # Run locally
./mvnw test -Dtest=UserServiceTest      # Run specific test class
./mvnw test -Dtest="UserServiceTest#testCreate"  # Run specific method

Bài viết mang tính chất "ghi chú - chia sẻ và phi lợi nhuận". Nếu thấy hữu ích, hãy chia sẻ nó tới bạn bè và đồng nghiệp của bạn nhé!

Happy coding 😎 👍🏻 🚀 🔥.

← Previous postmdc tracing
Next post →stack-queue