Автоматизация HTTP запросов в контексте Spring

от автора

Предыстория

Несколько месяцев назад поступила задача по написанию HTTP API работы с продуктом компании, а именно обернуть все запросы с помощью 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); 

… простенький метод, принимающий тип, тело и заголовки запроса. И все бы хорошо, но выглядело как костыль и не особо юзабельно в контексте Spring.
И пока товарищи джуны писали "костыли" в своих ветках, мне пришла в голову гениальнейшая идея — а почему бы не писать эти запросы "в одну строчку" (like Feign).

Идея

У нас в руках имеется мощный DI контейнер Spring, так почему бы не использовать его функционал в полной мере? В частности инициализации Data репозиториев на примере Jpa. Предо мной стояла задача инициализация класса типа интерфейс в контексте Spring и три варианта решения перехвата вызова метода, как типичной реализации — Aspect, PostProcess и BeanDefinitionRegistrar.

Кодовая база

Первым делом — аннотации, куда же без них, иначе как конфигурировать запросы.

1) Mapping — аннотация, идентифицирующая интерфейс как компонент HTTP вызовов.

@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Mapping {     /**      * Registered service application name, need for config      */     String alias(); }

Параметр alias отвечает за присваивание корневого роутинга сервиса, будь то https://habr.com, https://github.com, etc.

2) ServiceMapping — аннотация, идентифицирующая метод интерфейса, который должен быть вызван как стандартный HTTP запрос к приложению, откуда мы хотим получить ответ либо выполнить какое-либо действие.

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @Documented public @interface ServiceMapping {     /**      * Registered service application route      */     String path();      /**      * Registered service application route http-method      */     HttpMethod method();      Header[] defaultHeaders() default {};      Class<?> fallbackClass() default Object.class;      String fallbackMethod() default ""; }

Параметры:

  • path — путь запроса, пример alias + /ru/hub/${hub_name};
  • method — метод HTTP запроса (GET, POST, PUT, etc.);
  • defaultHeaders — статические заголовки запроса, которые неизменяемые для удаленного ресурса (Content-Type, Accept, etc.);
  • fallbackClass — класс отбраковки запроса, который обработался с ошибкой (Exception);
  • fallbackMethod — наименование метода класса, который должен вернуть корректный результат, если произошла ошибка (Exception).

3) Header — аннотация, идентифицирующая статические заголовки в настройках запроса

@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.ANNOTATION_TYPE}) @Documented public @interface Header {     String name();      String value(); }

Параметры:

  • name — наименование заголовка;
  • 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.*;  /**  * @author: GenCloud  * @created: 2019/08  */ @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;         });     }      /**      * It determines the meta-information of the executing method, calling an HTTP request based on the      * meta-information found; interceptors are also called.      *      * @param annotation - main annotation that defines the path, type, standard request parameters.      * @param method     - callable method      * @param args       - method arguments      * @return if the request is executed without errors, returns a clean server response in wrappers Mono/Flux.      * @throws Throwable      */     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);     } }

Вкратце объясню, что делает эта реализация объекта бина:

  • обеспечивает хранение мета-информации методов интерфейса с настройками запросов к ресурсу, таких как сами методы идентифицированные аннотациями, классы отбраковки, коллекция моделей настроек роутинга;
  • обеспечивает перехват вызова метода в контексте приложения с помощью 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;  /**  * @author: GenCloud  * @created: 2019/08  */ @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 в который поступает информация о аннотированных конфигурационных классах и фабрика-регистратор/хранилище бинов (компонентов, сервисов, репозиториев, etc.), и прямо в этом методе можно получить информацию о базовых пакетах приложения и "в какую сторону копать" для поиска наших интерфейсов и их инициализации с помощью BeanDefinitionBulder и нашей реализацией MappingFactoryBean. Для импортирования регистратора достаточно использовать аннотацию Import с именем этого класса (в текущей реализации модуля используется конфигурационный класс RestClientAutoConfiguration, где и прописаны необходимые аннотации для работы модуля).

Как использовать

Кейс — мы хотим получить список информации некоего репозитория GitHub пользователя.

1) Написание конфигурации для работы с сервисом (application.yml)

services:   routes:     - host: https://api.github.com # корневой роутинг АПИ GitHub       alias: github-service # наименование сервиса, использующееся в аннотации Mapping

1) Реализация интерфейса по взаимодействию с сервисом

@Mapping(alias = "github-service") // алиас сервиса указанный в конфигурации public interface RestGateway {     /**      * Метод получения информации всех репозиториев конкретного пользователя.      *      * @param userName - пользователь GitHub      * @return массив объектов LinkedHashMap      */     @ServiceMapping(path = "/users/${userName}/repos", method = GET)     @Type(type = ArrayList.class) // тип возвращаемого объекта при сериализации в обертке Mono/Flux     Mono<ArrayList> getRepos(@PathVariable("userName") String userName); }

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

Как результат выполнения в дебаге можно увидеть это (для удобства можно подложить за место типа ArrayList объектную обертку результирующего json ответа; код отличается, потому что использовал юнит тест в купе с reactor-test библиотекой, но принцип от этого не изменился):
image

Заключение

Не всем конечно по душе такой подход, сложный дебаг, не там ткнул аннотацию — получил оплеуху от ConfigurationException, еще и какие-то конфиги писать, ой…
Приму конструктивные пожелания и предложения по развитию API, надеюсь, что статья была полезной к прочтению. Спасибо за внимание.

Весь код доступен по ссылке — https://github.com/GenCloud/rest_client.


ссылка на оригинал статьи https://habr.com/ru/post/464089/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *