Posts filed under ‘Java Programming Language’

ASM #5: Memakai Agen

Pada tulisan sebelumnya, class hasil transformasi tidak dapat dipakai secara langsung, melainkan harus memakai interface atau superclass-nya. Bagaimana jika aku tetap ingin memakai seperti biasa tanpa membuat interface atau superclass? Salah satu solusi yang mungkin adalah dengan memakai agent. Class tetap dapat dipakai seperti biasa, bahkan tanpa membuat classloader baru. Hal ini karena agent akan dikerjakan saat JVM dijalankan.

JVM akan mencari dan mengerjakan method milik agent class yang definisinya seperti berikut:

public static void premain(String agentArgs, Instrumentation inst);
atau
public static void premain(String agentArgs);

Berikut ini adalah contoh agent class yang aku pakai:

public class Agent {

  public static void premain(String agentArgs, Instrumentation inst) {
    inst.addTransformer(new ClassFileTransformer(){

      @Override
      public byte[] transform(ClassLoader loader, String className,
          Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
          byte[] classfileBuffer) throws IllegalClassFormatException {

        if (className.equals("latihan/Music")) {
          try {

            ClassReader reader = new ClassReader(classfileBuffer);
            ClassWriter writer = new ClassWriter(reader, 0);
            ToStringAdapter adapter = new ToStringAdapter(writer);

            reader.accept(adapter,0);
            return writer.toByteArray();

          } catch (Exception ex) {
            ex.printStackTrace();
          }
        }
        return classfileBuffer;
      }

    });
  }

}

Agent di atas akan melakukan transformasi melalui ToStringAdapter (turunan dari ClassAdapter) yang sudah aku buat di tulisan sebelumnya. Untuk dapat memakai agent ini, aku masih harus membuat jar dari class ini, serta menambahkan manifest yang isinya sebagai berikut:

Manifest-Version: 1.0
Main-Class: latihan.Main
Premain-Class: latihan.Agent
Boot-Class-Path: lib/asm-3.1.jar lib/asm-commons-3.1.jar lib/asm-util-3.1.jar

Entry yang dibutuhkan untuk agent adalah Premain-Class dan Boot-Class-Path. Fungsinya dapat ditebak dari namanya: Premain-Class sama seperti Main-Class, hanya saja ia menyatakan class mana yang merupakan agent. Boot-Class-Path sama seperti Class-Path, hanya saja ia menyatakan classpath yang khusus dipakai oleh agent.

Sekarang, class Music dapat dipakai layaknya class normal lainnya (tentu saja dengan pengecualian sudah punya definisi method toString() secara otomatis):

public class Main {

  public Main() {
    Music m = new Music();
    m.setMusicID("MID-123");
    m.setTitle("TITLE");
    System.out.println("Music = " + m);
  }

  public static void main(String[] args) throws Exception {
    new Main();
  }

}

Btw, untuk menjalankan program di atas, aku tidak lupa menambahkan argument -javaagent agar JVM mengerjakan agent class, seperti:

java -javaagent:latihanASM.jar -jar latihanASM.jar

05 September 2009 at 10:44 AM Tinggalkan Komentar

ASM #3: Kode-Kode Bahasa Java

Hari ini aku akan mempelajari bagaimana byte code Java (atau bahasa assembler-nya Java) bekerja. Yang akan menjadi “bahan penelitian” adalah method berikut:

public String toString() {
  return "[MUSIC]: MusicID=[" + musicID + "]; title=[" + title + "]";
}

Setelah di-compile, method di atas akan memiliki atribut code seperti berikut:

00 03 00 01 00 00 00 25 
bb 00 27 59 12 29 b7 00 
2b 2a b4 00 14 b6 00 2d 
12 31 b6 00 2d 2a b4 00 
16 b6 00 2d 12 33 b6 00 
2d b6 00 35 b0 00 00 00 
02 00 18 00 00 00 06 00 
01 00 00 00 1f 00 19 00 
00 00 0c 00 01 00 00 00 
25 00 1a 00 1b 00 00

Wew, bagaimana menerjemahkan byte code yang tampak asing ini? Dua byte pertama, 00 03, adalah jumlah maksimum untuk operand stack. Nilai ini harus dihitung oleh compiler secara manual. Dua byte berikutnya, 00 01, adalah jumlah variabel lokal yang dipakai dalam method (termasuk parameter). Empat byte berikutnya, 00 00 00 25, menunjukkan kalau ada 0×25 (desimal 37) byte berikutnya yang berisi byte code. Dengan demikian, bagian yang benar-benar berisi byte code adalah 25 byte berikutnya, yaitu:

bb 00 27 59 12 29 b7 00 
2b 2a b4 00 14 b6 00 2d 
12 31 b6 00 2d 2a b4 00 
16 b6 00 2d 12 33 b6 00
2d b6 00 35 b0

Byte pertama, 0xbb menunjukkan bahwa instruksi tersebut adalah instruksi new. Dua byte berikutnya, 00 27, adalah referensi ke constant pool index ke-0×27 atau ke-39, dimana berupa class java/lang/StringBuilder. Ini artinya, JVM akan membuat instance baru dari class java.lang.StringBuilder dan men-push referensi instance tersebut ke operand stack.

Instruksi berikutnya adalah, 0×59, adalah instruksi dup. Instruksi ini tidak membutuhkan operand. Pada saat menemukan instruksi ini, JVM akan men-push nilai yang sama dengan nilai yang berada paling atas di operand stack saat ini.

Operand Stack (Sebelum dup):

| Instance StringBuilder | <-- TOP

Operand Stack (Setelah dup):

| Instance StringBuilder | <-- TOP
| Instance StringBuilder |

Instruksi berikutnya 0×12, adalah instruksi ldc. Instruksi ini akan men-load operand-nya (byte berikut-nya, yaitu 0×29 atau 41) yang merupakan referensi index di constant pool ke operand stack. Index ke-41 di operand stack adalah sebuah String “[MUSIC]: MusicID=[". Dengan demikian, isi operand stack akan menjadi:

Operand Stack (Setelah ldc):
| "[Music: MusicID=["      |  <-- TOP
| Instance StringBuilder   |
| Instance StringBuilder   |

P.S: Jumlah operand stack tidak boleh melebihi maksimum yang telah ditentukan sebelumnya, yaitu 3. Jika ada perintah yang men-push sekali lagi, maka JVM akan memunculkan pesan kesalahan.

Instruksi berikutnya, 0xb7, adalah instruksi invokespecial. Instruksi ini membutuhkan dua byte operand, 00 2b (desimal 43), yang merupakan referensi ke method <init> di constant pool. Selain itu, instruksi ini akan mengambil informasi instance class mana yang akan dikerjakan method-nya melalui informasi di operand stack. Karena method yang didefinisikan di index 43 adalah constructor StringBuffer yang mengambil sebuah parameter String, maka isi operand stack menjadi:

Operand Stack (Setelah invokespecial):
| Instance StringBuilder   | <-- TOP

Berikutnya, terdapat instruksi 0x2a atau aload_0. Instruksi ini akan me-load nilai yang terdapat di local variable array yang berada di index 0 ke operand stack. Seperti yang kita tahu, nilai yang paling awal di local variable array adalah referensi ke class yang sedang aktif (this). Akibatnya, isi operand stack akan menjadi:

Operand Stack (Setelah aload_0):
| this (instance class Music)      | <-- TOP
| Instance StringBuilder           |

Instruksi berikutnya, 0xb4, adalah instruksi getfield. Instruksi ini akan men-push nilai field yang ditunjukkan oleh dua byte berikutnya, 00 14 (desimal 20), ke operand stack. Sebelumnya, ia akan men-pop terlebih dahulu dari operand stack untuk mengetahui class mana yang akan diambil nilai field-nya. Karena index ke-20 adalah referensi ke field musicID, maka isi operand stack akan menjadi:

Operand Stack (Setelah getfield):
| "nilai musicID"        | <-- TOP
| Instance StringBuilder |

Instruksi berikutnya, 0xb6, adalah instruksi invokevirtual. Instruksi ini akan mengerjakan method yang direferensikan oleh dua byte berikutnya, 00 2d (atau desimal 45), yaitu method append. Setelah pengerjaan method append, isi operand stack akan menjadi:

Operand Stack (Setelah invokevirtual):
| Instance StringBuilder | <-- TOP

Instruksi berikutnya, 0x12, kembali lagi merupakan instruksi ldc, untuk men-push String "]; title=[" ke operand stack. Setelah itu, instruksi 0xb6 (invokevirtual) akan mengerjakan method append milik StringBuffer. Selanjutnya, instruksi 0x2a (aload_0) akan me-load nilai this ke operand stack. Dan instruksi 0xb4 (getfield) akan mengambil nilai dari field title dan men-push nilai tersebut ke operand stack. Instruksi 0xb6 (invokevirtual) kembali mengerjakan method append. Instruksi 0x12 (ldc) yang berikutnya akan men-push String "]” ke operand stack. Instruksi berikutnya 0xb6 kembali mengerjakan method append. Instruksi 0xb6 berikutnya akan mengerjakan method toString milik StringBuffer. Dan instruksi terakhir, 0xb0 adalah instruksi areturn, yang akan keluar dari method serta mengembalikan nilai berupa referensi class yang akan di-pop dari operand stack.

Wew, perjalanan yang cukup panjang hanya untuk sebuah method yang sangat sederhana, bahkan hanya satu baris saja. Untungnya, aku tidak perlu selalu menerjemahkan bytecode dengan cara manual seperti ini. Di situs ObjectWeb, dimana aku mendownload ASM, aku juga dapat men-download plugin Eclipse untuk melihat isi byte code dari sebuah source code Java. Setelah meng-install plugin tersebut, aku dapat memilih Window, Show View, Byte Code. Akan muncul sebuah window Byte Code di sebelah kanan workbench yang akan berisi dissasembler dari source code Java dimana kursor editor sedang aktif. Ini adalah output untuk method yang aku pakai di tulisan ini:

  public toString()Ljava/lang/String;
   L0
    LINENUMBER 31 L0
    NEW java/lang/StringBuilder
    DUP
    LDC "[MUSIC]: MusicID=["
    INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
    ALOAD 0
    GETFIELD latihan/Music.musicID : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "]; title=["
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 0
    GETFIELD latihan/Music.title : Ljava/lang/String;
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "]"
    INVOKEVIRTUAL java/lang/StringBuilder.append(Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
    ARETURN
   L1
    LOCALVARIABLE this Llatihan/Music; L0 L1 0
    MAXSTACK = 3
    MAXLOCALS = 1

29 Agustus 2009 at 11:42 PM Tinggalkan Komentar

ASM #2: Membedah File Class

Hari ini aku akan mempelajari bagaimana format sebuah file class. Untuk itu, aku membuat sebuah file class dari source code sederhana berikut:

package latihan;

public class Music {

  private String musicID;
  private String title;
  private final String musicPrefix = "01";

  public Music(String musicId, String title) {
    super();
    musicID = musicId;
    this.title = title;
  }
  public String getMusicID() {
    return musicID;
  }
  public void setMusicID(String musicId) {
    musicID = musicId;
  }
  public String getTitle() {
    return title;
  }
  public void setTitle(String title) {
    this.title = title;
  }
  public static Music getMusic(String id) {
    return null;
  }

}

Setelah selesai men-compile Music.java menjadi Music.class, aku membuka file Music.class melalui hex editor untuk melihat isinya. Setiap file class Java selalu diawali dengan empat byte 0xca, 0xfe, 0xba, 0xbe. Cafe Babe, huh? Setelah itu terdapat dua byte yang berisi minor version (nilainya 0) dan dua byte yang berisi major version (nilainya 50 pada contoh class-ku).

Dua byte berikutnya, 00 35, menunjukkan bahwa di dalam constant pool terdapat 34 item. Nilai ini merupakan jumlah item di constant pool ditambah dengan 1. Setelah itu, berikutnya adalah kumpulan byte untuk constant pool. Ukuran dan isinya dapat berbeda-beda tergantung kode program. Secara umum, constant pool terdiri atas satu atau lebih item, dimana setiap item selalu diawali dengan sebuah byte yang berisi jenis item (byte ini disebut tag).

Sebagai contoh, berikut ini adalah sebagian isi constant pool untuk kode program di atas:

Item #1
Tag: Method References
Class Index: 7
Name & Type Index: 27

...

Item #7
Tag: Class
Name Index: 33

...

Item #13
Tag: UTF8
String Value: <init>

...

Item #25
Tag: UTF8
String Value: SourceFile

Item #26
Tag: UTF8
String Value: Music.java

Item #27
Tag: Name & Type
Name Index: 13
Descriptor Index: 34

...

Item #33
Tag: UTF8
String Value: java/lang/Object

Item #34
Tag: UTF8
String Value: ()V

Item pertama menunjukkan informasi method, untuk class yang ada di item ke-7 (yaitu java/lang/Object). Informasi mengenai method yang ditunjukkan oleh item pertama dapat dilihat lebih lanjut di item ke-27, yang selanjutnya memberikan informasi mengenai method bernama “<init>” (item ke-13) yang descriptor-nya adalah “()V” (item ke-34). Btw, aku tidak membuat method dengan nama “<init>” di source code, darimana munculnya method “<init>”? Ini adalah nama yang khusus diberikan untuk constructor.

Setelah isi constant pool, terdapat dua byte yang berisi access flag. Nilai flag ini menunjukkan apakah class ini termasuk class final, abstract, atau merupakan interface. Nilai untuk class percobaan hari ini adalah 0×0021 yang merupakan kombinasi dari flag 0×0001 (class public) dan flag 0×0020 (selalu harus di-set untuk compiler baru).

Berikutnya terdapat dua byte yang merujuk pada item index di constant pool yang berisi referensi class ini (class yang dirujuk oleh keyword this). Lalu, berikutnya terdapat dua byte yang merujuk pada item index di constant pool yang berisi referensi super-class (parent-class). Dalam contoh class percobaan, super class-nya adalah java/lang/Object (item ke-7 di constant pool). Nilai dua byte ini dapat berupa 0×0000, jika class ini adalah java/lang/Object (satu-satunya class di Java yang tidak punya super class).

Setelah itu terdapat dua byte yang berisi jumlah inteface yang di-implement oleh class ini, diikuti dengan rangkaian index di constant pool untuk menjelaskan interface tersebut. Karena class percobaanku tidak men-implementasi-kan interface, nilai interface count adalah 0.

Berikutnya adalah dua byte yang berisi jumlah field/variabel yang didefinisikan dalam class ini (nilainya adalah 3, karena aku hanya mendefinisikan 3 variabel di source code). Berikutnya adalah struktur yang menjelaskan informasi field tersebut. Ukurannya bisa berbeda tergantung pada jumlah field/variabel yang didefinisikan di dalam class ini. Berikut ini adalah isi informasi field pada class percobaan hari ini:

Field #0
Access Flag:  PRIVATE
Name Index: 8
Descriptor Index: 9
Attribute Count: 0

Field #1
Access Flag:  PRIVATE
Name Index: 10
Descriptor Index: 9
Attribute Count: 0

Field #2
Access Flag:  PRIVATE  FINAL
Name Index: 11
Descriptor Index: 9
Attribute Count: 1
  Attribute Name Index: 12
  Attribute Length: 2 [00 02 ]

Setelah informasi field, terdapat informasi mengenai method. Dua byte pertama, seperti biasa, menunjukkan jumlah method yang didefinisikan dalam class ini (nilainya adalah 6, karena aku mendefinisikan 6 method di source code). Berikutnya, terdapat informasi mengenai method. Pada bagian ini terdapat informasi byte code Java untuk masing-masing method yang terletak di atribut dengan nama “Code”. Aku akan mempelajari lebih lanjut tentang atribut ini di kemudian hari. Ini adalah contoh informasi method pada class percobaan:

Method #0
Access Flag:  PUBLIC
Name Index: 13
Descriptor Index: 14
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 61
00 02 00 03 00 00 00 15
2a b7 00 01 2a 12 02 b5
00 03 2a 2b b5 00 04 2a
2c b5 00 05 b1 00 00 00
01 00 10 00 00 00 16 00
05 00 00 00 0a 00 04 00
07 00 0a 00 0b 00 0f 00
0c 00 14 00 0d 

Method #1
Access Flag:  PUBLIC
Name Index: 17
Descriptor Index: 18
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 29
00 01 00 01 00 00 00 05
2a b4 00 04 b0 00 00 00
01 00 10 00 00 00 06 00
01 00 00 00 0f 

Method #2
Access Flag:  PUBLIC
Name Index: 19
Descriptor Index: 20
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 34
00 02 00 02 00 00 00 06
2a 2b b5 00 04 b1 00 00
00 01 00 10 00 00 00 0a
00 02 00 00 00 12 00 05
00 13 

Method #3
Access Flag:  PUBLIC
Name Index: 21
Descriptor Index: 18
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 29
00 01 00 01 00 00 00 05
2a b4 00 05 b0 00 00 00
01 00 10 00 00 00 06 00
01 00 00 00 15 

Method #4
Access Flag:  PUBLIC
Name Index: 22
Descriptor Index: 20
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 34
00 02 00 02 00 00 00 06
2a 2b b5 00 05 b1 00 00
00 01 00 10 00 00 00 0a
00 02 00 00 00 18 00 05
00 19 

Method #5
Access Flag:  PUBLIC  STATIC
Name Index: 23
Descriptor Index: 24
Attribute Count: 1
Atribute Name Index: 15
Atribute Length: 26
00 01 00 01 00 00 00 02
01 b0 00 00 00 01 00 10
00 00 00 06 00 01 00 00
00 1b

Dan bagian yang paling terakhir dari sebuah file class Java adalah atribut untuk class tersebut. Dua byte pertama berisi informasi jumlah atribut, di-ikuti dengan definisi atribut. Ini adalah contoh informasi atribut untuk class percobaanku:

Attribute #0
Attribute Name Index: 25
Attribute Length: 2
00 1a

Atribut tersebut adalah atribut “Source File” yang nilainya adalah referensi ke item 0x1a (atau desimal 26) di constant pool, yang nilainya tidak lain adalah “Music.java”.

23 Agustus 2009 at 12:42 AM Tinggalkan Komentar

ASM #1: JVM, Mesin Yang Tidak Nyata

Untuk melakukan bytecode instrumentation melalui library seperti ASM, aku setidaknya harus mengerti bagaimana cara kerja JVM.  Setiap program Java, atau tepatnya class Java, akan dijalankan oleh Java Virtual Machine (JVM).  Mirip seperti mesin asli, JVM juga memiliki struktur-struktur seperti register dan stack.

Masing-masing thread yang dijalankan oleh JVM memiliki sebuah register Program Counter (pc).  Register pc berisi lokasi alamat instruksi yang sedang dikerjakan.  Untuk method native,  nilai register pc tidak didefinisikan.

Setiap thread di JVM juga memiliki Java Virtual Machine Stack yang kegunaannya mirip seperti di program biasa seperti C, seperti menampung nilai variabel lokal dan hasil perhitungan sementara.

Seluruh thread di JVM memiliki memory yang di-share yang disebut heap. Heap adalah lokasi memori yang berisi informasi instance dari sebuah class dan array.  Garbage collector akan bekerja secara otomatis untuk menentukan instance yang tidak dibutuhkan lagi dan membebaskan lokasi memori di heap sehingga memori dapat dipakai ulang.

Setiap kali sebuah method dipanggil, JVM akan membuat sebuah frame di Java Virtual Machine stack untuk thread bersangkutan.  Setelah method selesai dikerjakan, JVM akan memusnahkan frame tersebut.   Di dalam frame terdapat informasi local variables.  Sebuah “slot” local variable dapat mengandung nilai boolean, byte, char, short, int, float, reference, atau returnAddress.  Nilai long atau double memerlukan dua “slot”  local variable.

Setiap “slot” local variable memiliki index berurut mulai dari 0, 1, 2, dst.   Pada saat method mulai dijalankan, local variable 0 akan berisi referensi ke object yang mengandung method tersebut (nilai this dalam program Java).  Local variable 1, 2, dst berisi nilai parameter secara berurut.

Frame juga mengandung apa yang disebut operand stack yang banyak dipakai oleh instruksi JVM untuk menulis dan membaca nilai.  Pada saat frame pertama kali dibuat, operand stack tidak memiliki isi.

Instruksi JVM terdiri atas sebuah byte yang berisi opcode, diikuti oleh operands (jika ada).  Kebanyakan instruksi JVM bekerja pada tipe data tertentu.  Sebagai contoh, instruksi yang diawali dengan huruf i bekerja pada data int, huruf l bekerja pada data long, huruf s bekerja pada data short, huruf b bekerja pada data byte, huruf c bekerja pada data char, huruf f bekerja pada data float, huruf d bekerja pada data double, dan huruf a bekerja pada data reference.  O ya, di bahasa pemograman Java ada tipe data boolean, tapi  JVM tidak mengenal istilah boolean.   JVM akan menganggap boolean sebagai integer.

Instruksi load dan store dipakai untuk mengambil nilai dan menulis nilai ke dalam local variable. Sebagai contoh, iload 1 akan menulis local variable 1 yang bertipe int ke dalam operand stack.  Sebaliknya, istore 1 akan menulis nilai int yang ada di operand stack ke local variable 1.

Contoh instruksi aritmatika yang tersedia seperti iadd (penjumlahan), isub (pengurangan), imul (perkalian), idiv (pembagian),  irem (modulus), ineg (negation), ishl (shift left), ishr (shift right), ior (bitwise OR), iand (bitwise AND), ixor (bitwise XOR), iinc (local variable increment), lcmp (perbandingan).  Sebagai contoh, instruksi iadd akan menjumlahkan dua nilai int yang ada di operand stack (men-pop dua nilai terakhir), kemudian menyimpan hasil penjumlahan ke operand stack (men-push hasil penjumlahan).

JVM juga menyediakan beberapa instruksi untuk konversi, seperti i2l (int to long), i2f (int to float), i2d (int to double), l2f (long to float), l2d (long to double) dan f2d (float to double).

Untuk membuat instance class baru, terdapat instruksi new.  Untuk membuat array baru, tersedia instruksi newarray, anewarray, dan multianewarray.  Untuk meng-akses field dari sebuah object, instruksi berikut dapat dipergunakan: getfield, putfield, getstatic, putstatic.  Beberapa instruksi lain yang berkaitan dengan class: arraylength, instanceof, dan checkcast.

Instruksi berikut dipakai untuk memanipulasi operand stack: pop, pop2, dup, dup2, dup_xl, dup2_xl, dup_x2, dup2_x2, dan swap.

Contoh instruksi yang dipakai untuk control transfer (percabangan), misalnya: ifeq (jika sama dengan), iflt (jika lebih kecil), tableswitch, goto, dan ret.

Untuk memanggil method dari sebuah class, instruksi berikut dapat dipergunakan: invokevirtual, invokeinterface, invokespecial, dan invokestatic.  Untuk keluar dari method, instruksi berikut dapat dipergunakan sesuai dengan tipe kembalian dari method: return, ireturn, lreturn, freturn, dreturn, dan areturn.  Untuk membuat exception, instruksi athrow dapat dipergunakan.

Untuk mendukung sinkronisasi (keyword synchronize di bahasa pemograman Java), JVM menggunakan monitor, dan menyediakan instruksi berikut: monitorenter dan monitorexit.

17 Agustus 2009 at 2:08 AM Tinggalkan Komentar

MIDI: Bermain piano dengan Java

Tidak dipungkiri lagi hal yang paling menarik minat programmer pemula adalah pemograman grafis dan suara. Dan begitu juga denganku. Di masa-masa saat pertama kali belajar pemograman dengan Turbo Basic, aku menghabiskan banyak waktu mengubah layar monitor dan membuat speaker internal bersuara aneh. Saat itu ada keyword PLAY yang meminta input berupa string yang berisi deretan tangga nada yang hendak dipakai.

Untuk standar zaman sekarang, sudah ada standar MIDI untuk merekam dan menyalurkan instrumen suara. Java mendukung MIDI melalui sejumlah API yang berada di package javax.sound.midi. Berikut ini adalah contoh sederhana untuk memutar nada-nada:

import java.util.Random;

import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.Receiver;
import javax.sound.midi.ShortMessage;


public class LatihanMIDI {

  public LatihanMIDI() {
    MidiDevice.Info[] info = MidiSystem.getMidiDeviceInfo();
    System.out.println("Daftar MIDI Device yang tersedia:");
    for (MidiDevice.Info i : info) {
      System.out.println("Name = " + i.getName());
      System.out.println("Description = " + i.getDescription());
      System.out.println("Vendor = " + i.getVendor());
      System.out.println();
    }
    try {
      System.out.println("Memakai device pertama.");
      MidiDevice device = MidiSystem.getMidiDevice(info[0]);
      device.open();
      
      Receiver receiver = device.getReceiver();
      
      for (int i= 0; i<128; i++) {
        receiver.send(createMessage(i), -1);
        Thread.sleep(i);
      }
      for (int i=127; i>0; i--) {
        receiver.send(createMessage(i), -1);
        Thread.sleep(i);
      }
      Random rand = new Random();
      while (true) {
        int i = rand.nextInt(127);
        receiver.send(createMessage(i), -1);
        Thread.sleep(rand.nextInt(1000));
      }
      
      
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
  
  public ShortMessage createMessage(int n1) {
    ShortMessage msg = new ShortMessage();
    
    try {
      msg.setMessage(ShortMessage.NOTE_ON, 0, n1, 100);
    } catch (InvalidMidiDataException e) {
      e.printStackTrace();
    }
    return msg;
    
  }
  
  public static void main(String[] args) {
    new LatihanMIDI();
  }

}

Program ini akan terus berulang dan memainkan nada secara acak. O ya, aku memakai MIDI device yang pertama dalam listing (apapun itu), kemudian menulis byte MIDI langsung ke Receiver.

10 Juli 2009 at 2:16 AM Tinggalkan Komentar

For Newbie: Kesalahan Umum Saat Belajar Praktek RMI

RMI adalah fitur Java SE yang menjadi salah satu tulang punggung Java EE (misalnya, EJB memakai RMI). Sebagai seorang pemula, aku sering mengalami masalah, bahkan walaupun hanya mempraktekkan contoh RMI sederhana. Oleh sebab itu, aku akan menuliskan kesalahan-kesalahan yang aku temui disini supaya tidak terulang lagi. Ini adalah kesalahan-kesalahan diluar kesalahan kode program Java. Well, walaupun hanya kesalahan sederhana, kadang-kadang aku menghabiskan cukup banyak waktu untuk menemukannya. O ya, aku menganggap IDE yang dipakai adalah Eclipse.

Pertama: Jangan lupa membuat security manager, membuat file security policy, dan menyertakannya saat launching program. Contoh isi security policy:

grant codeBase "file:C:/Documents and Settings/Snake/My Documents/Eclipse Workspaces/Latihan RMI/bin" {
    permission java.security.AllPermission;
};

Perhatikan bahwa lokasi class hasil compile dari Eclipse berada di folder “bin“, bukan “src“. Folder “bin” secara default disaring dan tidak terlihat di Package Explorer.

Kedua: Jangan lupa mereferensikan file security policy di atas pada saat launching Java, dengan menambahkan argumen seperti berikut:

-Djava.security.policy="C:\Documents and Settings\Snake\My Documents\Eclipse Workspaces\Latihan RMI\src\security.policy"

Argumen ini dapat ditambahkan dengan memilih menu “Run“, “Run Configurations“, klik nama konfigurasi launcher yang sesuai, lalu klik tab “Arguments“, kemudian isi di bagian “VM Arguments“.

Ketiga: Jika java.rmi.codebase mengandung spasi, jangan lupa sesuaikan dengan syntax URL, misalnya:

-Djava.rmi.server.codebase="file:C:/Documents%20and%20Settings/Snake/My%20Documents/Eclipse%20Workspaces/Latihan%20RMI/bin/"

Keempat: Jika java.rmi.codebase adalah sebuah directory, bukan sebuah file JAR, jangan lupa tambahkan tanda slash (“/”) sebagai karakter paling terakhir. Perhatikan contoh di atas.

Kelima: Jangan lupa menambahkan referensi ke server (atau hanya referensi ke remote interface) di classpath milik program client RMI. Pada Eclipse, klik kanan di nama project, pilih properties. Klik pada Java Build Path, lalu pilih tab Projects, kemudian klik tombol “Add“, dan pilih project yang berisi program server RMI.

Keenam: Jangan lupa menjalankan program rmiregistry.exe sebelum mencoba menjalankan program server dan client RMI!

Ketujuh: Terlalu cepat menyerah…. :) Dengan asumsi kode program Java sudah benar, maka biasanya ada hal-hal sepele yang terlewat jika program RMI tidak berjalan sesuai harapan. Segera jalankan netstat atau lebih baik lagi sniffer seperti Wireshark. Pastikan tidak ada masalah dengan jaringan.

08 Juli 2009 at 3:44 AM Tinggalkan Komentar

JNDI: Akses Mudah Ke Layanan Direktori

Kali ini aku akan mencoba latihan mengakses LDAP server melalui JNDI. Karena JNDI bawaan dari JDK sudah menyertakan service provider untuk akses LDAP, aku tidak perlu mendownload library apapun. Yang aku perlukan hanya memiliki sebuah LDAP server dan LDAP client (optional). Untuk platform Windows, aku memilih memakai Apache Directory Server, sebuah LDAP server open-source berbasis Java. Selain dapat dijalankan standalone, ApacheDS juga dapat dijalankan dalam program Java lain (embedded), seperti pada server JBoss.

Setelah meng-install dan menjalankan ApacheDS, aku perlu memasukkan data ke dalam schema di partisi yang aktif. Cara paling gampang adalah dengan menginstall sebuah LDAP client yang mendukung import data. Dan pilihanku untuk LDAP client jatuh pada Apache Directory Studio. Catatan samping: Apache Directory Studio adalah tools berbasis Eclipse RCP. Tampilannya akan mirip “Eclipse“. Sebenarnya, “Eclipse” adalah sebuah platform yang lahir dari sebuah proyek IBM untuk menyamakan user interface di produk mereka sehingga user tidak perlu bingung beradaptasi dengan look & feel yang berbeda di tiap produk. Yang populer adalah Eclipse sebagai IDE untuk pemograman Java (Eclipse JDT), tapi “Eclipse” tidak hanya itu, ia bisa berupa apa saja yang memiliki “tampilan” serupa.

Aku membuat koneksi baru di Apache Directory Studio dengan memilih menu LDAP, New Connection. Lalu aku mengisi parameter yang ada dengan nilai default dari ApacheDS, dimana hostname adalah localhost, port adalah 10389, dan no encryption. Bind DN yang dipakai adalah “uid=admin, ou=system” dan default password-nya adalah “secret“. Setelah koneksi terjalin secara sukses, aku meng-import file %APACHEDS_HOME%\conf\example.ldif. Format file LDIF adalah format standar untuk bertukar data yang didukung oleh semua server LDAP. Setelah itu, aku dapat menemukan DN “ou=Users,dc=example,dc=com” yang berisi 6 data user (3 lagi tidak bisa dipakai saat ini). Sekarang aku siap untuk men-query data ini melalui program Java.

Langkah pertama yang aku lakukan adalah menyiapkan context, seperti pada program berikut:

Hashtable<String,String> env = new Hashtable<String,String>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:10389");
env.put(Context.SECURITY_PRINCIPAL, "uid=admin,ou=system");
env.put(Context.SECURITY_CREDENTIALS, "secret");
    
try {
  InitialDirContext ctx = new InitialDirContext(env);
  ...
} catch (NamingException e) {
  e.printStackTrace();
}
    

Setelah itu, aku bisa mulai melakukan operasi, seperti menampilkan nama user (atribut cn) dan email (attribut mail) di setiap child dari “ou=Users, dc=example, dc=com“, seperti pada contoh berikut:

NamingEnumeration<NameClassPair> bindingEnum = ctx.list("ou=Users,dc=example,dc=com");
StringBuffer output = new StringBuffer();
while (bindingEnum.hasMore()) {
  NameClassPair nameClass = bindingEnum.next();
  Attributes attr = ctx.getAttributes(nameClass.getNameInNamespace());
  if (attr.get("cn")!=null) {
    output.append("Name = " + attr.get("cn").get() + "; ");
  }
  if (attr.get("mail")!=null) {
    output.append("Email = " + attr.get("mail").get() + "; ");
  }
  output.append("\n");
}
System.out.println(output.toString());
    

Output dari program di atas adalah:

Name = Max Planck; Email = mplanck@example.com; 
Name = Albert Einstein; Email = aeinstein@example.com; 
Name = Neils Bohr; Email = nbohr@example.com; 
Name = Max Born; Email = mborn@example.com; 
Name = Wolfgang Pauli; Email = wpauli@example.com; 
Name = Marie Curie; Email = mcurie@example.com;
    

Untuk melakukan pencarian melalui filter yang didefinisikan dalam RFC 2254, aku dapat membuat kode program seperti berikut:


InitialDirContext ctx = new InitialDirContext(env);
StringBuffer output = new StringBuffer();
String strNama = JOptionPane.showInputDialog("Masukkan nama yang akan dicari:");
if (strNama!=null && strNama.length()>0) {
  SearchControls ctl = new SearchControls();
  ctl.setReturningAttributes(new String[] {"cn","mail"});
  NamingEnumeration<SearchResult> en = 
    ctx.search("ou=Users,dc=example,dc=com", "(&(cn=*" + strNama + "*)(mail=*))", ctl);

  if (!en.hasMore()) {
    System.out.println("Nama tidak ditemukan dalam server LDAP!");
    System.exit(0);
  }
  while (en.hasMore()) {
    Attributes attr = en.next().getAttributes();
    output.append("Nama = " + attr.get("cn").get() + "; ");
    output.append("Email = " + attr.get("mail").get() + "; ");
    output.append("\n");
  }
  
  System.out.println(output.toString());
}
    

08 Juli 2009 at 3:42 AM Tinggalkan Komentar

Full-screen Exclusive API: Menggambar Satu Layar Penuh

Tidak semua program puas menggambar di komponen Swing. Passive rendering yang memakai method paint(Graphics g) tidak sesuai untuk program grafis yang harus memiliki performance tinggi. Contohnya adalah program game, yang umumnya memakai full-screen active rendering. Umumnya, program seperti game memakai bantuan API DirectX atau OpenGL. Java juga mendukung penggambaran full-screen dengan double-buffering berupa page-flipping. Tapi karena Java dirancang untuk multi-platform, tidak seluruh platform mendukung fitur ini. Aku dapat memeriksanya dengan kode seperti berikut:

GraphicsEnvironment env = GraphicsEnvironment.
  getLocalGraphicsEnvironment();
GraphicsDevice gd = env.getDefaultScreenDevice();
if (gd.isFullScreenSupported()) {
  System.out.println("Fullscreen API is supported.");
} else {
  System.out.println("Fullscreen API is not supported.");
}
if (gd.isDisplayChangeSupported()) {
  System.out.println("Display resolution change is supported.");
} else {
  System.out.println("Display resolution change is not supported.");
}

DisplayMode dm = gd.getDisplayMode();
System.out.format("Display resolution %d x %d (%d bit, refresh rate %d Hz)",
  dm.getWidth(), dm.getHeight(), dm.getBitDepth(), dm.getRefreshRate());

Sekarang aku akan latihan membuat sebuah program yang menggambar full screen sekumpulan lingkaran dengan posisi dan warna yang acak, seperti berikut:

import java.awt.Color;
import java.awt.Graphics;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.image.BufferStrategy;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.JFrame;

public class Main extends JFrame {
  
  private static final long serialVersionUID = 4648172894076113183L;
  
  public Main() {
    GraphicsEnvironment env = GraphicsEnvironment.getLocalGraphicsEnvironment();
    GraphicsDevice device = env.getDefaultScreenDevice();
  
    setUndecorated(false);
    setResizable(false);
    setIgnoreRepaint(true);
    device.setFullScreenWindow(this);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    createBufferStrategy(2);
    BufferStrategy buffer = getBufferStrategy();
    
    Graphics g;
    List<LingkaranWarna> lstLingkaran = new ArrayList<LingkaranWarna>();
    while (true) {
      if (lstLingkaran.size()<100) {
        lstLingkaran.add(new LingkaranWarna());
      } else {
        lstLingkaran.clear();
      }
      g = buffer.getDrawGraphics();
      g.clearRect(0,0, getWidth(), getHeight());
      for (LingkaranWarna b : lstLingkaran) {
        b.gambar(g);
      }
      g.dispose();
      buffer.show();
      
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      } 
    }
    
  }

  protected class LingkaranWarna {

    private int lokasiX;
    private int lokasiY;
    private Color warna;
    private int sizeX;
    private int sizeY;
    
    public LingkaranWarna() {
      atributBaru();
    }
    
    private void atributBaru() {
      Random rand = new Random();
      lokasiX = rand.nextInt(1000);
      lokasiY = rand.nextInt(600);
      warna = new Color(rand.nextInt(255), rand.nextInt(255), rand.nextInt(255));
      sizeX = rand.nextInt(100);
      sizeY = rand.nextInt(100);
    }
    
    private void gambar(Graphics g) {
      g.setColor(warna);
      g.fillOval(lokasiX, lokasiY, sizeX, sizeY);
      atributBaru();
    }
  }
  public static void main(String[] args) {
    new Main();
  }

}

03 Juli 2009 at 6:56 AM Tinggalkan Komentar

Swing: Membuat Layout Manager Sendiri

Pada saat masih bekerja dulu, saya ditugaskan sebagai programmer di sebuah proyek berbasis Applet (Swing). Dan layaknya sebagai programmer baru, saya banyak membuat tampilan form atau melakukan modifikasi minor pada form yang sudah dibuat programmer lain. Dan itu semua harus aku lakukan melalui coding, tanpa bantuan visual editor. Dapat dibayangkan betapa banyak waktu yang terbuang untuk men-kode GridBagLayout dan memodifikasi kode yang sudah ada. Salah satu solusi yang aku tempuh dikemudian hari adalah dengan membuat generator (masih bukan editor ;) berbasis template. Tapi aku merasa masih ada yang kurang, pasti ada cara yang lebih baik untuk menghasilkan kode program Swing yang simple dan mudah di-maintain. Dan mungkin saja, jika aku masih bekerja, aku akan mencoba membuat layout manager. Tampilan untuk aplikasi cenderung memiliki pola yang sama dan sederhana, lagipula applet akan jarang di-resize, sehingga mungkin membuat layout manager bisa menjadi sebuah solusi yang baik. Berikut ini adalah contoh sebuah layout manager sederhana:

package latihan;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.LayoutManager2;
import java.awt.Point;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


public class DialogLayout implements LayoutManager2 {

  public static enum FunctionalType  {
    LABEL, INPUT, BUTTON
  }
  
  private Map<Component, ComponentInfo> mapComponent;
  private int currentLinePosition;
  private FunctionalType lastFunctionalType;
    
  public DialogLayout() {
    mapComponent = new HashMap<Component,ComponentInfo>();
    currentLinePosition = 0;
    lastFunctionalType = null;
  }
  
  @Override
  public void addLayoutComponent(Component comp, Object constraints) {
    if (constraints instanceof FunctionalType) {
      if (lastFunctionalType==FunctionalType.INPUT) {
        currentLinePosition++;
      }
      ComponentInfo cInfo = new ComponentInfo(currentLinePosition, (FunctionalType) constraints);
      mapComponent.put(comp, cInfo);
      lastFunctionalType = cInfo.getFunctionalType();
    } else {
      throw new IllegalArgumentException("Constraints for DialogLayout must be FunctionalType");
    }
  }

  @Override
  public float getLayoutAlignmentX(Container target) {
    return 0;
  }

  @Override
  public float getLayoutAlignmentY(Container target) {
    return 0;
  }

  @Override
  public void invalidateLayout(Container target) {
  }

  @Override
  public Dimension maximumLayoutSize(Container target) {
    return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
  }

  @Override
  public void addLayoutComponent(String name, Component comp) {
  }

  @Override
  public void layoutContainer(Container parent) {
    Component[] components = parent.getComponents();
    
    int totalWidth = parent.getWidth() - 15 ;
    int totalHeight = parent.getHeight();
    
    int labelLocation = 5;
    double labelWidth = 0.2 * totalWidth;
    
    int inputLocation = 5+(int)labelWidth+5;
    double inputWidth = 0.8 * totalWidth;
    
    Dimension size = new Dimension();
    Point location = new Point();
    int lineLocation;
    double preferredHeight = 20;
    
    // simpan Component yang berfungsi sebagai BUTTON untuk
    // diproses belakangan (sehingga bisa CENTER)
    List<Component> lstButtons = new ArrayList<Component>(); 
    int totalButtonsWidth = 0;
    int lineButtonsLocation = 0;
    
    for (Component c : components) {
      ComponentInfo cInfo = mapComponent.get(c);
      
        lineLocation = (int)(5 + cInfo.getLinePosition() * (preferredHeight+5)) ;
      
      if (cInfo.getFunctionalType()==FunctionalType.LABEL) {
        location.setLocation(labelLocation, lineLocation);
        size.setSize(labelWidth, preferredHeight);
      } else if (cInfo.getFunctionalType()==FunctionalType.INPUT) {
        location.setLocation(inputLocation, lineLocation);
        size.setSize(inputWidth, preferredHeight);
      } else if (cInfo.getFunctionalType()==FunctionalType.BUTTON) {
        lstButtons.add(c);
        totalButtonsWidth += c.getPreferredSize().getWidth();
        lineButtonsLocation = lineLocation;
      }
      
      c.setLocation(location);
      c.setSize(size);
      
    }
    
    int startLocation = (totalWidth - totalButtonsWidth)/2;
    
    for (Component c : lstButtons) {
      c.setSize(c.getPreferredSize());
      c.setLocation(startLocation, lineButtonsLocation);
      startLocation+=c.getPreferredSize().getWidth()+5;
    }
  }

  @Override
  public Dimension minimumLayoutSize(Container parent) {
    return new Dimension(500,500);
  }

  @Override
  public Dimension preferredLayoutSize(Container parent) {
    return new Dimension(500,500);
  }

  @Override
  public void removeLayoutComponent(Component comp) {
  }

  protected class ComponentInfo {
    private int linePosition;
    private FunctionalType functionalType;
    
    public ComponentInfo(int linePosition, FunctionalType type) {
      this.linePosition = linePosition;
      this.functionalType = type;
    }
    public int getLinePosition() {
      return linePosition;
    }
    public void setLinePosition(int linePosition) {
      this.linePosition = linePosition;
    }
    public FunctionalType getFunctionalType() {
      return functionalType;
    }
    public void setFunctionalType(FunctionalType functionalType) {
      this.functionalType = functionalType;
    }
  }
}
    

Untuk membuat tampilan form baru, seseorang hanya perlu memberikan kode seperti:

DialogLayout layout = new DialogLayout();
setLayout(layout);

add(lblNama, LABEL);
add(txtNama, INPUT);
add(lblAlamat, LABEL);
add(txtAlamat, INPUT);
add(lblTelepon, LABEL);
add(txtTelepon, INPUT);
add(btnTambah, BUTTON);
add(btnEdit, BUTTON);
add(btnHapus, BUTTON);
    

Kode program menjadi sangat sederhana dan mudah ditelurusi. Misalnya, aku bisa langsung tahu bahwa field Telepon akan ditampilkan setelah field Alamat dan sebelum kumpulan button. Selain itu, keuntungan lainnya adalah jika designer UI merasa ingin merubah peletakan dan jarak antar-komponen, programmer hanya perlu mengubah class DialogLayout saja, dan saat program dijalankan semua tampilan akan mengikuti perubahan tersebut.

01 Juli 2009 at 3:09 AM Tinggalkan Komentar

Swing: Membuat Thread Pekerja Keras Dan Tetap Responsif?

Setelah berulang kali membuat interface dengan Swing, aku cukup sering mendapatkan tulisan “Warning: Swing is not thread safe” di javadoc. Program dengan GUI Swing yang dibuat tanpa memperhatikan threading mungkin akan jalan dengan lancar, tapi ada kemungkinan ia akan bertingkah aneh suatu hari atau menjadi tidak responsif, dan kesalahan-kesalahan seperti ini sulit direproduksi. Dan masih berkaitan dengan thread, aku pernah membuat sebuah tool sederhana berbasis Swing untuk generate data yang butuh waktu cukup lama. Secara insting, yang ada dibenakku adalah membuat thread baru untuk proses yang lama tersebut. Tapi ternyata itu belum cukup, selama proses, tampilan GUI menjadi tidak responsif dan output diagnosa ke text area setiap item selesai diproses tidak muncul seperti yang aku harapkan.

Untuk mengatasi masalah ini, aku perlu mempelajari konsep threading di Swing. Ada tiga jenis thread disini, yaitu initial thread, event dispatch thread, dan worker thread (background thread). Initial thread adalah thread dimana aplikasi bermula, atau thread yang menjalankan method main. Event disptach thread adalah thread yang bertugas untuk menangani event-handling. Seluruh method milik komponen Swing yang tidak thread safe harus dijalankan di thread ini. Oleh sebab itu, umumnya initial thread akan mengerjakan SwingUtilities.invokeLater atau SwingUtilities.invokeAndWait, agar kode yang membentuk GUI (membuat frame, mengisi layout, dsb) berjalan di event dispatch thread. Contohnya adalah seperti:

public static void main (String args[]) {
  SwingUtilities.invokeLater(new Runnable() {
    public void run() {
      createAndShowGUI();
    }
  });
}
    

Untuk kode yang melakukan proses jangka panjang, aku harus mengerjakannya di worker thread. Jika dilakukan di event dispatch thread, maka GUI yang menjadi tidak responsif dan tidak sempat lagi menangangi event (karena sibuk mengerjakan proses jangka panjang tadi). Agar proses dikerjakan oleh worker thread, aku dapat menggunakan SwingWorker (baru di Java 6). Berikut ini adalah contohnya:

protected class TaskBerat extends SwingWorker<Void, Integer> {

  @Override
  protected Void doInBackground() throws Exception {
    for (int i=0; i<100; i++) {
      publish(i);
      Thread.sleep(100);
    }
    return null;
  }

  @Override
  protected void process(List<Integer> chunks) {
    for (Integer i : chunks) {
      txtOutput.append("Processing task #" + i + "\n");
    }
  }
}
    

Method doInBackground() akan dikerjakan oleh worker thread, sementara method process() akan dikerjakan oleh event dispatch thread. Aku menggunakan publish() di doInBackground() untuk mengirim nilai ke process(). Demi alasan performance, sekali pemanggilan process() mungkin akan melayani lebih dari satu publish(), dimana nilai yang di-publish disimpan dalam sebuah List. Karena process() dijalankan oleh event dispatch thread, aku bebas mengerjakan method milik Swing disini (sebenarnya method append() milik JTextArea termasuk thread-safe).

Satu lagi class yang berhubungan thread di Swing adalah class javax.swing.Timer. Perbedaannya dengan class java.util.Timer adalah Swing Timer dikerjakan oleh event dispatch thread, sehingga aman untuk memakai method milik komponen Swing di Swing Timer. Berikut ini contoh program yang menampilkan waktu saat ini:

public class LatihanTimer extends JFrame implements ActionListener {

  private JLabel lblWaktu;
	
  public LatihanTimer() {
   super("Latihan Timer");
		
   lblWaktu = new JLabel("[WAKTU]");
   add(lblWaktu);
   pack();
   setVisible(true);
		
   Timer timer = new Timer(1000, this);
   timer.start();
  }
	
  public static void main (String args[]) {
   SwingUtilities.invokeLater(new Runnable(){
		
     @Override
     public void run() {
       new LatihanTimer();
     }
			
   });
  }

  @Override
  public void actionPerformed(ActionEvent e) {
   lblWaktu.setText(DateFormat.getTimeInstance().format(new Date()));
  }
}

30 Juni 2009 at 8:57 PM Tinggalkan Komentar

Tulisan Lebih Lama


Arsip


Ikuti

Get every new post delivered to your Inbox.