Primera solución: Adaptador de controladores para manejar form-urlencoded
En Spring Boot, al utilizar la anotación @RequestBody con el tipo de contenido 'application/x-www-form-urlencoded;charset=UTF-8', se produce el error Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported. Por ejemplo, el siguiente código fallará:
@RequestMapping(value = "/act/service/model/{modelId}/save", method = RequestMethod.POST)
public void saveModel(@PathVariable String modelId, @RequestBody MultiValueMap<String, String> formData) {
// Lógica de negocio
}
Este código funciona en Spring MVC tradicional debido a la configuración de <mvc:annotation-driven>, que establece convertidores de mensajes específicos. En Spring MVC 3.1, esta configuración incluía:
<!-- Mapeo de solicitudes por anotación -->
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">
<property name="interceptors">
<list>
<ref bean="logInterceptor"/> <!-- Interceptores personalizados -->
</list>
</property>
</bean>
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
<property name="messageConverters">
<list>
<ref bean="byteConverter" />
<ref bean="stringConverter" />
<ref bean="resourceConverter" />
<ref bean="sourceConverter" />
<ref bean="formConverter" />
<ref bean="jaxbConverter" />
<ref bean="jsonConverter" />
</list>
</property>
</bean>
<!-- Definición de convertidores de mensajes -->
En versiones más recientes de Spring Boot, el RequestMappingHandlerAdapter no incluye por defecto el convertidor JSON. Para solucionar esto, podemos configurarlo manualmente:
@EnableWebMvc
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
List<HttpMessageConverter<?>> converters = new ArrayList<>(adapter.getMessageConverters());
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
List<MediaType> supportedMediaTypes = new ArrayList<>();
MediaType plainText = new MediaType(MediaType.TEXT_PLAIN, Charset.forName("UTF-8"));
supportedMediaTypes.add(plainText);
MediaType jsonType = new MediaType(MediaType.APPLICATION_JSON, Charset.forName("UTF-8"));
supportedMediaTypes.add(jsonType);
jsonConverter.setSupportedMediaTypes(supportedMediaTypes);
converters.add(jsonConverter);
adapter.setMessageConverters(converters);
return adapter;
}
}
Con esta configuración, el error desaparece y los parámetros se reciben correctamente.
Segunda solución: Resolver de argumentos personalizado para múltiples tipos de contenido
Crear un resolvedor de argumentos personalizado en Spring Boot para permitir que @RequestBody acepte además de JSON, el tipo de contenido application/x-www-form-urlencoded
Conceptos básicos: En SpringBoot, cuando un método del Controlador tiene un parámetro anotado con @RequestBody, el parámetro es procesado por RequestResponseBodyMethodProcessor. Si el Content-Type es application/x-www-form-urlencoded, se produce un error Unsupported Media Type, lo que exige que el Content-Type debe ser application/json.
Normalmente, si un parámetro del Controlador no tiene la anotación @RequestBody, es procesado por ServletModelAttributeMethodProcessor, que sí soporta application/x-www-form-urlencoded.
Requisito: Implementar que un parámetro con @RequestBody pueda recibir tanto solicitudes con Content-Type application/json como application/x-www-form-urlencoded.
Enfoque de implementación: Crear un resolvedor de argumentos personalizado que, cuando detecte @RequestBody, verifique el Content-Type. Si es application/x-www-form-urlencoded, utilice ServletModelAttributeMethodProcessor; de lo contrario, utilice RequestResponseBodyMethodProcessor.
Implementación:
Resolvedor de argumentos personalizado:
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;
import javax.servlet.http.HttpServletRequest;
/**
* Resolvedor de argumentos personalizado para extender RequestResponseBodyMethodProcessor
*
* Este resolvedor permite que parámetros con @RequestBody acepten:
* 1. Content-Type application/x-www-form-urlencoded (usando ServletModelAttributeMethodProcessor)
* 2. Content-Type application/json (usando RequestResponseBodyMethodProcessor)
*/
public class CustomArgumentResolver implements HandlerMethodArgumentResolver {
private final RequestResponseBodyMethodProcessor jsonProcessor;
private final ServletModelAttributeMethodProcessor formProcessor;
public CustomArgumentResolver(RequestResponseBodyMethodProcessor jsonProcessor,
ServletModelAttributeMethodProcessor formProcessor) {
this.jsonProcessor = jsonProcessor;
this.formProcessor = formProcessor;
}
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(RequestBody.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
final String formUrlEncoded = "application/x-www-form-urlencoded";
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
throw new IllegalStateException("La solicitud HTTP no debe ser nula");
}
String contentType = request.getContentType();
// Si es form-urlencoded, usar el procesador de formularios
if (formUrlEncoded.equals(contentType)) {
return formProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
// De lo contrario, usar el procesador JSON por defecto
return jsonProcessor.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
}
Regsitro del resolvedor de argumentos:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;
import org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor;
import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.List;
/**
* Configuración para registrar el resolvedor de argumentos personalizado
*/
@Configuration
public class ArgumentResolverConfig {
private final RequestMappingHandlerAdapter handlerAdapter;
public ArgumentResolverConfig(RequestMappingHandlerAdapter handlerAdapter) {
this.handlerAdapter = handlerAdapter;
}
@PostConstruct
public void registerCustomResolver() {
List<HandlerMethodArgumentResolver> existingResolvers = handlerAdapter.getArgumentResolvers();
CustomArgumentResolver customResolver = createCustomResolver(existingResolvers);
// Crear nueva lista de resolvedores con nuestro resolvedor personalizado
List<HandlerMethodArgumentResolver> newResolvers = new ArrayList<>(existingResolvers.size() + 1);
newResolvers.add(customResolver);
newResolvers.addAll(existingResolvers);
handlerAdapter.setArgumentResolvers(newResolvers);
}
private CustomArgumentResolver createCustomResolver(List<HandlerMethodArgumentResolver> resolvers) {
RequestResponseBodyMethodProcessor jsonProcessor = null;
ServletModelAttributeMethodProcessor formProcessor = null;
if (resolvers == null) {
throw new IllegalStateException("La lista de resolvedores no debe ser nula");
}
for (HandlerMethodArgumentResolver resolver : resolvers) {
if (jsonProcessor != null && formProcessor != null) {
break;
}
if (resolver instanceof RequestResponseBodyMethodProcessor) {
jsonProcessor = (RequestResponseBodyMethodProcessor) resolver;
continue;
}
if (resolver instanceof ServletModelAttributeMethodProcessor) {
formProcessor = (ServletModelAttributeMethodProcessor) resolver;
}
}
if (jsonProcessor == null || formProcessor == null) {
throw new IllegalStateException("Ambos procesadores (JSON y Form) deben estar disponibles");
}
return new CustomArgumentResolver(jsonProcessor, formProcessor);
}
}