上一節(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)
|
使用 match 方法匹配路徑
使用 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)
}
}
|
@Controller 注釋使用 /issues 的基本 URI 指定
Get 注釋將方法映射到 HTTP GET,URI 變量嵌入在名為 number 的 URI 中
方法參數(shù)可以選擇用 PathVariable 注釋
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)
}
}
}
|
嵌入式服務(wù)器和 HTTP 客戶端啟動
測試完成后清理服務(wù)器和客戶端
測試向 URI /issues/12 發(fā)送請求
然后斷言響應(yīng)是“Issue #12”
另一個測試斷言當(dāng)在 URL 中發(fā)送無效數(shù)字時返回 400 響應(yīng)
另一個測試斷言,當(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 方法:
所有方法注解默認為/。
多個 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"
}
}
|
指定多個模板
像往常一樣綁定到模板參數(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)
}
}
|
路由定義應(yīng)該是 DefaultRouteBuilder 的子類
使用@Inject 注入一個方法與控制器路由到
使用 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 匹配的變量,即使該值不存在也是如此。例如 /foo{/bar} 匹配對 /foo 和 /foo/abc 的請求。非可選變體是 /foo/{bar}。
要禁用路由編譯時驗證,請設(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 客戶端。
更多建議: