You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot Webflux如何实现多端点按MediaType动态返回不同响应?

解决方案:Spring Webflux 全局响应格式动态转换

针对你的需求,无需为每个端点编写重复代码,也不需要用ByteBuddy动态生成端点,利用Spring Webflux自身的全局响应拦截或自定义消息转换器即可实现根据Accept头动态返回JSON或格式化HTML。

方案一:使用ServerResponseBodyAdvice全局拦截(推荐)

这是最简单的实现方式,通过全局拦截所有响应,根据请求头动态修改响应内容和类型。

1. 实现全局响应拦截器

创建ControllerAdvice类,实现ServerResponseBodyAdvice接口,拦截响应并转换为HTML格式:

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import reactor.core.publisher.Mono;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;

@ControllerAdvice
public class HtmlResponseAdvice implements ServerResponseBodyAdvice<Object> {

    private final ObjectMapper objectMapper;

    // 注入Spring自动配置的ObjectMapper,开启格式化输出
    public HtmlResponseAdvice(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    @Override
    public boolean supports(MethodParameter returnType, Class<?> converterType) {
        // 对所有端点生效,可根据需求过滤特定控制器/返回类型
        return true;
    }

    @Override
    public Mono<Object> beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                        Class<?> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        // 检查请求Accept头是否包含text/html
        if (request.getHeaders().getAccept().contains(MediaType.TEXT_HTML)) {
            try {
                // 将Java对象序列化为格式化的JSON字符串
                String formattedJson = objectMapper.writeValueAsString(body);
                // 包装为带基础CSS的HTML页面
                String htmlPage = buildStyledHtml(formattedJson);
                // 修改响应Content-Type为text/html
                response.getHeaders().setContentType(MediaType.TEXT_HTML);
                // 返回HTML内容
                return Mono.just(htmlPage);
            } catch (Exception e) {
                // 序列化失败时 fallback 到原始JSON响应
                return Mono.just(body);
            }
        }
        // 非HTML请求直接返回原始响应
        return Mono.just(body);
    }

    // 构建带简易CSS的HTML页面
    private String buildStyledHtml(String formattedJson) {
        return """
               <!DOCTYPE html>
               <html>
               <head>
                   <meta charset="UTF-8">
                   <title>JSON Response</title>
                   <style>
                       body { font-family: 'Consolas', monospace; margin: 2rem; }
                       pre { 
                           background-color: #f8f9fa; 
                           padding: 1.5rem; 
                           border-radius: 6px; 
                           overflow-x: auto;
                           line-height: 1.4;
                       }
                   </style>
               </head>
               <body>
                   <pre>%s</pre>
               </body>
               </html>
               """.formatted(formattedJson);
    }
}

2. 配置内容协商(可选)

在application.yml中配置内容协商规则,确保Spring正确识别text/html类型:

spring:
  webflux:
    content-negotiation:
      media-types:
        html: text/html
        json: application/json

3. 端点无需修改

你的现有端点无需添加额外代码,甚至不需要指定produces属性——Spring会自动根据Accept头选择响应格式。如果需要显式声明支持的类型,也可以添加:

@GetMapping(value = "/api/users", produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_HTML_VALUE})
public Mono<List<User>> getUsers() {
    // 原有业务逻辑
}

方案二:自定义HttpMessageWriter(底层实现)

如果需要更精细的控制,可以自定义消息转换器,直接参与Webflux的响应序列化流程。

1. 实现自定义消息转换器

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import org.springframework.core.ResolvableType;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.codec.HttpMessageWriter;
import reactor.core.publisher.Mono;

import java.util.Collections;
import java.util.List;
import java.util.Map;

public class JsonToHtmlMessageWriter implements HttpMessageWriter<Object> {

    private final ObjectMapper objectMapper;

    public JsonToHtmlMessageWriter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    @Override
    public boolean canWrite(ResolvableType elementType, MediaType mediaType) {
        // 仅处理text/html类型的响应
        return MediaType.TEXT_HTML.isCompatibleWith(mediaType);
    }

    @Override
    public List<MediaType> getWritableMediaTypes() {
        return Collections.singletonList(MediaType.TEXT_HTML);
    }

    @Override
    public Mono<Void> write(Publisher<?> inputStream, ResolvableType elementType, MediaType mediaType,
                            ReactiveHttpOutputMessage message, Map<String, Object> hints) {
        return Mono.from(inputStream)
                .map(body -> {
                    try {
                        String formattedJson = objectMapper.writeValueAsString(body);
                        return buildStyledHtml(formattedJson);
                    } catch (Exception e) {
                        throw new RuntimeException("Failed to serialize to HTML", e);
                    }
                })
                .flatMap(html -> {
                    message.getHeaders().setContentType(MediaType.TEXT_HTML);
                    return message.writeWith(Mono.just(message.bufferFactory().wrap(html.getBytes())));
                });
    }

    private String buildStyledHtml(String formattedJson) {
        // 同方案一的HTML模板
        return """
               <!DOCTYPE html>
               <html>
               <head>
                   <meta charset="UTF-8">
                   <title>JSON Response</title>
                   <style>
                       body { font-family: 'Consolas', monospace; margin: 2rem; }
                       pre { 
                           background-color: #f8f9fa; 
                           padding: 1.5rem; 
                           border-radius: 6px; 
                           overflow-x: auto;
                           line-height: 1.4;
                       }
                   </style>
               </head>
               <body>
                   <pre>%s</pre>
               </body>
               </html>
               """.formatted(formattedJson);
    }
}

2. 注册自定义转换器到Webflux

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.config.WebFluxConfigurer;

import java.util.List;

@Configuration
public class WebfluxConfig implements WebFluxConfigurer {

    private final JsonToHtmlMessageWriter jsonToHtmlMessageWriter;

    public WebfluxConfig(JsonToHtmlMessageWriter jsonToHtmlMessageWriter) {
        this.jsonToHtmlMessageWriter = jsonToHtmlMessageWriter;
    }

    @Override
    public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
        List<HttpMessageWriter<?>> writers = configurer.getWriters();
        // 将自定义转换器放到列表最前面,优先处理text/html请求
        writers.add(0, jsonToHtmlMessageWriter);
    }

    @Bean
    public JsonToHtmlMessageWriter jsonToHtmlMessageWriter(ObjectMapper objectMapper) {
        return new JsonToHtmlMessageWriter(objectMapper);
    }
}

错误原因解释

你之前添加produces = {JSON, HTML}时出现no encoder for with preset Content-Type 'null'错误,是因为Spring Webflux默认没有提供将Java对象直接序列化为text/html的消息转换器,导致无法找到匹配的encoder。上述两种方案分别通过提前转换为String(方案一)或自定义转换器(方案二)解决了这个问题。

方案对比

  • 方案一:代码简洁,侵入性低,适合大多数场景,无需了解Webflux底层序列化流程。
  • 方案二:更灵活,适合需要自定义序列化逻辑、处理复杂响应的场景,但代码量稍大。

不推荐使用ByteBuddy动态创建端点,这种方式会增加系统复杂度和维护成本,Spring原生机制完全可以满足需求。

内容的提问来源于stack exchange,提问作者Oliver

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.04 08:55:22