Menambah atau mengubah endpoint yang dihasilkan Spring Data Rest

Pada artikel Membuat Restful Web Service Dengan Spring Data REST, saya menghasilkan REST API secara otomatis berdasarkan domain model dan Spring Data Repository. API yang dihasilkan pun telah mendukung HATEOAS dengan implementasi HAL. Dengan Spring Data REST, saya tidak perlu lagi menulis kode program controller dan Spring HATEOAS secara manual untuk setiap domain model yang ada.

Sebagai latihan, kali ini saya akan memakai Spring Data REST melalui Spring Boot. Pada saat membuat proyek baru, saya menambahkan dependency berupa Web, Rest Repositories dan JPA. Setelah proyek selesai dibuat, saya menambahkan entity baru seperti berikut ini:

package com.example.demo.domain;

import org.hibernate.validator.constraints.Email;
import org.hibernate.validator.constraints.NotBlank;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Pelanggan {

    @Id
    @GeneratedValue
    private Long id;

    @NotBlank @Email
    private String email;

    @NotBlank
    private String nama;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public String getNama() {
        return nama;
    }

    public void setNama(String nama) {
        this.nama = nama;
    }
}

Setelah itu, saya membuat sebuah repository untuk mengakses dan melakukan operasi CRUD terhadap Pelanggan seperti berikut ini:

package com.example.demo.repository;

import com.example.demo.domain.Pelanggan;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PelangganRepository extends JpaRepository<Pelanggan, Long> {
}

Sebagai langkah terakhir, saya akan menggunakan embedded database pada latihan kali ini. Untuk itu, saya menambahkan baris berikut ini pada file build.gradle:

...
dependencies {
  compile('com.h2database:h2:1.4.196')
  ...
}

Sekarang, bila saya menjalankan aplikasi Spring Boot ini, sesuai dengan spesifikasi HATEOAS, saya bisa mengakses http://localhost:8080/ untuk melihat daftar endpoints yang tersedia seperti yang terlihat pada gambar berikut ini:

Daftar endpoints yang tersedia

Daftar endpoints yang tersedia

Saya bisa melakukan operasi CRUD terhadap pelanggan melalui URI http://localhost:8080/pelanggans. Spring Data REST menggunakan nama plural (pelanggan menjadi pelanggans) sesuai spesifikasi REST, walaupun hal ini sebenarnya tidak relevan untuk Bahasa Indonesia. Selain itu, API yang dihasilkan juga mendukung operasi halaman (pagination) dan pengurutan (sorting).

Saya akan mencoba menyimpan pelanggan baru dengan memberikan request POST ke http://localhost:8080/pelanggans berisi JSON berikut ini (pastikan menyertakan Content-Type berupa application/json):

{
  "email": "phantom@diamondogs.pain",
  "nama": "Venom"
}

Saya akan memperoleh kembalian seperti pada gambar berikut ini:

JSON yang dikembalikan saat menyimpan pelanggan baru

JSON yang dikembalikan saat menyimpan pelanggan baru

Sesuai dengan spesifikasi HAL, JSON yang dikembalikan mengandung _links yang berisi referensi ke operasi berikutnya yang bisa dilakukan terhadap entity ini. Sampai disini, entity juga sudah tersimpan ke database.

Bayangkan bila saya harus melakukan langkah di atas secara manual. Saya perlu membuat sebuah controller baru. Selain itu, untuk tetap mendukung HATEOAS, saya perlu memakai Spring HATEOAS dan mendaftarkan _links secara manual. Cukup merepotkan, bukan?

Walaupung Spring Data REST sangat membantu, ada kalanya saya perlu menyediakan operasi lain selain CRUD yang belum disediakan oleh Spring Data REST secara otomatis. Selain itu saya juga mungkin perlu memodifikasi operasi yang tersedia. Sebagai contoh, saya umumnya perlu mengirim email ke pelanggan setelah menyimpannya ke database. Beruntungnya, Spring Data REST tetap memungkinkan saya untuk mengubah operasi yang sudah ada.

Untuk itu, saya perlu membuat controller baru seperti berikut ini:

package com.example.demo.web;

import com.example.demo.domain.Pelanggan;
import com.example.demo.repository.PelangganRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.PersistentEntityResource;
import org.springframework.data.rest.webmvc.PersistentEntityResourceAssembler;
import org.springframework.data.rest.webmvc.RepositoryRestController;
import org.springframework.hateoas.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.mail.Address;
import javax.mail.internet.MimeMessage;

@RepositoryRestController
public class PelangganController {

    private final PelangganRepository pelangganRepository;
    private final JavaMailSender mailSender;

    @Autowired
    public PelangganController(PelangganRepository pelangganRepository, JavaMailSender mailSender) {
        this.pelangganRepository = pelangganRepository;
        this.mailSender = mailSender;
    }

    @RequestMapping(method = RequestMethod.POST, value = "/pelanggans")
    public @ResponseBody ResponseEntity<?> save(@RequestBody Resource<Pelanggan> pelangganResource, PersistentEntityResourceAssembler assembler) {
        // Menyimpan pelanggan
        Pelanggan pelanggan = pelangganResource.getContent();
        pelanggan = pelangganRepository.save(pelanggan);

        // Mengirim email
        SimpleMailMessage email = new SimpleMailMessage();
        email.setTo(pelanggan.getEmail());
        email.setSubject("Selamat datang");
        email.setText(String.format("Hi, selamatt datang, %s", pelanggan.getNama()));
        mailSender.send(email);

        // Mempersiapkan kembalian
        PersistentEntityResource resource = assembler.toResource(pelanggan);
        return ResponseEntity.ok(resource);
    }
}

Kali ini, controller yang saya buat tidak menggunakan annotation @Controller melainkan @RepositoryRestController yang menandakan bahwa saya ingin memodifikasi hasil dari Spring Data REST. Method di controller saya akan menerima request dalam bentuk Resource dan juga mengembalikan resource agar mendukung HATEOAS. Agar tidak repot menghasilkan _links dengan Spring HATEOAS, saya cukup menggunakan PersistentEntityResourceAssembler untuk menghasilkan resource yang sama persis seperti yang dihasilkan oleh Spring Data REST sebelum dimodifikasi.

Iklan

Belajar Memakai Specification Di Spring Data JPA

Pada artikel Memakai Querydsl Dengan Spring Data JPA, saya menunjukkan bagaimana memakai Querydsl di Spring Data JPA. Pada kesempatan kali ini, saya akan mencoba membuat query dinamis dengan menggunakan fasilitas bawaan Spring Data JPA yaitu Specification. Karena interface ini merupakan bawaan dari Spring Data JPA, saya tidak perlu lagi menambahkan referensi ke Jar eksternal.

Mengapa perlu membuat query secara dinamis? Sebagai contoh, saya memiliki sebuah tabel PrimeFaces di facelet seperti berikut ini:

<p:dataTable var="vProduk" value="#{entityList}" selection="#{entityList.selected}" selectionMode="single" rowKey="#{vProduk.id}" 
    paginator="true" rows="10" id="tabelProduk" lazy="true" filterDelay="1000" resizableColumns="true">
    <p:column headerText="Kode" sortBy="#{vProduk.kode}" filterBy="#{vProduk.kode}">
        <h:outputText value="#{vProduk.kode}" />
    </p:column>
    <p:column headerText="Nama" sortBy="#{vProduk.nama}" filterBy="#{vProduk.nama}">
    <h:outputText value="#{vProduk.nama}" />
    </p:column>
    <p:column headerText="Jenis" sortBy="#{vProduk.jenis.nama}" filterBy="#{vProduk.jenis}" filterOptions="#{filterJenisProduk}" width="150">              
        <h:outputText value="#{vProduk.jenis.nama}" />
    </p:column>
    <p:column headerText="Harga (Rp)" sortBy="#{vProduk.harga}" filterBy="#{vProduk.harga}">
        <h:outputText value="#{vProduk.harga}" style="float: right">
            <f:convertNumber type="number" />
        </h:outputText>
    </p:column>
    <p:column headerText="Qty" sortBy="#{vProduk.qty}" filterBy="#{vProduk.qty}" width="100">
        <h:outputText value="#{vProduk.qty}" style="float: right">
            <f:convertNumber type="number" />
        </h:outputText>
    </p:column>               
    <p:ajax event="rowSelect" update=":mainForm:buttonPanel" />               
</p:dataTable>

Tampilan HTML untuk tabel di atas akan terlihat seperti pada gambar berikut ini:

Tabel dengan fasilitas filtering per kolom

Tabel dengan fasilitas filtering per kolom

Pada tampilan di atas, pengguna bisa melakukan pencarian secara bebas di setiap kolom. Sebagai contoh, pengguna bisa mencari berdasarkan kode dan jenis. Selain itu, pengguna juga bisa mencari berdasarkan kode dan jenis dan harga. Atau, pengguna juga bisa memilih mencari berdasarkan jenis dan harga. Kombinasi seperti ini akan sangat banyak bila ingin dibuat query-nya secara manual satu per satu. Ini adalah contoh kasus yang tepat untuk memakai fasilitas seperti Querydsl dan Specification.

Karena filter table seperti ini sering digunakan pada tabel yang berbeda, saya akan mulai dengan membuat sebuah implementasi Specification yang bisa dipakai ulang seperti pada kode program berikut ini:

public class FilterSpecification<T> implements Specification<T> {

  public enum Operation { EQ, LIKE, MIN };

  private List<Filter> filters = new ArrayList<>();

  public void and(String name, Operation op, Object...args) {
    filters.add(new Filter(name, op, args));
  }

  public void and(Filter filter) {
    filters.add(filter);
  }

  @Override
  public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {              
    Predicate result = cb.conjunction();
    for (Filter filter: filters) {
      result = cb.and(result, filter.toPredicate(cb, root));            
    }
    return result;      
  }

  class Filter {

    private String name;
    private Operation op;
    private Object[] args;

    public Filter(String name, Operation op, Object... args) {
      this.name = name;
      this.op = op;
      this.args = args;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Predicate toPredicate(CriteriaBuilder cb, Root<T> root) {
      Expression path = toPath(root, name);
      switch (op) {
      case EQ:
        return cb.equal(path, args[0]);
      case LIKE:                                
        return cb.like(path, (String) args[0]);
      case MIN: 
        return cb.greaterThanOrEqualTo(path, Integer.parseInt((String) args[0]));
      default:
        return null;
      }
    }

    @SuppressWarnings("rawtypes")
    public Expression toPath(Root root, String name) {
      Path result = null;
      if (name.contains("_")) {
        for (String node: name.split("_")) {
          node = StringUtils.uncapitalize(node);                    
          if (result == null) {
            result = root.get(node);
          } else {
            result = result.get(node);
          }
        }
      } else {
        result = root.get(name);
      }
      return (Expression) result;
    }

  }     

}

Setiap kali pengguna mengetik (atau memilih) kriteria filter di tabel, setelah delay selama 1000 ms, method load() di LazyDataModel akan dipanggil. Method ini perlu melakukan query di database untuk memperoleh hasil sesuai kriteria yang diberikan. Untuk itu, saya bisa menggunakan FilterSpecification yang saya buat seperti pada kode program berikut ini:

FilterSpecification<Produk> filterSpecification = new FilterSpecification<>();
if (getFilter("kode") != null) {
  filterSpecification.and("kode", Operation.LIKE, getFilterAsSearchExpr("kode"));
}
if (getFilter("nama") != null) {
  filterSpecification.and("nama", Operation.LIKE, getFilterAsSearchExpr("nama"));
}
if (getFilter("jenis") != null) {
  filterSpecification.and("jenis_id", Operation.EQ, getFilter("jenis"));
}
if (getFilter("harga") != null) {
  filterSpecification.and("harga", Operation.MIN, (String) getFilter("harga"));
}
if (getFilter("qty") != null) {
  filterSpecification.and("qty", Operation.MIN, (String) getFilter("qty"));
}

Method and() akan mendaftarkan sebuah Filter baru yang berisi nama path, operasi dan nilai pencarian. Untuk mengakses path di dalam path lain, saya menggunakan tanda garis bawah. Kode program yang memungkinkan hal ini terjadi dapat dilihat di method FilterSpecification.toPath().

Method toPredicate() hanya akan dipanggil pada saat Specification ini dipakai oleh salah satu method di repository Spring Data JPA. Agar bisa memakai Specification di repository Spring Data JPA, saya perlu menurunkan interface repository dari JpaSpecificationExecutor seperti yang terlihat pada kode program berikut ini:

public interface ProdukRepository extends JpaRepository<Produk, Long>, JpaSpecificationExecutor<Produk> {}

Sekarang, saya dapat memakai salah satu method yang mendukung Specification di ProdukRepository. Sebagai contoh, saya bisa melakukan filtering dengan menggunakan Specification sekaligus memperoleh hasil per halaman dengan Pageable dengan memanggil repository seperti berikut ini:

produkRepository.findAll(filterSpecification, pageable);

Dengan kode program yang sederhana dan mudah dipahami ini, saya kini bisa melakukan pencarian untuk kombinasi kolom berbeda. Sebagai contoh, bila saya mencari nama produk yang mengandung kata RING KEH dengan jenis HGP dan harga minimal Rp 100.000, saya akan memperoleh hasil seperti pada gambar berikut ini:

Melakukan filtering pada tabel

Melakukan filtering pada tabel

Belajar Membuat Aplikasi Web Dengan Spring Framework

Banyak sekali pemula yang kewalahan dalam mempelajari pemograman web di Java karena kewalahan dengan sekian banyak pilihan yang ada. Walaupun Java EE 7 kini sudah semakin mirip dengan Spring, untuk saat ini ia masih perlu banyak belajar lagi agar semakin dewasa. Saat ini tidak semua server Java EE 7 memiliki perilaku yang sama. Sebagai contoh, unit testing yang melibatkan CDI dan EJB begitu mudah dilakukan di server GlassFish karena ia mendukung EJBContainer untuk menjalankan embeddable container. Server WildFly tampaknya tidak menganut filosofi serupa dan lebih menyarankan penggunaan Arquillian untuk keperluan pengujian. Masalah perbedaan akan lebih sering muncul bila memakai Java EE API yang jarang dibahas seperti JMS.

Bila Java EE 7 adalah jalur resmi, maka teknologi dari Spring adalah jalur tidak resmi yang masih tetap bertahan sampai sekarang. Boleh dibilang CDI dari Java EE 7 menawarkan banyak fasilitas yang bersaing dengan Spring Container. Lalu kenapa masih memakai Spring? Karena Spring memiliki ekosistem yang luas dan dewasa. Memakai Spring ibarat masuk ke sebuah jeratan dunia baru yang beda sendiri. Spring dapat dijalankan pada server apa saja asalkan mendukung servlet (misalnya server Tomcat yang lawas).

Pada kesempatan ini, saya akan membuat sebuah aplikasi web sederhana dengan menggunakan Spring dan beberapa API dari Java EE 7 yang di-deploy pada server Tomcat. Aplikasi ini tidak membutuhkan server khusus untuk Java EE 7. Kode program lengkap untuk aplikasi latihan ini dapat dijumpai di https://github.com/JockiHendry/basic-inventory-web. Hasil akhirnya dapat dijumpai di https://basicinventory-latihanjocki.rhcloud.com/app/home.

Aplikasi ini memakai Gradle untuk mengelola proses siklus hidup seperti building. Sebagai contoh, bila saya memberikan perintah gradle build, sebuah file bernama ROOT.war akan dihasilkan untuk di-deploy pada servlet container seperti Tomcat. Sebagai informasi, tidak seperti di PHP yang berbasis file, sebuah aplikasi web di Java berada dalam bentuk file tunggal yang berakhir *.war (web archive). File ini adalah sebuah file ZIP yang berisi seluruh file yang dibutuhkan untuk menjalankan aplikasi dan juga metadata seperti nama dan versi aplikasi. Gradle juga secara otomatis men-download JAR yang dibutuhkan. Pada latihan ini, seluruh JAR yang saya pakai dapat dilihat di build.gradle di bagian dependencies.

Di dunia ini tidak ada teknologi yang sempurna! Suatu hari nanti, saya mungkin perlu mengganti salah satu teknologi yang dipakai dengan yang berbeda. Saya mungkin suatu hari ini ingin beralih dari JSF ke GWT. Atau, mungkin saya ingin berpindah dari database relational menjadi database NoSQL seperti MongoDB. Tentu saja saya tidak ingin sampai harus membuat ulang aplikasi dari awal. Untuk itu, saya berjaga-jaga dengan menggunakan arsitektur layering dimana saya mengelompokkan kode program ke dalam layer yang berbeda sesuai dengan perannya. Sebagai contoh, saya membuat package seperti pada gambar berikut ini:

Struktur package di aplikasi web

Struktur package di aplikasi web

Seluruh kode program yang berkaitan dengan presentation layer terletak di package com.jocki.inventory.view. Sementara itu, seluruh kode program yang berkaitan dengan service layer berada di package com.jocki.inventory.service. Sisanya, kode program yang berkaitan dengan persistence layer (mengakses database secara langsung) berada di package com.jocki.inventory.repository.

Salah satu syarat dalam layering adalah layer yang satu hanya bisa mengakses layer sesudahnya atau sebelumnya. Pada contoh ini, presentation layer hanya bisa mengakses service layer secara langsung. Setelah itu, service layer mengakses persistence layer guna membaca data dari database. Walaupun demikian, saya juga tidak begitu kaku. Misalnya, karena memakai Spring Data JPA pada persistence layer, saya mengizinkan presentation layer untuk memanggil finders secara langsung dari persistence layer tanpa melalui service layer.

Package com.jocki.inventory.domain berisi domain class yang bisa diakses oleh layer mana saja. Isi dari package ini adalah JPA entity yang mewakili domain, misalnya Konsumen, Supplier dan Sales. Karena saya ingin mengakses domain class secara langsung termasuk di presentation layer, ada baiknya saya membuat seluruh domain class menjadi Serializable.

Sama seperti domain class, masalah validasi adalah persoalan cross cutting yang dibutuhkan semua layer. Saya menyerahkan validasi pada Bean Validation yang menggunakan annotation seperti @NotBlank, @Size dan @NotNull pada domain class. Dengan demikian, validasi yang sama dan konsisten dapat diterapkan mulai dari presentation layer hingga persistence layer. Validasi ini juga dipakai oleh JPA untuk menghasilkan tabel lengkap dengan konstrain sehingga data yang tidak valid tidak bisa disimpan di database.

Bahkan, berkat PrimeFaces yang menyediakan komponen tambahan bagi JSF, validasi ini juga akan dikerjakan oleh kode program JavaScript sebelum nilai dikirim ke server. PrimeFaces memungkinkan validasi client side bila saya menambahkan atribut validateClient="true" pada <p:commandButton>. Hasil validasi client side akan terlihat seperti pada gambar berikut ini:

Validasi secara otomatis

Validasi secara otomatis

Untuk menampilkan pesan kesalahan, saya menggunakan <p:message>. Agar label untuk setiap pesan kesalahan juga ikut menjadi berwarna merah, saya tidak menggunakan <h:outputLabel> biasa melainkan <p:outputLabel>. Secara default, pesan kesalahan berada dalam bahasa Inggris. Karena validasi dilakukan di sisi klien, pesan kesalahan di ValidationMessage.properties tidak dapat dipakai. Oleh sebab itu, saya perlu membuat file JavaScript yang berisi pesan kesalahan dalam bahasa Indonesia seperti:

PrimeFaces.locales['en_US'] = {
  ...
  messages: {
    ...
    "javax.validation.constraints.NotNull.message": "tidak boleh kosong",
    "javax.validation.constraints.Size.message": "harus diantara {0} dan {1}",
    "javax.faces.validator.BeanValidator.MESSAGE": "{0}"
    ...
  }
}

Package com.jocki.inventory.config berisi konfigurasi aplikasi web. Salah satu pergerakan yang terlihat di Spring adalah peralihan konfigurasi berbasis XML menjadi konfigurasi programmatis melalui annotation. Bukan hanya Spring, tapi juga di Java. Sejak Servlet 3.0 (bagian dari JEE 6), file konfigurasi web.xml tidak wajib ada. Sebagai informasi, dulunya, setiap servlet wajib didaftarkan di web.xml. Sebagai gantinya, developer bisa membuat file javax.servlet.ServletContainerInitializer di folder META-INF/services untuk memberikan servlet container agar mengerjakan sebuah class untuk mencari definisi servlet dan sebagainya. Beruntungnya, semua ini sudah diurus oleh Spring WebMVC sehingga saya tinggal membuat turunan AbstractAnnotationConfigDispatcherServletInitializer seperti berikut ini:

public class DispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

  @Override
  protected Class<?>[] getRootConfigClasses() {
    return new Class[] { RootConfig.class, WebMvcConfig.class, WebFlowConfig.class };
  }

  @Override
  protected Class<?>[] getServletConfigClasses() {
    return null;
  }

  @Override
  protected String[] getServletMappings() {
    return new String[] { "/app/*" };
  }

  @Override
  public void onStartup(ServletContext servletContext) throws ServletException {
    servletContext.setInitParameter("javax.faces.DEFAULT_SUFFIX", ".xhtml");
    servletContext.setInitParameter("primefaces.FONT_AWESOME", "true");
    servletContext.setInitParameter("primefaces.CLIENT_SIDE_VALIDATION", "true");
    // Tambahkan konfigurasi development bila sedang tidak di-deploy di OpenShift
    if (System.getenv("OPENSHIFT_APP_NAME") == null) {
      servletContext.setInitParameter("javax.faces.PROJECT_STAGE", "Development");
      servletContext.setInitParameter("javax.faces.FACELETS_REFRESH_PERIOD", "1");
    } else {
      servletContext.setInitParameter("javax.faces.PROJECT_STAGE", "Production");
    }
    servletContext.addListener(ConfigureListener.class);
    super.onStartup(servletContext);
  }

}

Pada class di atas, saya menginstruksikan agar Spring selanjutnya membaca konfigurasi yang ada di class RootConfig, WebMvcConfig dan WebFlowConfig. Karena getServletMappings() mengembalikan /app/*, maka URL untuk aplikasi ini harus memiliki pola serupa seperti http://localhost/app/konsumen. Saya juga melakukan konfigurasi JSF pada class ini.

Aplikasi web biasanya membutuhkan state padahal protokol HTTP yang dipakai oleh website bersifat stateless. Oleh sebab itu, saya menggunakan Spring Web Flow (SWF) untuk memuluskan transisi antar halaman. SWF juga secara otomatis menerapkan POST-REDIRECT-GET sehingga tombol Back di browser dapat digunakan secara baik dan aman. Sebenarnya JSF 2.2 telah dilengkapi dengan dukungan deklarasi flow seperti pada SWF. Walaupun demikian, karena ini adalah fitur baru, saya merasa flow pada JSF masih terbatas sehingga saya memutuskan untuk tetap menggunakan SWF. Konfigurasi SWF dapat dilihat pada class WebFlowConfig yang merupakan turunan dari AbstractFacesFlowConfiguration. Class ini secara otomatis mendeklarasi mapping untuk akses ke URL seperti /javax.faces.resources/** untuk keperluan JSF.

Sebagai informasi, JavaServer Faces (JSF) adalah bagian dari Java EE yang memungkinkan developer memakai facelet dalam bentuk tag XML untuk menghasilkan komponen UI. Tujuannya adalah agar developer tidak perlu berhubungan langsung dengan semua operasi tingkat rendah yang melibatkan JavaScript, Ajax dan CSS styling. Bila komponen UI yang disediakan oleh JSF tidak cukup, pengguna bisa membuat komponen baru sendiri. Atau, pengguna juga dapat memakai komponen dari pihak ketiga seperti PrimeFaces, RichFaces, ICEfaces dan sebagainya.

Pengguna yang terbiasa membuat situs dengan PHP yang berorientasi file mungkin akan bertanya dimana sesungguhnya lokasi file dan bagaimana pemetaan file pada aplikasi web di Java. Berbeda jauh dari situs berorientasi file, sebuah servlet Java akan menerima input berupa URL tujuan lalu mengembalikan output sesuai dengan URL tersebut. Yup! Apapun URL-nya hanya akan ditangani oleh 1 servlet. Pada contoh di atas, saya secara tidak langsung hanya mendaftarkan org.springframework.web.servlet.DispatcherServlet dari Spring dan javax.faces.webapp.FacesServlet (dari Mojarra, implementasi JSF yang saya pakai). Setiap request URL akan ditangani oleh salah satu servlet di atas, lebih tepatnya adalah oleh DispatcherServlet milik Spring karena FacesServlet hanya dibutuhkan untuk inisialisasi JSF. Method yang menangani request akan terlihat seperti void service(HttpServletRequest request, HttpServletResponse response). Apa yang dikirim oleh pengguna mulai dari URL, parameter hingga cookie bisa dibaca melalui request. Untuk menulis hasil yang dikirim kepada pengguna, baik HTML, gambar, JSON, dan sebagainya, cukup tulis byte per byte melalui response.

Terlihat rumit? Saya tidak perlu melakukan pemograman tingkat rendah sampai ke servlet karena semua ini sudah ditangani oleh Spring WebMVC. Pada konfigurasi WebMvcConfig, saya membuat kode program seperti berikut ini:

@Configuration
@EnableWebMvc
@ComponentScan
public class WebMvcConfig extends WebMvcConfigurerAdapter {

  @Inject
  private WebFlowConfig webFlowConfig;

  @Override
  public void addViewControllers(ViewControllerRegistry registry) {
    registry.addViewController("/error").setViewName("error");
  }

  @Bean
  public UrlBasedViewResolver faceletsViewResolver() {
    UrlBasedViewResolver resolver = new UrlBasedViewResolver();
    resolver.setViewClass(JsfView.class);
    resolver.setPrefix("/WEB-INF/views/");
    resolver.setSuffix(".xhtml");
    return resolver;
  }

  ...
}

Pada konfigurasi di atas, saya memetakan URL /error dengan sebuah view bernama error di addViewControllers(). Dengan demikian, bila pengguna membuka URL seperti http://localhost/app/error, maka view dengan nama error akan ditampilkan. UrlBasedViewResolver yang saya pakai akan mencari view error di lokasi /WEB-INF/views/error.xhtml yang harus dalam bentuk facelet (JSF). Bila facelet tidak ditemukan, maka Spring mencari file dengan nama yang sama persis di src/main/webapp. Bila file tidak juga ditemukan, pesan kesalahan 404 akan diberikan pada pengguna. Pengguna tidak akan pernah bisa mengakses isi folder WEB-INF secara langsung walaupun folder ini terletak di dalam src/main/webapp.

Konfigurasi pada class RootConfig berisi hal diluar presentation layer seperti berikut ini:

@Configuration
@EnableJpaRepositories(basePackages="com.jocki.inventory.repository")
@EnableTransactionManagement
@ComponentScan("com.jocki.inventory")
public class RootConfig {

  @Bean
  public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws NamingException {
    LocalContainerEntityManagerFactoryBean emf = new LocalContainerEntityManagerFactoryBean();
    emf.setDataSource(dataSource());
    emf.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
    emf.setPackagesToScan("com.jocki.inventory.domain");

    Map<String,? super Object> jpaProperties = new HashMap<>();
    jpaProperties.put("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect");
    emf.setJpaPropertyMap(jpaProperties);

    return emf;
  }

  @Bean
  public DataSource dataSource() throws NamingException {
    JndiTemplate jndiTemplate = new JndiTemplate();
    DataSource dataSource = (DataSource) jndiTemplate.lookup("java:comp/env/jdbc/inventory");
    return dataSource;
  }

  @Bean
  public JpaTransactionManager transactionManager(EntityManagerFactory emf) {
    JpaTransactionManager transactionManager = new JpaTransactionManager();
    transactionManager.setEntityManagerFactory(emf);
    return transactionManager;
  }

}

Pada konfigurasi di atas, saya melakukan pengaturan JPA dan data source yang dipakai. Saya menambahkan annotation @EnableTransactionManagement sehingga transaksi pada service layer dapat diaktifkan. Selain itu, saya menambahkan @EnableJpaRepositories agar Spring Data JPA bisa secara otomatis menghasilkan implementasi dari interface repository yang ada. Saya juga menggunakan JNDI sehingga saya tidak perlu mendeklarasikan lokasi, nama pengguna dan password untuk database secara langsung di dalam kode program. Sebagai gantinya, server yang menjalankan aplikasi ini harus mendeklarasikan resource JNDI dengan nama jdbc/inventory yang berisi DataSource yang mewakili database yang hendak dipakai. Pada Tomcat, deklarasi ini dapat dilakukan dengan menambahkan baris seperti berikut ini pada file context.xml di folder conf:

<Resource auth="Container" driverClassName="com.mysql.jdbc.Driver" name="jdbc/inventory"
password="12345" type="javax.sql.DataSource" url="jdbc:mysql://localhost/latihandb"
username="namauser" validationQuery="/* ping */ SELECT 1"/>

Pada server percobaan, saya membuat resource jdbc/inventory yang merujuk pada database lokal. Untuk live server, tentunya resource ini harus merujuk pada database di live server. Peralihan ke database yang berbeda ini dapat berlangsung secara transparan tanpa harus mengubah kode program karena informasi koneksi database tidak tersimpan di dalam aplikasi.

Walaupun idealnya konfigurasi web.xml bisa dibuang sepenuhnya, untuk saat ini, saya masih tidak bisa lepas dari web.xml. Sebagai contoh, deklarasi halaman kesalahanan dan referensi JNDI tetap dibutuhkan pada web.xml:

<error-page>
  <location>/app/error</location>
</error-page>
<resource-ref>
  <description>Default Datasource</description>
  <res-ref-name>jdbc/inventory</res-ref-name>
  <res-type>javax.sql.DataSource</res-type>
  <res-auth>Container</res-auth>
</resource-ref>

Pada deklarasi di atas, bila terjadi kesalahan pada aplikasi web, maka URL /app/error akan ditampilkan. Sejak Servlet 3.0, saya bisa menggunakan 1 halaman error generik untuk seluruh jenis kesalahan yang ada. Sesuai dengan konfigurasi yang saya lakukan pada WebMvcConfig, URL ini akan menampilkan facelet yang terletak di src/main/webapp/WEB-INF/views/error.xhtml. Pada facelet ini, saya bisa mengakses informasi lebih lengkap mengenai kesalahan yang terjadi melalui EL seperti #{request.getAttribute('javax.servlet.error.message')} atau #{request.getAttribute('javax.servlet.error.status_code')}.

Sebuah halaman web biasanya memiliki template yang konsisten, misalnya mengandung bagian header dan footer. Oleh sebab itu, daripada mengetik ulang bagian yang sama berkali-kali (dan mengubahnya berkali-kali bila salah!), saya akan menggunakan fasilitas template dari JSF. Untuk itu, saya mendeklarasikan sebuah template di src/main/webapp/WEB-INF/layouts/standard.xml yang isinya seperti berikut ini:

<html ...>
<f:view contentType="text/html">
    <h:head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> 
        <title>Basic Inventory Web Demo</title>
        <h:outputStylesheet name="css/basic.css" /> 
        <h:outputScript name="js/locales.js" /> 
        <ui:insert name="headIncludes" />   
    </h:head>
    <h:body>
        <p:layout fullPage="true" style="border: 0px;">
            <p:layoutUnit position="west" resizable="true" collapsible="true" styleClass="main-menu">
                ...
            </p:layoutUnit>
            <p:layoutUnit position="center" styleClass="main-panel">
                <h:form id="mainForm">
                    <ui:insert name="content" />
                </h:form>
            </p:layoutUnit>
            <p:layoutUnit position="south" styleClass="main-panel">
                ...
            </p:layoutUnit>                       
        </p:layout>               
    </h:body>
</f:view>
<p:ajaxStatus onerror="PF('dialogKesalahan').show();"/>                                                   
<p:dialog header="Terjadi Kesalahan" id="dialogKesalahan" widgetVar="dialogKesalahan" modal="true" resizable="false">   
    <p>Telah terjadi kesalahan saat berkomunikasi dengan server.</p>
    <p>Silahkan perbaharui halaman atau ulangi beberapa saat kemudian!</p>
</p:dialog>
...
</html>

Pada facelet di atas, selain layout, saya juga menambahkan p:ajaxStatus secara global untuk setiap halaman yang ada sehingga pesan kesalahan akan selalu muncul bila terjadi kesalahan Ajax pada halaman mana saja seperti yang terlihat pada gambar berikut ini:

Tampilan pesan kesalahan pada Ajax

Tampilan pesan kesalahan pada Ajax

Salah satu kasus dimana pesan kesalahan ini akan selalu muncul adalah bila pengguna membuka sebuah halaman JSF dan meninggalkannya untuk jangka waktu yang lama. JSF menyimpan informasi komponen view yang dihasilkannya ke dalam session. Secara default, session di Tomcat diakhiri bila tidak ada aktifitas dari pengguna setelah 30 menit. Hal ini perlu dilakukan untuk mengirit memori. Bila pengguna mencoba memakai komponen JSF setelah session berakhir, ia akan menemukan setiap tombol tidak akan bekerja. Solusi untuk masalah ini cukup sederhana: pengguna harus me-refresh halaman untuk memulai session baru. PrimeFaces 5 sebenarnya memiliki komponen <p:ajaxExceptionHandler> untuk menghasilkan informasi kesalahan yang lebih jelas tetapi ia membutuhkan deklarasi halaman kesalahan secara eksplisit di web.xml.

File yang memakai template perlu menggunakan ui:composition dan mengisi nilai atribut template sesuai dengan lokasi file template yang hendak dipakai. Selain itu, pengguna template juga perlu menggunakan ui:define untuk menyisipkan nilai pada ui:insert di template. Sebagai contoh, facelet untuk pesan kesalahan akan terlihat seperti berikut ini:

<ui:composition ...  template="/WEB-INF/layouts/standard.xhtml">
    <ui:define name="content">
        <div class="ui-message-error ui-widget ui-corner-all">          
            <div class="ui-message-error-detail">
                <p><strong>Telah terjadi kesalahan saat menampilkan halaman ini!</strong></p>               
                <p>Status kesalahan: <strong><h:outputText value="#{request.getAttribute('javax.servlet.error.status_code')}"/></strong></p>
                ...
            </div>
        </div>
    </ui:define>
</ui:composition>

Saya mendeklarasikan setiap flow SWF dalam folder masing-masing di src/main/webapp/WEB-INF/flows. Sebagai contoh, URL seperti app/konsumen akan mengakses flow yang didefinisikan di src/main/webapp/WEB-INF/flows/konsumen/konsumen-flow.xml. SWF mendukung flow inheritance sehingga saya dapat memakai ulang pola flow yang sama berkali-kali. Sebagai contoh, semua flow untuk operasi CRUD diturunkan dari src/main/webapp/WEB-INF/flows/crud/crud-flow.xml yang memiliki aliran seperti:

Contoh flow di SWF

Contoh flow di SWF

Selain flow inheritance, SWF juga mendukung embedded flow. Ini adalah solusi yang tepat untuk Ajax. Sebagai contoh, pada flow di atas, bila pengguna melakukan aksi edit atau tambah, maka flow src/main/webapp/WEB-INF/flows/crud/entry/entry-flow.xml akan dikerjakan:

<flow ... abstract="true">

    <input name="selectedEntity" />    

    <view-state id="entry">
        <on-render>
            <set name="viewScope.updateMode" value="(selectedEntity != null) and (selectedEntity.id != null)" />
        </on-render>  
        <transition on="simpan" />
        <transition on="kembali" to="kembali" bind="false" validate="false" />
        <transition on="update" to="updateAction" />
    </view-state>     

    <action-state id="updateAction">                                        
        <transition on="success" to="kembali" />
        <transition on="error" to="entry" />  
    </action-state>

    <end-state id="kembali" />

</flow>

Karena saya menambahkan atribut abstract dengan nilai true, flow di atas tidak akan pernah bisa dipanggil secara langsung dengan URL seperti /app/crud/entry. Bila dipakai sebagai embedded flow, setiap perpindahan view-state pada flow di atas tidak akan menyebabkan request halaman baru (tidak ada POST-REDIRECT-GET) sehingga semuanya bisa berlangsung tanpa merefresh halaman secara penuh.

Contoh flow yang mengimplementasikan crud/entry adlaah konsumen/entry yang definisinya terlihat seperti berikut ini:

<flow ... parent="crud/entry">

  <view-state id="entry">

    <on-entry>
      <evaluate expression="kotaRepository.findAll()" result="viewScope.daftarKota" />
    </on-entry>

    <transition on="simpan">
      <evaluate expression="konsumenAction.simpan(selectedEntity, flowRequestContext)" />
    </transition>

    <transition on="kembali" to="kembali" bind="false" validate="false" />

    <transition on="update" to="updateAction" />

  </view-state>       

  <action-state id="updateAction">
     <evaluate expression="konsumenAction.update(selectedEntity, flowRequestContext)" />        
  </action-state>

</flow>

Pada saat view entry pertama kali dikerjakan, method findAll() dari kotaRepository akan dikerjakan dan hasilnya akan ditampung sebagai variabel di daftarKota (dengan view scope). Secara default, id dari view-state dipakai untuk menentukan facelet yang hendak ditampilkan, dalam hal ini adalah src/main/webapp/WEB-INF/konsumen/entry/entry.xhtml yang isinya seperti berikut ini:

<ui:composition ... template="/WEB-INF/layouts/standard.xhtml">
    <ui:define name="content">              
        <p:growl for="pesanInformasi" globalOnly="true" showDetail="true" />
        <p:panel id="panel" columns="3" styleClass="entry" header="#{updateMode? 'Edit Konsumen': 'Tambah Konsumen'}">
            <p:focus />
            <h:panelGrid columns="3" cellpadding="5">

                <p:outputLabel for="id" value="Id:" rendered="#{updateMode}"/>
                <p:inputText id="id" size="50" value="#{selectedEntity.id}" disabled="true" rendered="#{updateMode}"/>
                <p:message for="id" display="text" rendered="#{updateMode}"/>

                <p:outputLabel for="nama" value="Nama:" />
                <p:inputText id="nama" size="50" value="#{selectedEntity.nama}">
                    <p:clientValidator event="keyup" />
                </p:inputText>
                <p:message for="nama" display="text" />

                <p:outputLabel for="alamat" value="Alamat:" />
                <p:inputTextarea id="alamat" rows="5" cols="50" value="#{selectedEntity.alamat}">
                    <p:clientValidator event="keyup" />
                </p:inputTextarea>                                                    
                <p:message for="alamat" display="text" />

                <p:outputLabel for="kota" value="Kota:" />
                <p:selectOneMenu id="kota" value="#{selectedEntity.kota}" filter="true" converter="#{kotaConverter}">
                    <f:selectItems value="#{daftarKota}" var="vKota" itemValue="#{vKota}"/>
                  </p:selectOneMenu>  
                <p:message for="kota" display="text" />           

            </h:panelGrid>
            <div class="buttonPanel">
                <p:commandButton id="simpan" value="Simpan" icon="fa fa-floppy-o" action="simpan" validateClient="true" rendered="#{!updateMode}" update="mainForm"/>
                <p:commandButton id="update" value="Update" icon="fa fa-edit" action="update" validateClient="true" rendered="#{updateMode}" update="mainForm"/>                    
                <p:commandButton id="kembali" value="Kembali" icon="fa fa-arrow-left" action="kembali" immediate="true" />
            </div>                                    
        </p:panel>                            
    </ui:define>  
</ui:composition>

Pada facelet di atas, saya melakukan binding dari setiap komponen input ke variabel selectedEntity di flow melalui atribut value. Saya menambahkan p:clientValidator agar validasi JavaScript langsung dilakukan pada saat saya mengetik di komponen input. Khusus untuk p:selectOneMenu yang menampilkan pilihan daftar kota, saya perlu memberikan variabel daftar kota yang sudah saya query sebelumnya. Selain itu, saya juga perlu memberikan sebuah converter yang bisa menerjemahkan id kota menjadi nama kota dan sebaliknya (dapat dijumpai di src/main/java/view/converter/KotaConverter). Bila saya menampilkan view ini di browser dan nilai selectedEntity adalah null, saya akan memperoleh tampilan seperti berikut ini:

Contoh tampilan data entry

Contoh tampilan data entry

Bila seandainya selectedEntity tidak bernilai null, maka saya akan memperoleh tampilan seperti berikut ini:

Contoh tampilan edit

Contoh tampilan edit

Pada <p:commandButton>, saya menambahkan atribut update dengan nilai mainForm supaya saat tombol di-klik, hanya panel ini saja yang perlu diperbaharui melalui Ajax. Nilai action seperti 'simpan', 'update' atau 'kembali' harus sesuai dengan yang saya pakai di &lt;transition&gt; di deklarasi flow. Khusus untuk tombol kembali, saya menambahkan atribute immediate dengan nilai true agar validasi dan binding tidak dilakukan.

Pada view display, saya menggunakan sebuah <p:dataTable> yang dilengkapi dengan halaman, filter, dan pengurutan seperti yang terlihat pada gambar berikut ini:

Contoh tampilan tampilan tabel

Contoh tampilan tampilan tabel

Untuk meningkatkan kinerja, daripada men-query isi tabel sekaligus, saya hanya men-query 10 record saja per halaman. Begitu pengguna men-klik nomor halaman yang berbeda, kode program akan men-query 10 record lain yang dibutuhkan (melalui Ajax). Hal ini dapat dicapai dengan memberikan nilai "true" pada atribut lazy. Selain itu, saya juga perlu sebuah turunan LazyDataModel. Pada class ini, terdapat method seperti:

List load(int first, int pageSize, String sortField, SortOrder sortOrder, Map<String, Object> filters)

yang perlu di-override. Beruntungnya, Spring Data JPA mendukung pembatasan query per halaman dengan menggunakan interface Pageable (bentuk konkrit-nya adalah PageRequest). Pageable juga dapat dipakai untuk menyimpan informasi pengurutan.

Untuk membuat persistence layer, saya cukup membuat interface seperti:

public interface KonsumenRepository extends JpaRepository<Konsumen, Long> {

  Page findByNamaLike(String nama, Pageable pageable);

  Page findByAlamatLike(String alamat, Pageable pageable);

  Page findByKota_NamaLike(String namaKota, Pageable pageable);

}

Spring Data JPA akan membuat implementasi dari interface ini secara otomatis. Saya hanya perlu menambahkan @Inject pada class lain yang perlu memakai interface ini. Seluruh method yang ada mengembalikan object Page. Object ini tidak hanya berisi daftar Konsumen yang di-query, tetapi juga informasi lain seperti jumlah seluruh konsumen yang ada. Informasi seperti ini dibutuhkan untuk membuat daftar halaman, menentukan apakah ini adalah halaman pertama atau halaman terakhir, dan sebagainya. Selain finders, JpaRepository sudah menyediakan method seperti save() dan delete() untuk melakukan operasi CRUD pada sebuah JPA entity.

Pada service layer, saya membuat class seperti:

@Service @Transactional
public class KonsumenService {

  @Inject
  private transient KonsumenRepository konsumenRepository;

  @Override
  public Konsumen simpan(Konsumen konsumen) {
    return konsumenRepository.save(konsumen);
  }

  @Override
  public Konsumen update(Konsumen konsumenBaru) {
    Konsumen konsumen = konsumenRepository.findOne(konsumenBaru.getId());
    konsumen.setNama(konsumenBaru.getNama());
    konsumen.setAlamat(konsumenBaru.getAlamat());
    konsumen.setKota(konsumenBaru.getKota());
    return konsumen;
  }

  @Override
  public void hapus(Konsumen konsumen) {
    konsumenRepository.delete(konsumen.getId());
  }

}

Saya menggunakan annotation @Service untuk memberi tanda bahwa class di atas adalah bagian dari service layer. Selain itu, saya juga memberikan annotation @Transactional agar setiap method di dalam class ini dikerjakan dalam sebuah transaksi database. Bila terjadi kesalahan selama eksekusi sebuah method yang diindikasi oleh sebuah Exception, maka proses rollback harus dilakukan sehingga tidak ada perubahan yang terjadi pada database. Saya tidak menggunakan save() pada method update() melainkan men-update satu per satu atribut yang ada karena pada domain class yang kompleks, sering kali method update() hanya boleh memperbaharui sebagian atribut saja.

Berkat Spring container, saya bisa memakai class service layer di presentation layer dengan menggunakan @Inject seperti pada:

@Component
public class KonsumenAction {

  @Inject
  private KonsumenService konsumenService;

  ...

}

Saya kemudian bisa memakainya seperti pada:

public String update(@NotNull Konsumen entity, RequestContext context) {
  try {
    konsumenService.update(entity);
    return "success";
  } catch (Exception ex) {
    addErrorMessage(ex.getMessage());
    return "error";
  }
}

public void addInfoMessage(String message) {
  FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_INFO, "Sukses", message);
  FacesContext.getCurrentInstance().addMessage("pesanInformasi", facesMessage);
}

public void addErrorMessage(String message) {
  FacesMessage facesMessage = new FacesMessage(FacesMessage.SEVERITY_ERROR, "Terjadi kesalahan", message);
  FacesContext.getCurrentInstance().addMessage("pesanInformasi", facesMessage);
}

Method simpan() mengembalikan sebuah String yang mewakili transisi yang akan dilakukan pada SWF. Selain itu, saya juga bisa menambahkan pesan informasi dalam bentuk FacesMessage yang bisa ditampilkan oleh PrimeFaces melalui p:growl seperti yang terlihat pada gambar berikut ini:

Tampilan pesan informasi melalui growl

Tampilan pesan informasi melalui growl

Karena KonsumenAction memiliki annotation @Component, maka singleton-nya sudah disediakan oleh Spring sehingga saya bisa langsung memakainya di SWF seperti pada contoh berikut ini:

<action-state id="updateAction">
    <evaluate expression="konsumenAction.update(selectedEntity, flowRequestContext)" />
    <transition on="success" to="kembali" />
    <transition on="error" to="entry" />          
</action-state>

Sebagai langkah terakhir, saya perlu men-deploy aplikasi web ini ke sebuah server. Bila saya menjalankan perintah gradle build, saya akan memperoleh sebuah file bernama ROOT.war di folder webapps. Masalahnya adalah kemana saya harus meletakkan file ini? Salah satu solusi gratis yang bisa saya pakai adalah dengan menggunakan OpenShift (https://www.openshift.com/) yang menyediakan layanan PaaS. Saya mulai dengan membuat sebuah cartridge DIY (Do It Yourself) baru dan men-install Tomcat 8 pada cartridge tersebut. Saya juga menambahkan sebuah cartridge MySQL untuk memperoleh sebuah database MySQL. Setelah men-setup script di folder .openshift dan memindahkan kode program ke server OpenShift dengan menggunakan Git, aplikasi bisa di-deploy dan dijalankan.

Salah satu hal yang harus diperhatikan saat memakai OpenShift adalah hanya port 15000 – 35530 yang boleh digunakan. Port 8080 adalah satu-satunya yang diperbolehkan diluar port yang diizinkan. Setiap request HTTP (di port 80) akan di forward ke port 8080 di server virtual saya. Saya juga perlu mengubah context.xml di server Tomcat 8 yang saya install agar memiliki resource bernama jdbc/inventory yang merujuk pada database MySQL. Saya bisa memperoleh informasi mengenai alamat host, port, user dan ip dengan melihat isi environment variable seperti OPENSHIFT_MYSQL_DB_HOST, OPENSHIFT_MYSQL_DB_PORT, OPENSHIFT_MYSQL_DB_USERNAME dan OPENSHIFT_MYSQL_DB_PASSWORD. Selain itu, sebagai pengguna gratis, aplikasi yang saya deploy akan memasuki idling state bila tidak pernah diakses selama 24 jam. Ini akan menyebabkan pengguna yang pertama kali mengakses aplikasi setelah idling state harus mengalami sedikit delay.

Menyimpan Template simple-escp Di Database

Ada beberapa pengguna aplikasi yang sering kali ingin mengubah hasil percetakan di printer dari waktu ke waktu. Bila kode program percetakan disimpan di dalam aplikasi, ini berarti saya harus mengubah kode program setiap kali ada perubahan layout. Hal ini lama-lama bisa merepotkan! Oleh sebab itu, pada artikel Memakai simple-escp Di Griffon, saya mendefinisikan template percetakan dalam bentuk file JSON. Setiap kali ada yang ingin merubah layout percetakan, mereka bisa meng-edit file JSON ini sendiri tanpa harus menunggu saya.

Solusi di atas bekerja dengan baik bila template laporan terletak di server yang dipakai bersama (misalnya aplikasi web). Bagaimana dengan aplikasi desktop yang dikembangkan dengan menggunakan Griffon dan simple-jpa? Saya dapat menyimpan template JSON untuk simple-escp ke dalam database. Dengan demikian, perubahan template yang dibuat oleh satu pengguna tetap dapat dilihat dan dipakai oleh pengguna lainnya.

Sebagai contoh, saya bisa membuat sebuah entity untuk mewakili template simple-escp seperti berikut ini:

@DomainClass @Entity @Canonical
class TemplateFaktur {

    @NotBlank
    String nama

    @Lob
    String isi

}

Saya memakai annotation @Lob pada field isi untuk menandakan bahwa kolom tersebut dapat di-isi dengan banyak karaketer (large object type). Pada Hibernate JPA dan database MySQL Server, kombinasi ini akan menghasilkan field dengan tipe longtext yang dapat menampung hingga maksimum 4 GB karakter (bandingkan dengan VARCHAR yang menampung maksimal 255 karakter).

Untuk mencetak template simple-escp berdasarkan TemplateFaktur yang sudah disimpan di database, saya dapat menggunakan kode program seperti berikut ini:

TemplateFaktur template = findTemplateFakturByNama(model.nama)
JsonTemplate template = new JsonTemplate(template.isi)
PrintPreviewPane printPreviewPane = view.printPreviewPane
printPreviewPane.display(template, DataSources.from(model.dataSource, model.options))

Sekarang, saya perlu membuat sebuah MVC untuk mengedit template yang ada. Agar pengguna bisa lebih nyaman dalam meng-edit template, saya akan menggunakan groovy.ui.ConsoleTextEditor bawaan Groovy. ConsoleTextEditor mengandung sebuah JTextPane yang dilengkapi dengan syntax highlighting sehingga sangat berguna untuk menampilkan dokumen teks yang memiliki syntax seperti bahasa pemograman. Sebagai contoh, saya bisa mendefinisikan view seperti berikut ini:

actions {
    action(id: 'cari', name: 'Cari', closure: controller.cari)
    action(id: 'simpan', name: 'Simpan', closure: controller.simpan)
    action(id: 'reset', name: 'Reset', closure: controller.reset)
}

panel(id: 'mainPanel') {
    borderLayout()

    panel(constraints:PAGE_START) {
        flowLayout(alignment: FlowLayout.LEFT)
        comboBox(id: 'namaTemplateFaktur', model: model.namaTemplateFaktur)
        button(action: cari)
    }

    widget(new ConsoleTextEditor(), id: 'inputEditor', constraints: CENTER)

    panel(constraints: PAGE_END) {
        flowLayout(alignment: FlowLayout.LEFT)
        button(action: simpan)
        button(action: reset)
    }
}

Untuk menampilkan template dari database untuk di-edit, saya dapat menggunakan kode program seperti berikut ini di controller:

def cari = {
    execInsideUISync {
        String namaTemplateFaktur = model.namaTemplateFaktur.selectedItem
        if (namaTemplateFaktur) {
            String isi = findTemplateFakturBy(namaTemplateFaktur)?.isi
            TextEditor textEditor = view.inputEditor.textEditor
            DefaultStyledDocument doc = new DefaultStyledDocument()
            doc.setDocumentFilter(new SimpleEscpFilter(doc))
            doc.insertString(0, isi?: '', null)
            textEditor.setDocument(doc)
            textEditor.caretPosition = 0
        }
    }
}

Secara default, ConsoleTextEditor akan melakukan syntax highlighting berdasarkan format Groovy. Karena simple-escp memakai format yang berbeda, saya akan mendefinisikan sebuah DocumentFilter baru yang saya sebut sebagai SimpleEscpFilter yang isinya seperti berikut ini:

class SimpleEscpFilter extends StructuredSyntaxDocumentFilter {

    public static final String VARIABLES = /(?ms:${.*?})/
    public static final String FUNCTIONS = /(?ms:%{.*?})/
    public static final String CODE = /(?ms:{{.*?}})/

    SimpleEscpFilter(DefaultStyledDocument document) {
        super(document)

        StyleContext styleContext = StyleContext.getDefaultStyleContext()
        Style defaultStyle = styleContext.getStyle(StyleContext.DEFAULT_STYLE)

        Style variables = styleContext.addStyle(VARIABLES, defaultStyle)
        StyleConstants.setForeground(variables, Color.GREEN.darker().darker())
        getRootNode().putStyle(VARIABLES, variables)

        Style functions = styleContext.addStyle(FUNCTIONS, defaultStyle)
        StyleConstants.setForeground(functions, Color.BLUE.darker().darker())
        getRootNode().putStyle(FUNCTIONS, functions)

        Style code = styleContext.addStyle(CODE, defaultStyle)
        StyleConstants.setForeground(code, Color.MAGENTA.darker().darker())
        getRootNode().putStyle(CODE, code)
    }

}

StructuredSyntaxDocumentFilter adalah DocumentFilter bawaan Groovy yang melakukan syntax highlighting berdasarkan ekspresi Regex. Pada implementasi di atas, saya memberikan pewarnaan yang berbeda untuk setiap komponen dalam template simple-escp. Bila saya menjalankan program, saya akan memperoleh hasil seperti pada gambar berikut ini:

Tampilan editor

Tampilan editor

Bila pengguna men-klik tombol simpan, saya dapat menyimpan perubahan dengan kode program seperti berikut ini:

def simpan = {
    TemplateFaktur templateFaktur = findTemplateFakturByNama(namaTemplateFaktur)
    if (!templateFaktur) {
       templateFaktur = new TemplateFaktur(nama: model.namaTemplateFaktur.selectedItem)
       persist(templateFaktur)
    }
    templateFaktur.isi = view.inputEditor.textEditor.text
}

Sekarang, saya tidak perlu khawatir lagi harus men-deploy ulang aplikasi hanya karena perubahan kecil di layout percetakan. Bila pengguna tidak memiliki tim IT yang memahami syntax simple-escp, setidaknya saya masih bisa mengirim email berisi template yang sudah dimodifikasi untuk di-copy paste oleh mereka. Ini masih jauh lebih baik daripada harus men-deploy ulang aplikasi 😀

Melakukan Query Full-Text Search Dengan Hibernate Search

Pada artikel Memakai Full-Text Search Di MySQL Server, saya menunjukkan bagaimana menerapkan Full-Text Search (FTS) dengan menggunakan MySQL Server. Kali ini, saya akan mencoba alternatif lain dimana FTS dilakukan dari sisi aplikasi dengan menggunakan Hibernate Search. Dengan demikian, FTS tetap dapat dilakukan pada database apapun yang didukung oleh Java. Tidak perlu lagi terikat pada syntax MATCH(...) AGAINST(...) yang hanya berlaku di MySQL Server.

Saya tidak akan ragu menggunakan Hibernate Search pada aplikasi web. Akan tetapi pada pada aplikasi desktop yang tidak memiliki server aplikasi, ia bisa menimbulkan kerumitan baru. Agar index yang dibuat Hibernate Search dapat diperbaharui dan dipakai bersama oleh banyak klien (disebut juga slave), maka dibutuhkan sebuah program yang menjalankan Hibernate Search sebagai master. Hibernate Search mendukung kasus seperti ini dengan memanfaatkan JMS atau JGroups. Tentu saja ini akan lebih repot bila dibandingkan dengan memakai fitur yang sudah ada di server database.

Sebagai latihan, saya akan memakai Hibernate Search pada sebuah aplikasi Griffon. Saya akan mulai dengan menambahkan dependency di BuildConfig.groovy seperti berikut ini:

compile('org.hibernate:hibernate-search-orm:5.0.0.Final')

Hibernate Search terintegrasi sangat baik dengan Hibernate JPA. Hal ini bisa terlihat dari pengaturan Hibernate Search yang dilakukan pada persistence.xml milik JPA. Sebagai contoh, saya menambahkan baris konfigurasi berikut ini:

<?xml...>
  <persistence-unit name="default" transaction-type="RESOURCE_LOCAL">
    <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
    <class>domain.inventory.Produk</class>
    ...
    <properties>
      ...
      <property name="hibernate.search.default.directory_provider" value="filesystem"/>
      <property name="hibernate.search.default.indexBase" value="C:/indexes"/>
    </properties>
  </persistence-unit>
</persistence>

Hibernate Search akan memakai Apache Lucene untuk melakukan indexing. Pada konfigurasi di atas, saya akan memakai DirectoryProvider berupa FSDirectoryProvider yang akan menyimpan index Lucene ke dalam folder yang telah ditentukan oleh hibernate.search.default.indexBase.

Berikutnya, saya akan memberikan annotation Hibernate Search pada entity JPA yang sudah ada. Saya perlu menambahkan @Indexed bila ingin entity JPA diproses. Selain itu, saya perlu menambahkan @Field pada property yang hendak di-index. Sebagai contoh, entity JPA saya akan terlihat seperti berikut ini:

@DomainClass @Entity
@Indexed
class Produk {

    ...

    @NotBlank @Size(min=3, max=150)
    @Field
    String nama

    ...

}

Sekarang, setiap kali entity JPA ditambah, dimodifikasi atau di-update, maka index akan diperbaharui. Saya akan menemukan informasi index disimpan di folder yang saya tentukan pada hibernate.search.default.indexBase. Isi file dalam index sesuai dengan format yang dipakai oleh Apache Lucene.

Lalu, bagaimana bila saya memiliki data lama yang belum ada index-nya? Saya dapat menghasilkan index untuk data yang sudah ada dengan menggunakan kode program seperti berikut ini:

Search.getFullTextEntityManager(getEntityManager()).createIndexer().startAndWait()

Untuk melakukan query, saya bisa memakai cara yang JPA-friendly seperti pada contoh berikut ini:

import simplejpa.SimpleJpaUtil
import org.hibernate.search.jpa.FullTextEntityManager
import org.hibernate.search.jpa.Search
import org.hibernate.search.query.dsl.QueryBuilder
import javax.persistence.Query
import domain.Produk

def produkRepository = SimpleJpaUtil.instance.repositoryManager.findRepository('Produk')
produkRepository.withTransaction {
   FullTextEntityManager fem = Search.getFullTextEntityManager(entityManager)
   QueryBuilder b = fem.getSearchFactory().buildQueryBuilder().forEntity(Produk).get()
   def luceneQuery = b.keyword().onField("nama").matching("digital").createQuery();
   def hasil = fem.createFullTextQuery(luceneQuery).getResultList()   
   println hasil
}

Kode program di atas akan mencetak semua produk yang namanya mengandung kata 'digital'. Method fem.createFullTextQuery() akan menghasilkan sebuah Query JPA biasa yang memiliki method getResultList() untuk menghasilkan List berisi object Produk yang ditemukan.

org.hibernate.search.query.dsl.QueryBuilder adalah builder bawaan Hibernate Search untuk mempermudah programmer sehingga tidak perlu membuat query Lucene secara langsung. Builder tersebut menyediakan fluent API untuk menghasilkan sebuah org.apache.lucene.search.Query pada saat createQuery() dipanggil.

Untuk menunjukkan kemampuan FTS pada Hibernate Search, saya bisa menambahkan fuzzy() pada QueryBuilder seperti berikut ini:

def luceneQuery = b
  .keyword()
  .fuzzy()
  .onField("nama")
  .matching("telephone")
  .createQuery();

Query di atas akan mencari nama produk yang mengandung nama 'telephone' dan yang mendekatinya! Pada produk dengan nama Indonesia, kata yang paling sering digunakan adalah 'telepon'. Bila terdapat .fuzzy(), maka produk yang namanya mengandung 'telepon' juga akan ikut disertakan pada hasil pencarian.

Bila saya melakukan pencarian untuk lebih dari 1 kata seperti pada:

def luceneQuery = b
  .keyword()
  .onField("nama")
  .matching("kabel telepon")
  .createQuery();

maka object Produk yang memiliki nama 'kabel telepon' akan ditampilkan pada baris paling awal. Produk lain yang hanya mengandung kata 'kabel' atau 'telepon' tetap akan dikembalikan, tetapi pada urutan yang lebih rendah.

Walaupun membantu dalam melakukan FTS, saya menemukan kendala dimana versi Hibernate Search yang saya pakai tidak mendukung named entity graph yang diperkenalkan oleh JPA 2.1. Tanpa named entity graph, saya harus menentukan apa saja relasi yang perlu di-fetch secara manual setiap kali melakukan pencarian.

Memakai Fasilitas Structured Search & Replace Di IntelliJ IDEA

Setelah memakai sebuah program cukup lama, saya menemukan bahwa bila JOptionPane dipanggil di luar event dispatching thread (EDT), terkadang-kadang akan muncul kesalahan tak terduga secara acak. Walaupun kesalahan acak ini tidak akan menganggu jalannya aplikasi, kehadirannya bisa membuat pengguna menjadi tidak tenang. Oleh sebab itu, saya membuat sebuah wrapper yang akan memastikan bahwa JOptionPane dipanggil dari EDT.

Dengan demikian, saya perlu mengubah kode program seperti:

if (JOptionPane.showConfirmDialog(view.mainPanel, 'Apakah Anda yakin ingin menghapus?',
   'Konfirmasi Hapus', JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE) != JOptionPane.YES_OPTION) {
     return
}

menjadi berikut ini:

if (!DialogUtils.confirm(view.mainPanel, 'Apakah Anda yakin ingin menghapus?', 'Konfirmasi Hapus', 
   JOptionPane.WARNING_MESSAGE)) {
     return
}

Masalahnya adalah kode program yang harus diubah jumlahnya sangat banyak sekali. Saya tidak yakin dengan kualitas yang dihasilkan bila saya meng-edit kode program satu per satu di setiap class yang ada. Saya juga tidak bisa memakai fasilitas search and replace biasa karena parameter seperti pesan dan judul dialog bisa berbeda-beda.

Karena memakai IntelliJ IDEA, saya langsung teringat pada fasilitas structured replace yang dapat diakses melalui menu Edit, Find, Replace Structurally…. Structured search & replace dapat menyimpan bagian dari hasil pencarian sebagai variabel yang kemudian dapat dipakai sebagai replacement di posisi yang berbeda. Namun kelebihan utamanya yang sangat penting adalah pencarian dilakukan berdasarkan syntax Java atau Groovy sehingga whitespace (seperti spasi atau tab) dan urutan tidak akan mempengaruhi hasil pencarian.

Sebagai contoh, saya mengisi dialog yang muncul dengan template seperti pada gambar berikut ini:

Structured search & replace

Structured search & replace

Bagian yang diapit oleh tanda dollar ($) adalah nama variabel. Mereka memiliki fungsi yang hampir mirip dengan capturing group di regex. Bila saya men-klik tombol Edit variables…, saya bisa menyaring lebih lanjut lagi nilai variabel yang akan masuk dalam kategori pencarian:

Membatasi pencarian berdasarkan variabel

Membatasi pencarian berdasarkan variabel

Bila saya memulai pencarian, saya akan menemukan hasil seperti berikut ini:

Hasil pencarian

Hasil pencarian

Untuk melihat seperti apa hasil perubahan yang akan dilakukan, saya dapat men-klik tombol Preview Replacement. Selain itu, saya juga bisa mencoba mengubah hanya 1 baris terlebih dahulu dengan men-klik tombol Replace Selected. Setelah yakin dengan hasil perubahan, saya pun segera men-klik tombol Replace All untuk melakukan perubahan secara global pada 53 struktur kode program yang ditemukan tersebut.

Sebagai contoh lain, saya juga ingin semua kode program untuk closure close diapit oleh execInsideUISync sehingga kode program apa pun yang ada di dalamnya akan dikerjakan di EDT. Untuk mencapai tujuan tersebut, saya dapat menggunakan template seperti berikut ini:

Structured search & replace

Structured search & replace

Saya menghilangkan def di bagian replacement template agar IntelliJ IDEA tidak menghasilkan def yang duplikat. Ini adalah perilaku yang aneh karena IDEA harusnya bisa lebih pintar. Walaupun demikian, yang terpenting adalah pada akhirnya 29 method berhasil dimodifikasi secara global oleh structured search dengan hasil sesuai yang diharapkan.

Belajar Memakai Inspections Di IntelliJ IDEA

Salah satu fasilitas unik yang ada di IntelliJ IDEA adalah apa yang disebut sebagai inspections.  Ini mirip seperti fitur yang ditawarkan oleh FindBugs (http://findbugs.sourceforge.net) tetapi terintegrasi langsung pada IDE dan lebih lengkap lagi.  Pada versi gratis-nya, IntelliJ IDEA Community Edition, hanya inspections untuk Java dan Groovy yang aktif.

Inspections akan memberikan saran dan peringatan bila ada kode program yang mencurigakan.  Kode program memang benar secara struktur bahasa (tidak memiliki syntax error), akan tetapi kode program bisa saja sulit dimengerti atau memiliki bug.  Apa saja pemeriksaan yang bisa dilakukan oleh inspections di IntelliJ IDEA?  Saya dapat melihatnya dengan memilih menu File, Settings…  Pada dialog yang muncul, saya kemudian memilih menu Editor, Inspections.  Disini, saya bisa melihat apa saja pemeriksaan yang bisa dilakukan beserta keterangannya di bagian description seperti yang terlihat pada gambar berikut ini:

Memilih jenis inspections yang akan dilakukan.

Memilih jenis inspections yang akan dilakukan.

Pada komputer yang lambat, inspections kerap membuat ‘dunia’ menjadi lambat.  Untuk memakai IDEA sebagai editor biasa dan mengurangi kepintarannya, saya dapat men-klik tombol penghapus untuk mematikan seluruh inspections yang ada:

Mematikan seluruh inspections.

Mematikan seluruh inspections.

Secara default, tidak seluruh inspections aktif.  Apakah bila saya mengaktifkan seluruhnya berarti saya bisa menghasilkan kode program yang paling berkualitas?  Tidak!  Tidak ada satu pendekatan universal mengenai kualitas kode program.  Masing-masing developer juga bisa memiliki style-nya masing-masing.  Saya hanya perlu memilih beberapa aturan penting dan menerapkannya secara konsisten!  Yup, ‘konsisten’ adalah salah satu rahasia umum untuk menjaga kualitas kode program!

Saya perlu memilih secara seksama jenis pemeriksaan yang ingin saya lakukan.  Alasan lain saya tidak mengaktifkan seluruh pemeriksaan adalah beberapa pemeriksaan sesungguhnya saling bertolak belakang.  Sebagai contoh, perhatikan 2 jenis pemeriksaan berikut ini:

Contoh inspection yang saling bertolak belakang.

Contoh inspection yang saling bertolak belakang.

Pemeriksaan pertama, Instance method call not qualified with ‘this’, akan memberikan peringatan bila akses ke method di sebuah class tidak diawali dengan this.  Ini akan membuat kode program menjadi seperti pada bahasa pemograman PHP yang menggunakan this untuk mengakses method lain di dalam class yang sama. Sebaliknya, pemeriksan Unnecessary 'this' qualifier akan memberikan peringatan bila akses ke method di dalam class yang sama diawali dengan this. Hal ini karena code style dengan mengawali menggunakan this sering dianggap membingungkan. Dengan demikian, saya tidak dapat mengaktifkan kedua pemeriksaan tersebut secara bersamaan! Saya perlu memilih salah satu atau tidak mengaktifkan keduanya 🙂

Setelah mengaktifkan inspections, saya akan menemukan sebuah kotak kecil di sisi kanan editor, seperti yang terlihat pada gambar berikut ini:

Hasil inspection langsung diperoleh saat program diketik.

Hasil inspection langsung diperoleh saat program diketik.

Kotak ini akan berisi warna sesuai dengan hasil pemeriksaan. Secara default, warna kuning untuk Warning, warna merah untuk Error, warna hijau terang untuk Typo, dan sebagainya. Warna ini juga bisa diatur oleh pengguna.

Bila saya meletakkan pointer mouse agak lama di kotak kuning tersebut, saya akan menemukan informasi seperti:

Informasi hasil inspections.

Informasi hasil inspections.

Warning bukan sebuah kesalahan kode program (dilihat dari syntax atau struktur bahasa), tetapi sesuatu yang mencurigakan. Program tetap dapat berjalan walaupun ada kesalahan pemeriksaan dengan tingkat warning.

Untuk memperbaiki kesalahan, saya dapat menekan tombol Alt + Enter pada bagian kode program yang bermasalah. Saran perubahan akan muncul secara otomatis. Selain itu, bila saya menekan tombol panah kanan, saya bisa memilih untuk mematikan pemeriksaan secara global atau mengabaikan pemeriksaan untuk baris tersebut, seperti yang terlihat pada gambar berikut ini:

Aksi yang dapat dilakukan terhadap hasil inspection.

Aksi yang dapat dilakukan terhadap hasil inspection.

Selain pemeriksaan yang dilakukan secara otomatis setiap kali saya mengetik kode program, saya juga bisa menjalankan inspections secara global dengan memilih menu Analyze, Inspect Code. Pada dialog yang muncul, saya bisa memilih jenis pemeriksaan dan scope pemeriksaan. Setelah men-klik tombol Ok, saya akan memperoleh hasil yang terlihat seperti pada gambar berikut ini:

Hasil inspections secara global.

Hasil inspections secara global.

Salah satu fasilitas baru dari IntelliJ IDEA 14 adalah menu Analyze, Code Cleanup…. Fitur ini akan melakukan pemeriksaan secara global (sesuai dengan scope yang telah ditentukan) dan melakukan perbaikan secara otomatis bila memungkinkan.

Bila saya ingin mematikan inspections untuk sebuah file, saya dapat men-klik tombol wajah yang disebut Hector yang terletak di bagian kanan bawah editor, seperti yang terlihat pada gambar berikut ini:

Mematikan inspections pada file tertentu.

Mematikan inspections pada file tertentu.

Pada popup yang muncul, saya dapat menggeser slider ke posisi Syntax sehingga hanya pemeriksaan tata bahasa saja yang dilakukan atau ke posisi None untuk tidak melakukan pemeriksaan sama sekali. Selain itu, saya dapat memberi tanda centang pada Power Save Mode untuk membuat IntelliJ IDEA bekerja lebih cepat dengan mengorbankan kepintarannya.

Mengatasi Permasalahan N+1 Pada Query Di Hibernate

Apa itu permasalahan N+1? Sebagai contoh, anggap saja saya memiliki sebuah JPA entity seperti berikut ini:

@NamedEntityGraphs([
    @NamedEntityGraph(name='FakturJualOlehSales.Piutang', attributeNodes=[
        @NamedAttributeNode('listItemFaktur'),
        @NamedAttributeNode('piutang')
    ])
])
class FakturJualOlehSales extends FakturJual {

    @NotNull(groups=[Default,InputPenjualanOlehSales]) @ManyToOne
    Konsumen konsumen

    @NotNull(groups=[Default]) @Type(type="org.jadira.usertype.dateandtime.joda.PersistentLocalDate")
    LocalDate jatuhTempo

    @OneToOne(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY)
    KewajibanPembayaran piutang

    @OneToOne(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.LAZY)
    BonusPenjualan bonusPenjualan

    @OneToMany(cascade=CascadeType.ALL, orphanRemoval=true, fetch=FetchType.EAGER)
    @JoinTable(name='FakturJual_retur')
    @OrderColumn
    List<PenerimaanBarang> retur = []

    ...

}

Class FakturJualOlehSales memiliki banyak relasi dengan class lainnya. Ini adalah sesuatu yang wajar terutama bila perancangan dilakukan dengan menggunakan metode DDD yang mengedepankan aggregation. Sebagai turunan dari FakturJual, class FakturJualOlehSales memiliki relasi one-to-many dengan ItemFaktur. Class FakturJualOlehSales juga memiliki relasi one-to-one dengan KewajibanPembayaran (untuk mewakili piutang yang ditimbulkan oleh faktur ini) dan BonusPenjualan (untuk mewakili bonus yang diberikan pada faktur ini). Class KewajibanPembayaran selanjutnya memiliki relasi one-to-many dengan Pembayaran. Begitu juga, class BonusPenjualan selanjutnya memiliki relasi one-to-many dengan ItemBarang. Selain itu, class FakturJualOlehSales juga memiliki relasi one-to-many dengan PenerimaanBarang untuk mewakili retur.

Sebuah class dengan relasi yang kompleks, bukan? Tapi saya tidak selalu butuh semua nilai relasi yang ada setiap kali berhadapan dengan FakturJualOlehSales. Sebagai contoh, pada screen untuk entry data faktur, saya tidak perlu menampilkan nilai piutang. Tetapi pada screen untuk memasukkan data pembayaran piutang, saya perlu nilai piutang tetapi tidak perlu informasi seperti bonusPenjualan.

Oleh sebab itu, saya memberikan nilai atribut fetch=FetchType.LAZY pada beberapa atribut agar Hibernate tidak men-query atribut tersebut. Teknik ini disebut lazy loading. Nilai dari atribut piutang atau bonusPenjualan hanya akan di-query pada saat ia diakses. Ini hanya berlaku selama entity masih berada dalam pengelolaan EntityManager. Bila sudah diluar cakupan EntityManager, saya akan memperoleh pesan kesalahan LazyLoadingException yang sangat terkenal.

Cara lain untuk membaca nilai yang lazy adalah dengan menggunakan query JP QL yang melakukan join fetch secara manual. Khusus untuk JPA 2.1, pilihan yang lebih nyaman adalah dengan menggunakan fasilitas named entity graph. Dengan fasilitas ini, saya tidak perlu menghabiskan banyak waktu memikirkan query! Sebagai contoh, saya mendeklarasikan sebuah named entity graph dengan nama FakturJualOlehSales.Piutang yang akan menyertakan nilai atribut piutang pada saat FakturJualOlehSales dibaca dari database.

Berikut adalah contoh kode program yang memakai named entity graph melalui simple-jpa:

FakturJualRepository repo = simplejpa.SimpleJpaUtil.instance
    .repositoryManager.findRepository('FakturJual')

int start = System.currentTimeMillis();
repo.findAllFakturJualOlehSalesFetchPiutang();
int stop = System.currentTimeMillis();

println "Delta = ${stop-start}"

Walaupun kode program di atas terlihat sederhana, tapi kinerjanya tidak memuaskan! Hibernate ternyata mengerjakan sangat banyak query, seperti:

select distinct ... from FakturJual fakturjual0_ left outer join KewajibanPembayaran kewajibanp1_ on fakturjual0_.piutang_id=kewajibanp1_.id left outer join FakturJual_listItemFaktur listitemfa2_ on fakturjual0_.id=listitemfa2_.FakturJual_id where ...

select ... from Produk produk0_ inner join Satuan satuan1_ on produk0_.satuan_id=satuan1_.id left outer join Supplier supplier2_ on produk0_.supplier_id=supplier2_.id where produk0_.id=?

select ... from Produk produk0_ inner join Satuan satuan1_ on produk0_.satuan_id=satuan1_.id left outer join Supplier supplier2_ on produk0_.supplier_id=supplier2_.id where produk0_.id=?

select ... from Produk produk0_ inner join Satuan satuan1_ on produk0_.satuan_id=satuan1_.id left outer join Supplier supplier2_ on produk0_.supplier_id=supplier2_.id where produk0_.id=?

...

select ... from Konsumen konsumen0_ inner join Region region1_ on konsumen0_.region_id=region1_.id left outer join Region region2_ on region1_.bagianDari_id=region2_.id inner join Sales sales3_ on konsumen0_.sales_id=sales3_.id inner join Gudang gudang4_ on sales3_.gudang_id=gudang4_.id where konsumen0_.id=?

select ... from Konsumen konsumen0_ inner join Region region1_ on konsumen0_.region_id=region1_.id left outer join Region region2_ on region1_.bagianDari_id=region2_.id inner join Sales sales3_ on konsumen0_.sales_id=sales3_.id inner join Gudang gudang4_ on sales3_.gudang_id=gudang4_.id where konsumen0_.id=?

select ... from Konsumen konsumen0_ inner join Region region1_ on konsumen0_.region_id=region1_.id left outer join Region region2_ on region1_.bagianDari_id=region2_.id inner join Sales sales3_ on konsumen0_.sales_id=sales3_.id inner join Gudang gudang4_ on sales3_.gudang_id=gudang4_.id where konsumen0_.id=?

...

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

...

select ... from kewajibanpembayaran_items listpembay0_ left outer join BilyetGiro bilyetgiro1_ on listpembay0_.bilyetGiro_id=bilyetgiro1_.id where listpembay0_.KewajibanPembayaran_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from kewajibanpembayaran_items listpembay0_ left outer join BilyetGiro bilyetgiro1_ on listpembay0_.bilyetGiro_id=bilyetgiro1_.id where listpembay0_.KewajibanPembayaran_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from kewajibanpembayaran_items listpembay0_ left outer join BilyetGiro bilyetgiro1_ on listpembay0_.bilyetGiro_id=bilyetgiro1_.id where listpembay0_.KewajibanPembayaran_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

Permasalahan ini sering kali disebut sebagai permasalahan N+1. Nilai 1 adalah query pertama untuk SELECT * FROM x. Setelah itu, untuk N jumlah record yang diperoleh dari query pertama, lakukan query lain untuk membaca nilai di tabel lain seperti SELECT * FROM y WHERE y.x_id = x.id. Dengan demikian, semakin banyak jumlah record yang hendak dibaca, semakin banyak juga query tambahan yang perlu dilakukan. Permasalahan N+1 biasanya adalah pemborosan kinerja yang tak seharusnya terjadi karena ia dapat digantikan dengan join dan/atau subquery.

Sebagai patokan, saya akan menyimpan hasil eksekusi program di atas dan membuat versi grafik-nya yang terlihat seperti pada gambar berikut ini:

Grafik yang menunjukkan kinerja awal

Grafik yang menunjukkan kinerja awal

Pada grafik di atas, pada eksekusi pertama kali, saya akan memperoleh penalti kinerja yang cukup tinggi. Ini adalah karakteristik dari simple-jpa. Selain itu, hal ini juga ditambah lagi dengan server database yang belum memiliki cache hasil query.

Saya akan mulai dengan berusaha menghilangkan query N+1 ke tabel konsumen. FakturJualOlehSales memiliki hubungan @ManyToOne dengan konsumen. Saya menemukan bahwa dengan menyertakan definisi atribut konsumen pada named entity graph, maka query N+1 untuk relasi ke konsumen tidak akan muncul lagi. Perubahan yang saya lakukan menyebabkan definisi named entity graph saya menjadi seperti berikut ini:

@NamedEntityGraph(name='FakturJualOlehSales.Piutang', attributeNodes=[
    @NamedAttributeNode('listItemFaktur'),
    @NamedAttributeNode(value='konsumen', subgraph='konsumen'),
    @NamedAttributeNode('piutang')
], subgraphs = [
    @NamedSubgraph(name='konsumen', attributeNodes=[
        @NamedAttributeNode('region'),
        @NamedAttributeNode('sales')
    ])
])

Sekarang, nilai untuk konsumen tidak akan di-query satu per satu lagi, melainkan diperoleh melalui join pada saat mengambil nilai FakturJualOlehSales seperti yang terlihat pada SQL yang dihasilkan oleh Hiberate:

select distinct ... from FakturJual fakturjual0_ ... left outer join Konsumen konsumen2_ on fakturjual0_.konsumen_id=konsumen2_.id left outer join Region region3_ on konsumen2_.region_id=region3_.id left outer join Sales sales4_ on konsumen2_.sales_id=sales4_.id ...

Sayang sekali saya tidak dapat melakukan hal yang sama untuk Produk karena listItemFaktur adalah sebuah @ElementCollection yang tidak dianggap sebuah entity sehingga tidak dapat diatur melalui named entity graph.

Sampai disini, apakah versi yang memakai left outer join akan lebih cepat dari versi N+1? Saya akan kembali melakukan sedikit percobaan dan menemukan hasil seperti yang terlihat pada gambar berikut ini:

Grafik yang menunjukkan kinerja setelah perubahan.

Grafik yang menunjukkan kinerja setelah perubahan.

Pada grafik di atas, terlihat bahwa perubahan yang saya lakukan memberikan sedikit peningkatan kinerja (sekitar 8%). Hal ini karena pada rancangan saya, tidak banyak yang bisa dioptimalkan dengan cara seperti ini.

Sebagai langkah berikutnya, saya akan menghindari query N+1 untuk retur dengan menjadikannya sebagai 1 query tunggal yang terpisah. Saya dapat menggunakan @Fetch(FetchMode.SUBSELECT) untuk keperluan seperti ini. Sebagai informasi, @Fetch adalah annotation khusus dari Hibernate dan bukan merupakan bagian dari JPA! Sebagai contoh, saya mengubah kode program menjadi seperti berikut ini:

class FakturJualOlehSales extends FakturJual {

   ...

   @OneToMany(...) @JoinTable()
   @Fetch(FetchMode.SUBSELECT)
   List<PenerimaanBarang> retur = []

   ...

}

Konfigurasi di atas akan menyebabkan seluruh query N+1 yang tadinya seperti:

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id=?

...

digantikan oleh sebuah query tunggal yang isinya seperti berikut ini:

select ... from FakturJual_retur retur0_ inner join PenerimaanBarang penerimaan1_ on retur0_.retur_id=penerimaan1_.id inner join Gudang gudang2_ on penerimaan1_.gudang_id=gudang2_.id where retur0_.FakturJual_id in (select fakturjual0_.id from FakturJual fakturjual0_ ...)

Saya segera menambahkan @Fetch(FetchMode.SUBSELECT) pada beberapa atribut lainnya yang memiliki permasalahan N+1. Setelah itu, saya mencoba menjalankan program dan kini memperoleh kinerja seperti yang terlihat pada grafis berikut ini:

Grafik yang menunjukkan kinerja setelah perubahan.

Grafik yang menunjukkan kinerja setelah perubahan.

Kali ini saya memperoleh peningkatan kinerja yang cukup drastis karena saya menemukan banyak atribut yang bisa dioptimalkan melalui @Fetch(FetchMode.SUBSELECT). Sebagai hasil akhir, setelah berupaya menghilangkan sebagian besar query N+1, saya memperoleh peningkatan kinerja sebesar 56%. Tidak ada perubahan yang perlu saya lakukan pada kode program yang membaca entitas; ia tetap merupakan sebuah baris yang polos seperti findAllFakturJualFetchPiutang().

Belajar Membuat Komponen Baru Di Swing

NetBeans Swing GUI Builder boleh dibilang adalah visual editor terbaik untuk Swing. Berkat NetBeans Swing GUI Builder, pengguna dapat merancang GUI berbasis Swing dengan mudah dan cepat seperti pada seri ‘visual’ -nya Microsoft. Masalah yang kemudian muncul adalah kampus lokal kemudian membuat kurikulum pemograman Swing dan menggantinya dengan visual builder. Mereka berusaha mengajarkan Swing dan Java seperti pada teknik Rapid Application Development (RAD) layaknya Visual Basic.

Memilih untuk memakai GUI Builder atau membuat GUI secara kode program adalah selera ‘hidup’ masing-masing developer yang tidak boleh dipaksakan. Tapi, mempelajari komponen Swing tanpa menyentuh arsitektur Swing dan memperlakukan mereka layaknya komponen terbatas seperti di seri ‘visual’ adalah sebuah kesalahan besar. Mengapa demikian? Hal ini karena justru kelebihan utama Swing adalah sifatnya yang modular dan extensible.

Sebagai contoh, saya menemukan keterbatasan JToolBar pada kasus yang saya hadapi. Bila ada banyak icon di JToolBar dan ukuran layar terbatas, maka mereka akan terpotong. Sebagai contoh, pada JToolBar saya, terdapat 9 icon besar. Tapi bila JFrame diperkecil, maka icon yang tidak terlihat akan ‘hilang’ seperti yang ditunjukkan pada gambar berikut ini:

JToolBar akan menyembunyikan icon bila tidak muat

JToolBar akan menyembunyikan icon bila tidak muat

Saya membutuhkan fasilitas scrolling sehingga saya tetap dapat memilih icon yang tidak ditampilkan. Tapi, tidak ada properties di JToolBar yang bisa saya atur untuk keperluan ini! Apakah ini berarti ‘kiamat’? Pada teknologi GUI lain, bila sudah tidak ada properties yang bisa diatur, jawabannya mungkin ‘iya’. Tapi tidak untuk Swing! Pada Swing, saya bisa membuat komponen baru dengan mudah. Saya bisa men-reuse elemen dari sebuah komponen tanpa harus membuat segala sesuatunya dari awal.

Sebagai contoh, saya membuat versi JToolBar yang bisa di-scroll dengan nama ScrollableToolBar yang kode programnya terlihat seperti berikut ini:

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.basic.BasicArrowButton;
import java.awt.*;
import java.awt.event.*;

public class ScrollableToolBar extends JPanel {

    private JToolBar toolBar;
    private JScrollPane scrollPane;
    private JButton btnLeft;
    private JButton btnRight;

    public ActionLeft actionLeft = new ActionLeft();
    public ActionRight actionRight = new ActionRight();

    public ScrollableToolBar() {
        toolBar = new JToolBar();
        scrollPane = new JScrollPane(toolBar, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        scrollPane.getViewport().addChangeListener(new DisplayButtonChangeListener());
        btnLeft = new BasicArrowButton(SwingConstants.WEST);
        btnLeft.setAction(actionLeft);
        btnLeft.addMouseListener(new PressButtonMouseAdapter(actionLeft));
        btnRight = new BasicArrowButton(SwingConstants.EAST);
        btnRight.addMouseListener(new PressButtonMouseAdapter(actionRight));

        setLayout(new BorderLayout());
        add(scrollPane, BorderLayout.CENTER);
        add(btnLeft, BorderLayout.LINE_START);
        add(btnRight, BorderLayout.LINE_END);
    }

    public JToolBar getToolBar() {
        return toolBar;
    }

    @Override
    public Component add(Component comp) {
        return toolBar.add(comp);
    }

    public class ActionLeft extends AbstractAction {

        @Override
        public void actionPerformed(ActionEvent e) {
            JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
            scrollBar.setValue(scrollBar.getValue() - scrollBar.getBlockIncrement());
        }

    }

    public class ActionRight extends AbstractAction {

        @Override
        public void actionPerformed(ActionEvent e) {
            JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
            scrollBar.setValue(scrollBar.getValue() + scrollBar.getBlockIncrement());
        }

    }

    public class PressButtonMouseAdapter extends MouseAdapter implements ActionListener {

        private Action action;
        private Timer timer;

        public PressButtonMouseAdapter(final Action action) {
            this.action = action;
            timer = new Timer(20, this);
        }

        @Override
        public void mousePressed(MouseEvent e) {
            timer.start();
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            timer.stop();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            action.actionPerformed(null);
        }
    }

    private class DisplayButtonChangeListener implements ChangeListener {

        @Override
        public void stateChanged(ChangeEvent e) {
            JViewport viewport = scrollPane.getViewport();
            boolean buttonVisible = (toolBar.getWidth() > viewport.getWidth());
            btnLeft.setVisible(buttonVisible);
            btnRight.setVisible(buttonVisible);
        }
    }

}

Sekarang, saya dapat melakukan scrolling pada JToolBar seperti yang diperlihatkan pada gambar berikut ini:

Komponen baru dengan JToolBar yang dapat di-scroll

Komponen baru dengan JToolBar yang dapat di-scroll

ScrollableToolBar diturunkan dari class JPanel sehingga merupakan komponen yang terdiri dari beberapa komponen lainnya. Pada kode program di atas, ScrollableToolBar terdiri atas 2 JButton, sebuah JScrollPane dan sebuah JToolBar yang masing-masing diwakili oleh variabel seperti yang terlihat pada visualisasi berikut ini:

Komposisi dari ScrollableToolBar

Komposisi dari ScrollableToolBar

Saya membutuhkan JScrollPane untuk melakukan scrolling. Tapi, saya tidak ingin memperlihatkan scrollbar. Oleh sebab itu, saya membuat JScrollPane dengan constructor seperti pada:

scrollPane = new JScrollPane(toolBar, 
   ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER, 
   ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

btnLeft dan btnRight adalah sebuah JButton biasa yang memiliki gambar panah. Untuk itu, saya memakai BasicArrowButton yang merupakan komponen yang dipakai oleh JScrollBar. Yup, scrollbar sudah punya tombol panah, saya tidak perlu membuat lagi dari awal!. Disini terlihat bahwa saya bisa me-reuse elemen sebuah komponen dengan mudah. Btw, visual editor biasanya tidak akan menampilkan BasicArrowButton untuk di-drag sehingga komponen seperti ini hanya bisa dipakai melalui kode program! Pada constructor BasicArrowButton, saya menentukan arah tanda panah seperti yang terlihat pada kode program berikut ini:

btnLeft = new BasicArrowButton(SwingConstants.WEST);
btnRight = new BasicArrowButton(SwingConstants.EAST);

Untuk melakukan scrolling pada btnLeft atau btnRight di-klik, saya memanipulasi scrollbar tak terlihat milik JScrollPane seperti yang terlihat pada kode program berikut ini:

public class ActionLeft extends AbstractAction {

  @Override
  public void actionPerformed(ActionEvent e) {
    JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
    scrollBar.setValue(scrollBar.getValue() - scrollBar.getBlockIncrement());
  }

}

public class ActionRight extends AbstractAction {

   @Override
   public void actionPerformed(ActionEvent e) {
     JScrollBar scrollBar = scrollPane.getHorizontalScrollBar();
     scrollBar.setValue(scrollBar.getValue() + scrollBar.getBlockIncrement());
   }

}

Saya tidak perlu khawatir bila nilai yang berikan melalui JScrollBar.setValue() melewati batas yang diperbolehkan karena setValue() akan memakai nilai maksimal atau nilai minimum bila ada nilai yang melewati batas yang diperbolehkan.

Action di atas didaftarkan agar dikerjakan bila JButton di-klik. Pada saat melakukan scrolling, pengguna cenderung tidak hanya men-klik, melainkan menahan tombol sehingga proses scrolling dapat berlangsung terus menerus. Oleh sebab itu, saya memakai MouseAdapter untuk menjalankan sebuah timer yang aktif bila pengguna menahan tombol (pada handler mousePressed) dan tidak aktif setelah pengguna melepaskan tombol (pada handler mouseReleased). Hal ini terlihat pada cuplikan kode program berikut ini:

public class PressButtonMouseAdapter extends MouseAdapter implements ActionListener {

  private Action action;
  private Timer timer;

  public PressButtonMouseAdapter(final Action action) {
    this.action = action;
    timer = new Timer(20, this);
  }

  @Override
  public void mousePressed(MouseEvent e) {
    timer.start();
  }

  @Override
  public void mouseReleased(MouseEvent e) {
    timer.stop();
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    action.actionPerformed(null);
  }
}

Class javax.swing.Timer (jangan tertukar dengan java.util.Timer!) adalah sebuah class yang sangat berguna untuk mengerjakan sebuah aksi pada GUI secara periodik. Class ini tidak terlihat di NetBeans Swing GUI Editor sehingga mahasiswa yang mempelajari Swing sebagai komponen drag-n-drop cenderung mengabaikannya. Ini adalah salah satu alasan mengapa saya selalu menyarankan mahasiswa mulai membuat GUI berbasis Swing melalui kode program terlebih dahulu. Setelah akrab dengan Swing, mereka boleh menyentuh Swing GUI Editor (analogi yang sama pada dunia web: mahasiswa belajar menulis HTML dan CSS terlebih dahulu baru boleh menyentuh GUI designer seperti Adobe Dreamweaver).

Btw, salah satu cara jitu untuk mempelajari Swing adalah dengan mempelajari kode program untuk komponen Swing itu sendiri. Yup, source untuk seluruh komponen Swing (dan juga API Java lainnya) bisa dibaca secara bebas!. Sebagai contoh, saya meniru cara memakai Timer saat tombol mouse ditahan dengan mempelajari kode program untuk JScrollBar itu sendiri yang bisa ditemui pada class javax.swing.plaf.basic.BasicScrollBarUI. Ini adalah cuplikan kode programnya:

...

/**
 * Listener for cursor keys.
 */
protected class ArrowButtonListener extends MouseAdapter
{
   // Because we are handling both mousePressed and Actions
   // we need to make sure we don't fire under both conditions.
   // (keyfocus on scrollbars causes action without mousePress
   boolean handledEvent;

   public void mousePressed(MouseEvent e)          {
     if(!scrollbar.isEnabled()) { return; }
       // not an unmodified left mouse button
       //if(e.getModifiers() != InputEvent.BUTTON1_MASK) {return; }
       if( ! SwingUtilities.isLeftMouseButton(e)) { return; }
       int direction = (e.getSource() == incrButton) ? 1 : -1;

       scrollByUnit(direction);
       scrollTimer.stop();
       scrollListener.setDirection(direction);
       scrollListener.setScrollByBlock(false);
       scrollTimer.start();

       handledEvent = true;
       if (!scrollbar.hasFocus() && scrollbar.isRequestFocusEnabled()) {
          scrollbar.requestFocus();
       }
   }

   public void mouseReleased(MouseEvent e)         {
       scrollTimer.stop();
       handledEvent = false;
       scrollbar.setValueIsAdjusting(false);
   }
}
...

Sebagai langkah terakhir, saya ingin tombol btnLeft dan btnRight hanya muncul bila wilayah yang ada tidak cukup. Bila masih ada banyak tempat kosong, maka kedua tombol tersebut tidak perlu muncul. Untuk itu, saya perlu berinteraksi dengan JViewPort yang dimiliki oleh JScrollPane. Class JViewPort mewakili wilayah yang terlihat di JScrollPane. Saya bisa mengerjakan sebuah aksi setiap kali wilayah ini berubah dengan membuat kode program seperti berikut ini:

scrollPane.getViewport().addChangeListener(
   new DisplayButtonChangeListener());

Pada ChangeListener yang ada, saya akan membandingkan lebar JViewPort dengan lebar JToolBar. Bila JViewPort masih cukup menampung seluruh JToolBar, maka saya menyembunyikan btnLeft dan btnRight seperti yang terlihat pada cuplikan kode program berikut ini:

private class DisplayButtonChangeListener implements ChangeListener {

   @Override
   public void stateChanged(ChangeEvent e) {
     JViewport viewport = scrollPane.getViewport();
     boolean buttonVisible = (toolBar.getWidth() > viewport.getWidth());
     btnLeft.setVisible(buttonVisible);
     btnRight.setVisible(buttonVisible);
   }

}

Sampai disini, saya sudah memperoleh sebuah komponen baru sesuai dengan keinginan saya, cukup dengan membuat sebuah class baru. Sifat reusable dan extensible merupakan kelebihan Swing yang membuatnya jauh lebih tangguh dari sekedar komponen visual biasa yang di-drag n drop pada GUI editor.

Mencari Kebocoran Memori Di Program Java Dengan JVisualVM dan Eclipse MemoryAnalyzer

Kebocoran memori pada sebuah program terjadi bila programmer memakai memori untuk menampung data tetapi tidak melepaskan wilayah memori tersebut walaupun data sudah tidak dipakai lagi. Bila hal ini terjadi terus menerus, sebanyak apapun memori yang tersedia, pada akhirnya akan ‘habis’. Istilah ‘habis’ yang dimaksud adalah data di memori secara logika sesungguhnya sudah tidak dipakai lagi dan boleh digunakan untuk menampung data baru, tetapi yang terjadi adalah wilayah memori tersebut tetap dianggap sedang dipakai.

Pada bahasa C, pengguna dapat meminta wilayah memori untuk dipakai dengan menggunakan malloc(). Setelah tidak dibutuhkan lagi, wilayah memori tersebut dilepaskan dengan menggunakan free(). Pada Java, programmer perlu mengalokasikan penggunaan memori dengan new tetapi tidak perlu melepaskan wilayah memori tersebut secara manual. Fasilitas garbage collector di Java akan mencari object yang sudah tidak pakai dan mem-‘buang’-nya secara otomatis. Garbage collector selalu bekerja di balik layar pada saat program Java dijalankan.

Bila sudah ada garbage collector, apakah itu berarti kebocoran memori tidak akan terjadi di program yang dibuat dengan Java? Tidak juga! Garbage collector hanya akan mem-‘buang’ object yang benar-benar tidak dirujuk oleh object lainnya lagi. Jadi, walaupun sebuah object sudah tidak dipakai lagi, tetapi bila masih ada object lain yang menyimpan referensinya, maka garbage collector tidak akan mem-‘buang’ object tersebut.

Sebagai contoh, saya memantau sebuah program Java yang saya buat dengan menggunakan JVisualVM. Ini adalah tool bawaan JDK yang dapat dijumpai di folder seperti C:\Program Files\java\jdk1.7\bin\jvisualvm.exe. Sebagai informasi, di folder ini juga masih ada banyak tool berguna lainnya yang tidak memiliki shortcut di Start Menu. Setelah menjalankan JVisualVM, saya dapat melihat semua program Java yang sedang berjalan di komputer lokal di window Applications . Saya kemudian men-double click pada program Java yang hendak saya pantau. Setelah itu, saya men-klik pada tab Monitor. Pada bagian Heap, saya menemukan tampilan seperti pada gambar berikut ini:

Grafik menunjukkan ada kebocoran memori

Grafik menunjukkan ada kebocoran memori

Grafik di atas memperlihatkan memori di heap secara terus menerus meningkat hingga mencapai batas maksimal yang diperbolehkan. Ini akan menyebabkan program Java berjalan sangat lambat. Bahkan setelah saya men-klik tombol Perform GC, grafik tidak menunjukkan penurunan wilayah heap yang terpakai. Saya yakin telah terjadi kebocoran memori yang sangat besar karena program saya saat ini tidak sedang melakukan apa-apa. Bagaimana mungkin sebuah program yang sedang santai ‘memakan’ memori sebesar itu?

Lalu bagaimana cara mengetahui kode program mana yang menyebabkan terjadinya kebocoran memori? Saya akan mulai melakukan troubleshooting dengan men-klik tombol Heap Dump. Pada window Applications, saya men-klik kanan heap dump yang dihasilkan dan memilih Save As untuk menyimpannya sebagai sebuah file.

Saya akan menggunakan Eclipse MemoryAnalyzer (MAT) untuk melakukan analisa secara otomatis. MAT dapat di-download sebagai sebuah aplikasi standalone (Eclipse RCP) di http://eclipse.org/mat/downloads.php. Setelah men-extract file zip yang di-download, saya men-double click file MemoryAnalyzer.exe untuk menjalankan MAT. Setelah program dijalankan, saya memilih menu File, Open Heap Dump… untuk membuka file heap dump yang sudah saya buat sebelumnya. Pada dialog Getting Started, saya memilih Leak Suspects Report dan men-klik tombol Finish.

Setelah proses analisa selesai, saya memperoleh laporan seperti yang terlihat pada gambar berikut ini:

Hasil analisa Eclipse MemoryAnalyzer

Hasil analisa Eclipse MemoryAnalyzer

Laporan tersebut memperlihatkan bahwa ada 31 object yang merupakan instance dari class PrintPreviewPane yang memakai hingga 67% memori di heap. Padahal seluruh JPanel tersebut sudah tidak dipakai lagi karena saya sudah menutup dialog-nya sejak lama. Saya sudah menemukan ‘tersangka’! Langkah selanjutnya adalah mengetahui kronologi kejadian agar saya bisa memperbaiki kebocoran ini. Untuk itu, saya bisa melihat pada bagian Reference Pattern seperti pada gambar berikut ini:

Output pada Reference pattern

Output pada Reference pattern

Ternyata PrintPreviewPane adalah sebuah JPanel yang ditampilkan melalui JDialog. Bila seandainya setiap JDialog yang ada tersebut dibersihkan oleh garbage collector, program bisa memperoleh memori sebesar 144 MB untuk dipakai ulang. Seluruh child dan data yang terkandung di dalam PrintPreviewPane tidak dihapus sama sekali oleh garbage collector.

Mengapa garbage collector tidak membuang JDialog yang sudah tidak ditampilkan dari memori? Ini adalah kesalahan klasik dimana saya lupa memanggil dispose() dari JDialog setelah menutup dialog tersebut. Pada Swing, hampir seluruh top-level container seperti JFrame, JDialog, JWindow dan sebagainya harus di-dispose() bila sudah tidak dipakai lagi. Loh, bukankah seharusnya garbage collector bekerja secara pintar? Masalahnya, top-level container memakai resource spesifik milik sistem operasi yang berhubungan dengan GUI. Selain itu, top-level container juga bisa ditampilkan ulang setelah ditutup, misalnya dengan setVisible(true). Hanya dengan dispose() baru seluruh alokasi sumber daya milik sistem operasi tersebut akan benar-benar dilepaskan.

Saya pun segera memperbaiki kesalahan pada kode program Java yang saya buat, misalnya dengan menambahkan kode program seperti:

d.dispose();
d = null;

Sekarang, bila saya memakai JVisualVM untuk memantau aktifitas memori, saya akan menemukan hasil seperti pada gambar berikut ini:

Aktifitas memori setelah kebocoran 'besar' diperbaiki

Aktifitas memori setelah kebocoran ‘besar’ diperbaiki

Terlihat bahwa garbage collector berhasil menghapus object yang tidak dibutuhkan dimana grafik penggunaan memori terlihat menurun. Kebocoran memori mungkin saja masih terjadi, tetapi tidak ada lagi kebocoran ‘besar’ yang menyebabkan program menjadi sangat lambat akibat kehabisan memori.