Los 10 Errores Más Comunes De Spring Framework
Spring framework, una fuente abierta de Java, es una herramienta popular en la creación de aplicaciones de alto rendimiento usando simples objetos Java, pero como con toda herramienta, el uso inapropiado puede crear problemas. En este artículo, cubrimos los errores más básicos al usar Spring framework, para que así los desarrolladores nuevos y experimentados tengan una guía de lo que deben evitar.
Spring framework, una fuente abierta de Java, es una herramienta popular en la creación de aplicaciones de alto rendimiento usando simples objetos Java, pero como con toda herramienta, el uso inapropiado puede crear problemas. En este artículo, cubrimos los errores más básicos al usar Spring framework, para que así los desarrolladores nuevos y experimentados tengan una guía de lo que deben evitar.
Toni enjoys architecting software solutions and applying his engineering skills to solve interesting real-world problems.
Expertise
PREVIOUSLY AT
Spring es sin duda uno de los frameworks más populares de Java, y también una bestia difícil de domar. Mientras que sus conceptos básicos son muy fáciles de entender, convertirse en un desarrollador fuerte de Spring requiere algo de tiempo y esfuerzo.
En este artículo vamos a cubrir algunos de los errores más comunes en Spring, específicamente orientado hacia aplicaciones web y Spring Boot. Como lo vemos en la página web de Spring Boot, éste toma un punto de vista sobre cómo las aplicaciones de producción ya lista se deberían construir, así que este artículo tratará de imitar esa visión y proporcionar un resumen de algunos consejos que se podrían incorporar en el desarrollo de una aplicación web Sping Boot estándar.
En caso de que no conozcas Spring Boot pero aun así te gustaría probar algunas de las cosas que se mencionan, creé un repositorio GitHub que acompaña este artículo. Si te sientes perdido en algún momento durante este artículo, te recomendaría clonar el repositorio y jugar con el código en tu máquina local.
Error Común #1: Ir A Un Nivel Muy Bajo
Comenzamos con este error común porque el síndrome “no inventado aquí” es muy común en el mundo del desarrollo software. Los síntomas incluyen, re-escribir, regularmente, piezas de código usado comúnmente y muchos desarrolladores parecen sufrir por esto.
Mientras que entender el trabajo interno de una biblioteca y su implementación es en gran parte bueno y necesario (y, también, puede ser un gran proceso de aprendizaje), es perjudicial para tu desarrollo como ingeniero software estar, constantemente lidiando con los mismos detalles de implementación de bajo nivel. Hay un motivo por el cual existen las abstracciones y frameworks como Spring, el cual es precisamente separarte de manuales de trabajo repetitivos y permitir que te concentres en detalles de más alto nivel—tus objetos de dominio y lógica de negocios.
Así que acepta las abstracciones – la próxima vez que te enfrentes con un problema en particular, haz una búsqueda rápida primero y determina si alguna biblioteca que resuelva ese problema ya está integrada a Spring; en estos tiempos, es probable que encuentres una solución adecuada ya existente. Como ejemplo de una biblioteca útil, usaré como ejemplos las anotaciones de Proyecto Lombok en lo que resta de este artículo. Lombok se usa como un generador de código modelo y así, el desarrollador perezoso dentro de ti no debería tener problema alguno al familiarizarse con la biblioteca. Como ejemplo, observa como se ve un “Java Bean estándar” con Lombok:
@Getter
@Setter
@NoArgsConstructor
public class Bean implements Serializable {
int firstBeanProperty;
String secondBeanProperty;
}
Como te puedes imaginar, el código de arriba compila:
public class Bean implements Serializable {
private int firstBeanProperty;
private String secondBeanProperty;
public int getFirstBeanProperty() {
return this.firstBeanProperty;
}
public String getSecondBeanProperty() {
return this.secondBeanProperty;
}
public void setFirstBeanProperty(int firstBeanProperty) {
this.firstBeanProperty = firstBeanProperty;
}
public void setSecondBeanProperty(String secondBeanProperty) {
this.secondBeanProperty = secondBeanProperty;
}
public Bean() {
}
}
Sin embargo, ten en cuenta que es muy probable que tengas que instalar un plugin en caso de que quieras usar Lombok con tu IDE. La versión IntelliJ IDEA del plugin se puede encontrar aquí.
Error Común #2: ‘Filtrar’ Trabajo Interno
Exponer tu estructura interna nunca es buena idea, ya que crea una inflexibilidad en el diseño de servicio y consecuentemente promueve malas prácticas de código. ‘Filtrar’ trabajo interno se manifiesta al hacer accesible la estructura de base de datos desde ciertos puntos de salida API. Como ejemplo, digamos que el siguiente POJO (“Plain Old Java Object”) representa una tabla en tu base de datos:
@Entity
@NoArgsConstructor
@Getter
public class TopTalentEntity {
@Id
@GeneratedValue
private Integer id;
@Column
private String name;
public TopTalentEntity(String name) {
this.name = name;
}
}
Digamos que existe un punto de salida que necesita acceder a la data TopTalentEntity
. Aunque es muy tentador regresar instancias de TopTalentEntity
, una solución más flexible puede ser crear una nueva clase para representar la data TopTalentEntity
en el punto de salida de la API:
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class TopTalentData {
private String name;
}
Así, hacer cambios al back-end de tu base de datos no requerirá ningún cambio adicional en la capa de servicios. Considera lo que sucedería en el caso de que se agregara un campo de ‘contraseña’ a TopTalentEntity
para guardar la función hash de la contraseña de los usuarios en la base de datos – sin algún conector como TopTalentData
, olvidar cambiar el servicio front-end expondría accidentalmente, ¡información secreta no deseable!
Error Común #3: Falta de Separación de Preocupaciones
Mientras tu aplicación crece, la organización de código se convierte rápidamente en algo mucho más importante. Irónicamente, la gran parte de los principios de una buena ingeniería software comienza a romperse en escala – en especial, en casos donde no se ha pensado bien en el diseño de la arquitectura de la aplicación. Uno de los errores más comunes al que los desarrolladores tienden a sucumbir, es maximizar las preocupaciones de código y es ¡extremadamente fácil de hacer!
Lo que usualmente rompe la separación de preocupaciones es solo ‘lanzar’ una nueva funcionalidad a clases ya existentes. Esto es, una excelente solución a corto plazo (para empezar, requiere menos mecanografía) pero inevitablemente se convierte en un problema más adelante, ya sea durante el período de pruebas, mantenimiento o en algún momento entre estos dos. Considera el siguiente controlador, el cual regresa TopTalentData
desde su repositorio:
@RestController
public class TopTalentController {
private final TopTalentRepository topTalentRepository;
@RequestMapping("/toptal/get")
public List<TopTalentData> getTopTalent() {
return topTalentRepository.findAll()
.stream()
.map(this::entityToData)
.collect(Collectors.toList());
}
private TopTalentData entityToData(TopTalentEntity topTalentEntity) {
return new TopTalentData(topTalentEntity.getName());
}
}
En primera instancia, puede parecer que no hay algo particularmente incorrecto con este código; proporciona una lista de TopTalentData
la cual está siendo extraída desde las instancias de TopTalentEntity
. Al mirar más de cerca, sin embargo, podemos ver que hay algunas cosas que están siendo llevadas a cabo por TopTalentController
; principalmente está mapeando peticiones a un punto de salida en particular, extrayendo data de un repositorio y convirtiendo entidades recibidas desde TopTalentRepository
a un formato diferente. Una solución más ‘limpia’ sería separar esas preocupaciones en sus propias clases. Podría verse algo como esto:
@RestController
@RequestMapping("/toptal")
@AllArgsConstructor
public class TopTalentController {
private final TopTalentService topTalentService;
@RequestMapping("/get")
public List<TopTalentData> getTopTalent() {
return topTalentService.getTopTalent();
}
}
@AllArgsConstructor
@Service
public class TopTalentService {
private final TopTalentRepository topTalentRepository;
private final TopTalentEntityConverter topTalentEntityConverter;
public List<TopTalentData> getTopTalent() {
return topTalentRepository.findAll()
.stream()
.map(topTalentEntityConverter::toResponse)
.collect(Collectors.toList());
}
}
@Component
public class TopTalentEntityConverter {
public TopTalentData toResponse(TopTalentEntity topTalentEntity) {
return new TopTalentData(topTalentEntity.getName());
}
}
Una ventaja adicional de esta jerarquía es que nos permite determinar dónde reside la funcionalidad, solo con inspeccionar el nombre de la clase. Además, durante el período de prueba podemos sustituir fácilmente cualquiera de las clases con un simulacro de una implementación si es necesario.
Error Común #4: Manejo Pobre e Inconsistente de Errores
El tema de la inconsistencia no es necesariamente exclusivo de Spring (o Java) pero sigue siendo una faceta importante a considerar cuando se trabaje en proyectos de Spring. Mientras que el estilo de codificación se puede debatir (y es usualmente un tema de acuerdo dentro de un equipo o una compañía entera), tener un estándar común puede ser una gran ayuda de productividad. Esto es muy verdadero, en especial con equipos multi personales; la consistencia permite que el rechazo ocurra sin que se gasten muchos recursos en reconfortar o proporcionar largas explicaciones en cuanto a las responsabilidades de las diferentes clases.
Considera un proyecto Spring con sus diferentes archivos de configuración, servicios y controladores. Al ser semánticamente consistente al darles nombre crea una estructura fácil de investigar, donde todo desarrollador por muy nuevo que sea, puede manejarse alrededor del código; adjuntar sufijos de configuración a las clases de configuración, sufijos de Servicio a tus servicios y sufijos de Controlador a tus controladores, por ejemplo.
Cercanamente relacionado al tema de consistencia, se encuentra el manejo de errores del lado del servidor, el cual merece un énfasis específico. Si en algún momento has tenido que manejar respuestas de excepción de una API mal escrita, probablemente sabes porque – puede ser engorroso analizar excepciones de manera apropiada, y aún más engorroso es determinar la razón por la que esas excepciones ocurrieron inicialmente.
Como desarrollador API, idealmente deberías querer cubrir todos los puntos de salida que enfrentan los usuarios y traducirlos a un formato de error común. Esto, usualmente, significa tener un código de error genérico y una descripción, en vez de la solución pretexto como a) regresar el mensaje “500 Error de Servidor Interno”, o b) dejar que el usuario haga la búsqueda de la solución (lo cual se debe evitar fervientemente, ya que expone tu trabajo interno, aparte de ser esto difícil de manejar por el cliente).
Un ejemplo de un error común de formato de respuesta puede ser:
@Value
public class ErrorResponse {
private Integer errorCode;
private String errorMessage;
}
Algo similar a esto se encuentra comúnmente en las API más populares, y tiende a funcionar muy bien ya que puede ser documentada fácil y sistemáticamente. Se pueden traducir excepciones a este formato al proporcionar la anotación @ExceptionHandler
a un método (un ejemplo de una anotación está en el Error Común #6).
Error Común #5: Lidiar de Manera Incorrecta con el Multihilo
Sin importar si se encuentra en escritorios o aplicaciones web, en Spring o no, el multihilo puede ser difícil de manejar. Los problemas causados por ejecuciones paralelas de programas están cargados de tensión, son elusivos y extremadamente difíciles de depurar – de hecho, dada la naturaleza del problema, una vez que te das cuenta de que estás tratando con un problema de ejecución paralela, probablemente tengas que proceder con el depurador enteramente, e inspeccionar tu código “a mano” hasta que encuentres la causa del error de raíz. Desafortunadamente, una solución regular no existe para resolver tales problemas; dependiendo de tu caso específico, tendrás que estudiar la situación y luego atacar el problema desde el ángulo que creas mejor.
Por supuesto, idealmente querrías evitar multihilar los bugs completamente. De nuevo, un acercamiento estándar no existe, pero aquí hay unas consideraciones prácticas para depurar y prevenir errores de multihilo:
Evita el Estado Global
Primero, siempre recuerda el problema del “estado global”. Si estás creando una aplicación de multihilo, absolutamente todo que sea modificable debería monitorearse muy de cerca y, si es posible, eliminarlo completamente. Si hay algún motivo por el cual la variable global deba mantenerse modificable, usa muy cuidadosamente la sincronización y rastrea el desempeño de tu aplicación para confirmar que no está fallando debido a los nuevos períodos de espera.
Evita la Mutabilidad
Ésta viene desde la programación funcional y, adaptada a OOP, dice que la clase de mutabilidad y cambiar el estado debería evitarse. Esto, en resumen, significa proceder con métodos de fijación y tener campos finales privados en todas tus clases modelo. El único momento en que sus valores se pueden mutar es durante la construcción. De esta manera te puedes asegurar de que no surjan problemas de contención y que las propiedades de objeto que acceden, siempre proveerán los valores correctos.
Registra Data Crucial
Evalúa dónde podría causar problemas tu aplicación y de manera preventiva, registra toda la data crucial. Si ocurre un error, estarás agradecido de tener información que diga cuales peticiones se recibieron y tendrás mejor percepción de porque tu aplicación falló. Es necesario notar, nuevamente, que el registro introduce el archivo adicional I/O y, por ende, no se debería abusar de éste, ya que podría impactar severamente el desempeño de tu aplicación.
Re-Usa Implementaciones Existentes
Cuando necesites generar tus propios hilos (ej. Para hacer peticiones asincrónicas a diferentes servicios), reutiliza implementaciones seguras ya existentes, en vez de crear tus propias soluciones. Esto, mayormente, significa que deberás utilizar ExecutorServices y CompletableFutures de Java 8 con su estilo limpio y funcional para la creación de hilo. Spring también permite procesar peticiones asincrónicas a través de la clase DeferredResult.
Error Común #6: No Emplear Validaciones Basadas en Anotaciones
Imaginemos que nuestro servicio TopTalent, visto anteriormente, requiere un punto de salida para agregar nuevo Talento Top (Top Talent). Más aún, digamos que, por alguna razón muy válida, cada nuevo nombre necesita tener exactamente 10 caracteres. Una manera válida de hacer esto puede ser:
@RequestMapping("/put")
public void addTopTalent(@RequestBody TopTalentData topTalentData) {
boolean nameNonExistentOrHasInvalidLength =
Optional.ofNullable(topTalentData)
.map(TopTalentData::getName)
.map(name -> name.length() == 10)
.orElse(true);
if (nameNonExistentOrInvalidLength) {
// throw some exception
}
topTalentService.addTopTalent(topTalentData);
}
Sin embargo, lo arriba visto (aparte de estar construido pobremente) no es realmente una solución ‘limpia’. Estamos buscando más de un tipo de validez (principalmente, que TopTalentData
no es nulo, y que TopTalentData.name
no es nulo, y que TopTalentData.name
tiene 10 caracteres), al igual que lanzar una excepción si la data es inválida.
Esto se puede ejecutar de forma mucho más limpia al emplear el validador Hibernate con Spring. Primero, debemos re-factorizar el método addTopTalent
para apoyar la validación:
@RequestMapping("/put")
public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) {
topTalentService.addTopTalent(topTalentData);
}
@ExceptionHandler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) {
// handle validation exception
}
Adicionalmente, tendremos que indicar que propiedad queremos validar en la clase TopTalentData
:
public class TopTalentData {
@Length(min = 10, max = 10)
@NotNull
private String name;
}
Ahora Spring interceptará la petición y la validará antes de que el método sea invocado – no hay necesidad de emplear pruebas manuales adicionales.
Otra manera en la que pudimos haber conseguido el mismo efecto es creando nuestras propias anotaciones. Aunque usualmente, solo emplearás anotaciones hechas a la medida cuando tus necesidades excedan el set limitado integrado de Hibernate, para este ejemplo vamos a asumir que @Length no existe. Harías un validador que revisa el largo de la cadena de caracteres al crear dos clases adicionales, una para validación y otra para propiedades de anotación:
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = { MyAnnotationValidator.class })
public @interface MyAnnotation {
String message() default "String length does not match expected";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int value();
}
@Component
public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> {
private int expectedLength;
@Override
public void initialize(MyAnnotation myAnnotation) {
this.expectedLength = myAnnotation.value();
}
@Override
public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
return s == null || s.length() == this.expectedLength;
}
}
Nota que en estos casos, las mejores prácticas de separación de preocupaciones requieren que marques una propiedad como válida si es nula (s == null
dentro del método isValid
), y luego utiliza una anotación @NotNull
si es un requerimiento adicional para la propiedad:
public class TopTalentData {
@MyAnnotation(value = 10)
@NotNull
private String name;
}
Error Común #7: (Todavía) Usar una Configuración Base-XML
Mientras que XML fue necesario en las versiones anteriores de Spring, actualmente la mayor parte de la configuración se puede hacer exclusivamente vía Java código / anotaciones; las configuraciones XML solo son código de texto estándar adicional e innecesario.
Este artículo (al igual que su repositorio acompañante GitHub) usa anotaciones para configurar Spring y éste sabe cuáles beans debería transferir, porque el paquete raíz ha sido anotado con una anotación compuesta @SpringBootApplication
, así:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
La anotación compuesta (puedes aprender más sobre esto en la documentación Spring) simplemente le da a Spring una pista en la que los paquetes deberían ser escaneados para retirar beans. En nuestro caso específico, esto significa que lo siguiente debajo del paquete de arriba (co.kukurin) será usado para transferir:
-
@Component
(TopTalentConverter
,MyAnnotationValidator
) -
@RestController
(TopTalentController
) -
@Repository
(TopTalentRepository
) -
@Service
(TopTalentService
) classes
Si tuviéramos clases de anotación @Configuration
adicionales, serían revisadas por la configuración de base Java.
Error Común #8: Olvidarse de los Perfiles
Un problema común en el desarrollo de un servidor es distinguir entre los distintos tipos de configuración, usualmente tus configuraciones de desarrollo y producción. En vez de reemplazar manualmente varias entradas de configuración cada vez que pasas de probar tu aplicación a implementarla, una manera más útil sería emplear perfiles.
Considera el caso donde estás usando una base de datos de memoria integrada para un desarrollo local, con una base de datos MySQL en producción. Esto significaría, en esencia, que estarías utilizando una URL diferente y (ojalá), credenciales diferentes para acceder a cada una. Veamos cómo esto se puede hacer en dos archivos de configuración distintos:
application.yaml file
# set default profile to 'dev'
spring.profiles.active: dev
# production database details
spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal'
spring.datasource.username: root
spring.datasource.password:
application-dev.yaml file
spring.datasource.url: 'jdbc:h2:mem:'
spring.datasource.platform: h2
Se asume que no querrías accidentalmente ejecutar alguna acción en tu base de datos de producción mientras revisas el código, así que tiene sentido que establezcas el perfil por defecto en desarrollo. En el servidor, puedes invalidar manualmente el perfil de configuración al proporcionar un parámetro -Dspring.profiles.active=prod
en JVM. Alternativamente, puedes establecer tu variable de ambiente OS al perfil por defecto deseado.
Error Común #9: No Ser Capaz de Aceptar la Inyección de Dependencia
Usar adecuadamente la inyección de dependencia con Spring significa permitirle transferir todos tus objetos juntos, al escanear todas las clases de configuración deseadas; esto prueba ser útil al separar relaciones y también hace que las pruebas se desarrollen mucho más fácil. En vez de emparejar de manera ajustada las clases de esta manera:
public class TopTalentController {
private final TopTalentService topTalentService;
public TopTalentController() {
this.topTalentService = new TopTalentService();
}
}
Estamos permitiendo que Spring haga la escritura por nosotros:
public class TopTalentController {
private final TopTalentService topTalentService;
public TopTalentController(TopTalentService topTalentService) {
this.topTalentService = topTalentService;
}
}
Misko Hevery de Google talk explica los ‘porqués’ de la inyección de dependencia a profundidad, así que veamos cómo se usa en práctica. En la sección de separación de preocupaciones (Error Común #3), creamos una clase de servicio y un controlador. Digamos que queremos probar el controlador bajo la suposición que TopTalentService
se comporta correctamente. Podemos insertar un objeto ficticio en lugar de la implementación de servicio actual al proporcionar una clase de configuración separada:
@Configuration
public class SampleUnitTestConfig {
@Bean
public TopTalentService topTalentService() {
TopTalentService topTalentService = Mockito.mock(TopTalentService.class);
Mockito.when(topTalentService.getTopTalent()).thenReturn(
Stream.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList()));
return topTalentService;
}
}
Luego podemos inyectar el objeto ficticio diciéndole a Spring que utilice SampleUnitTestConfig
como su proveedor de configuración:
@ContextConfiguration(classes = { SampleUnitTestConfig.class })
Esto nos permite usar la configuración de contexto para inyectar el bean hecho a la medida a una unidad de prueba.
Error Común #10: Falta de Pruebas, o Pruebas Inapropiadas
Aunque la idea de pruebas de unidad ha estado con nosotros por mucho tiempo, muchos desarrolladores parece que “olvidan” hacer esto (en especial si no es algo que se requiere), o simplemente lo agregan como algo sin importancia. Esto es obviamente algo que no queremos que suceda, ya que las pruebas no sólo deberían verificar lo correcto de tu código, pero también servir como documentación sobre cómo la aplicación debería comportarse en distintas situaciones.
Cuando probamos servicios web, raramente estás haciendo pruebas de unidad exclusivamente ‘puras’, ya que la comunicación en HTTP usualmente requiere que invoques a DispatcherServlet
de Spring y veas lo que sucede cuando un HttpServletRequest
se recibe de verdad (convirtiéndolo en una prueba de integración, lidiar con la validación, publicación de entregas, etc). REST Asegura, un DSL de Java para hacer pruebas fáciles de servicios REST, mejor que MockMVC, ha probado que puede dar una solución muy elegante. Considera el siguiente trozo de un código con inyección de dependencia:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {
Application.class,
SampleUnitTestConfig.class
})
public class RestAssuredTestDemonstration {
@Autowired
private TopTalentController topTalentController;
@Test
public void shouldGetMaryAndJoel() throws Exception {
// given
MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given()
.standaloneSetup(topTalentController);
// when
MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get");
// then
response.then().statusCode(200);
response.then().body("name", hasItems("Mary", "Joel"));
}
}
SampleUnitTestConfig
transfiere una implementación falsa de TopTalentService
a TopTalentController
mientras que todas las otras clases son transferidas usando la configuración estándar, inferida por los paquetes de escaneo arraigados en el paquete de clase de Aplicación. RestAssuredMockMvc
es simplemente usado para establecer un ambiente ligero y envía una petición GET
al punto de salida /toptal/get
.
Convertirse en un Maestro Spring
Spring es un framework de mucho poder, al cual es fácil dar comienzo pero requiere algo de dedicación y tiempo para poder lograr un dominio completo. Tomarse el tiempo para familiarizarte con el framework, mejorará tu productividad a la larga y eventualmente te ayudará a escribir códigos más limpios y convertirte en un mejor desarrollador.
Si buscas más material referente a este tema, Spring In Action es un buen libro, el cual cubre muchos de los temas clave de Spring.
Toni Kukurin
Poreč, Croatia
Member since November 17, 2016
About the author
Toni enjoys architecting software solutions and applying his engineering skills to solve interesting real-world problems.
Expertise
PREVIOUSLY AT