La opinión de Pollito acerca del desarrollo en Spring Boot 6: Lógica de negocio
Posted on October 15, 2024 • 8 minutes • 1585 words • Other languages: English
- Un poco de contexto
- 1. Crear un @Mapper
- 2. Crear un caché
- 3. Crear UsersService e implementarlo
- 4. Llamar los métodos en UsersController
- 4. Ejecutar la aplicación y ver los resultados
- Siguiente lectura
Un poco de contexto
Esta es la sexta parte de la serie de blogs Spring Boot Development .
- El objetivo de esta serie es ser una demostración de cómo consumir y crear una API siguiendo los principios del Desarrollo impulsado por contratos .
- Para lograrlo, estamos creando un microservicio Java Spring Boot que maneje información sobre los usuarios.
- Puedes encontrar el resultado final de la serie en el repo de GitHub - branch feature/feignClient .
- A continuación se muestra un diagrama de componentes. Para una explicación más detallada, visite Entendiendo el proyecto
De momento hemos creado:
- LogFilter.
- GlobalControllerAdvice.
- UsersController.
- UsersApi.
En este blog vamos a crear UsersService y UsersApiCacheService. ¡Comencemos!
1. Crear un @Mapper
Los mappers son una situación de “elige tu propia aventura” . El que yo uso es MapStruct .
Cree una interfaz @Mapper que reciba una lista de usuarios de jsonplaceholder y devuelva una lista de nuestros propios usuarios de microservicio.
import dev.pollito.post.model.User;
import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.MappingConstants;
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface UserMapper {
List<User> map(List<com.typicode.jsonplaceholder.model.User> users);
}
Mantenga la capa de integración de API separada de la capa del controlador
Si compruebas lo que está haciendo el mapper, está mapeando esto:
[
{
"address": {
"city": "Gwenborough",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
},
"street": "Kulas Light",
"suite": "Apt. 556",
"zipcode": "92998-3874"
},
"company": {
"bs": "harness real-time e-markets",
"catchPhrase": "Multi-layered client-server neural-net",
"name": "Romaguera-Crona"
},
"email": "Sincere@april.biz",
"id": 1,
"name": "Leanne Graham",
"phone": "1-770-736-8031 x56442",
"username": "Bret",
"website": "hildegard.org"
}
]
A esto:
[
{
"address": {
"city": "Gwenborough",
"geo": {
"lat": "-37.3159",
"lng": "81.1496"
},
"street": "Kulas Light",
"suite": "Apt. 556",
"zipcode": "92998-3874"
},
"company": {
"bs": "harness real-time e-markets",
"catchPhrase": "Multi-layered client-server neural-net",
"name": "Romaguera-Crona"
},
"email": "Sincere@april.biz",
"id": 1,
"name": "Leanne Graham",
"phone": "1-770-736-8031 x56442",
"username": "Bret",
"website": "hildegard.org"
}
]
No, no fue un error, no escribí lo mismo dos veces.
Quizás te preguntes… ¿Por qué? ¿Por qué realizar el proceso de mapeo en lugar de simplemente devolver la respuesta DTO de feignClient?
-
Incluso si quisieras, no puedes: debido a la forma en que este proyecto depende del desarrollo impulsado por contratos con el uso de openapi-generator-maven-plugin .
- Aunque sabemos que tanto el DTO de respuesta feignClient como el DTO de retorno @RestController tienen la misma estructura interna, desde el punto de vista del proyecto, esos dos son objetos diferentes que no tienen nada en común.
-
De todos modos, sería una mala práctica: imaginemos que no utilizas el plugin y, en su lugar, escribes tus propios DTO a mano. Aquí tienes una lista de motivos por los que utilizar la misma clase para mapear la respuesta de feignClient y el retorno de @RestController es una mala idea:
- Si utiliza el mismo DTO para ambos, cualquier cambio en la API externa (campos nuevos, campos obsoletos, etc.) podría afectar innecesariamente su código interno.
- El uso de un DTO específico de @RestController le permite filtrar y adaptar la respuesta para exponer solo lo que realmente se necesita.
- Esto evita la filtración de información confidencial o irrelevante y ayuda a reducir el tamaño de la carga útil, lo que mejora el rendimiento.
- Los DTO de API externas a menudo necesitan mapear formatos o estructuras de datos que no son directamente útiles para su aplicación.
- Por ejemplo, una API de terceros puede devolver fechas como cadenas, pero internamente es posible que desee trabajar con LocalDate.
Al tener DTO independientes, el código se vuelve más fácil de probar y mantener. Los cambios en los servicios externos no afectarán directamente la funcionalidad principal de su aplicación.
2. Crear un caché
Esto es opcional, pero se recomienda para nuestro caso específico. Estamos consumiendo una API cuya respuesta nunca cambia (o si lo hace, no nos importa). Entonces, ¿por qué no almacenar en caché la respuesta en lugar de preguntar lo mismo una y otra vez?
Ten en cuenta que el almacenamiento en caché puede generar respuestas obsoletas. En el mundo real, eso puede convertirse en un efecto secundario no deseado.
2.1. Agregar dependencias
Estas son:
- Spring Boot Starter Cache : Starter for using Spring Framework’s caching support.
- Caffeine Cache : A high performance caching library.
Aquí te dejo un copy-paste listo para usar. Considera revisar la última versión.
Dento del tag <dependencies>:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.8</version>
</dependency>
2.2 Agregar tiempo de expiración en application.yml
Bajo jsonplaceholder, cree una nueva propiedad “expiresAfter”.
application.yml
jsonplaceholder:
baseUrl: https://jsonplaceholder.typicode.com/
expiresAfter: 24 #hours
spring:
application:
name: user_manager_backend
No olvides agregarlo a la clase @ConfigurationProperties para que puedas tener acceso.
config/properties/JsonPlaceholderConfigProperties.java
import lombok.AccessLevel;
import lombok.Data;
import lombok.experimental.FieldDefaults;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
@Configuration
@ConfigurationProperties(prefix = "jsonplaceholder")
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class JsonPlaceholderConfigProperties {
String baseUrl;
Integer expiresAfter;
}
2.3. Crear una configuración de caché
config/CacheConfig.java
import com.github.benmanes.caffeine.cache.Caffeine;
import dev.pollito.user_manager_backend.config.properties.JsonPlaceholderConfigProperties;
import java.util.concurrent.TimeUnit;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
@RequiredArgsConstructor
public class CacheConfig {
private final JsonPlaceholderConfigProperties jsonPlaceholderConfigProperties;
public static final String JSON_PLACEHOLDER_CACHE = "JSON_PLACEHOLDER_CACHE";
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(JSON_PLACEHOLDER_CACHE);
caffeineCacheManager.setCaffeine(
Caffeine.newBuilder()
.expireAfterWrite(jsonPlaceholderConfigProperties.getExpiresAfter(), TimeUnit.HOURS));
return caffeineCacheManager;
}
}
2.4. Crear UsersApiCacheService e implementarlo
service/UsersApiCacheService.java
import com.typicode.jsonplaceholder.model.User;
import java.util.List;
public interface UsersApiCacheService {
List<User> getUsers();
}
service/impl/UsersApiCacheServiceImpl.java
import static dev.pollito.user_manager_backend.config.CacheConfig.JSON_PLACEHOLDER_CACHE;
import com.typicode.jsonplaceholder.api.UserApi;
import com.typicode.jsonplaceholder.model.User;
import dev.pollito.user_manager_backend.service.UsersApiCacheService;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UsersApiCacheServiceImpl implements UsersApiCacheService {
private final UserApi userApi;
@Override
@Cacheable(value = JSON_PLACEHOLDER_CACHE)
public List<User> getUsers() {
return userApi.getUsers();
}
}
3. Crear UsersService e implementarlo
service/UsersService.java
import dev.pollito.user_manager_backend.model.SortDirection;
import dev.pollito.user_manager_backend.model.User;
import dev.pollito.user_manager_backend.model.UserSortProperty;
import dev.pollito.user_manager_backend.model.Users;
public interface UsersService {
User findById(Long id);
Users findAll(
Integer pageNumber,
Integer pageSize,
UserSortProperty sortProperty,
SortDirection sortDirection,
String q);
}
service/impl/UsersServiceImpl.java
import dev.pollito.user_manager_backend.mapper.UserModelMapper;
import dev.pollito.user_manager_backend.model.Pageable;
import dev.pollito.user_manager_backend.model.SortDirection;
import dev.pollito.user_manager_backend.model.User;
import dev.pollito.user_manager_backend.model.UserSortProperty;
import dev.pollito.user_manager_backend.model.Users;
import dev.pollito.user_manager_backend.service.UsersApiCacheService;
import dev.pollito.user_manager_backend.service.UsersService;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UsersServiceImpl implements UsersService {
private final UsersApiCacheService userApi;
private final UserModelMapper userModelMapper;
@Override
public Users findAll(
Integer pageNumber,
Integer pageSize,
UserSortProperty sortProperty,
SortDirection sortDirection,
String q) {
List<User> users = getUsersFromApi();
users = filterUsers(q, users);
users = sortUsers(users, sortProperty, sortDirection);
return new Users()
.content(usersSubList(users, pageNumber, pageSize))
.pageable(new Pageable().pageNumber(pageNumber).pageSize(pageSize))
.totalElements(users.size());
}
@Override
public User findById(Long id) {
return getUsersFromApi().stream()
.filter(user -> user.getId().equals(id))
.findFirst()
.orElseThrow(NoSuchElementException::new);
}
private List<User> getUsersFromApi() {
return userModelMapper.map(userApi.getUsers());
}
private static List<User> filterUsers(String q, List<User> users) {
if (Objects.nonNull(q) && !q.isEmpty()) {
users = users.stream().filter(user -> filterUsersPredicate(q, user)).toList();
}
return users;
}
private static boolean filterUsersPredicate(@NotNull String q, @NotNull User user) {
String query = q.toLowerCase();
return (Objects.nonNull(user.getEmail()) && user.getEmail().toLowerCase().contains(query))
|| (Objects.nonNull(user.getName()) && user.getName().toLowerCase().contains(query))
|| (Objects.nonNull(user.getUsername())
&& user.getUsername().toLowerCase().contains(query));
}
private static List<User> sortUsers(
List<User> users, @NotNull UserSortProperty sortProperty, SortDirection sortDirection) {
Comparator<User> comparator =
switch (sortProperty) {
case EMAIL -> Comparator.comparing(User::getEmail);
case ID -> Comparator.comparing(User::getId);
case NAME -> Comparator.comparing(User::getName);
case USERNAME -> Comparator.comparing(User::getUsername);
};
if (SortDirection.DESC.equals(sortDirection)) {
comparator = comparator.reversed();
}
return users.stream().sorted(comparator).toList();
}
private static @NotNull List<User> usersSubList(
@NotNull List<User> users, Integer pageNumber, Integer pageSize) {
int total = users.size();
int fromIndex = Math.min(pageNumber * pageSize, total);
int toIndex = Math.min(fromIndex + pageSize, total);
return users.subList(fromIndex, toIndex);
}
}
¿Por qué paginación?
- Preparación para el futuro: Si bien el conjunto de datos actual puede ser pequeño (UsersApi solo devuelve 10 usuarios), podría crecer con el tiempo. Si la paginación no está integrada desde el principio, actualizarla más adelante puede ser complejo y requerir versiones o cambios importantes.
- Flexibilidad para clientes: Diferentes clientes pueden preferir consumir cantidades más pequeñas de datos, incluso para conjuntos de datos pequeños.
- Optimización del rendimiento: Incluso con conjuntos de datos pequeños, algunas operaciones (por ejemplo, clasificación, filtrado) pueden agregar gastos generales. La paginación permite que el servidor y los clientes acuerden fragmentos de datos, lo que puede ayudar a mantener el rendimiento bajo diferentes cargas de trabajo.
- Seguridad y estabilidad: La paginación puede ayudar a mitigar los riesgos de denegación de servicio. Incluso para conjuntos de datos pequeños, limitar las respuestas evita una recuperación excesiva de datos accidental (o maliciosa).
4. Llamar los métodos en UsersController
Llame al método @Service en la clase @RestController.
controller/UsersController.java
import dev.pollito.user_manager_backend.api.UsersApi;
import dev.pollito.user_manager_backend.model.SortDirection;
import dev.pollito.user_manager_backend.model.User;
import dev.pollito.user_manager_backend.model.UserSortProperty;
import dev.pollito.user_manager_backend.model.Users;
import dev.pollito.user_manager_backend.service.UsersService;
import lombok.RequiredArgsConstructor;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class UsersController implements UsersApi {
private final UsersService usersService;
@Override
public ResponseEntity<Users> findAll(
Integer pageNumber,
Integer pageSize,
@NotNull UserSortProperty sortProperty,
@NotNull SortDirection sortDirection,
String q) {
return ResponseEntity.ok(
usersService.findAll(pageNumber, pageSize, sortProperty, sortDirection, q));
}
@Override
public ResponseEntity<User> findById(Long id) {
return ResponseEntity.ok(usersService.findById(id));
}
}
Quiero que admires cómo la llamada a UsersService es solo una línea cada una.
- No lógica de logs.
- No try catch.
- No
if(Objects.isNull())
.
Solo la línea de retorno… Es hermoso de ver, es casi arte. Para cosas como estas me gusta programar.
4. Ejecutar la aplicación y ver los resultados
Realice un Maven clean and compile, y ejecute la clase de aplicación principal. Luego haga una solicitud GET a localhost:8080/users .
Repita la solicitud. La memoria caché entrará en acción y debería encontrar un tiempo de respuesta mucho más rápido: pasó de 1014 ms a 13 ms, un aumento de velocidad del 98,7%.
Siguiente lectura
La opinión de Pollito acerca del desarrollo en Spring Boot 7: Unit tests