Hintergrund
Vor einigen Monaten bestand die Aufgabe darin, eine HTTP-API für die Arbeit mit einem Unternehmensprodukt zu schreiben, nämlich alle Anforderungen mit RestTemplate zu verpacken, dann Informationen aus der Anwendung abzufangen und die Antwort zu ändern. Eine ungefähre Implementierung des Dienstes für die Arbeit mit der Anwendung war wie folgt:
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);
... eine einfache Methode, die die Header für Typ, Text und Anforderung akzeptiert. Und alles wäre in Ordnung, aber es sah aus wie eine Krücke und im Kontext des Frühlings nicht sehr brauchbar.
Und während Kollegen in ihren Filialen funktional über den alten Mechanismus schrieben, hatte ich die genialste Idee - warum nicht diese Anfragen „in einer Zeile“ schreiben (wie Feign).
Idee
Wir haben einen leistungsstarken Spring DI-Behälter in der Hand. Warum also nicht seine Funktionalität voll ausnutzen? Insbesondere das Initialisieren von Datenrepositorys anhand des Jpa-Beispiels. Ich stand vor der Aufgabe, eine Klasse eines Schnittstellentyps im Kontext von Spring und drei Lösungen zum Abfangen eines Methodenaufrufs als typische Implementierung zu initialisieren - Aspect, PostProcess und BeanDefinitionRegistrar.
Codebasis
Zuallererst Anmerkungen, wo ohne sie, sonst wie man Abfragen konfiguriert.
1) Mapping - Annotation, die die Schnittstelle als Bestandteil von HTTP-Aufrufen identifiziert.
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Mapping { String alias(); }
Der Alias-Parameter ist für die Zuweisung des Root-Routings des Dienstes verantwortlich, sei es https://habr.com , https://github.com usw.
2) ServiceMapping - eine Anmerkung, die eine Schnittstellenmethode angibt, die als Standard-HTTP-Anforderung an die Anwendung aufgerufen werden soll, von der aus wir eine Antwort erhalten oder eine Aktion ausführen möchten.
@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 ""; }
Parameter:
- Pfad - Anforderungspfad, Beispielalias + / ru / hub / $ {Hubname};
- Methode - HTTP-Anforderungsmethode (GET, POST, PUT usw.);
- defaultHeaders - statische Anforderungsheader, die für eine Remote-Ressource unveränderlich sind (Inhaltstyp, Akzeptieren usw.);
- fallbackClass - die Klasse der Ablehnung der Anforderung, die mit einem Fehler verarbeitet wurde (Ausnahme);
- fallbackMethod - Der Name der Klassenmethode, die im Fehlerfall das richtige Ergebnis zurückgeben soll (Ausnahme).
3) Header - Anmerkung zur Identifizierung statischer Header in den Anforderungseinstellungen
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) @Documented public @interface Header { String name(); String value(); }
Parameter:
- Name - Titel der Kopfzeile;
- value ist der Wert des Headers.
Der nächste Schritt besteht darin, Ihre FactoryBean zu implementieren, um den Aufruf von Schnittstellenmethoden abzufangen.
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); } }
Ich werde kurz erklären, was diese Implementierung des bin-Objekts bewirkt:
- bietet Speicherung von Metainformationen von Schnittstellenmethoden mit Ressourcenanforderungseinstellungen, wie z. B. den Methoden selbst, die durch Anmerkungen identifiziert werden, Ablehnungsklassen, eine Sammlung von Routingeinstellungsmodellen;
- bietet das Abfangen eines Methodenaufrufs im Anwendungskontext unter Verwendung von CGlib (MappingFactoryBean # getObject ()), d.h. Formal gibt es keine Implementierung der aufgerufenen Methode, aber die Methode wird physisch abgefangen und abhängig von den Parametern, den Annotations- und Methodenargumenten wird die HTTP-Anforderung verarbeitet.
Der dritte Schritt ist die Implementierung der Low-Level-Komponente des DI-Containers Spring und insbesondere der ImportBeanDefinitionRegistrar-Schnittstelle.
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; } }; } }
Das heißt, Was passiert zu Beginn des Anwendungsstarts? Wenn das Spring REFRESH-Kontextereignis ausgelöst wird, werden alle in den Anwendungskontext importierten importBeanDefinitionRegistrar-Schnittstellenimplementierungen einbezogen, und die registerBeanDefinitions-Methode wird aufgerufen, die Informationen zu kommentierten Konfigurationsklassen und dem Factory-Registrar / Store von Beans (Komponenten) erhält. Dienste, Repositorys usw.), und direkt in dieser Methode können Sie Informationen zu den grundlegenden Anwendungspaketen und "welche Art zu graben" erhalten, um nach unseren Schnittstellen zu suchen und sie mit zu initialisieren die Leistungsfähigkeit von BeanDefinitionBulder und unsere Implementierung von MappingFactoryBean. Zum Importieren des Registrars reicht es aus, die Annotation " Importieren" mit dem Namen dieser Klasse zu verwenden (in der aktuellen Implementierung des Moduls wird die Konfigurationsklasse "RestClientAutoConfiguration" verwendet, in der die erforderlichen Annotationen geschrieben sind, damit das Modul funktioniert).
Wie man es benutzt
Fall - Wir möchten eine Liste mit Informationen aus einem bestimmten GitHub-Benutzer-Repository abrufen.
1) Schreiben der Konfiguration für die Arbeit mit dem Dienst (application.yml)
services: routes: - host: https://api.github.com # GitHub alias: github-service # , Mapping
1) Implementierung der Schnittstelle für die Interaktion mit dem Dienst
@Mapping(alias = "github-service")
2) Serviceabruf
@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(); } }
Als Ergebnis der Ausführung im Debug können Sie dies sehen (der Einfachheit halber können Sie anstelle eines ArrayList-Typs einen Objekt-Wrapper für die resultierende JSON-Antwort einfügen; der Code unterscheidet sich, da ein Komponententest in einem Kompartiment mit einer Reaktortestbibliothek verwendet wurde, das Prinzip jedoch nicht geändert wurde):

Fazit
Natürlich mag nicht jeder diesen Ansatz, ein kompliziertes Debugging, eine an der falschen Stelle gestochene Annotation - von ConfigurationException einen Schlag ins Gesicht bekommen, ein paar Konfigurationen schreiben, oh ...
Ich werde konstruktive Wünsche und Vorschläge für die Entwicklung der API annehmen, ich hoffe, dass der Artikel zum Lesen nützlich war. Vielen Dank für Ihre Aufmerksamkeit.
Der gesamte Code ist hier verfügbar.