قم بإنشاء مكتبة أنماط Spring Data Repository الخاصة بك باستخدام Dynamic Proxy و Spring IoC

ولكن ماذا لو كنت تستطيع إنشاء واجهة ، على سبيل المثال ، مثل هذا:


@Service public interface GoogleSearchApi { /** * @return http status code for Google main page */ @Uri("https://www.google.com") int mainPageStatus(); } 

ثم حقنه فقط واستدعي أساليبه:


 @SpringBootApplication public class App implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(App.class); private final GoogleSearchApi api; public App(GoogleSearchApi api) { this.api = api; } @Override public void run(String... args) { LOG.info("Main page status: " + api.mainPageStatus()); } public static void main(String[] args) { SpringApplication.run(App.class, args); } } 

هذا ممكن التنفيذ (وليس صعبًا جدًا). بعد ذلك سأبين كيف ولماذا أفعل ذلك.


في الآونة الأخيرة ، كان عليّ تبسيط تفاعل المطورين مع أحد الأطر المستخدمة. كان من الضروري منحهم طريقة أكثر بساطة وأكثر ملاءمة للعمل معه من تلك التي تم تنفيذها بالفعل.


الخصائص التي أردت تحقيقها من هذا الحل:


  • الوصف التعريفي للعمل المطلوب
  • الحد الأدنى من الكود المطلوب
  • التكامل مع إطار حقن التبعية المستخدم (في حالتنا ، الربيع)

يتم تطبيق هذا في مكتبات بيانات الربيع وملفات التحديثية . في نفوسهم ، يصف المستخدم التفاعل المطلوب في شكل واجهة جافا ، مع استكمال التعليقات التوضيحية. لا يحتاج المستخدم إلى كتابة التطبيق بنفسه - حيث تقوم المكتبة بإنشائه في وقت التشغيل استنادًا إلى توقيعات الأساليب والشروح والأنواع.


عندما درست الموضوع ، كان لدي الكثير من الأسئلة ، كانت الإجابات التي كانت منتشرة عبر الإنترنت. في تلك اللحظة ، فإن مقالاً كهذا لن يؤذيني. لذلك ، حاولت هنا جمع كل المعلومات وتجربتي في مكان واحد.


سأوضح في هذا المنشور كيف يمكنك تنفيذ هذه الفكرة ، باستخدام مثال برنامج التضمين لعميل http. مثال على لعبة ، ليست مصممة للاستخدام الحقيقي ، ولكن لإظهار النهج. شفرة المصدر للمشروع يمكن دراستها على bitbucket .


كيف تبدو للمستخدم


يصف المستخدم الخدمة التي يحتاجها في شكل واجهة. على سبيل المثال ، لتنفيذ طلبات http على google:


 /** * Some Google requests */ @Service public interface GoogleSearchApi { /** * @return http status code for Google main page */ @Uri("https://www.google.com") int mainPageStatus(); /** * @return request object for Google main page */ @Uri("https://www.google.com") HttpGet mainPageRequest(); /** * @param query search query * @return result of search request execution */ @Uri("https://www.google.com/search?q={query}") CloseableHttpResponse searchSomething(String query); /** * @param query doodle search query * @param language doodle search language * @return http status code for doodle search result */ @Uri("https://www.google.com/doodles/?q={query}&hl={language}") int searchDoodleStatus(String query, String language); } 

ما سيتم تنفيذه في نهاية المطاف هذه الواجهة هو الذي يحدده التوقيع. إذا كان نوع الإرجاع int ، فسيتم تنفيذ طلب http وسيتم إرجاع الحالة كرمز نتيجة. إذا كان نوع الإرجاع هو CloseableHttpResponse ، فسيتم إرجاع الاستجابة بالكامل وما إلى ذلك. حيث سيتم تقديم الطلب ، سنأخذ Uri من التعليق التوضيحي ، ونستبدل نفس القيم المنقولة بدلاً من العناصر النائبة في محتوياته.


في هذا المثال ، قصرت نفسي على دعم ثلاثة أنواع من الإرجاع وتعليق توضيحي واحد. يمكنك أيضًا استخدام أسماء الأساليب ، وأنواع المعلمات لاختيار تطبيق ، واستخدام جميع أنواع المجموعات منها ، لكنني لن أفتح هذا الموضوع في هذا المنشور.


عندما يريد المستخدم استخدام هذه الواجهة ، يقوم بتضمينها في الكود الخاص به باستخدام Spring:


 @SpringBootApplication public class App implements CommandLineRunner { private static final Logger LOG = LoggerFactory.getLogger(App.class); private final GoogleSearchApi api; public App(GoogleSearchApi api) { this.api = api; } @Override @SneakyThrows public void run(String... args) { LOG.info("Main page status: " + api.mainPageStatus()); LOG.info("Main page request: " + api.mainPageRequest()); LOG.info("Doodle search status: " + api.searchDoodleStatus("tesla", "en")); try (CloseableHttpResponse response = api.searchSomething("qweqwe")) { LOG.info("Search result " + response); } } public static void main(String[] args) { SpringApplication.run(App.class, args); } } 

كان التكامل مع Spring ضروريًا في مشروع عملي ، لكنه بالطبع ليس الوحيد الممكن. إذا كنت لا تستخدم حقن التبعية ، يمكنك الحصول على التنفيذ ، على سبيل المثال ، من خلال طريقة المصنع الثابت. ولكن في هذه المقالة سأنظر الربيع.


هذه الطريقة مريحة للغاية: قم فقط بتمييز الواجهة كواحد من مكونات Spring (تعليق الخدمة في هذه الحالة) ، وهو جاهز للتنفيذ والاستخدام.


كيفية الحصول على الربيع لدعم هذا السحر


يقوم تطبيق Spring النموذجي بمسح classpath عند بدء التشغيل ويبحث عن جميع المكونات المميزة بتعليقات توضيحية خاصة. بالنسبة لهم ، فإنه يسجل BeanDefinitions ، وصفات من خلالها سيتم إنشاء هذه المكونات. لكن إذا كان فصل الربيع في حالة الطبقات الملموسة ، يعرف كيفية إنشائها ، وما الذي يسمونه منشئي الإنشاءات ، وما الذي يجب عليهم تمريره ، ثم بالنسبة للفئات المجردة والواجهات ، فإنه لا يملك مثل هذه المعلومات. لذلك ، بالنسبة إلى GoogleSearchApi Spring ، فلن يقوم بإنشاء BeanDefinition. في هذا سيحتاج مساعدة منا.


لإنهاء منطق معالجة BeanDefinitions ، هناك واجهة BeanDefinitionRegistryPostProcessor في الربيع. مع ذلك ، يمكننا أن نضيف إلى BeanDefinitionRegistry أي تعريف للفاصوليا التي نريدها.


لسوء الحظ ، لم أجد طريقة للاندماج في منطق Spring لمسح classpath من أجل معالجة كل من البقول العادية وواجهاتنا في مسار واحد. لذلك ، قمتُ بإنشاء واستخدام سليل فئة ClassPathScanningCandidateComponentProvider للعثور على جميع الواجهات التي تحمل علامة تعليق توضيحي للخدمة:


حزمة مسح رمز كاملة وتسجيل BeanDefinitions:


DynamicProxyBeanDefinitionRegistryPostProcessor
 @Component public class DynamicProxyBeanDefinitionRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { // ,     private static final String[] SCAN_PACKAGES = {"com"}; private final InterfaceScanner classpathScanner; public DynamicProxyBeanDefinitionRegistryPostProcessor() { classpathScanner = new InterfaceScanner(); //   .      Service classpathScanner.addIncludeFilter(new AnnotationTypeFilter(Service.class)); } @Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { for (String basePackage : SCAN_PACKAGES) { createRepositoryProxies(basePackage, registry); } } @SneakyThrows private void createRepositoryProxies(String basePackage, BeanDefinitionRegistry registry) { for (BeanDefinition beanDefinition : classpathScanner.findCandidateComponents(basePackage)) { Class<?> clazz = Class.forName(beanDefinition.getBeanClassName()); //      bean definition BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz); builder.addConstructorArgValue(clazz); //,          builder.setFactoryMethodOnBean( "createDynamicProxyBean", DynamicProxyBeanFactory.DYNAMIC_PROXY_BEAN_FACTORY ); registry.registerBeanDefinition(ClassUtils.getShortNameAsProperty(clazz), builder.getBeanDefinition()); } } @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { } private static class InterfaceScanner extends ClassPathScanningCandidateComponentProvider { InterfaceScanner() { super(false); } @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { return beanDefinition.getMetadata().isInterface(); } } } 

القيام به! في بداية التطبيق ، سيقوم Spring بتنفيذ هذا الرمز وتسجيل جميع الواجهات الضرورية ، مثل الفول.


إنشاء تطبيق للفاصوليا الموجودة يتم تفويضه إلى مكون منفصل من DynamicProxyBeanFactory:


 @Component(DYNAMIC_PROXY_BEAN_FACTORY) public class DynamicProxyBeanFactory { public static final String DYNAMIC_PROXY_BEAN_FACTORY = "repositoryProxyBeanFactory"; private final DynamicProxyInvocationHandlerDispatcher proxy; public DynamicProxyBeanFactory(DynamicProxyInvocationHandlerDispatcher proxy) { this.proxy = proxy; } @SuppressWarnings("unused") public <T> T createDynamicProxyBean(Class<T> beanClass) { //noinspection unchecked return (T) Proxy.newProxyInstance(beanClass.getClassLoader(), new Class[]{beanClass}, proxy); } } 

لإنشاء التطبيق ، يتم استخدام آلية Dynamic Proxy القديمة الجيدة. يتم إنشاء تطبيق سريعًا باستخدام أسلوب Proxy.newProxyInstance. تم بالفعل كتابة الكثير من المقالات عنه ، لذا لن أسهب هنا بالتفصيل.


العثور على المعالج المناسب ومعالجة المكالمات


كما ترى ، فإن DynamicProxyBeanFactory يعيد توجيه طريقة المعالجة إلى DynamicProxyInvocationHandlerDispatcher. نظرًا لأن لدينا العديد من تطبيقات المعالج (لكل تعليق توضيحي ، لكل نوع تم إرجاعه ، وما إلى ذلك) ، فمن المنطقي إنشاء مكان مركزي للتخزين والبحث فيه.


لتحديد ما إذا كان المعالج مناسبًا لمعالجة الطريقة التي تم استدعاءها ، قمت بتوسيع واجهة InvocationHandler القياسية بطريقة جديدة


 public interface HandlerMatcher { /** * @return {@code true} if handler is able to handle given method, {@code false} othervise */ boolean canHandle(Method method); } public interface ProxyInvocationHandler extends InvocationHandler, HandlerMatcher { } 

والنتيجة هي واجهة ProxyInvocationHandler ، والتي ستكون تطبيقاتها معالجاتنا. أيضًا ، سيتم تمييز تطبيقات المعالج على أنها مكون بحيث يمكن لـ Spring جمعها لنا في قائمة واحدة كبيرة داخل DynamicProxyInvocationHandlerDispatcher:


DynamicProxyInvocationHandlerDispatcher
 package com.bachkovsky.dynproxy.lib.proxy; import lombok.SneakyThrows; import org.springframework.stereotype.Component; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.List; /** * Top level dynamic proxy invocation handler, which finds correct implementation based and uses it for method * invocation */ @Component public class DynamicProxyInvocationHandlerDispatcher implements InvocationHandler { private final List<ProxyInvocationHandler> proxyHandlers; /** * @param proxyHandlers all dynamic proxy handlers found in app context */ public DynamicProxyInvocationHandlerDispatcher(List<ProxyInvocationHandler> proxyHandlers) { this.proxyHandlers = proxyHandlers; } @Override public Object invoke(Object proxy, Method method, Object[] args) { switch (method.getName()) { // three Object class methods don't have default implementation after creation with Proxy::newProxyInstance case "hashCode": return System.identityHashCode(proxy); case "toString": return proxy.getClass() + "@" + System.identityHashCode(proxy); case "equals": return proxy == args[0]; default: return doInvoke(proxy, method, args); } } @SneakyThrows private Object doInvoke(Object proxy, Method method, Object[] args) { return findHandler(method).invoke(proxy, method, args); } private ProxyInvocationHandler findHandler(Method method) { return proxyHandlers.stream() .filter(h -> h.canHandle(method)) .findAny() .orElseThrow(() -> new IllegalStateException("No handler was found for method: " + method)); } } 

في طريقة findHandler ، نذهب إلى جميع المعالجات ونعيد أول واحد يمكنه التعامل مع الطريقة التي تم تمريرها. قد لا تكون آلية البحث هذه فعالة للغاية عندما يكون هناك الكثير من تطبيقات المعالج. ربما ستحتاج إلى التفكير في بنية أكثر ملاءمة لتخزينها من قائمة.


تنفيذ المعالج


تتضمن مهام المعالجات قراءة معلومات حول الطريقة التي تم استدعاءها للواجهة ومعالجة المكالمة نفسها.


ما الذي يجب أن يفعله المعالج في هذه الحالة:


  1. قراءة الشرح أوري ، والحصول على محتوياته
  2. استبدل العناصر النائبة Uri في السلسلة بقيم حقيقية
  3. قراءة طريقة نوع العودة
  4. إذا كان نوع الإرجاع مناسبًا ، فقم بمعالجة الطريقة وإرجاع النتيجة.

هناك حاجة إلى النقاط الثلاث الأولى لجميع الأنواع التي تم إرجاعها ، لذلك وضعت الكود العام في فئة فائقة مجردة
HttpInvocationHandler:


 public abstract class HttpInvocationHandler implements ProxyInvocationHandler { final HttpClient client; private final UriHandler uriHandler; HttpInvocationHandler(HttpClient client, UriHandler uriHandler) { this.client = client; this.uriHandler = uriHandler; } @Override public boolean canHandle(Method method) { return uriHandler.canHandle(method); } final String getUri(Method method, Object[] args) { return uriHandler.getUriString(method, args); } } 

تطبق فئة مساعد UriHandler العمل مع شرح Uri: قراءة القيم ، واستبدال العناصر النائبة. لن أعطي الكود هنا ، لأن انها نفعية جدا.
لكن تجدر الإشارة إلى أنه لقراءة أسماء المعلمات من توقيع طريقة java ، تحتاج إلى إضافة الخيار "-المعلمات" عند التحويل البرمجي .
HttpClient - غلاف على Apachevsky CloseableHttpClient ، هو الواجهة الخلفية لهذه المكتبة.


كمثال لمعالج محدد ، سأقدم معالجًا يعرض رمز استجابة الحالة:


 @Component public class HttpCodeInvocationHandler extends HttpInvocationHandler { public HttpCodeInvocationHandler(HttpClient client, UriHandler uriHandler) { super(client, uriHandler); } @Override @SneakyThrows public Integer invoke(Object proxy, Method method, Object[] args) { try (CloseableHttpResponse resp = client.execute(new HttpGet(getUri(method, args)))) { return resp.getStatusLine().getStatusCode(); } } @Override public boolean canHandle(Method method) { return super.canHandle(method) && method.getReturnType().equals(int.class); } } 

مصنوعة معالجات أخرى بالمثل. تعد إضافة معالجات جديدة أمرًا بسيطًا ولا تتطلب تعديل التعليمات البرمجية الموجودة - ما عليك سوى إنشاء معالج جديد ووضع علامة عليه كمكون Spring.


هذا كل شيء. الرمز مكتوب وجاهز للذهاب.


استنتاج


كلما فكرت في مثل هذا التصميم ، زادت عيوبه. نقاط الضعف التي أراها:


  • اكتب السلامة ، وهي ليست كذلك. قم بتعيين التعليق التوضيحي بشكل غير صحيح - قبل الاجتماع مع RuntimeException. استخدام مزيج خاطئ من نوع الإرجاع والشرح - نفس الشيء.
  • دعم ضعيف من IDE. عدم الإكمال التلقائي. لا يمكن للمستخدم رؤية الإجراءات المتاحة له في موقفه (كما لو أنه وضع "نقطة" بعد الكائن ورأى قائمة بالطرق المتاحة)
  • هناك احتمالات قليلة للتطبيق. يتبادر إلى ذهن عميل HTTP المذكور بالفعل ، ويذهب العميل إلى قاعدة البيانات. لكن لماذا يمكن تطبيق ذلك؟

ومع ذلك ، في مسودة العمل الخاصة بي ، فإن هذا النهج قد ترسخ وأصبح شائعًا. المزايا التي ذكرتها بالفعل - البساطة ، وهي كمية صغيرة من الشفرة ، والتصريح ، تسمح للمطورين بالتركيز على كتابة رمز أكثر أهمية.


ما رأيك في هذا النهج؟ هل يستحق كل هذا الجهد؟ ما هي المشاكل التي تراها في هذا النهج؟ بينما لا أزال أحاول فهم ذلك ، بينما يتم تداوله في إنتاجنا ، أود أن أسمع رأي الآخرين في هذا الأمر. آمل أن تكون هذه المواد مفيدة لشخص ما.

Source: https://habr.com/ru/post/ar458548/


All Articles