В этой статье продолжим дорабатывать предыдущий пример с Eureka и client-side load balancing — добавим в него Spring Cloud API Gateway.
Что такое Spring Cloud API Gateway
Это отдельное Spring Boot приложение, через которое проходят все запросы, реализация шаблона Reverse Proxy. То есть микросервисы не знают друг о друге, а обращаются к прокси. Внешнему пользователю тоже известен только прокси. Прокси, в свою очередь, анализирует запрос, перенаправляет его к нужному микросервису и возвращает ответ обратно. Ниже мы рассмотрим, как в прокси задать условия — какой запрос к какому микросервису направить.
Зачем нужен Spring Cloud API Gateway
Например для того, чтобы зафиксировать REST API. Представьте, что вы разрабатываете микросервисы и обсуждаете с фронтенд-разработчиком REST API. У вас постоянно что-то меняется: сегодня микросервис выдает данные по такому url, завтра — по-другому. А то и вовсе данные будут выдаваться новым микросервисом.
Можно зафиксировать REST API на прокси и менять внутреннюю структуру как угодно. Снаружи ничего не поменяется, просто прокси будет обращаться по другим адресам. А обращения к самому прокси останутся прежними.
Spring Cloud API Gateway vs. Zuul
Пример сделан на новом Spring Cloud API Gateway.
Zuul имеет примерно ту же функциональность, но Zuul 1.x не реактивный. Zuul 2.0 реактивный, но Spring его поддерживает хуже, чем Zuul 1.x. Поэтому если у нас реактивный стек, то Spring Cloud API Gateway — лучший выбор.
Наша структура
Мы продолжим разрабатывать предыдущий пример. Есть микросервис Zoo — он выдает случайное животное. Раньше пользователь в браузере обращался к нему, но теперь Zoo будет запущен в двух экземплярах (вот, еще одно преимущество), а пользователь будет обращаться к прокси, запущенному на порту 8080. Это прокси и есть наш Spring Cloud API Gateway.
Есть также микросервис Random Animal — к нему обращался Zoo, чтобы получить это животное, но теперь Zoo будет обращаться тоже к прокси. А прокси, в свою очередь, к Random Animal. Random Animal тоже запущен в двух экземплярах (но можно запустить сколько угодно, пример будет работать).
В общем картина такая:
Proxy должен как-то решать, к какому микросервису направлять пришедший запрос.
Эта логика задается с помощью элементов, перечисленных ниже.
Элементы Spring Cloud API Gateway
(Spring Cloud API Gateway работает на сервере Netty.)
Чтобы сопоставить входной url (идущий в Spring Cloud Gateway) выходному url (идущему к микросервису), нужно задать три пункта:
- Predicate: условие, при котором запрос перенаправляется (например, если url соответствует такому-то шаблону).
- URI: содержит uri куда перенаправляем запрос (к какому микросервису).
- Filter (необязательно): как модифицировать запрос (на пути туда или обратно).
Эти три пункта (еще идентификатор Route-а) объединены в Route — основной строительный блок. Из нескольких таких блоков и состоит настройка.
Два Route-а мы настроим ниже. Но сначала добавим в приложение Maven-зависимость и зададим ему имя (для Eureka).
Maven-зависимость
Чтобы сделать Spring Boot приложение API Gateway-ем, добавим зависимость:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
Также (поскольку мы уже включили в предыдущий пример Eureka), чтобы другие микросервисы могли обращаться к API Gateway-ю по имени, а не по адресу с портом (типа localhost:8080), сделаем Spring Cloud API Gateway клиентом Eureka:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
А обращаться они будут к нему по имени proxy, что и зададим ниже.
Имя приложения API Gateway
В нашей системе API Gateway будет обнаруживаться по имени proxy — так мы назовем наше приложение:
server: port: 8080 spring: application: name: proxy
А запущен он будет на порту 8080.
Сопоставление адресов: настройка Route-ов: URI, Predicate и Filter
Итак, зададим, что если url обращения к прокси (который у нас на порту 8080) начинается с /zoo:
localhost:8080/zoo/....
то прокси переводит обращение на микросервис Zoo.
А если с /random-animal:
localhost:8080/random-animal/...
то на микросервис Random Animal:
Вообще настроить Spring Cloud API Gateway можно как в коде, так и в application.yml.
Настройка в коде
Видно, что в настройке фигурируют два Route (как на картинке выше). Для каждого задан Uri, Predicate и Filter:
@Configuration class ProxyConfig { @Bean RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route("random_animal_route", route -> route.path("/random-animal/**") .and() .method(HttpMethod.GET) .filters(filter -> filter.stripPrefix(1) ) .uri("lb://random-animal")) .route("zoo_route", route -> route.path("/zoo/**") .filters(filter -> filter.stripPrefix(1) ) .uri("lb://zoo")) .build(); } }
- Uri
В uri() задан микросервис, куда идет перенаправление: zoo или random-animal. Важно не пытаться прописать тут вложенные пути — не сработает. Только имя микросервиса (под которым он зарегистрирован в Eureka). Префикс lb говорит о том, что обращение к микросервису должно проходить через балансировщик нагрузки — то есть заодно еще автоматически будет принято решение о том, к какому именно экземпляру микросервиса обратиться.
- Predicate
Здесь могут быть заданы любые условия, по которым запрос отбирается. Как уже говорилось, мы отбираем запрос по path() — задаем шаблон «url начинается с того-то». Еще сказано, что это должен быть метод GET (просто для демонстрации возможностей). Все эти заданные условия и есть предикат.
- Filter
В фильтре мы отбрасываем вот эту начальную часть url, по которой выбрали запрос (/zoo/ и /random-animal/). В контроллер микросервиса запрос пойдет без этой части.
Аргумент 1 в методе:
filter.stripPrefix(1)
означает, что именно одну часть отбрасываем. Если бы мы отбирали запрос по двум начальным частям, например localhost:8080/zoo/part2/..., то отбросили бы две части, чтобы не тянуть их в контроллер.
Проверка
Контроллер в микросервисе Zoo у нас такой:
@RestController public class ZooController { @Autowired private RandomAnimalClient randomAnimalClient; @GetMapping("/animals/any") ResponseEntity<Animal> seeAnyAnimal(){ return randomAnimalClient.random(); } }
Обращение к нему через прокси будет таким:
/zoo отбрасывается, в контроллер идет /animals/any.
А внутри контроллера в Zoo обращение ко второму микросервису уже по имени:
@Component public class RandomAnimalClient { private static final Logger LOGGER = LoggerFactory .getLogger(RandomAnimalClient.class); private final RestTemplate loadBalancedTemplate; RandomAnimalClient(@LoadBalanced RestTemplate loadBalancedTemplate, DiscoveryClient discoveryClient) { this.loadBalancedTemplate = loadBalancedTemplate; } public ResponseEntity<Animal> random1() { LOGGER.debug("Sending request for animal {}"); return loadBalancedTemplate.getForEntity("http://proxy/random-animal/random", Animal.class); } }
В запросе
http://proxy/random-animal/random
использовано имя нашего API Gateway в Eureka — proxy. Обращение сделано через @LoadBalanced RestTemplate — это значит, что экземпляров proxy вообще может быть несколько (можно запустить API Gateway на нескольких портах — и запрос будет работать).
Часть url /random-animal/ служит для того, чтобы отобрать запрос для направления в микросервис Random Animal, а затем отбрасывается. В микросервис идет только /random.
Соответственно контроллер в микросервисе Random Animal принимает запросы, начинающиеся с /random:
@RestController public class RandomAnimalController { private final AnimalDao animalDao; public RandomAnimalController(AnimalDao animalDao){ this.animalDao=animalDao; } @GetMapping("/random") public Animal randomAnimal(){ Animal animal=animalDao.random(); System.out.println(animal); return animal; } }
Настройка в application.yml
Настройку из кода можно перенести в файл настроек:
spring: cloud: gateway: routes: - id: random_animal_route uri: lb://random-animal predicates: - Path=/random-animal/** filters: - StripPrefix=1 - id: zoo_route uri: lb://zoo predicates: - Path=/zoo/** filters: - StripPrefix=1
Итоги
Пример можно скачать на GitHub. Все три части — Zoo, Random Animal и Proxy — можно запускать на любом количестве портов.
Далее рассмотрим Spring Cloud Configuration Server.
Добрый день,
Спасибо за статью, очень хорошо описано.
Единственное не понял один момент: так что всё таки что мне надо ввести в адресную строку браузера со стороны клиента чтобы вернулось рандомальное животное?. В начале статьи вы пишете что клиент как и любой другой микросервис обращается только к прокси. Но далее указываете запрос http://localhost:8080/zoo/animals/any тоесть тут прокси почему то нет. И если такой запрос ввести в адресную строку браузера, то я проверил будет 500-я ошибка, и в стектрейсте прокси-сервиса будет чото такое:
java.net.UnknownHostException: failed to resolve ‘DESKTOP-EHPR0NG’ after 2 queries
at io.netty.resolver.dns.DnsResolveContext.finishResolve(DnsResolveContext.java:1013) ~[netty-resolver-dns-4.1.58.Final.jar:4.1.58.Final]
Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
Error has been observed at the following site(s):
|_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
|_ checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
|_ checkpoint ⇢ HTTP GET «/zoo/animals/any» [ExceptionHandlingWebHandler]
Подскажите, что я делаю не правильно?
Прокси в примере — это то, что запущено на порту 8080. По имени proxy мы можем обращаться к нему из других микросервисов (только благодаря Eureka), но не из браузера.
(Zoo в примере запущен на 8081, из браузера обращение http://localhost:8080/zoo/animals/any есть обращение к прокси, а не к Zoo).
Перепроверка примера ошибок не показала. Но убедитесь, что запускаете Eureka ДО запуска остальных микросервисов. Чтобы микросервис смог зарегистрироваться в Eureka, Eureka должна быть предварительно запущена. Тогда обращение по имени http://proxy/random-animal/random (proxy вместо localhost:8080) сработает, а иначе нет. Посмотрите пример по Eureka.
Спасибо, да работает, видимо чего то не доглядел в первый раз
По адресу Еврики http://localhost:8761/ можно проверить список зарегистрированных микросервисов.
Добрый день!
Спасибо за статью, очень ценный и полезный материал.
Один вопрос, как запретить прямой доступ к сервисам zoo и random?
А, то получается, что весь смысл PROXY сервиса теряется из-за того, что можно напрямую обращаться к сервисам zoo и random, указав полный путь к сервису, например, вот так: http://localhost:8101/animals/any, или вот так: http://localhost:8201/random
Настройками сети — я так понимаю, это уж дело сисадмина.
Zuul Proxy, насколько я помню из коробки закрывал доступ к микросервисам и достучаться до них не было возможно. Если найду решение, обязательно поделюсь. Еще раз спасибо за статью!
меня тоже интересует этот вопрос -как закрыть доступ к остальным сервисам
Получилось найти какое-то решение?
Одно из самых понятных объяснений, которые я нашел в сети. Спасибо вам огромное!
Из application.yml убрал эту запись, не хотело взлетать:
eureka:
client:
healthcheck:
enabled: true