上一节中使用的 @Controller 注释是允许您控制 HTTP 路由构造的几个注释之一。
URI 路径
@Controller 注释的值是一个 RFC-6570 URI 模板,因此您可以使用 URI 模板规范定义的语法在路径中嵌入 URI 变量。
许多其他框架,包括 Spring,都实现了 URI 模板规范
实际实现由扩展 UriTemplate 的 UriMatchTemplate 类处理。
您可以在应用程序中使用此类来构建 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 构建路径以包含在您的响应中。
URI 路径变量
URI 变量可以通过方法参数引用。例如:
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 中
方法参数可以选择用 PathVariable 注释
URI变量的值在实现中被引用
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)
}
}
}
|
嵌入式服务器和 HTTP 客户端启动
测试完成后清理服务器和客户端
测试向 URI /issues/12 发送请求
然后断言响应是“Issue #12”
另一个测试断言当在 URL 中发送无效数字时返回 400 响应
另一个测试断言,当 URL 中未提供数字时,将返回 404 响应。要执行的路由需要存在的变量。
请注意,前面示例中的 URI 模板要求指定数字变量。您可以使用以下语法指定可选的 URI 模板:/issues{/number} 并使用 @Nullable 注释数字参数。
下表提供了 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}
|
可选查询参数
|
/books?max=10&offset=10
|
/books{/path:.*}{.ext}
|
正则表达式路径与扩展匹配
|
/books/foo/bar.xml
|
URI 保留字符匹配
默认情况下,RFC-6570 URI 模板规范定义的 URI 变量不能包含保留字符,例如 /、? 等等。
如果您希望匹配或扩展整个路径,这可能会有问题。根据规范的第 3.2.3 节,您可以使用 + 运算符使用保留扩展或匹配。
例如,URI /books/{+path} 与 /books/foo 和 /books/foo/bar 都匹配,因为 + 指示变量路径应包含保留字符(在本例中为 /)。
路由注释
前面的示例使用 @Get 注释添加了一个接受 HTTP GET 请求的方法。下表总结了可用的注释以及它们如何映射到 HTTP 方法:
所有方法注解默认为/。
多个 URI
每个路由注释都支持多个 URI 模板。对于每个模板,都会创建一条路线。此功能非常有用,例如更改 API 的路径并保留现有路径以实现向后兼容性。例如:
多个 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"
}
}
|
指定多个模板
像往常一样绑定到模板参数
多个模板的路由验证更加复杂。如果一个通常需要的变量在所有模板中都不存在,则该变量被认为是可选的,因为它可能不会在每次执行该方法时都存在。
以编程方式构建路线
如果您不喜欢使用注解而是在代码中声明所有路由,那么不要担心,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)
}
}
|
路由定义应该是 DefaultRouteBuilder 的子类
使用@Inject 注入一个方法与控制器路由到
使用 RouteBuilder::GET(String,Class,String,Class… ) 等方法路由到控制器方法。请注意,即使使用问题控制器,路由也不知道其@Controller 注释,因此必须指定完整路径。
不幸的是,由于类型擦除,Java 方法 lambda 引用不能与 API 一起使用。对于 Groovy,有一个可以子类化的 GroovyRouteBuilder 类,它允许传递 Groovy 方法引用。
路由编译时验证
Micronaut 支持在编译时使用验证库验证路由参数。首先,将验证依赖项添加到您的构建中:
build.gradle
annotationProcessor "io.micronaut:micronaut-validation" // Java only
kapt "io.micronaut:micronaut-validation" // Kotlin only
implementation "io.micronaut:micronaut-validation"
通过对类路径的正确依赖,路由参数将在编译时自动检查。如果满足以下任一条件,编译将失败:
可选变量是允许路由与 URI 匹配的变量,即使该值不存在也是如此。例如 /foo{/bar} 匹配对 /foo 和 /foo/abc 的请求。非可选变体是 /foo/{bar}。
要禁用路由编译时验证,请设置系统属性 -Dmicronaut.route.validation=false。对于使用 Gradle 的 Java 和 Kotlin 用户,可以通过从 annotationProcessor/kapt 范围中移除验证依赖来实现相同的效果。
路由非标准 HTTP 方法
@CustomHttpMethod 注释支持客户端或服务器的非标准 HTTP 方法。 RFC-4918 Webdav 等规范需要额外的方法,例如 REPORT 或 LOCK。
路由示例
@CustomHttpMethod(method = "LOCK", value = "/{name}")
String lock(String name)
注释可以在任何可以使用标准方法注释的地方使用,包括控制器和声明性 HTTP 客户端。
更多建议: