Spring Boot Dasar

Target: Pemula yang telah memahami Java dasar, OOP, Generic, Collection, dan Database (JDBC), serta ingin menguasai fondasi arsitektur enterprise modern menggunakan Spring Boot 3.3+ (Spring Framework 6.x & Java 21 LTS). Versi: Spring Boot 3.x (Java 17+) Prasyarat: Java OOP Fokus modul pembelajaran ini: Inversion of Control (IoC) mental model → @SpringBootApplication & Component Scanning → IoC Container & ApplicationContext → Stereotypes (@Component, @Service, @Repository) → @Configuration & @Bean → Constructor Dependency Injection (Best Practice) → @Primary & @Qualifier → Bean Lifecycle (@PostConstruct, @PreDestroy) → Scopes & @Lazy@Value & @ConfigurationProperties → Multi-Environment Profiles → Event Listener → Logging SLF4J → mini project engine notifikasi & diskon modular CLI.


Cara Belajar

🟢 Fundamental
→ wajib dipahami: Inversion of Control, ApplicationContext, Stereotype Beans, dan Constructor Injection
 
🟡 Lanjutan
→ pelajari setelah memahami DI: Bean Lifecycle, Scopes, @ConfigurationProperties, Profiles, dan Event Listener
 
🔴 Advanced / Operasional
→ penting untuk arsitektur production: Logging SLF4J/Logback, Conditional Beans, dan Runner Interfaces

Mental model alur siklus hidup Spring Boot Application & IoC Container:

               Entry Point: main()


       SpringApplication.run(App.class, args)


       ┌───────────────────────────────────────┐
       │     INISIALISASI APPLICATIONCONTEXT   │
       │  1. Scan Packages (@ComponentScan)    │
       │  2. Instansiasi Beans (@Component)    │
       │  3. Suntik Ketergantungan (DI)        │
       │  4. Jalankan @PostConstruct           │
       └───────────────────┬───────────────────┘


       ┌───────────────────────────────────────┐
       │   APLIKASI SIAP / RUNNERS BERJALAN    │
       │   (CommandLineRunner / Dev Services)  │
       └───────────────────┬───────────────────┘

                           ▼  Aplikasi Dimatikan (Shutdown)
       ┌───────────────────────────────────────┐
       │   PEMBERSIHAN BEAN (@PreDestroy)      │
       └───────────────────────────────────────┘

Hafalan:

IoC Container      → wadah sentral Spring yang membuat, mengonfigurasi, dan merangkai siklus hidup objek (Beans)
Spring Bean        → objek Java biasa (POJO) yang diinstansiasi dan dikelola penuh oleh Spring IoC Container
Dependency Injection (DI) → pola di mana objek menerima dependensi dari luar (disediakan Container), bukan membuat sendiri via new
Constructor Injection → cara penyuntikan dependensi melalui constructor class (standar emas industri, immutable & testable)
@SpringBootApplication → anotasi gabungan pembuka yang mengaktifkan auto-configuration dan component scanning

Daftar Isi

🟢 Fundamental

  1. Pengenalan Spring Boot & Mental Model Inversion of Control (IoC)
  2. Struktur Proyek Spring Boot & Anatomi @SpringBootApplication
  3. Spring IoC Container & ApplicationContext
  4. Mendefinisikan Bean dengan Stereotype Annotations
  5. Mendefinisikan Custom Bean dengan @Configuration & @Bean
  6. Dependency Injection (DI) & Constructor Injection
  7. Mengatasi Ambiguitas Bean: @Primary & @Qualifier
  8. Bean Lifecycle: @PostConstruct & @PreDestroy

🟡 Lanjutan

  1. Bean Scopes (singleton vs prototype)
  2. Lazy Initialization Bean (@Lazy)
  3. Mengambil Nilai Konfigurasi dengan @Value
  4. Type-Safe Configuration dengan @ConfigurationProperties
  5. Format Konfigurasi: application.properties vs application.yaml
  6. Spring Profiles & Multi-Environment Setup
  7. Conditional Beans (@ConditionalOnProperty, @ConditionalOnMissingBean)
  8. Event Handling di Spring Boot (@EventListener)

🔴 Advanced / Operasional

  1. Spring Boot Logging dengan SLF4J & Logback
  2. Runner Interfaces (CommandLineRunner & ApplicationRunner)

🛠️ Referensi & Praktik

  1. Peta Ingatan Cepat
  2. Tabel Ringkasan
  3. Cheat Code Spring Boot Dasar 10 Detik
  4. Urutan Belajar yang Disarankan
  5. Mini Project: Production-Ready Modular Order Notification & Discount Engine CLI
  6. Referensi Resmi

1. 🟢 Pengenalan Spring Boot & Mental Model Inversion of Control (IoC)

Konsep

Dalam pemrograman tradisional (tanpa framework), class Anda bertanggung jawab penuh untuk menginstansiasi seluruh objek yang dibutuhkannya secara manual menggunakan kata kunci new. Ini menyebabkan kode saling mengunci rapat (tightly coupled), sulit di-unit test, dan rawan kesalahan manajemen memori.

Inversion of Control (IoC) membalikkan kendali tersebut:

  • Anda tidak lagi memanggil new Service() atau new Repository().
  • Spring IoC Container yang bertugas membuat objek, mengonfigurasinya, dan menyuntikkannya ke class yang membutuhkan saat aplikasi dimulai.
  • Objek yang dikelola oleh Spring IoC Container disebut Spring Bean.

Contoh

// 1. CARA LAMA (Tightly Coupled - Hindari di Spring)
class OldOrderService {
    private EmailNotification emailNotification = new EmailNotification(); // Membuat sendiri via 'new'
}
 
// 2. CARA SPRING BOOT (Inversion of Control - Loose Coupling)
@Service
public class ModernOrderService {
    private final NotificationService notificationService;
 
    // Spring yang secara otomatis menyuntikkan implementasi NotificationService yang tepat
    public ModernOrderService(NotificationService notificationService) {
        this.notificationService = notificationService;
    }
 
    public void processOrder(String orderId) {
        notificationService.sendNotification("Pesanan #" + orderId + " berhasil diproses!");
    }
}

Cara Kerja

Pendekatan Tradisional:
OrderService ──(new)──> EmailNotification (Tergantung Kaku)
 
Pendekatan Spring IoC:
┌───────────────────────────────────────────────┐
│              SPRING IOC CONTAINER             │
│  [NotificationService] ──(Inject)──> [OrderService]
└───────────────────────────────────────────────┘

Hafalan:

Inversion of Control (IoC) → prinsip pemindahan tanggung jawab penciptaan objek dari programmer ke Spring Container
Loose Coupling             → kondisi di mana antar komponen aplikasi tidak saling bergantung secara kaku

2. 🟢 Struktur Proyek Spring Boot & Anatomi @SpringBootApplication

Konsep

Setiap aplikasi Spring Boot memiliki satu class utama (Entry Point) yang ditandai dengan anotasi @SpringBootApplication.

Anotasi ini adalah anotasi komposit (gabungan dari 3 anotasi penting):

  1. @SpringBootConfiguration: Menandai class sebagai sumber konfigurasi bean Spring.
  2. @EnableAutoConfiguration: Menginstruksikan Spring Boot untuk secara cerdas mengonfigurasi komponen database, server, dan security secara otomatis berdasarkan dependensi starter yang ada di pom.xml / build.gradle.
  3. @ComponentScan: Menginstruksikan Spring untuk memindai (scan) seluruh class, package, dan sub-package di bawah lokasi class utama untuk mencari anotasi stereotype (@Component, @Service, @Repository, @Controller).

Contoh

package com.belajar.springboot;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
 
@SpringBootApplication
public class BelajarSpringBootApplication {
    public static void main(String[] args) {
        // Memulai dan menginisialisasi seluruh ekosistem Spring Boot
        SpringApplication.run(BelajarSpringBootApplication.class, args);
    }
}

Cara Kerja

com.belajar.springboot (Root Package - Lokasi @SpringBootApplication)

  ├──> com.belajar.springboot.service    (Otomatis Ter-scan)
  ├──> com.belajar.springboot.repository (Otomatis Ter-scan)
  └──> com.belajar.springboot.controller (Otomatis Ter-scan)

Hafalan:

@SpringBootApplication      → anotasi pembuka entry point yang mengaktifkan auto-configuration dan component scanning
SpringApplication.run(Class, args) → memicu proses booting container, web server, dan context Spring

Kesalahan Umum

❌ Meletakkan class @Service atau @Repository di luar root package @SpringBootApplication (misal: class utama di com.belajar.app, tetapi service di com.lain.service), sehingga service tidak pernah ter-scan dan memicu error NoSuchBeanDefinitionException.

✅ Selalu letakkan class @SpringBootApplication di root package teratas proyek Anda.


3. 🟢 Spring IoC Container & ApplicationContext

Konsep

ApplicationContext adalah antarmuka utama yang merepresentasikan Spring IoC Container.

Melalui ApplicationContext, kita dapat:

  • Mengambil objek Bean berdasarkan tipe class atau nama bean: context.getBean(Class)
  • Memeriksa ketersediaan bean: context.containsBean(String)
  • Memeriksa total jumlah bean yang terdaftar di aplikasi.

Contoh

package com.belajar.springboot;
 
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
 
@Component
class SystemClock {
    public void printCurrentTime() {
        System.out.println("⏰ Waktu Sistem: " + System.currentTimeMillis());
    }
}
 
@SpringBootApplication
public class ApplicationContextDemo {
    public static void main(String[] args) {
        // Mengambil referensi ApplicationContext saat booting
        ApplicationContext context = SpringApplication.run(ApplicationContextDemo.class, args);
 
        // Mengambil Bean dari Container secara manual
        SystemClock clock = context.getBean(SystemClock.class);
        clock.printCurrentTime();
 
        System.out.println("Total Bean Terdaftar: " + context.getBeanDefinitionCount());
    }
}

Output

⏰ Waktu Sistem: 1724935000000
Total Bean Terdaftar: 78

Hafalan:

ApplicationContext context = SpringApplication.run(...) → mengakses wadah penampung bean Spring
context.getBean(TargetClass.class)                     → mengambil instance bean yang dikelola container

4. 🟢 Mendefinisikan Bean dengan Stereotype Annotations

Konsep

Cara paling umum mendaftarkan class kita sendiri agar menjadi Spring Bean adalah dengan menambahkan salah satu Stereotype Annotation di atas deklarasi class:

AnotasiLapisan ArsitekturTujuan & Kegunaan
@ComponentUmum / UtilityAnotasi dasar untuk class pembantu (helper), generator, listener.
@ServiceBusiness LogicMenampung aturan bisnis (Service Layer), transaksi, kalkulasi.
@RepositoryData Access (DAO)Menangani query database, otomatis menerjemahkan vendor SQLException.
@ControllerPresentationMenangani request HTTP berbasis Web HTML / MVC.
@RestControllerRESTful APIMenangani request HTTP yang mengembalikan format JSON / XML.

Contoh

package com.belajar.springboot.service;
 
import org.springframework.stereotype.Service;
 
@Service
public class DiskonService {
    public double hitungDiskon(double totalBelanja) {
        if (totalBelanja >= 500_000) {
            return totalBelanja * 0.10; // Diskon 10%
        }
        return 0.0;
    }
}

Hafalan:

@Component  → penanda umum bean Spring
@Service    → penanda bean khusus logika bisnis aplikasi
@Repository → penanda bean khusus akses data dan database

5. 🟢 Mendefinisikan Custom Bean dengan @Configuration & @Bean

Konsep

Jika Anda ingin mendaftarkan class pihak ketiga (Third-Party Libraries seperti ObjectMapper Jackson, Redis Template, atau RestTemplate yang kodenya tidak bisa Anda beri anotasi @Component langsung), gunakan kombinasi:

  1. @Configuration: Menandai class Java murni sebagai pabrik konfigurasi bean.
  2. @Bean: Diletakkan di atas method. Nilai kembalian (return value) dari method tersebut akan otomatis didaftarkan menjadi Spring Bean di dalam IoC Container.

Secara default, nama Bean adalah nama method-nya.

Contoh

package com.belajar.springboot.config;
 
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.text.SimpleDateFormat;
 
@Configuration
public class ThirdPartyConfig {
 
    // Mendaftarkan SimpleDateFormat dari library Java standar sebagai Spring Bean
    @Bean
    public SimpleDateFormat standardDateFormat() {
        return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    }
 
    // Memberi nama kustom pada bean
    @Bean(name = "indonesianDateFormat")
    public SimpleDateFormat indonesianDateFormat() {
        return new SimpleDateFormat("dd MMMM yyyy");
    }
}

Cara Kerja

Spring Boot Startup ──> Scan @Configuration ──> Jalankan method @Bean ──> Simpan objek di Container

Hafalan:

@Configuration → menandai class sebagai konfigurasi pabrik pembuatan bean
@Bean          → mendaftarkan return value sebuah method menjadi objek Spring Bean

6. 🟢 Dependency Injection (DI) & Constructor Injection

Konsep

Dependency Injection adalah mekanisme di mana Spring menyuntikkan (inject) bean A ke dalam bean B yang membutuhkannya.

Tiga cara melakukan DI di Spring:

  1. Field Injection (@Autowired private Service s;):SANGAT TIDAK DISARANKAN. Sulit di-unit test (karena private), menyembunyikan dependensi, dan rawan NullPointer di luar Spring context.
  2. Setter Injection: ⚠️ Digunakan hanya jika dependensi bersifat opsional (nullable).
  3. Constructor Injection (STANDAR EMAS INDUSTRI / REKOMENDASI UTAMA):
    • Mendeklarasikan field sebagai private final (Immutability).
    • Mudah di-unit test murni tanpa perlu menyalakan Spring Context (new Service(mockDep)).
    • Sejak Spring 4.3+, jika class hanya memiliki 1 constructor, anotasi @Autowired tidak wajib ditulis lagi.

Contoh

package com.belajar.springboot.service;
 
import org.springframework.stereotype.Service;
 
@Service
public class CheckoutService {
    // 1. Deklarasi field immutable (final)
    private final DiskonService diskonService;
 
    // 2. Constructor Injection (Aman, Bersih, dan Standard Modern)
    public CheckoutService(DiskonService diskonService) {
        this.diskonService = diskonService;
    }
 
    public double hitungTotalAkhir(double totalBelanja) {
        double diskon = diskonService.hitungDiskon(totalBelanja);
        return totalBelanja - diskon;
    }
}

Cara Kerja

1. Container membuat Bean DiskonService
2. Container membuat Bean CheckoutService sambil menyuapkan DiskonService ke Constructor

Hafalan:

public TargetService(DependencyService dependency) { this.dependency = dependency; } → pola Constructor Injection resmi

7. 🟢 Mengatasi Ambiguitas Bean: @Primary & @Qualifier

Konsep

Jika Anda memiliki sebuah Interface (misal: PaymentGateway) dan memiliki lebih dari satu class implementasi yang sama-sama terdaftar sebagai Bean (GopayService dan OvoService), Spring akan bingung memilih dan melempar error NoUniqueBeanDefinitionException.

Dua cara menyelesaikannya:

  1. @Primary: Menandai salah satu implementasi sebagai default bean utama jika tidak ada instruksi spesifik.
  2. @Qualifier("beanName"): Memilih secara eksplisit bean mana yang ingin disuntikkan berdasarkan nama bean-nya (secara default nama bean adalah nama class berhuruf kecil di awal, misal: gopayService).

Contoh

package com.belajar.springboot.payment;
 
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
 
// Interface Bersama
interface PaymentGateway {
    String prosesBayar(double nominal);
}
 
// Implementasi 1 (Sebagai Pilihan Utama Default via @Primary)
@Component
@Primary
class GopayGateway implements PaymentGateway {
    @Override
    public String prosesBayar(double nominal) {
        return "Pembayaran GoPay Rp " + nominal + " Berhasil";
    }
}
 
// Implementasi 2
@Component("ovoGateway")
class OvoGateway implements PaymentGateway {
    @Override
    public String prosesBayar(double nominal) {
        return "Pembayaran OVO Rp " + nominal + " Berhasil";
    }
}
 
// Service yang memilih Gateway spesifik via @Qualifier
@Service
class OrderPaymentProcessor {
    private final PaymentGateway paymentGateway;
 
    // Memilih OvoGateway secara spesifik
    public OrderPaymentProcessor(@Qualifier("ovoGateway") PaymentGateway paymentGateway) {
        this.paymentGateway = paymentGateway;
    }
 
    public void bayar(double total) {
        System.out.println(paymentGateway.prosesBayar(total));
    }
}

Hafalan:

@Primary                   → menjadikan bean sebagai prioritas default saat terjadi ambiguitas
@Qualifier("beanName")     → menentukan secara spesifik bean mana yang di-inject berdasarkan nama

8. 🟢 Bean Lifecycle: @PostConstruct & @PreDestroy

Konsep

Seringkali sebuah Bean membutuhkan persiapan awal (misal: membuka socket koneksi, menghangatkan cache data) tepat setelah dependensinya di-inject, dan pembersihan resource saat aplikasi dimatikan.

Anotasi Lifecycle Standar (Jakarta Annotation):

  • @PostConstruct: Dijalankan tepat satu kali segera setelah Bean selesai diinstansiasi dan seluruh Constructor Injection selesai.
  • @PreDestroy: Dijalankan tepat satu kali sesaat sebelum Bean dihancurkan dan aplikasi dimatikan (Graceful Shutdown).

Contoh

package com.belajar.springboot.lifecycle;
 
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Component;
 
@Component
public class CacheService {
 
    @PostConstruct
    public void inisialisasiCache() {
        System.out.println("🟢 [LIFECYCLE] @PostConstruct: Memuat data master ke memory cache...");
    }
 
    public void layaniRequest() {
        System.out.println("⚡ Melayani request dari cache.");
    }
 
    @PreDestroy
    public void bersihkanCache() {
        System.out.println("🔴 [LIFECYCLE] @PreDestroy: Menutup koneksi & membersihkan cache memori...");
    }
}

Cara Kerja

Instansiasi Objek ──> Constructor Injection ──> @PostConstruct ──> [Bean Siap Pakai] ──> Shutdown ──> @PreDestroy

Hafalan:

@PostConstruct → method inisialisasi awal setelah dependensi bean lengkap terpasang
@PreDestroy    → method pembersihan resource sebelum bean dimusnahkan saat aplikasi berhenti

9. 🟡 Bean Scopes (singleton vs prototype)

Konsep

Scope menentukan bagaimana Spring membuat instance dari sebuah Bean:

  1. singleton (DEFAULT):
    • Hanya dibuat tepat 1 instance di seluruh aplikasi.
    • Instance yang sama akan di-share dan disuntikkan ke seluruh komponen lain (Hemat memori).
    • Wajib bersifat Stateless (jangan menyimpan state data user di dalam instance variable).
  2. prototype (@Scope("prototype")):
    • Dibuat instance objek baru setiap kali bean tersebut diminta atau di-inject ke komponen lain.
  3. Web Scopes: request (1 instance per HTTP request), session (1 instance per HTTP session).

Contoh

package com.belajar.springboot.scope;
 
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.UUID;
 
// Prototype: Instance baru dengan ID unik setiap kali dipanggil
@Component
@Scope("prototype")
public class RequestIdGenerator {
    private final String id = UUID.randomUUID().toString();
 
    public String getId() { return id; }
}

Hafalan:

@Scope("singleton") → 1 instance tunggal untuk seluruh aplikasi (default)
@Scope("prototype") → instance baru selalu diciptakan setiap kali di-inject atau dipanggil

10. 🟡 Lazy Initialization Bean (@Lazy)

Konsep

Secara default, Spring menganut prinsip Eager Initialization (seluruh singleton bean diinstansiasi di awal saat booting aplikasi agar kesalahan dependensi langsung terdeteksi seketika).

Jika Anda memiliki Bean yang memakan resource berat dan jarang dipakai (misal: Report Heavy Engine), tambahkan anotasi @Lazy:

  • Bean tersebut tidak akan dibuat saat booting awal.
  • Bean baru dibuat pertama kali saat ada komponen yang benar-benar memanggil atau menggunakannya.

Contoh

package com.belajar.springboot.service;
 
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
 
@Service
@Lazy
public class HeavyReportService {
    public HeavyReportService() {
        System.out.println("🐢 [LAZY] HeavyReportService baru diinstansiasi saat dibutuhkan!");
    }
 
    public void generatePdf() {
        System.out.println("Menghasilkan laporan PDF kompleks...");
    }
}

Hafalan:

@Lazy → menunda pembuatan objek bean sampai pertama kali dipanggil untuk mempercepat startup

11. 🟡 Mengambil Nilai Konfigurasi dengan @Value

Konsep

Anotasi @Value digunakan untuk menyuntikkan nilai konfigurasi individual dari file application.properties atau application.yaml langsung ke field atau parameter constructor class.

Sintaks Format:

  • ${nama.property:nilaiDefault} : Mengambil nilai, dan menggunakan nilaiDefault jika key tidak ditemukan di properties.
  • #{systemProperties['user.home']} : Menggunakan SpEL (Spring Expression Language).

Contoh

Isi application.properties:

app.name=Toko Berkah API
app.version=2.1.0
app.max-upload-size=50

Class Java:

package com.belajar.springboot.config;
 
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
 
@Component
public class AppMetadata {
    private final String appName;
    private final String appVersion;
    private final int timeoutSeconds;
 
    public AppMetadata(
        @Value("${app.name}") String appName,
        @Value("${app.version:1.0.0}") String appVersion,
        @Value("${app.timeout:30}") int timeoutSeconds // 30 adalah default value jika key tidak ada
    ) {
        this.appName = appName;
        this.appVersion = appVersion;
        this.timeoutSeconds = timeoutSeconds;
    }
 
    public void printInfo() {
        System.out.printf("App: %s (v%s) | Timeout: %ds%n", appName, appVersion, timeoutSeconds);
    }
}

Hafalan:

@Value("${property.key:defaultValue}") → menyuntikkan nilai konfigurasi individual ke variabel

12. 🟡 Type-Safe Configuration dengan @ConfigurationProperties

Konsep

Untuk konfigurasi yang kompleks dan memiliki banyak properti bertingkat (misal: konfigurasi database, payment gateway, mail server), penggunaan @Value satu per satu menjadi tidak praktis dan rawan typo.

@ConfigurationProperties (Standar Modern):

  1. Memetakan (bind) seluruh grup properti hierarkis langsung ke dalam Java Class atau Java Record.
  2. Type-Safe: Otomatis mengonversi tipe data String ke int, Duration, boolean, List<String>, dll.
  3. Mendukung validasi otomatis dengan Jakarta Validation (@Validated).

Contoh

Isi application.yaml:

app:
  payment:
    merchant-id: "MID-9921"
    secret-key: "SuperSecretKey123"
    sandbox-mode: true
    allowed-banks:
      - BCA
      - BNI
      - MANDIRI

Deklarasi Record Properties (Java 21):

package com.belajar.springboot.config;
 
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
 
@ConfigurationProperties(prefix = "app.payment")
public record PaymentGatewayProperties(
    String merchantId,
    String secretKey,
    boolean sandboxMode,
    List<String> allowedBanks
) {}

Mengaktifkan di Configuration:

@Configuration
@EnableConfigurationProperties(PaymentGatewayProperties.class)
public class PaymentConfig {}

Hafalan:

@ConfigurationProperties(prefix = "app.section") → mapping kelompok konfigurasi hierarki ke objek/record Java

13. 🟡 Format Konfigurasi: application.properties vs application.yaml

Konsep

Spring Boot mendukung dua format konfigurasi utama yang terletak di folder src/main/resources/:

  1. application.properties (Format Key-Value Datar):
server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/db_toko
spring.datasource.username=root
spring.datasource.password=rahasia
  1. application.yaml / application.yml (Format Hierarkis Berindentasi - Sangat Direkomendasikan):
server:
  port: 8080
 
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db_toko
    username: root
    password: rahasia

Hafalan:

application.yaml → format konfigurasi modern hierarkis yang lebih bersih, mudah dibaca, dan bebas pengulangan prefix

14. 🟡 Spring Profiles & Multi-Environment Setup

Konsep

Aplikasi backend biasanya berjalan di beberapa lingkungan (environments): local, dev, staging, dan prod. Setiap environment membutuhkan konfigurasi berbeda (misal: database lokal vs database cloud production).

Spring Profiles memungkinkan kita:

  1. Memisahkan file konfigurasi:
    • application.yaml (Konfigurasi umum dasar).
    • application-dev.yaml (Konfigurasi khusus development).
    • application-prod.yaml (Konfigurasi khusus production).
  2. Membatasi Bean agar hanya aktif di profile tertentu: @Profile("dev") atau @Profile("!prod").
  3. Mengaktifkan profile aktif:
    • Di file: spring.profiles.active: dev
    • Di terminal saat menjalankan jar: java -jar app.jar --spring.profiles.active=prod

Contoh

package com.belajar.springboot.mail;
 
import org.springframework.context.annotation.Profile;
import org.springframework.stereotype.Service;
 
interface EmailSender {
    void kirimEmail(String tujuan, String pesan);
}
 
// Hanya aktif saat profile 'dev' aktif (Hanya cetak konsol, tidak kirim email sungguhan)
@Service
@Profile("dev")
class FakeEmailSender implements EmailSender {
    @Override
    public void kirimEmail(String tujuan, String pesan) {
        System.out.printf("📧 [DEV FAKE EMAIL] Ke: %s | Pesan: %s%n", tujuan, pesan);
    }
}
 
// Hanya aktif saat profile 'prod' aktif
@Service
@Profile("prod")
class SmtpRealEmailSender implements EmailSender {
    @Override
    public void kirimEmail(String tujuan, String pesan) {
        System.out.printf("🚀 [PROD REAL SMTP] Mengirim email jaringan ke: %s%n", tujuan);
    }
}

Hafalan:

@Profile("profileName")       → mengaktifkan bean hanya pada profile tertentu yang sedang aktif
spring.profiles.active=prod   → memilih environment profile yang aktif

15. 🟡 Conditional Beans (@ConditionalOnProperty, @ConditionalOnMissingBean)

Konsep

Spring Boot memiliki fitur Conditional Beans yang memungkinkan pembuatan Bean secara kondisional berdasarkan ketersediaan properti konfigurasi atau ketersediaan bean lain:

  • @ConditionalOnProperty(name = "fitur.ai.enabled", havingValue = "true"): Bean hanya dibuat jika properti tersebut bernilai true.
  • @ConditionalOnMissingBean(Interface.class): Bean hanya dibuat jika pengguna belum mendefinisikan custom bean untuk interface tersebut (pola dasar pembuatan Spring Boot Auto-Configuration).
  • @ConditionalOnClass(ClassName.class): Bean hanya aktif jika library jar tertentu ada di classpath.

Contoh

package com.belajar.springboot.feature;
 
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;
 
@Component
@ConditionalOnProperty(name = "app.feature.cashback.enabled", havingValue = "true")
public class CashbackFeatureService {
    public CashbackFeatureService() {
        System.out.println("🎁 Fitur Cashback AKTIF berdasarkan konfigurasi application.yaml!");
    }
}

Hafalan:

@ConditionalOnProperty(name = "key", havingValue = "val") → mendaftarkan bean hanya jika property sesuai
@ConditionalOnMissingBean(Class.class)                    → mendaftarkan bean fallback jika belum ada bean sejenis

16. 🟡 Event Handling di Spring Boot (@EventListener)

Konsep

Untuk menjaga antar modul aplikasi tetap terpisah bebas (Decoupled), Spring menyediakan sistem Event Handling internal:

  1. Event Object: Class POJO / Record yang membawa data kejadian (misal: OrderCreatedEvent).
  2. Event Publisher: Komponen yang memicu kejadian menggunakan ApplicationEventPublisher.publishEvent(event).
  3. Event Listener: Komponen yang mendengarkan dan merespons kejadian secara otomatis menggunakan anotasi @EventListener.

Contoh

package com.belajar.springboot.events;
 
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
 
// 1. Data Event
record OrderSuccessEvent(String orderId, double totalAmount) {}
 
// 2. Publisher
@Service
class OrderPlacementService {
    private final ApplicationEventPublisher eventPublisher;
 
    public OrderPlacementService(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }
 
    public void placeOrder(String orderId, double amount) {
        System.out.println("🛒 Pesanan #" + orderId + " berhasil dibuat.");
        // Menerbitkan Event ke seluruh Listener yang terdaftar
        eventPublisher.publishEvent(new OrderSuccessEvent(orderId, amount));
    }
}
 
// 3. Listener (Modul Notifikasi)
@Component
class OrderNotificationListener {
    @EventListener
    public void onOrderSuccess(OrderSuccessEvent event) {
        System.out.printf("🔔 [EVENT LISTENER] Mengirim struk notifikasi untuk Order #%s senilai Rp %,.2f%n", 
            event.orderId(), event.totalAmount());
    }
}

Cara Kerja

OrderPlacementService ──(publishEvent)──> Spring Event Bus ──> OrderNotificationListener (@EventListener)

Hafalan:

eventPublisher.publishEvent(eventObject) → memicu publikasi event internal ke dalam Spring Container
@EventListener                           → menandai method penerima yang otomatis menangkap event

17. 🔴 Spring Boot Logging dengan SLF4J & Logback

Konsep

Dalam aplikasi enterprise, DILARANG MENGGUNAKAN System.out.println() karena tidak memiliki level keparahan (log levels), tidak memiliki timestamp, dan tidak dapat dialihkan ke file log / cloud monitoring (ELK Stack).

Spring Boot menggunakan SLF4J (Simple Logging Facade for Java) dengan implementasi default Logback.

Tingkat Keparahan Log (dari terendah ke tertinggi): TRACE DEBUG INFO (Default) WARN ERROR.

Contoh

package com.belajar.springboot.logging;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
 
@Service
public class LogDemoService {
    // Inisialisasi Logger SLF4J
    private static final Logger log = LoggerFactory.getLogger(LogDemoService.class);
 
    public void prosesData(String input) {
        log.debug("Memulai proses input dengan nilai: {}", input); // Hanya tampil jika log level DEBUG aktif
 
        if (input == null || input.isBlank()) {
            log.warn("Peringatan: Input kosong diterima!");
            return;
        }
 
        try {
            log.info("Memproses transaksi untuk user: {}", input);
        } catch (Exception e) {
            log.error("Terjadi kegagalan fatal saat memproses: {}", e.getMessage(), e);
        }
    }
}

Cara Kerja

Pengaturan Log Level di application.yaml:

logging:
  level:
    root: INFO
    com.belajar.springboot: DEBUG # Aktifkan DEBUG khusus untuk package aplikasi kita

Hafalan:

private static final Logger log = LoggerFactory.getLogger(Class.class); → inisialisasi standard logger SLF4J
log.info("Pesan: {}", param);                                           → mencetak log dengan parameter placeholder {}

18. 🔴 Runner Interfaces (CommandLineRunner & ApplicationRunner)

Konsep

Seringkali kita perlu menjalankan perintah atau seeding data otomatis tepat setelah Spring Boot Context selesai dimuat dan seluruh Bean siap:

  1. CommandLineRunner: Menerima argumen terminal mentah bertipe String... args.
  2. ApplicationRunner: Menerima argumen terminal bertipe ApplicationArguments (mempermudah pemecahan opsi flag --nama=nilai).

Kedua interface ini otomatis dideteksi dan dieksekusi oleh Spring Boot saat aplikasi baru menyala.

Contoh

package com.belajar.springboot.runner;
 
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
 
@Component
@Order(1) // Urutan eksekusi prioritas pertama
public class StartupSeederRunner implements CommandLineRunner {
    @Override
    public void run(String... args) {
        System.out.println("🚀 [RUNNER] Aplikasi Spring Boot Siap! Menjalankan startup health check...");
    }
}

Hafalan:

CommandLineRunner  → interface eksekutor logika otomatis saat aplikasi pertama kali selesai booting
@Order(priority)   → mengatur urutan prioritas eksekusi antar Runner (angka terkecil = jalan duluan)

19. 🛠️ Peta Ingatan Cepat

                       PETA ARSITEKTUR SPRING BOOT CORE

       ┌──────────────────────────────┼──────────────────────────────┐
       ▼                              ▼                              ▼
INVERSION OF CONTROL (IoC)    DEPENDENCY INJECTION (DI)      CONFIG & PROFILES
├─ @SpringBootApplication     ├─ Constructor Injection (Best)├─ application.yaml
├─ ApplicationContext         ├─ @Primary (Default Priority) ├─ @Value ("${key}")
├─ @Component / @Service      ├─ @Qualifier ("beanName")     ├─ @ConfigurationProperties
└─ @Configuration + @Bean     └─ Lifecycle: @PostConstruct   └─ @Profile ("dev"/"prod")

20. 📚 Tabel Ringkasan

Anotasi / InterfaceLokasi TargetFungsi & Karakteristik Utama
@SpringBootApplicationMain ClassMengaktifkan Auto-Configuration, Component Scanning, dan Config
@ComponentClassMendaftarkan class umum menjadi Spring Bean di Container
@ServiceClassMendaftarkan class logika bisnis (Service Layer)
@RepositoryClassMendaftarkan class akses data/database (DAO Layer)
@ConfigurationClassClass pabrik pembuatan custom bean
@BeanMethodMendaftarkan return value method menjadi Spring Bean
@PrimaryClass / BeanMenjadikan bean prioritas utama jika ada banyak pilihan
@QualifierConstructor / ParamMemilih bean spesifik berdasarkan nama bean
@PostConstructMethodDieksekusi otomatis tepat setelah constructor & DI selesai
@PreDestroyMethodDieksekusi otomatis sesaat sebelum aplikasi dimatikan
@ConfigurationPropertiesClass / RecordMapping grup konfigurasi hierarkis secara type-safe
@ProfileClass / BeanMembatasi bean hanya aktif di environment profile tertentu
CommandLineRunnerInterface ClassMenjalankan logika konsol otomatis saat startup selesai

21. ⚡ Cheat Code Spring Boot Dasar 10 Detik

// 1. Template Standard Service dengan Constructor Injection
@Service
public class OrderService {
    private final PaymentGateway paymentGateway;
    private final NotificationService notificationService;
 
    public OrderService(PaymentGateway paymentGateway, NotificationService notificationService) {
        this.paymentGateway = paymentGateway;
        this.notificationService = notificationService;
    }
}
 
// 2. Template Custom Third-Party Bean
@Configuration
public class JacksonConfig {
    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper();
    }
}
 
// 3. Template ConfigurationProperties Record (Java 21)
@ConfigurationProperties(prefix = "app.security")
public record SecurityProperties(String jwtSecret, long expirationMs) {}

22. 🧭 Urutan Belajar yang Disarankan

Langkah 1: Pahami Mental Model IoC & Stereotypes
├── Pahami mengapa tidak boleh menggunakan 'new' manual
└── Kuasai perbedaan @Component, @Service, @Repository, dan @Configuration


Langkah 2: Kuasai Constructor-based Dependency Injection
├── Deklarasikan field sebagai 'private final'
└── Pelajari cara mengatasi ambiguitas bean dengan @Primary dan @Qualifier


Langkah 3: Kuasai Manajemen Konfigurasi
├── Gunakan application.yaml
├── Hindari @Value berlebihan, gunakan @ConfigurationProperties (Type-Safe)
└── Kuasai multi-environment dengan Spring Profiles (@Profile)


Langkah 4: Kuasai Observability & Event Decoupling
├── Ganti System.out dengan Logger SLF4J
└── Manfaatkan ApplicationEventPublisher & @EventListener


Langkah 5: Siap Melangkah ke Spring Boot Web (REST API) & Spring Data JPA!

23. 🏗️ Mini Project: Production-Ready Modular Order Notification & Discount Engine CLI

Aplikasi Spring Boot Core lengkap dan runnable yang mengintegrasikan: Constructor Injection, Multiple Profiles, @ConfigurationProperties, Event Listener, Custom Third-Party Bean, SLF4J Logging, dan CommandLineRunner.

package com.belajar.store;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
 
import java.text.DecimalFormat;
import java.util.List;
 
// 1. Type-Safe Configuration Properties (Record)
@ConfigurationProperties(prefix = "store.discount")
record DiscountProperties(double minOrderAmount, double discountPercentage, boolean promoActive) {}
 
// 2. Third-Party Utility Configuration
@Configuration
@EnableConfigurationProperties(DiscountProperties.class)
class StoreAppConfig {
    @Bean
    public DecimalFormat rupiahFormatter() {
        return new DecimalFormat("Rp #,##0.00");
    }
}
 
// 3. Domain Event
record OrderCompletedEvent(String orderId, String customer, double finalAmount) {}
 
// 4. Notification Channel Interface
interface NotificationService {
    void notifyCustomer(String customer, String message);
}
 
// Implementasi Dev Profile (Mock Console)
@Service
@Profile("dev")
class DevNotificationService implements NotificationService {
    private static final Logger log = LoggerFactory.getLogger(DevNotificationService.class);
 
    @Override
    public void notifyCustomer(String customer, String message) {
        log.info("🧪 [DEV NOTIFICATION] Kepada: {} | Isi: {}", customer, message);
    }
}
 
// Implementasi Prod Profile (Simulasi Gateway Nyata)
@Service
@Profile("prod")
class ProdNotificationService implements NotificationService {
    private static final Logger log = LoggerFactory.getLogger(ProdNotificationService.class);
 
    @Override
    public void notifyCustomer(String customer, String message) {
        log.info("🚀 [PROD SMS GATEWAY] Mengirim SMS ke: {} | {}", customer, message);
    }
}
 
// 5. Discount Engine Service (Constructor Injection)
@Service
class DiscountEngineService {
    private final DiscountProperties discountProps;
 
    public DiscountEngineService(DiscountProperties discountProps) {
        this.discountProps = discountProps;
    }
 
    public double calculateDiscount(double subtotal) {
        if (discountProps.promoActive() && subtotal >= discountProps.minOrderAmount()) {
            return subtotal * (discountProps.discountPercentage() / 100.0);
        }
        return 0.0;
    }
}
 
// 6. Order Processing Service (Event Publisher)
@Service
class OrderEngineService {
    private static final Logger log = LoggerFactory.getLogger(OrderEngineService.class);
 
    private final DiscountEngineService discountEngine;
    private final ApplicationEventPublisher eventPublisher;
    private final DecimalFormat rupiahFormatter;
 
    public OrderEngineService(
        DiscountEngineService discountEngine,
        ApplicationEventPublisher eventPublisher,
        DecimalFormat rupiahFormatter
    ) {
        this.discountEngine = discountEngine;
        this.eventPublisher = eventPublisher;
        this.rupiahFormatter = rupiahFormatter;
    }
 
    public void checkout(String orderId, String customer, double subtotal) {
        log.info("Memproses checkout pesanan: {} untuk {}", orderId, customer);
 
        double diskon = discountEngine.calculateDiscount(subtotal);
        double totalAkhir = subtotal - diskon;
 
        log.info("Subtotal: {} | Diskon: {} | Total Akhir: {}", 
            rupiahFormatter.format(subtotal), 
            rupiahFormatter.format(diskon), 
            rupiahFormatter.format(totalAkhir));
 
        // Menerbitkan Event
        eventPublisher.publishEvent(new OrderCompletedEvent(orderId, customer, totalAkhir));
    }
}
 
// 7. Event Listener (Decoupled Notification Handler)
@Component
class OrderEventListener {
    private final NotificationService notificationService;
    private final DecimalFormat rupiahFormatter;
 
    public OrderEventListener(NotificationService notificationService, DecimalFormat rupiahFormatter) {
        this.notificationService = notificationService;
        this.rupiahFormatter = rupiahFormatter;
    }
 
    @EventListener
    public void onOrderCompleted(OrderCompletedEvent event) {
        String pesan = String.format("Pesanan Anda #%s sebesar %s telah berhasil diproses!", 
            event.orderId(), rupiahFormatter.format(event.finalAmount()));
        notificationService.notifyCustomer(event.customer(), pesan);
    }
}
 
// 8. Main Application & Startup Runner
@SpringBootApplication
public class StoreApplication {
    public static void main(String[] args) {
        SpringApplication.run(StoreApplication.class, args);
    }
 
    @Bean
    public CommandLineRunner demoRunner(OrderEngineService orderService) {
        return args -> {
            System.out.println("\n==================================================");
            System.out.println("   MODULAR ORDER & DISCOUNT ENGINE SPRING BOOT    ");
            System.out.println("==================================================");
 
            // Simulasi Order 1 (Dapat Diskon)
            orderService.checkout("ORD-001", "Budi Santoso", 750_000.0);
 
            // Simulasi Order 2 (Tanpa Diskon)
            orderService.checkout("ORD-002", "Siti Nurhaliza", 200_000.0);
 
            System.out.println("==================================================\n");
        };
    }
}

Output Demonstrasi (Profile dev)

2026-08-29T19:30:00.120+07:00  INFO 12345 --- [main] c.b.s.StoreApplication: Starting StoreApplication using Java 21
 
==================================================
   MODULAR ORDER & DISCOUNT ENGINE SPRING BOOT    
==================================================
2026-08-29T19:30:00.450+07:00  INFO 12345 --- [main] c.b.s.OrderEngineService: Memproses checkout pesanan: ORD-001 untuk Budi Santoso
2026-08-29T19:30:00.452+07:00  INFO 12345 --- [main] c.b.s.OrderEngineService: Subtotal: Rp 750,000.00 | Diskon: Rp 75,000.00 | Total Akhir: Rp 675,000.00
2026-08-29T19:30:00.455+07:00  INFO 12345 --- [main] c.b.s.DevNotificationService: 🧪 [DEV NOTIFICATION] Kepada: Budi Santoso | Isi: Pesanan Anda #ORD-001 sebesar Rp 675,000.00 telah berhasil diproses!
 
2026-08-29T19:30:00.458+07:00  INFO 12345 --- [main] c.b.s.OrderEngineService: Memproses checkout pesanan: ORD-002 untuk Siti Nurhaliza
2026-08-29T19:30:00.459+07:00  INFO 12345 --- [main] c.b.s.OrderEngineService: Subtotal: Rp 200,000.00 | Diskon: Rp 0.00 | Total Akhir: Rp 200,000.00
2026-08-29T19:30:00.460+07:00  INFO 12345 --- [main] c.b.s.DevNotificationService: 🧪 [DEV NOTIFICATION] Kepada: Siti Nurhaliza | Isi: Pesanan Anda #ORD-002 sebesar Rp 200,000.00 telah berhasil diproses!
==================================================

24. 🔗 Referensi Resmi