Spring6项目支持JSONP遇类型转换异常,求解决方案
问题描述
迁移Spring6遗留应用时,因新版Spring移除原生JSONP支持,自行实现了JsonpMappingJacksonValue和AbstractJsonpResponseBodyAdvice类,但运行时抛出类型转换错误:
Caused by: java.lang.ClassCastException: class org.springframework.http.converter.json.MappingJacksonValue cannot be cast to class com.webapp.controller.advice.JsonpMappingJacksonValue (org.springframework.http.converter.json.MappingJacksonValue and com.webapp.controller.advice.JsonpMappingJacksonValue are in unnamed module of loader org.springframework.boot.loader.LaunchedURLClassLoader
自定义类代码如下:
JsonpMappingJacksonValue
package com.webapp.controller.advice; import org.springframework.http.converter.json.MappingJacksonValue; public class JsonpMappingJacksonValue extends MappingJacksonValue { private String jsonpFunction; public JsonpMappingJacksonValue(Object value) { super(value); } public void setJsonpFunction(String functionName) { this.jsonpFunction = functionName; } public String getJsonpFunction() { return this.jsonpFunction; } }
AbstractJsonpResponseBodyAdvice
package com.webapp.controller.advice; import jakarta.servlet.http.HttpServletRequest; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.json.MappingJacksonValue; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.http.server.ServletServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.web.servlet.mvc.method.annotation.AbstractMappingJacksonResponseBodyAdvice; import java.util.regex.Pattern; public class AbstractJsonpResponseBodyAdvice extends AbstractMappingJacksonResponseBodyAdvice { /** * Pattern for validating jsonp callback parameter values. */ private static final Pattern CALLBACK_PARAM_PATTERN = Pattern.compile("[0-9A-Za-z_\\.]*"); private final Log logger = LogFactory.getLog(getClass()); private final String[] jsonpQueryParamNames; protected AbstractJsonpResponseBodyAdvice(String... queryParamNames) { Assert.isTrue(!ObjectUtils.isEmpty(queryParamNames), "At least one query param name is required"); this.jsonpQueryParamNames = queryParamNames; } @Override protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); JsonpMappingJacksonValue jsonpBodyContainer = (JsonpMappingJacksonValue) bodyContainer; for (String name : this.jsonpQueryParamNames) { String value = servletRequest.getParameter(name); if (value != null) { if (!isValidJsonpQueryParam(value)) { if (logger.isDebugEnabled()) { logger.debug("Ignoring invalid jsonp parameter value: " + value); } continue; } MediaType contentTypeToUse = getContentType(contentType, request, response); response.getHeaders().setContentType(contentTypeToUse); jsonpBodyContainer.setJsonpFunction(value); break; } } } /** * Validate the jsonp query parameter value. The default implementation * returns true if it consists of digits, letters, or "_" and ".". * Invalid parameter values are ignored. * @param value the query param value, never {@code null} * @since 4.1.8 */ protected boolean isValidJsonpQueryParam(String value) { return CALLBACK_PARAM_PATTERN.matcher(value).matches(); } /** * Return the content type to set the response to. * This implementation always returns "application.properties/javascript". * @param contentType the content type selected through content negotiation * @param request the current request * @param response the current response * @return the content type to set the response to */ protected MediaType getContentType(MediaType contentType, ServerHttpRequest request, ServerHttpResponse response) { return new MediaType("application.properties", "javascript"); } }
错误原因
AbstractJsonpResponseBodyAdvice的beforeBodyWriteInternal方法中,尝试将Spring框架自动创建的MappingJacksonValue实例强制转换为自定义子类JsonpMappingJacksonValue,但框架生成的实例并非自定义子类对象,导致类型转换失败。
修复方案
方案一:移除自定义JsonpMappingJacksonValue,直接复用框架类
Spring原生的MappingJacksonValue已内置jsonpFunction的get/set方法,无需自定义子类。修改beforeBodyWriteInternal方法:
@Override protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaType contentType, MethodParameter returnType, ServerHttpRequest request, ServerHttpResponse response) { HttpServletRequest servletRequest = ((ServletServerHttpRequest) request).getServletRequest(); for (String name : this.jsonpQueryParamNames) { String value = servletRequest.getParameter(name); if (value != null) { if (!isValidJsonpQueryParam(value)) { if (logger.isDebugEnabled()) { logger.debug("Ignoring invalid jsonp parameter value: " + value); } continue; } MediaType contentTypeToUse = getContentType(contentType, request, response); response.getHeaders().setContentType(contentTypeToUse); // 直接调用框架类的setJsonpFunction方法 bodyContainer.setJsonpFunction(value); break; } } }
同时删除自定义的JsonpMappingJacksonValue类。
方案二:保留自定义类,确保生成子类实例
若需保留自定义JsonpMappingJacksonValue,需重写AbstractMappingJacksonResponseBodyAdvice的wrapBody方法,返回自定义子类实例:
@Override protected MappingJacksonValue wrapBody(Object body, MethodParameter returnType, MediaType contentType) { return new JsonpMappingJacksonValue(body); }
这样beforeBodyWriteInternal接收的bodyContainer就是自定义子类对象,强制转换不再报错。
额外修正:修复Content-Type错误
代码中getContentType方法的application.properties/javascript是错误的,替换为标准的JSONP响应类型:
protected MediaType getContentType(MediaType contentType, ServerHttpRequest request, ServerHttpResponse response) { return MediaType.APPLICATION_JAVASCRIPT; }
注册JSONP处理类
创建具体子类并注册为Spring Bean,指定JSONP查询参数名(如常用的callback):
package com.webapp.controller.advice; import org.springframework.web.bind.annotation.ControllerAdvice; @ControllerAdvice public class JsonpResponseBodyAdvice extends AbstractJsonpResponseBodyAdvice { public JsonpResponseBodyAdvice() { super("callback"); } }
内容的提问来源于stack exchange,提问作者Kristian Martinez

