Seperti yang Anda ketahui, seorang programmer sejati harus melakukan 3 hal dalam hidupnya: membuat bahasa pemrogramannya sendiri, menulis sistem operasinya sendiri dan membuat ORM-nya sendiri. Dan jika saya menulis bahasa itu sejak lama (mungkin saya akan memberi tahu Anda lain waktu), dan OS masih di depan, maka saya ingin memberi tahu Anda tentang ORM sekarang. Untuk lebih tepatnya, ini bukan tentang ORM itu sendiri, tetapi tentang implementasi satu fitur kecil, lokal dan, sepertinya, fitur yang sangat sederhana.
Bersama-sama kita akan melangkah jauh dari kegembiraan menemukan solusi sederhana untuk kepahitan kesadaran akan kerapuhan dan kesalahannya. Dari menggunakan API khusus publik hingga peretasan kotor. Dari "hampir tanpa refleksi" hingga "setinggi lutut di dalam byte-code interpreter."
Siapa yang peduli bagaimana menganalisis bytecode, kesulitan apa yang ditimpanya, dan betapa luar biasanya hasil yang bisa Anda dapatkan pada akhirnya, selamat datang di kucing.
Isi
1 - Bagaimana semuanya dimulai.
2-4 - Dalam perjalanan menuju bytecode.
5 - Siapa bytecode.
6 - Analisis itu sendiri. Demi bab ini segala sesuatu dikandung dan di dalamnya ada nyali.
7 - Apa lagi yang bisa selesai. Mimpi, Mimpi ...
Kata Penutup - Kata Penutup.
UPD: Segera setelah publikasi, bagian 6-8 hilang (demi semuanya dimulai). Diperbaiki
Bagian Satu Masalah
Bayangkan kita memiliki skema sederhana. Ada klien, dia punya beberapa akun. Salah satunya default. Selain itu, klien dapat memiliki beberapa kartu SIM dan masing-masing kartu SIM dapat diatur secara eksplisit, atau klien default dapat digunakan.

Berikut adalah bagaimana model ini dijelaskan dalam kode kami (menghilangkan getter / setter / konstruktor / ...).
@JdbcEntity(table = "CLIENT") public class Client { @JdbcId private Long id; @JdbcColumn private String name; @JdbcJoinedObject(localColumn = "DEFAULTACCOUNT") private Account defaultAccount; } @JdbcEntity(table = "ACCOUNT") public class Account { @JdbcId private Long id; @JdbcColumn private Long balance; @JdbcJoinedObject(localColumn = "CLIENT") private Client client; } @JdbcEntity(table = "CARD") public class Card { @JdbcId private Long id; @JdbcColumn private String msisdn; @JdbcJoinedObject(localColumn = "ACCOUNT") private Account account; @JdbcJoinedObject(localColumn = "CLIENT") private Client client; }
Dalam ORM sendiri, kami memiliki persyaratan untuk tidak adanya proxy (kami harus membuat turunan dari kelas khusus ini) dan satu permintaan. Dengan demikian, inilah sql yang dikirim ke database ketika mencoba untuk mendapatkan peta.
select CARD.id id, CARD.msisdn msisdn, ACCOUNT_2.id ACCOUNT_2_id, ACCOUNT_2.balance ACCOUNT_2_balance, CLIENT_3.id CLIENT_3_id, CLIENT_3.name CLIENT_3_name, CLIENT_1.id CLIENT_1_id, CLIENT_1.name CLIENT_1_name, ACCOUNT_4.id ACCOUNT_4_id, ACCOUNT_4.balance ACCOUNT_4_balance from CARD left outer join CLIENT CLIENT_1 on CARD.CLIENT = CLIENT_1.id left outer join ACCOUNT ACCOUNT_2 on CARD.ACCOUNT = ACCOUNT_2.id left outer join CLIENT CLIENT_3 on ACCOUNT_2.CLIENT = CLIENT_3.id left outer join ACCOUNT ACCOUNT_4 on CLIENT_1.DEFAULTACCOUNT = ACCOUNT_4.id;
Ups. Klien dan tagihan digandakan. Benar, jika Anda berpikir tentang hal ini, ini dapat dimengerti - bagaimanapun juga, kerangka kerjanya tidak tahu bahwa klien kartu dan klien akun kartu adalah klien yang sama. Dan permintaan harus dihasilkan secara statis dan hanya satu (ingat pembatasan pada keunikan permintaan?).
Ngomong-ngomong, untuk alasan yang persis sama, tidak ada bidang Card.client.defaultAccount.client
dan Card.client.defaultAccount.client
di sini sama sekali. Hanya kami yang tahu bahwa client
dan client.defaultAccount.client
selalu cocok. Dan kerangka itu tidak tahu, baginya ini adalah tautan arbitrer. Dan apa yang harus dilakukan dalam kasus seperti itu tidak terlalu jelas. Saya tahu 3 opsi:
- Jelaskan invarian secara eksplisit dalam anotasi.
- Membuat pertanyaan rekursif (
with recursive
/ connect by
). - Untuk mencetak gol.
Tebak opsi mana yang kami pilih? Benar Akibatnya, semua bidang rekursif tidak diisi sama sekali sekarang dan selalu ada nol.
Tetapi jika Anda melihat lebih dekat, Anda dapat melihat masalah kedua di belakang duplikasi, dan itu jauh lebih buruk. Apa yang kita inginkan? Nomor kartu dan saldo. Apa yang kamu dapatkan 4 bergabung dan 10 kolom. Dan hal ini tumbuh secara eksponensial! Baik i.e. kami benar-benar memiliki situasi di mana, pertama, demi keindahan dan integritas, kami sepenuhnya menggambarkan model pada anotasi, dan kemudian, demi 5 bidang , permintaan untuk 15 bergabung dan 150 kolom diminta. Dan pada saat ini menjadi sangat menakutkan.
Bagian Dua Solusi yang berfungsi tetapi tidak nyaman
Sebuah solusi sederhana segera memohon. Hanya pengeras suara yang akan digunakan yang harus diseret! Mudah dikatakan. Pilihan yang paling jelas (untuk menulis pilihan dengan tangan Anda) kami akan segera menjatuhkannya. Nah, belum, kami jelaskan modelnya agar tidak menggunakannya. Dahulu kala metode khusus dibuat - partialGet
. Ini, tidak seperti get
sederhana, menerima List<String>
- nama bidang yang harus diisi. Untuk melakukan ini, Anda harus terlebih dahulu mendaftarkan alias di tabel
@JdbcJoinedObject(localColumn = "ACCOUNT", sqlTableAlias = "a") private Account account; @JdbcJoinedObject(localColumn = "CLIENT", sqlTableAlias = "c") private Client client;
Dan kemudian nikmati hasilnya.
List<String> requiredColumns = asList("msisdn", "c_a_balance", "a_balance"); String query = cardMapper.getSelectSQL(requiredColumns, DatabaseType.ORACLE); System.out.println(query);
select CARD.msisdn msisdn, c_a.balance c_a_balance, a.balance a_balance from CARD left outer join ACCOUNT a on CARD.ACCOUNT = a.id left outer join CLIENT c on CARD.CLIENT = c.id left outer join ACCOUNT c_a on c.DEFAULTACCOUNT = c_a.id;
Dan semuanya tampak baik-baik saja, tetapi, pada kenyataannya, tidak. Begini cara itu akan digunakan dalam kode nyata.
Card card = cardDAO.partialGet(cardId, "msisdn", "c_a_balance", "a_balance"); ... ... ... ... ... ... long clientId = card.getClient().getId();
Dan ternyata sekarang Anda bisa menggunakan partialGet hanya jika jarak antara itu dan hasilnya hanya beberapa baris. Tetapi jika hasilnya jauh atau, Tuhan melarang, dilewatkan dalam beberapa metode, maka sudah sangat sulit untuk memahami bidang mana yang diisi dan mana yang tidak. Selain itu, jika NPE terjadi di suatu tempat, maka Anda masih perlu memahami apakah itu benar-benar kembali dari database nol, atau apakah kami tidak mengisi bidang ini. Semua dalam semua, sangat tidak bisa diandalkan.
Anda dapat, tentu saja, hanya menulis objek lain dengan pemetaan Anda secara khusus untuk permintaan tersebut, atau bahkan sepenuhnya memilih semuanya dengan tangan Anda dan merakitnya menjadi beberapa Tuple
. Sebenarnya, pada kenyataannya sekarang di sebagian besar tempat kita melakukan hal itu. Tapi tetap saja saya ingin tidak menulis pilihan dengan tangan saya, dan tidak menduplikasi pemetaan.
Bagian tiga. Solusi yang mudah namun tidak bisa digunakan.
Jika Anda berpikir lebih banyak, maka dengan cepat jawabannya muncul di benak saya - Anda perlu menggunakan antarmuka. Maka deklarasikan saja
public interface MsisdnAndBalance { String getMsisdn(); long getBalance(); }
Dan gunakan
MsisdnAndBalance card = cardDAO.partialGet(cardId, ...);
Dan itu saja. Jangan menelepon sesuatu yang ekstra. Apalagi dengan transisi ke Kotlin / sepuluh / lomb, bahkan tipe mengerikan ini bisa dihilangkan. Tapi di sini poin paling penting masih dihilangkan. Argumen apa yang harus diteruskan ke partialGet
? Tali, seperti sebelumnya, tidak terasa seperti lagi, karena risikonya terlalu besar untuk membuat kesalahan dan menulis bidang yang salah. Dan saya ingin Anda bisa melakukannya
MsisdnAndBalance card = cardDAO.partialGet(cardId, MsisdnAndBalance.class);
Atau bahkan lebih baik di Kotlin melalui generik reified
val card = cardDAO.paritalGet<MsisdnAndBalance>(cardId)
Ehh, kesalahan besar. Sebenarnya, keseluruhan cerita selanjutnya adalah implementasi dari opsi ini.
Bagian Empat Dalam perjalanan ke bytecode
Masalah utamanya adalah metode berasal dari antarmuka, dan anotasi ada di atas bidang. Dan kita perlu menemukan bidang yang sama ini dengan metode. Pikiran pertama dan yang paling jelas adalah menggunakan konvensi Java Bean standar. Dan untuk properti sepele, ini bahkan berfungsi. Namun ternyata sangat tidak stabil. Sebagai contoh, ada baiknya mengganti nama metode dalam antarmuka (melalui refactoring ideologis), karena semuanya langsung berantakan. Idenya cukup pintar untuk mengubah nama metode di kelas implementasi, tetapi tidak cukup untuk memahami bahwa itu adalah pengambil dan Anda perlu mengganti nama bidang itu sendiri. Dan solusi serupa mengarah pada duplikasi bidang. Sebagai contoh, jika saya memerlukan metode getClientId()
di antarmuka saya, maka saya tidak dapat mengimplementasikannya dengan satu-satunya cara yang benar
public class Client implements HasClientId { private Long id; @Override public Long getClientId() { return id; } }
public class Card implements HasClientId { private Client client; @Override public Long getClientId() { return client.getId(); } }
Dan saya harus menduplikasi bidang. Dan di Client
seret id
dan clientId
, dan di peta di sebelah klien secara eksplisit clientId
. Dan pastikan semua ini tidak pergi. Selain itu, saya juga ingin getter dengan logika non-sepele bekerja, misalnya
public class Card implements HasBalance { private Account account; private Client client; public long getBalance() { if (account != null) return account.getBalance(); else return client.getDefaultAccount().getBalance(); } }
Jadi opsi dengan pencarian berdasarkan nama tidak lagi diperlukan, Anda perlu sesuatu yang lebih rumit.
Pilihan selanjutnya benar-benar gila dan tidak hidup lama di kepalaku, tetapi demi kelengkapan cerita saya juga akan menggambarkannya. Pada tahap penguraian, kita dapat membuat entitas kosong dan cukup bergiliran menulis beberapa nilai di bidang, dan setelah itu kita mendapatkan getter dan melihatnya mengubah apa yang mereka kembalikan atau tidak. Jadi kita akan melihat bahwa dari catatan di bidang name
, nilai getClientId
tidak berubah, tetapi dari catatan id
- itu berubah. Selain itu, situasi ketika getter dan bidang dari berbagai jenis (seperti isActive() = i_active != 0
) secara otomatis didukung di sini. Tetapi setidaknya ada tiga masalah serius (mungkin lebih, tetapi saya tidak berpikir lebih jauh).
- Persyaratan yang jelas untuk esensi dengan algoritma ini adalah untuk mengembalikan nilai "sama" dari pengambil jika bidang "sesuai" tidak berubah. "Satu dan sama" - dari sudut pandang operator perbandingan yang telah kami pilih.
==
itu jelas tidak bisa (jika beberapa getAsInt() = Integer.parseInt(strField))
akan berhenti bekerja getAsInt() = Integer.parseInt(strField))
. Tetap sama. Jadi, jika pengambil mengembalikan semacam entitas pengguna yang dihasilkan oleh bidang pada setiap panggilan, maka ia harus memiliki penggantian yang equals
. - Pemetaan kompresi. Seperti pada contoh dengan
int -> boolean
atas. Jika kita memeriksa nilai 0 dan 1, maka kita akan melihat perubahan. Tetapi jika pada usia 40 dan 42, maka kedua kali kita menjadi kenyataan. - Mungkin ada konverter kompleks di getter yang mengandalkan invarian tertentu di bidang (misalnya, format string khusus). Dan pada data yang kami hasilkan, mereka akan memberikan pengecualian.
Jadi secara umum, opsi ini juga tidak berfungsi.
Dalam proses membahas semuanya, pada awalnya saya bercanda mengucapkan ungkapan "baiklah, nafig, lebih mudah untuk melihat bytecode, semuanya ditulis di sana." Pada saat itu, saya bahkan tidak menyadari bahwa ide ini akan menelan saya, dan seberapa jauh segalanya akan berjalan.
Bagian Lima Apa itu bytecode dan bagaimana cara kerjanya
new #4, dup, invokespecial #5, areturn
Jika Anda mengerti apa yang ditulis di sini dan apa kode ini, maka Anda dapat langsung beralih ke bagian selanjutnya .
Penafian 1. Sayangnya, untuk memahami cerita selanjutnya, Anda memerlukan setidaknya pemahaman dasar tentang seperti apa Java bytecode, jadi saya akan menulis beberapa paragraf tentang hal itu. Sama sekali tidak berpura-pura menjadi lengkap.
Penafian 2. Ini akan secara eksklusif tentang tubuh metode. Baik tentang kumpulan konstan, atau tentang struktur kelas secara keseluruhan, atau bahkan tentang metode deklarasi sendiri, saya akan mengatakan sepatah kata pun.
Hal utama yang perlu Anda pahami tentang bytecode adalah assembler ke mesin virtual Java stack. Ini berarti bahwa argumen untuk instruksi diambil dari stack dan nilai balik dari instruksi didorong kembali ke stack. Dari sudut pandang ini, kita dapat mengatakan bahwa bytecode ditulis dalam notasi Polandia terbalik . Selain tumpukan, metode ini juga memiliki berbagai variabel lokal. Setelah memasukkan metode, this
dan semua argumen dari metode ini ditulis ke dalamnya, dan variabel lokal disimpan di sana selama eksekusi. Ini adalah contoh sederhana.
public class Foo { private int bar; public int updateAndReturn(long baz, String str) { int result = (int) baz; result += str.length(); bar = result; return result; } }
Saya akan menulis komentar dalam format
# [(<local_variable_index>:<actual_value>)*], [(<value_on_stack>)*]
Atas tumpukan di sebelah kiri.
public int updateAndReturn(long, java.lang.String); Code: # [0:this, 1:long baz, 3:str], () 0: lload_1 # [0:this, 1:long baz, 3:str], (long baz) 1: l2i # [0:this, 1:long baz, 3:str], (int baz) 2: istore 4 # [0:this, 1:long baz, 3:str, 4:int baz], () 4: iload 4 # [0:this, 1:long baz, 3:str, 4:int baz], (int baz) 6: aload_3 # [0:this, 1:long baz, 3:str, 4:int baz], (str, int baz) 7: invokevirtual #2 // Method java/lang/String.length:()I # [0:this, 1:long baz, 3:str, 4:int baz], (length(str), int baz) 10: iadd # [0:this, 1:long baz, 3:str, 4:int baz], (length(str) + int baz) 11: istore 4 # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], () 13: aload_0 # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], (this) 14: iload 4 # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], (length(str) + int baz, this) 16: putfield #3 // Field bar:I # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], (), bar 19: iload 4 # [0:this, 1:long baz, 3:str, 4:length(str) + int baz], (length(str) + int baz) 21: ireturn # int ,
Ada banyak instruksi. Daftar lengkap perlu dilihat di bab keenam JVMS , di Wikipedia ada pengulangan singkat . Sejumlah besar instruksi saling menduplikasi untuk tipe yang berbeda (misalnya, iload
untuk int dan lload
lama). Juga, untuk bekerja dengan 4 variabel lokal pertama, instruksi mereka disoroti (misalnya, misalnya, ada lload_1
dan tidak menerima argumen sama sekali, tetapi hanya lload
, itu akan mengambil jumlah variabel lokal sebagai argumen. Dalam contoh di atas, ada iload
sama).
Secara global, kami akan tertarik pada kelompok instruksi berikut:
*load*
, *store*
- baca / tulis variabel lokal*aload
, *astore
- baca / tulis elemen array dengan indeksgetfield
, putfield
- bidang baca / tulisgetstatic
, putstatic
- baca / tulis bidang statischeckcast
- dilemparkan di antara tipe objek. Perlu karena nilai yang diketik terletak pada stack dan dalam variabel lokal. Misalnya, di atas adalah l2i untuk para pemain yang panjang -> int.invoke*
- pemanggilan metode*return
- mengembalikan nilai dan keluar dari metode
Bagian Enam Rumah
Bagi mereka yang melewatkan pengantar yang begitu panjang, serta untuk mengalihkan perhatian dari masalah dan alasan asli dalam hal perpustakaan, kami merumuskan masalah lebih tepat.
Perlu, memiliki contoh java.lang.reflect.Method
di tangan, untuk mendapatkan daftar semua bidang non-statis (baik objek saat ini dan semua objek bersarang) yang bacaannya (langsung atau secara transitif) akan berada di dalam metode ini.
Misalnya, untuk metode seperti itu
public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); }
Anda perlu mendapatkan daftar dua bidang: client.defaultAccount.balance
dan client.defaultAccount.balance
.
Saya akan menulis, jika mungkin, solusi umum. Tetapi di beberapa tempat Anda harus menggunakan pengetahuan tentang masalah asli untuk menyelesaikan masalah yang tidak terselesaikan, dalam kasus umum, masalah.
Pertama, Anda perlu mendapatkan bytecode dari body metode itu sendiri, tetapi Anda tidak bisa melakukan ini secara langsung melalui Java. Tapi sejak itu Karena metode ini awalnya ada di dalam beberapa kelas, lebih mudah untuk mendapatkan kelas itu sendiri. Secara global, saya tahu dua opsi: masuk dalam proses pemuatan kelas dan mencegat byte[]
sudah dibaca byte[]
sana, atau cukup menemukan file ClassName.class
pada disk dan membacanya. Intersepsi memuat di tingkat perpustakaan biasa tidak dapat dilakukan. Anda harus menghubungkan javaagent atau menggunakan ClassLoader kustom. Bagaimanapun, langkah-langkah tambahan diperlukan untuk mengkonfigurasi jvm / aplikasi, dan ini tidak nyaman. Anda bisa melakukannya dengan lebih mudah. Semua kelas "biasa" selalu dalam file yang sama dengan ekstensi ".class", path yang merupakan paket kelas. Ya, itu tidak akan bekerja untuk menemukan kelas yang ditambahkan secara dinamis atau kelas yang dimuat oleh beberapa classloader kustom, tetapi kita membutuhkan ini untuk model jdbc, jadi kita dapat mengatakan dengan keyakinan bahwa semua kelas akan dikemas dalam "cara default" dalam toples. Total:
private static InputStream getClassFile(Class<?> clazz) { String file = clazz.getName().replace('.', '/') + ".class"; ClassLoader cl = clazz.getClassLoader(); if (cl == null) return ClassLoader.getSystemResourceAsStream(file); else return cl.getResourceAsStream(file); }
Hore, baca array byte. Apa yang akan kita lakukan selanjutnya? Pada prinsipnya, ada beberapa perpustakaan di Jawa untuk membaca / menulis bytecode, tetapi ASM biasanya digunakan untuk pekerjaan tingkat paling rendah. Karena itu diasah untuk kinerja tinggi dan operasi on-the-fly, API pengunjung adalah yang utama di sana - asm secara berurutan membaca kelas dan menarik metode yang sesuai
public abstract class ClassVisitor { public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {...} public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {...} public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {...} ... } public abstract class MethodVisitor { protected MethodVisitor mv; public MethodVisitor(final int api, final MethodVisitor mv) { ... this.mv = mv; } public void visitJumpInsn(int opcode, Label label) { if (mv != null) { mv.visitJumpInsn(opcode, label); } } ... }
Pengguna diundang untuk mendefinisikan kembali metode yang menarik baginya dan menulis analisis / transformasi logika sendiri di sana. Secara terpisah, pada contoh MethodVisitor
, saya ingin menarik perhatian pada kenyataan bahwa semua pengunjung memiliki implementasi standar melalui delegasi.
Selain api utama, ada juga API Pohon di luar kotak. Jika Core API adalah analog dari parser SAX, maka Tree API adalah analog dari DOM. Kami mendapatkan objek di dalamnya tempat semua informasi tentang kelas / metode disimpan dan kami bisa menganalisanya sesuai keinginan dengan lompatan ke tempat mana pun. Bahkan, api ini adalah implementasi *Visitor
yang di dalam metode visit*
hanya menyimpan informasi. Hampir semua metode di sana terlihat seperti ini:
public class MethodNode extends MethodVisitor { @Override public void visitJumpInsn(final int opcode, final Label label) { instructions.add(new JumpInsnNode(opcode, getLabelNode(label))); } ... }
Sekarang kita akhirnya dapat memuat metode untuk analisis.
private static class AnalyzerClassVisitor extends ClassVisitor { private final String getterName; private final String getterDesc; private MethodNode methodNode; public AnalyzerClassVisitor(Method getter) { super(ASM6); this.getterName = getter.getName(); this.getterDesc = getMethodDescriptor(getter); } public MethodNode getMethodNode() { if (methodNode == null) throw new IllegalStateException(); return methodNode; } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
Metode membaca kode lengkap.MethodNode
tidak dikembalikan secara langsung, tetapi pembungkus dengan sepasang ext. bidang karena kita akan membutuhkannya nanti juga. Titik masuk (dan satu-satunya metode publik) adalah readMethod(Method): MethodInfo
.
public class MethodReader { public static class MethodInfo { private final String internalDeclaringClassName; private final int classAccess; private final MethodNode methodNode; public MethodInfo(String internalDeclaringClassName, int classAccess, MethodNode methodNode) { this.internalDeclaringClassName = internalDeclaringClassName; this.classAccess = classAccess; this.methodNode = methodNode; } public String getInternalDeclaringClassName() { return internalDeclaringClassName; } public int getClassAccess() { return classAccess; } public MethodNode getMethodNode() { return methodNode; } } public static MethodInfo readMethod(Method method) { Class<?> clazz = method.getDeclaringClass(); String internalClassName = getInternalName(clazz); try (InputStream is = getClassFile(clazz)) { ClassReader cr = new ClassReader(is); AnalyzerClassVisitor cv = new AnalyzerClassVisitor(internalClassName, method); cr.accept(cv, SKIP_DEBUG | SKIP_FRAMES); return new MethodInfo(internalClassName, cv.getAccess(), cv.getMethodNode()); } catch (IOException e) { throw new RuntimeException(e); } } private static InputStream getClassFile(Class<?> clazz) { String file = clazz.getName().replace('.', '/') + ".class"; ClassLoader cl = clazz.getClassLoader(); if (cl == null) return ClassLoader.getSystemResourceAsStream(file); else return cl.getResourceAsStream(file); } private static class AnalyzerClassVisitor extends ClassVisitor { private final String className; private final String getterName; private final String getterDesc; private MethodNode methodNode; private int access; public AnalyzerClassVisitor(String internalClassName, Method getter) { super(ASM6); this.className = internalClassName; this.getterName = getter.getName(); this.getterDesc = getMethodDescriptor(getter); } public MethodNode getMethodNode() { if (methodNode == null) throw new IllegalStateException(); return methodNode; } public int getAccess() { return access; } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (!name.equals(className)) throw new IllegalStateException(); this.access = access; } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { if (!name.equals(getterName) || !desc.equals(getterDesc)) return null; return new AnalyzerMethodVisitor(access, name, desc, signature, exceptions); } private class AnalyzerMethodVisitor extends MethodVisitor { public AnalyzerMethodVisitor(int access, String name, String desc, String signature, String[] exceptions) { super(ASM6, new MethodNode(ASM6, access, name, desc, signature, exceptions)); } @Override public void visitEnd() { if (methodNode != null) throw new IllegalStateException(); methodNode = (MethodNode) mv; } } } }
Saatnya melakukan analisis secara langsung. Bagaimana cara membuatnya? Pikiran pertama adalah untuk melihat semua instruksi getfield
. Setiap getfield
secara statis mengatakan bidang mana itu dan kelas mana. Ini dapat dianggap seperlunya semua bidang kelas kami yang ada aksesnya. Tetapi, sayangnya, ini tidak berhasil. Masalah pertama di sini adalah kelebihannya ditangkap.
class Foo { private int bar; private int baz; public int test() { return bar + new Foo().baz; } }
Dengan algoritma ini, kami menganggap bahwa bidang baz diperlukan, walaupun, pada kenyataannya, tidak. Namun masalah ini masih bisa dicetak. Tetapi apa yang harus dilakukan dengan metode?
public class Client implements HasClientId { private Long id; public Long getId() { HasClientId obj = this; return obj.getClientId(); } @Override public Long getClientId() { return id; } }
Jika kita mencari panggilan metode dengan cara yang sama seperti kita mencari bidang bacaan, maka kita tidak akan menemukan getClientId
. Karena tidak ada panggilan ke Client.getClientId
, tetapi hanya panggilan ke HasClientId.getClientId
. Tentu saja, Anda dapat mempertimbangkan semua metode pada kelas saat ini, semua kacamata super dan semua antarmuka untuk digunakan, tetapi ini sudah terlalu banyak. Jadi, Anda dapat secara tidak sengaja menangkap toString
, dan di dalamnya ada daftar semua bidang secara umum.
Selain itu, kami ingin panggilan getter pada objek bersarang berfungsi juga
public class Account { private Client client; public long getClientId() { return client.getId(); } }
Dan di sini panggilan ke metode Client.getId
sekali tidak berlaku untuk kelas Account
.
Dengan keinginan yang kuat, Anda masih bisa memikirkan peretasan untuk kasus-kasus khusus untuk beberapa waktu, tetapi cukup cepat sampai pada pemahaman bahwa "hal-hal tidak dilakukan seperti itu" dan Anda perlu memonitor sepenuhnya alur eksekusi dan pergerakan data. getfield
, this
, - this
. Berikut ini sebuah contoh:
class Client { public long id; } class Account { public long id; public Client client; public long test() { return client.id + new Account().id; } }
class Account { public Client client; public long test(); Code: 0: aload_0 1: getfield #2 // Field client:LClient; 4: getfield #3 // Field Client.id:J 7: new #4 // class Account 10: dup 11: invokespecial #5 // Method "<init>":()V 14: getfield #6 // Field id:J 17: ladd 18: lreturn }
1: getfield
this
, aload_0
.4: getfield
â , 1: getfield
, , , this
.14: getfield
. Karena , ( Account
), this
, , 7: new
.
, Account.client.id
, Account.id
â . , , .
â , , aload_0
getfield
this
, , . , . . â ! -, . MethodNode
, ( ). , .. (//) .
:
public class Analyzer<V extends Value> { public Analyzer(final Interpreter<V> interpreter) {...} public Frame<V>[] analyze(final String owner, final MethodNode m) {...} }
Analyzer
( Frame
, ) . , , , , //etc.
public abstract class Interpreter<V extends Value> { public abstract V newValue(Type type); public abstract V newOperation(AbstractInsnNode insn) throws AnalyzerException; public abstract V copyOperation(AbstractInsnNode insn, V value) throws AnalyzerException; public abstract V unaryOperation(AbstractInsnNode insn, V value) throws AnalyzerException; public abstract V binaryOperation(AbstractInsnNode insn, V value1, V value2) throws AnalyzerException; public abstract V ternaryOperation(AbstractInsnNode insn, V value1, V value2, V value3) throws AnalyzerException; public abstract V naryOperation(AbstractInsnNode insn, List<? extends V> values) throws AnalyzerException; public abstract void returnOperation(AbstractInsnNode insn, V value, V expected) throws AnalyzerException; public abstract V merge(V v, V w); }
V
â , , . Analyzer
, , , . , getfield
â , , . , unaryOperation(AbstractInsnNode insn, V value): V
, . 1: getfield
Value
, " client
, Client
", 14: getfield
" â - , ".
merge(V v, V w): V
. , , . Sebagai contoh:
public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); }
Account.getBalance()
. - . . ? merge
.
â SuperInterpreter extends Interpreter<SuperValue>
? Benar SuperValue
. â , . , .
public class Value extends BasicValue { private final Set<Ref> refs; private Value(Type type, Set<Ref> refs) { super(type); this.refs = refs; } } public class Ref { private final List<Field> path; private final boolean composite; public Ref(List<Field> path, boolean composite) { this.path = path; this.composite = composite; } }
composite
. , . , String
. String.length()
, , name
, name.value.length
. , length
â , , arraylength
. ? Tidak! â . , , , . , Date
, String
, Long
, . , , .
class Persion { @JdbcColumn(converter = CustomJsonConverter.class) private PassportInfo passportInfo; }
PassportInfo
. , . , composite
. .
public class Ref { private final List<Field> path; private final boolean composite; public Ref(List<Field> path, boolean composite) { this.path = path; this.composite = composite; } public List<Field> getPath() { return path; } public boolean isComposite() { return composite; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Ref ref = (Ref) o; return Objects.equals(path, ref.path); } @Override public int hashCode() { return Objects.hash(path); } @Override public String toString() { if (path.isEmpty()) return "<[this]>"; else return "<" + path.stream().map(Field::getName).collect(joining(".")) + ">"; } public static Ref thisRef() { return new Ref(emptyList(), true); } public static Optional<Ref> childRef(Ref parent, Field field, Configuration configuration) { if (!parent.isComposite()) return empty(); if (parent.path.contains(field))
public class Value extends BasicValue { private final Set<Ref> refs; private Value(Type type, Set<Ref> refs) { super(type); this.refs = refs; } public Set<Ref> getRefs() { return refs; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Value value = (Value) o; return Objects.equals(refs, value.refs); } @Override public int hashCode() { return Objects.hash(super.hashCode(), refs); } @Override public String toString() { return "(" + refs.stream().map(Object::toString).collect(joining(",")) + ")"; } public static Value typedValue(Type type, Ref ref) { return new Value(type, singleton(ref)); } public static Optional<Value> childValue(Value parent, Value child) { Type type = child.getType(); Set<Ref> fields = parent.refs.stream() .flatMap(p -> child.refs.stream().map(c -> childRef(p, c))) .filter(Optional::isPresent) .map(Optional::get) .collect(toSet()); if (fields.isEmpty()) return empty(); return of(new Value(type, fields)); } public static Optional<Value> childValue(Value parent, FieldInsnNode childInsn, Configuration configuration) { Type type = Type.getType(childInsn.desc); Field child = resolveField(childInsn); Set<Ref> fields = parent.refs.stream() .map(p -> childRef(p, child, configuration)) .filter(Optional::isPresent) .map(Optional::get) .collect(toSet()); if (fields.isEmpty()) return empty(); return of(new Value(type, fields)); } public static Value mergeValues(Collection<Value> values) { List<Type> types = values.stream().map(BasicValue::getType).distinct().collect(toList()); if (types.size() != 1) { String typesAsString = types.stream().map(Type::toString).collect(joining(", ", "(", ")")); throw new IllegalStateException("could not merge " + typesAsString); } Set<Ref> fields = values.stream().flatMap(v -> v.refs.stream()).distinct().collect(toSet()); return new Value(types.get(0), fields); } public static boolean isComposite(BasicValue value) { return value instanceof Value && value.getType().getSort() == Type.OBJECT && ((Value) value).refs.stream().anyMatch(Ref::isComposite); } }
, . Ayo pergi!
public class FieldsInterpreter extends BasicInterpreter {
, BasicInterpreter
. BasicValue
( , Value
extends BasicValue
) .
public class BasicValue implements Value { public static final BasicValue UNINITIALIZED_VALUE = new BasicValue(null); public static final BasicValue INT_VALUE = new BasicValue(Type.INT_TYPE); public static final BasicValue FLOAT_VALUE = new BasicValue(Type.FLOAT_TYPE); public static final BasicValue LONG_VALUE = new BasicValue(Type.LONG_TYPE); public static final BasicValue DOUBLE_VALUE = new BasicValue(Type.DOUBLE_TYPE); public static final BasicValue REFERENCE_VALUE = new BasicValue(Type.getObjectType("java/lang/Object")); public static final BasicValue RETURNADDRESS_VALUE = new BasicValue(Type.VOID_TYPE); private final Type type; public BasicValue(final Type type) { this.type = type; } }
( (Value)basicValue
) , , ( " iconst
") .
newValue
. , , " ". , this
catch
. , , . BasicInterpreter
BasicValue(actualType)
BasicValue.REFERENCE_VALUE
. .
@Override public BasicValue newValue(Type type) { if (type != null && type.getSort() == OBJECT) return new BasicValue(type); return super.newValue(type); }
entry point. this
. , - , , this
, BasicValue(actualType)
, Value.typedValue(actualType, Ref.thisRef())
. , , this
newValue
, . , .. , this
. this
. , . , this
0. , . , , . .
@Override public BasicValue copyOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException { if (wasUpdated || insn.getType() != VAR_INSN || ((VarInsnNode) insn).var != 0) { return super.copyOperation(insn, value); } switch (insn.getOpcode()) { case ALOAD: return typedValue(value.getType(), thisRef()); case ISTORE: case LSTORE: case FSTORE: case DSTORE: case ASTORE: wasUpdated = true; } return super.copyOperation(insn, value); }
. . , , , â , . , .
@Override public BasicValue merge(BasicValue v, BasicValue w) { if (v.equals(w)) return v; if (v instanceof Value || w instanceof Value) { if (!Objects.equals(v.getType(), w.getType())) { if (v == UNINITIALIZED_VALUE || w == UNINITIALIZED_VALUE) return UNINITIALIZED_VALUE; throw new IllegalStateException("could not merge " + v + " and " + w); } if (v instanceof Value != w instanceof Value) { if (v instanceof Value) return v; else return w; } return mergeValues(asList((Value) v, (Value) w)); } return super.merge(v, w); }
. ""? ? Tidak juga. . , .. . , 3 ( ): putfield
, putstatic
, aastore
. . putstatic
( ) . , . putfield
aastore
. , , . ( ) . , . , â .
public class Account { private Client client; public Long getClientId() { return Optional.ofNullable(client).map(Client::getId).orElse(null); } }
, ( ofNullable
Optional
client
value
), . . . , - ofNullable(client)
, - map(Client::getId)
, .
putfield
, putstatic
aastore
.
@Override public BasicValue binaryOperation(AbstractInsnNode insn, BasicValue value1, BasicValue value2) throws AnalyzerException { if (insn.getOpcode() == PUTFIELD && Value.isComposite(value2)) { throw new IllegalStateException("could not trace " + value2 + " over putfield"); } return super.binaryOperation(insn, value1, value2); } @Override public BasicValue ternaryOperation(AbstractInsnNode insn, BasicValue value1, BasicValue value2, BasicValue value3) throws AnalyzerException { if (insn.getOpcode() == AASTORE && Value.isComposite(value3)) { throw new IllegalStateException("could not trace " + value3 + " over aastore"); } return super.ternaryOperation(insn, value1, value2, value3); } @Override public BasicValue unaryOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException { if (Value.isComposite(value)) { switch (insn.getOpcode()) { case PUTSTATIC: { throw new IllegalStateException("could not trace " + value + " over putstatic"); } ... } } return super.unaryOperation(insn, value); }
. checkcast
. : . â
Client client1 = ...; Object objClient = client1; Client client2 = (Client) objClient;
, . , , client1
objClient
, . , checkcast
.
.
class Foo { private List<?> list; public void trimToSize() { ((ArrayList<?>) list).trimToSize(); } }
. , , , . , , , , , . ? , ! . , , , null/0/false. . â
@JdbcJoinedObject(localColumn = "CLIENT") private Client client;
, , ORM , . checkcast
@Override public BasicValue unaryOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException { if (Value.isComposite(value)) { switch (insn.getOpcode()) { ... case CHECKCAST: { Class<?> original = reflectClass(value.getType()); Type targetType = getObjectType(((TypeInsnNode) insn).desc); Class<?> afterCast = reflectClass(targetType); if (afterCast.isAssignableFrom(original)) { return value; } else { throw new IllegalStateException("type specification not supported"); } } } } return super.unaryOperation(insn, value); }
â getfield
. â ?
class Foo { private Foo child; public Foo test() { Foo loopedRef = this; while (ThreadLocalRandom.current().nextBoolean()) { loopedRef = loopedRef.child; } return loopedRef; } }
, . ? child
, child.child
, child.child.child
? ? , . , . ,
null.
child, null, , , . Ref.childRef
if (parent.path.contains(field)) return empty();
. , .
" ". . , . , , ( @JdbcJoinedObject
, @JdbcColumn
), , . ORM .
, getfield
, . , , , . â .
@Override public BasicValue unaryOperation(AbstractInsnNode insn, BasicValue value) throws AnalyzerException { if (Value.isComposite(value)) { switch (insn.getOpcode()) { ... case GETFIELD: { Optional<Value> optionalFieldValue = childValue((Value) value, (FieldInsnNode) insn, configuration); if (!optionalFieldValue.isPresent()) break; Value fieldValue = optionalFieldValue.get(); if (configuration.isInterestingField(resolveField((FieldInsnNode) insn))) { context.addUsedField(fieldValue); } if (Value.isComposite(fieldValue)) { return fieldValue; } break; } ... } } return super.unaryOperation(insn, value); }
. , , . , invoke*
. , , , . , :
public long getClientId() { return getClient().getId(); }
, , . , . . , . ? . . .
class Account implements HasClient { @JdbcJoinedObject private Client client; public Client getClient() { return client; } }
Account.client
. , . . â , .
public static class Result { private final Set<Value> usedFields; private final Value returnedCompositeValue; }
? , . . , .. ( , â ), , areturn
, , , *return
. MethodNode
( , Tree API) . . â . , ? . .
private static Value getReturnedCompositeValue(Frame<BasicValue>[] frames, AbstractInsnNode[] insns) { Set<Value> resultValues = new HashSet<>(); for (int i = 0; i < insns.length; i++) { AbstractInsnNode insn = insns[i]; switch (insn.getOpcode()) { case IRETURN: case LRETURN: case FRETURN: case DRETURN: case ARETURN: BasicValue value = frames[i].getStack(0); if (Value.isComposite(value)) { resultValues.add((Value) value); } break; } } if (resultValues.isEmpty()) return null; return mergeValues(resultValues); }
analyzeField
public static Result analyzeField(Method method, Configuration configuration) { if (Modifier.isNative(method.getModifiers())) throw new IllegalStateException("could not analyze native method " + method); MethodInfo methodInfo = readMethod(method); MethodNode mn = methodInfo.getMethodNode(); String internalClassName = methodInfo.getInternalDeclaringClassName(); int classAccess = methodInfo.getClassAccess(); Context context = new Context(method, classAccess); FieldsInterpreter interpreter = new FieldsInterpreter(context, configuration); Analyzer<BasicValue> analyzer = new Analyzer<>(interpreter); try { analyzer.analyze(internalClassName, mn); } catch (AnalyzerException e) { throw new RuntimeException(e); } Frame<BasicValue>[] frames = analyzer.getFrames(); AbstractInsnNode[] insns = mn.instructions.toArray(); Value returnedCompositeValue = getReturnedCompositeValue(frames, insns); return new Result(context.getUsedFields(), returnedCompositeValue); }
, -, . invoke*
. 5 :
invokespecial
â . , , ( super.call()
).invokevirtual
â . . , .invokeinterface
â , invokevirtual
, â .invokestatic
âinvokedynamic
â , 7 JSR 292. JVM, invokedynamic
( dynamic). , (+ ), . , Invokedynamic: ? .
, , , . invokedynamic
, . , , , (, ), invokedynamic
. , "" . , invokedynamic
, .
. , . , . , this
, 0? , - , FieldsInterpreter
copyOperation
. , MethodAnalyzer.analyzeFields
" this
" " " ( this
â ). , . , , . , - . , (- Optional.ofNullable(client)
). .
, invokestatic
(.. , this
). invokespecial
, invokevirtual
invokeinterface
. , . , , jvm. invokespecial
, , . invokevirtual
invokeinterface
. , .
public String objectToString(Object obj) { return obj.toString(); }
public static java.lang.String objectToString(java.lang.Object); Code: 0: aload_0 1: invokevirtual #104 // Method java/lang/Object.toString:()Ljava/lang/String; 4: areturn
, , ( ) . , , . ? ORM . ORM , , . invokevirtual
invokeinterface
.
Hore! . Apa selanjutnya , ( , this
), ( , ) . !
@Override public BasicValue naryOperation(AbstractInsnNode insn, List<? extends BasicValue> values) throws AnalyzerException { Method method = null; Value methodThis = null; switch (insn.getOpcode()) { case INVOKESPECIAL: {...} case INVOKEVIRTUAL: {...} case INVOKEINTERFACE: { if (Value.isComposite(values.get(0))) { MethodInsnNode methodNode = (MethodInsnNode) insn; Class<?> objectClass = reflectClass(values.get(0).getType()); Method interfaceMethod = resolveInterfaceMethod(reflectClass(methodNode.owner), methodNode.name, getMethodType(methodNode.desc)); method = lookupInterfaceMethod(objectClass, interfaceMethod); methodThis = (Value) values.get(0); } List<?> badValues = values.stream().skip(1).filter(Value::isComposite).collect(toList()); if (!badValues.isEmpty()) throw new IllegalStateException("could not pass " + badValues + " as parameter"); break; } case INVOKESTATIC: case INVOKEDYNAMIC: { List<?> badValues = values.stream().filter(Value::isComposite).collect(toList()); if (!badValues.isEmpty()) throw new IllegalStateException("could not pass " + badValues + " as parameter"); break; } } if (method != null) { MethodAnalyzer.Result methodResult = analyzeFields(method, configuration); for (Value usedField : methodResult.getUsedFields()) { childValue(methodThis, usedField).ifPresent(context::addUsedField); } if (methodResult.getReturnedCompositeValue() != null) { Optional<Value> returnedValue = childValue(methodThis, methodResult.getReturnedCompositeValue()); if (returnedValue.isPresent()) { return returnedValue.get(); } } } return super.naryOperation(insn, values); }
. , . , JVMS 1 1. . â . , , . , .. , - , 2 â . , , . , â . , ResolutionUtil LookupUtil .
!
.
, 80% 20% 20% 80% . , , , ?
. .
. (.. ), . , . , .
public class Account { private Client client; public Long getClientId() { return Optional.ofNullable(client).map(Client::getId).orElse(null); } }
Optional
, ofNullable
getClientId
, , value
. , returnedCompositeValue
â , , . , , ( ) , . -. , , , " value
Optional@1234
Client@5678
" .
invokedynamic
, . indy , . , . , . , invokedynamic. . . , java.lang.invoke.LambdaMetafactory.metafactory
. , , , . java.lang.invoke.StringConcatFactory.makeConcat/makeConcatWithConstants
. . toString()
. , , , , . , , , /- . jvm, . , , . . . , , . indy . ? indy , â CallSite
. . , , LambdaMetafactory.metafactory
getValue
, . getValue
. ( ) . , , , stateless. , ! - , . CallSite
ConstantCallSite
, MutableCallSite
VolatileCallSite
. mutable volatile , , ConstantCallSite
. "- ". , , . , VM, .
Kata penutup
- , . - partialGet
. , . , , , , " " .
, .