Quarkus #
Ketika microservice dan container menjadi standar arsitektur modern, kelemahan JVM tradisional mulai terasa lebih nyata: startup time beberapa detik, konsumsi memori yang besar bahkan untuk service yang kecil, dan warm-up time sebelum JIT compilation mencapai performa optimal. Quarkus dirancang oleh Red Hat secara spesifik untuk menjawab tantangan ini. Alih-alih melakukan refleksi dan class loading di runtime seperti framework Java pada umumnya, Quarkus memindahkan sebagian besar kerja framework ke waktu kompilasi — dependency injection, konfigurasi, dan metadata anotasi diproses saat build, bukan saat aplikasi pertama kali dijalankan. Hasilnya dramatis: startup dalam milidetik, konsumsi memori yang jauh lebih rendah, dan dengan GraalVM Native Image, aplikasi Java bisa dikompilasi menjadi binary native yang berjalan tanpa JVM sama sekali. Quarkus juga tidak memperkenalkan API baru — ia menggunakan standar industri yang sudah ada: Jakarta EE (JAX-RS, CDI, JPA) dan MicroProfile (Config, Health, Metrics, OpenAPI).
Filosofi Quarkus: Shift Left #
Istilah “shift left” dalam konteks Quarkus berarti memindahkan pekerjaan sebanyak mungkin dari runtime ke build time. Ini adalah perbedaan mendasar dari cara kerja framework Java tradisional.
flowchart TD
subgraph TRADITIONAL[Framework Tradisional — Runtime Heavy]
A1[Startup] --> B1[Scan classpath]
B1 --> C1[Proses anotasi]
C1 --> D1[Buat proxy]
D1 --> E1[Wire dependency]
E1 --> F1[Siap melayani request]
note1[⏱ Startup: 5-15 detik\n💾 Memori: 200-500 MB]
end
subgraph QUARKUS[Quarkus — Build-Time Optimization]
A2[Build Time] --> B2[Scan classpath]
B2 --> C2[Proses anotasi]
C2 --> D2[Buat proxy]
D2 --> E2[Hasilkan bytecode optimized]
A3[Runtime Startup] --> F3[Load pre-computed metadata]
F3 --> G3[Siap melayani request]
note2[⏱ Startup: 0.3-1 detik\n💾 Memori: 50-150 MB]
endJVM Mode vs Native Mode #
Quarkus bisa berjalan dalam dua mode yang punya tradeoff berbeda:
| JVM Mode | Native Mode | |
|---|---|---|
| Cara build | mvn package | mvn package -Pnative |
| Startup time | ~0.5–2 detik | ~10–50 milidetik |
| Memori awal | ~100–200 MB | ~30–80 MB |
| Peak throughput | Sangat tinggi (JIT) | Tinggi (AOT, tanpa JIT warm-up) |
| Build time | Cepat (~10 detik) | Lama (2–5 menit, butuh GraalVM) |
| Debugging | Normal | Terbatas |
| Cocok untuk | Long-running service | Serverless, CLI, container pendek |
flowchart TD
A{Service berjalan\nterus-menerus?} -- Ya --> B{Startup time\nkritis?}
A -- Tidak --> NATIVE[Native Mode\nServerless / FaaS]
B -- Ya --> NATIVE
B -- Tidak --> C{Developer\nexperience prioritas?}
C -- Ya --> JVM[JVM Mode\nlebih mudah di-debug]
C -- Tidak --> D{Memory footprint\nkritis?}
D -- Ya --> NATIVE
D -- Tidak --> JVMSetup Project #
Cara termudah membuat project Quarkus baru adalah melalui code.quarkus.io atau CLI:
# Install Quarkus CLI
curl -Ls https://sh.jbang.dev | bash -s - trust add https://repo1.maven.org/maven2/io/quarkus/quarkus-cli/
curl -Ls https://sh.jbang.dev | bash -s - app install --fresh --verbose quarkus@quarkusio
# Buat project baru
quarkus create app com.contoh:toko-online \
--extension='rest,rest-jackson,hibernate-orm-panache,jdbc-postgresql,smallrye-openapi'
# Masuk ke direktori dan jalankan dev mode (hot reload)
cd toko-online
quarkus dev
# atau: ./mvnw quarkus:dev
Alternatif dengan Maven:
mvn io.quarkus.platform:quarkus-maven-plugin:3.11.0:create \
-DprojectGroupId=com.contoh \
-DprojectArtifactId=toko-online \
-Dextensions="rest,rest-jackson,hibernate-orm-panache,jdbc-postgresql"
Struktur direktori project Quarkus:
toko-online/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/contoh/
│ │ │ ├── model/ ← entity Panache
│ │ │ ├── resource/ ← JAX-RS resource (controller)
│ │ │ └── service/ ← logika bisnis
│ │ └── resources/
│ │ ├── application.properties ← konfigurasi
│ │ └── import.sql ← data awal (opsional)
│ └── test/
│ └── java/
│ └── com/contoh/
├── pom.xml
└── src/main/docker/
├── Dockerfile.jvm ← Docker image JVM mode
└── Dockerfile.native ← Docker image native mode
File pom.xml kunci:
<properties>
<quarkus.platform.version>3.11.0</quarkus.platform.version>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.quarkus.platform</groupId>
<artifactId>quarkus-bom</artifactId>
<version>${quarkus.platform.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- REST API dengan Jackson JSON -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<!-- ORM dengan Panache (wrapper Hibernate) -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm-panache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<!-- Validasi -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<!-- Health check dan metrics (MicroProfile) -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
<!-- OpenAPI / Swagger UI -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<!-- Testing -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Konfigurasi #
Quarkus menggunakan src/main/resources/application.properties dengan dukungan MicroProfile Config. Quarkus juga mendukung .env file dan environment variable secara native.
# ===== Datasource =====
quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=postgres
quarkus.datasource.password=rahasia
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/tokoonline
# Connection pool (Agroal)
quarkus.datasource.jdbc.max-size=10
quarkus.datasource.jdbc.min-size=2
# ===== Hibernate ORM =====
quarkus.hibernate-orm.database.generation=validate
# none → tidak ubah schema
# validate → validasi schema (production)
# update → update schema (development)
# drop-and-create → reset schema (test)
quarkus.hibernate-orm.log.sql=false
# ===== HTTP =====
quarkus.http.port=8080
quarkus.http.cors=true
quarkus.http.cors.origins=https://frontend.contoh.com,http://localhost:3000
quarkus.http.cors.methods=GET,POST,PUT,DELETE,OPTIONS
# ===== OpenAPI =====
quarkus.smallrye-openapi.info-title=Toko Online API
quarkus.smallrye-openapi.info-version=1.0.0
mp.openapi.extensions.smallrye.operationIdStrategy=METHOD
quarkus.swagger-ui.always-include=true # tampilkan Swagger UI di production (nonaktifkan jika tidak perlu)
# ===== Logging =====
quarkus.log.level=INFO
quarkus.log.category."com.contoh".level=DEBUG
# ===== Native build =====
quarkus.native.additional-build-args=-H:ResourceConfigurationFiles=resources-config.json
Konfigurasi Kustom dengan @ConfigProperty #
import org.eclipse.microprofile.config.inject.ConfigProperty;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.Optional;
@ApplicationScoped
public class AppConfig {
// Property wajib — aplikasi gagal start jika tidak ada
@ConfigProperty(name = "app.nama")
String appNama;
// Property dengan nilai default
@ConfigProperty(name = "app.batas-halaman", defaultValue = "20")
int batasHalaman;
// Property opsional
@ConfigProperty(name = "app.api-key")
Optional<String> apiKey;
// Property dari environment variable: APP_UPLOAD_DIR
// (titik dan tanda hubung dalam nama property dikonversi ke underscore)
@ConfigProperty(name = "app.upload.dir", defaultValue = "/tmp/uploads")
String uploadDir;
public String getAppNama() { return appNama; }
public int getBatasHalaman() { return batasHalaman; }
public Optional<String> getApiKey() { return apiKey; }
public String getUploadDir() { return uploadDir; }
}
# application.properties
app.nama=Toko Online
app.batas-halaman=25
app.upload.dir=/data/uploads
# app.api-key= (opsional, tidak perlu didefinisikan)
Profiles di Quarkus #
# Berlaku untuk semua profile
quarkus.datasource.db-kind=postgresql
# Profile dev — aktif saat quarkus dev
%dev.quarkus.datasource.db-kind=h2
%dev.quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb
%dev.quarkus.hibernate-orm.database.generation=drop-and-create
%dev.quarkus.hibernate-orm.log.sql=true
# Profile test — aktif saat mvn test
%test.quarkus.datasource.db-kind=h2
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1
%test.quarkus.hibernate-orm.database.generation=drop-and-create
# Profile prod — aktif di production
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql://prod-db:5432/tokoonline
%prod.quarkus.hibernate-orm.database.generation=validate
Dependency Injection dengan CDI #
Quarkus menggunakan CDI (Contexts and Dependency Injection) — standar Jakarta EE — sebagai mekanisme dependency injection-nya.
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
// @ApplicationScoped — satu instance untuk seluruh aplikasi (thread-safe!)
// @Singleton — mirip ApplicationScoped, sedikit lebih ringan (tidak ada proxy)
// @RequestScoped — satu instance per HTTP request
// @SessionScoped — satu instance per HTTP session
@ApplicationScoped
public class ProdukService {
// Inject dengan @Inject — constructor injection lebih direkomendasikan
@Inject
ProdukRepository produkRepository;
// Constructor injection — lebih eksplisit dan mudah ditest
// (keduanya valid di Quarkus)
private final NotifikasiService notifikasiService;
public ProdukService(NotifikasiService notifikasiService) {
this.notifikasiService = notifikasiService;
}
public java.util.List<Produk> semuaProduk() {
return produkRepository.listAll();
}
}
Qualifier dan Alternative #
import jakarta.enterprise.inject.Produces;
import jakarta.enterprise.inject.Alternative;
import jakarta.inject.Qualifier;
import java.lang.annotation.*;
// Definisi qualifier kustom
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE})
public @interface EmailProduction {}
// Implementasi production
@ApplicationScoped
@EmailProduction
public class SmtpEmailService implements EmailService {
@Override
public void kirim(String ke, String subjek, String isi) {
// implementasi SMTP sungguhan
System.out.println("Email terkirim via SMTP ke: " + ke);
}
}
// Implementasi mock untuk development/test
@Alternative
@io.quarkus.arc.profile.IfBuildProfile("dev")
@ApplicationScoped
public class MockEmailService implements EmailService {
@Override
public void kirim(String ke, String subjek, String isi) {
System.out.println("[MOCK] Email ke " + ke + ": " + subjek);
}
}
REST Resource dengan JAX-RS #
Quarkus menggunakan JAX-RS (Jakarta RESTful Web Services) sebagai standar untuk mendefinisikan REST endpoint. API-nya sangat mirip dengan Spring MVC tapi menggunakan anotasi yang berbeda.
package com.contoh.resource;
import com.contoh.model.Produk;
import com.contoh.service.ProdukService;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import java.net.URI;
import java.util.List;
@Path("/api/produk")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = "Produk", description = "Manajemen data produk")
public class ProdukResource {
@Inject
ProdukService produkService;
// GET /api/produk
@GET
@Operation(summary = "Ambil semua produk")
public List<Produk> semuaProduk() {
return produkService.semuaProduk();
}
// GET /api/produk/{id}
@GET
@Path("/{id}")
@Operation(summary = "Ambil produk berdasarkan ID")
public Response cariProduk(@PathParam("id") Long id) {
return produkService.cariById(id)
.map(produk -> Response.ok(produk).build())
.orElse(Response.status(Response.Status.NOT_FOUND)
.entity(new ErrorResponse("Produk dengan ID " + id + " tidak ditemukan"))
.build());
}
// GET /api/produk/cari?kata=laptop
@GET
@Path("/cari")
public List<Produk> cari(@QueryParam("kata") String kata,
@QueryParam("halaman") @DefaultValue("0") int halaman,
@QueryParam("ukuran") @DefaultValue("20") int ukuran) {
return produkService.cariByNama(kata, halaman, ukuran);
}
// POST /api/produk
@POST
@Operation(summary = "Buat produk baru")
public Response buatProduk(@Valid ProdukRequest request) {
Produk produk = produkService.buat(request);
// Return 201 Created dengan Location header ke resource baru
return Response.created(URI.create("/api/produk/" + produk.id))
.entity(produk)
.build();
}
// PUT /api/produk/{id}
@PUT
@Path("/{id}")
public Response perbaruiProduk(@PathParam("id") Long id,
@Valid ProdukRequest request) {
return produkService.perbarui(id, request)
.map(produk -> Response.ok(produk).build())
.orElse(Response.status(Response.Status.NOT_FOUND).build());
}
// DELETE /api/produk/{id}
@DELETE
@Path("/{id}")
public Response hapusProduk(@PathParam("id") Long id) {
boolean berhasil = produkService.hapus(id);
return berhasil
? Response.noContent().build()
: Response.status(Response.Status.NOT_FOUND).build();
}
}
// DTO request
public class ProdukRequest {
@jakarta.validation.constraints.NotBlank(message = "Nama tidak boleh kosong")
@jakarta.validation.constraints.Size(min = 3, max = 100)
public String nama;
@jakarta.validation.constraints.NotNull
@jakarta.validation.constraints.DecimalMin("0.01")
public java.math.BigDecimal harga;
@jakarta.validation.constraints.Min(0)
public int stok;
}
// DTO error response
public record ErrorResponse(String pesan) {}
Akses Database dengan Panache #
Panache adalah abstraksi Quarkus di atas Hibernate ORM yang menghilangkan boilerplate. Ada dua pendekatan: Active Record (entity yang juga bisa query diri sendiri) dan Repository.
Active Record Pattern #
package com.contoh.model;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "produk")
public class Produk extends PanacheEntity {
// PanacheEntity sudah menyediakan field `id` (Long, auto-generated)
// dan semua method query statis
@NotBlank
@Size(max = 100)
@Column(nullable = false, length = 100)
public String nama;
@NotNull
@DecimalMin("0.01")
@Column(nullable = false, precision = 15, scale = 2)
public BigDecimal harga;
@Min(0)
@Column(nullable = false)
public int stok;
@Column(nullable = false)
public boolean aktif = true;
@Column(name = "dibuat_pada")
public LocalDateTime dibuatPada;
// Lifecycle callback
@PrePersist
public void preSimpan() {
dibuatPada = LocalDateTime.now();
}
// ===== Query Methods — didefinisikan di dalam entity =====
// Ambil semua produk aktif, urut nama
public static List<Produk> semuaAktif() {
return list("aktif = true ORDER BY nama");
}
// Cari berdasarkan nama (partial, case-insensitive)
public static List<Produk> cariByNama(String kata) {
return list("lower(nama) like lower(?1)", "%" + kata + "%");
}
// Produk dengan stok menipis
public static List<Produk> stokMenipis(int batas) {
return list("stok <= ?1 AND aktif = true ORDER BY stok asc", batas);
}
// Hitung produk aktif
public static long hitungAktif() {
return count("aktif = true");
}
// Update stok secara bulk (lebih efisien dari update satu per satu)
public static long tambahStokSemua(int jumlah) {
return update("stok = stok + ?1 WHERE aktif = true", jumlah);
}
}
Repository Pattern #
package com.contoh.repository;
import com.contoh.model.Produk;
import io.quarkus.hibernate.orm.panache.PanacheRepository;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.List;
@ApplicationScoped
public class ProdukRepository implements PanacheRepository<Produk> {
// PanacheRepository menyediakan: findById, listAll, persist, delete, count, dll
public List<Produk> cariByNama(String kata) {
return list("lower(nama) like lower(?1)", "%" + kata + "%");
}
public List<Produk> stokMenipis(int batas) {
return list("stok <= ?1 AND aktif = true", batas);
}
// Paginasi
public List<Produk> semuaDenganPaginasi(int halaman, int ukuran) {
return findAll()
.page(halaman, ukuran)
.list();
}
public long totalHalaman(int ukuranHalaman) {
return findAll().pageCount(ukuranHalaman);
}
}
Service dengan Transaksi #
package com.contoh.service;
import com.contoh.model.Produk;
import com.contoh.repository.ProdukRepository;
import com.contoh.resource.ProdukRequest;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.NotFoundException;
import java.util.List;
import java.util.Optional;
@ApplicationScoped
public class ProdukService {
@Inject
ProdukRepository produkRepository;
// @Transactional — buka transaksi di awal, commit di akhir (atau rollback jika exception)
// Method tanpa @Transactional tidak perlu transaksi (read-only aman)
public List<Produk> semuaProduk() {
return produkRepository.listAll();
}
public Optional<Produk> cariById(Long id) {
return produkRepository.findByIdOptional(id);
}
public List<Produk> cariByNama(String kata, int halaman, int ukuran) {
if (kata == null || kata.isBlank()) {
return produkRepository.semuaDenganPaginasi(halaman, ukuran);
}
return produkRepository.cariByNama(kata);
}
@Transactional
public Produk buat(ProdukRequest request) {
Produk produk = new Produk();
produk.nama = request.nama;
produk.harga = request.harga;
produk.stok = request.stok;
produkRepository.persist(produk);
return produk;
}
@Transactional
public Optional<Produk> perbarui(Long id, ProdukRequest request) {
return produkRepository.findByIdOptional(id).map(produk -> {
produk.nama = request.nama;
produk.harga = request.harga;
produk.stok = request.stok;
// Tidak perlu memanggil persist/update — Hibernate mendeteksi perubahan
// pada managed entity secara otomatis (dirty checking)
return produk;
});
}
@Transactional
public boolean hapus(Long id) {
return produkRepository.deleteById(id);
}
@Transactional
public void kurangiStok(Long id, int jumlah) {
Produk produk = produkRepository.findByIdOptional(id)
.orElseThrow(() -> new NotFoundException("Produk tidak ditemukan: " + id));
if (produk.stok < jumlah) {
throw new IllegalStateException(
"Stok tidak cukup. Tersedia: " + produk.stok + ", diminta: " + jumlah);
}
produk.stok -= jumlah;
}
}
Exception Handling Global #
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import jakarta.validation.ConstraintViolationException;
// @Provider mendaftarkan class ini sebagai JAX-RS provider (tidak butuh anotasi lain)
@Provider
public class ValidationExceptionMapper
implements ExceptionMapper<ConstraintViolationException> {
@Override
public Response toResponse(ConstraintViolationException exception) {
var errors = exception.getConstraintViolations().stream()
.collect(java.util.stream.Collectors.toMap(
cv -> cv.getPropertyPath().toString(),
cv -> cv.getMessage()
));
var body = java.util.Map.of(
"status", 400,
"pesan", "Validasi gagal",
"errors", errors
);
return Response.status(Response.Status.BAD_REQUEST)
.entity(body)
.build();
}
}
@Provider
public class NotFoundExceptionMapper
implements ExceptionMapper<jakarta.ws.rs.NotFoundException> {
@Override
public Response toResponse(jakarta.ws.rs.NotFoundException exception) {
return Response.status(Response.Status.NOT_FOUND)
.entity(java.util.Map.of("pesan", exception.getMessage()))
.build();
}
}
@Provider
public class GenericExceptionMapper implements ExceptionMapper<Exception> {
@Override
public Response toResponse(Exception exception) {
// Log error untuk internal tracking
java.util.logging.Logger.getLogger(getClass().getName())
.severe("Unhandled exception: " + exception.getMessage());
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(java.util.Map.of("pesan", "Terjadi kesalahan internal"))
.build();
}
}
Health Check dengan MicroProfile Health #
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;
import org.eclipse.microprofile.health.Readiness;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
// Liveness — apakah aplikasi masih hidup? (restart jika gagal)
@Liveness
@ApplicationScoped
public class LivenessCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
return HealthCheckResponse.up("aplikasi-liveness");
}
}
// Readiness — apakah aplikasi siap menerima request?
// (keluarkan dari load balancer jika gagal, jangan restart)
@Readiness
@ApplicationScoped
public class DatabaseReadinessCheck implements HealthCheck {
@Inject
jakarta.inject.Provider<javax.sql.DataSource> dataSourceProvider;
@Override
public HealthCheckResponse call() {
try {
javax.sql.DataSource ds = dataSourceProvider.get();
try (var conn = ds.getConnection();
var stmt = conn.createStatement()) {
stmt.execute("SELECT 1");
return HealthCheckResponse.named("database-readiness")
.up()
.withData("database", "PostgreSQL")
.withData("status", "terhubung")
.build();
}
} catch (Exception e) {
return HealthCheckResponse.named("database-readiness")
.down()
.withData("error", e.getMessage())
.build();
}
}
}
Endpoint health tersedia secara otomatis:
# Liveness check — Kubernetes livenessProbe
curl http://localhost:8080/q/health/live
# Readiness check — Kubernetes readinessProbe
curl http://localhost:8080/q/health/ready
# Semua health check
curl http://localhost:8080/q/health
# Contoh response
{
"status": "UP",
"checks": [
{ "name": "aplikasi-liveness", "status": "UP" },
{ "name": "database-readiness", "status": "UP", "data": { "database": "PostgreSQL" } }
]
}
Pengujian #
Quarkus menyediakan @QuarkusTest yang menjalankan aplikasi penuh dalam mode test, dan REST-Assured untuk testing HTTP endpoint.
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;
// @QuarkusTest — start aplikasi penuh dengan konfigurasi %test
@QuarkusTest
public class ProdukResourceTest {
@Test
public void semuaProduk_tanpaData_kembalikanArrayKosong() {
given()
.when().get("/api/produk")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("", hasSize(0));
}
@Test
public void buatProduk_denganDataValid_kembalikan201() {
String body = """
{
"nama": "Laptop Gaming",
"harga": 15000000,
"stok": 5
}
""";
given()
.contentType(ContentType.JSON)
.body(body)
.when().post("/api/produk")
.then()
.statusCode(201)
.header("Location", containsString("/api/produk/"))
.body("nama", equalTo("Laptop Gaming"))
.body("id", greaterThan(0));
}
@Test
public void buatProduk_namaKosong_kembalikan400() {
String body = """
{ "nama": "", "harga": 100000, "stok": 1 }
""";
given()
.contentType(ContentType.JSON)
.body(body)
.when().post("/api/produk")
.then()
.statusCode(400)
.body("errors", hasKey("nama"));
}
@Test
public void cariProduk_idTidakAda_kembalikan404() {
given()
.when().get("/api/produk/999")
.then()
.statusCode(404);
}
}
Mock Bean untuk Test #
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;
import org.mockito.Mockito;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
@QuarkusTest
public class ProdukResourceMockTest {
// InjectMock menggantikan bean dengan mock Mockito
@InjectMock
ProdukService produkService;
@BeforeEach
public void setup() {
Produk laptop = new Produk();
laptop.id = 1L;
laptop.nama = "Laptop";
laptop.harga = new java.math.BigDecimal("15000000");
Mockito.when(produkService.semuaProduk()).thenReturn(List.of(laptop));
Mockito.when(produkService.cariById(1L)).thenReturn(Optional.of(laptop));
Mockito.when(produkService.cariById(99L)).thenReturn(Optional.empty());
}
@Test
public void semuaProduk_kembalikanDataMock() {
given()
.when().get("/api/produk")
.then()
.statusCode(200)
.body("size()", equalTo(1))
.body("[0].nama", equalTo("Laptop"));
}
}
Build Native Image #
# Pastikan GraalVM sudah terinstall dengan native-image
# Export GRAALVM_HOME dan JAVA_HOME ke GraalVM
# Build native image (membutuhkan ~2-5 menit)
./mvnw package -Pnative
# Atau dengan container (tidak butuh GraalVM lokal — Docker harus jalan)
./mvnw package -Pnative -Dquarkus.native.container-build=true
# Jalankan binary native
./target/toko-online-1.0-SNAPSHOT-runner
# Build Docker image untuk native
docker build -f src/main/docker/Dockerfile.native -t toko-online:native .
docker run -i --rm -p 8080:8080 toko-online:native
Perbandingan nyata startup time:
# JVM mode
java -jar target/quarkus-app/quarkus-run.jar
→ Quarkus 3.x.x started in 0.852s
# Native mode
./target/toko-online-runner
→ Quarkus 3.x.x started in 0.018s ← 47x lebih cepat
# Spring Boot (JVM)
java -jar target/spring-boot-app.jar
→ Started Application in 3.421s
Native compilation punya keterbatasan: beberapa fitur Java yang bergantung pada refleksi (seperti serialisasi kustom, dynamic proxy, atau class loading) membutuhkan konfigurasi tambahan (reflect-config.json). Library pihak ketiga yang belum “Quarkus-aware” mungkin tidak kompatibel dengan native compilation.Kapan Menggunakan Quarkus dan Kapan Tidak #
GUNAKAN QUARKUS JIKA:
✓ Serverless atau FaaS — startup time milidetik sangat penting
✓ Container-dense environment — memory footprint kecil = lebih banyak pod
✓ Microservice kecil yang di-deploy dan di-restart sering
✓ Tim sudah familiar dengan Jakarta EE / MicroProfile
✓ Butuh native compilation untuk binary yang berjalan tanpa JVM
✓ Kubernetes-native — integrasi health check, metrics, dan config sangat mudah
✓ Dev mode dengan live coding yang sangat cepat (hot reload instant)
PERTIMBANGKAN ALTERNATIF JIKA:
✗ Tim lebih familiar dengan Spring — Spring Boot punya ekosistem lebih besar
✗ Banyak library pihak ketiga yang belum Quarkus-compatible
✗ Butuh native compilation tapi banyak pakai refleksi → konfigurasi kompleks
✗ Aplikasi monolith besar — keunggulan Quarkus kurang terasa
✗ Tim butuh dukungan komunitas yang sangat luas → Spring masih lebih besar
Ringkasan #
- Quarkus memindahkan kerja framework ke build time — dependency injection, anotasi processing, dan konfigurasi diselesaikan saat kompilasi, bukan saat startup. Hasilnya startup milidetik dan memori lebih kecil.
- JVM mode vs Native mode adalah tradeoff throughput vs startup/memory — gunakan JVM mode untuk long-running service, Native mode untuk serverless dan container yang sering di-restart.
- Quarkus menggunakan standar industri: JAX-RS untuk REST, CDI untuk dependency injection, Hibernate/Panache untuk ORM, MicroProfile untuk config/health/metrics — tidak ada API proprietary baru yang perlu dipelajari.
- Panache menghilangkan boilerplate Hibernate — pilih Active Record untuk kesederhanaan (
Produk.list(...)) atau Repository pattern untuk separasi concern yang lebih bersih.@Transactionaldi method service yang melakukan write — Hibernate secara otomatis mendeteksi perubahan pada managed entity (dirty checking) dan mengeksekusi SQL saat transaksi commit.- MicroProfile Health (
@Liveness,@Readiness) terintegrasi langsung dengan Kubernetes probe — tidak perlu konfigurasi tambahan di deployment YAML.@QuarkusTestdengan REST-Assured adalah kombinasi standar untuk integration test — aplikasi berjalan penuh dengan profil%testyang mengarah ke database in-memory H2.quarkus devadalah dev mode terbaik di ekosistem Java — perubahan kode diterapkan secara instan tanpa restart, dan Dev UI tersedia dihttp://localhost:8080/q/dev.