Micronaut HTTP路由

2023-03-02 16:09 更新

上一節(jié)中使用的 @Controller 注釋是允許您控制 HTTP 路由構(gòu)造的幾個注釋之一。

URI 路徑

@Controller 注釋的值是一個 RFC-6570 URI 模板,因此您可以使用 URI 模板規(guī)范定義的語法在路徑中嵌入 URI 變量。

許多其他框架,包括 Spring,都實現(xiàn)了 URI 模板規(guī)范

實際實現(xiàn)由擴展 UriTemplate 的 UriMatchTemplate 類處理。

您可以在應(yīng)用程序中使用此類來構(gòu)建 URI,例如:

使用 UriTemplate

 Java Groovy  Kotlin 
UriMatchTemplate template = UriMatchTemplate.of("/hello/{name}");

assertTrue(template.match("/hello/John").isPresent()); // (1)
assertEquals("/hello/John", template.expand( // (2)
        Collections.singletonMap("name", "John")
));
given:
UriMatchTemplate template = UriMatchTemplate.of("/hello/{name}")

expect:
template.match("/hello/John").isPresent() // (1)
template.expand(["name": "John"]) == "/hello/John" // (2)
val template = UriMatchTemplate.of("/hello/{name}")

assertTrue(template.match("/hello/John").isPresent) // (1)
assertEquals("/hello/John", template.expand(mapOf("name" to "John")))  // (2)
  1. 使用 match 方法匹配路徑

  2. 使用 expand 方法將模板擴展為 URI

您可以使用 UriTemplate 構(gòu)建路徑以包含在您的響應(yīng)中。

URI 路徑變量

URI 變量可以通過方法參數(shù)引用。例如:

URI 變量示例

 Java Groovy  Kotlin 
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;

@Controller("/issues") // (1)
public class IssuesController {

    @Get("/{number}") // (2)
    public String issue(@PathVariable Integer number) { // (3)
        return "Issue # " + number + "!"; // (4)
    }
}
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable

@Controller("/issues") // (1)
class IssuesController {

    @Get("/{number}") // (2)
    String issue(@PathVariable Integer number) { // (3)
        "Issue # " + number + "!" // (4)
    }
}
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.PathVariable

@Controller("/issues") // (1)
class IssuesController {

    @Get("/{number}") // (2)
    fun issue(@PathVariable number: Int): String { // (3)
        return "Issue # $number!" // (4)
    }
}
  1. @Controller 注釋使用 /issues 的基本 URI 指定

  2. Get 注釋將方法映射到 HTTP GET,URI 變量嵌入在名為 number 的 URI 中

  3. 方法參數(shù)可以選擇用 PathVariable 注釋

  4. URI變量的值在實現(xiàn)中被引用

Micronaut 為上述控制器映射 URI /issues/{number}。我們可以通過編寫單元測試來斷言這種情況:

測試 URI 變量

 Java Groovy  Kotlin 
import io.micronaut.context.ApplicationContext;
import io.micronaut.http.client.HttpClient;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.runtime.server.EmbeddedServer;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;

public class IssuesControllerTest {

    private static EmbeddedServer server;
    private static HttpClient client;

    @BeforeClass // (1)
    public static void setupServer() {
        server = ApplicationContext.run(EmbeddedServer.class);
        client = server
                    .getApplicationContext()
                    .createBean(HttpClient.class, server.getURL());
    }

    @AfterClass // (2)
    public static void stopServer() {
        if (server != null) {
            server.stop();
        }
        if (client != null) {
            client.stop();
        }
    }

    @Test
    public void testIssue() {
        String body = client.toBlocking().retrieve("/issues/12"); // (3)

        assertNotNull(body);
        assertEquals("Issue # 12!", body); // (4)
    }

    @Test
    public void testShowWithInvalidInteger() {
        HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () ->
                client.toBlocking().exchange("/issues/hello"));

        assertEquals(400, e.getStatus().getCode()); // (5)
    }

    @Test
    public void testIssueWithoutNumber() {
        HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () ->
                client.toBlocking().exchange("/issues/"));

        assertEquals(404, e.getStatus().getCode()); // (6)
    }
}
import io.micronaut.context.ApplicationContext
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer
import spock.lang.AutoCleanup
import spock.lang.Shared
import spock.lang.Specification

class IssuesControllerTest extends Specification {

    @Shared
    @AutoCleanup // (2)
    EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) // (1)

    @Shared
    @AutoCleanup // (2)
    HttpClient client = HttpClient.create(embeddedServer.URL) // (1)

    void "test issue"() {
        when:
        String body = client.toBlocking().retrieve("/issues/12") // (3)

        then:
        body != null
        body == "Issue # 12!" // (4)
    }

    void "/issues/{number} with an invalid Integer number responds 400"() {
        when:
        client.toBlocking().exchange("/issues/hello")

        then:
        def e = thrown(HttpClientResponseException)
        e.status.code == 400 // (5)
    }

    void "/issues/{number} without number responds 404"() {
        when:
        client.toBlocking().exchange("/issues/")

        then:
        def e = thrown(HttpClientResponseException)
        e.status.code == 404 // (6)
    }
}
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.micronaut.context.ApplicationContext
import io.micronaut.http.client.HttpClient
import io.micronaut.http.client.exceptions.HttpClientResponseException
import io.micronaut.runtime.server.EmbeddedServer

class IssuesControllerTest: StringSpec() {

    val embeddedServer = autoClose( // (2)
        ApplicationContext.run(EmbeddedServer::class.java) // (1)
    )

    val client = autoClose( // (2)
        embeddedServer.applicationContext.createBean(
            HttpClient::class.java,
            embeddedServer.url) // (1)
    )

    init {
        "test issue" {
            val body = client.toBlocking().retrieve("/issues/12") // (3)

            body shouldNotBe null
            body shouldBe "Issue # 12!" // (4)
        }

        "test issue with invalid integer" {
            val e = shouldThrow<HttpClientResponseException> {
                client.toBlocking().exchange<Any>("/issues/hello")
            }

            e.status.code shouldBe 400 // (5)
        }

        "test issue without number" {
            val e = shouldThrow<HttpClientResponseException> {
                client.toBlocking().exchange<Any>("/issues/")
            }

            e.status.code shouldBe 404 // (6)
        }
    }
}
  1. 嵌入式服務(wù)器和 HTTP 客戶端啟動

  2. 測試完成后清理服務(wù)器和客戶端

  3. 測試向 URI /issues/12 發(fā)送請求

  4. 然后斷言響應(yīng)是“Issue #12”

  5. 另一個測試斷言當(dāng)在 URL 中發(fā)送無效數(shù)字時返回 400 響應(yīng)

  6. 另一個測試斷言,當(dāng) URL 中未提供數(shù)字時,將返回 404 響應(yīng)。要執(zhí)行的路由需要存在的變量。

請注意,前面示例中的 URI 模板要求指定數(shù)字變量。您可以使用以下語法指定可選的 URI 模板:/issues{/number} 并使用 @Nullable 注釋數(shù)字參數(shù)。

下表提供了 URI 模板示例及其匹配項:

表 1. URI 模板匹配
模板 描述 匹配 URI

/books/{id}

簡單匹配

/books/1

/books/{id:2}

最多兩個字符的變量

/books/10

/books{/id}

一個可選的 URI 變量

/books/10 or /books

/book{/id:[a-zA-Z]+}

帶有正則表達式的可選 URI 變量

/books/foo

/books{?max,offset}

可選查詢參數(shù)

/books?max=10&offset=10

/books{/path:.*}{.ext}

正則表達式路徑與擴展匹配

/books/foo/bar.xml

URI 保留字符匹配

默認情況下,RFC-6570 URI 模板規(guī)范定義的 URI 變量不能包含保留字符,例如 /、? 等等。

如果您希望匹配或擴展整個路徑,這可能會有問題。根據(jù)規(guī)范的第 3.2.3 節(jié),您可以使用 + 運算符使用保留擴展或匹配。

例如,URI /books/{+path} 與 /books/foo 和 /books/foo/bar 都匹配,因為 + 指示變量路徑應(yīng)包含保留字符(在本例中為 /)。

路由注釋

前面的示例使用 @Get 注釋添加了一個接受 HTTP GET 請求的方法。下表總結(jié)了可用的注釋以及它們?nèi)绾斡成涞?nbsp;HTTP 方法:

表 2. HTTP 路由注釋
注解 HTTP 方法

@Delete

DELETE

@Get

GET

@Head

HEAD

@Options

OPTIONS

@Patch

PATCH

@Put

PUT

@Post

POST

@Trace

TRACE

所有方法注解默認為/。

多個 URI

每個路由注釋都支持多個 URI 模板。對于每個模板,都會創(chuàng)建一條路線。此功能非常有用,例如更改 API 的路徑并保留現(xiàn)有路徑以實現(xiàn)向后兼容性。例如:

多個 URI

 Java Groovy  Kotlin 
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;

@Controller("/hello")
public class BackwardCompatibleController {

    @Get(uris = {"/{name}", "/person/{name}"}) // (1)
    public String hello(String name) { // (2)
        return "Hello, " + name;
    }
}
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/hello")
class BackwardCompatibleController {

    @Get(uris = ["/{name}", "/person/{name}"]) // (1)
    String hello(String name) { // (2)
        "Hello, $name"
    }
}
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get

@Controller("/hello")
class BackwardCompatibleController {

    @Get(uris = ["/{name}", "/person/{name}"]) // (1)
    fun hello(name: String): String { // (2)
        return "Hello, $name"
    }
}
  1. 指定多個模板

  2. 像往常一樣綁定到模板參數(shù)

多個模板的路由驗證更加復(fù)雜。如果一個通常需要的變量在所有模板中都不存在,則該變量被認為是可選的,因為它可能不會在每次執(zhí)行該方法時都存在。

以編程方式構(gòu)建路線

如果您不喜歡使用注解而是在代碼中聲明所有路由,那么不要擔(dān)心,Micronaut 有一個靈活的 RouteBuilder API,可以輕松地以編程方式定義路由。

首先,繼承 DefaultRouteBuilder 并將要路由到的控制器注入到該方法中,然后定義您的路由:

URI 變量示例

 Java Groovy  Kotlin 
import io.micronaut.context.ExecutionHandleLocator;
import io.micronaut.web.router.DefaultRouteBuilder;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

@Singleton
public class MyRoutes extends DefaultRouteBuilder { // (1)

    public MyRoutes(ExecutionHandleLocator executionHandleLocator,
                    UriNamingStrategy uriNamingStrategy) {
        super(executionHandleLocator, uriNamingStrategy);
    }

    @Inject
    void issuesRoutes(IssuesController issuesController) { // (2)
        GET("/issues/show/{number}", issuesController, "issue", Integer.class); // (3)
    }
}
import io.micronaut.context.ExecutionHandleLocator
import io.micronaut.core.convert.ConversionService
import io.micronaut.web.router.GroovyRouteBuilder

import jakarta.inject.Inject
import jakarta.inject.Singleton

@Singleton
class MyRoutes extends GroovyRouteBuilder { // (1)

    MyRoutes(ExecutionHandleLocator executionHandleLocator,
             UriNamingStrategy uriNamingStrategy,
             ConversionService conversionService) {
        super(executionHandleLocator, uriNamingStrategy, conversionService)
    }

    @Inject
    void issuesRoutes(IssuesController issuesController) { // (2)
        GET("/issues/show/{number}", issuesController.&issue) // (3)
    }
}
import io.micronaut.context.ExecutionHandleLocator
import io.micronaut.web.router.DefaultRouteBuilder
import io.micronaut.web.router.RouteBuilder
import jakarta.inject.Inject
import jakarta.inject.Singleton

@Singleton
class MyRoutes(executionHandleLocator: ExecutionHandleLocator,
               uriNamingStrategy: RouteBuilder.UriNamingStrategy) :
        DefaultRouteBuilder(executionHandleLocator, uriNamingStrategy) { // (1)

    @Inject
    fun issuesRoutes(issuesController: IssuesController) { // (2)
        GET("/issues/show/{number}", issuesController, "issue", Int::class.java) // (3)
    }
}
  1. 路由定義應(yīng)該是 DefaultRouteBuilder 的子類

  2. 使用@Inject 注入一個方法與控制器路由到

  3. 使用 RouteBuilder::GET(String,Class,String,Class… ) 等方法路由到控制器方法。請注意,即使使用問題控制器,路由也不知道其@Controller 注釋,因此必須指定完整路徑。

不幸的是,由于類型擦除,Java 方法 lambda 引用不能與 API 一起使用。對于 Groovy,有一個可以子類化的 GroovyRouteBuilder 類,它允許傳遞 Groovy 方法引用。

路由編譯時驗證

Micronaut 支持在編譯時使用驗證庫驗證路由參數(shù)。首先,將驗證依賴項添加到您的構(gòu)建中:

build.gradle

annotationProcessor "io.micronaut:micronaut-validation" // Java only
kapt "io.micronaut:micronaut-validation" // Kotlin only
implementation "io.micronaut:micronaut-validation"

通過對類路徑的正確依賴,路由參數(shù)將在編譯時自動檢查。如果滿足以下任一條件,編譯將失?。?/p>

  • URI 模板包含一個可選的變量,但方法參數(shù)未使用 @Nullable 進行注釋或者是一個 java.util.Optional。

可選變量是允許路由與 URI 匹配的變量,即使該值不存在也是如此。例如 /foo{/bar} 匹配對 /foo 和 /foo/abc 的請求。非可選變體是 /foo/{bar}。

  • URI 模板包含方法參數(shù)中缺少的變量。

要禁用路由編譯時驗證,請設(shè)置系統(tǒng)屬性 -Dmicronaut.route.validation=false。對于使用 Gradle 的 Java 和 Kotlin 用戶,可以通過從 annotationProcessor/kapt 范圍中移除驗證依賴來實現(xiàn)相同的效果。

路由非標(biāo)準(zhǔn) HTTP 方法

@CustomHttpMethod 注釋支持客戶端或服務(wù)器的非標(biāo)準(zhǔn) HTTP 方法。 RFC-4918 Webdav 等規(guī)范需要額外的方法,例如 REPORT 或 LOCK。

路由示例

@CustomHttpMethod(method = "LOCK", value = "/{name}")
String lock(String name)

注釋可以在任何可以使用標(biāo)準(zhǔn)方法注釋的地方使用,包括控制器和聲明性 HTTP 客戶端。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號