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
相关产品推荐
相关产品推荐

