La opinión de Pollito acerca del desarrollo en Spring Boot 5: Configuración de interfaces feignClient
Posted on October 4, 2024 • 6 minutes • 1151 words • Other languages: English
- Un poco de contexto
- Roadmap
- 1. Crear una nueva excepción
- 2. Gestionar la nueva excepción creada
- 3. Cree una implementación de Error Decoder
- 4. Agregue el valor de la URL en application.yml
- 5. Cree una clase @ConfigurationProperties
- 6. Configure el feignClient
- 7. Cree un nuevo @Pointcut
- Siguiente lectura
Un poco de contexto
Esta es la quinta 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 la configuración necesaria para usar el UsersApi. ¡Comencemos!
Roadmap
Debido a que las interfaces feignClient son un enfoque declarativo para realizar llamadas a una API REST, se necesita mucha configuración antes de poder usarlas.
Algunos de estos pasos se podrían obviar en favor de un enfoque más simple, pero como esta es la opinión de Pollito, las cosas se harán como yo las considero correctas.
Este blog va a ser largo… ¡Comencemos!
1. Crear una nueva excepción
exception/JsonPlaceholderException.java
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public class JsonPlaceholderException extends RuntimeException{
private final int status;
}
No es necesario crear campos en la clase, ya que podría estar vacía. Pero aquí hay algunas cosas que pueden ser útiles luego:
- Status: es útil cuando se maneja la excepción y se necesita aplicar una lógica diferente según el estado de la respuesta.
- Una clase de error: si el servicio al que estás llamando tiene una estructura de error definida (o incluso varias), y está definida en el archivo OAS de dicho servicio, entonces, al compilar, tendrás una clase POJO de Java que representa esa estructura de error. Úsalos aquí como campos private final transient.
Aquí muestro un ejemplo de una clase Exception que tiene un campo de error.
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import moe.jikan.models.Error; // <-- Generated by an openapi-generator-maven-plugin execution task
@RequiredArgsConstructor
@Getter
public class JikanException extends RuntimeException {
private final transient Error error;
}
2. Gestionar la nueva excepción creada
Qué NO hacer
A menos que haya una lógica de negocio que implique que se debe hacer algo cuando falla la llamada a la API REST (u otra muy buena razón), siempre permita que la excepción se propague.
No haga esto:
SomeObject foo(){
try{
//business code
Something something = someClient.getSomething();
//more business code and eventually return SomeObject
}catch(Exception e){
return null;
}
}
Para obtener más información sobre por qué esto es malo, recomiendo este artículo sobre Fast Fail exception handling
Qué hacer
Deje que la clase @RestControllerAdvice se encargue de la excepción propagada.
Una vez aquí, tiene dos opciones:
- Si no le importa que sea convierta en un 500 INTERNAL ERROR, no haga nada y pase al siguiente paso.
- Si le importa, gestione la excepción.
Vayamos al escenario 2.
controller/advice/GlobalControllerAdvice.java
import dev.pollito.user_manager_backend.exception.JsonPlaceholderException;
import io.opentelemetry.api.trace.Span;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
@RestControllerAdvice
@Slf4j
public class GlobalControllerAdvice {
@ExceptionHandler(NoResourceFoundException.class)
public ProblemDetail handle(@NotNull NoResourceFoundException e) {
return buildProblemDetail(e, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(JsonPlaceholderException.class)
public ProblemDetail handle(@NotNull JsonPlaceholderException e) {
return buildProblemDetail(
e, e.getStatus() == 400 ? HttpStatus.BAD_REQUEST : HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(Exception.class)
public ProblemDetail handle(@NotNull Exception e) {
return buildProblemDetail(e, HttpStatus.INTERNAL_SERVER_ERROR);
}
@NotNull
private static ProblemDetail buildProblemDetail(@NotNull Exception e, HttpStatus status) {
String exceptionSimpleName = e.getClass().getSimpleName();
log.error("{} being handled", exceptionSimpleName, e);
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(status, e.getLocalizedMessage());
problemDetail.setTitle(exceptionSimpleName);
problemDetail.setProperty("timestamp", DateTimeFormatter.ISO_INSTANT.format(Instant.now()));
problemDetail.setProperty("trace", Span.current().getSpanContext().getTraceId());
return problemDetail;
}
}
3. Cree una implementación de Error Decoder
Esta es la implementación más simple de Error Decoder:
errordecoder/JsonPlaceholderErrorDecoder.java
import dev.pollito.user_manager_backend.exception.JsonPlaceholderException;
import feign.Response;
import feign.codec.ErrorDecoder;
import org.jetbrains.annotations.NotNull;
public class JsonPlaceholderErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String s, @NotNull Response response) {
return new JsonPlaceholderException(response.status());
}
}
Puede ser tan creativo como lo necesite su lógica de negocio.
A continuación, se muestra un ejemplo de una implementación más compleja de Error Decoder. El error que obtiene de la llamada a la API REST se asigna a una clase Error que forma parte de una excepción, por lo que se puede usar en otro lugar (probablemente una clase @RestControllerAdvice).
import com.fasterxml.jackson.databind.ObjectMapper;
import dev.pollito.user_manager_backend.exception.JikanException;
import feign.Response;
import feign.codec.ErrorDecoder;
import java.io.IOException;
import java.io.InputStream;
import moe.jikan.models.Error; // <-- Generated by an openapi-generator-maven-plugin execution task
public class JikanErrorDecoder implements ErrorDecoder {
@Override
public Exception decode(String s, Response response) {
try (InputStream body = response.body().asInputStream()) {
return new JikanException(new ObjectMapper().readValue(body, Error.class));
} catch (IOException e) {
return new Default().decode(s, response);
}
}
}
4. Agregue el valor de la URL en application.yml
Si todavía no ha cambiado el nombre de application.properties, cámbiele el nombre a application.yml.
src/main/resources/application-dev.yml
jsonplaceholder:
baseUrl: https://jsonplaceholder.typicode.com/
spring:
application:
name: user_manager_backend #name of your application here
- Es importante que el nombre de las claves raíz (en este ejemplo en particular, ‘jsonplaceholder’) esté en minúsculas.
- De lo contrario, más adelante recibirá el error “Prefix must be in canonical form”.
- El orden en este archivo no importa. Me gusta tener todo ordenado alfabéticamente.
5. Cree una clase @ConfigurationProperties
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;
}
6. Configure el feignClient
api/config/JsonPlaceholderApiConfig.java
import com.typicode.jsonplaceholder.api.UserApi; //todo: replace here
import dev.pollito.user_manager_backend.config.properties.JsonPlaceholderConfigProperties;
import dev.pollito.user_manager_backend.errordecoder.JsonPlaceholderErrorDecoder;
import feign.Feign;
import feign.Logger;
import feign.gson.GsonDecoder;
import feign.gson.GsonEncoder;
import feign.okhttp.OkHttpClient;
import feign.slf4j.Slf4jLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScans;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScans(
value = {
@ComponentScan(
basePackages = {
"com.typicode.jsonplaceholder.api", //todo: replace here
})
})
@RequiredArgsConstructor
public class JsonPlaceholderApiConfig {
private final JsonPlaceholderConfigProperties jsonPlaceholderConfigProperties;
@Bean
public UserApi userApi(){ //todo: replace here
return Feign.builder()
.client(new OkHttpClient())
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.errorDecoder(new JsonPlaceholderErrorDecoder())
.logger(new Slf4jLogger(UserApi.class)) //todo: replace here
.logLevel(Logger.Level.FULL)
.target(UserApi.class, jsonPlaceholderConfigProperties.getBaseUrl()); //todo: replace here
}
}
Reemplace los valores marcados usando esta imagen como guía:
7. Cree un nuevo @Pointcut
Imprima logs de todo lo que entra y sale de la interfaz feignClient.
Para ello:
- Cree un nuevo @Pointcut que coincida con el feignClient que le interesa.
- Agregue el Pointcut a los métodos @Before y @AfterReturning.
Después de ambos pasos, la clase debería verse así:
aspect/LogAspect.java
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Slf4j
public class LogAspect {
@Pointcut("execution(public * dev.pollito.user_manager_backend.controller..*.*(..))")
public void controllerPublicMethodsPointcut() {}
@Pointcut("execution(public * com.typicode.jsonplaceholder.api.*.*(..))")
public void jsonPlaceholderApiMethodsPointcut() {}
@Before("controllerPublicMethodsPointcut() || jsonPlaceholderApiMethodsPointcut()")
public void logBefore(@NotNull JoinPoint joinPoint) {
log.info(
"[{}] Args: {}",
joinPoint.getSignature().toShortString(),
Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(
pointcut = "controllerPublicMethodsPointcut() || jsonPlaceholderApiMethodsPointcut()",
returning = "result")
public void logAfterReturning(@NotNull JoinPoint joinPoint, Object result) {
log.info("[{}] Response: {}", joinPoint.getSignature().toShortString(), result);
}
}
Siguiente lectura
La opinión de Pollito acerca del desarrollo en Spring Boot 6: Lógica de negocio