1. Término binding: una palabra, mecanismos diferentes
Ya tenemos preparado el punto de extensión de MVC. Pero antes de poner en él converters y formatters, es importante no confundir dos flujos de datos que se ven muy similares: una cadena de una petición HTTP y una cadena de la configuración de la aplicación. Por fuera ambas se parecen a «algún texto», pero por dentro tienen reglas diferentes, tiempos de vida diferentes y lugares de configuración diferentes.
La palabra binding en Spring es como la palabra «llave» en español: la llave de la puerta, la llave musical y la llave en criptografía se escriben igual, pero es mejor no intentar abrir un apartamento con una llave musical (aunque la idea resulta dramática). En Spring, por “binding” se suele entender «la transformación de datos externos en tipos de Java»: cadenas en int, texto en enum, JSON en objeto, YAML en configuración, etc.
El problema es que los datos externos pueden ser muy diferentes. Una petición HTTP llega en tiempo de ejecución, muchas veces, de diferentes personas y a veces con sorpresas. La configuración de la aplicación se lee al inicio (normalmente una sola vez) y controla el comportamiento del servicio como sistema. Ambos procesos «convierten cadenas en tipos», pero viven en capas diferentes, en tiempos diferentes y con reglas de juego diferentes. Si los confundes, obtendrás expectativas extrañas, errores extraños y preguntas muy filosóficas dirigidas a los logs.
Fuentes de cadenas: URL de la petición y application.yaml
Es muy útil separar mentalmente: de dónde exactamente proviene la cadena que Spring está intentando convertir en LocalDate o CourseTrack. Para el web-binding la fuente es casi siempre la misma: la petición HTTP. Los query-parameters (?limit=20&track=SPRING) y las path variables (/courses/spring-boot) llegan físicamente como texto en la URL. Además, hay cabeceras, cookies, cuerpo de la petición, pero hoy nos centraremos en los query-parameters.
Para el config-binding la fuente es diferente: archivos de configuración (application.yaml), variables de entorno, system properties, CLI args. También son textuales por naturaleza, incluso si YAML parece «estructurado». Y aquí la mente del principiante da un salto lógico: «¡Oh! Allí hay cadenas, y aquí también hay cadenas. Entonces la conversión es la misma». Y Spring responde: «Encantado. No».
Para fijarlo, comparemos con un mismo ejemplo: una cadena en la URL y una cadena en YAML.
URL (web):
/api/catalog/courses?limit=20&track=SPRING
YAML (config):
app:
catalog:
max-featured-count: 4 # límite de cursos "featured", política del servicio
En ambos casos aparecen 20, SPRING, 4. Pero en el primer caso es la entrada del usuario/cliente en una petición concreta, y en el segundo son los ajustes de la aplicación como sistema. Y a partir de ahí, estos datos viajan por conductos diferentes.
2. Web-binding en MVC: ?limit=20 se convierte en int
Analicemos el camino del query-parameter sin misterios. El navegador o el cliente envía una petición, en la URL hay cadenas, y Spring MVC debe resolver dos tareas: qué método del controlador invocar y cómo preparar los argumentos de ese método. Para @RequestParam, Spring toma el valor del parámetro como cadena e intenta convertirlo al tipo del parámetro del método: int, boolean, enum, LocalDate, etc. Esto ocurre en cada petición, justo antes de invocar su controlador.
Esquemáticamente (de forma muy simplificada), el web-binding se ve así:
flowchart TD
A["Petición HTTP
...?limit=20"] --> B["Spring MVC
argumentos del método"]
B --> C["ConversionService
cadena -> tipo"]
C --> D["@RequestParam int limit"]
D --> E["Invocación del método del controlador"]
Lo más agradable: si declaras los tipos directamente en la firma del método, el controlador se vuelve corto y honesto. No «analiza la cadena», describe un contrato: «necesito un int limit y un CourseTrack track».
Por ejemplo:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@GetMapping("/api/catalog/courses") // endpoint del catálogo de cursos
public List<CourseCard> findCourses(
@RequestParam(defaultValue = "20") int limit, // MVC convertirá la cadena a int (o devolverá 400 si hay error)
@RequestParam(required = false) CourseTrack track // null si no se pasó el parámetro
) {
// El controlador no se ocupa del análisis ni de la validación "manual" — recibe los tipos
return service.find(limit, track);
}
Aquí el punto clave: limit y track llegan en la petición como cadenas, pero tu método no las ve como cadenas. Ese «trabajo sucio» lo hace MVC.
Y otro detalle que a menudo sorprende: si Spring no puede convertir la cadena al tipo requerido, tu método no se invoca en absoluto. El controlador ni siquiera «llega a empezar» su trabajo. Normalmente esto termina con una respuesta de error estándar (a menudo 400 Bad Request), sin que tengas que capturar NumberFormatException manualmente. No es que «Spring sea malo», es que Spring intenta protegerte del análisis manual en cada método.
3. MVC ConversionService: el traductor entre cadena y tipo
Si imaginamos Spring MVC como un aeropuerto, entonces ConversionService es esa persona en el mostrador que traduce el idioma de «pasajero» (cadenas en la petición) al idioma de «servicio» (tipos de Java). Apenas lo ves en el código del controlador, pero es quien explica por qué se puede escribir @RequestParam int limit y no tener que hacer Integer.parseInt(...) debajo de cada endpoint.
Es importante no confundir: ConversionService no es sobre JSON ni sobre Jackson. Jackson se conecta cuando se trata del cuerpo de la petición/respuesta en JSON (y eso a través de HTTP message converters). Los query-parameters se basan principalmente en conversion/formatting, no en serialización.
Si intentaras hacer todo «manualmente», el controlador se convertiría instantáneamente en un “centro de Strings”, donde la vida consiste en parseInt, trim, toUpperCase y un odio silencioso hacia los usuarios que enviaron limit= 20 con espacios.
Anti-ejemplo (se puede hacer así, pero es triste):
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@GetMapping("/api/catalog/courses")
public List<CourseCard> findCourses(@RequestParam String limit) {
// Obtener manualmente la cadena de la petición casi siempre es una mala idea: se extenderá por los controladores
int value = Integer.parseInt(limit.trim()); // puede fallar, y no será muy bonito
return service.find(value, null); // pierdes los parámetros de dominio mientras intentas «analizar la vida»
}
Cuando ves un código así, casi siempre deberías parar y preguntarte: «¿Realmente quiero que cada uno de mis controladores se ocupe del análisis manual de cadenas?» Normalmente la respuesta es: «No. Quiero vivir».
Y aquí recordamos que en la lección 1 creamos WebConfiguration implements WebMvcConfigurer. Esa es precisamente la capa donde se añaden las reglas de conversión web, si las predeterminadas no son suficientes. Importante: esto afecta a la capa web — las entradas de la petición. No es un «convertidor universal para toda la aplicación».
4. Config-binding en Spring Boot: cadenas en la configuración de la aplicación
Ahora cambiemos a otro modo de pensar. La configuración de la aplicación no llega «cada vez». Se lee al inicio y responde a la pregunta: «¿Cómo será esta aplicación en este entorno?» Por ejemplo: cómo se llama el servicio, qué flags están activados, cuántos elementos mostrar, qué features están activos.
El esquema (también simplificado) se ve así:
flowchart TD
A["application.yaml / env vars / args"] --> B["Spring Boot binder"]
B --> C["Objetos Java de configuración"]
C --> D["Los beans reciben la configuración
a través de DI"]
Y sí, aquí también hay «binding»: los valores de cadena de YAML deben convertirse en int, boolean, enum, Duration, etc. Pero este binding vive en el mundo de la configuración y la construcción inicial de la aplicación, no en el mundo de las peticiones HTTP.
Para ilustrar (solo para ver otro contexto) podemos imaginar un futuro modelo de configuración CatalogProperties:
import org.springframework.boot.context.properties.ConfigurationProperties;
@ConfigurationProperties("app.catalog") // prefijo de las claves en application.yaml
public class CatalogProperties {
// Política de la aplicación como sistema, no un "parámetro de petición"
private int maxFeaturedCount;
public int getMaxFeaturedCount() {
return maxFeaturedCount; // lo que se leyó de YAML/env/args al inicio
}
public void setMaxFeaturedCount(int maxFeaturedCount) {
// Si aquí aparece "banana" en lugar de un número, se detectará al inicio, no en una petición HTTP concreta
this.maxFeaturedCount = maxFeaturedCount;
}
}
Y el YAML correspondiente:
app:
catalog:
max-featured-count: 4
Presta atención a dos diferencias que a menudo pasan desapercibidas.
En primer lugar, en YAML la clave es max-featured-count (kebab-case), mientras que en Java el campo es maxFeaturedCount (camelCase). Para el config-binding esto es algo normal: Spring Boot sabe relacionar estos nombres. En el web-binding no se presupone esa «renombración» automática, porque el web-binding trabaja con los nombres de los parámetros de la petición (?limit=), no con «claves de configuración».
En segundo lugar, si la configuración está rota (por ejemplo, en lugar de un número en YAML aparece banana), se manifestará al inicio. No es una «petición mala», es «la aplicación como sistema no puede ensamblarse». Y el comportamiento aquí es diferente: lo más probable es que obtengas un error de inicio, no una respuesta elegante a una petición HTTP concreta.
5. Comparación: web-binding vs config-binding
Para dejar de confundirse, es útil comparar una vez los mecanismos como dos subsistemas diferentes. Ambos convierten cadenas, pero su objetivo es diferente, y por lo tanto las reglas y los puntos de personalización también difieren.
| Característica | Web-binding (Spring MVC) | Config-binding (Spring Boot) |
|---|---|---|
| Fuente de datos | Petición HTTP (query params, path variables, etc.) | application.yaml, env vars, system props, args |
| Cuándo ocurre | En cada petición, antes de invocar el controlador | Al inicio de la aplicación, al construir el contexto |
| Objetivo principal | Convertir la entrada del cliente en tipos para el manejador de la petición | Configurar el comportamiento de la aplicación sin modificar código |
| Dónde se ve el resultado | En los parámetros del método del controlador | En los objetos de configuración que se inyectan mediante DI |
| Qué ocurre si hay error de conversión | Normalmente la petición no llega al método del controlador, el cliente recibe un error | A menudo el problema se ve al inicio: la aplicación no se ensambla o se ensambla incorrectamente |
| Dónde se añaden reglas de conversión | Puntos de extensión de MVC (WebMvcConfigurer, converters/formatters) | Modelo de configuración y mecanismos del Boot binder (más adelante en el curso) |
La conclusión práctica principal es muy simple: si ahora estás viendo @RequestParam, piensa “MVC”. Si estás viendo YAML y la configuración del servicio, piensa “Boot config”. No intentes arreglar uno con la configuración del otro, como no se intenta arreglar una impresora con la configuración del microondas (aunque en la oficina a veces ocurren cosas así).
6. catalog-service: tipos en el controlador y tipos en la configuración
Ahora aterricemos la teoría en nuestro catalog-service, donde tenemos los tipos de dominio CourseTrack, CourseLevel y filtros en los endpoints del catálogo. En la capa web queremos que el controlador reciba tipos de dominio, no cadenas. Esto hace que la firma sea legible: el método muestra inmediatamente qué filtros soporta el endpoint.
Por ejemplo, de forma aproximada:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.time.LocalDate;
import java.util.List;
@GetMapping("/api/catalog/courses")
public List<CourseCard> findCourses(
@RequestParam(required = false) CourseTrack track, // llegará como cadena desde la URL, MVC lo convertirá a enum/tipo
@RequestParam(required = false) CourseLevel level, // análogo: web-binding, no lectura de YAML
@RequestParam(required = false) LocalDate launchedAfter // el formato de fecha se decide por las reglas de MVC conversion/formatting
) {
// Aquí ya hay tipos, no "texto crudo": al servicio le es más fácil vivir
return service.find(track, level, launchedAfter);
}
Aquí track, level, launchedAfter son web-binding. Se toman de la URL, y Spring MVC intenta convertirlos.
Y ahora imagina la capa de configuración (sin profundizar): por ejemplo, el límite de cursos featured. Esto no es una entrada del usuario, es una política del servicio. Hoy el límite es 4, mañana 6 — y queremos cambiarlo sin modificar código. Ese parámetro tiene sentido que viva en la configuración de la aplicación, no en un query-parameter de cada petición.
Por ejemplo, así (solo como idea de configuración):
app:
catalog:
max-featured-count: 4
Y aquí nace la confusión clásica: un novato mira max-featured-count y piensa que MVC aplicará automáticamente esa regla a @RequestParam int limit. No, son capas diferentes. MVC no lee tus app.catalog.* como «valores predeterminados para la petición». Para la petición, el valor predeterminado se define bien con defaultValue directamente en @RequestParam, bien con la lógica del servicio, bien con una política de dominio separada, pero todo eso no llega «mágicamente desde YAML a cada endpoint».
7. Dónde configurar la conversión
Después de esta lección, conviene poner en la cabeza dos pequeñas señales de tráfico. La primera: WebMvcConfigurer y sus métodos son sobre la capa web. Si añades reglas en addFormatters, influyes en cómo Spring MVC convierte una cadena de la URL en un tipo de Java. Esto es excelente, útil y seguro, pero no hace que tu configuración «de repente sea segura en cuanto a tipos» ni configura cómo Boot lee application.yaml.
La segunda señal: los mecanismos de configuración de Boot son sobre cómo la aplicación se ensambla y vive como sistema. No tienen por qué coincidir con las reglas del web-binding, porque las tareas son diferentes. La configuración es la capacidad de gestionar el servicio; el web-binding es un contrato cómodo y predecible de los endpoints.
Si quieres recordarlo con una sola imagen, puede ser así:
flowchart TD
subgraph R["Runtime: cada petición HTTP"]
Q["URL: ?track=SPRING"] --> MVC["MVC binding
(ConversionService)"]
MVC --> C["Argumentos del método del controlador"]
end
flowchart TD
subgraph S["Startup: inicio de la aplicación"]
Y["YAML / env / args"] --> B["Boot config binding
(binder)"]
B --> P["Properties/Configuración"]
end
En cuanto empiezas a pensar «¿se podría con una sola configuración hacer que tanto las peticiones se analicen como la configuración se vincule de la misma manera?», lo más probable es que estés intentando pegar dos subsistemas diferentes. En Java, eso suele terminar con un código «universal» que es más complejo y menos predecible que dos soluciones sencillas en cada lugar.
8. Errores típicos en web- y config-binding
Antes de seguir adelante con el día, es útil repasar con calma los rastrillos en los que casi todo el mundo pisa. No son «errores vergonzosos»: son una parte normal del crecimiento en Spring, porque la terminología es similar, pero los mecanismos son diferentes. Pero si conoces estas situaciones de antemano, te ahorrarás mucho tiempo y algunas neuronas (que, como se sabe, en el backend no se regeneran — aunque escribiremos suficientes logs).
Error nº 1: pensar que web-binding y config-binding son el mismo mecanismo porque «allí hay cadenas, y aquí también hay cadenas».
Normalmente se manifiesta así: el desarrollador añade configuraciones en application.yaml y espera que @RequestParam empiece a analizarse «con las mismas reglas». En realidad, MVC y Boot binder son subsistemas diferentes, y los cambios en uno no tienen por qué afectar al otro. Se cura muy simple: pregúntate siempre «¿este valor ha llegado desde HTTP o desde la configuración de la aplicación?».
Error nº 2: dejar los parámetros del controlador como cadenas y analizarlos manualmente en cada método.
Ese código se extiende rápidamente: trim, parseInt, toUpperCase empiezan a vivir en el controlador, y el controlador se convierte en una mezcla de «contrato API» y «biblioteca de análisis de cadenas». Spring MVC sabe convertir los tipos básicos por sí mismo, y para los formatos de dominio hay reglas centralizadas. Es mejor que el controlador siga siendo un límite fino, no una fábrica de NumberFormatException.
Error nº 3: esperar que defaultValue en @RequestParam sea lo mismo que el «valor predeterminado en la configuración».
defaultValue es una regla para un endpoint concreto y un parámetro de petición concreto: si el parámetro no llega, MVC sustituirá la cadena e intentará convertirla al tipo. El valor predeterminado de configuración es una regla de comportamiento del servicio como sistema. Son niveles diferentes. Si los mezclas, obtendrás una API donde parte de los valores predeterminados están «repartidos» por los controladores y otra parte vive en la configuración, siendo muy difícil entender qué es realmente la política de la aplicación.
Error nº 4: intentar «arreglar» un comportamiento extraño del binding activando @EnableWebMvc.
Esto es como tratar un ruido extraño en el coche quitando las puertas: a veces el ruido realmente desaparece, pero ahora tienes otro problema. @EnableWebMvc cambia MVC a un modo más manual y rompe fácilmente los útiles valores predeterminados de Boot. Si tu objetivo es ajustar puntualmente la conversión de los valores de entrada, lo correcto es mantenerte en WebMvcConfigurer y pequeñas adiciones sobre la auto-configuración.
Error nº 5: pensar que si el tipo es el mismo (CourseTrack), entonces las reglas de entrada deberían ser «universales».
CourseTrack puede llegar desde una petición (donde los usuarios suelen escribir java-backend) y desde la configuración (donde normalmente se escriben valores cuidados como JAVA_BACKEND). Estas fuentes se comportan de manera diferente, y tienes derecho a procesarlas de manera diferente. La unificación «a cualquier precio» a menudo empeora las cosas: complicas el sistema por la belleza, no por un comportamiento comprensible.
GO TO FULL VERSION