كما تعلم ، يجب أن يقوم المبرمج الحقيقي بثلاثة أشياء في حياته: إنشاء لغة البرمجة الخاصة به ، وكتابة نظام التشغيل الخاص به وإنشاء ORM الخاص به. وإذا كتبت اللغة منذ فترة طويلة (ربما سأخبرك ببعض الوقت الآخر) ، ولا يزال نظام التشغيل متقدمًا ، فأود أن أخبرك عن ORM في الوقت الحالي. لكي نكون أكثر دقة ، لا يتعلق الأمر حتى بـ ORM نفسه ، ولكن حول تنفيذ ميزة واحدة صغيرة محلية ومحلية ، كما يبدو ، ميزة بسيطة تمامًا.
سنذهب معًا على طول الطريق من فرحة إيجاد حل بسيط إلى مرارة الوعي بهشاشته وعدم صحته. من استخدام واجهة برمجة تطبيقات عامة حصرية إلى عمليات اختراق قذرة. من "بدون انعكاس تقريبًا" إلى "عمق الركبة في مترجم كود البايت".
من يهتم بكيفية تحليل البايت كود ، وما هي الصعوبات المشحونة به ، وما هي النتيجة المذهلة التي يمكنك الحصول عليها في النهاية ، مرحبًا بك في القط.
المحتويات
1 - كيف بدأ كل شيء.
2-4 - في الطريق إلى البايت كود.
5 - من هو الرمز الثانوي.
6 - التحليل نفسه. من أجل هذا الفصل ، تم تصور كل شيء وكان فيه الشجاعة.
7 - ماذا يمكن أن ينتهي. أحلام ، أحلام ...
خاتمة - خاتمة.
حدث: مباشرة بعد النشر ، فقدت أجزاء 6-8 (من أجل كل شيء بدأ). ثابت.
الجزء الأول المشكلة
تخيل أن لدينا مخطط بسيط. هناك عميل لديه عدة حسابات. واحد منهم تخلف عن السداد. أيضًا ، يمكن أن يكون لدى العميل عدة بطاقات SIM ويمكن تعيين كل بطاقة SIM بشكل صريح ، أو يمكن استخدام عميل افتراضي.

إليك كيفية وصف هذا النموذج في الكود الخاص بنا (حذف الحروف / المستوطنين / المنشئين / ...).
@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; }
في ORM نفسها ، لدينا مطلب لغياب الوكلاء (يجب علينا إنشاء مثيل من هذه الفئة المعينة) وطلب واحد. وفقًا لذلك ، إليك ما يتم إرسال SQL إلى قاعدة البيانات عند محاولة الحصول على خريطة.
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;
عفوًا. يتم تكرار العميل والفاتورة. صحيح ، إذا كنت تفكر في ذلك ، فهذا أمر مفهوم - بعد كل شيء ، لا يعرف إطار العمل أن عميل البطاقة وعميل حساب البطاقة هما نفس العميل. ويجب إنشاء الطلب بشكل ثابت وواحد فقط (تذكر القيد على تفرد الطلب؟).
بالمناسبة ، للسبب نفسه بالضبط ، لا توجد حقول Card.client.defaultAccount.client
و Card.client.defaultAccount.client
هنا على الإطلاق. نحن نعلم فقط أن client
. client.defaultAccount.client
دائمًا. والإطار لا يعرفه ، فهذه صلة تعسفية. وما يجب القيام به في مثل هذه الحالات ليس واضحًا جدًا. أعرف 3 خيارات:
- وصف الثوابت بشكل صريح في التعليقات التوضيحية.
- عمل استعلامات عودية (
with recursive
/ connect by
). - ليسجل.
خمن أي خيار اخترناه؟ حق. نتيجة لذلك ، لا يتم ملء جميع الحقول العودية على الإطلاق الآن ، وهناك دائمًا فارغة.
ولكن إذا نظرت عن كثب ، يمكنك رؤية المشكلة الثانية وراء الازدواجية ، وهي أسوأ بكثير. ماذا نريد؟ رقم البطاقة ورصيدها. على ماذا حصلت؟ 4 وصلات و 10 أعمدة. وهذا الشيء ينمو بشكل كبير! حسنًا ، أي لدينا بالفعل موقف حيث ، أولاً ، من أجل الجمال والنزاهة ، وصفنا تمامًا النموذج على التعليقات التوضيحية ، وبعد ذلك ، من أجل 5 حقول ، يتم طلب 15 رابطًا و 150 عمودًا . وفي هذه اللحظة يصبح الأمر مخيفًا حقًا.
الجزء الثاني حل عملي ولكن غير مريح
يطرح حل بسيط على الفور. يجب فقط سحب مكبرات الصوت التي سيتم استخدامها! من السهل القول. الخيار الأكثر وضوحا (لكتابة التحديد بيديك) سوف نسقط على الفور. حسنًا ، ليس بعد ذلك ، وصفنا النموذج حتى لا نستخدمه. منذ وقت طويل تم عمل طريقة خاصة - partialGet
. وهي ، على عكس get
ببساطة ، تقبل List<String>
- أسماء الحقول المراد ملؤها. للقيام بذلك ، يجب عليك أولاً تسجيل الأسماء المستعارة في الجداول
@JdbcJoinedObject(localColumn = "ACCOUNT", sqlTableAlias = "a") private Account account; @JdbcJoinedObject(localColumn = "CLIENT", sqlTableAlias = "c") private Client client;
ثم استمتع بالنتيجة.
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;
ويبدو أن كل شيء على ما يرام ، ولكن في الحقيقة لا. إليك كيفية استخدامه في الكود الحقيقي.
Card card = cardDAO.partialGet(cardId, "msisdn", "c_a_balance", "a_balance"); ... ... ... ... ... ... long clientId = card.getClient().getId();
وتبين أنه لا يمكنك الآن استخدام PartGet إلا إذا كانت المسافة بينه واستخدام النتيجة هي بضعة أسطر فقط. ولكن إذا كانت النتيجة بعيدة أو تم تمريرها ، لا سمح الله ، في بعض الطرق ، فمن الصعب للغاية بالفعل فهم الحقول التي يتم ملؤها وأيها لا. علاوة على ذلك ، إذا حدث NPE في مكان ما ، فأنت لا تزال بحاجة إلى فهم ما إذا كان قد عاد حقًا من قاعدة البيانات الفارغة ، أو ما إذا كنا لم نملأ هذا الحقل. بشكل عام ، لا يمكن الاعتماد عليه.
يمكنك ، بالطبع ، فقط كتابة كائن آخر مع رسم الخرائط الخاص بك خصيصًا للطلب ، أو حتى تحديد الشيء بالكامل بيديك وتجميعه في Tuple
. في الواقع ، في الواقع الآن في معظم الأماكن نفعل ذلك بالضبط. ولكن ما زلت أود عدم كتابة التحديدات بيدي ، وعدم تكرار الخرائط.
الجزء الثالث. حل مناسب ولكنه غير عملي.
إذا كنت تفكر أكثر من ذلك بقليل ، فحينئذٍ تتبادر إلى ذهني الإجابة بسرعة - فأنت بحاجة إلى استخدام الواجهات. ثم أعلن فقط
public interface MsisdnAndBalance { String getMsisdn(); long getBalance(); }
واستخدم
MsisdnAndBalance card = cardDAO.partialGet(cardId, ...);
هذا كل ما في الأمر. لا تدعو أي شيء إضافي. علاوة على ذلك ، مع الانتقال إلى Kotlin / ten / lomb ، يمكن التخلص من هذا النوع الرهيب. ولكن هنا لا يزال يتم حذف أهم نقطة. ما الحجج التي يجب تمريرها إلى partialGet
؟ الثياب ، كما كان من قبل ، لم تعد تشعر ، لأن الخطر أكبر من أن يرتكب أخطاء ويكتب الحقول الخاطئة. وأريدك أن تكون قادرًا بطريقة أو بأخرى
MsisdnAndBalance card = cardDAO.partialGet(cardId, MsisdnAndBalance.class);
أو حتى أفضل على Kotlin من خلال الأدوية الجنسية
val card = cardDAO.paritalGet<MsisdnAndBalance>(cardId)
إيه ، خطأ فادح. في الواقع ، القصة الكاملة الأخرى هي على وجه التحديد تنفيذ هذا الخيار.
الجزء الرابع في الطريق إلى البايت كود
المشكلة الرئيسية هي أن الأساليب تأتي من الواجهة ، والتعليقات التوضيحية أعلى الحقول. ونحتاج إلى إيجاد هذه الحقول نفسها بالطرق. الفكرة الأولى والأكثر وضوحًا هي استخدام اصطلاح Java Bean القياسي. وبالنسبة للخصائص التافهة ، هذا يعمل حتى. لكن تبين أنها غير مستقرة للغاية. على سبيل المثال ، يجدر إعادة تسمية طريقة في واجهة (من خلال إعادة التنظيم الإيديولوجي) ، حيث ينهار كل شيء على الفور. الفكرة ذكية بما يكفي لإعادة تسمية الأساليب في فصول التنفيذ ، ولكنها ليست كافية لفهم أنها كانت بداية وأنك بحاجة إلى إعادة تسمية الحقل نفسه. ويؤدي حل مماثل إلى ازدواجية الحقول. على سبيل المثال ، إذا كنت بحاجة إلى طريقة getClientId()
في getClientId()
، فلا يمكنني تنفيذها بالطريقة الصحيحة الوحيدة
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(); } }
ولا بد لي من تكرار الحقول. وفي Client
اسحب كل من id
و clientId
، وفي الخريطة بجوار العميل لديك clientId
بشكل صريح. وتأكد من أن كل هذا لا يترك. علاوة على ذلك ، أريد أيضًا أن تعمل الرسائل ذات المنطق غير التافه ، على سبيل المثال
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(); } }
لذلك لم يعد هناك حاجة إلى خيار البحث بالاسم ، فأنت بحاجة إلى شيء أكثر صعوبة.
كان الخيار التالي مجنونًا تمامًا ولم يعيش طويلًا في رأسي ، ولكن من أجل اكتمال القصة سأصفها أيضًا. في مرحلة التحليل ، يمكننا إنشاء كيان فارغ والتناوب ببساطة في كتابة بعض القيم في الحقول ، وبعد ذلك نحصل على الحروف وننظر في أنها غيرت ما عادوا أم لا. لذلك سنرى أنه من السجل في حقل name
لا تتغير قيمة getClientId
، ولكن من id
السجل - يتغير. علاوة على ذلك ، يتم دعم الحالة التي يتم فيها دعم الحروف والحقول من أنواع مختلفة (مثل isActive() = i_active != 0
) هنا. ولكن هناك ثلاث مشاكل خطيرة على الأقل (ربما أكثر ، لكنني لم أفكر أكثر).
- إن الشرط الواضح للجوهر مع هذه الخوارزمية هو إرجاع القيمة "نفسها" من الجابت إذا لم يتغير الحقل "المطابق". "واحد ونفس" - من وجهة نظر عامل المقارنة الذي اخترناه.
==
من الواضح أنه لا يمكن أن يكون (وإلا فإن بعض getAsInt() = Integer.parseInt(strField))
سيتوقف عن العمل getAsInt() = Integer.parseInt(strField))
. يبقى يساوي. لذا ، إذا أرجعت دالة getter نوعًا من كيان المستخدم الذي تم إنشاؤه بواسطة الحقول عند كل مكالمة ، فيجب أن يكون له تجاوز equals
. - تعيينات الضغط. كما في المثال مع
int -> boolean
أعلاه. إذا تحققنا من القيمتين 0 و 1 ، فسوف نرى تغييرًا. ولكن إذا كان في 40 و 42 ، فإننا في كلتا الحالتين نحقق. - قد يكون هناك محولات معقدة في الحروف التي تعتمد على ثوابت معينة في الحقول (على سبيل المثال ، تنسيق سلسلة خاصة). وعلى بياناتنا التي تم إنشاؤها ، سوف يرمون استثناءات.
بشكل عام ، الخيار لا يعمل أيضًا.
في عملية مناقشة الأمر برمته ، قلت في البداية مازحة عبارة "حسنًا ، nafig ، من الأسهل رؤية الرمز الثانوي ، كل شيء مكتوب هناك." في ذلك الوقت ، لم أكن أدرك حتى أن هذه الفكرة ستبتلعني ، وإلى أي مدى سيذهب كل شيء.
الجزء الخامس ما هو الرمز الثانوي وكيف يعمل
new #4, dup, invokespecial #5, areturn
إذا فهمت ما هو مكتوب هنا وما يفعله هذا الرمز ، فيمكنك التخطي إلى الجزء التالي .
إخلاء المسؤولية 1. لسوء الحظ ، لفهم القصة الإضافية ، تحتاج على الأقل إلى فهم أساسي لما يبدو عليه Java bytecode ، لذا سأكتب فقرتين حول هذا الموضوع. لا تتظاهر بأي حال من الأحوال على أنها كاملة.
إخلاء المسؤولية 2. سيتمحور حصريًا حول مجموعة الأساليب. لن أقول كلمة واحدة ، لا حول التجمع المستمر ، ولا حول هيكل الفئة ككل ، ولا حتى حول إعلانات الأسلوب نفسها.
الشيء الرئيسي الذي تحتاج إلى فهمه حول البايت كود هو المجمّع إلى الجهاز الظاهري لمكدس جافا. هذا يعني أن وسيطات التعليمات مأخوذة من المكدس ويتم إرجاع قيم الإرجاع من التعليمات إلى المكدس. من وجهة النظر هذه ، يمكننا القول أن الرمز الثانوي مكتوب بترميز بولندي عكسي . بالإضافة إلى المكدس ، تحتوي الطريقة أيضًا على مجموعة من المتغيرات المحلية. عند إدخال الطريقة ، يتم كتابة this
الوسيطات وجميع هذه الطريقة فيها ، ويتم تخزين المتغيرات المحلية هناك أثناء التنفيذ. هنا مثال بسيط.
public class Foo { private int bar; public int updateAndReturn(long baz, String str) { int result = (int) baz; result += str.length(); bar = result; return result; } }
سأكتب التعليقات في الشكل
# [(<local_variable_index>:<actual_value>)*], [(<value_on_stack>)*]
أعلى المكدس على اليسار.
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 ,
هناك الكثير من التعليمات. يجب الاطلاع على القائمة الكاملة في الفصل السادس من JVMS ، على ويكيبيديا هناك رواية قصيرة . يكرر عدد كبير من التعليمات بعضها البعض لأنواع مختلفة (على سبيل المثال ، iload
ل int و lload
لفترة طويلة). أيضًا ، للعمل مع المتغيرات المحلية الأربعة الأولى ، يتم تمييز تعليماتها (على سبيل المثال ، على سبيل المثال ، هناك lload_1
ولا يأخذ أي وسيطات على الإطلاق ، ولكن هناك فقط lload
، سيأخذ عدد المتغير المحلي كوسيطة. في المثال أعلاه هناك iload
مماثل).
على الصعيد العالمي ، سنكون مهتمين بمجموعات التعليمات التالية:
*load*
، *store*
- قراءة / كتابة متغير محلي*aload
، *astore
- قراءة / كتابة عنصر صفيف حسب الفهرسgetfield
، putfield
- مجال القراءة / الكتابةgetstatic
، putstatic
- قراءة / كتابة مجال ثابتcheckcast
- يلقي بين أنواع الكائنات. تحتاج لأن تكمن القيم المكتوبة على المكدس والمتغيرات المحلية. على سبيل المثال ، كان أعلاه l2i للمدة الطويلة -> int.invoke*
- استدعاء الأسلوب*return
- إرجاع القيمة والخروج من الطريقة
الجزء السادس المنزل
بالنسبة لأولئك الذين فاتتهم مثل هذه المقدمة الطويلة ، وكذلك من أجل صرف الانتباه عن المشكلة الأصلية والعقل من حيث المكتبة ، فإننا نقوم بصياغة المشكلة بدقة أكبر.
من الضروري ، وجود نسخة java.lang.reflect.Method
في متناول اليد ، للحصول على قائمة بجميع الحقول غير الثابتة (سواء الحالية أو جميع الكائنات المتداخلة) التي ستكون قراءاتها (مباشرة أو بشكل عابر) داخل هذه الطريقة.
على سبيل المثال ، لمثل هذه الطريقة
public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); }
تحتاج إلى الحصول على قائمة من حقلين: account.balance
و client.defaultAccount.balance
.
سأكتب ، إذا أمكن ، حلًا معممًا. ولكن في مكانين ، سيكون عليك استخدام المعرفة بالمشكلة الأصلية لحل المشاكل غير القابلة للحل ، في الحالة العامة ، المشاكل.
تحتاج أولاً إلى الحصول على الرمز الفرعي لجسم الطريقة نفسها ، ولكن لا يمكنك القيام بذلك مباشرة من خلال Java. لكن منذ ذلك الحين نظرًا لأن الطريقة موجودة أصلاً داخل فئة معينة ، فمن السهل الحصول على الفئة نفسها. على الصعيد العالمي ، أعرف خيارين: إسفين في عملية تحميل الفصل واعتراض byte[]
المقروء بالفعل byte[]
هناك ، أو ببساطة العثور على ملف ClassName.class
على القرص وقراءته. لا يمكن اعتراض التحميل على مستوى المكتبة المعتادة. تحتاج إلى توصيل javaagent أو استخدام ClassLoader مخصص. في أي حال ، هناك حاجة إلى خطوات إضافية لتكوين jvm / application ، وهذا غير مريح. يمكنك القيام بذلك بسهولة. تكون جميع الفئات "العادية" دائمًا في نفس الملف بالملحق ".class" ، المسار الذي هي حزمة الفئة. نعم ، لن يعمل للعثور على فئات أو فئات مضافة ديناميكيًا تم تحميلها بواسطة بعض محمل الفصول المخصصة ، ولكننا نحتاج هذا لنموذج jdbc ، لذلك يمكننا القول بثقة أن جميع الفئات سيتم تعبئتها "بالطريقة الافتراضية" في الجرار. المجموع:
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); }
مرحبا ، قراءة مجموعة البايت. ماذا سنفعل به بعد ذلك؟ من حيث المبدأ ، هناك العديد من المكتبات في Java لقراءة / كتابة رمز البايت ، ولكن ASM يستخدم عادة في معظم الأعمال ذات المستوى المنخفض. لأن يتم شحذها من أجل الأداء العالي والتشغيل السريع ، واجهة برمجة تطبيقات الزائر هي الواجهة الرئيسية هناك - ASM يقرأ الفصل بالتتابع ويسحب الطرق المناسبة
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); } } ... }
المستخدم مدعو لإعادة تعريف الأساليب التي تهمه وكتابة منطق التحليل / التحويل الخاص به هناك. بشكل منفصل ، على سبيل المثال ، MethodVisitor
، أود أن MethodVisitor
الانتباه إلى حقيقة أن جميع الزوار لديهم تنفيذ افتراضي من خلال التفويض.
بالإضافة إلى واجهة برمجة التطبيقات الرئيسية ، هناك أيضًا واجهة برمجة تطبيقات شجرة خارج الصندوق. إذا كانت Core API عبارة عن تمثيلي لمحلل SAX ، فإن Tree API هي تمثيلية لـ DOM. نحصل على كائن يتم تخزين جميع المعلومات حول الفئة / الطريقة بداخله ويمكننا تحليله كما نريد مع القفزات إلى أي مكان. في الواقع ، هذا api هو *Visitor
تطبيق *Visitor
الذي يخزن المعلومات داخل visit*
. تقريبا كل الطرق هناك تبدو كما يلي:
public class MethodNode extends MethodVisitor { @Override public void visitJumpInsn(final int opcode, final Label label) { instructions.add(new JumpInsnNode(opcode, getLabelNode(label))); } ... }
الآن يمكننا أخيرا تحميل طريقة التحليل.
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) {
طريقة قراءة الشفرة كاملة.لا MethodNode
إرجاع MethodNode
مباشرة ، ولكن غلاف مع زوج من تحويلة. الحقول لأن سنحتاجها لاحقًا أيضًا. نقطة الإدخال (والطريقة العامة الوحيدة) هي 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; } } } }
حان الوقت للقيام بالتحليل مباشرة. كيف تصنعه؟ الفكرة الأولى هي النظر في جميع تعليمات getfield
. يقول كل getfield
بشكل ثابت أي مجال هو وأي فئة. يمكن اعتبار جميع حقول الفصل التي كان الوصول إليها ضرورية. ولكن ، للأسف ، هذا لا يعمل. المشكلة الأولى هنا هي أنه يتم القبض على الفائض.
class Foo { private int bar; private int baz; public int test() { return bar + new Foo().baz; } }
مع هذه الخوارزمية ، نعتبر أن حقل الباز مطلوب ، على الرغم من أنه في الواقع لا. ولكن لا يزال من الممكن تسجيل هذه المشكلة. ولكن ماذا تفعل مع الأساليب؟
public class Client implements HasClientId { private Long id; public Long getId() { HasClientId obj = this; return obj.getClientId(); } @Override public Long getClientId() { return id; } }
إذا بحثنا عن استدعاءات الطريقة بنفس الطريقة التي نبحث فيها عن حقول القراءة ، فلن نجد getClientId
. لأنه لا يوجد استدعاء Client.getClientId
، ولكن فقط مكالمة إلى HasClientId.getClientId
. بالطبع ، يمكنك التفكير في جميع الطرق في الفصل الحالي ، وجميع الفئات الفائقة وجميع الواجهات لاستخدامها ، ولكن هذا كثير بالفعل. لذا يمكنك التقاط toString
طريق الخطأ ، toString
قائمة بجميع الحقول بشكل عام.
علاوة على ذلك ، نريد مكالمات getter على الكائنات المتداخلة للعمل أيضًا
public class Account { private Client client; public long getClientId() { return client.getId(); } }
وهنا لا يتم تطبيق استدعاء Client.getId
على فئة Account
على الإطلاق.
برغبة قوية ، لا يزال بإمكانك التفكير في عمليات الاختراق للحالات الخاصة لبعض الوقت ، ولكن بسرعة كبيرة إلى فهم أن "الأشياء لا تتم على هذا النحو" وتحتاج إلى مراقبة تدفق التنفيذ وحركة البيانات بشكل كامل. getfield
, this
, - this
. هنا مثال:
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
. لأن , ( 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
. , , . على سبيل المثال:
public long getBalance() { Account acc; if (account != null) acc = account; else acc = client.getDefaultAccount(); return acc.getBalance(); }
Account.getBalance()
. - . . ? merge
.
— SuperInterpreter extends Interpreter<SuperValue>
? حق. 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
. ? لا! — . , , , . , 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); } }
, . دعنا نذهب!
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); }
. ""? ? ليس بالفعل. . , .. . , 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
.
مرحى! . ما هي الخطوة التالية؟ , ( , 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, .
خاتمة
- , . - partialGet
. , . , , , , " " .
, .