Tampilan
Kenapa penting
Sejauh ini kamu mengompilasi dan menjalankan satu file dengan java File.java. Project backend sungguhan punya ratusan class, puluhan library pihak ketiga (driver database, JSON, logging, framework test), dan harus dibangun dengan cara yang sama persis di laptopmu, laptop rekan setim, dan server CI. Menyusun classpath dan mengunduh .jar secara manual tidak mungkin dipertahankan. Maven menyelesaikan ini: kamu mendeklarasikan project dan dependency-nya di satu file pom.xml, lalu Maven mengunduh library, mengompilasi, menjalankan test, dan mengemas hasilnya. Spring Boot, mayoritas project Java enterprise, dan folder exercises/ di Jalur Java memakai Maven — membaca POM dan memahami error build adalah keterampilan harian developer backend.
Konsep inti
POM dan koordinat GAV
POM (Project Object Model) adalah file pom.xml di akar project. Contoh minimal untuk project toko kecil:
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>id.toko</groupId>
<artifactId>toko-app</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>Setiap artifact Maven — project-mu maupun library — diidentifikasi oleh tiga koordinat, disingkat GAV:
| Elemen | Arti | Contoh |
|---|---|---|
groupId | organisasi/pemilik, biasanya domain terbalik | id.toko |
artifactId | nama modul atau library | toko-app |
version | versi; akhiran -SNAPSHOT = masih dikembangkan, bisa berubah | 1.0.0-SNAPSHOT |
packaging menentukan hasil build: jar (default), war, atau pom (project induk tanpa kode — dibahas di bab berikutnya). properties berisi nilai yang bisa dirujuk dengan ${nama}. Dua properti di atas wajib kamu kenal: maven.compiler.release menentukan versi Java target (setara javac --release 25), dan project.build.sourceEncoding mencegah huruf non-ASCII rusak di mesin yang encoding default-nya berbeda.
Versi release seperti 1.0.0 dianggap tidak pernah berubah, sedangkan 1.0.0-SNAPSHOT boleh diterbitkan ulang berkali-kali selama pengembangan.
Lifecycle dan phase
Maven bekerja dengan lifecycle: urutan phase yang tetap. Lifecycle default (yang paling sering dipakai) memuat, antara lain:
| Phase | Yang terjadi (project jar) |
|---|---|
validate | cek POM valid dan informasi lengkap |
compile | kompilasi src/main/java ke target/classes |
test | kompilasi & jalankan test di src/test/java (plugin surefire) |
package | kemas hasil kompilasi menjadi target/<artifactId>-<version>.jar |
verify | pemeriksaan tambahan (integration test, aturan kualitas) |
install | salin artifact ke repository lokal ~/.m2 |
deploy | unggah artifact ke repository remote (Nexus, Artifactory, dsb) |
Aturan pentingnya: menjalankan sebuah phase menjalankan semua phase sebelumnya. clean adalah lifecycle terpisah yang menghapus folder target/. Berikut potongan output nyata ./mvnw clean package:
text
[INFO] --- clean:3.2.0:clean (default-clean) @ toko-app ---
[INFO] --- resources:3.4.0:resources (default-resources) @ toko-app ---
[INFO] --- compiler:3.16.0:compile (default-compile) @ toko-app ---
[INFO] --- resources:3.4.0:testResources (default-testResources) @ toko-app ---
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ toko-app ---
[INFO] --- surefire:3.6.0:test (default-test) @ toko-app ---
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0
[INFO] --- jar:3.5.0:jar (default-jar) @ toko-app ---
[INFO] Building jar: .../toko-app/target/toko-app-1.0.0-SNAPSHOT.jar
[INFO] BUILD SUCCESSSetiap baris --- compiler:3.16.0:compile adalah goal sebuah plugin (compile milik maven-compiler-plugin versi 3.16.0) yang terikat ke suatu phase. Jadi phase hanyalah "titik" di lifecycle, sedangkan pekerjaan nyatanya dilakukan oleh goal plugin.
Perintah yang paling sering kamu pakai: ./mvnw test saat mengembangkan, ./mvnw verify sebelum push (inilah yang biasanya dijalankan CI), dan ./mvnw install hanya bila project lain di mesinmu perlu memakai artifact ini.
Dependency dan scope
Library ditambahkan di <dependencies> dengan koordinat GAV-nya. Scope menentukan di classpath mana library itu tersedia:
| Scope | Kompilasi main | Test | Ikut saat runtime/dikemas | Contoh |
|---|---|---|---|---|
compile (default) | ya | ya | ya | library JSON |
provided | ya | ya | tidak (disediakan server) | Servlet API di Tomcat |
runtime | tidak | ya | ya | driver JDBC PostgreSQL |
test | tidak | ya | tidak | JUnit, AssertJ, Mockito |
Scope menjaga batas yang sehat. Kode produksi yang tanpa sengaja meng-import JUnit akan gagal dikompilasi — output nyata:
text
[ERROR] COMPILATION ERROR :
[ERROR] .../src/main/java/id/toko/Bad.java:[3,29] package org.junit.jupiter.api does not existDriver JDBC diberi scope runtime karena kodemu cukup bergantung pada interface java.sql; implementasinya baru dibutuhkan saat aplikasi berjalan.
Dependency transitif dan konflik versi
Library juga punya dependency. Bila kamu menambahkan jackson-databind, Maven otomatis membawa jackson-core dan jackson-annotations — ini dependency transitif. Lihat pohonnya dengan mvn dependency:tree:
text
[INFO] id.toko:toko-app:jar:1.0.0-SNAPSHOT
[INFO] +- tools.jackson.core:jackson-databind:jar:3.2.4:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.22:compile
[INFO] | \- tools.jackson.core:jackson-core:jar:3.2.4:compile
[INFO] \- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO] +- org.junit.jupiter:junit-jupiter-api:jar:6.1.3:test
[INFO] | +- org.opentest4j:opentest4j:jar:1.3.0:test
[INFO] | +- org.junit.platform:junit-platform-commons:jar:6.1.3:test
[INFO] | +- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] | \- org.jspecify:jspecify:jar:1.0.0:test
[INFO] +- org.junit.jupiter:junit-jupiter-params:jar:6.1.3:test
[INFO] \- org.junit.jupiter:junit-jupiter-engine:jar:6.1.3:test
[INFO] \- org.junit.platform:junit-platform-engine:jar:6.1.3:testBila artifact yang sama diminta dengan versi berbeda, Maven memakai aturan nearest wins: versi yang paling dekat dengan project menang — bukan versi tertinggi. Misalnya kamu menulis jackson-annotations:2.18.0 langsung di POM. Dengan -Dverbose Maven menunjukkan apa yang dibuang:
text
[INFO] +- tools.jackson.core:jackson-databind:jar:3.2.4:compile
[INFO] | \- (com.fasterxml.jackson.core:jackson-annotations:jar:2.22:compile - omitted for conflict with 2.18.0)
[INFO] \- com.fasterxml.jackson.core:jackson-annotations:jar:2.18.0:compileBuild tetap sukses, tetapi jackson-databind kini berjalan dengan versi lebih tua dari yang ia butuhkan — gejalanya baru muncul saat runtime sebagai NoSuchMethodError atau ClassNotFoundException. Cara memperbaiki konflik: samakan versi lewat dependencyManagement/BOM (bagian berikut), atau buang transitif yang tidak diinginkan dengan <exclusions>.
dependencyManagement dan BOM
<dependencyManagement> adalah daftar versi yang disepakati, bukan daftar dependency yang dipakai. Dependency yang ditulis di <dependencies> tanpa <version> akan mengambil versi dari sini. Bila tidak ada versinya di mana pun, POM ditolak:
text
[ERROR] 'dependencies.dependency.version' for tools.jackson.core:jackson-databind:jar is missing. @ line 31, column 21BOM (Bill of Materials) adalah POM khusus berisi dependencyManagement untuk sekumpulan artifact yang dirilis bersama. Kamu meng-import-nya dengan <type>pom</type> dan <scope>import</scope>, lalu cukup menulis GAV tanpa versi:
xml
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>6.1.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope> <!-- versi diambil dari BOM -->
</dependency>
</dependencies>Semua modul JUnit (api, params, engine, platform) dijamin berversi sama. Spring Boot memakai pola yang sama dalam skala besar: satu BOM mengatur versi ratusan library yang sudah diuji bersama.
Plugin: compiler dan surefire
Semua pekerjaan Maven dilakukan plugin. Dua yang paling penting: maven-compiler-plugin (goal compile dan testCompile, membaca maven.compiler.release) dan maven-surefire-plugin (goal test, menjalankan JUnit dan menghentikan build bila ada test gagal). Kunci versinya secara eksplisit:
xml
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>Tanpa versi eksplisit, Maven memakai versi bawaan yang bergantung pada versi Maven-nya sendiri. Saat dicoba dengan Maven 3.9.16, project tanpa <plugins> memakai compiler 3.15.0 dan surefire 3.5.4 — ganti versi Maven, hasil build ikut bergeser diam-diam.
Maven Wrapper dan repository lokal
Maven Wrapper adalah skrip mvnw (Linux/macOS) dan mvnw.cmd (Windows) plus file .mvn/wrapper/maven-wrapper.properties yang mencatat versi Maven. Saat pertama dijalankan, wrapper mengunduh Maven versi itu, sehingga semua orang dan CI memakai versi yang sama tanpa instalasi manual. Folder exercises/ di Jalur Java memakai wrapper — itulah kenapa perintahnya ./mvnw -Psolution verify, bukan mvn ...:
text
# exercises/.mvn/wrapper/maven-wrapper.properties (potongan)
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zipSetiap artifact yang diunduh disimpan di repository lokal ~/.m2/repository, dengan path mengikuti GAV: ~/.m2/repository/org/junit/jupiter/junit-jupiter/6.1.3/. Build berikutnya memakai salinan lokal ini; bila tidak ada, Maven mengunduh dari Maven Central (atau mirror perusahaan). Dua opsi yang berguna: -o (offline, hanya memakai ~/.m2) dan -U (paksa cek ulang SNAPSHOT di remote).
Contoh di dunia backend
POM sebuah order-service untuk toko kopi online — dengan keempat scope dan dua BOM. Pohon dependency-nya diverifikasi dengan ./mvnw dependency:tree.
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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>id.tokokopi</groupId>
<artifactId>order-service</artifactId>
<version>0.3.0-SNAPSHOT</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>3.2.4</jackson.version>
<junit.version>6.1.3</junit.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>tools.jackson</groupId>
<artifactId>jackson-bom</artifactId>
<version>${jackson.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
<version>${junit.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- compile: dipakai kode utama, ikut saat runtime -->
<dependency>
<groupId>tools.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- provided: disediakan server (mis. Tomcat), tidak ikut dikemas -->
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<!-- runtime: driver JDBC baru dibutuhkan saat aplikasi berjalan -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.14</version>
<scope>runtime</scope>
</dependency>
<!-- test: hanya untuk src/test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.27.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.16.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.0</version>
</plugin>
</plugins>
</build>
</project>Potongan hasil ./mvnw dependency:tree — perhatikan scope di ujung setiap baris, dan dependency transitif yang mewarisi scope induknya (checker-qual ikut runtime, byte-buddy ikut test):
text
[INFO] id.tokokopi:order-service:jar:0.3.0-SNAPSHOT
[INFO] +- tools.jackson.core:jackson-databind:jar:3.2.4:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.22:compile
[INFO] | \- tools.jackson.core:jackson-core:jar:3.2.4:compile
[INFO] +- jakarta.servlet:jakarta.servlet-api:jar:6.1.0:provided
[INFO] +- org.postgresql:postgresql:jar:42.7.14:runtime
[INFO] | \- org.checkerframework:checker-qual:jar:3.55.1:runtime
[INFO] +- org.junit.jupiter:junit-jupiter:jar:6.1.3:test
[INFO] | ...
[INFO] \- org.assertj:assertj-core:jar:3.27.7:test
[INFO] \- net.bytebuddy:byte-buddy:jar:1.18.3:testVersi diletakkan di properties agar mudah dinaikkan di satu tempat. Saat aplikasi berkembang, kamu akan sering membuka pohon ini untuk menjawab "library X datang dari mana?" atau "kenapa versinya bukan yang saya tulis?". Di Spring Boot nanti, sebagian besar versi di atas akan diatur oleh BOM Spring Boot, sehingga <version> makin jarang kamu tulis sendiri.
Pertanyaan interview
1. Apa beda phase dan goal di Maven?
Phase adalah langkah dalam lifecycle (compile, test, package, ...) yang urutannya tetap. Goal adalah tugas konkret milik plugin, misalnya compiler:compile atau surefire:test, yang terikat ke phase tertentu. Menjalankan mvn package menjalankan semua phase sampai package beserta goal-goal yang terikat padanya; menjalankan goal langsung (mvn dependency:tree) hanya mengeksekusi goal itu.
2. Bagaimana Maven menyelesaikan konflik versi dependency transitif?
Dengan nearest wins: versi yang paling dekat ke project di pohon dependency dipakai; bila jaraknya sama, yang dideklarasikan lebih dulu menang. Ini bisa menghasilkan versi yang lebih tua dari yang dibutuhkan library lain. Solusinya mengunci versi lewat dependencyManagement atau BOM, atau memakai <exclusions>, lalu memeriksa hasilnya dengan mvn dependency:tree -Dverbose.
3. Kapan memakai scope provided dibanding runtime?
provided untuk API yang dibutuhkan saat kompilasi tetapi disediakan lingkungan eksekusi, misalnya Servlet API di server aplikasi — tidak ikut dikemas. runtime kebalikannya: tidak dibutuhkan saat kompilasi tetapi wajib ada saat berjalan, misalnya driver JDBC yang dimuat lewat interface java.sql.
Jebakan umum
- Mengira
dependencyManagementmenambah dependency. Ia hanya mengatur versi; tetap tulis<dependency>di<dependencies>. - Mengira Maven memilih versi tertinggi. Aturannya nearest wins — cek dengan
mvn dependency:tree -Dverbosesetiap kali munculNoSuchMethodErrorsetelah menambah library. - Tidak mengunci versi plugin. Versi bawaan berubah mengikuti versi Maven; tulis
<version>untuk compiler, surefire, dan plugin lain. - Memakai
mvnglobal padahal project punya wrapper. Selalu./mvnwagar versinya sama dengan tim dan CI. - Menjalankan
installuntuk setiap build. Cukupverify;installmengisi~/.m2dan bisa membuat project lain di mesinmu diam-diam memakai SNAPSHOT lokal yang usang. - Commit folder
target/. Itu hasil build; masukkan ke.gitignore.
Ringkasan
pom.xmlmendeklarasikan project: koordinat GAV,packaging,properties, dependency, dan plugin.- Menjalankan sebuah phase menjalankan semua phase sebelumnya;
verifyadalah pilihan standar sebelum push dan di CI. - Scope
compile,provided,runtime,testmenentukan di classpath mana library tersedia. - Konflik transitif diselesaikan dengan nearest wins; periksa dengan
mvn dependency:treedan kunci versi lewatdependencyManagement/BOM. - Kunci versi plugin, pakai Maven Wrapper (
./mvnw), dan ingat bahwa artifact di-cache di~/.m2/repository.