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:
| Engine | Ciri khas |
|---|---|
| Jakarta Pages (JSP) | veteran; template dikompilasi menjadi servlet oleh container |
| Thymeleaf | template HTML valid yang bisa dibuka langsung di peramban (natural templating) |
| FreeMarker | template 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 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:
@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("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
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:
== 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, <script>alert('xss')</script>!</h1>
<p>Keranjang Anda berisi 0 barang.</p>
</html>
== Render daftar barang ==
<ul>
<li>Buku Java</li>
<li>Pena</li>
<li>Tas<kecil></li>
</ul>Bedah baris-per-baris mesin kecil ini:
- Baris 9-15 (
escapeHtml): urutan penggantian penting — karakter&harus diganti paling awal, sebelum penggantian lain menghasilkan&yang akan ter-escape dua kali. Fungsi ini adalah pembeda antara mesin template yang aman dan yang berbahaya. - Baris 18 (
Pattern.compile("\\$\\{(\\w+)}")): pola menangkap${nama}sekaligus mengambil isi tanda kurung sebagai capture group — nama variabel yang dicari di konteks. - Baris 21-24 (loop
find+appendReplacement): mekanisme standar penggantian regex di Java. Nilai dari konteks melewatiescapeHtmldulu, baru disisipkan — pada Thymeleaf, perilaku ini adalah bawaanth:text; atributth:utextyang mem-bypass escaping adalah pintu kebalikan yang harus dipakai sangat hemat. - Baris 23 (
Matcher.quoteReplacement): hasil escape bisa berisi karakter\atau$yang bermakna khusus bagiappendReplacement;quoteReplacementmenetralkan keduanya. - 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. - Baris 49-57 (render per baris): perulangan
<li>disimulasikan dengan memanggilrenderberulang 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 berkasimport 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("&", "&")
.replace("<", "<")
.replace(">", ">")
.replace("\"", """)
.replace("'", "'");
}
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);
}
}