Setiap percakapan jaringan dimulai dari satu pertanyaan sederhana: ke mana kita hendak terhubung? Jawabannya berupa alamat seperti https://api.example.com/v1/produk?halaman=2, dan string seperti itu punya struktur yang ketat. Kesalahan kecil — spasi yang tidak dienkode, query yang disambung manual, host yang tidak divalidasi — bisa membuat permintaan gagal atau bahkan membuka celah keamanan. Class URI pada package java.net adalah alat resmi Java untuk membedah, memvalidasi, dan menyusun alamat tersebut. Chapter ini menguraikan anatomi URI, membedakan URI dengan URL, lalu melatih kita menyusun query string yang aman dengan URLEncoder.

Anatomi URI #

URI (Uniform Resource Identifier) adalah identifier generik untuk sumber daya. Formatnya didefinisikan RFC 3986 dan bentuk umumnya dapat kita petakan sebagai berikut:

text
scheme://authority/path?query#fragment
   |        |         |     |       |
 https  api.example.com  /v1/produk ?halaman=2  #ulasan

Komponen authority sendiri dapat memuat userinfo, host, dan port ([email protected]:8443). Class URI menyediakan satu method get untuk tiap komponen, sehingga kita tidak perlu memilah string secara manual dengan split — parsing yang dijamin sesuai spesifikasi sudah tersedia. Mari kita bedah satu URI lengkap:

java BedahUri.java
import java.net.URI;

public class BedahUri {
    public static void main(String[] args) throws Exception {
        URI uri = URI.create("https://api.example.com:8443/v1/produk?halaman=2&urut=harga#ulasan");

        System.out.println("scheme   : " + uri.getScheme());
        System.out.println("host     : " + uri.getHost());
        System.out.println("port     : " + uri.getPort());
        System.out.println("path     : " + uri.getPath());
        System.out.println("query    : " + uri.getQuery());
        System.out.println("fragment : " + uri.getFragment());

        // Merujuk alamat relatif terhadap URI dasar
        URI dasar = URI.create("https://api.example.com/v1/");
        URI gabung = dasar.resolve("produk/42");
        System.out.println("resolve  : " + gabung);

        // Membangun URI dari komponen (komponen dienkode otomatis)
        URI dibangun = new URI("https", null, "api.example.com", -1,
                "/v1/produk", "halaman=3", null);
        System.out.println("dibangun : " + dibangun);
    }
}

Output yang Dihasilkan:

text
scheme   : https
host     : api.example.com
port     : 8443
path     : /v1/produk
query    : halaman=2&urut=harga
fragment : ulasan
resolve  : https://api.example.com/v1/produk/42
dibangun : https://api.example.com/v1/produk?halaman=3

Mari kita telaah baris-baris kode di atas:

  1. Baris 5 (URI.create(...)): membangun objek URI lewat parsing string sekali jalan. Method ini melempar IllegalArgumentException jika string tidak valid, sehingga cocok untuk konstanta yang sudah kita yakini benar.
  2. Baris 7-12 (uri.getScheme() dst.): membongkar URI menjadi komponennya. Perhatikan bahwa getPort() mengembalikan int; jika port tidak tertulis pada URI, nilai kembaliannya -1, bukan 0.
  3. Baris 15-16 (dasar.resolve("produk/42")): menggabungkan URI dasar dengan referensi relatif, persis seperti cara browser menyelesaikan tautan relatif pada halaman web. Karena path dasar berakhir dengan /, hasilnya menjadi /v1/produk/42.
  4. Baris 19-20 (new URI(scheme, userInfo, host, port, path, query, fragment)): membangun URI dari komponen terpisah. Constructor tujuh argumen ini mengenkode tiap komponen sesuai perannya, jadi karakter khusus pada path atau query ditangani dengan benar. Nilai port -1 berarti “tanpa port eksplisit”.

URI, URL, dan URN #

Tiga istilah ini sering dipakai bergantian padahal berbeda cakupan. URL (Uniform Resource Locator) adalah URI yang menunjuk lokasi dan cara mengambilnya — skema http, https, atau ftp memberi tahu mekanisme aksesnya. URN (Uniform Resource Name) adalah URI yang menamai sumber daya tanpa lokasi, misalnya urn:isbn:978-0-13-468599-1 untuk sebuah buku. Ringkasnya: setiap URL adalah URI, tetapi tidak setiap URI adalah URL.

Class java.net.URL lebih tua dari URI dan membawa perilaku yang membuatnya kurang cocok untuk parsing murni. Method URL.equals() membandingkan alamat IP hasil resolusi DNS — dua URL dengan host berbeda bisa dianggap sama jika DNS menunjuk ke mesin yang sama, dan pemanggilan equals() dapat memicu pencarian jaringan. Karena itu praktik yang dianjurkan pada kode modern: gunakan URI untuk parsing, validasi, dan manipulasi alamat; konversi ke URL hanya bila API lawas menuntutnya lewat uri.toURL().

Menyusun Query String dengan URLEncoder #

Query string adalah wilayah rawan: nilai dari pengguna bisa berisi spasi, &, =, atau karakter non-ASCII yang harus dienkode agar tidak merusak struktur. Class URLEncoder melakukan enkode format application/x-www-form-urlencoded: spasi menjadi +, karakter khusus menjadi %XX, dan huruf serta angka dibiarkan. Charset wajib ditentukan secara eksplisit — pada contoh ini StandardCharsets.UTF_8:

java EnkodeUrl.java
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

public class EnkodeUrl {
    static String bangunKueri(Map<String, String> parameter) {
        StringBuilder hasil = new StringBuilder();
        for (Map.Entry<String, String> e : parameter.entrySet()) {
            if (hasil.length() > 0) {
                hasil.append('&');
            }
            hasil.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8));
            hasil.append('=');
            hasil.append(URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
        }
        return hasil.toString();
    }

    public static void main(String[] args) {
        Map<String, String> parameter = new LinkedHashMap<>();
        parameter.put("nama", "Budi Santoso");
        parameter.put("kota", "Bandung");
        parameter.put("saringan", "harga < 100000");

        System.out.println("kueri    : " + bangunKueri(parameter));
        System.out.println("nama     : " + URLDecoder.decode("Budi+Santoso", StandardCharsets.UTF_8));
        System.out.println("saringan : " + URLDecoder.decode("harga+%3C+100000", StandardCharsets.UTF_8));
    }
}

Output yang Dihasilkan:

text
kueri    : nama=Budi+Santoso&kota=Bandung&saringan=harga+%3C+100000
nama     : Budi Santoso
saringan : harga < 100000

Mari kita telaah bagian-bagian pentingnya:

  1. Baris 8-18 (bangunKueri): method pembantu yang mengubah peta parameter menjadi query string siap pakai. Setiap kunci dan nilai melewati URLEncoder.encode secara terpisah — tanda & dan = pemisah hanya disisipkan oleh method ini, bukan berasal dari data pengguna.
  2. Baris 22 (new LinkedHashMap<>()): LinkedHashMap menjaga urutan sisipan, sehingga query yang dihasilkan sama setiap kali program dijalankan. HashMap biasa pun deterministik di Java, tetapi urutannya mengikuti hash internal sehingga lebih sulit dibaca.
  3. Baris 27 ("saringan", "harga < 100000"): nilai berisi spasi dan < sengaja dipilih untuk membuktikan enkode bekerja — keduanya tidak boleh masuk query string mentah.
  4. Baris 31-32 (URLDecoder.decode(...)): mengembalikan bentuk asli nilai. Decoder memahami baik + untuk spasi maupun bentuk %XX, dan wajib diberi charset yang sama dengan saat enkode.

Dengan kemampuan membedah dan menyusun alamat, fondasi teoretis kita sudah lengkap. Selanjutnya kita turun ke mekanisme paling mendasar dalam komunikasi Java: socket — titik akhir ip:port tempat data benar-benar mengalir.

Referensi #

Berkas Sumber

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

BedahUri.java

Unduh berkas
java
import java.net.URI;

public class BedahUri {
    public static void main(String[] args) throws Exception {
        URI uri = URI.create("https://api.example.com:8443/v1/produk?halaman=2&urut=harga#ulasan");

        System.out.println("scheme   : " + uri.getScheme());
        System.out.println("host     : " + uri.getHost());
        System.out.println("port     : " + uri.getPort());
        System.out.println("path     : " + uri.getPath());
        System.out.println("query    : " + uri.getQuery());
        System.out.println("fragment : " + uri.getFragment());

        // Merujuk alamat relatif terhadap URI dasar
        URI dasar = URI.create("https://api.example.com/v1/");
        URI gabung = dasar.resolve("produk/42");
        System.out.println("resolve  : " + gabung);

        // Membangun URI dari komponen (komponen dienkode otomatis)
        URI dibangun = new URI("https", null, "api.example.com", -1,
                "/v1/produk", "halaman=3", null);
        System.out.println("dibangun : " + dibangun);
    }
}

EnkodeUrl.java

Unduh berkas
java
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Map;

public class EnkodeUrl {
    static String bangunKueri(Map<String, String> parameter) {
        StringBuilder hasil = new StringBuilder();
        for (Map.Entry<String, String> e : parameter.entrySet()) {
            if (hasil.length() > 0) {
                hasil.append('&');
            }
            hasil.append(URLEncoder.encode(e.getKey(), StandardCharsets.UTF_8));
            hasil.append('=');
            hasil.append(URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8));
        }
        return hasil.toString();
    }

    public static void main(String[] args) {
        Map<String, String> parameter = new LinkedHashMap<>();
        parameter.put("nama", "Budi Santoso");
        parameter.put("kota", "Bandung");
        parameter.put("saringan", "harga < 100000");

        System.out.println("kueri    : " + bangunKueri(parameter));
        System.out.println("nama     : " + URLDecoder.decode("Budi+Santoso", StandardCharsets.UTF_8));
        System.out.println("saringan : " + URLDecoder.decode("harga+%3C+100000", StandardCharsets.UTF_8));
    }
}