Memakai Reverse Ajax “Comet” Dengan Spring Web MVC 3.2

Hujan turun tanpa henti di malam yang ceriah ini.   Karena hari ini adalah hari terakhir di tahun 2012.  Dan tulisan ini akan menjadi tulisan terakhir di tahun 2012.  Selamat merayakan tahun baru dan selamat datang, 2013!  Kode program terakhir yang saya ubah di tahun 2012 berkaitan dengan sebuah aplikasi web yang saya buat dimana tampilannya seperti berikut ini:

Permintaan Perubahan

Permintaan Perubahan

Halaman tersebut menampilkan data termasuk status data yang ada. Perubahan status data, seperti “Belum Disajikan” menjadi “Sudah Disajikan” dilakukan oleh pengguna lain di komputer lain. Perubahan ini perlu diperbaharui oleh halaman tersebut.

Tetapi yang terjadi adalah bila pengguna membuka halaman ini, maka sampai selama-lamanya halaman akan terlihat sama, walaupun pengguna lain telah men-update status data.   Informasi yang ditampilkan menjadi tidak up-to-date lagi.

Lalu bagaimana cara memperbaharui tampilan? Pengguna bisa men-klik tombol Refresh atau menekan tombol F5!!   Bayangkan jika pengguna harus memantau halaman ini terus menerus selama jam kerja (misalnya seorang kasir), maka tombol F5 bisa jadi tombol yang paling cepat pudar di keyboard 😉

Lalu apakah ada solusi untuk membuat halaman ini terlihat lebih profesional dimana perubahan data oleh user lain bisa langsung diperbaharui?  Yup!  Saya bisa menggunakan apa yang disebut sebagai teknik Reverse Ajax.   Bila biasanya client yang duluan menghubungi server, maka pada Reverse Ajax, seolah-olah server yang akan menghubungi client.   Solusi lain adalah dengan memakai WebSocket (bagian dari HTML5) dengan syarat browser pengguna sudah mendukung.

Teknik Reverse Ajax terdiri atas polling, piggyback, dan Comet (long polling).

Polling adalah cara yang paling sederhana, yaitu dengan membuat sebuah timer JavaScript yang memeriksa perubahan data di server secara periodik. Cara ini akan sangat membebani server karena selalu ada data yang dikirim dari server biarpun tidak ada perubahan.

Piggyback akan mengembalikan status perubahan bersamaan dengan sebuah request normal. Misalnya, pada saat user men-klik sesuatu di halaman, sekalian ikut kembalian event perubahan.   Cara ini lebih hemat bandwidth dibandingkan dengan teknik polling. Kelemahannya adalah harus ada sebuah request yang normal terlebih dahulu, baru event perubahan yang terakumulasi di server dikirim balik bersamaan dengan reponse.   Saya tidak bisa menggunakan piggyback karena saya ingin halaman tetap diperbaharui biarpun pengguna hanya duduk diam menatap monitor.

Teknik Reverse Ajax yang lebih efisien adalah Comet atau sering disebut juga dengan long polling. Pada Comet, client akan melakukan request, tetapi server tidak akan langsung mengembalikan nilai.   Request ini akan terbuka untuk waktu yang lama.   Selama selang waktu tersebut, server dapat mengirim data ke client kapan saja.   Teknik ini adalah teknik yang paling efisien karena bandwidth hanya akan dipakai bila server perlu mengembalikan data ke client.

Saya akan memakai teknik Comet.   Untuk itu, saya harus memenuhi persyaratan, yaitu memiliki server yang mendukung asynchronous reponse.  Selain itu saya juga perlu memakai bahasa pemograman yang mendukung multithreading. Sebagai contoh, bila saya memakai Apache + PHP ‘murni’ dan berusaha menghentikan request dengan sleep(), maka selama thread di-sleep(), seluruh client lain tidak akan bisa mengakses web!!   Kinerja web malah akan jauh lebih buruk dibanding memakai teknik polling.  Server apa yang sudah mendukung asynchronous response?   Tomcat 6 ke atas, Jetty 6 ke atas, Glassfish dengan Grizzly, dan sebagainya.   Saya akan memakai Tomcat 7.

Apa contoh teknologi bahasa pemograman yang mendukung asynchronous response?   Servlet 3 di Java EE!   Ada juga framework yang mempermudah seperti CometD, DWR (Direct Web Remoting), dan Atmosphere.   Selain itu,  Spring Web MVC sejak versi 3.2 telah mendukung asynchronous response.   Karnea aplikasi saya dari awal dikembangkan dengan Spring Web MVC, maka yang perlu saya lakukan adalah men-upgrade versi Spring yang dipakai ke versi 3.2.   Dengan memakai Apache Maven, yang perlu saya lakukan hanya mengganti nilai property org.springframework-version menjadi 3.2.0.RELEASE, dan Maven akan mendownload JAR yang dibutuhkan.

Setelah men-upgrade versi Spring ke 3.2.0.RELEASE, saya perlu mengubah file web.xml. Perubahan yang saya lakukan adalah:

<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0">

Selain itu, saya perlu menambahkan <async-supported> di definisi org.springframework.web.servlet.DispatcherServlet milik Spring Web MVC seperti yang terlihat berikut ini:

<servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>    
    <init-param>      
      <param-name>contextConfigLocation</param-name>
      <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
    </init-param>    
    <load-on-startup>1</load-on-startup>
    <async-supported>true</async-supported>
</servlet>

Karena saya memakai Spring Security yang mendefinisikan sebuah filter di web.xml yaitu org.springframework.web.filter.DelegatingFilterProxy, maka saya juga perlu menambahkan <async-supported> pada definisi filter tersebut. Berikut ini adalah perubahan yang saya lakukan:

<filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    <async-supported>true</async-supported>    
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ASYNC</dispatcher>
  </filter-mapping>
</filter>

Setelah ini, saya bisa mulai membuat kode program.  Saya akan mulai dari service layer.  Saya sedikit frustasi karena tidak menemukan pendekatan yang lebih rapi yang tidak mengotori service layer dengan objek DeferredResult milik presentation layer.  Saya selalu berusaha menghindari coupling seperti ini, tapi sepertinya tidak ada pilihan yang lebih gampang.   Pada service layer, saya mendefisinikan sebuah  struktur untuk menampung DeferredResult yang mewakili asynchronous request seperti berikut ini:

private List<DeferredResult<Boolean>> listDeferredResult = new Vector<DeferredResult<Boolean>>();

Saya memakai Vector karena class tersebut thread safe.  Pada asynchronous request yang diberikan halaman kasir, saya hanya mengembalikan sebuah nilai true bila halaman perlu diperbaharui atau false bila tidak.  Hal ini karena tabel dibuat dengan jqGrid dan saya tidak ingin repot mengelola data.

Pada satu atau lebih service yang menyebabkan perubahan di halaman kasir, saya memberikan kode program seperti berikut ini:

public Pemesanan prosesTambahPemesanan(Pemesanan pemesanan) {
   ...
   prosesNotifikasiPembayaran();
}

public Pemesanan prosesUpdatePemesanan(Pemesanan pemesanan) {
   ...
   prosesNotifikasiPembayaran();
}

private void prosesNotifikasiPembayaran() {
   for (DeferredResult<Boolean> deferredResult: listDeferredResult) {
      deferredResult.setResult(Boolean.TRUE);
   }
}

Method prosesNotifikasiPembayaran() akan menyebabkan asynchronous response selesai dengan nilai kembali berupa true.

Tidak lupa saya juga menyediakan method untuk mendaftar dan menghapus setiap DeferredResult yang ada di Vector, seperti berikut ini:

public void daftarNotifikasiPembayaran(DeferredResult<Boolean> deferredResult) {
  listDeferredResult.add(deferredResult);
}

public void hapusNotifikasiPembayaran(DeferredResult<Boolean> deferredResult) {
  listDeferredResult.remove(deferredResult);
}

Ok, service layer sudah selesai, sekarang saya akan ke presentation layer.  Saya menambahkan method berikut ini di controller:

@RequestMapping(value="pembaharuan")
@ResponseBody
public DeferredResult<Boolean> notifikasiPembayaran() {
  final DeferredResult<Boolean> deferredResult = new DeferredResult<Boolean>(30000l, Boolean.FALSE);
  pemesananService.daftarNotifikasiPembayaran(deferredResult);
  deferredResult.onCompletion(new Runnable() {
    @Override
    public void run() {
      pemesananService.hapusNotifikasiPembayaran(deferredResult);
    }
  });
  return deferredResult;
}

Pada kode program controller tersebut, saya memberikan waktu tunggu 30.000 ms atau 30 detik.  Bila setelah 30 detik, belum ada yang memanggil method DeferredResult.setResult(), maka nilai false akan dikembalikan.

Langkah terakhir, menambahkan kode program ini di view JSP:

...
<spring:url value="/pembayaran/pembaharuan" var="urlPembaharuan" />
...
function comet() {
  $.getJSON("${urlPembaharuan}", function(data) {
     if (data==true) {
        gridUtama.trigger('reloadGrid');
     }
     comet();
  });
}
...

Pada kode program di atas, bila hasil asynchronous request adalah true, maka dialog jqGrid akan di-refresh.   Setelah itu, tidak peduli nilai kembali adalah true atau false, fungsi tersebut akan kembali memanggil dirinya sendiri.

Untuk mencobanya, saya membuka tiga browser yang berbeda, dimana pada 2 browser, saya membuka halaman untuk kasir.  Satu browser-nya lagi untuk perubahan yang harus di-update oleh halaman kasir, seperti pada gambar berikut ini:

Pengujian

Pengujian

Pada gambar di atas, pada saat tombol “Iya” di-klik, maka tabel di browser Kasir A maupun browser Kasir B akan di-reload secara otomatis.

Teknik Comet (long polling) memang agak mirip polling, dimana client harus melakukan request secara periodik.  Tetapi penggunaan asynchronous request membuat Comet jauh lebih efisien dibanding polling:

  1. Pada  polling,  bila interval adalah 30 detik, maka client benar-benar harus menunggu selama 30 detik baru ada hasil yang diperoleh.  Pada Comet, bila interval adalah 30 detik, lalu pada detik ke-15 sudah ada event, maka hasil akan segera dikembalikan pada saat itu juga.
  2. Karena alasan di atas, untuk memperoleh hasil yang real-time, maka polling biasanya memiliki interval yang singkat, misalnya 10ms.   Hal ini menyebabkan semakin banyak request yang dilakukan ke server secara periodik dan akan membebani server.
Iklan

Memakai Autocomplete Widget Di Spring Web MVC

Saya memiliki sebuah halaman pencarian dimana pengguna bisa mengetik keyword pencarian, lalu isi halaman akan diperbaharui berdasarkan keyword tersebut secara AJAX.   Perubahan isi konten secara otomatis mungkin akan memberatkan browser pengguna; selain itu, saya harus men-query seluruh data setiap kali pengguna melakukan perubahan keyword yang di-ketik.  Bayangkan seorang pengguna yang menekan backspace, mengetik satu huruf, menekan backspace, mengetik dua huruf, menekan backspace dua kali, dan seterusnya.  Berapa kali harus request AJAX yang dilakukan ke server?  Hal ini dapat diatasi dengan AutoComplete widget dari jQuery UI yang memiliki option delay yang akan menunggu sesuai dengan milliseconds yang diberikan sebelum menghubungi server.   Dengan memakai Autocomplete widget, maka isi konten tidak lagi diperbaharui secara otomatis, tetapi seiring pengguna mengetik, akan muncul panduan keyword yang bisa dipilih oleh pengguna.

Berikut ini adalah kode HTML untuk bagian pencarian:

<div class="search custom_general_menu">
  <form name="formSearch" id="formSearch" action="#" method="post">
    <p>
      <input name="keyword" id="keyword" placeholder="Cari menu makanan..." type="text" />
      <input value="" id="cariMenu" class="btnsearch" type="submit" />
    </p>
  </form>
</div>

Untuk memakai Autocomplete widget, maka saya menambahkan baris berikut ini di halaman jspx tersebut:

...
  <spring:url value="/menumakanan/autocomplete" var="urlAutocomplete" />
  ...
  <script type="text/javascript">
     ...
     $("input#keyword").autocomplete({
        delay: 500,
        minLength: 3,
        source: "${urlAutocomplete}"
     });
     ...
  </script>
...

Pada konfigurasi di atas, setelah 500 ms  sejak user mengetik sebuah huruf  baru akan dilakukan request ke URL /menumakanan/autocomplete, dimana akan dilewatkan parameter term berupa keyword yang diketik oleh user.

Okay, bagian sisi web front-end sudah selesai, sekarang saya perlu melakukan perubahan di sisi back-end.

Saya akan mulai dengan menambahkan sebuah query baru yang hanya mengembalikan String di MenuMakananRepository.java.  Karena saya memakai Spring Data JPA, saya hanya perlu menambah sebuah method baru dengan isi seperti berikut ini:

...
@Query("SELECT menu.namaMenu FROM MenuMakanan menu WHERE menu.namaMenu LIKE :namaMenu")
List<String> findNamaMenuMakanan(@Param("namaMenu") String namaMenuMakanan);
...

Setelah itu, saya menambahkan sebuah service baru di MenuMakananService.java dengan isi seperti berikut ini:

...
public List<String> getNamaMenuMakanan(String namaMenuYangDicari) {
  return menuMakananRepository.findNamaMenuMakanan(
    String.format("%%%s%%", namaMenuYangDicari));
}
...

Sampai disini, saya akan membuat test case pada unit test MenuMakananService tersebut untuk memastikan bahwa tidak ada yang salah pada kode program.  Mm, kadang-kadang saya suka memakai test driven development (TDD) dimana saya membuat unit test terlebih dahulu baru membuat kode program di atas.  Biasanya, saya mengikuti  prinsip TDD hanya jika saya masih belum memahami sepenuhnya apa yang harus saya buat.

Setelah memastikan tidak ada yang salah di service layer, saya akan lanjut ke controller.   Saya akan membuat sebuah class Java yang mewakili JSON yang akan dikembalikan nanti.  Isi class tersebut akan terlihat seperti:

public class Autocomplete {

  public String value;

  public Autocomplete(String value) {
     this.value = value;
  }

  public String getValue() {
     return value;
  }

  public void setValue(String value) {
     this.value = value;
  }
}

Class ini nantinya akan diterjemahkan menjadi JSON oleh Jackson.  Format JSON ini yang diharapkan oleh Autocomplete widget adalah seperti berikut ini:

[
  {label: 'Item 1', value: 'KODE1'},
  {label: 'Item 2', value: 'KODE2'},
  {label: 'Item 3', value: 'KODE3'}
]

Nilai label akan ditampilkan dalam popup nanti, sementara nilai value akan dimasukkan ke dalam text box. Karena umumnya, kedua nilai tersebut sama, maka Autocomplete widget juga menerima JSON seperti berikut ini:

[
  {value: 'Item 1'},
  {value: 'Item 2'},
  {value: 'Item 3'}
]

Dengan demikian, untuk menghasilkan format JSON di atas, saya hanya perlu meletakkan setiap object Autocomplete ke dalam sebuah List.

Sekarang, saya tinggal menambahkan sebuah method baru di controller MenuMakananController.java dengan isi seperti berikut ini:

...
@RequestMapping(value="autocomplete")
@ResponseBody
public List<Autocomplete> autocomplete(@RequestParam("term") String term) {
   return (List<Autocomplete>) CollectionUtils.collect(menuMakananService.getNamaMenuMakanan(term),
     new Transformer() {
       @Override
       public Object transform(Object input) {
          return new Autocomplete((String) input);
       }
     }
   });
}
...

Pada kode program controller di atas, saya memakai class CollectionUtils dari Apache Commons Collections untuk menyederhanakan kode program.  Sesungguhnya, perbedaan jumlah baris tidak terlalu beda jauh dibanding membuat kode looping for biasa, tetapi saya selalu membiasakan diri untuk tidak melakukan reinvent the wheel (kecuali untuk kasus-kasus tertentu, misalnya tweaking kinerja kode program).  Well,  seorang computer scientist akan tertarik mempelajari kode program internal dan seluk-beluknya (bahkan membuat sesuatu yang baru yang lebih baik!), tetapi seorang software developer harus berusaha menyelesaikan sebuah tugas secara efisien dan secepat mungkin.

Sekarang, bila saya menjalankan halaman web tersebut,  Autocompletion widget akan bekerja seperti yang terlihat di gambar berikut ini:

Tampilan Autocomplete widget

Tampilan Autocomplete widget

Menangani Exception Di Spring Web Flow – Dengan Berpindah View

Tulisan ini merupakan kelanjutan dari tulisan Menangani Exception Di Spring Web Flow.  Pada tulisan tersebut, jika terdapat exception,  maka Spring Web Flow akan kembali menampilkan halaman yang sama tetapi dengan menyertakan pesan kesalahan.  Sementara dalam beberapa kasus, saya mungkin harus menampilkan halaman (view) lain bila terdapat exception.  Selain itu, mungkin saja exception terjadi di action-state bukan view-state sehingga exception tidak akan ditampilkan.

Bila saya membongkar kode program class program EventFactorySupport, saya akan menemukan fakta bahwa event id yang dihasilkan bila terjadi kesalahan adalah “error“.   Yup, salah satu alasan kenapa saya suka framework open source adalah saya dapat mencari tahu sendiri bila ada yang tidak saya mengerti.   Saat memakai tools closed-source seperti .NET, jika ada sesuatu yang ingin saya lakukan dan Microsoft (dan blogger pendukungnya) belum menjelaskan bagaimana caranya, maka saya hanya bisa pasrah menunggu.  Kembali ke EventFactorySupport, bila saya membuka isi kode programnya, saya akan menemukan bagian seperti berikut ini:

public class EventFactorySupport {
  private static final String SUCCESS_EVENT_ID = "success";
  private static final String ERROR_EVENT_ID = "error";
  private static final String YES_EVENT_ID = "yes";
  private static final String NO_EVENT_ID = "no";
  private static final String NULL_EVENT_ID = "null";
  ...
}

Dengan demikian, bila saya ingin berpindah halaman saat terjadi kesalahan, saya perlu menangkap event iderror” seperti pada contoh definisi flow berikut ini:

...
<action-state id="simpanPemesanan">
  <evaluate expression="pemesananController.simpanPemesanan(flowScope.pemesanan, messageContext)" />
  <transition on="error" to="halamanKesalahan" />
  <transition on="selesai" />
</action-state>

<end-state id="selesai" view="redirect:/" />

Karena method simpanPemesanan dipanggil saat berada di action-state, maka saya perlu melakukan redirect ke sebuah view-state.  Hal ini karena action-state tidak akan pernah menampilkan apa-apa.

Definisi view-state halamanKesalahan sama seperti biasanya, hanya saja harus mendeklarasikan model pemesanan yang akan di-bind!  Bila tidak ada model yang di-bind, maka pesan kesalahan tidak akan ditampilkan.  Informasi lebih lanjut mengenai cara menampilkan kesalahan dapat saya pelajari dengan membaca isi kode program org.springframework.web.servlet.tags.form.ErrorsTag dan org.springframework.web.servlet.BindStatus.   Berikut ini adalah contoh definisi view-state halamanKesalahan:

<view-state id="pesanKesalahan" view="kesalahan/pesanKesalahan" model="pemesanan" />

Pada file JSP untuk view-state tersebut, seperti biasanya, saya perlu menambahkan <form:errors> untuk menampilkan pesan kesalahan dari controller, seperti pada contoh berikut ini:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<div xmlns:jsp="http://java.sun.com/JSP/Page"
     xmlns:form="http://www.springframework.org/tags/form">
  ...
  <form:form modelAttribute="pemesanan" htmlEscape="true">
    ...
    <form:errors />
    ...
  </form:form>
  ...
</div>

Menangani Exception Di Spring Web Flow

Kesalahan selalu dapat terjadi dalam setiap kondisi.  Sebuah aplikasi yang baik harus dapat mempersiapkan diri terhadap kesalahan.  Di Java, kesalahan selalu diwakili melalui sebuah class yang diturunkan dari java.lang.Exception.   Sebagai contoh, untuk operasi penyimpanan domain object Pemesanan, saya bisa membuat beberapa class Exception seperti ItemTidakTersedia, LewatDariBatasWaktu, atau TerlaluBanyakHutang 🙂   Setiap kali penyimpanan dilakukan, bila ada “kesalahan”, saya akan men-throw salah satu dari Exception di atas.

Seandainya saya memakai Spring Web Flow untuk mengelola aliran view di web front end, maka bila terdapat Exception, saya seharusnya tidak boleh lanjut ke state berikutnya (alur normal).   Tapi bila saya hanya sekedar men-throw Exception, Spring Web Flow akan menampilkan halaman stack trace HTTP Status 500 yang penuh kode program.  Seharusnya pengguna tidak perlu melihat halaman ini. Lalu, apa yang harus saya lakukan agar bila terjadi Exception, Spring Web Flow akan kembali ke halaman yang sama, tetapi dengan menampilkan pesan kesalahan yang mudah dipahami pengguna?

Berikut ini adalah contoh isi definisi flow untuk pemesanan saya:

...
<view-state id="konfirmasiPemesanan" view="pemesanan/konfirmasi" model="pemesanan">
  <transition on="prosesPemesanan" to="selesai">
    <evaluate expression="pemesananController.simpanPemesanan(flowScope.pemesanan, messageContext)" />
  </transition>
  <transition on="kembali" to="isiDataPemesanan" />
  <transition on="batalkanPemesanan" to="batal" bind="false" />
</view-state>

<end-state id="selesai" view="pemesanan/selesai">
  <output name="pemesanan" />
</end-state>

<end-state id="batal" view="redirect:/" />

Jika terjadi event “prosesPemesanan” (yang dipicu oleh sebuah tombol di view)  di view konfirmasiPemesanan, maka method simpanPemesanan di pemesananController akan dikerjakan.  Pada method inilah ada kemungkinan Exception di-throw.   Yang harus saya lakukan adalah menentukan pesan kesalahan yang akan ditampilkan bila sebuah Exception terjadi melalui MessageContext.  Selain itu, saya harus mengembalikan sebuah Event sehingga Spring Web Flow dapat membedakan apakah telah terjadi kesalahan atau tidak.  Berikut ini adalah contoh isi method simpanPemesanan yang saya buat:

public Event simpanPemesanan(Pemesanan pemesanan, MessageContext messageContext) {
  try {
    pemesananService.simpanPemesanan(pemesanan);
  } catch (ItemTidakTersedia ex) {
    messageContext.addMessage(new MessageBuilder().error().defaultText("Tunggu 3 hari lagi ya!"));
    return new EventFactorySupport().error(this);
  } catch (LewatDariBatasWaktu ex) {
    messageContext.addMessage(new MessageBuilder().error().defaultText("Kami tidak melayani yang beginian!"));
    return new EventFactorySupport().error(this);
  } catch (TerlaluBanyakHutang ex) {
    messageContext.addMessage(new MessageBuilder().error().defaultText("Maaf, Anda terlalu banyak hutang!"));
    return new EventFactorySupport().error(this);
  }
  return new EventFactorySupport().success(this);
}

Sekarang, jika terjadi kesalahan, maka Spring Web Flow akan memperbaharui halaman yang sama dengan yang sedang ditampilkan yaitu view pemesanan/konfirmasi.  Bedanya adalah bila terjadi kesalahan, messageContext akan berisi pesan kesalahan yang saya buat di atas.  Lalu bagaimana cara menampilkan pesan kesalahan di messageContext?  Saya bisa memakai Spring Web MVC tags seperti pada contoh berikut ini di halaman JSP yang mewakili view:

<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
<div xmlns:jsp="http://java.sun.com/JSP/page"
     xmlns:form="http://www.springframework.org/tags/form">
...
<form:form id="frmUtama" action="${flowExecutionUrl}" modelAttribute="pemesanan">
  <div id="pesanKesalahan"><form:error /></div>
  ...
</form:form>
...
</div>

Pada contoh di atas, tag <form:error> akan menampilkan pesan kesalahan di messageContext bila seandainya terdapat pesan kesalahan.

‘Kebanyakan’ memakai context:component-scan di Spring MVC + Spring Core

Jika saya membuat proyek baru dengan memakai template Spring MVC dari STS, maka secara otomatis akan dibuatkan dua file konfigurasi Spring, yaitu:

  1. /src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml
  2. /src/main/webapp/WEB-INF/spring/root-context.xml

Jika saya membuka definisi DispatcherServlet di web.xml, saya akan menemukan parameter seperti berikut ini:

<servlet>
  <servlet-name>appServlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
  </init-param>
</servlet>

Terlihat bahwa file konfigurasi servlet-context.xml akan dipakai oleh DispatcherServlet.

Dengan pemikiran yang naif, saya awalnya menganggap bahwa root-context.xml akan dipanggil secara tidak langsung oleh servlet-context.xml sehingga isinya akan digabungkan ke dalam 1 context.  Tapi ternyata saya salah!!!

root-context.xml dan servlet-context.xml akan dipakai untuk membuat dua context yang berdiri sendiri!!  Masing-masing isi-nya berdiri sendiri..

Lalu, apa konsekuensinya?  Biasanya, pada servlet-context.xml, saya mendefinisikan hal-hal yang berhubungan dengan Spring Web MVC.  Sementara pada root-context.xml, saya akan menyertakan file konfigurasi lain yang berhubungan dengan JPA, Spring Security, dan sebagainya.

Pada servlet-context.xml yang naif, saya mengisyaratkan Spring Core agar membuat bean secara otomatis dengan mencari annotation di package yang saya tentukan:

<context:component-scan base-package="com.lena.aplikasi" />

Demikian juga, pada root-context.xml, saya perlu menentukan class-class yang akan di-scan, dengan konfigurasi seperti:

<context:component-scan base-package="com.lena.aplikasi" />

Secara naif, saya mengira setiap class yang memiliki annotation yang diturunkan dari @Component akan dibuat menjadi singleton dalam 1 context yang sama.  Tapi ternyata tidak!  Masing-masing class akan dibuat menjadi singleton dalam 2 context yang berbeda.  Untuk setiap class komponen, akan ada 1 singleton di context servlet-context.xml dan 1 singleton lagi di context root-context.xml.

Untuk membuktikannya, saya mencari di log console saat menjalankan aplikasi.  Saya akan melihat ada DUA baris terpisah yang mengandung nama bean kompoen saya, yaitu:

Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@80e8fe: ... [daftar bean]

dan

Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1b6c585: ... [daftar bean]

Apakah ada masalah jika diteruskan seperti ini?  Tentu saja.  Membuat definisi bean dua kali berarti duplikasi penggunaan memori.  Selain itu, waktu startup akan menjadi lebih lama.  Dan, bisa jadi akan muncul bug-bug suatu hari nanti.   Saya  menemukan bug aneh seperti ini karena pada saat melakukan integration testing pada service layer dengan  JUnit, saya hanya memakai  root-context.xml saja.  Sementara ternyata, pada saat aplikasi dijalankan sepenuhnya (dengan servlet-context.xml dan root-context.xml), terdapat perilaku yang berbeda.

Lalu bagaimana solusinya?  Saya mulai membiasakan diri hanya menyertakan bean/komponen yang berhubungan dengan web di servlet-context.xml.  Karena biasanya class-class yang berhubungan dengan web berada di subpackage web, maka saya mendefinisikan ulang component-scan di servlet-context.xml menjadi:

<context:component-scan base-package="com.lena.aplikasi.web" />

Sementara itu, pada root-context.xml, saya akan menyertakan sisa-nya.  Tapi karena komponen-komponen lain tersebut bisa saja terpencar di berbagai subpackage yang berbeda-beda, maka  root-context.xml saya tetap mencari seluruh class yang ada, tetapi kali ini mengabaikan yang ada di subpackage web:

<context:component-scan base-package="com.lena.aplikasi">
   <context:exclude-filter type="regex" expression="com\.lena\.aplikasi\.web\..*" />
</context:component-scan>

Memakai MessageConverter di Spring 3.1 Untuk Web Services REST

Salah satu fitur menarik yang diperkenalkan oleh Spring Framework 3.1 adalah message converter yang dalam bentuk tag <mvc:message-converters>.  Dengan message converter, saya bisa membuat representasi  object di aplikasi saya dengan mudah dalam bentuk JSON dan XML secara otomatis.   Untuk mendukung konversi objek ke/dari JSON, saya memakai Jackson JSON library (http://jackson.codehaus.org).  Sementara untuk konversi objek ke/dari XML, saya memakai Castor (http://castor.codehaus.org).

Saya mulai dengan membuat sebuah proyek baru di STS, dengan memakai template Spring MVC Project.   Sebelum mulai membuat kode program, saya menambahkan dependencies Maven ke proyek saya seperti yang terlihat pada gambar berikut ini:

Dependencies Maven

Dependencies Maven

Kemudian saya membuat sebuah class dengan nama Mahasiswa di package co.id.jocki.domain.  Isi dari class Mahasiswa adalah:

package co.id.jocki.domain;

import java.io.Serializable;

public class Mahasiswa implements Serializable {

	private static final long serialVersionUID = 225855015823197676L;

	private String nim;
	private String nama;
	private int usia;

	public String getNim() {
		return nim;
	}
	public void setNim(String nim) {
		this.nim = nim;
	}
	public String getNama() {
		return nama;
	}
	public void setNama(String nama) {
		this.nama = nama;
	}
	public int getUsia() {
		return usia;
	}
	public void setUsia(int usia) {
		this.usia = usia;
	}	

}

Lalu, pada package co.id.jocki, saya membuat sebuah class bernama LatihanController.  Isi dari class tersebut adalah:

package co.id.jocki;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import co.id.jocki.domain.Mahasiswa;

@Controller
@RequestMapping(value="/mahasiswa")
public class LatihanController {

	private Mahasiswa mahasiswa;

	@RequestMapping(value="/listdata", method=RequestMethod.GET)
	@ResponseBody
	public Mahasiswa listData() {
		if (mahasiswa==null) {
			mahasiswa = new Mahasiswa();
			mahasiswa.setNama("Makdalena Hendry");
			mahasiswa.setNim("99999999");
			mahasiswa.setUsia(21);
		}
		return mahasiswa;
	}

}

Pada kasus nyata, tentu saja isi controller tidak sesederhana ini (misalnya masih ada get, delete, update, dsb).  Object yang ada juga tidak dibuat disini, melainkan seharusnya diambil dari medium penyimpanan (misalnya database).

Yang menarik disini adalah saya  tidak melakukan proses transformasi ke JSON ataupun XML secara manual.  Saya juga tidak memanggil sebuah fungsi ajaib.  Saya hanya mengembalikan sebuah objek mahasiswa seperti biasanya layaknya kode program standard.

Lalu bagaimana konversi bisa dilakukan?  Karena saya memberitahukannya secara deklaratif (tanpa kode program) dengan mengedit file servlet-context.xml.  File ini dapat ditemukan di lokasi src/main/webapp/WEB-INF/spring/appServlet.  Saya mengubah file tersebut sehingga isinya menjadi:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

  <mvc:annotation-driven>
    <mvc:message-converters>
      <beans:bean class="org.springframework.http.converter.json.MappingJacksonHttpMessageConverter"/>
      <beans:bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter">
        <beans:property name="marshaller" ref="castorMarshaller"/>
        <beans:property name="unmarshaller" ref="castorMarshaller"/>
      </beans:bean>
    </mvc:message-converters>
  </mvc:annotation-driven>

  <beans:bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller">
    <beans:property name="mappingLocation" value="classpath:oxm-mapping.xml"/>
  </beans:bean>

  <context:component-scan base-package="co.id.jocki" />

</beans:beans>

Rahasianya terletak di <mvc:message-converters> dimana saya mendeklarasikan bean dari class MappingJacksonHttpMessageConverter dan class MarshallingHttpMessageConverter.  Khusus untuk yang XML, saya perlu membuat file oxm-mapping.xml (nama yang sama seperti di property mappingLocation di bean castorMarshaller.

Saya akan membuat file oxm-mapping.xml ini di folder src/main/resources.  Isi file tersebut menentukan bagaimana memetakan sebuah class Java ke XML (dan sebaliknya) seperti yang terlihat di berikut ini:

<?xml version="1.0" encoding="UTF-8"?>
<mapping xmlns="http://castor.exolab.org/"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://castor.exolab.org/ http://castor.org/mapping.xsd">

  <class name="co.id.jocki.domain.Mahasiswa">
    <map-to xml="mahasiswa" ns-uri="http://jockihendry.com/mahasiswa/"
       ns-prefix="mahasiswa"/>

    <field name="nim" type="string" >
      <bind-xml auto-naming="deriveByField" node="element"/>
    </field>

    <field name="nama" type="string">
      <bind-xml auto-naming="deriveByField" node="element" />
    </field>

    <field name="usia" type="integer">
      <bind-xml auto-naming="deriveByField" node="element" />
    </field>
  </class>

</mapping>

Setelah ini, saya  menjalankan tc Server untuk menguji web service REST tersebut.

Untuk melakukan pengujian, saya akan menggunakan cURL (http://curl.haxx.se), sebuah tools command-line yang bisa dipakai untuk browsing berbasis teks.  Karena saya pernah meng-install Zend Studio, tools tersebut secara otomatis sudah ada dan siap dipakai.

Saya mulai dengan memanggil halaman http://localhost:8080/latihan-rest/mahasiswa/listdata.  Pada kode program, terlihat controller hanya mengembalikan sebuah objek mahasiswa.  Tetapi saya menginginkan kembalian berupa JSON.  Dan, server saya ternyata sudah mendukungnya seperti yang terlihat di tampilan berikut:

Komunikasi REST dengan JSON

Komunikasi REST dengan JSON

Lalu, kali ini saya menginginkan kembalian berupa XML.  Dan sekali lagi, server saya secara otomatis sudah mendukungnya seperti yang terlihat di gambar berikut:

Komunikasi REST dengan XML

Komunikasi REST dengan XML

Memakai Spring MVC di SpringSource Tool Suite

MVC adalah sebuah design pattern yang memisahkan logika aplikasi ke dalam tiga wilayah yang berbeda yaitu Model, View, dan Controller.  Hal ini berbeda dengan multi-tier (N-tier) dimana pada N-tier terdapat pemisahan tier pada mesin yang berbeda di komputer yang berbeda.  Salah satu alasan terbentuknya arsitektur N-tier adalah untuk menciptakan skalabilitas yang lebih baik.  Sebagai contoh, program database umumnya membutuhkan memori dalam jumlah besar sementara program logika membutuhkan prosesor yang kencang.  Dengan arsitektur N-tier, database berada di sebuah tier tersendiri yang diletakkan pada server dengan memori berjumlah besar dan prosesor sedang.  Sementara itu, business logic dapat berada di sebuah server dengan memori sedang tetapi memiliki prosesor berkecepatan tinggi.

Bila N-tier tercipta karena alasan skalabilitas, maka MVC terlahir untuk memisahkan program kita kedalam tiga elemen yang terpisah dan sebisa mungkin tidak saling terkait. Sebagai contoh, bagian View yang tadinya adalah HTML, dapat diganti menjadi PDF, Flash, ataupun WAP (mobile), tanpa harus memprogram ulang Controller dan Model.  Bila kita ingin meletakkan MVC ke dalam N-tier, maka MVC berada di Presentation Tier.

Pada artikel ini, saya akan membuat sebuah aplikasi MVC sederhana dengan memakai Spring MVC (bagian dari Spring Framework) dan IDE SpringSource Tool Suite (STS).  Spring Framework memang lebih terkenal dengan IoC container-nya, yang banyak ditiru di JEE 6.  Memakai framework ini menimbulkan sebuah perasaan nostalgia tersendiri, karena Spring Framework adalah framework Java yang pertama kali saya pakai setelah lulus kuliah dan bekerja sebagai programmer junior.

Pada STS, saya mulai dengan memilih menu File, New, dan Spring Template Project.  Pada kotak dialog New Template Project, saya memilih Spring MVC Project.  Pada dialog New Spring MVC Project yang muncul, saya mengisi Project Name dengan latihan-mvc, mengisi top-level package dengan co.id.jocki.latihan dan kemudian men-klik tombol Finish.  STS akan membuat struktur proyek seperti pada gambar berikut ini:

Struktur Proyek Spring MVC

Struktur Proyek Spring MVC

Template Spring MVC secara otomatis membuat sebuah controller bernama HomeController dan sebuah view dengan nama home.  Saya akan mengabaikan dua file ini.

Membuat Model

Pada langkah berikutnya, saya membuat sebuah model dengan nama Mahasiswa di package co.id.jocki.mahasiswa.  Saya mulai dengan men-klik kanan pada src/main/java kemudian memilih New, Class.  Lalu saya mengisi dialog New Java Class dengan seperti yang terlihat di gambar:

Membuat Model Baru

Membuat Model Baru

Class Mahasiswa sebagai model memiliki isi seperti berikut ini:

package co.id.jocki.mahasiswa;

public class Mahasiswa {

  private String nim;
  private String nama;
  private Date tanggalLahir;

  public String getNim() {
    return nim;
  }
  public void setNim(String nim) {
    this.nim = nim;
  }
  public String getNama() {
    return nama;
  }
  public void setNama(String nama) {
    this.nama = nama;
  }
  public Date getTanggalLahir() {
    return tanggalLahir;
  }
  public void setTanggalLahir(Date tanggalLahir) {
    this.tanggalLahir = tanggalLahir;
  }
}

Sebelum beranjak meninggalkan model ini, saya ingin mencoba menggunakan fitur Bean Validation yang didukung oleh Spring Framework.  Dengan Bean Validation, saya tidak perlu report-repot melakukan validasi di sisi client maupun di sisi server, karena semuanya akan dilakukan otomatis oleh Spring Framework.  Untuk itu, saya perlu menambahkan dependency baru di Maven dengan melakukan langkah-langkah seperti berikut ini:

  1. Buka file pom.xml di STS.  Akan muncul sebuah halaman editor untuk Maven.
  2. Buka tab Dependencies (disebelah kanan tab Overview).
  3. Klik tombol Add…
  4. Pada bagian Enter groupId, artifactId or sha1 prefix or pattern, isi dengan validation-api.
  5. Klik pada javax.validationseperti yang terlihat di gambar berikut ini:

    Dependency Maven untuk validation-api

    Dependency Maven untuk validation-api

  6. Klik tombol OK.
  7. Kembali Klik tombol Add…
  8. Pada bagian Enter groupId, artifactId or sha1 prefix or pattern, isi dengan hibernate-validator.
  9. Klik pada org.hibernateseperti yang terlihat di gambar berikut ini:

    Dependency Maven untuk hibernate-validator

    Dependency Maven untuk hibernate-validator

  10. Klik tombol OK.
  11. Kembali klik tombol Add…
  12. Pada bagian Enter groupId, artifactId or sha1 prefix or pattern, isi dengan joda-time.
  13. Klik pada joda-timeseperti yang terlihat di gambar berikut ini:

    Dependency Maven untuk joda-time

    Dependency Maven untuk joda-time

  14. Klik tombol OK.
  15. Klik kanan pada nama proyek, pilih Maven, Update Dependencies… dan klik pada tombol OK. Beri tanda centang pada Force Update of Snapshots/Releases.  Jangan lupa menghubungkan komputer ke internet agar Maven dapat men-download JAR yang dibutuhkan.  Hibernate Validator dan Joda Time adalah dua framework terpisah yang bukan merupakan bagian dari Spring Framework.

Sekarang, kembali ke class Mahasiswa.java, saya akan mengubah isi file tersebut menjadi:

package co.id.jocki.mahasiswa;

import java.util.Date;

import javax.validation.constraints.Past;
import javax.validation.constraints.Size;

import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.format.annotation.DateTimeFormat;

public class Mahasiswa {

    @Size(min=8, max=8, message="NIM harus 8 digit!")
    private String nim;

    @NotEmpty(message="Nama harus di-isi!")
    private String nama;

    @Past(message="Tanggal harus berada di masa lalu!")    
    @DateTimeFormat(pattern="dd/MM/YYYY")    
    private Date tanggalLahir;

    public String getNim() {
        return nim;
    }
    public void setNim(String nim) {
        this.nim = nim;
    }
    public String getNama() {
        return nama;
    }
    public void setNama(String nama) {
        this.nama = nama;
    }
    public Date getTanggalLahir() {
        return tanggalLahir;
    }
    public void setTanggalLahir(Date tanggalLahir) {
        this.tanggalLahir = tanggalLahir;
    }   
}

Pada kode program di atas, saya memberikan validasi @Size pada atribut nim dimana panjang nim harus 8 karakter.  Pesan yang ada di message akan ditampilkan di view bila nilai tidak memenuhi persyaratan.  Saya juga memberikan validasi @NotEmpty untuk atribute nama, sehingga nantinya nama wajib di-isi.  Saya memberikan validasi @Past di tanggalLahir untuk memastikan bahwa tanggalLahir yang dimasukkan oleh pengguna tidak melewati batas hari ini (karena tidak mungkin ada mahasiswa yang tanggal lahirnya tahun depan tetapi sudah duluan mendaftar).

Membuat View

Sekarang, saya akan membuat View.  Pada src/main/webapp/WEB-INF/views, saya men-klik kanan folder views dan memilih New, Others.  Pada dialog yang muncul, saya memilih Web, JSP File, kemudian men-klik tombol Next.  Pada File name, saya mengisi dengan nama tambahMahasiswa.jsp.  Setelah itu, saya men-klik tombol Finish.  Struktur proyek akan terlihat seperti pada gambar berikut ini:

Menambah view baru

Menambah view baru

Berikut ini adalah isi file tambahMahasiswa.jsp:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>    
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<style type="text/css">
    .error {color: red; }    
</style>
<title>Tambah Mahasiswa Baru</title>
</head>
<body>
    <h1>Tambah Mahasiswa Baru</h1>    
    <form:form modelAttribute="mahasiswa" method="post">
        <fieldset>    
        <p>
            <form:label path="nim" for="nim" cssErrorClass="error">NIM:</form:label>
            <form:input path="nim" />
            <form:errors path="nim" cssClass="error"/>
        </p>

        <p>
            <form:label path="nama" for="nama" cssErrorClass="error">Nama:</form:label>
            <form:input path="nama" />
            <form:errors path="nama" cssClass="error"/>
        </p>

        <p>
            <form:label path="tanggalLahir" for="tanggalLahir" cssErrorClass="error">Tanggal Lahir:</form:label>
            <form:input path="tanggalLahir"/>
            <form:errors path="tanggalLahir" cssClass="error"/>
        </p>
        <p>
            <input type="submit"/>
        </p>        
        </fieldset>
    </form:form>
</body>
</html>

Pada halaman JSP tersebut, saya memakai tag library yang disediakan oleh Spring Framework dengan memakai directive taglib.  Spring MVC menyediakan tag seperti <form:form>, <form:label>, <form:input> dan <form:errors> untuk mempermudah data-binding dengan model.  Pada JSP tersebut, saya melakukan binding dengan model Mahasiswa.  Nilai path di <form:label>, <form:input> dan <form:errors> harus sesuai dengan nama atribut/variabel di model. Pada <form:label> saya menambahkan cssErrorClass sehingga bila terjadi kesalahan validasi, maka class tersebut akan dipakai (pada contoh ini, mengubah warna tulisan menjadi merah).

Kemudian, dengan mengikuti langkah yang sama dengan yang diatas, saya membuat sebuah view baru dengan nama lihatMahasiswa.jsp.  Isi dari file lihatMahasiswa.jsp adalah:

<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
    pageEncoding="ISO-8859-1"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>   
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<style type="text/css">
    table {margin-bottom: 20px; border: solid 1px black;}
    th {text-align: left; padding-right: 20px; border-bottom: solid 2px black;}
    td {padding-right: 20px;}    
</style>
<title>Lihat Daftar Mahasiswa</title>
</head>
<body>
<h1>Daftar Mahasiswa</h1>
<table>
<thead>
    <tr><th>NIM</th><th>Nama</th><th>Tanggal Lahir</th></tr>
</thead>
<c:forEach items="${lstMahasiswa}" var="mahasiswa">
    <tr>
    <td>${mahasiswa.nim}</td>
    <td>${mahasiswa.nama}</td>
    <td><fmt:formatDate value="${mahasiswa.tanggalLahir}" pattern="dd/MM/yyyy"/></td>
    </tr>
</c:forEach>
</table>
<a href="mahasiswa">Tambah Mahasiswa Baru</a>
</body>
</html>

Pada halaman JSP ini, saya mengharapkan controller untuk mengirimkan sebuah List dengan nama lstMahasiswa.  List tersebut harus merupakan kumpulan dari class Mahasiswa (model yang saya pakai).  Lalu dengan menggunakan JSTL <c:forEach>, saya melakukan perulangan untuk menampilkan setiap class Mahasiswa sebagai baris di dalam tabel.  Saya juga menggunakan JSTL <fmt:formatDate> untuk men-format atribut tanggalLahir yang bertipe java.util.Date.

Membuat Controller

Berikutnya, saya membuat sebuah controller dengan nama MahasiswaController.java di package yang sama.  Caranya adalah dengan men-klik kanan di nama package co.id.jocki.mahasiswa, kemudian memilih menu New, Class.  Pada dialog New Java Class yang muncul, isi nama dengan MahasiswaController, kemudian klik tombol Finish.  Kemudian, saya membuat kode program seperti berikut ini untuk MahasiswaController.java:

package co.id.jocki.mahasiswa;

import java.util.ArrayList;
import java.util.List;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping(value="/mahasiswa")
public class MahasiswaController {

    private List<Mahasiswa> lstMahasiswa = new ArrayList<Mahasiswa>();

    @RequestMapping(method=RequestMethod.GET)
    public String getFormTambah(Model model) {
        model.addAttribute(new Mahasiswa());
        return "tambahMahasiswa";
    }    

    @RequestMapping(method=RequestMethod.POST)
    public String tambahMahasiswa(@Valid Mahasiswa mahasiswa, BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "tambahMahasiswa";
        }
        lstMahasiswa.add(mahasiswa);
        model.addAttribute("lstMahasiswa", lstMahasiswa);
        return "lihatMahasiswa";
    }
}

Untuk menjalankan proyek ini, klik pada icon Run As… dan pilih Run As, Run on Server.  Secara default, STS akan memakai server VMware vFabric tc Server Developer Edition v2.7 seperti yang terlihat digambar berikut ini:

Menjalankan Proyek

Menjalankan Proyek

Pada dasarnya tc Server adalah sebuah server Apache Tomcat yang dilengkapi fitur tambahan (dan dukungan komersial).  Pada kotak dialog yang muncul, beri centang pada Always use this server when running this project.  Klik tombol Next.  Pastikan bahwa latihan-mvc berada di daftar configured, bukan di available.  Setelah itu klik tombol Finish.

STS akan berusaha menjalankan tc Server sembari menampilkan informasi di window Console.  Bila tc Server sudah selesai dijalankan, akan terdapat baris dengan tulisan seperti:

INFO: Server startup in 2789 ms

Saya akan mulai dengan membuka browser dan memasukkan URL http://localhost:8080/latihan-mvc/mahasiswa.  Hal ini akan menyebabkan salah satu dari method di MahasiswaController dikerjakan, karena nilai value di @RequestMapping untuk MahasiswaController adalah /mahasiswa.

Karena saya memasukkan URL secara biasa dengan mengetik di browser, maka request method yang saya pakai adalah request method GET.  Dengan demikian, getFormTambah() di MahasiswaController akan dikerjakan.  getFormTambah() hanya membuat sebuah objek Mahasiswa baru, kemudian mengembalikan sebuah String tambahMahasiswa.  Hal ini akan menyebabkan view tambahMahasiswa.jsp ditampilkan di browser, seperti yang terlihat di gambar berikut ini:

View tambahMahasiswa

View tambahMahasiswa

Pada saat saya menekan tombol Submit Query, maka URL /mahasiswa akan kembali dipanggil, tapi kali ini dengan request method POST.  Hal ini menyebabkan method tambahMahasiswa() di MahasiswaController dikerjakan.  Saya menambahkan annotation @Valid pada model Mahasiswa sehingga Spring MVC akan melakukan validasi model yang dikirim oleh view secara otomatis seperti yang terlihat pada gambar berikut ini:

Validasi secara otomatis

Validasi secara otomatis

Bila tidak ada kesalahan, maka model yang dikirim oleh view akan disimpan ke dalam sebuah List.  Tentu saja, pada proyek nyata, model tidak disimpan ke dalam list melainkan kedalam sebuah database.  Setelah menyimpan model ke dalam List, tambahMahasiswa() akan memanggil view lihatMahasiswa.jsp sembari mengirimkan List berisi model Mahasiswa dengan nama atribut lstMahasiswa, sehingga tampilan yang diperoleh adalah seperti berikut ini:

Tampilan view lihatMahasiswa

Tampilan view lihatMahasiswa

Untuk melihat gambaran umum MVC di proyek secara garis besar, saya dapat membuka window Spring Explorer yang terlihat seperti pada gambar berikut ini:

Tampilan Spring Explorer

Tampilan Spring Explorer