Chapter sebelumnya memperlihatkan DriverManager sebagai perute yang memilih driver berdasarkan URL. Pertanyaan praktisnya segera muncul: dari mana driver itu datang, dan bagaimana ia sampai terdaftar di dalam daftar DriverManager? Driver JDBC adalah pustaka .jar buatan vendor — bukan bagian dari JDK — sehingga kita harus menempatkannya di classpath secara eksplisit. Chapter ini membahas tiga hal yang menyertai kehidupan seorang driver: pendaftaran lewat mekanisme modern Service Provider Interface, pengelolaan sebagai dependensi proyek, dan format alamatnya yang universal: URL JDBC.

Driver sebagai Dependensi Eksternal #

JDK hanya membawa interface JDBC (java.sql), sedangkan implementasinya — kode yang berbicara protokol PostgreSQL atau membaca berkas SQLite — dikirim vendor sebagai berkas .jar. Untuk proyek yang memakai build tool, jar driver dideklarasikan sebagai dependensi biasa:

xml Dependensi sqlite-jdbc pada pom.xml (Maven)
<dependency>
    <groupId>org.xerial</groupId>
    <artifactId>sqlite-jdbc</artifactId>
    <version>3.46.1.0</version>
</dependency>
kotlin Dependensi driver PostgreSQL pada build.gradle.kts
dependencies {
    implementation("org.postgresql:postgresql:42.7.4")
}

Tanpa build tool, jar bisa dilampirkan langsung saat menjalankan program: java -cp .:sqlite-jdbc.jar Aplikasi (di Windows, pemisahnya ;). Kedua cuplikan di atas ditampilkan sebagai konfigurasi karena lingkungan uji Course ini tidak menjamin akses ke Maven Central, jadi kita tidak mengklaim pernah mengeksekusinya. Jika jar driver tidak ada di classpath, akibatnya persis seperti yang kita lihat pada Chapter Arsitektur JDBC: SQLException No suitable driver found.

Cara Driver Mendaftar #

Ada dua zaman pendaftaran driver. Cara lawas, sebelum JDBC 4.0 (2006), tiap aplikasi wajib memuat class driver secara manual sebelum getConnection:

java Cara lawas: Class.forName (JDBC 3 era)
// Blok statis di dalam class driver yang memanggil DriverManager.registerDriver
Class.forName("org.sqlite.JDBC");
Connection conn = DriverManager.getConnection("jdbc:sqlite:toko.db");

Baris Class.forName memaksa JVM memuat class org.sqlite.JDBC; efek sampingnya, static initializer pada class driver menjalankan DriverManager.registerDriver(new ...). Cara modern memakai Service Provider Interface (SPI): vendor cukup menyertakan berkas teks META-INF/services/java.sql.Driver di dalam jar yang berisi nama class driver, misalnya satu baris org.sqlite.JDBC. Ketika class DriverManager pertama kali dipakai, DriverManager memanggil ServiceLoader.load(Driver.class) yang memindai seluruh jar di classpath, membaca berkas tersebut, membuat instance tiap driver, dan mendaftarkannya otomatis — tanpa satu baris Class.forName di kode aplikasi.

text Isi berkas META-INF/services/java.sql.Driver di dalam jar driver
org.sqlite.JDBC

Anatomi URL JDBC #

Format umum URL JDBC adalah jdbc:<subprotokol>:<detail>. Bagian jdbc: menandai protokol; subprotokol (misalnya sqlite, postgresql) dipakai DriverManager untuk mencocokkan driver; detail sepenuhnya milik vendor — bisa berupa path berkas, host dan port, atau parameter koneksi. Format yang paling sering kita jumpai:

SubprotokolContoh URLArti detail
sqlitejdbc:sqlite:/data/toko.dbpath berkas database
postgresqljdbc:postgresql://host:5432/toko?sslmode=requirehost, port, database, parameter
mysqljdbc:mysql://host:3306/toko?useSSL=falsehost, port, database, parameter
oraclejdbc:oracle:thin:@host:1521:tokovarian tipis + nama layanan
sqlserverjdbc:sqlserver://host;databaseName=tokohost dan opsi berpemisah titik koma

Port yang tidak ditulis akan memakai port default vendor — 5432 untuk PostgreSQL, 3306 untuk MySQL, 1521 untuk Oracle. Parameter setelah tanda ? mengatur perilaku koneksi (SSL, timeout, zona waktu) dan dipisah tanda &.

Membedah URL dengan Regex #

Memahami format URL membuat kita bisa menulis pemeriksa awal di aplikasi — misalnya memvalidasi URL dari berkas konfigurasi sebelum menghubungi database. Program berikut membedah beberapa URL JDBC memakai substring dan regular expression, lalu memetakan subprotokol ke driver yang menyediakannya:

java UrlJdbc.java
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class UrlJdbc {

    public static void main(String[] args) {
        String[] daftarUrl = {
                "jdbc:sqlite:/data/toko.db",
                "jdbc:postgresql://localhost:5432/toko?sslmode=require",
                "jdbc:mysql://127.0.0.1:3306/toko?useSSL=false&serverTimezone=UTC",
                "jdbc:demo:toko",
                "http://localhost:5432/toko"
        };
        for (String url : daftarUrl) {
            bedah(url);
            System.out.println();
        }
    }

    static void bedah(String url) {
        System.out.println("URL         : " + url);
        if (!url.startsWith("jdbc:")) {
            System.out.println("status      : DITOLAK, skema wajib diawali \"jdbc:\"");
            return;
        }
        int titikDua = url.indexOf(':', 5);
        if (titikDua < 0) {
            System.out.println("status      : DITOLAK, subprotokol tidak ditemukan");
            return;
        }
        String subprotokol = url.substring(5, titikDua);
        String sisa = url.substring(titikDua + 1);
        System.out.println("subprotokol : " + subprotokol);

        if (sisa.startsWith("//")) {
            // bentuk jaringan: //host:port/database?param=nilai&...
            Matcher m = Pattern
                    .compile("^//([^/:?]+)(?::(\\d+))?/([^?]+)(?:\\?(.*))?$")
                    .matcher(sisa);
            if (m.matches()) {
                System.out.println("host        : " + m.group(1));
                System.out.println("port        : "
                        + (m.group(2) == null ? "default" : m.group(2)));
                System.out.println("database    : " + m.group(3));
                if (m.group(4) != null && !m.group(4).isBlank()) {
                    System.out.println("parameter   : " + m.group(4));
                }
            } else {
                System.out.println("status      : format //host:port/database tidak cocok");
                return;
            }
        } else {
            // bentuk berkas (SQLite): path relatif atau absolut
            System.out.println("berkas      : " + sisa);
        }
        System.out.println("driver      : " + namaDriver(subprotokol));
    }

    static String namaDriver(String subprotokol) {
        return switch (subprotokol) {
            case "sqlite" -> "org.sqlite.JDBC (jar: org.xerial:sqlite-jdbc)";
            case "postgresql" -> "org.postgresql.Driver (jar: org.postgresql:postgresql)";
            case "mysql" -> "com.mysql.cj.jdbc.Driver (jar: com.mysql:mysql-connector-j)";
            default -> "TIDAK DIKENAL: tidak ada driver untuk subprotokol ini";
        };
    }
}

Output yang Dihasilkan:

text
URL         : jdbc:sqlite:/data/toko.db
subprotokol : sqlite
berkas      : /data/toko.db
driver      : org.sqlite.JDBC (jar: org.xerial:sqlite-jdbc)

URL         : jdbc:postgresql://localhost:5432/toko?sslmode=require
subprotokol : postgresql
host        : localhost
port        : 5432
database    : toko
parameter   : sslmode=require
driver      : org.postgresql.Driver (jar: org.postgresql:postgresql)

URL         : jdbc:mysql://127.0.0.1:3306/toko?useSSL=false&serverTimezone=UTC
subprotokol : mysql
host        : 127.0.0.1
port        : 3306
database    : toko
parameter   : useSSL=false&serverTimezone=UTC
driver      : com.mysql.cj.jdbc.Driver (jar: com.mysql:mysql-connector-j)

URL         : jdbc:demo:toko
subprotokol : demo
berkas      : toko
driver      : TIDAK DIKENAL: tidak ada driver untuk subprotokol ini

URL         : http://localhost:5432/toko
status      : DITOLAK, skema wajib diawali "jdbc:"

Mari kita telaah inti program di atas:

  1. url.indexOf(':', 5): pencarian titik dua dimulai dari indeks 5 — tepat setelah teks jdbc: — sehingga titik dua pertama yang ditemukan adalah pemisah subprotokol. Hasil -1 berarti URL seperti jdbc:postgres tanpa detail, dan program menolaknya.
  2. sisa.startsWith("//"): cabang ini membedakan dua keluarga URL. Bentuk //... berarti koneksi jaringan (PostgreSQL, MySQL); bentuk lain berarti alamat lokal seperti berkas SQLite.
  3. Regex ^//([^/:?]+)(?::(\\d+))?/([^?]+)(?:\\?(.*))?$: kelompok pertama menangkap host (karakter apa pun kecuali /, :, ?); kelompok kedua opsional menangkap port digital; kelompok ketiga menangkap nama database hingga ?; kelompok keempat opsional menangkap seluruh parameter.
  4. m.group(2) == null ? "default": tanda tanya pada regex membuat kelompok port opsional — URL tanpa port tetap cocok, dan kita melaporkannya sebagai default.
  5. switch expression namaDriver: pemetaan subprotokol ke class driver meniru keputusan DriverManager — hanya driver yang mengaku menerima subprotokol tersebut yang akan dipakai; subprotokol demo memang tidak dikenal karena driver shim kita tidak terpasang di sini.

Kita sekarang tahu bagaimana driver hadir di classpath, mendaftar diri lewat SPI, dan ditemukan lewat URL. Satu langkah tersisa sebelum SQL bisa berjalan: membuka Connection — beserta cara menangani kegagalannya yang sehat.

Referensi #

Berkas Sumber

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

UrlJdbc.java

Unduh berkas
java
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class UrlJdbc {

    public static void main(String[] args) {
        String[] daftarUrl = {
                "jdbc:sqlite:/data/toko.db",
                "jdbc:postgresql://localhost:5432/toko?sslmode=require",
                "jdbc:mysql://127.0.0.1:3306/toko?useSSL=false&serverTimezone=UTC",
                "jdbc:demo:toko",
                "http://localhost:5432/toko"
        };
        for (String url : daftarUrl) {
            bedah(url);
            System.out.println();
        }
    }

    static void bedah(String url) {
        System.out.println("URL         : " + url);
        if (!url.startsWith("jdbc:")) {
            System.out.println("status      : DITOLAK, skema wajib diawali \"jdbc:\"");
            return;
        }
        int titikDua = url.indexOf(':', 5);
        if (titikDua < 0) {
            System.out.println("status      : DITOLAK, subprotokol tidak ditemukan");
            return;
        }
        String subprotokol = url.substring(5, titikDua);
        String sisa = url.substring(titikDua + 1);
        System.out.println("subprotokol : " + subprotokol);

        if (sisa.startsWith("//")) {
            // bentuk jaringan: //host:port/database?param=nilai&...
            Matcher m = Pattern
                    .compile("^//([^/:?]+)(?::(\\d+))?/([^?]+)(?:\\?(.*))?$")
                    .matcher(sisa);
            if (m.matches()) {
                System.out.println("host        : " + m.group(1));
                System.out.println("port        : "
                        + (m.group(2) == null ? "default" : m.group(2)));
                System.out.println("database    : " + m.group(3));
                if (m.group(4) != null && !m.group(4).isBlank()) {
                    System.out.println("parameter   : " + m.group(4));
                }
            } else {
                System.out.println("status      : format //host:port/database tidak cocok");
                return;
            }
        } else {
            // bentuk berkas (SQLite): path relatif atau absolut
            System.out.println("berkas      : " + sisa);
        }
        System.out.println("driver      : " + namaDriver(subprotokol));
    }

    static String namaDriver(String subprotokol) {
        return switch (subprotokol) {
            case "sqlite" -> "org.sqlite.JDBC (jar: org.xerial:sqlite-jdbc)";
            case "postgresql" -> "org.postgresql.Driver (jar: org.postgresql:postgresql)";
            case "mysql" -> "com.mysql.cj.jdbc.Driver (jar: com.mysql:mysql-connector-j)";
            default -> "TIDAK DIKENAL: tidak ada driver untuk subprotokol ini";
        };
    }
}