Spring Framework klasik menyambungkan dependensi dengan indah, tetapi membangun aplikasi web utuh dengannya masih menuntut konfigurasi panjang: mendefinisikan DispatcherServlet di web.xml, mengatur view resolver, menyiapkan DataSource dan transaction manager satu per satu. Tahun 2014, Spring Boot hadir untuk memangkas pekerjaan itu dengan tiga gagasan: auto-configuration, starter, dan embedded server. Pada Chapter ini kita bedah ketiganya serta prinsip convention over configuration yang menjadikan aplikasi Spring semakin cepat dimulai.

Auto-Configuration Berdasarkan Kondisi #

Auto-configuration adalah kode konfigurasi pabrikan yang aktif hanya bila kondisinya terpenuhi. Anotasi @EnableAutoConfiguration (termasuk otomatis dalam @SpringBootApplication) memicu Spring Boot membaca ratusan kelas konfigurasi kandidat dari pustaka di classpath. Setiap kandidat dipasangi anotasi kondisional — @ConditionalOnClass memeriksa keberadaan kelas tertentu, @ConditionalOnMissingBean mundur bila kita sudah menyediakan bean sendiri, @ConditionalOnProperty memeriksa nilai properti. Hasil akhirnya: konfigurasi yang menyesuaikan diri dengan pustaka yang kita pakai.

Berikut potongan kelas auto-configuration nyata yang ditampilkan apa adanya untuk Spring Boot 3.4/3.5:

java
@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@ConditionalOnMissingBean(DataSource.class)
@EnableConfigurationProperties(DataSourceProperties.class)
public class DataSourceAutoConfiguration {

    @Bean
    @ConditionalOnProperty("spring.datasource.url")
    public DataSource dataSource(DataSourceProperties properties) {
        return properties.initializeDataSourceBuilder().build();
    }
}

Mekanisme mundurnya penting dicamkan: anotasi @ConditionalOnMissingBean berarti konfigurasi pabrikan selalu tunduk pada bean yang kita definisikan sendiri. Kita bisa menimpa hampir semua perilaku bawaan tanpa melawan framework — cukup mendeklarasikan bean dengan tipe yang sama.

Starter sebagai Satuan Kebutuhan #

Masalah klasik dependency management adalah dependency hell: satu fitur menuntut belasan pustaka dengan versi yang harus saling cocok. Starter menyelesaikannya dengan mengemas satu kebutuhan fungsional menjadi satu koordinat Maven — spring-boot-starter-web membawa Spring MVC, Jackson, dan Tomcat; spring-boot-starter-data-jpa membawa Hibernate dan Spring Data; spring-boot-starter-test membawa JUnit 5, AssertJ, dan Mockito. Versi seluruh pustaka di dalamnya sudah diuji kompatibel oleh tim Spring Boot, dan nomor versi starter cukup satu: versi Boot.

xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.0</version>
    <relativePath/>
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

Perhatikan bahwa dependency starter di atas tidak memuat elemen <version> — parent POM spring-boot-starter-parent mengelola versi lewat dependency management. Pola parent POM ini juga menyeting encoding UTF-8, plugin Java, dan konfigurasi filter resource secara baku.

Mensimulasikan Tabel Keputusan #

Dengan Java murni, kita bisa merekonstruksi inti pengambilan keputusan auto-configuration: tabel kandidat dengan syarat kelas dan properti, dievaluasi satu per satu:

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class TabelAutoconfig {

    // Satu kandidat auto-configuration dengan syarat @ConditionalOnClass
    // dan (opsional) syarat @ConditionalOnProperty.
    record Kandidat(String nama, String kelasWajib, String propertiWajib, String efek) { }

    private static final List<Kandidat> KANDIDAT = List.of(
            new Kandidat("DataSourceAutoConfiguration",
                    "javax.sql.DataSource", "spring.datasource.url",
                    "pool koneksi HikariCP dibuat"),
            new Kandidat("WebMvcAutoConfiguration",
                    "org.springframework.web.servlet.DispatcherServlet", null,
                    "infrastruktur MVC + Jackson terpasang"),
            new Kandidat("JpaRepositoriesAutoConfiguration",
                    "jakarta.persistence.EntityManager", null,
                    "infrastruktur Spring Data JPA aktif"),
            new Kandidat("RedisAutoConfiguration",
                    "org.springframework.data.redis.RedisConnection", null,
                    "cache Redis"),
            new Kandidat("TaskSchedulingAutoConfiguration",
                    "org.springframework.scheduling.TaskScheduler", "spring.task.scheduling.pool.size",
                    "executor @Scheduled dikonfigurasi"));

    public static void main(String[] args) {
        // Simulasi classpath hasil starter web + data-jpa + scheduling.
        Set<String> kelasDiClasspath = Set.of(
                "javax.sql.DataSource",
                "org.springframework.web.servlet.DispatcherServlet",
                "jakarta.persistence.EntityManager",
                "org.springframework.scheduling.TaskScheduler");

        // Simulasi application.properties yang ditulis pengembang.
        Map<String, String> properti = new LinkedHashMap<>();
        properti.put("spring.datasource.url", "jdbc:h2:mem:demo");
        properti.put("spring.task.scheduling.pool.size", "4");

        System.out.println("Kelas kunci di classpath : " + kelasDiClasspath.size());
        System.out.println("Properti pengembang      : " + properti.keySet());
        System.out.println();

        int aktif = 0;
        for (Kandidat k : KANDIDAT) {
            boolean kelasOk = kelasDiClasspath.contains(k.kelasWajib());
            boolean propOk = k.propertiWajib() == null
                    || properti.containsKey(k.propertiWajib());
            boolean cocok = kelasOk && propOk;
            if (cocok) {
                aktif++;
            }
            System.out.println((cocok ? "MATCH " : "LEWAT ") + k.nama());
            System.out.println("       syarat kelas    : " + k.kelasWajib()
                    + " -> " + (kelasOk ? "ada" : "tidak ada"));
            if (k.propertiWajib() != null) {
                System.out.println("       syarat properti : " + k.propertiWajib()
                        + " -> " + (propOk ? "terisi" : "kosong"));
            }
            System.out.println("       hasil           : "
                    + (cocok ? k.efek() : "konfigurasi dilewati"));
            System.out.println();
        }
        System.out.println("Total aktif: " + aktif + " dari " + KANDIDAT.size() + " kandidat");
    }
}

Output yang Dihasilkan:

text
Kelas kunci di classpath : 4
Properti pengembang      : [spring.datasource.url, spring.task.scheduling.pool.size]

MATCH DataSourceAutoConfiguration
       syarat kelas    : javax.sql.DataSource -> ada
       syarat properti : spring.datasource.url -> terisi
       hasil           : pool koneksi HikariCP dibuat

MATCH WebMvcAutoConfiguration
       syarat kelas    : org.springframework.web.servlet.DispatcherServlet -> ada
       hasil           : infrastruktur MVC + Jackson terpasang

MATCH JpaRepositoriesAutoConfiguration
       syarat kelas    : jakarta.persistence.EntityManager -> ada
       hasil           : infrastruktur Spring Data JPA aktif

LEWAT RedisAutoConfiguration
       syarat kelas    : org.springframework.data.redis.RedisConnection -> tidak ada
       hasil           : konfigurasi dilewati

MATCH TaskSchedulingAutoConfiguration
       syarat kelas    : org.springframework.scheduling.TaskScheduler -> ada
       syarat properti : spring.task.scheduling.pool.size -> terisi
       hasil           : executor @Scheduled dikonfigurasi

Total aktif: 4 dari 5 kandidat

Mari kita bedah simulasi tersebut:

  1. Set<String> kelasDiClasspath: himpunan ini memainkan peran classpath nyata hasil starter web + data-jpa + scheduling. Kandidat Redis tidak menemukan kelas kuncinya, sehingga barisnya berakhir LEWAT — persis perilaku @ConditionalOnClass yang memeriksa RedisConnection.
  2. Map<String, String> properti: kandidat DataSourceAutoConfiguration dan TaskSchedulingAutoConfiguration mencantumkan syarat properti; kandidat yang propertinya kosong dianggap tidak memenuhi syarat, menirui @ConditionalOnProperty.
  3. Loop evaluasi: setiap kandidat menghasilkan baris MATCH atau LEWAT beserta alasan dan efeknya. Angka akhir Total aktif: 4 dari 5 kandidat menunjukkan bahwa auto-configuration bukan mengaktifkan semuanya, melainkan memilih berdasarkan bukti.

Embedded Server dan Fat Jar #

Konvensi ketiga Spring Boot: aplikasi membawa server sendiri. spring-boot-starter-web menyertakan Tomcat tertanam yang berjalan di dalam JVM aplikasi — tidak ada lagi instalasi application server terpisah. Proses build menghasilkan fat jar: satu berkas JAR berisi kelas kita, seluruh pustaka dependensi, dan server tertanam, yang dijalankan cukup dengan java -jar aplikasi.jar. Penyiapan deployment menjadi urusan menyalin satu berkas, cocok untuk container image dan platform cloud modern.

Konvensi bawaannya pun menegaskan filosofi convention over configuration: kelas aplikasi di src/main/java, konfigurasi di src/main/resources/application.properties, port 8080, dan context path /. Semua bisa diubah lewat property — tetapi kita baru perlu mengubahnya saat kebutuhan sungguhan muncul.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

TabelAutoconfig.java

Unduh berkas
java
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class TabelAutoconfig {

    // Satu kandidat auto-configuration dengan syarat @ConditionalOnClass
    // dan (opsional) syarat @ConditionalOnProperty.
    record Kandidat(String nama, String kelasWajib, String propertiWajib, String efek) { }

    private static final List<Kandidat> KANDIDAT = List.of(
            new Kandidat("DataSourceAutoConfiguration",
                    "javax.sql.DataSource", "spring.datasource.url",
                    "pool koneksi HikariCP dibuat"),
            new Kandidat("WebMvcAutoConfiguration",
                    "org.springframework.web.servlet.DispatcherServlet", null,
                    "infrastruktur MVC + Jackson terpasang"),
            new Kandidat("JpaRepositoriesAutoConfiguration",
                    "jakarta.persistence.EntityManager", null,
                    "infrastruktur Spring Data JPA aktif"),
            new Kandidat("RedisAutoConfiguration",
                    "org.springframework.data.redis.RedisConnection", null,
                    "cache Redis"),
            new Kandidat("TaskSchedulingAutoConfiguration",
                    "org.springframework.scheduling.TaskScheduler", "spring.task.scheduling.pool.size",
                    "executor @Scheduled dikonfigurasi"));

    public static void main(String[] args) {
        // Simulasi classpath hasil starter web + data-jpa + scheduling.
        Set<String> kelasDiClasspath = Set.of(
                "javax.sql.DataSource",
                "org.springframework.web.servlet.DispatcherServlet",
                "jakarta.persistence.EntityManager",
                "org.springframework.scheduling.TaskScheduler");

        // Simulasi application.properties yang ditulis pengembang.
        Map<String, String> properti = new LinkedHashMap<>();
        properti.put("spring.datasource.url", "jdbc:h2:mem:demo");
        properti.put("spring.task.scheduling.pool.size", "4");

        System.out.println("Kelas kunci di classpath : " + kelasDiClasspath.size());
        System.out.println("Properti pengembang      : " + properti.keySet());
        System.out.println();

        int aktif = 0;
        for (Kandidat k : KANDIDAT) {
            boolean kelasOk = kelasDiClasspath.contains(k.kelasWajib());
            boolean propOk = k.propertiWajib() == null
                    || properti.containsKey(k.propertiWajib());
            boolean cocok = kelasOk && propOk;
            if (cocok) {
                aktif++;
            }
            System.out.println((cocok ? "MATCH " : "LEWAT ") + k.nama());
            System.out.println("       syarat kelas    : " + k.kelasWajib()
                    + " -> " + (kelasOk ? "ada" : "tidak ada"));
            if (k.propertiWajib() != null) {
                System.out.println("       syarat properti : " + k.propertiWajib()
                        + " -> " + (propOk ? "terisi" : "kosong"));
            }
            System.out.println("       hasil           : "
                    + (cocok ? k.efek() : "konfigurasi dilewati"));
            System.out.println();
        }
        System.out.println("Total aktif: " + aktif + " dari " + KANDIDAT.size() + " kandidat");
    }
}