Panduan Instrumentasi Java

1. Pengenalan

Dalam tutorial ini, kita akan membincangkan mengenai Java Instrumentation API. Ini memberikan kemampuan untuk menambahkan kod byte ke kelas Java yang sudah dikompilasi.

Kami juga akan membincangkan ejen java dan bagaimana kami menggunakannya untuk menyusun kod kami.

2. Persediaan

Sepanjang artikel ini, kami akan membuat aplikasi menggunakan instrumentasi.

Aplikasi kami akan terdiri daripada dua modul:

  1. Aplikasi ATM yang membolehkan kita mengeluarkan wang
  2. Dan ejen Java yang akan membolehkan kita mengukur prestasi ATM kita dengan mengukur masa melaburkan wang

Ejen Java akan mengubah kod ATM byte yang membolehkan kita mengukur masa pengeluaran tanpa perlu mengubah aplikasi ATM.

Projek kami akan mempunyai struktur berikut:

com.baeldung.instrumentation base 1.0.0 pom  agent application 

Sebelum membincangkan secara terperinci mengenai instrumen, mari kita lihat apa itu agen java.

3. Apa itu Ejen Java

Secara amnya, ejen java hanyalah fail balang yang dibuat khas. Ini menggunakan API Instrumentasi yang disediakan oleh JVM untuk mengubah kod byte yang ada yang dimuat dalam JVM.

Agar ejen dapat bekerja, kita perlu menentukan dua kaedah:

  • premain - akan memuatkan ejen secara statik menggunakan parameter -javaagent pada permulaan JVM
  • agentmain - akan memuatkan ejen ke dalam JVM secara dinamis menggunakan Java Attach API

Konsep menarik yang perlu diingat adalah bahawa pelaksanaan JVM, seperti Oracle, OpenJDK, dan lain-lain, dapat menyediakan mekanisme untuk memulakan ejen secara dinamis, tetapi itu bukan syarat.

Pertama, mari kita lihat bagaimana kita menggunakan ejen Java yang ada.

Selepas itu, kita akan melihat bagaimana kita dapat membuatnya dari awal untuk menambahkan fungsi yang kita perlukan dalam kod byte kita.

4. Memuatkan Ejen Java

Untuk dapat menggunakan ejen Java, kita mesti memuatkannya terlebih dahulu.

Kami mempunyai dua jenis beban:

  • statik - menggunakan premain untuk memuatkan ejen menggunakan pilihan -javaagent
  • dinamik - menggunakan agen utama untuk memuatkan ejen ke dalam JVM menggunakan Java Attach API

Seterusnya, kita akan melihat setiap jenis beban dan menerangkan bagaimana ia berfungsi.

4.1. Beban Statik

Memuatkan ejen Java pada permulaan aplikasi disebut beban statik. Beban statik mengubah kod byte pada masa permulaan sebelum mana-mana kod dijalankan.

Perlu diingat bahawa beban statik menggunakan kaedah premain , yang akan dijalankan sebelum kod aplikasi berjalan, untuk menjalankannya kita dapat melaksanakannya:

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

Penting untuk diperhatikan bahawa kita harus selalu meletakkan parameter - javaagent sebelum parameter - jar .

Berikut adalah log untuk arahan 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 kaedah premain dijalankan dan kapan kelas MyAtm diubah. Kami juga melihat dua log transaksi pengeluaran ATM yang mengandungi masa yang diperlukan untuk menyelesaikan setiap operasi.

Ingatlah bahawa dalam aplikasi asal kami tidak mempunyai masa penyelesaian untuk transaksi, ia ditambahkan oleh ejen Java kami.

4.2. Beban Dinamik

Prosedur memuatkan ejen Java ke JVM yang sudah berjalan disebut beban dinamik. Ejen dilampirkan menggunakan Java Attach API.

Senario yang lebih rumit adalah ketika kita sudah menjalankan aplikasi ATM dalam pengeluaran dan kita ingin menambahkan jumlah masa transaksi secara dinamik tanpa waktu henti untuk aplikasi kita.

Mari tulis sepotong kecil kod untuk melakukan perkara itu dan kami akan memanggil kelas ini sebagai AgentLoader. Untuk kesederhanaan, kami akan memasukkan kelas ini ke dalam fail balang aplikasi. Oleh itu, fail jar aplikasi kami boleh memulakan permohonan kami, dan melampirkan ejen kami ke aplikasi ATM:

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

Sekarang kita mempunyai AgentLoader , kita memulakan aplikasi dengan memastikan bahawa dalam jeda sepuluh saat antara transaksi, kita akan melampirkan ejen Java kita secara dinamis menggunakan AgentLoader .

Mari juga tambahkan gam yang akan membolehkan kita memulakan aplikasi atau memuatkan ejen.

Kami akan memanggil Pelancar kelas ini dan ini akan menjadi kelas fail 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); } } }

Memulakan Permohonan

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 Ejen Java

Selepas operasi pertama, kami melampirkan ejen 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

Setelah kami melampirkan ejen kami ke JVM, kami akan melihat bahawa kami mempunyai masa penyelesaian untuk operasi pengeluaran ATM kedua.

Ini bermaksud bahawa kami menambahkan fungsi kami dengan cepat, semasa 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 Ejen Java

Setelah mengetahui cara menggunakan ejen, mari lihat bagaimana kita boleh membuatnya. Kami akan melihat bagaimana menggunakan Javassist untuk menukar kod byte dan kami akan menggabungkannya dengan beberapa kaedah API instrumentasi.

Oleh kerana ejen java menggunakan Java Instrumentation API, sebelum terlalu dalam membuat ejen kami, mari kita lihat beberapa kaedah yang paling banyak digunakan dalam API ini dan penerangan ringkas mengenai apa yang mereka lakukan:

  • addTransformer - menambah transformer ke mesin instrumentasi
  • getAllLoadedClasses - mengembalikan pelbagai semua kelas yang dimuatkan oleh JVM
  • retransformClasses - memudahkan instrumentasi kelas yang sudah dimuat dengan menambahkan kod byte
  • removeTransformer - nyahdaftar pengubah yang dibekalkan
  • redefineClasses - mentakrifkan semula sekumpulan kelas yang disediakan menggunakan fail kelas yang dibekalkan, yang bermaksud bahawa kelas akan diganti sepenuhnya, tidak diubah suai dengan 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.

Oleh itu, kita dapat mencari senarai lengkap atribut nyata dalam dokumentasi rasmi Pakej Instrumentasi.

Dalam fail jar ejen Java terakhir, kami akan menambahkan baris berikut ke fail manifes:

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

Ejen instrumentasi Java kami kini lengkap. Untuk menjalankannya, lihat memuat bahagian Java Agent artikel ini.

6. Kesimpulannya

Dalam artikel ini, kami membincangkan mengenai Java Instrumentation API. Kami melihat bagaimana memuatkan ejen Java ke dalam JVM secara statik dan dinamik.

Kami juga melihat bagaimana kami membuat ejen Java kami sendiri dari awal.

Seperti biasa, pelaksanaan contoh yang lengkap dapat dilihat di Github.