Panduan untuk Instrumentasi Java

1. Perkenalan

Dalam tutorial ini, kita akan berbicara tentang Java Instrumentation API. Ini memberikan kemampuan untuk menambahkan kode byte ke kelas Java terkompilasi yang ada.

Kami juga akan berbicara tentang agen java dan cara kami menggunakannya untuk melengkapi kode kami.

2. Penyiapan

Sepanjang artikel, kami akan membuat aplikasi menggunakan instrumentasi.

Aplikasi kita akan terdiri dari dua modul:

  1. Aplikasi ATM yang memungkinkan kita menarik uang
  2. Dan agen Java yang akan memungkinkan kita mengukur kinerja ATM kita dengan mengukur waktu pengeluaran uang yang diinvestasikan

Agen Java akan memodifikasi kode byte ATM yang memungkinkan kami mengukur waktu penarikan tanpa harus mengubah aplikasi ATM.

Proyek kami akan memiliki struktur berikut:

com.baeldung.instrumentation base 1.0.0 pom  agent application 

Sebelum membahas detail instrumentasi, mari kita lihat apa itu java agent.

3. Apa Itu Agen Java

Secara umum, agen java hanyalah file jar yang dibuat khusus. Ini menggunakan API Instrumentasi yang disediakan JVM untuk mengubah kode byte yang ada yang dimuat dalam JVM.

Agar agen bisa bekerja, kita perlu mendefinisikan dua metode:

  • premain - akan memuat agen secara statis menggunakan parameter -javaagent pada startup JVM
  • agentmain - akan memuat agen secara dinamis ke JVM menggunakan Java Attach API

Konsep menarik untuk diingat adalah bahwa implementasi JVM, seperti Oracle, OpenJDK, dan lainnya, dapat menyediakan mekanisme untuk memulai agen secara dinamis, tetapi ini bukan persyaratan.

Pertama, mari kita lihat bagaimana kita akan menggunakan agen Java yang sudah ada.

Setelah itu, kita akan melihat bagaimana kita bisa membuatnya dari awal untuk menambahkan fungsionalitas yang kita butuhkan dalam kode byte kita.

4. Memuat Agen Java

Untuk dapat menggunakan agen Java, kita harus memuatnya terlebih dahulu.

Kami memiliki dua jenis beban:

  • static - menggunakan premain untuk memuat agen menggunakan opsi -javaagent
  • dynamic - memanfaatkan agentmain untuk memuat agen ke JVM menggunakan Java Attach API

Selanjutnya, kita akan melihat setiap jenis beban dan menjelaskan cara kerjanya.

4.1. Beban Statis

Memuat agen Java saat startup aplikasi disebut beban statis. Beban statis mengubah kode byte pada waktu startup sebelum kode apa pun dieksekusi.

Ingatlah bahwa beban statis menggunakan metode premain , yang akan berjalan sebelum kode aplikasi apa pun berjalan, untuk menjalankannya, kita dapat mengeksekusi:

java -javaagent:agent.jar -jar application.jar

Penting untuk dicatat bahwa kita harus selalu meletakkan parameter - javaagent sebelum parameter - jar .

Di bawah ini adalah log untuk perintah kami:

22:24:39.296 [main] INFO - [Agent] In premain method 22:24:39.300 [main] INFO - [Agent] Transforming class MyAtm 22:24:39.407 [main] INFO - [Application] Starting ATM application 22:24:41.409 [main] INFO - [Application] Successful Withdrawal of [7] units! 22:24:41.410 [main] INFO - [Application] Withdrawal operation completed in:2 seconds! 22:24:53.411 [main] INFO - [Application] Successful Withdrawal of [8] units! 22:24:53.411 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

Kita dapat melihat kapan metode premain dijalankan dan kapan kelas MyAtm diubah. Kami juga melihat dua log transaksi penarikan ATM yang berisi waktu yang dibutuhkan untuk menyelesaikan setiap operasi.

Ingatlah bahwa dalam aplikasi asli kami, kami tidak memiliki waktu penyelesaian transaksi ini, itu ditambahkan oleh agen Java kami.

4.2. Beban Dinamis

Prosedur memuat agen Java ke JVM yang sudah berjalan disebut beban dinamis. Agen dilampirkan menggunakan Java Attach API.

Skenario yang lebih kompleks adalah ketika kita sudah menjalankan aplikasi ATM dalam produksi dan kita ingin menambahkan total waktu transaksi secara dinamis tanpa downtime untuk aplikasi kita.

Mari kita tulis sedikit kode untuk melakukannya dan kita akan menamakan kelas ini AgentLoader. Untuk mempermudah, kami akan meletakkan kelas ini di file jar aplikasi. Jadi file jar aplikasi kita dapat memulai aplikasi kita, dan melampirkan agen kita ke aplikasi ATM:

VirtualMachine jvm = VirtualMachine.attach(jvmPid); jvm.loadAgent(agentFile.getAbsolutePath()); jvm.detach();

Sekarang setelah kita memiliki AgentLoader , kita memulai aplikasi kita memastikan bahwa dalam jeda sepuluh detik antar transaksi, kita akan melampirkan agen Java kita secara dinamis menggunakan AgentLoader .

Mari tambahkan juga lem yang memungkinkan kita memulai aplikasi atau memuat agen.

Kami akan memanggil Peluncur kelas ini dan itu akan menjadi kelas file jar utama kami:

public class Launcher { public static void main(String[] args) throws Exception { if(args[0].equals("StartMyAtmApplication")) { new MyAtmApplication().run(args); } else if(args[0].equals("LoadAgent")) { new AgentLoader().run(args); } } }

Memulai Aplikasi

java -jar application.jar StartMyAtmApplication 22:44:21.154 [main] INFO - [Application] Starting ATM application 22:44:23.157 [main] INFO - [Application] Successful Withdrawal of [7] units!

Melampirkan Agen Java

Setelah operasi pertama, kami memasang agen java ke JVM kami:

java -jar application.jar LoadAgent 22:44:27.022 [main] INFO - Attaching to target JVM with PID: 6575 22:44:27.306 [main] INFO - Attached to target JVM and loaded Java agent successfully 

Periksa Log Aplikasi

Sekarang setelah kami memasang agen kami ke JVM, kami akan melihat bahwa kami memiliki waktu penyelesaian total untuk operasi penarikan ATM kedua.

Ini berarti kami menambahkan fungsionalitas kami dengan cepat, saat aplikasi kami berjalan:

22:44:27.229 [Attach Listener] INFO - [Agent] In agentmain method 22:44:27.230 [Attach Listener] INFO - [Agent] Transforming class MyAtm 22:44:33.157 [main] INFO - [Application] Successful Withdrawal of [8] units! 22:44:33.157 [main] INFO - [Application] Withdrawal operation completed in:2 seconds!

5. Membuat Agen Java

Setelah mempelajari cara menggunakan agen, mari kita lihat bagaimana kita bisa membuatnya. Kami akan melihat cara menggunakan Javassist untuk mengubah kode byte dan kami akan menggabungkan ini dengan beberapa metode API instrumentasi.

Karena agen java menggunakan Java Instrumentation API, sebelum terlalu jauh membuat agen kami, mari kita lihat beberapa metode yang paling sering digunakan dalam API ini dan deskripsi singkat tentang apa yang mereka lakukan:

  • addTransformer - menambahkan trafo ke mesin instrumentasi
  • getAllLoadedClasses - mengembalikan array dari semua kelas yang saat ini dimuat oleh JVM
  • retransformClasses - memfasilitasi instrumentasi kelas yang sudah dimuat dengan menambahkan kode byte
  • removeTransformer - membatalkan registrasi trafo yang disediakan
  • redefineClasses - mendefinisikan ulang set kelas yang disediakan menggunakan file kelas yang disediakan, yang berarti bahwa kelas tersebut akan diganti sepenuhnya, tidak dimodifikasi seperti retransformClasses

5.1. Create the Premain and Agentmain Methods

We know that every Java agent needs at least one of the premain or agentmain methods. The latter is used for dynamic load, while the former is used to statically load a java agent into a JVM.

Let's define both of them in our agent so that we're able to load this agent both statically and dynamically:

public static void premain( String agentArgs, Instrumentation inst) { LOGGER.info("[Agent] In premain method"); String className = "com.baeldung.instrumentation.application.MyAtm"; transformClass(className,inst); } public static void agentmain( String agentArgs, Instrumentation inst) { LOGGER.info("[Agent] In agentmain method"); String className = "com.baeldung.instrumentation.application.MyAtm"; transformClass(className,inst); }

In each method, we declare the class that we want to change and then dig down to transform that class using the transformClass method.

Below is the code for the transformClass method that we defined to help us transform MyAtm class.

In this method, we find the class we want to transform and using the transform method. Also, we add the transformer to the instrumentation engine:

private static void transformClass( String className, Instrumentation instrumentation) { Class targetCls = null; ClassLoader targetClassLoader = null; // see if we can get the class using forName try { targetCls = Class.forName(className); targetClassLoader = targetCls.getClassLoader(); transform(targetCls, targetClassLoader, instrumentation); return; } catch (Exception ex) { LOGGER.error("Class [{}] not found with Class.forName"); } // otherwise iterate all loaded classes and find what we want for(Class clazz: instrumentation.getAllLoadedClasses()) { if(clazz.getName().equals(className)) { targetCls = clazz; targetClassLoader = targetCls.getClassLoader(); transform(targetCls, targetClassLoader, instrumentation); return; } } throw new RuntimeException( "Failed to find class [" + className + "]"); } private static void transform( Class clazz, ClassLoader classLoader, Instrumentation instrumentation) { AtmTransformer dt = new AtmTransformer( clazz.getName(), classLoader); instrumentation.addTransformer(dt, true); try { instrumentation.retransformClasses(clazz); } catch (Exception ex) { throw new RuntimeException( "Transform failed for: [" + clazz.getName() + "]", ex); } }

With this out of the way, let's define the transformer for MyAtm class.

5.2. Defining Our Transformer

A class transformer must implement ClassFileTransformer and implement the transform method.

We'll use Javassist to add byte-code to MyAtm class and add a log with the total ATW withdrawal transaction time:

public class AtmTransformer implements ClassFileTransformer { @Override public byte[] transform( ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { byte[] byteCode = classfileBuffer; String finalTargetClassName = this.targetClassName .replaceAll("\\.", "/"); if (!className.equals(finalTargetClassName)) { return byteCode; } if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) { LOGGER.info("[Agent] Transforming class MyAtm"); try { ClassPool cp = ClassPool.getDefault(); CtClass cc = cp.get(targetClassName); CtMethod m = cc.getDeclaredMethod( WITHDRAW_MONEY_METHOD); m.addLocalVariable( "startTime", CtClass.longType); m.insertBefore( "startTime = System.currentTimeMillis();"); StringBuilder endBlock = new StringBuilder(); m.addLocalVariable("endTime", CtClass.longType); m.addLocalVariable("opTime", CtClass.longType); endBlock.append( "endTime = System.currentTimeMillis();"); endBlock.append( "opTime = (endTime-startTime)/1000;"); endBlock.append( "LOGGER.info(\"[Application] Withdrawal operation completed in:" + "\" + opTime + \" seconds!\");"); m.insertAfter(endBlock.toString()); byteCode = cc.toBytecode(); cc.detach(); } catch (NotFoundException | CannotCompileException | IOException e) { LOGGER.error("Exception", e); } } return byteCode; } }

5.3. Creating an Agent Manifest File

Finally, in order to get a working Java agent, we'll need a manifest file with a couple of attributes.

Karenanya, kami dapat menemukan daftar lengkap atribut manifes di dokumentasi resmi Paket Instrumentasi.

Di file jar agen Java terakhir, kami akan menambahkan baris berikut ke file manifes:

Agent-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent Can-Redefine-Classes: true Can-Retransform-Classes: true Premain-Class: com.baeldung.instrumentation.agent.MyInstrumentationAgent

Agen instrumentasi Java kami sekarang sudah lengkap. Untuk menjalankannya, lihat bagian Memuat Agen Java di artikel ini.

6. Kesimpulan

Pada artikel ini, kami berbicara tentang Java Instrumentation API. Kami melihat cara memuat agen Java ke JVM baik secara statis maupun dinamis.

Kami juga melihat bagaimana kami akan membuat agen Java kami sendiri dari awal.

Seperti biasa, implementasi lengkap dari contoh ini dapat ditemukan di Github.