Living life and Make it Better

life, learn, contribute

Endy Muhardin

Software Developer berdomisili di Jabodetabek, berkutat di lingkungan open source, terutama Java dan Linux.

Manajemen Proyek Sederhana

Di berbagai milis yang saya ikuti, ada diskusi menarik yang muncul. Apa saja yang harus dilakukan untuk dalam project management? Bagaimana SOP (standard operating procedure) nya? Tools apa yang digunakan?

Saya sudah mencoba berbagai metodologi manajemen proyek sepanjang karir saya. Ala koboi di jaman jahiliah dulu, ala waterfall di perusahaan terdahulu, ala CMMI di BaliCamp, dan berbagai ala-ala lainnya. Sepanjang perjalanan tersebut, saya juga membaca tentang berbagai metodologi seperti XP, Scrum, Crystal, RUP, dsb.

Dari semuanya, tentu tidak ada metodologi yang bisa digunakan di semua kondisi. Segalanya serba tergantung keadaan. Walaupun demikian, hasil pengalaman bertahun-tahun tersebut sudah memberikan saya pemahaman tentang latar belakang di aturan-aturan CMMI maupun di metodologi lainnya. Begitu kita mendapatkan alasan dibalik aturan tersebut, kita bisa mengambil esensinya dan mengimplementasikannya dengan bentuk lain yang sesuai dengan situasi kita.

Berikut adalah manajemen proyek ala ArtiVisi.

Fase dan Siklus

Sebelum membahas lebih jauh tentang manajemen proyek, kita luruskan dulu beberapa istilah. Kita mengenal istilah fase dan siklus (lifecycle)

Fase adalah tahapan yang dilalui dalam membuat aplikasi. Biasanya, dalam satu fase akan terdiri dari banyak task.

Fase yang ada di internal ArtiVisi adalah sebagai berikut :

  1. Requirement Development
  2. Coding
  3. User Acceptance Test
  4. Implementasi

Keempat fase ini berjalan secara serial, artinya, fase coding baru bisa dimulai setelah requirement development selesai, dsb. Mendengar ini, pembaca yang beraliran progresif revolusioner pasti langsung demo di Bundaran HI, “Waterfall is dead, long live Agile !!!”

Tunggu dulu sebentar, waterfall dan iteratif itu adalah lifecycle, bukan fase.

Lalu apa itu lifecycle? Lifecycle adalah urutan kita menjalankan fase tersebut di keseluruhan project. Bagaimana maksudnya ini?

Begini, misalnya kita membuat aplikasi akunting. Aplikasi ini terdiri dari beberapa modul yaitu; ledger, hutang/piutang, dan manajemen kas. Lifecycle waterfall adalah apabila kita menjalankan project dengan urutan seperti ini :

  1. Project Planning

    1. Modul Ledger
    2. Modul Hutang/Piutang
    3. Modul Kas
  2. Requirement Development

    1. Modul Ledger
    2. Modul Hutang/Piutang
    3. Modul Kas
  3. Coding

    1. Modul Ledger
    2. Modul Hutang/Piutang
    3. Modul Kas
  4. UAT

    1. Modul Ledger
    2. Modul Hutang/Piutang
    3. Modul Kas
  5. Implementasi

    1. Modul Ledger
    2. Modul Hutang/Piutang
    3. Modul Kas

Lifecycle waterfall memang memiliki banyak kelemahan, apalagi kalau projectnya besar. Karena rentang waktu yang jauh antara fase requirement dan fase UAT, maka biasanya pada saat UAT user merasa kaget karena merasa aplikasi yang dibuat tidak sesuai ekspektasi. Entah karena user sudah lupa requirement yang dia bikin sendiri, ataupun karena proses bisnisnya memang sudah berubah.

Lifecycle yang dijalankan ArtiVisi adalah Staged Delivery, seperti yang dijelaskan di bukunya Steve McConnell berjudul Rapid Development. Berikut urutannya.

  1. Global / High Level

    1. Project Planning
    2. Requirement Development
  2. Modul Ledger

    1. Project Planning
    2. Requirement Development
    3. Coding
    4. UAT
    5. Implementasi
  3. Modul Hutang/Piutang

    1. Project Planning
    2. Requirement Development
    3. Coding
    4. UAT
    5. Implementasi
  4. Modul Kas

    1. Project Planning
    2. Requirement Development
    3. Coding
    4. UAT
    5. Implementasi

Pendekatan ini bukan waterfall, karena delivery-nya iteratif dan kecil-kecil. Satu modul biasanya berkisar 1 - 2 bulan saja, kira-kira sesuai dengan panjang sprint yang dianjurkan di metodologi Scrum. Tapi pendekatan ini juga bukan murni agile, karena ada global project planning dan requirement di awal. Untuk apa ada kegiatan itu? Tidak lain dan tidak bukan adalah untuk mengantisipasi interkoneksi antar modul.

Bila kita langsung mengambil salah satu modul untuk diiterasi, maka ada resiko modul tersebut tidak nyambung dengan modul lainnya. Memang bisa disambungkan, tapi nanti akan berkesan tambal sulam. Istilahnya Fred Brooks dalam Mythical Man Month, tidak ada Conceptual Integrity. Oleh karena itu kita perlu melakukan global requirement development untuk melihat interaksi antar modul.

Pembaca yang teliti mungkin juga akan bertanya, mengapa kita menggunakan istilah Requirement Development, bukannya Requirement Gathering? Ini karena untuk mendapatkan requirement, perlu usaha ekstra, tidak sekedar memungut (gathering) informasi di sana-sini. Seringkali kita harus menanyakan hal yang sama dari beberapa sudut pandang untuk mendapatkan apa maunya user. Di saat lain, kita harus bisa menebak fitur yang tersirat. Kalau user bilang, “Harus ada fitur untuk menyimpan data transaksi”, berarti tidak cuma ada screen edit dan list. Tapi juga harus ada fitur search berdasarkan tanggal, produk, nilai transaksi, dan atribut lainnya. Kadangkala ini juga berarti harus ada fitur untuk export/import ke format file lainnya. Jadi, untuk mendapatkan requirement, perlu proses development. Mulai dari sedikit, lalu diakumulasi, dan dikristalisasi sehingga benar-benar akurat dan lengkap. Jangan sampai ada yang ketinggalan.

Kelima fase yang dijelaskan di atas merupakan fase yang sekuensial. Artinya, satu fase dijalankan setelah fase lain selesai. Kita tidak bisa mulai implementasi sebelum UAT selesai. Well, bisa sih, tapi hasilnya tidak akan menyenangkan. Demikian juga, kalau belum selesai coding, ya jangan UAT dulu. Bisa sih maksain UAT, tapi tentu tidak akan menyenangkan. Begitu juga halnya kalau kita coding sebelum jelas requirementnya.

Selain fase yang sekuensial tersebut, ada juga serangkaian kegiatan yang harus kita lakukan secara kontinyu sepanjang project. Yaitu project tracking dan change management.

Project Tracking

Project tracking tidak sulit. Setiap minggu, harus ada orang yang melihat project plan dan membandingkan dengan kondisi sekarang. Task mana yang sudah selesai, mana yang sedang dikerjakan, mana yang sudah selesai. Kondisi sekarang ini direkap ke dalam satu laporan, 1-2 halaman, dan dilaporkan ke client dan manajemen. Di ArtiVisi, mengadopsi dari BaliCamp, selain rekap task juga disertakan rekap resiko dan masalah yang terjadi dalam proyek. Ini akan menjadi bahan diskusi dengan client, bagaimana cara memecahkan masalah tersebut, dan bagaimana mencegah resiko agar tidak berdampak merugikan.

Requirement Development

Sebelum membahas change management, kita bahas dulu requirement. Di ArtiVisi, requirement bentuknya adalah daftar screen aplikasi, biasanya dibuat dengan Netbeans. Kami tidak menggunakan format naratif seperti yang dianut beberapa organisasi dan dinamai User Requirement Specification (URS), Business Requirement Specification (BRS), atau Software Requirement Specification (SRS), User Story, atau istilah-istilah lainnya. Berdasarkan pengalaman, dokumen naratif membuat user malas berinteraksi. Untuk aplikasi shopping cart sederhana saja, bila dibuatkan *RS, akan menjadi lebih dari 20 halaman. Apalagi untuk aplikasi yang besar. Client akan malas membaca, dan akibatnya pada saat aplikasi dideliver hasilnya tidak sesuai ekspektasi, walaupun sesuai dengan *RS.

ArtiVisi menggunakan prototype aplikasi untuk requirement. Kita buatkan screen desktop ataupun web, diisi data hardcoded, dan sudah memiliki menu dan flow. Dari screen registrasi klik submit akan masuk screen konfirmasi, dst. Dengan menggunakan prototype yang bisa diisi dan diklik, user akan lebih bersemangat untuk berinteraksi, sehingga hasil requirement akan menjadi berkualitas. Pada fase requirement ini user bebas meminta modifikasi apapun asal masih dalam scope yang disepakati.

Setelah tidak ada lagi perubahan signifikan, semua screen dicapture, dimasukkan ke dokumen, dan diapprove client (sign off). Ini akan menjadi patokan dalam proses development, diantaranya digunakan oleh programmer untuk coding, dan technical writer untuk membuat user manual.

Change Management

Perubahan setelah sign off harus melalui prosedur change management. Ini gunanya agar progress project bisa terkendali. Sering sekali banyak project molor dan tidak selesai-selesai karena banyak perubahan ini itu yang tidak dikelola dengan baik sehingga tidak terlihat titik akhirnya.

Prosedur Change Management

Prosedur change management tidak rumit. Hanya terdiri dari tiga langkah saja, yaitu :

  1. Requester menjelaskan perubahan yang diminta, apa alasan yang mendasari perubahan
  2. Tim development menganalisa dampak perubahan, apakah ada coding yang berubah, user manual, test scenario, dsb. Output dari analisa ini adalah estimasi berapa tambahan waktu, tenaga, dan biaya untuk mengeksekusi perubahan ini
  3. Manajemen di sisi client memutuskan berdasarkan estimasi tersebut, apakah perubahan ini akan :

    • Dijalankan segera
    • Ditunda ke iterasi berikut
    • Ditolak, artinya tidak jadi dijalankan pada project ini, mungkin saja dijadikan project baru.

Dengan adanya proses ini, akan jelas berapa hari project akan mundur, dan berapa biaya tambahannya.

Demikian sharing tentang praktek yang kita gunakan di ArtiVisi. Metodologi ini cukup sederhana, sehingga bisa dijalankan bahkan di project yang one-man-show. Project manager dia, business analyst dia, programmer dia juga, training lagi-lagi dia, terima transferan tidak lain dan tidak bukan adalah dia juga. Di project berskala besar, metodologi ini juga cukup efektif untuk membuat project berjalan secara predictable.

Masih ada penjelasan lebih lanjut yang harus ditulis, diantaranya bagaimana cara menghitung (estimasi) project, dan bagaimana melakukan project planning dan tracking yang efektif. Ini akan dibahas di tulisan yang lain.

Bila ingin berkomentar atau berdiskusi, silahkan bergabung di milis it-project-indonesia@googlegroups.com dengan cara mengirim email kosong ke it-project-indonesia-subscribe@googlegroups.com


Konfigurasi lokasi logfile pada Spring MVC

Di mana kita harus menyimpan log output aplikasi kita? Tentunya kita ingin menggunakan lokasi yang dinamis sesuai dengan lokasi deployment. Misalnya, di Windows kita mungkin mendeploy aplikasi kita di

C:\Program Files\Apache Tomcat\webapps\aplikasi-saya

Sedangkan di Linux, kita mendeploy aplikasi di

/opt/apache-tomcat/webapps/aplikasi-saya

Dengan kemungkinan seperti di atas, bagaimana kita harus menulis konfigurasi log4j? Mudah, bila kita menggunakan Spring MVC.

Kita bisa menggunakan Log4jConfigListener yang disediakan Spring. Class ini memungkinkan kita menggunakan variabel di konfigurasi log4j kita. Kita mendaftarkan class ini di dalam web.xml, sebelum ContextLoaderListener, seperti ini :

    <listener>
        <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

Dengan adanya Log4jConfigListener ini, kita bisa menyebutkan lokasi konfigurasi log4j seperti ini :

    <context-param>
        <param-name>log4jConfigLocation</param-name>
        <param-value>classpath:artivisi-log4j.properties</param-value>
    </context-param>

Isi artivisi-log4j.properties terlihat seperti ini :

# Konfigurasi kategori
log4j.rootLogger=INFO,fileout

# File output
log4j.appender.fileout=org.apache.log4j.DailyRollingFileAppender
log4j.appender.fileout.file=${webapp.root.path}/WEB-INF/logs/application.log
log4j.appender.fileout.datePattern='.'yyyy-MM-dd
log4j.appender.fileout.layout=org.apache.log4j.PatternLayout
log4j.appender.fileout.layout.conversionPattern=%d [%t] %p (%F:%L) ­ %m%n

Perhatikan konfigurasi log4j.appender.fileout.file. Kita menggunakan variabel ${webapp.root.path} yang akan diisi dengan nilai lokasi deployment aplikasi web kita. Variabel ${webapp.root.path} ini didefinisikan dalam web.xml sebagai berikut :

    <context-param>
        <param-name>webAppRootKey</param-name>
        <param-value>webapp.root.path</param-value>
    </context-param>

Dengan konfigurasi ini, kita dapat meletakkan log output kita di

C:\Program Files\Apache Tomcat\webapps\aplikasi-saya\WEB-INF\logs\application.log

bila kita mendeploy di Windows, dan di

/opt/apache-tomcat/webapps/aplikasi-saya/WEB-INF/logs/application.log

bila kita deploy di Linux.

Konfigurasi di atas bisa disederhanakan lagi bila kita mengikuti nilai default yang disediakan Spring, yaitu cukup seperti ini dalam web.xml :

    <listener>
        <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class>
    </listener>

    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

Kemudian memberi nama file konfigurasi logger kita log4j.properties yang berada di top level dalam classpath, dan berisi seperti ini :

# Konfigurasi kategori
log4j.rootLogger=INFO,fileout

# File output
log4j.appender.fileout=org.apache.log4j.DailyRollingFileAppender
log4j.appender.fileout.file=${webapp.root}/WEB-INF/logs/application.log
log4j.appender.fileout.datePattern='.'yyyy-MM-dd
log4j.appender.fileout.layout=org.apache.log4j.PatternLayout
log4j.appender.fileout.layout.conversionPattern=%d [%t] %p (%F:%L) ­ %m%n

Nilai variabel ${webapp.root} secara default akan diisi dengan lokasi deployment tanpa harus mengkonfigurasi webAppRootKey


Menjalankan Spring HTTP Invoker di Sun JRE 6 HTTP Server

Dulu, kita sudah mencoba untuk membuat remoting service dengan menggunakan Spring Framework. Salah satu protokol yang digunakan adalah Spring HTTP Invoker. Untuk mempublish service dengan protokol ini, kita harus menggunakan servlet engine, misalnya Tomcat.

Akan tetapi, Sun Microsystem merilis Java versi 6 yang sudah dilengkapi dengan HTTP Server sederhana. Dengan memanfaatkan fitur ini, kita tidak perlu lagi menggunakan Tomcat hanya untuk mempublish service dengan HTTP Invoker. Ini akan sangat bermanfaat untuk aplikasi kecil yang ingin dipanggil oleh aplikasi lain.

Pada artikel ini, kita akan mempublish service dengan protokol HTTP Invoker pada HTTP Server yang disediakan oleh Sun JRE versi 6.

Di Netbeans, kita akan membuat tiga project, yaitu

  • remoting-shared : project ini menampung interface RemotingService, yang akan digunakan di client dan server

  • remoting-server : project ini yang akan mempublish service. Implementasi RemotingService juga ada di sini

  • remoting-client : project ini yang akan mengakses service yang dipublish remoting-server

Berikut screenshot Netbeans. Project remoting-server dan remoting-client memiliki dependensi terhadap remoting-shared.

Pertama, mari kita lihat dulu service interfacenya. Berikut adalah kode programnya.

package com.artivisi.tutorial.remoting.spring.service.api;

public interface RemotingService {
    public String halo(String nama);
}

Kode program ini berada di project remoting-shared.

Berikutnya, kita lihat dulu di sisi client. Kita cuma butuh satu class untuk menjalankan aplikasi, yaitu ClientLauncher sebagai berikut.

package com.artivisi.tutorial.remoting.spring.client;

public class ClientLauncher {
    private static final Logger log = Logger.getLogger(ClientLauncher.class.getName());
    public static void main(String[] args) {
        AbstractApplicationContext ctx = 
                new ClassPathXmlApplicationContext("client-ctx.xml", ClientLauncher.class);
        ctx.registerShutdownHook();

        RemotingService service = (RemotingService) ctx.getBean("remotingService");

        String msg = service.halo("endy");

        log.info("Pesan dari server : "+msg);
    }
}

ClientLauncher akan membaca client-ctx.xml yang isinya seperti ini.

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

    <!--  proxy dengan protokol HTTP Invoker  -->
    <bean id="remotingService"
    class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
         <property name="serviceUrl" 
         value="http://localhost:9090/RemotingService"/>

         <property name="serviceInterface"
         value="com.artivisi.tutorial.remoting.spring.service.api.RemotingService"/>
    </bean>
</beans>

Seperti kita lihat di atas, client mengakses service yang ada di komputer lokal (localhost) di port 9090, dengan nama service RemotingService.

Selanjutnya, mari kita implement project remoting-server. Di sini ada implementasi RemotingService sebagai berikut

/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package com.artivisi.tutorial.remoting.spring.service.impl;

@Service("remotingService")
public class RemotingServiceImpl implements RemotingService {
    Logger log = Logger.getLogger(RemotingServiceImpl.class.getName());
    
    public String halo(String nama) {
        log.info("Terima dari client : "+nama);
        return "Halo, "+nama;
    }

}

Kemudian ada class untuk menjalankan aplikasi di sisi server. Berikut ServerLauncher.

package com.artivisi.tutorial.remoting.spring.server;

public class ServerLauncher {
    public static void main(String[] args) {
        AbstractApplicationContext ctx =
                new ClassPathXmlApplicationContext("server-ctx.xml", ServerLauncher.class);
        ctx.registerShutdownHook();
    }
}

ServerLauncher membaca file konfigurasi server-ctx.xml. Inilah isinya.

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

    <!-- menginstankan Sun HttpServer dalam JRE 6 -->
    <bean class="org.springframework.remoting.support.SimpleHttpServerFactoryBean">
    	<property name="contexts">
    		<map>
    			<entry key="/RemotingService" value-ref="remotingServiceHttpInvoker"/>
    		</map>
    	</property>
        <property name="port" value="9090" />
    </bean>

</beans>

Pada blok konfigurasi pertama, kita menginstankan Sun HttpServer yang ada di JRE 6. HttpServer ini akan berjalan di port 9090, sesuai dengan yang kita konfigurasi di sisi client. Di sana terlihat bahwa URL /RemotingService akan ditangani oleh remotingServiceHttpInvoker. Berikut konfigurasinya

    <!--  publish service dengan protokol HttpInvoker  -->
    <bean id="remotingServiceHttpInvoker" 
          class="org.springframework.remoting.httpinvoker.SimpleHttpInvokerServiceExporter"
          p:service-ref="remotingService"
          p:serviceInterface="com.artivisi.tutorial.remoting.spring.service.api.RemotingService"
     />

Selanjutnya, kita suruh Spring mendeteksi implementasi service kita secara otomatis, yaitu class yang ada anotasi @Service.

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

Berikut adalah dependensi pustaka di project client.

Library untuk Project Client

Dan ini untuk di server.

Library untuk Project Server

Keseluruhan project akan terlihat seperti ini.

Struktur Folder semua Project

Semua library dapat diambil dari distribusi Spring Framework dan Repository SpringSource.

Coba jalankan ServerLauncher, kita akan melihat log seperti ini.

Jul 3, 2009 2:57:25 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@be2358: display name [org.springframework.context.support.ClassPathXmlApplicationContext@be2358]; startup date [Fri Jul 03 14:57:25 WIT 2009]; root of context hierarchy
Jul 3, 2009 2:57:26 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [server-ctx.xml]
Jul 3, 2009 2:57:26 PM org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
INFO: Bean factory for application context [org.springframework.context.support.ClassPathXmlApplicationContext@be2358]: org.springframework.beans.factory.support.DefaultListableBeanFactory@f11404
Jul 3, 2009 2:57:26 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@f11404: defining beans [org.springframework.remoting.support.SimpleHttpServerFactoryBean#0,remotingServiceHttpInvoker,remotingService,org.springframework.context.annotation.internalCommonAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor]; root of factory hierarchy
Jul 3, 2009 2:57:27 PM org.springframework.remoting.support.SimpleHttpServerFactoryBean afterPropertiesSet
INFO: Starting HttpServer at address 0.0.0.0/0.0.0.0:9090

Lalu jalankan ClientLauncher, inilah log yang muncul.

Jul 3, 2009 2:58:12 PM org.springframework.context.support.AbstractApplicationContext prepareRefresh
INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@be2358: display name [org.springframework.context.support.ClassPathXmlApplicationContext@be2358]; startup date [Fri Jul 03 14:58:12 WIT 2009]; root of context hierarchy
Jul 3, 2009 2:58:12 PM org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions
INFO: Loading XML bean definitions from class path resource [client-ctx.xml]
Jul 3, 2009 2:58:13 PM org.springframework.context.support.AbstractApplicationContext obtainFreshBeanFactory
INFO: Bean factory for application context [org.springframework.context.support.ClassPathXmlApplicationContext@be2358]: org.springframework.beans.factory.support.DefaultListableBeanFactory@d2906a
Jul 3, 2009 2:58:13 PM org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons
INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@d2906a: defining beans [remotingService]; root of factory hierarchy
Jul 3, 2009 2:58:13 PM com.artivisi.tutorial.remoting.spring.client.ClientLauncher main
INFO: Pesan dari server : Halo, endy

Setelah ClientLauncher dijalankan, di log server akan muncul informasi sebagai berikut.

INFO: Terima dari client : endy

Demikianlah cara menggunakan embedded HttpServer. Selamat mencoba.


PostgreSQL Sequence dengan Hibernate

Menggunakan PostgreSQL Sequence dengan Hibernate

Pada artikel ini, kita akan membahas tentang bagaimana membuat Hibernate menggunakan sequence yang dibuat PostgreSQL.

Bila kita membuat Hibernate mapping untuk entity class sebagai berikut:

@Entity @Table(name="example")
public class Example {
    @Id @GeneratedValue
    private Integer id;

    private String name;
 
    // getter dan setter    
}

kemudian menyuruh Hibernate untuk menggenerate DDL ke PostgreSQL, maka kita akan mendapatkan hasil sebagai berikut.

create table example (id int4 not null, name varchar(255), primary key (id))
create sequence example_id_seq

Artinya, Hibernate akan membuat sequence bernama example_id_seq dan menggunakannya untuk menghasilkan id.

Skema yang dihasilkan ini berbeda dengan skema yang biasa digunakan DBA PostgreSQL dalam membuat tabel, yaitu seperti ini

CREATE TABLE example (id serial, name text)

Bila kita menggunakan mapping di atas ke skema tabel dengan id bertipe serial ini, kita akan mendapatkan exception sebagai berikut.

SEVERE: ERROR: relation "hibernate_sequence" does not exist
Exception in thread "main" org.hibernate.exception.SQLGrammarException: could not get next sequence value
Caused by: org.postgresql.util.PSQLException: ERROR: relation "hibernate_sequence" does not exist

Ini disebabkan karena mapping di atas akan mencari sequence bernama hibernate_sequence yang tidak ada kalau kita membuat tabel dengan id serial.

Solusinya adalah dengan menggunakan Hibernate Annotation Extension, yaitu anotasi @GenericGenerator seperti ini.

@Entity @Table(name="example")
public class Example {
    @Id @GeneratedValue(strategy=GenerationType.SEQUENCE, generator="pg_seq")
    @GenericGenerator(name="pg_seq", strategy="sequence", parameters={
        @Parameter(name="sequence", value="example_id_seq")
    })
    private Integer id;

    private String name;
}

Nama sequence diambil dari sequence yang dibuatkan PostgreSQL pada saat kita melakukan create table. Nama sequence ini bisa dilihat dengan mengetikkan \d example, yang akan menghasilkan output sebagai berikut.

\d example
                          Table "public.example"
 Column |  Type   |                       Modifiers                       
--------+---------+-------------------------------------------------------
 id     | integer | not null default nextval('example_id_seq'::regclass)
 name   | text    | 

Barulah setelah itu kita bisa menyimpan object ke database dengan mulus.


Integrasi aplikasi kantor pusat dan cabang [Bagian 3]

Pada artikel sebelumnya, kita telah membahas tentang konsep integrasi aplikasi, dan implementasinya menggunakan Spring Integration. Contoh implementasi kita kemarin, walaupun mencakup integrasi end-to-end, masih sangat sederhana.

Kali ini kita akan mengeksplorasi use-case yang lebih kompleks, yaitu cara menangani berbagai operasi service. Misalnya untuk tipe data yang sama, contohnya Penjualan, kita memiliki service untuk simpan dan hapus. Object penjualan juga lebih kompleks daripada object produk.

Untuk memecahkan masalah ini, kita menggunakan Router. Router bertugas memilih channel sesuai dengan message yang datang. Berikut skema penggunaan router.

Skema Penggunaan Router

Sebelum membahas tentang router, baiklah kita lihat dulu data penjualan yang akan dikirim. Berikut class penjualan

Penjualan

public class Penjualan {
    private Integer id;
    private Date tanggal = new Date();
    private List<PenjualanDetail> details = new ArrayList<PenjualanDetail>();
    // getter dan setter
}

Berikutnya, penjualan detail.

PenjualanDetail

public class PenjualanDetail {
    private Integer id;
    private Penjualan penjualan;
    private Produk produk;
    private Integer jumlah;
    
    // getter dan setter
}

Object baru ini tentu saja harus kita buatkan transformernya untuk mengkonversi object menjadi JSON.

CabangService

public class JsonTransformer{
    public Penjualan jsonToPenjualan(String json){
        Map<String , Class> binding = new HashMap<String , Class>();
        binding.put("details", PenjualanDetail.class);
        return (Penjualan) JSONObject.toBean(JSONObject.fromObject(json), Penjualan.class, binding);
    }

    public String penjualanToJson(Penjualan p){
        // remove dulu cyclic dependency
        for (PenjualanDetail detail : p.getDetails()) {
            detail.setPenjualan(null);
        }

        return JSONObject.fromObject(p).toString();
    }
}

Penjualan dan PenjualanDetail akan kita proses melalui method simpan dan hapus yang ada di CabangService.

CabangService

public class CabangService {
    public void save(Penjualan p) {
        System.out.println("Simpan penjualan dengan ID : "+p.getId());
        System.out.println("Tanggal : "+new SimpleDateFormat("dd MMM yyyy").format(p.getTanggal()));
        System.out.println("Details : ");
        for (PenjualanDetail detail : p.getDetails()) {
            System.out.println("Produk : "+detail.getProduk().getKode());
            System.out.println("Jumlah : "+detail.getJumlah());
        }
    }

    public void delete(Penjualan p) {
        System.out.println("Hapus penjualan dengan ID : "+p.getId());
    }
}

Tentunya nanti method save dan delete ini akan lebih canggih dari ini, misalnya insert dan delete ke database.

Kalau kita lihat skemanya, ada satu titik di mana message akan dilihat dan disalurkan ke channel yang sesuai.

Routing Message

Penjualan yang akan disimpan dimasukkan ke channel penjualan-simpan. Sedangkan object penjualan yang akan dihapus dimasukkan ke channel penjualan-hapus. Dengan demikian, kita harus membuat router yang mampu menentukan channel yang akan dipilih dengan melihat message yang masuk ke dalam router tersebut. Operasi yang akan dilakukan (simpan atau hapus) harus dimasukkan ke message header atau message content. Agar sederhana, kita tambahkan saja satu property di class Penjualan untuk menentukan operasi yang akan dilakukan. Bila class Penjualan ini dimapping menggunakan JPA, kita bisa menandai property ini dengan anotasi @Transient agar isinya tidak disimpan di database.

Karena nantinya property operasi ini akan digunakan tidak saja untuk Penjualan, tapi juga transaksi lainnya, baiklah kita buat saja di superclass DomainObject sebagai berikut.

Penjualan

public class DomainObject {
    private String operasi;
}

Berikut class Penjualan yang sudah dimodifikasi.

Penjualan

public class Penjualan extends DomainObject {
    private Integer id;
    private Date tanggal = new Date();
    private List<PenjualanDetail> details = new ArrayList< <PenjualanDetail>();
    
    // getter dan setter
}

Property operasi ini akan dilihat oleh router untuk menentukan nama channel. Berikut implementasi Router.

Router

public class NamaKelasDanOperasiRouter {
    public String pilihChannel(Object msg){
        String classname = msg.getClass().getSimpleName().toLowerCase();

        if(!DomainObject.class.isAssignableFrom(msg.getClass())) {
            throw new IllegalArgumentException("Harus object bertipe : "+DomainObject.class.getName());
        }

        DomainObject obj = (DomainObject) msg;
        String operasi = obj.getOperasi().toLowerCase();

        return classname + "-" + operasi;
    }
}

Router ini akan membaca nama class dari object yang diterimanya dan menggabungkannya dengan property operasi. Jadi, object Penjualan dengan operasi simpan akan menghasilkan nama channel penjualan-simpan. Demikian juga object Pembelian dengan operasi hapus akan menghasilkan channel pembelian-hapus.

Berikut unit test untuk router di atas.

Router

public class NamaKelasDanOperasiRouterTest {

    @Test
    public void testPilihChannel() {
        Penjualan p = new Penjualan();
        p.setOperasi("simpan");
        assertEquals("penjualan-simpan", new NamaKelasDanOperasiRouter().pilihChannel(p));
    }

}

Berikut aliran message mulai dari gateway sampai menjadi JSON.

Dari Gateway sampai menjadi JSON

Dan ini adalah konfigurasi Spring Integration untuk flow di atas.

Gateway ke JSON

<gateway id="gateway"
    service-interface="com.artivisi.explore.spring.integration.gateway.Gateway"
    default-request-channel="outgoingPenjualan"
/>

<channel id="outgoingPenjualan"/>

<transformer 
    input-channel="outgoingPenjualan" 
    output-channel="outgoingJson"
    ref="jsonTransformer" 
    method="penjualanToJson"
/>

<channel id="outgoingJson"/>

Pada artikel sebelumnya, kita mengganti implementasi email dengan shared folder supaya proses development lebih cepat. Akses ke shared folder jauh lebih cepat daripada email yang dibatasi oleh kecepatan internet. Kali ini, kita akan menggunakan bridge, yaitu hubungan langsung antar channel. Ini kita gunakan untuk menghubungkan channel outgoingJson dengan incomingJson. Hubungan ini pada artikel sebelumnya diimplementasikan menggunakan shared folder dan email.

bridge antara incoming dan outgoing

Berikut konfigurasi bridge untuk menghubungkan outgoingJson dan incomingJson.

Bridge

<bridge 
    input-channel="outgoingJson" 
    output-channel="incomingJson" 
/>

Dari incomingJson, kita konversi dulu menjadi object. Berikut konfigurasinya.

JSON ke Object

<channel id="incomingJson"/>

<transformer 
    input-channel="incomingJson" 
    output-channel="incomingPenjualan"
    ref="jsonTransformer" 
    method="jsonToPenjualan"
/>

Setelah menjadi object, kita masukkan ke router untuk ditentukan channelnya.

Routing

<router 
    input-channel="incomingPenjualan" 
    ref="namaKelasDanOperasiRouter" 
    method="pilihChannel"
/>

Kemudian, dari channel kita sambungkan ke method save dan delete.

Method Save

<channel id="penjualan-simpan"/>

<service-activator 
    input-channel="penjualan-simpan"
    ref="cabangService"
    method="save"/>

Method Delete

<channel id="penjualan-hapus"/>

<service-activator 
    input-channel="penjualan-hapus"
    ref="cabangService"
    method="delete"/>

Terakhir, kita buat class untuk menjalankan semua rangkaian integrasi ini.

CabangRouterDemo

public class CabangRouterDemo {
    public static void main(String[] args) {
         // 1. Menginstankan Spring Application Context
        AbstractApplicationContext ctx 
            = new ClassPathXmlApplicationContext("cabang-router-ctx.xml", CabangRouterDemo.class);
        ctx.registerShutdownHook();
        Gateway gw = (Gateway) ctx.getBean("gateway");
        Penjualan p1 = bikinTransaksi(100, "simpan");
        Penjualan p2 = bikinTransaksi(102, "hapus");

        gw.send(p1);
        gw.send(p2);

        System.exit(0);
    }

    private static Penjualan bikinTransaksi(Integer id, String operasi) {
        Penjualan p = new Penjualan();
        p.setId(id);
        p.setOperasi(operasi);

        Produk pr1 = new Produk(1001, "PR-001", "Produk 001");
        Produk pr2 = new Produk(1002, "PR-002", "Produk 002");
        Produk pr3 = new Produk(1003, "PR-003", "Produk 003");

        PenjualanDetail pd1 = new PenjualanDetail(11, pr1, 1);
        PenjualanDetail pd2 = new PenjualanDetail(12, pr2, 2);
        PenjualanDetail pd3 = new PenjualanDetail(13, pr3, 3);

        p.addPenjualanDetail(pd1);
        p.addPenjualanDetail(pd2);
        p.addPenjualanDetail(pd3);

        return p;
    }
}

Demikianlah penggunaan routing dengan Spring Integration. Seperti kita lihat, dengan menggunakan Spring Integration, aplikasi kita menjadi fleksibel dan mudah ditest. Semua implementasi kode program Java, baik itu transformer, router, dan service method, bisa ditest dengan mudah menggunakan JUnit. Kita juga lihat bahwa semua kode program Java kita tidak memiliki ketergantungan terhadap Spring Integration.