导读:本期聚焦于刘卫东创作的《如何在 Micronaut 中基于 Accept 头自动路由不同响应内容类型》,敬请观看详情。同一个接口返回 JSON 还是 XML,甚至纯文本?在 Micronaut 框架里,这件事可以交给内容协商机制自动完成,不需要在控制器里写一堆 if 判断。本文详细讲解如何利用客户端请求中的 Accept 头,让服务端根据媒体类型自动选择不同的处理方法,涵盖 Produces 注解的用法、多个控制器方法映射同一路径的写法、质量因子 q 值对路由优先级的影响,以及常见的 406 Not Acceptable 排查思路。文中附带完整的代码示例,包括 JSON 与 XML 双格式接口的实现、全局异常处理和测试验证方法,帮助你在实际项目中快速落地多格式 API 输出。

Micronaut 在设计上借鉴了大量 Spring 的经验,同时针对编译期处理做了极致优化,内容协商(Content Negotiation)就是其中一个用起来很舒服的特性。所谓内容协商,指的是客户端在 HTTP 请求头里通过 Accept 声明自己期望的响应格式,服务端根据这个头自动挑选对应的处理方法返回 JSON、XML 或其他媒体类型。整个过程完全由框架路由完成,控制器代码不需要写任何手动判断逻辑。本文将从基础用法讲起,一步步实现基于 Accept 头的多格式响应接口,并分析路由匹配的细节和常见的踩坑点。

如何在 Micronaut 中基于 Accept 头自动路由不同响应内容类型

一、内容协商的基本原理

HTTP 协议本身早就为多格式响应提供了标准方案。当客户端发起请求时,可以在 Accept 头中携带期望的媒体类型,例如 application/json、application/xml 或者 text/plain。服务端拿到这个头之后,会与自己能够产出的格式列表做交集,选出最合适的一种作为响应的 Content-Type。

Micronaut 的实现方式是把媒体类型直接纳入路由匹配条件。也就是说,@Controller 中两个方法可以映射完全相同的 URI 和 HTTP 方法,只要各自通过 @Produces 声明的媒体类型不同,框架在编译期就会生成两条独立的路由记录。运行时根据请求头挑选命中的那一条,匹配失败则返回 406 状态码。

这一点和手动在方法里读 Accept 头再分支处理相比优势明显:方法职责单一,每种格式的序列化逻辑互不干扰,测试的时候也可以针对单一格式编写独立的用例,不必在一次测试里覆盖所有分支。

二、实现 JSON 与 XML 双格式接口

先定义一个简单的数据类,然后写两个方法分别处理 JSON 和 XML 请求。Micronaut 内置了对 JSON 的支持,XML 序列化则需要额外引入 micronaut-jaxb 模块,并用 JAXB 注解标注实体类。

// build.gradle 依赖
// implementation "io.micronaut.xml:micronaut-jaxb"

import io.micronaut.http.annotation.*;
import io.micronaut.http.MediaType;
import jakarta.xml.bind.annotation.*;

@XmlRootElement(name = "user")
@XmlAccessorType(XmlAccessType.FIELD)
public class User {
    @XmlElement(name = "name")
    private String name;

    @XmlElement(name = "email")
    private String email;

    public User() {}

    public User(String name, String email) {
        this.name = name;
        this.email = email;
    }

    public String getName() { return name; }
    public String getEmail() { return email; }
}

@Controller("/api/users")
public class UserController {

    @Get("/{id}")
    @Produces(MediaType.APPLICATION_JSON)
    public User getUserJson(Long id) {
        return new User("张三", "zhangsan@ipipp.com");
    }

    @Get("/{id}")
    @Produces(MediaType.APPLICATION_XML)
    public User getUserXml(Long id) {
        return new User("张三", "zhangsan@ipipp.com");
    }
}

两个方法都映射到 /api/users/{id},唯一区别是 @Produces 的值。当请求头为 Accept: application/json 时走第一个方法,Accept: application/xml 时走第二个方法。如果想支持纯文本,再加一个 @Produces(MediaType.TEXT_PLAIN) 的方法并返回拼好的字符串即可,灵活性非常高。

需要注意,如果客户端不发送 Accept 头,Micronaut 默认会按照 application/json 处理,这也是多数 REST 客户端的默认行为,所以 JSON 方法通常作为兜底存在。

三、质量因子与路由优先级

Accept 头并非只能写一个值,客户端可以一次声明多个可接受的格式,并用 q 值表达偏好程度,例如:

Accept: text/html;q=0.9, application/json;q=1.0, */*;q=0.5

Micronaut 在解析这类头时会读取 q 值并排序,优先匹配权重最高的媒体类型。上例中 application/json 权重为 1.0,服务端会优先尝试 JSON 方法;如果对应方法不存在,再看 text/html;都失败时 */* 会匹配任意声明过的 @Produces。

这里有个容易忽略的细节:当 Accept 头里出现通配符,比如 application/*,框架会在所有以 application/ 开头的方法中挑选一个,具体选哪一个取决于路由注册顺序。为了避免结果不确定,建议服务端每种格式都有明确的 @Produces 声明,客户端尽量使用精确的媒体类型而不是通配符。

四、406 问题排查与测试验证

实践里最常见的报错是 406 Not Acceptable,出现的原因基本只有两类:一是服务端根本没有声明客户端请求的媒体类型,二是 XML 依赖没有引入导致相关路由未注册。排查时可以先确认依赖,再用 curl 逐个验证:

curl -H "Accept: application/json" http://localhost:8080/api/users/1
curl -H "Accept: application/xml"  http://localhost:8080/api/users/1

两条命令分别应返回 JSON 和 XML 内容,响应头中的 Content-Type 也会相应变化。写成自动化测试更稳妥,Micronaut 的嵌入式服务器让整件事非常简单:

@MicronautTest
public class UserControllerTest {

    @Inject
    @Client("/")
    HttpClient client;

    @Test
    void testJsonResponse() {
        HttpRequest<Object> request = HttpRequest.GET("/api/users/1")
                .accept(MediaType.APPLICATION_JSON_TYPE);
        HttpResponse<String> resp = client.toBlocking()
                .exchange(request, String.class);
        assertEquals(HttpStatus.OK, resp.getStatus());
        assertTrue(resp.getBody().get().contains("\"name\""));
    }

    @Test
    void testXmlResponse() {
        HttpRequest<Object> request = HttpRequest.GET("/api/users/1")
                .accept(MediaType.APPLICATION_XML_TYPE);
        HttpResponse<String> resp = client.toBlocking()
                .exchange(request, String.class);
        assertTrue(resp.getBody().get().contains("<user>"));
    }
}

另外提醒一点,如果某些旧客户端只支持 text/html,而你希望给它们返回一段可读的 HTML 页面,同样只需增加一个对应的 @Produces(MediaType.TEXT_HTML) 方法,不需要改动任何现有逻辑。这种按格式拆分方法的写法在接口需要同时服务前端页面、移动端和第三方系统时尤其实用,每种消费方的输出格式独立演进,互不影响,维护成本远低于在一个方法里做字符串拼接分支。

Micronaut内容协商Accept 头修改时间:2026-09-10 06:40:36

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0910/53875.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。