قبل التاريخ
قبل بضعة أشهر ، كانت المهمة تتمثل في كتابة واجهة برمجة تطبيقات HTTP للعمل مع منتج الشركة ، أي التفاف جميع الطلبات باستخدام RestTemplate ثم اعتراض المعلومات من التطبيق وتعديل الاستجابة. كان التنفيذ التقريبي لخدمة التطبيق كما يلي:
if (headers == null) { headers = new HttpHeaders(); } if (headers.getFirst("Content-Type") == null) { headers.add("Content-Type", MediaType.APPLICATION_JSON_VALUE); } HttpEntity<Object> entity; if (body == null) { entity = new HttpEntity<>(headers); } else { entity = new HttpEntity<>(body, headers); } final String uri = String.format("%s%s/%s", workingUrl, apiPath, request.info()); final Class<O> type = (Class<O>) request.type(); final O response = (O)restTemplate.exchange(uri, request.method(), entity, type);
... طريقة بسيطة تقبل رؤوس الكتابة والنص والطلب. وكل شيء سيكون على ما يرام ، لكنه بدا وكأنه عكاز وغير قابلة للاستخدام للغاية في سياق الربيع.
وبينما كتب زملائي وظيفية في فروعهم على الآلية القديمة ، كان لدي فكرة مبتكرة - لماذا لا أكتب هذه الطلبات "في سطر واحد" (مثل Feign).
فكرة
لدينا حاوية Spring DI قوية في أيدينا ، فلماذا لا تستخدم وظائفها بالكامل؟ على وجه الخصوص ، تهيئة مستودعات البيانات باستخدام مثال Jpa. لقد واجهت مهمة تهيئة فئة من نوع الواجهة في سياق Spring وثلاثة حلول لاعتراض استدعاء طريقة كتطبيق نموذجي - Aspect و PostProcess و BeanDefinitionRegistrar.
قاعدة الكود
بادئ ذي بدء ، التعليقات التوضيحية ، حيث بدونها ، وإلا كيفية تكوين الاستعلامات.
1) رسم الخرائط - شرح توضيحي يحدد الواجهة كمكون لمكالمات HTTP.
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Mapping { String alias(); }
المعلمة الاسم المستعار مسؤولة عن تعيين التوجيه الجذر للخدمة ، سواء كان ذلك https://habr.com ، https://github.com ، إلخ.
2) ServiceMapping - تعليق توضيحي يحدد طريقة واجهة يجب أن يُطلق عليها كطلب HTTP قياسي للتطبيق ، من حيث نريد الحصول على استجابة أو القيام ببعض الإجراءات.
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface ServiceMapping { String path(); HttpMethod method(); Header[] defaultHeaders() default {}; Class<?> fallbackClass() default Object.class; String fallbackMethod() default ""; }
خيارات:
- المسار - مسار الطلب ، مثال الاسم المستعار + / ru / hub / $ {hub_name} ؛
- الطريقة - طريقة طلب HTTP (GET ، POST ، PUT ، إلخ) ؛
- defaultHeaders - رؤوس الطلبات الثابتة غير القابلة للتغيير لمورد بعيد (نوع المحتوى ، قبول ، وما إلى ذلك) ؛
- fallbackClass - فئة رفض الطلب الذي تمت معالجته مع وجود خطأ (استثناء) ؛
- fallbackMethod - اسم طريقة الفصل التي يجب أن تُرجع النتيجة الصحيحة في حالة حدوث خطأ (استثناء).
3) رأس - شرح توضيحي لتحديد الرؤوس الثابتة في إعدادات الطلب
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) @Documented public @interface Header { String name(); String value(); }
خيارات:
- اسم - عنوان الرأس ؛
- القيمة هي قيمة الرأس.
والخطوة التالية هي تطبيق FactoryBean لاعتراض استدعاء أساليب الواجهة.
MappingFactoryBean.java package org.restclient.factory; import lombok.ToString; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.ArrayUtils; import org.restclient.annotations.RestInterceptor; import org.restclient.annotations.ServiceMapping; import org.restclient.annotations.Type; import org.restclient.config.ServicesConfiguration; import org.restclient.config.ServicesConfiguration.RouteSettings; import org.restclient.interceptor.Interceptor; import org.restclient.model.MappingMetadata; import org.restclient.model.Pair; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.ResponseEntity; import org.springframework.jmx.access.InvocationFailureException; import org.springframework.lang.NonNull; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.client.RestClientResponseException; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.client.WebClient.ResponseSpec; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import javax.naming.ConfigurationException; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; @Slf4j @ToString public class MappingFactoryBean implements BeanFactoryAware, FactoryBean<Object>, ApplicationContextAware { private static final Collection<String> ignoredMethods = Arrays.asList("equals", "hashCode", "toString"); private Class<?> type; private List<Object> fallbackInstances; private List<MappingMetadata> metadatas; private String alias; private ApplicationContext applicationContext; private BeanFactory beanFactory; @Override public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } @Override public Class<?> getObjectType() { return type; } @Override public boolean isSingleton() { return true; } @Override public Object getObject() { return Enhancer.create(type, (MethodInterceptor) (instance, method, args, methodProxy) -> { final boolean skip = ignoredMethods.stream().anyMatch(ignore -> method.getName().equals(ignore)); final ServiceMapping annotation = method.getAnnotation(ServiceMapping.class); if (!skip && annotation != null) { return invokeMethod(annotation, method, args); } return null; }); } private Object invokeMethod(ServiceMapping annotation, Method method, Object[] args) throws Throwable { final MappingMetadata metadata = findMetadataByMethodName(method.getName()); if (metadata == null) { throw new NoSuchMethodException(String.format("Cant find metadata for method %s. Check your mapping configuration!", method.getName())); } final RouteSettings routeSettings = findSettingsByAlias(alias); final String host = routeSettings.getHost(); String url = metadata.getUrl().replace(String.format("${%s}", alias), host); final HttpMethod httpMethod = metadata.getHttpMethod(); final HttpHeaders httpHeaders = metadata.getHttpHeaders(); final List<Pair<String, Object>> foundVars = new ArrayList<>(); final List<Pair<String, Object>> foundParams = new ArrayList<>(); final List<Pair<String, Object>> foundHeaders = new ArrayList<>(); final Parameter[] parameters = method.getParameters(); final Object body = initHttpVariables(args, parameters, foundVars, foundParams, foundHeaders); url = replaceHttpVariables(url, foundVars, foundParams, foundHeaders, httpHeaders); preHandle(args, body, httpHeaders); if (log.isDebugEnabled()) { log.debug("Execute Service Mapping request"); log.debug("Url: {}", url); log.debug("Headers: {}", httpHeaders); if (body != null) { log.debug("Body: {}", body); } } final Object call = handleHttpCall(annotation, args, url, httpMethod, body, httpHeaders, metadata); postHandle(ResponseEntity.ok(call)); return call; } private Object handleHttpCall(ServiceMapping annotation, Object[] args, String url, HttpMethod httpMethod, Object body, HttpHeaders httpHeaders, MappingMetadata metadata) throws Throwable { final WebClient webClient = WebClient.create(url); ResponseSpec responseSpec; final Class<?> returnType = metadata.getReturnType(); try { if (body != null) { responseSpec = webClient .method(httpMethod) .headers(c -> c.addAll(httpHeaders)) .body(BodyInserters.fromPublisher(Mono.just(body), Object.class)) .retrieve(); } else { responseSpec = webClient .method(httpMethod) .headers(c -> c.addAll(httpHeaders)) .retrieve(); } } catch (RestClientResponseException ex) { if (log.isDebugEnabled()) { log.debug("Error on execute route request - Code: {}, Error: {}, Route: {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), url); } final String fallbackMethod = metadata.getFallbackMethod(); final Object target = fallbackInstances.stream() .filter(o -> o.getClass().getSimpleName().equals(annotation.fallbackClass().getSimpleName())) .findFirst().orElse(null); Method fallback = null; if (target != null) { fallback = Arrays.stream(target.getClass().getMethods()) .filter(m -> m.getName().equals(fallbackMethod)) .findFirst() .orElse(null); } if (fallback != null) { args = Arrays.copyOf(args, args.length + 1); args[args.length - 1] = ex; final Object result = fallback.invoke(target, args); return Mono.just(result); } else if (returnType == Mono.class) { return Mono.just(ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString())); } else if (returnType == Flux.class) { return Flux.just(ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString())); } else { return Mono.empty(); } } final Method method = metadata.getMethod(); final Type classType = method.getDeclaredAnnotation(Type.class); final Class<?> type = classType == null ? Object.class : classType.type(); if (returnType == Mono.class) { return responseSpec.bodyToMono(type); } else if (returnType == Flux.class) { return responseSpec.bodyToFlux(type); } return null; } private String replaceHttpVariables(String url, final List<Pair<String, Object>> foundVars, final List<Pair<String, Object>> foundParams, final List<Pair<String, Object>> foundHeaders, final HttpHeaders httpHeaders) { for (Pair<String, Object> pair : foundVars) { url = url.replace(String.format("${%s}", pair.getKey()), String.valueOf(pair.getValue())); } for (Pair<String, Object> pair : foundParams) { url = url.replace(String.format("${%s}", pair.getKey()), String.valueOf(pair.getValue())); } foundHeaders.forEach(pair -> { final String headerName = pair.getKey(); if (httpHeaders.getFirst(headerName) != null) { httpHeaders.set(headerName, String.valueOf(pair.getValue())); } else { log.warn("Undefined request header name '{}'! Check mapping configuration!", headerName); } }); return url; } private Object initHttpVariables(final Object[] args, final Parameter[] parameters, final List<Pair<String, Object>> foundVars, final List<Pair<String, Object>> foundParams, final List<Pair<String, Object>> foundHeaders) { Object body = null; for (int i = 0; i < parameters.length; i++) { final Object value = args[i]; final Parameter parameter = parameters[i]; final PathVariable pv = parameter.getDeclaredAnnotation(PathVariable.class); final RequestParam rp = parameter.getDeclaredAnnotation(RequestParam.class); final RequestHeader rh = parameter.getDeclaredAnnotation(RequestHeader.class); final RequestBody rb = parameter.getDeclaredAnnotation(RequestBody.class); if (rb != null) { body = value; } if (rh != null) { foundHeaders.add(new Pair<>(rh.value(), value)); } if (pv != null) { final String name = pv.value(); foundVars.add(new Pair<>(name, value)); } if (rp != null) { final String name = rp.value(); foundParams.add(new Pair<>(name, value)); } } return body; } private void preHandle(Object[] args, Object body, HttpHeaders httpHeaders) { final Map<String, Interceptor> beansOfType = applicationContext.getBeansOfType(Interceptor.class); beansOfType.values() .stream() .filter(i -> i.getClass().isAnnotationPresent(RestInterceptor.class) && ArrayUtils.contains(i.getClass().getDeclaredAnnotation(RestInterceptor.class).aliases(), alias)) .forEach(i -> i.preHandle(args, body, httpHeaders)); } private void postHandle(ResponseEntity<?> responseEntity) { final Map<String, Interceptor> beansOfType = applicationContext.getBeansOfType(Interceptor.class); beansOfType.values() .stream() .filter(i -> i.getClass().isAnnotationPresent(RestInterceptor.class) && ArrayUtils.contains(i.getClass().getDeclaredAnnotation(RestInterceptor.class).aliases(), alias)) .forEach(i -> i.postHandle(responseEntity)); } private MappingMetadata findMetadataByMethodName(String methodName) { return metadatas .stream() .filter(m -> m.getMethodName().equals(methodName)).findFirst() .orElseThrow(() -> new InvocationFailureException("")); } private RouteSettings findSettingsByAlias(String alias) throws ConfigurationException { final ServicesConfiguration servicesConfiguration = applicationContext.getAutowireCapableBeanFactory().getBean(ServicesConfiguration.class); return servicesConfiguration.getRoutes() .stream() .filter(r -> r.getAlias().equals(alias)) .findFirst() .orElseThrow(() -> new ConfigurationException(String.format("Cant find service host! Check configuration. Alias: %s", alias))); } @SuppressWarnings("unused") public Class<?> getType() { return type; } @SuppressWarnings("unused") public void setType(Class<?> type) { this.type = type; } @SuppressWarnings("unused") public List<MappingMetadata> getMetadatas() { return metadatas; } @SuppressWarnings("unused") public void setMetadatas(List<MappingMetadata> metadatas) { this.metadatas = metadatas; } @SuppressWarnings("unused") public String getAlias() { return alias; } @SuppressWarnings("unused") public void setAlias(String alias) { this.alias = alias; } @SuppressWarnings("unused") public List<Object> getFallbackInstances() { return fallbackInstances; } @SuppressWarnings("unused") public void setFallbackInstances(List<Object> fallbackInstances) { this.fallbackInstances = fallbackInstances; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MappingFactoryBean that = (MappingFactoryBean) o; return Objects.equals(type, that.type); } @Override public int hashCode() { return Objects.hash(type); } }
سأشرح بإيجاز ما يقوم به هذا التطبيق لكائن bin:
- يوفر تخزين المعلومات الوصفية لأساليب الواجهة مع إعدادات طلب الموارد ، مثل الطرق التي تم تحديدها بواسطة التعليقات التوضيحية أو فئات الرفض أو مجموعة من نماذج إعدادات التوجيه ؛
- يوفر اعتراض لاستدعاء أسلوب في سياق التطبيق باستخدام CGlib (MappingFactoryBean # getObject ()) ، أي بشكل رسمي ، لا يوجد تطبيق للأسلوب الذي تم استدعاؤه ، لكن يتم اعتراض الطريقة ماديًا ، ووفقًا للمعلمات والوسائط التوضيحية وسيطات الطريقة ، تتم معالجة طلب HTTP.
والخطوة الثالثة هي تنفيذ المكون المنخفض المستوى من الحاوية DI Spring ، وبالتحديد واجهة ImportBeanDefinitionRegistrar.
ServiceMappingRegistrator.java package org.restclient.factory; import lombok.extern.slf4j.Slf4j; import org.restclient.annotations.Header; import org.restclient.annotations.Mapping; import org.restclient.annotations.ServiceMapping; import org.restclient.model.MappingMetadata; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.EnvironmentAware; import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.lang.NonNull; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestParam; import javax.naming.ConfigurationException; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Parameter; import java.util.*; import java.util.stream.Collectors; @Slf4j public class ServiceMappingRegistrator implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { private ResourceLoader resourceLoader; private Environment environment; @Override public void setEnvironment(@NonNull Environment environment) { this.environment = environment; } @Override public void setResourceLoader(@NonNull ResourceLoader resourceLoader) { this.resourceLoader = resourceLoader; } @Override public void registerBeanDefinitions(@NonNull AnnotationMetadata metadata, @NonNull BeanDefinitionRegistry registry) { registerMappings(metadata, registry); } private void registerMappings(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { final ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(resourceLoader); final Set<String> basePackages = getBasePackages(metadata); final AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(Mapping.class); scanner.addIncludeFilter(annotationTypeFilter); basePackages .stream() .map(scanner::findCandidateComponents) .flatMap(Collection::stream) .filter(candidateComponent -> candidateComponent instanceof AnnotatedBeanDefinition) .map(candidateComponent -> (AnnotatedBeanDefinition) candidateComponent) .map(AnnotatedBeanDefinition::getMetadata) .map(ClassMetadata::getClassName) .forEach(className -> buildGateway(className, registry)); } private void buildGateway(String className, BeanDefinitionRegistry registry) { try { final Class<?> type = Class.forName(className); final List<Method> methods = Arrays .stream(type.getMethods()) .filter(method -> method.isAnnotationPresent(ServiceMapping.class)) .collect(Collectors.toList()); final String alias = type.getDeclaredAnnotation(Mapping.class).alias(); final List<MappingMetadata> metadatas = new ArrayList<>(); final List<Object> fallbackInstances = new ArrayList<>(); for (Method method : methods) { final ServiceMapping serviceMapping = method.getDeclaredAnnotation(ServiceMapping.class); final Class<?>[] args = method.getParameterTypes(); final Header[] defaultHeaders = serviceMapping.defaultHeaders(); final String path = serviceMapping.path(); final HttpMethod httpMethod = serviceMapping.method(); final HttpHeaders httpHeaders = new HttpHeaders(); final StringBuilder url = new StringBuilder(); url.append("${").append(alias).append("}").append(path); final Parameter[] parameters = method.getParameters(); for (int i = 0; i < parameters.length; i++) { final Parameter parameter = parameters[i]; for (Annotation annotation : parameter.getAnnotations()) { if (!checkValidParams(annotation, args)) { break; } if (annotation instanceof RequestParam) { final String argName = ((RequestParam) annotation).value(); if (argName.isEmpty()) { throw new ConfigurationException("Configuration error: defined RequestParam annotation dont have value! Api method: " + method.getName() + ", Api Class: " + type); } final String toString = url.toString(); if (toString.endsWith("&") && i + 1 == args.length) { url.append(argName).append("=").append("${").append(argName).append("}"); } else if (!toString.endsWith("&") && i + 1 == args.length) { url.append("?").append(argName).append("=").append("${").append(argName).append("}"); } else if (!toString.endsWith("&")) { url.append("?").append(argName).append("=").append("${").append(argName).append("}").append("&"); } else { url.append(argName).append("=").append("${").append(argName).append("}").append("&"); } } else if (annotation instanceof PathVariable) { final String argName = ((PathVariable) annotation).value(); if (argName.isEmpty()) { throw new ConfigurationException("Configuration error: defined PathVariable annotation dont have value! Api method: " + method.getName() + ", Api Class: " + type); } final String toString = url.toString(); final String argStr = String.format("${%s}", argName); if (!toString.contains(argStr)) { if (toString.endsWith("/")) { url.append(argStr); } else { url.append("/").append(argStr); } } } else if (annotation instanceof RequestHeader) { final String argName = ((RequestHeader) annotation).value(); if (argName.isEmpty()) { throw new ConfigurationException("Configuration error: defined RequestHeader annotation dont have value! Api method: " + method.getName() + ", Api Class: " + type); } httpHeaders.add(argName, String.format("${%s}", argName)); } } } if (defaultHeaders.length > 0) { Arrays.stream(defaultHeaders) .forEach(header -> httpHeaders.add(header.name(), header.value())); } final Object instance = serviceMapping.fallbackClass().newInstance(); fallbackInstances.add(instance); final String fallbackName = serviceMapping.fallbackMethod(); final String buildedUrl = url.toString(); final MappingMetadata mappingMetadata = new MappingMetadata(method, httpMethod, buildedUrl, httpHeaders, fallbackName); metadatas.add(mappingMetadata); log.info("Bind api path - alias: {}, url: {}", alias, buildedUrl); } final BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(MappingFactoryBean.class); beanDefinitionBuilder.addPropertyValue("type", className); beanDefinitionBuilder.addPropertyValue("alias", alias); beanDefinitionBuilder.addPropertyValue("metadatas", metadatas); beanDefinitionBuilder.addPropertyValue("fallbackInstances", fallbackInstances); final AbstractBeanDefinition beanDefinition = beanDefinitionBuilder.getBeanDefinition(); final BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{type.getSimpleName()}); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); } catch (IllegalAccessException | InstantiationException | ClassNotFoundException | ConfigurationException e) { e.printStackTrace(); } } private boolean checkValidParams(Annotation annotation, Object[] args) { Arrays .stream(args) .map(Object::getClass) .forEach(type -> { if (annotation instanceof RequestParam) { if (type.isAnnotationPresent(PathVariable.class)) { throw new IllegalArgumentException("Annotation RequestParam cannot be used with PathVariable"); } } else if (annotation instanceof PathVariable) { if (type.isAnnotationPresent(RequestParam.class)) { throw new IllegalArgumentException("Annotation PathVariable cannot be used with RequestParam"); } } }); return true; } private Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) { Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(SpringBootApplication.class.getCanonicalName()); if (attributes == null) { attributes = importingClassMetadata.getAnnotationAttributes(ComponentScan.class.getCanonicalName()); } Set<String> basePackages = new HashSet<>(); if (attributes != null) { basePackages = Arrays.stream((String[]) attributes.get("scanBasePackages")).filter(StringUtils::hasText).collect(Collectors.toSet()); Arrays.stream((Class[]) attributes.get("scanBasePackageClasses")).map(ClassUtils::getPackageName).forEach(basePackages::add); } if (basePackages.isEmpty()) { basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName())); } return basePackages; } private ClassPathScanningCandidateComponentProvider getScanner() { return new ClassPathScanningCandidateComponentProvider(false, environment) { @Override protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { boolean isCandidate = false; if (beanDefinition.getMetadata().isIndependent()) { if (!beanDefinition.getMetadata().isAnnotation()) { isCandidate = true; } } return isCandidate; } }; } }
أي ما يحدث في بداية بدء التطبيق - عند تشغيل حدث سياق Spring REFRESH ، سيتم إشراك جميع تطبيقات واجهة ImportBeanDefinitionRegistrar التي يتم استيرادها في سياق التطبيق ، وسيتم استدعاء طريقة registerBeanDefinitions ، والتي ستتلقى معلومات حول فئات التكوين المشروح وسجل المصنع / تخزين الفاصوليا (المكونات ، الخدمات ، والمستودعات ، وما إلى ذلك) ، وبهذه الطريقة يمكنك الحصول على معلومات حول حزم التطبيقات الأساسية و "أي طريقة للحفر" للبحث عن واجهاتنا وتهيئتها باستخدام قوة BeanDefinitionBulder وتنفيذنا MappingFactoryBean. لاستيراد المسجل ، يكفي استخدام التعليق التوضيحي للاستيراد باسم هذه الفئة (في التطبيق الحالي للوحدة النمطية ، يتم استخدام فئة التكوين RestClientAutoConfiguration ، حيث تتم كتابة التعليقات التوضيحية اللازمة للوحدة لكي تعمل).
كيفية الاستخدام
الحالة - نريد الحصول على قائمة بالمعلومات من مخزن مستخدم GitHub معين.
1) كتابة التكوين للعمل مع الخدمة (application.yml)
services: routes: - host: https://api.github.com # GitHub alias: github-service # , Mapping
1) تنفيذ واجهة للتفاعل مع الخدمة
@Mapping(alias = "github-service")
2) دعوة الخدمة
@SprinBootApplication public class RestApp { public static void main(String... args) { final ConfigurableApplicationContext context = SpringApplication.run(RestApp.class, args); final RestGateway restGateway = context.getType(RestGateway.class); final Mono<ArrayList> response = restGateway.getRepos("gencloud"); response.doOnSuccess(list -> log.info("Received response: {}", list)).subscribe(); } }
نتيجة للتنفيذ في تصحيح الأخطاء ، يمكنك رؤية هذا (للراحة ، يمكنك وضع برنامج التفاف كائن لاستجابة json الناتجة بدلاً من نوع ArrayList ؛ الرمز مختلف لأنه استخدم اختبار وحدة في مقصورة مع مكتبة اختبار المفاعل ، لكن المبدأ لم يتغير):

استنتاج
بالطبع لا يحب الجميع هذا النهج ، وهو عبارة عن تصحيح معقد ، وتعليق توضيحي في المكان الخطأ - حصلت على صفعة في الوجه من ConfigurationException ، اكتب بعض التكوينات ، ...
أقبل رغبات واقتراحات بناءة لتطوير واجهة برمجة التطبيقات ، وآمل أن تكون المقالة مفيدة للقراءة. شكرا لاهتمامكم
كل رمز متاح هنا.