डायनेमिक प्रॉक्सी और स्प्रिंग 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 क्लाइंट के लिए रैपर के उदाहरण का उपयोग करके इस विचार को लागू कर सकते हैं। खिलौने का एक उदाहरण, वास्तविक उपयोग के लिए नहीं, बल्कि दृष्टिकोण का प्रदर्शन करने के लिए। परियोजना के स्रोत कोड का अध्ययन बिटकॉइन पर किया जा सकता है।


यह उपयोगकर्ता के लिए कैसा दिखता है


उपयोगकर्ता एक इंटरफ़ेस के रूप में अपनी ज़रूरत की सेवा का वर्णन करता है। उदाहरण के लिए, Google पर http अनुरोध करने के लिए:


 /** * 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); } 

इस इंटरफ़ेस का कार्यान्वयन आखिरकार क्या होगा यह हस्ताक्षर द्वारा निर्धारित किया जाता है। यदि रिटर्न प्रकार अंतर है, तो एक HTTP अनुरोध निष्पादित किया जाएगा और स्थिति कोड परिणाम के रूप में वापस आ जाएगी। यदि वापसी का प्रकार क्लोसएबल हेटपार्ट्सन है, तो पूरी प्रतिक्रिया वापस आ जाएगी, और इसी तरह। जहां अनुरोध किया जाएगा, हम उरी को एनोटेशन से लेंगे, इसकी सामग्री में प्लेसहोल्डर्स के बजाय समान हस्तांतरित मूल्यों को प्रतिस्थापित करते हुए।


इस उदाहरण में, मैंने तीन वापसी प्रकारों और एक एनोटेशन का समर्थन करने के लिए खुद को सीमित किया। आप कार्यान्वयन का चयन करने के लिए विधि के नाम, पैरामीटर प्रकारों का भी उपयोग कर सकते हैं, उनमें से सभी प्रकार के संयोजनों का उपयोग कर सकते हैं, लेकिन मैं इस विषय को इस पोस्ट में नहीं खोलूंगा।


जब कोई उपयोगकर्ता इस इंटरफ़ेस का उपयोग करना चाहता है, तो वह स्प्रिंग का उपयोग करके इसे अपने कोड में एम्बेड करता है:


 @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); } } 

मेरी कार्यशील परियोजना में स्प्रिंग के साथ एकीकरण की आवश्यकता थी, लेकिन यह निश्चित रूप से एकमात्र संभव नहीं है। यदि आप निर्भरता इंजेक्शन का उपयोग नहीं करते हैं, तो आप स्थैतिक कारखाने विधि के माध्यम से, उदाहरण के लिए, कार्यान्वयन प्राप्त कर सकते हैं। लेकिन इस लेख में मैं वसंत पर विचार करूंगा।


यह दृष्टिकोण बहुत सुविधाजनक है: बस अपने इंटरफ़ेस को स्प्रिंग (इस मामले में सेवा एनोटेशन) के एक घटक के रूप में चिह्नित करें, और यह कार्यान्वयन और उपयोग के लिए तैयार है।


इस जादू का समर्थन करने के लिए वसंत कैसे प्राप्त करें


एक विशिष्ट स्प्रिंग एप्लिकेशन स्टार्टअप पर क्लासपैथ को स्कैन करता है और विशेष एनोटेशन के साथ चिह्नित सभी घटकों की तलाश करता है। उनके लिए, यह बीनडिफिनिशन, व्यंजनों को पंजीकृत करता है जिसके द्वारा ये घटक बनाए जाएंगे। लेकिन अगर ठोस वर्गों के मामले में, वसंत जानता है कि उन्हें कैसे बनाया जाए, निर्माणकर्ताओं को क्या कहा जाए, और उनमें क्या पारित किया जाए, तो अमूर्त वर्गों और इंटरफेस के लिए ऐसी जानकारी नहीं है। इसलिए, हमारे GoogleSearchApi स्प्रिंग के लिए BeanDefinition नहीं बनाया जाएगा। इसमें उसे हमसे मदद की जरूरत होगी।


BeanDefinitions प्रसंस्करण के तर्क को समाप्त करने के लिए, वसंत में एक BeanDefinitionRegistryPostProcessor इंटरफ़ेस है। इसके साथ, हम सेम की किसी भी परिभाषा में बीनडिफिनेशनरेजी को जोड़ सकते हैं।


दुर्भाग्य से, मुझे साधारण बीन्स और हमारे इंटरफेस दोनों को एक ही पास में संसाधित करने के लिए क्लासपैथ स्कैन के स्प्रिंग लॉजिक में एकीकृत करने का कोई तरीका नहीं मिला। इसलिए, मैंने सर्विस एनोटेशन के साथ चिह्नित सभी इंटरफेस को खोजने के लिए ClassPathScanningCandidateComponentProvider वर्ग के वंशज का निर्माण और उपयोग किया:


पूर्ण पैकेज स्कैन कोड और बीनडेफिनिशन का पंजीकरण:


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(); } } } 

हो गया! आवेदन की शुरुआत में, स्प्रिंग इस कोड को निष्पादित करेगा और सेम की तरह सभी आवश्यक इंटरफेस को पंजीकृत करेगा।


पाया सेम का एक कार्यान्वयन बनाने के लिए 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); } } 

कार्यान्वयन बनाने के लिए, पुराने पुराने डायनेमिक प्रॉक्सी तंत्र का उपयोग किया जाता है। Proxy.newProxyInstance पद्धति का उपयोग करके मक्खी पर एक कार्यान्वयन बनाया जाता है। उनके बारे में बहुत सारे लेख पहले ही लिखे जा चुके हैं, इसलिए मैं यहां विस्तार से नहीं लिखूंगा।


सही हैंडलर ढूंढना और कॉल प्रोसेसिंग


जैसा कि आप देख सकते हैं, DynamicProxyBeanFactory रीडायरेक्ट विधि को डायनामिकप्रोक्सी इंवोकेशनहैंडलरडिप्लाइसर प्रसंस्करण। चूंकि हमारे पास संभावित रूप से हैंडलर के कई कार्यान्वयन हैं (प्रत्येक एनोटेशन के लिए, प्रत्येक लौटे प्रकार के लिए, आदि), उनके भंडारण और खोज के लिए कुछ केंद्रीय स्थान स्थापित करना तर्कसंगत है।


यह निर्धारित करने के लिए कि हैंडलर को किस विधि को संसाधित करने के लिए उपयुक्त है, मैंने एक नई विधि के साथ मानक 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 इंटरफ़ेस है, जिसके कार्यान्वयन हमारे हैंडलर होंगे। इसके अलावा, हैंडलर कार्यान्वयन को घटक के रूप में चिह्नित किया जाएगा ताकि स्प्रिंग हमारे लिए डायनामिकप्रॉक्सी इनवोकेशनहैंडलरडिपचैकर के अंदर एक बड़ी सूची में एकत्र कर सके:


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)); } } 

फाइंडहैंडलर विधि में, हम सभी हैंडलर के माध्यम से जाते हैं और पहले वाले को वापस करते हैं जो पारित विधि को संभाल सकता है। यह खोज तंत्र बहुत प्रभावी नहीं हो सकता है जब बहुत सारे हैंडलर कार्यान्वयन हों। शायद तब आपको उन्हें एक सूची की तुलना में संग्रहीत करने के लिए कुछ और उपयुक्त संरचना के बारे में सोचना होगा।


हैंडलर कार्यान्वयन


हैंडलर के कार्यों में इंटरफ़ेस की तथाकथित विधि के बारे में जानकारी पढ़ना और कॉल को स्वयं संसाधित करना शामिल है।


इस मामले में हैंडलर को क्या करना चाहिए:


  1. उरी एनोटेशन पढ़ें, इसकी सामग्री प्राप्त करें
  2. उड़ी प्लेसहोल्डर्स को वास्तविक मानों के साथ स्ट्रिंग में बदलें
  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 हेल्पर वर्ग के औजार उरी एनोटेशन के साथ काम करते हैं: रीडिंग वैल्यू, प्लेसहोल्डर्स की जगह। मैं यहां कोड नहीं दूंगा, क्योंकि यह काफी उपयोगी है।
लेकिन यह ध्यान देने योग्य है कि जावा विधि के हस्ताक्षर से पैरामीटर के नाम को पढ़ने के लिए, आपको संकलन करते समय विकल्प "-पैरामीटर" को जोड़ना होगा
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); } } 

अन्य हैंडलर भी इसी तरह बनाए जाते हैं। नए हैंडलर जोड़ना सरल है और मौजूदा कोड के संशोधन की आवश्यकता नहीं है - बस एक नया हैंडलर बनाएं और इसे स्प्रिंग घटक के रूप में चिह्नित करें।


वह सब है। कोड लिखा है और जाने के लिए तैयार है।


निष्कर्ष


जितना मैं इस तरह के डिजाइन के बारे में सोचता हूं, उतना ही मुझे इसमें खामियां दिखाई देती हैं। कमजोरियाँ जो मुझे दिखाई देती हैं:


  • टाइप सेफ्टी, जो नहीं है। RuntimeException के साथ मिलने से पहले - एनोटेशन को गलत तरीके से सेट करें। वापसी प्रकार और एनोटेशन के गलत संयोजन का उपयोग किया - एक ही बात।
  • आईडीई से कमजोर समर्थन। ऑटो पूरा होने की कमी। उपयोगकर्ता यह नहीं देख सकता है कि उसकी स्थिति में उसके लिए क्या कार्य उपलब्ध हैं (जैसे कि वह वस्तु के बाद "डॉट" लगाता है और उपलब्ध सूची की सूची देखता है)
  • आवेदन के लिए कुछ संभावनाएं हैं। पहले से ही उल्लेख किया गया http क्लाइंट दिमाग में आता है, और क्लाइंट डेटाबेस पर जाता है। लेकिन इसे और क्यों लागू किया जा सकता है?

हालांकि, मेरे काम के मसौदे में दृष्टिकोण ने मूल लिया है और लोकप्रिय है। जो फायदे मैंने पहले ही बताए हैं - सादगी, कोड की थोड़ी मात्रा, गिरावट, डेवलपर्स को अधिक महत्वपूर्ण कोड लिखने पर ध्यान केंद्रित करने की अनुमति देते हैं।


आप इस दृष्टिकोण के बारे में क्या सोचते हैं? क्या यह प्रयास के लायक है? इस दृष्टिकोण में आपको क्या समस्याएं दिखती हैं? जबकि मैं अभी भी इसे समझने की कोशिश कर रहा हूं, जबकि यह हमारे उत्पादन में चारों ओर लुढ़का हुआ है, मैं यह सुनना चाहूंगा कि अन्य लोग इसके बारे में क्या सोचते हैं। मुझे उम्मीद है कि यह सामग्री किसी के लिए उपयोगी थी।

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


All Articles