من مترجم: الجريمة من عدم وجود عامل nameOf في Java دفعتني إلى ترجمة هذه المقالة. بالنسبة للصبر - في نهاية المقال ، هناك تنفيذ جاهز في المصادر والثنائيات.أحد الأشياء التي يفتقر إليها مطورو المكتبات في جافا غالبًا هي حرفية الملكية. في هذا المنشور ، سأوضح كيف يمكنك استخدام أسلوب مرجع من Java 8 بطريقة إبداعية لمحاكاة حرفية الخاصية باستخدام توليد بايت.
Akin إلى الفئة الحرفية (على سبيل المثال ،
Customer.class
) ، فإن الخاصية الحرفية تجعل من الممكن الإشارة إلى خصائص فئة الفاصوليا آمنة من النوع. قد يكون هذا مفيدًا لتصميم واجهة برمجة تطبيقات حيث تكون هناك حاجة لتنفيذ إجراءات على الخصائص أو تكوينها بطريقة أو بأخرى.
من المترجم: تحت القطع ، نقوم بتحليل كيفية تنفيذ ذلك من الوسائل المرتجلة.على سبيل المثال ، ضع في اعتبارك واجهة برمجة تطبيقات تهيئة تعيين الفهرس في Hibernate Search:
new SearchMapping().entity(Address.class) .indexed() .property("city", ElementType.METHOD) .field();
أو طريقة
validateValue()
من واجهة برمجة تطبيقات Bean Validation API ، والتي تتيح لك التحقق من القيمة مقابل القيود المفروضة على الموقع:
Set<ConstraintViolation<Address>> violations = validator.validateValue(Address.class, "city", "Purbeck" );
في كلتا الحالتين ، يتم استخدام نوع
String
للإشارة إلى خاصية
city
لكائن
Address
.
يمكن أن يؤدي ذلك إلى أخطاء:
- قد لا تحتوي فئة العنوان على خاصية
city
على الإطلاق. أو قد ينسى شخص ما تحديث اسم سلسلة الخاصية بعد إعادة تسمية طرق get / set عند إعادة البناء. - في حالة
validateValue()
، ليس لدينا طريقة للتحقق من أن نوع القيمة التي تم تمريرها يطابق نوع الخاصية.
يمكن لمستخدمي واجهة برمجة التطبيقات هذه التعرف على هذه المشكلات فقط من خلال تشغيل التطبيق. ألن يكون رائعًا إذا منع المترجم ونظام النوع هذا الاستخدام من البداية؟ إذا كانت Java تحتوي على خصائص حرفية ، فيمكننا القيام بذلك (هذا الرمز لا يترجم):
mapping.entity(Address.class) .indexed() .property(Address::city, ElementType.METHOD ) .field();
و:
validator.validateValue(Address.class, Address::city, "Purbeck");
يمكننا تجنب المشاكل المذكورة أعلاه: أي خطأ مطبعي في اسم الخاصية سيؤدي إلى خطأ في الترجمة ، والذي يمكن ملاحظته مباشرة في IDE الخاص بك. سيتيح لنا هذا تصميم واجهة برمجة تطبيقات تهيئة Hibernate Search بحيث لا تقبل سوى خصائص فئة العنوان عندما نقوم بتكوين كيان العنوان. وفي حالة التحقق من
validateValue()
Bean ValidateValue
validateValue()
ستساعد الخاصية الحرفية في التأكد من تمرير قيمة من النوع الصحيح.
مرجع طريقة Java 8
لا يدعم Java 8 حرفية الخاصية (ولا يُخطط لدعمها في Java 11) ، ولكن في نفس الوقت ، يوفر طريقة مثيرة لمحاكاة هذه الطرق: مرجع الطريقة (مرجع الطريقة). في البداية ، تمت إضافة مرجع الطريقة لتبسيط العمل مع تعبيرات لامدا ، ولكن يمكن استخدامها كحرفات ملكية للفقراء.
ضع في اعتبارك فكرة استخدام مرجع لطريقة getter كخاصية حرفية:
validator.validateValue(Address.class, Address::getCity, "Purbeck");
من الواضح أن هذا لن يعمل إلا إذا كان لديك معلم. ولكن إذا كانت فصولك تتبع بالفعل اتفاقية JavaBeans ، وهو ما يحدث غالبًا ، فلا بأس بذلك.
كيف سيبدو إعلان طريقة
validateValue()
؟ النقطة الأساسية هي استخدام نوع
Function
الجديد:
public <T, P> Set<ConstraintViolation<T>> validateValue( Class<T> type, Function<? super T, P> property, P value);
باستخدام معلمتين للكتابة ، يمكننا التحقق من صحة نوع الحاوية والخصائص والقيمة التي تم تمريرها. من وجهة نظر واجهة برمجة التطبيقات ، حصلنا على ما نحتاج إليه: من الآمن استخدامه ، وسيكمل IDE تلقائيًا أسماء الطرق التي تبدأ بـ
Address::
. ولكن كيف يمكن اشتقاق اسم الخاصية من كائن
Function
في تطبيق أسلوب
validateValue()
؟
ثم يبدأ المرح ، حيث أن الواجهة الوظيفية للدالة تعلن فقط عن طريقة واحدة -
apply()
، والتي تنفذ رمز الوظيفة لمثيل
T
تم تمريره. لا يبدو أن هذا هو ما نحتاجه.
ByteBuddy للإنقاذ
كما اتضح ، فإن الحيلة تكمن في تطبيق الوظيفة! من خلال إنشاء مثيل وكيل من النوع T ، لدينا هدف استدعاء الأسلوب والحصول على اسمه في معالج استدعاء الوكيل. (من المترجم: فيما يلي نتحدث عن وكلاء جافا الديناميكيين - java.lang.reflect.Proxy).
تدعم Java الوكلاء الديناميكيين خارج الصندوق ، ولكن هذا الدعم يقتصر على الواجهات فقط. نظرًا لأن واجهة برمجة التطبيقات الخاصة بنا يجب أن تعمل مع أي حبوب ، بما في ذلك الفصول الحقيقية ، سأستخدم أداة رائعة ، ByteBuddy ، بدلاً من Proxy. يوفر ByteBuddy DSL بسيط لإنشاء فصول على الطاير ، وهو ما نحتاجه.
لنبدأ بتحديد واجهة تسمح لنا بتخزين واسترداد اسم الخاصية المستخرج من مرجع الطريقة.
public interface PropertyNameCapturer { String getPropertyName(); void setPropertyName(String propertyName); }
نستخدم الآن ByteBuddy لإنشاء فئات وكيل برمجياً تتوافق مع أنواع الاهتمام بالنسبة لنا (على سبيل المثال: العنوان) وتنفيذ
PropertyNameCapturer
:
public <T> T getPropertyNameCapturer(Class<T> type) { DynamicType.Builder<?> builder = new ByteBuddy() (1) .subclass( type.isInterface() ? Object.class : type ); if (type.isInterface()) { (2) builder = builder.implement(type); } Class<?> proxyType = builder .implement(PropertyNameCapturer.class) (3) .defineField("propertyName", String.class, Visibility.PRIVATE) .method( ElementMatchers.any()) (4) .intercept(MethodDelegation.to( PropertyNameCapturingInterceptor.class )) .method(named("setPropertyName").or(named("getPropertyName"))) (5) .intercept(FieldAccessor.ofBeanProperty()) .make() .load( (6) PropertyNameCapturer.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER ) .getLoaded(); try { @SuppressWarnings("unchecked") Class<T> typed = (Class<T>) proxyType; return typed.newInstance(); (7) } catch (InstantiationException | IllegalAccessException e) { throw new HibernateException( "Couldn't instantiate proxy for method name retrieval", e ); } }
قد تبدو الشفرة مربكة بعض الشيء ، لذا دعوني أشرح ذلك. أولاً نحصل على مثيل ByteBuddy (1) ، وهو نقطة دخول DSL. يتم استخدامه لإنشاء أنواع ديناميكية إما توسع النوع المطلوب (إذا كانت فئة) أو ترث كائن وتنفذ النوع المطلوب (إذا كانت واجهة) (2).
ثم ، نشير إلى أن النوع يقوم بتنفيذ واجهة PropertyNameCapturer وإضافة حقل لتخزين اسم الخاصية المطلوبة (3). ثم نقول أن المكالمات إلى جميع الطرق يجب اعتراضها من قبل PropertyNameCapturingInterceptor (4). يجب فقط الوصول إلى setPropertyName () و getPropertyName () (من واجهة PropertyNameCapturer) إلى الخاصية الحقيقية التي تم إنشاؤها سابقًا (5). وأخيرًا ، يتم إنشاء الفصل وتحميله (6) واستنساخه (7).
هذا كل ما نحتاجه لإنشاء أنواع الوكيل ، وذلك بفضل ByteBuddy ، يمكن القيام بذلك في بضعة أسطر من التعليمات البرمجية. الآن دعونا نلقي نظرة على اعتراض المكالمات:
public class PropertyNameCapturingInterceptor { @RuntimeType public static Object intercept(@This PropertyNameCapturer capturer, @Origin Method method) { (1) capturer.setPropertyName(getPropertyName(method)); (2) if (method.getReturnType() == byte.class) { (3) return (byte) 0; } else if ( ... ) { }
يقبل الأسلوب intercept () الأسلوب الذي تم استدعاؤه والهدف الخاص بالمكالمة (1).
@This
و
@This
لتحديد المعلمات المناسبة بحيث يمكن ByteBuddy إنشاء استدعاءات intercept () الصحيحة في وكيل ديناميكي.
لاحظ أنه لا يوجد اعتماد صارم للمستقبل على أنواع ByteBuddy ، حيث يتم استخدام ByteBuddy فقط لإنشاء وكيل ديناميكي ، ولكن ليس عند استخدامه.
من خلال استدعاء
getPropertyName()
(4) ، يمكننا الحصول على اسم الخاصية المطابق
getPropertyName()
الطريقة الذي تم تمريره ، وحفظه في
PropertyNameCapturer
(2). إذا لم تكن الطريقة عبارة عن getter ، فإن الشفرة تطرح استثناءً (5). لا يهم نوع الإرجاع في getter ، لذلك نرجع null بالنظر إلى نوع الخاصية (3).
نحن الآن جاهزون للحصول على اسم الخاصية في طريقة
validateValue()
:
public <T, P> Set<ConstraintViolation<T>> validateValue( Class<T> type, Function<? super T, P> property, P value) { T capturer = getPropertyNameCapturer(type); property.apply(capturer); String propertyName = ((PropertyLiteralCapturer) capturer).getPropertyName();
بعد تطبيق الوظيفة على الوكيل الذي تم إنشاؤه ، نرسل النوع إلى PropertyNameCapturer ونحصل على الاسم من Method.
لذا ، باستخدام بعض من سحر توليد البايت ، استخدمنا مرجع الطريقة من Java 8 لمحاكاة حرفية الملكية.
بالطبع ، إذا كان لدينا حرفًا في الممتلكات العقارية في اللغة ، فسوف نكون جميعًا في وضع أفضل. حتى أنني سأسمح بالعمل مع الممتلكات الخاصة ، وربما يمكن الرجوع إلى الخصائص من التعليقات التوضيحية. ستكون حرفية الممتلكات العقارية مرتبة (بدون البادئة "get") ولن تبدو وكأنها اختراق.
من المترجم
تجدر الإشارة إلى أن اللغات الجيدة الأخرى تدعم بالفعل (أو تقريبًا) آلية مماثلة:
إذا كنت تستخدم مشروع لومبوك فجأة مع جافا ، فسيتم كتابة
مولد وقت تجميع الرمز البايت له.
مستوحى من الطريقة الموضحة في المقالة ، قام خادمك المتواضع بتجميع مكتبة صغيرة تنفذ nameOfProperty () لـ Java 8:
كود المصدرثنائيات