Membalas permintaan dengan JSON berarti menyusun teks pendek; membalas dengan halaman HTML berarti menyusun dokumen panjang yang berubah-ubah pada satu dua bagiannya saja. Menulis dokumen itu dengan rentetan println membuat kode Java tidak terbaca dan tata letak tidak bisa disunting tanpa kompilasi ulang. Template engine menyelesaikan masalah ini dengan memisahkan dua peran: berkas template menyimpan tata letak dengan lubang placeholder, sedangkan kode Java hanya menyiapkan data untuk mengisi lubang tersebut. Chapter ini membahas cara kerja template engine — dari JSP klasik sampai Thymeleaf modern — lalu membangun mesin mini yang benar-benar merender HTML, lengkap dengan perlindungan escaping.

Mesin Template di Ekosistem Java #

Tiga keluarga template engine lazim dipakai di sisi server Java:

EngineCiri khas
Jakarta Pages (JSP)veteran; template dikompilasi menjadi servlet oleh container
Thymeleaftemplate HTML valid yang bisa dibuka langsung di peramban (natural templating)
FreeMarkertemplate serbaguna untuk HTML, surel, dan berkas teks lain

Ketiganya memakai pola yang sama: berkas template berisi teks statis bercampur ekspresi, mesin menggabungkan template dengan model data, hasilnya menjadi body balasan. Pada Thymeleaf, ekspresi ditulis sebagai atribut HTML sehingga perancang halaman tetap dapat membuka berkas template di peramban tanpa server:

html produk.html dengan Thymeleaf
<html xmlns:th="http://www.thymeleaf.org">
  <h1>Selamat datang, <span th:text="${nama}">Budi</span>!</h1>
  <ul>
    <li th:each="b : ${barang}" th:text="${b}">contoh barang</li>
  </ul>
</html>

Atribut th:text menggantikan isi elemen saat rendering — teks “Budi” dan “contoh barang” hanya penampil statis untuk mode desain. Kontroler Spring MVC yang memakai template ini cukup mengembalikan nama template beserta model:

java Kontroler Spring MVC memanggil template
@GetMapping("/produk")
public String halamanProduk(Model model) {
    model.addAttribute("nama", "Budi Santoso");
    model.addAttribute("barang", List.of("Buku Java", "Pena"));
    return "produk";   // memetakan ke templat produk.html
}

Mesin Template dalam Prinsip #

Apa yang dilakukan Thymeleaf di balik layar sebenarnya bisa kita tulis sendiri dalam ukuran kecil: cari placeholder dengan regular expression, ambil nilai dari peta konteks, terjemahkan nilai ke bentuk aman untuk HTML, lalu sisipkan. MesinTemplateMini melakukan tepat itu:

import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MesinTemplateMini {

    static String escapeHtml(String teks) {
        return teks.replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&#39;");
    }

    static String render(String template, Map<String, String> konteks) {
        Pattern pola = Pattern.compile("\\$\\{(\\w+)}");
        Matcher cocok = pola.matcher(template);
        StringBuilder hasil = new StringBuilder();
        while (cocok.find()) {
            String nilai = konteks.getOrDefault(cocok.group(1), "");
            cocok.appendReplacement(hasil, Matcher.quoteReplacement(escapeHtml(nilai)));
        }
        cocok.appendTail(hasil);
        return hasil.toString();
    }

    public static void main(String[] args) {
        String template = """
                <html>
                  <h1>Selamat datang, ${nama}!</h1>
                  <p>Keranjang Anda berisi ${jumlah} barang.</p>
                </html>
                """;

        Map<String, String> konteksNormal = new LinkedHashMap<>();
        konteksNormal.put("nama", "Budi Santoso");
        konteksNormal.put("jumlah", "3");
        System.out.println("== Render dengan data normal ==");
        System.out.print(render(template, konteksNormal));

        Map<String, String> konteksCurang = new LinkedHashMap<>();
        konteksCurang.put("nama", "<script>alert('xss')</script>");
        konteksCurang.put("jumlah", "0");
        System.out.println("== Render dengan masukan berbahaya ==");
        System.out.print(render(template, konteksCurang));

        String barisTemplate = "  <li>${barang}</li>\n";
        List<String> barang = List.of("Buku Java", "Pena", "Tas<kecil>");
        System.out.println("== Render daftar barang ==");
        StringBuilder daftar = new StringBuilder("<ul>\n");
        for (String b : barang) {
            Map<String, String> konteksBaris = new LinkedHashMap<>();
            konteksBaris.put("barang", b);
            daftar.append(render(barisTemplate, konteksBaris));
        }
        daftar.append("</ul>");
        System.out.print(daftar);
    }
}

Output yang Dihasilkan:

text
== Render dengan data normal ==
<html>
  <h1>Selamat datang, Budi Santoso!</h1>
  <p>Keranjang Anda berisi 3 barang.</p>
</html>
== Render dengan masukan berbahaya ==
<html>
  <h1>Selamat datang, &lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;!</h1>
  <p>Keranjang Anda berisi 0 barang.</p>
</html>
== Render daftar barang ==
<ul>
  <li>Buku Java</li>
  <li>Pena</li>
  <li>Tas&lt;kecil&gt;</li>
</ul>

Bedah baris-per-baris mesin kecil ini:

  1. Baris 9-15 (escapeHtml): urutan penggantian penting — karakter & harus diganti paling awal, sebelum penggantian lain menghasilkan &amp; yang akan ter-escape dua kali. Fungsi ini adalah pembeda antara mesin template yang aman dan yang berbahaya.
  2. Baris 18 (Pattern.compile("\\$\\{(\\w+)}")): pola menangkap ${nama} sekaligus mengambil isi tanda kurung sebagai capture group — nama variabel yang dicari di konteks.
  3. Baris 21-24 (loop find + appendReplacement): mekanisme standar penggantian regex di Java. Nilai dari konteks melewati escapeHtml dulu, baru disisipkan — pada Thymeleaf, perilaku ini adalah bawaan th:text; atribut th:utext yang mem-bypass escaping adalah pintu kebalikan yang harus dipakai sangat hemat.
  4. Baris 23 (Matcher.quoteReplacement): hasil escape bisa berisi karakter \ atau $ yang bermakna khusus bagi appendReplacement; quoteReplacement menetralkan keduanya.
  5. Baris 43-47 (konteksCurang): masukan <script>alert('xss')</script> diproses sebagai data, bukan markup. Output membuktikan seluruh tanda kurung sudut dan kutip berubah menjadi entitas HTML — script tampil sebagai teks biasa di layar pengguna, tidak dieksekusi peramban.
  6. Baris 49-57 (render per baris): perulangan <li> disimulasikan dengan memanggil render berulang kali pada template baris. Engine sungguhan menyediakan sintaks bawaan (th:each, <c:forEach>) agar logika perulangan tetap di template, bukan di kode Java.

Dari Halaman ke Platform #

Template menuntaskan rantai rendering sisi server: permintaan tiba, session mengenali pengguna, data disiapkan, template menghasilkan HTML. Sejauh ini kita membangun semuanya di atas servlet secara manual — pendekatan yang bagus untuk belajar namun membosankan untuk produksi. Ekosistem Java menaikkan seluruh lapisan ini menjadi platform standar bernama Jakarta EE: kumpulan spesifikasi yang menyediakan servlet, template, dependency injection, dan API REST dalam satu kesatuan. Chapter berikutnya memetakan platform tersebut.

Referensi #

Berkas Sumber

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

MesinTemplateMini.java

Unduh berkas
java
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class MesinTemplateMini {

    static String escapeHtml(String teks) {
        return teks.replace("&", "&amp;")
                .replace("<", "&lt;")
                .replace(">", "&gt;")
                .replace("\"", "&quot;")
                .replace("'", "&#39;");
    }

    static String render(String template, Map<String, String> konteks) {
        Pattern pola = Pattern.compile("\\$\\{(\\w+)}");
        Matcher cocok = pola.matcher(template);
        StringBuilder hasil = new StringBuilder();
        while (cocok.find()) {
            String nilai = konteks.getOrDefault(cocok.group(1), "");
            cocok.appendReplacement(hasil, Matcher.quoteReplacement(escapeHtml(nilai)));
        }
        cocok.appendTail(hasil);
        return hasil.toString();
    }

    public static void main(String[] args) {
        String template = """
                <html>
                  <h1>Selamat datang, ${nama}!</h1>
                  <p>Keranjang Anda berisi ${jumlah} barang.</p>
                </html>
                """;

        Map<String, String> konteksNormal = new LinkedHashMap<>();
        konteksNormal.put("nama", "Budi Santoso");
        konteksNormal.put("jumlah", "3");
        System.out.println("== Render dengan data normal ==");
        System.out.print(render(template, konteksNormal));

        Map<String, String> konteksCurang = new LinkedHashMap<>();
        konteksCurang.put("nama", "<script>alert('xss')</script>");
        konteksCurang.put("jumlah", "0");
        System.out.println("== Render dengan masukan berbahaya ==");
        System.out.print(render(template, konteksCurang));

        String barisTemplate = "  <li>${barang}</li>\n";
        List<String> barang = List.of("Buku Java", "Pena", "Tas<kecil>");
        System.out.println("== Render daftar barang ==");
        StringBuilder daftar = new StringBuilder("<ul>\n");
        for (String b : barang) {
            Map<String, String> konteksBaris = new LinkedHashMap<>();
            konteksBaris.put("barang", b);
            daftar.append(render(barisTemplate, konteksBaris));
        }
        daftar.append("</ul>");
        System.out.print(daftar);
    }
}