CodeGym /Cursos /Spring REST & MVC /Disciplina final de configuración

Disciplina final de configuración

Spring REST & MVC
Nivel 28, Lección 4
Disponible

1. Disciplina de configuración

En este punto, ya tenemos todos los detalles: los parámetros de ejecución están fuera del código, el application.yml base se ha vuelto legible, app.attachments.* llega al almacenamiento a través de AttachmentProperties, y la sobreescritura local se aplica sobre la configuración base. Ahora queda reunir todo esto en una línea base reproducible del proyecto y verificar rápidamente tres cosas: si hay código fijo, si los límites de multipart entran en conflicto entre sí, y si ha aparecido una segunda fuente de verdad para storage-dir. Así que comencemos.

Cuando un proyecto es pequeño, es muy fácil caer en el mito: «lo importante es que se ejecute, ya limpiaremos la configuración después». Es como decir: «lo importante es que la bombilla se encienda, ya colocaremos el cableado con cuidado después». Técnicamente, la luz funcionará, pero un día toparás con el cable sin querer y descubrirás lo que significa «¿por qué funcionaba ayer?».

Ahora no se trata de volver a debatir qué se debe externalizar al YAML. Esa cuestión ya la hemos cerrado. Aquí la tarea es más práctica: hacer que Task Tracker API se ejecute de la misma manera en todo el equipo y que la configuración no oculte una segunda copia de la lógica. La configuración del framework vive bajo spring.*, la configuración de la aplicación bajo app.*, los parámetros de ejecución se modifican mediante la configuración base, el perfil y las sobreescrituras, mientras que el contrato de la API y las reglas del dominio permanecen en el código.

2. Claves de inicio obligatorias Task Tracker API

Si vemos la configuración como un pequeño contrato entre el «código» y el «entorno», este contrato debe tener un conjunto mínimo obligatorio de puntos. De lo contrario, se da la situación: la aplicación se inicia «de alguna manera», los errores parecen aleatorios y uno termina depurando no la tarea, sino su propio entorno. En un proyecto educativo no necesitamos universalidad, sino predecibilidad, por lo que es mejor tener algunas claves obligatorias, pero mantenerlas en un lugar claro y comprensible.

En nuestro proyecto existe una buena separación natural: todo lo relacionado con Spring MVC y la infraestructura de servlets está bajo spring.*, y todo lo que se refiere a las reglas de inicio específicas de nuestra aplicación está bajo app.*. Esto no es una «regla por la regla», sino una forma de hacer que el archivo sea legible: incluso una persona que abra application.yml por primera vez entenderá rápidamente dónde están los «ajustes de la plataforma» y dónde los «ajustes del proyecto».

A continuación se muestra una tabla con las claves mínimas obligatorias que conviene mantener en la configuración base (no en un perfil) para que Task Tracker API se ejecute igual para todos.

Clave Ejemplo de valor Para qué sirve Nivel
spring.mvc.problemdetails.enabled true Activa Problem Details como formato base de errores (application/problem+json) nivel framework (spring.*)
spring.servlet.multipart.max-file-size 10MB Limita el tamaño de un solo archivo en la subida nivel framework (spring.*)
spring.servlet.multipart.max-request-size 12MB Limita el tamaño total de la solicitud multipart (archivo + metadatos + partes auxiliares) nivel framework (spring.*)
app.attachments.storage-dir ./data/attachments Indica a la capa de almacenamiento dónde guardar el contenido de los archivos nivel aplicación (app.*)

Este conjunto es suficiente para que la línea base de ejecución del proyecto sea explícita y no esté distribuida en la memoria de los desarrolladores.

3. Límites de multipart armonizados

Los límites de multipart son ese caso en el que la configuración puede ser «correcta sintácticamente», pero «incorrecta semánticamente». Y esto es especialmente desagradable porque el error no se manifiesta donde uno espera: la solicitud ni siquiera llega a tu controlador, y tú terminas mirando el registro preguntándote «¿por qué no se llama a mi endpoint? ¡Si estoy seguro de que lo escribí!».

En multipart tenemos al menos dos magnitudes: el tamaño máximo de un solo archivo y el tamaño máximo de toda la solicitud. Si lo simplificamos con una analogía doméstica, max-file-size es el límite del «tamaño de una maleta», y max-request-size es el límite del «peso total del equipaje». Es lógico que el equipaje total no debe ser menor que una maleta, de lo contrario permites una maleta de 20 kg, pero el maletero solo admite 10 kg. Formalmente no has violado nada, pero el viaje terminará en el primer control.

Ejemplo malo, que parece «normal», pero en la práctica genera imprevisibilidad:

spring:
  servlet:
    multipart:
      # Contradicción: un solo archivo puede tener 20MB...
      max-file-size: 20MB
      # ...pero toda la solicitud completa solo 10MB
      max-request-size: 10MB

Ahora te contradices a ti mismo: un solo archivo puede tener 20MB, pero toda la solicitud solo 10MB. Para el usuario, esto se manifestará como «o bien un 413 Payload Too Large, o algún error extraño de carga», y para ti como «¿por qué no funciona si permití 20MB?».

Una buena estrategia base para nuestro proyecto es simple: dado que subimos un solo archivo y un pequeño bloque de metadatos JSON, entonces max-request-size debe ser ligeramente mayor que max-file-size. Por ejemplo, 10MB y 12MB son un par claro. Además, se lee bien a simple vista: se ve que la solicitud incluye no solo el archivo.

spring:
  servlet:
    multipart:
      # Un solo archivo
      max-file-size: 10MB
      # Toda la solicitud (archivo + metadatos)
      max-request-size: 12MB

Otro matiz: es muy fácil poner valores enormes «por si acaso» para que «no estorben». Pero en un proyecto educativo esto es contraproducente: es más importante aprender a controlar los límites que hacer una «subida infinita». Los límites grandes aumentan el riesgo de empezar a almacenar en memoria o registros lo que no se debe almacenar, y en general dificultan la depuración.

4. Valores por defecto explícitos

Los valores por defecto en la configuración son el punto donde los principiantes suelen caer en una trampa: o no ponen ningún valor por defecto («que sea como salga»), o lo ponen de tal manera que el proyecto queda vinculado a una sola máquina («en mi disco D queda todo bonito»). La verdad aquí es simple: el valor base debe ser portable y debe ser visible directamente en la configuración base.

Para app.attachments.storage-dir, ese valor en nuestro proyecto es ./data/attachments. No es un «fallback oculto en una clase Java», sino el valor por defecto explícito del proyecto. Es visible inmediatamente después de abrir application.yml, y por lo tanto cualquier desarrollador entiende a dónde irán los archivos por defecto.

app:
  attachments:
    # Ruta base para el repositorio; las diferencias locales se sobrescriben con el perfil o la variable de entorno
    storage-dir: ./data/attachments

Por eso AttachmentProperties no debe inventar un segundo fallback dentro del código. Si storage-dir es una propiedad crítica, la configuración base la define explícitamente, y @Validated y @NotBlank protegen contra un valor vacío al inicio. De lo contrario, aparecen dos verdades: el YAML dice una cosa, y la clase Java silenciosamente proporciona otra.

5. Eliminamos el código fijo en el almacenamiento

Se considera que la configuración está hecha no cuando has añadido un bonito YAML, sino cuando has eliminado del código las últimas «cadenas mágicas» que contradicen la idea de ajustes externos. Una situación muy típica: añadiste app.attachments.storage-dir, pero en algún lugar del almacenamiento todavía hay un Path.of("/tmp/...") porque «solo estaba probando». Al cabo de una semana ya no recuerdas que era una prueba y empiezas a buscar «por qué los archivos no están ahí».

Después de AttachmentProperties, al almacenamiento le queda una única tarea honesta: tomar un objeto de configuración tipificado y trabajar con él. No debe elegir la ruta por sí mismo, mantener una constante de respaldo ni discutir con el YAML.

package com.example.tasktracker.infrastructure.storage;

import com.example.tasktracker.config.AttachmentProperties;
import java.nio.file.Path;
import org.springframework.stereotype.Service;

@Service
public class LocalAttachmentStorage {
    private final Path root;

    public LocalAttachmentStorage(AttachmentProperties props) {
        // La única fuente de verdad sobre el directorio es el objeto de configuración tipificado
        this.root = Path.of(props.storageDir());
    }
}

Sí, aquí se podría añadir normalización de la ruta (normalize()), conversión a absoluta (toAbsolutePath()), creación del directorio (Files.createDirectories(root)). Pero eso ya son detalles de la implementación del almacenamiento de archivos. En el contexto de la lección de hoy, es importante fijar la disciplina: el almacenamiento no elige la ruta por sí mismo ni la almacena en los fuentes.

6. Validación de la configuración

La validación de la configuración es un «seguro contra el factor humano». Y es especialmente útil allí donde, sin la configuración, el subsistema no puede funcionar físicamente. Para storage-dir es exactamente nuestro caso: una ruta vacía o ausente debe romper el inicio de inmediato, no aparecer en la primera subida como Path.of(null) o un extraño 500.

Este nodo ya lo tenemos cubierto con @Validated y @NotBlank en AttachmentProperties. Para la línea base final, es importante una idea: storage-dir sigue siendo una clave de nivel de aplicación obligatoria, el application.yml base le da un valor explícito, y el fail-fast al inicio evita que la aplicación entre en un estado semí-funcional.

Otro límite útil: no mezcles en tu mente la «validación de la configuración» con la «validación de las solicitudes». Cuando un usuario envía un JSON incorrecto, eso es un 400 Bad Request y nuestro contrato de error. Cuando la aplicación tiene una configuración rota, es un problema de inicio, y el comportamiento correcto suele ser uno solo: la aplicación no debe arrancar.

7. Línea base application.yml

En la etapa final, queremos hacer dos cosas a la vez: mantener el archivo breve y hacerlo lo suficientemente explícito para no depender de los «valores por defecto de Spring Boot que no recuerdo». Esto no es una contradicción si mantenemos la disciplina: fijas exactamente los ajustes que afectan el comportamiento de tu escenario de aplicación, sin intentar «configurar todo Spring».

A continuación se muestra un ejemplo del application.yml base que se adapta bien al estado actual de nuestro proyecto. Define Problem Details, los límites de multipart y storage-dir. Todo lo demás se deja con los valores por defecto, porque o no afecta al escenario o requiere una disciplina aparte (que en este curso deliberadamente no ampliamos).

spring:
  mvc:
    problemdetails:
      # Formato de errores unificado para la API
      enabled: true
  servlet:
    multipart:
      # Un solo archivo a subir
      max-file-size: 10MB
      # Toda la solicitud multipart completa (archivo + metadatos)
      max-request-size: 12MB

app:
  attachments:
    # Directorio base de almacenamiento; el perfil y las variables de entorno pueden sobrescribirlo
    storage-dir: ./data/attachments

En este fragmento, no solo es importante el valor concreto, sino que ahora es fácil «leerlo como un documento». Directamente en el YAML se ve lo que el proyecto espera: errores en Problem Details, subida de archivos de hasta 10MB y almacenamiento de adjuntos en ./data/attachments. Ese es el objetivo de la disciplina: que la configuración deje de ser un «conocimiento secreto del autor del proyecto» y se convierta en una parte explícita del repositorio.

8. Errores típicos en la configuración

Error n.º 1: dejar parte del código fijo «por si acaso».
Muy a menudo, después de implementar @ConfigurationProperties, queda en el código una constante antigua, por ejemplo Path.of("/tmp/task-tracker/uploads"), o un fallback silencioso dentro de AttachmentProperties, y empieza a contradecir al YAML. El resultado es que la configuración existe, el objeto tipificado existe, pero los archivos siguen yendo al lugar antiguo. En cuanto la ruta se ha externalizado a la configuración, no debe quedar una segunda verdad oculta sobre el directorio ni en la implementación del almacenamiento ni en la clase de configuración.

Error n.º 2: hacer que max-request-size sea menor que max-file-size.
Esta configuración es formalmente correcta, pero lógicamente contradictoria. Hace que la subida comience a fallar «antes del controlador», y tú intentas arreglar el lugar equivocado. Una buena regla: si el multipart lleva un solo archivo y una pequeña parte de metadatos, entonces max-request-size debe ser ligeramente mayor que max-file-size, para que no surja la sensación de «hoy se subió, mañana no».

Error n.º 3: externalizar al YAML lo que es parte del contrato de la API.
Trasladar a la configuración restricciones de DTO, reglas de estados, transiciones permitidas, limitaciones en title, es un error de arquitectura. Estas cosas deben vivir en el código, porque describen el contrato de la API y el dominio de la aplicación, no el entorno de ejecución. De lo contrario, terminarás con una situación en la que un desarrollador «ajustó el YAML» y los clientes de repente vieron un comportamiento diferente sin que cambiara la versión de la API.

Error n.º 4: no distinguir entre spring.* y app.* y crear un «vertedero plano de claves».
Cuando las propiedades se mezclan en la raíz del archivo sin estructura, el YAML deja de ser un documento. Una persona lee un «saco de claves» y no entiende qué controla qué. La disciplina aquí es simple: todo lo que pertenece a la plataforma (MVC, multipart) permanece en spring.*, todo lo que pertenece a tu dominio de aplicación (attachments.storage-dir) vive en app.*.

Error n.º 5: no validar las propiedades críticas a nivel de aplicación.
Si storage-dir es obligatorio, la falta de validación significa que la aplicación puede arrancar y luego fallar en el momento de la subida de un archivo. Este es el peor escenario: el problema se descubre tarde, en un lugar impredecible y generalmente con un error menos claro. @Validated y @NotBlank en AttachmentProperties son una forma sencilla de lograr el comportamiento «o arrancamos correctamente, o no arrancamos en absoluto».

1
Tarea
Spring REST & MVC, nivel 28, lección 4
Bloqueada
Baseline m\u00ednimo de configuraci\u00f3n para un escenario de subida
Baseline m\u00ednimo de configuraci\u00f3n para un escenario de subida
1
Tarea
Spring REST & MVC, nivel 28, lección 4
Bloqueada
Upload endpoint con directorio de almacenamiento configurable
Upload endpoint con directorio de almacenamiento configurable
1
Cuestionario/control
Spring Configuración, nivel 28, lección 4
No disponible
Spring Configuración
Configuraciones, perfiles y validación
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION