使用Spring Cloud Gateway保護反應式微服務(一)

反應式編程是使你的應用程序更高效的一種愈來愈流行的方式。響應式應用程序異步調用響應,而不是調用資源並等待響應。這使他們能夠釋放處理能力,僅在必要時執行處理,而且比其餘系統更有效地擴展。html

Java生態系統在反應框架中佔有至關大的份額,其中包括Play框架,Ratpack,Vert.x和Spring WebFlux。像反應式編程同樣,微服務架構能夠幫助大型團隊快速擴展,而且可使用上述任何出色的框架進行構建。java

今天,我想向你展現如何使用Spring Cloud Gateway,Spring Boot和Spring WebFlux構建反應性微服務架構。咱們將利用Spring Cloud Gateway,由於API網關一般是雲原生微服務架構中的重要組件,可爲你全部的後端微服務提供聚合層。react

本教程將向你展現如何使用REST API構建微服務,該API返回新車列表。你將使用Eureka進行服務發現,並使用Spring Cloud Gateway將請求路由到微服務。而後,你將集成Spring Security,以便只有通過身份驗證的用戶才能訪問你的API網關和微服務。web

 

 

PrerequisitesHTTPie (or cURL), Java 11+, and an internet connection.spring

Spring Cloud Gateway vs. Zuul

Zuul是Netflix的API網關。Zuul最初於2013年發佈,最初並不具備反應性,但Zuul 2是完全的重寫,使其具備反應性。不幸的是,Spring Cloud不支持Zuul 2,而且可能永遠不會支持。mongodb

如今,Spring Cloud Gateway是Spring Cloud Team首選的API網關實現。它基於Spring 5,Reactor和Spring WebFlux。不只如此,它還包括斷路器集成,使用Eureka進行服務發現,而且與OAuth 2.0集成起來要容易得多!docker

接下來讓咱們深刻了解。數據庫

建立一個Spring Cloud Eureka Server項目

首先建立一個目錄來保存你的全部項目,例如spring-cloud-gateway。在終端窗口中導航至它,並建立一個包括Spring Cloud Eureka Server做爲依賴項的discovery-service項目。編程

 

http https://start.spring.io/starter.zip javaVersion==11 artifactId==discovery-service \json

  name==eureka-service baseDir==discovery-service \

  dependencies==cloud-eureka-server | tar -xzvf -

 

上面的命令使用HTTPie。我強烈建議安裝它。你也可使用捲曲。運行curlhttps//start.spring.io以查看語法。

 

在其主類上添加@EnableEurekaServer,以將其用做Eureka server。

 

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

 

@EnableEurekaServer

@SpringBootApplication

public class EurekaServiceApplication {...}

將如下屬性添加到項目的src / main / resources / application.properties文件中,以配置其端口並關閉Eureka註冊。

 

server.port=8761

eureka.client.register-with-eureka=false

要使發現服務在Java 11+上運行,請添加對JAXB的依賴關係。

 

<dependency>

    <groupId>org.glassfish.jaxb</groupId>

    <artifactId>jaxb-runtime</artifactId>

</dependency>

使用./mvnw spring-boot:run或經過在IDE中運行它來啓動項目。

建立一個Spring Cloud Gateway項目

接下來,建立一個包含一些Spring Cloud依賴項的api-gateway項目。

http https://start.spring.io/starter.zip javaVersion==11 artifactId==api-gateway \

  name==api-gateway baseDir==api-gateway \

  dependencies==actuator,cloud-eureka,cloud-feign,cloud-gateway,cloud-hystrix,webflux,lombok | tar -xzvf -

一分鐘後,咱們將從新配置該項目。

使用Spring WebFlux建立反應式微服務

Car微服務將包含此示例代碼的很大一部分,由於它包含支持CRUD的功能齊全的REST API。

使用start.spring.io建立car-service項目:

 

http https://start.spring.io/starter.zip javaVersion==11 artifactId==car-service \

  name==car-service baseDir==car-service \

  dependencies==actuator,cloud-eureka,webflux,data-mongodb-reactive,flapdoodle-mongo,lombok | tar -xzvf -

這個命令中的dependencies參數頗有趣。你能夠看到其中包括Spring WebFlux,以及MongoDB。Spring Data還爲Redis和Cassandra提供了響應式驅動程序。

你可能還對R2DBC(反應性關係數據庫鏈接)感興趣,R2DBC是一種將反應性編程API引入SQL數據庫的工做。在本示例中,我沒有使用它,由於在start.spring.io上尚不可用。

使用Spring WebFlux構建REST API

我是大衆的忠實擁護者,尤爲是公交車和臭蟲等經典車。你是否知道大衆在將來幾年內將推出大量電動汽車? 我對ID Buzz感到很是興奮,它具備經典曲線,全電動並且擁有350匹以上的馬力!

若是你不熟悉ID Buzz,請看這張來自大衆汽車的照片。

 

 

讓咱們從這個API示例中得到樂趣,並使用電動VW做爲咱們的數據集。該API將跟蹤各類汽車名稱和發佈日期。

在src / main / java /…/ CarServiceApplication.java中添加Eureka註冊,示例數據初始化和反應性REST API:

 

package com.example.carservice;

 

import lombok.AllArgsConstructor;

import lombok.Data;

import lombok.NoArgsConstructor;

import lombok.extern.slf4j.Slf4j;

import org.springframework.boot.ApplicationRunner;

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.netflix.eureka.EnableEurekaClient;

import org.springframework.context.annotation.Bean;

import org.springframework.data.annotation.Id;

import org.springframework.data.mongodb.core.mapping.Document;

import org.springframework.data.mongodb.repository.ReactiveMongoRepository;

import org.springframework.http.HttpStatus;

import org.springframework.http.ResponseEntity;

import org.springframework.web.bind.annotation.*;

import reactor.core.publisher.Flux;

import reactor.core.publisher.Mono;

 

import java.time.LocalDate;

import java.time.Month;

import java.util.Set;

import java.util.UUID;

 

@EnableEurekaClient (1)

@SpringBootApplication

@Slf4j (2)

public class CarServiceApplication {

 

    public static void main(String[] args) {

        SpringApplication.run(CarServiceApplication.class, args);

    }

 

    @Bean (3)

    ApplicationRunner init(CarRepository repository) {

        // Electric VWs from https://www.vw.com/electric-concepts/

        // Release dates from https://www.motor1.com/features/346407/volkswagen-id-price-on-sale/

        Car ID = new Car(UUID.randomUUID(), "ID.", LocalDate.of(2019, Month.DECEMBER, 1));

        Car ID_CROZZ = new Car(UUID.randomUUID(), "ID. CROZZ", LocalDate.of(2021, Month.MAY, 1));

        Car ID_VIZZION = new Car(UUID.randomUUID(), "ID. VIZZION", LocalDate.of(2021, Month.DECEMBER, 1));

        Car ID_BUZZ = new Car(UUID.randomUUID(), "ID. BUZZ", LocalDate.of(2021, Month.DECEMBER, 1));

        Set<Car> vwConcepts = Set.of(ID, ID_BUZZ, ID_CROZZ, ID_VIZZION);

 

        return args -> {

            repository

                    .deleteAll() (4)

                    .thenMany(

                            Flux

                                    .just(vwConcepts)

                                    .flatMap(repository::saveAll)

                    )

                    .thenMany(repository.findAll())

                    .subscribe(car -> log.info("saving " + car.toString())); (5)

        };

    }

}

 

@Document

@Data

@NoArgsConstructor

@AllArgsConstructor

class Car { (6)

    @Id

    private UUID id;

    private String name;

    private LocalDate releaseDate;

}

 

interface CarRepository extends ReactiveMongoRepository<Car, UUID> {

} (7)

 

@RestController

class CarController { (8)

 

    private CarRepository carRepository;

 

    public CarController(CarRepository carRepository) {

        this.carRepository = carRepository;

    }

 

    @PostMapping("/cars")

    @ResponseStatus(HttpStatus.CREATED)

    public Mono<Car> addCar(@RequestBody Car car) { (9)

        return carRepository.save(car);

    }

 

    @GetMapping("/cars")

    public Flux<Car> getCars() { (10)

        return carRepository.findAll();

    }

 

    @DeleteMapping("/cars/{id}")

    public Mono<ResponseEntity<Void>> deleteCar(@PathVariable("id") UUID id) {

        return carRepository.findById(id)

                .flatMap(car -> carRepository.delete(car)

                        .then(Mono.just(new ResponseEntity<Void>(HttpStatus.OK)))

                )

                .defaultIfEmpty(new ResponseEntity<>(HttpStatus.NOT_FOUND));

    }

}

  1. 添加@EnableEurekaClient批註以進行服務發現
  2. @ Slf4j是Lombok的一個方便註釋,可用於登陸類
  3. ApplicationRunner bean用默認數據填充MongoDB
  4. 刪除MongoDB中的全部現有數據,以避免添加新數據
  5. 訂閱結果,以便同時調用deleteAll()和saveAll()
  6. 帶有Spring Data NoSQL和Lombok註釋的汽車類,以減小樣板
  7. CarRepository接口擴展了ReactiveMongoRepository,幾乎沒有任何代碼便可提供CRUD功能!
  8. 使用CarRepository執行CRUD操做的CarController類
  9. Spring WebFlux返回單個對象的Mono發佈者

10. 爲多個對象返回一個Flex發佈者

 

若是你使用IDE來構建項目,則須要爲IDE設置Lombok

你還須要修改car-service項目的application.properties以設置其名稱和端口

 

spring.application.name=car-service

server.port=8081

運行MongoDB

運行MongoDB的最簡單方法是從car-service / pom.xml中的flappoodle依賴項中刪除測試範圍。這將致使你的應用程序啓動嵌入式MongoDB依賴關係。

 

<dependency>

    <groupId>de.flapdoodle.embed</groupId>

    <artifactId>de.flapdoodle.embed.mongo</artifactId>

    <!--<scope>test</scope>-->

</dependency>

你還可使用Homebrew安裝和運行MongoDB

 

brew tap mongodb/brew

brew install mongodb-community@4.2

mongod

或者,使用Docker:

docker run -d -it -p 27017:27017 mongo

使用WebFlux傳輸數據

這就完成了使用Spring WebFlux構建REST API所需完成的全部工做。

「可是等等!」你可能會說。 「我覺得WebFlux就是關於流數據的?」

在此特定示例中,你仍然能夠從/cars端點流式傳輸數據,但不能在瀏覽器中。

除了使用服務器發送事件或WebSocket以外,瀏覽器沒法使用流。可是,非瀏覽器客戶端能夠經過發送具備application/stream+json值的Accept header來獲取JSON流(感謝Rajeev Singh的技巧)。

你能夠經過啓動瀏覽器並使用HTTPie發出請求來測試此時一切正常。可是,編寫自動化測試會更好!

使用WebTestClient測試你的WebFlux API

WebClient是Spring WebFlux的一部分,可用於發出響應請求,接收響應以及使用有效負載填充對象。 伴隨類WebTestClient可用於測試WebFlux API。 它包含與WebClient類似的請求方法,以及檢查響應正文,狀態和標頭的方法。

修改car-service項目中的src/test/java/…/CarServiceApplicationTests.java類以包含如下代碼。

 

package com.example.carservice;

 

import org.junit.Test;

import org.junit.runner.RunWith;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.boot.test.context.SpringBootTest;

import org.springframework.http.MediaType;

import org.springframework.test.context.junit4.SpringRunner;

import org.springframework.test.web.reactive.server.WebTestClient;

import reactor.core.publisher.Mono;

 

import java.time.LocalDate;

import java.time.Month;

import java.util.Collections;

import java.util.UUID;

 

@RunWith(SpringRunner.class)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,

        properties = {"spring.cloud.discovery.enabled = false"})

public class CarServiceApplicationTests {

 

    @Autowired

    CarRepository carRepository;

 

    @Autowired

    WebTestClient webTestClient;

 

    @Test

    public void testAddCar() {

        Car buggy = new Car(UUID.randomUUID(), "ID. BUGGY", LocalDate.of(2022, Month.DECEMBER, 1));

 

        webTestClient.post().uri("/cars")

                .contentType(MediaType.APPLICATION_JSON_UTF8)

                .accept(MediaType.APPLICATION_JSON_UTF8)

                .body(Mono.just(buggy), Car.class)

                .exchange()

                .expectStatus().isCreated()

                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)

                .expectBody()

                .jsonPath("$.id").isNotEmpty()

                .jsonPath("$.name").isEqualTo("ID. BUGGY");

    }

 

    @Test

    public void testGetAllCars() {

        webTestClient.get().uri("/cars")

                .accept(MediaType.APPLICATION_JSON_UTF8)

                .exchange()

                .expectStatus().isOk()

                .expectHeader().contentType(MediaType.APPLICATION_JSON_UTF8)

                .expectBodyList(Car.class);

    }

 

    @Test

    public void testDeleteCar() {

        Car buzzCargo = carRepository.save(new Car(UUID.randomUUID(), "ID. BUZZ CARGO",

                LocalDate.of(2022, Month.DECEMBER, 2))).block();

 

        webTestClient.delete()

                .uri("/cars/{id}", Collections.singletonMap("id", buzzCargo.getId()))

                .exchange()

                .expectStatus().isOk();

    }

}

爲了證實它有效,請運行./mvnw test。測試經過後,請拍一下本身的背!

若是你使用的是Windows,請使用 mvnw test.

 

這篇先講到這,下篇繼續:使用Spring Cloud Gateway保護反應式微服務(二)

另外近期整理了一套完整的java架構思惟導圖,分享給一樣正在認真學習的每位朋友~

相關文章
相關標籤/搜索