Статьи

Создание нескольких функций RouterFunction в Spring WebFlux

В этой статье мы рассмотрим определение нескольких функций маршрутизатора для различных логических доменов в Spring WebFlux. Это не может быть проблемой, если вы создаете «Микросервисы», так как вы, скорее всего, будете работать только в одном домене для каждой службы, но если вы этого не сделаете, вам, вероятно, потребуется включить в ваше приложение несколько доменов, которые пользователи или ваши собственные сервисы могут взаимодействовать с. Код для этого настолько прост, насколько я надеялся, и может быть объяснен в нескольких предложениях. Чтобы сделать этот пост немного более интересным, мы рассмотрим код Spring, который делает все это возможным.

Если вы новичок в WebFlux, я рекомендую взглянуть на мою предыдущую статью [Работа с Spring WebFlux] ( https://lankydanblog.com/2018/03/15/doing-stuff-with-spring-webflux/ ), где я написал несколько подробных примеров и объяснений по этому вопросу.

Итак, давайте сначала установим сцену. У вас есть два разных домена в вашем приложении, скажем люди и местоположения. Возможно, вы захотите держать их отделенными друг от друга не только логически, но и внутри вашего кода. Для этого вам нужен способ определения ваших маршрутов отдельно от каждого другого домена. Вот что мы рассмотрим в этом посте.

Если вы думаете, что уже знаете ответ на эту проблему, то вы, вероятно, правы. Это действительно так просто. Давайте работать до этого, хотя. Чтобы создать маршруты только для домена пользователей, создайте компонент RouterFunction который отображается на соответствующие функции-обработчики, как RouterFunction ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
@Configuration
public class MyRouter {
  // works for a single bean
  @Bean
  public RouterFunction<ServerResponse> routes(PersonHandler personHandler) {
    return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get)
        .andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all)
        .andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post)
        .andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put)
        .andRoute(DELETE("/people/{id}"), personHandler::delete)
        .andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry);
  }
}

Это создает маршруты к различным функциям-обработчикам в PersonHandler .

Итак, теперь мы хотим добавить маршруты для логики местоположения. Мы могли бы просто добавить маршруты к этому бину, как показано ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
@Configuration
public class MyRouter {
  // not ideal!
  @Bean
  public RouterFunction<ServerResponse> routes(PersonHandler personHandler, LocationHandler locationHandler) {
    return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get)
        .andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all)
        .andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post)
        .andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put)
        .andRoute(DELETE("/people/{id}"), personHandler::delete)
        .andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry)
        .andRoute(GET("/locations/{id}").and(accept(APPLICATION_JSON)), locationHandler::get);
  }
}

Теперь компонент содержит ссылку на LocationHandler поэтому маршрут местоположения можно настроить. Проблема с этим решением состоит в том, что он требует, чтобы код был связан вместе. Более того, если вам нужно добавить еще больше обработчиков, вы скоро будете перегружены количеством зависимостей, вводимых в этот bean-компонент.

Способ обойти это — создать несколько RouterFunction . Вот и все. Таким образом, если мы создадим один в домене людей, скажем PersonRouter и один в домене местоположений с именем LocationRouter , каждый из них может определить нужные им маршруты, а Spring сделает все остальное. Это работает, потому что Spring проходит через контекст приложения, находит или создает любые компоненты RouterFunction и объединяет их в одну функцию для последующего использования.

Используя эту информацию, мы можем написать код ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
@Configuration
public class PersonRouter {
  // solution
  @Bean
  public RouterFunction<ServerResponse> peopleRoutes(PersonHandler personHandler) {
    return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get)
        .andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all)
        .andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post)
        .andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put)
        .andRoute(DELETE("/people/{id}"), personHandler::delete)
        .andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry);
  }
}

и

1
2
3
4
5
6
7
8
@Configuration
public class LocationRouter {
  // solution
  @Bean
  public RouterFunction<ServerResponse> locationRoutes(LocationHandler locationHandler) {
    return RouterFunctions.route(GET("/locations/{id}").and(accept(APPLICATION_JSON)), locationHandler::get);
  }
}

PersonRouter может храниться с кодом другого человека / человека, и LocationRouter может делать то же самое.

Чтобы сделать это более интересным, почему это работает?

RouterFunctionMapping — это класс, который извлекает все bean- RouterFunction созданные в контексте приложения. Компонент RouterFunctionMapping создается в WebFluxConfigurationSupport который является эпицентром для конфигурации Spring WebFlux. @EnableWebFlux аннотацию @EnableWebFlux в класс конфигурации или полагаясь на автоконфигурацию, запускается цепочка событий, и одним из них является сбор всех наших RouterFunction .

Ниже представлен класс RouterFunctionMapping . Я удалил его конструкторы и несколько методов, чтобы сделать фрагмент здесь немного легче для восприятия.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class RouterFunctionMapping extends AbstractHandlerMapping implements InitializingBean {
 
  @Nullable
  private RouterFunction<?> routerFunction;
 
  private List<HttpMessageReader<?>> messageReaders = Collections.emptyList();
 
  // constructors
 
  // getRouterFunction
 
  // setMessageReaders
 
  @Override
  public void afterPropertiesSet() throws Exception {
    if (CollectionUtils.isEmpty(this.messageReaders)) {
      ServerCodecConfigurer codecConfigurer = ServerCodecConfigurer.create();
      this.messageReaders = codecConfigurer.getReaders();
    }
 
    if (this.routerFunction == null) {
      initRouterFunctions();
    }
  }
 
  /**
   * Initialized the router functions by detecting them in the application context.
   */
  protected void initRouterFunctions() {
    if (logger.isDebugEnabled()) {
      logger.debug("Looking for router functions in application context: " +
          getApplicationContext());
    }
 
    List<RouterFunction<?>> routerFunctions = routerFunctions();
    if (!CollectionUtils.isEmpty(routerFunctions) && logger.isInfoEnabled()) {
      routerFunctions.forEach(routerFunction -> logger.info("Mapped " + routerFunction));
    }
    this.routerFunction = routerFunctions.stream()
        .reduce(RouterFunction::andOther)
        .orElse(null);
  }
 
  private List<RouterFunction<?>> routerFunctions() {
    SortedRouterFunctionsContainer container = new SortedRouterFunctionsContainer();
    obtainApplicationContext().getAutowireCapableBeanFactory().autowireBean(container);
 
    return CollectionUtils.isEmpty(container.routerFunctions) ? Collections.emptyList() :
        container.routerFunctions;
  }
 
  // getHandlerInternal
 
  private static class SortedRouterFunctionsContainer {
 
    @Nullable
    private List<RouterFunction<?>> routerFunctions;
 
    @Autowired(required = false)
    public void setRouterFunctions(List<RouterFunction<?>> routerFunctions) {
      this.routerFunctions = routerFunctions;
    }
  }
 
}

Путь к извлечению всех маршрутов начинается в afterPropertiesSet который вызывается после RouterFunctionMapping компонента RouterFunctionMapping . Поскольку его внутренняя RouterFunction имеет значение null она вызывает initRouterFunctions вызывая серию методов, ведущих к выполнению routerFunctions . Создается новый SortedRouterFunctionsContainer (частный статический класс), устанавливающий поле routerFunctions путем внедрения всех RouterFunction из контекста приложения. Это работает, так как Spring вводит все bean-компоненты типа T при вводе List<T> . Теперь извлеченные функции RouterFunction объединяются в единую RouterFunction которая теперь используется для маршрутизации всех входящих запросов к соответствующему обработчику.

Это все, что нужно сделать. В заключение, определение нескольких RouterFunction для разных бизнес-доменов очень просто, поскольку вы просто создаете их в любой области, в которой они наиболее целесообразны, и Spring отключается и извлекает их все. Чтобы раскрыть некоторые чудеса, мы RouterFunctionMapping чтобы увидеть, как RouterFunction нами функции RouterFunction собираются и объединяются, чтобы их можно было использовать для маршрутизации запросов к обработчикам. В качестве заключительного замечания я понимаю, что этот пост в некоторых отношениях довольно тривиален, но иногда кажущаяся очевидной информация может быть довольно полезной.

Если вы еще этого не сделали, я рекомендую посмотреть мой предыдущий пост [Работа с Spring WebFlux] ( https://lankydanblog.com/2018/03/15/doing-stuff-with-spring-webflux/ ).

Наконец, если вы нашли этот пост полезным и хотели бы следить за моими новыми постами, пока я пишу их, вы можете подписаться на меня в Twitter по адресу @LankyDanDev .

Опубликовано на Java Code Geeks с разрешения Дэна Ньютона, партнера нашей программы JCG . См. Оригинальную статью здесь: Создание нескольких функций RouterFunction в Spring WebFlux

Мнения, высказанные участниками Java Code Geeks, являются их собственными.