Pollito's Opinion on Spring Boot Development 5: Configuration of feignClient interfaces
Posted on October 4, 2024 • 6 minutes • 1126 words • Other languages: Español
- Some context
- Roadmap
- 1. Create a new Exception
- 2. Handle the new created Exception
- 3. Create an Error Decoder implementation
- 4. Add the URL value in application.yml
- 5. Create a @ConfigurationProperties class
- 6. Configure the feignClient
- 7. Create a new @Pointcut
- Next lecture
Some context
This is the fifth part of the Spring Boot Development blog series.
- The objective of the series is to be a demonstration of how to consume and create an API following Design by Contract principles .
- To achieve that, we are creating a Java Spring Boot Microservice that handles information about users.
- You can find the code of the final result at this GitHub repo - branch feature/feignClient .
- Here’s a diagram of its components. For a deep explanation visit Understanding the project
So far we created:
- LogFilter.
- GlobalControllerAdvice.
- UsersController.
- UsersApi.
In this blog we are going to create the configuration needed to use the UsersApi. Let’s start!
Roadmap
Because feignClient interfaces are a declarative approach to make REST API calls, a lot of configuration is needed before being able to use them.
Some of these steps could be skipped in favour of a simpler approach. But because this is Pollito’s opinion, things are going to be made as how I consider them correct.
This blog is going to be a long one… Let’s start!
1. Create a new Exception
exception/JsonPlaceholderException.java
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Getter
public class JsonPlaceholderException extends RuntimeException{
private final int status;
}
There’s no need to create fields in the class, it could be empty. But here are some things that can be helpful down the road:
- Status: Useful when handling the exception, and you need to do different logic based on the status of the response.
- An error class: If the service you are calling has a defined error structure (or even multiple), and it’s defined in said service OAS file, then when building, you’ll have a java POJO class representing that error structure. Use them here as private final transient fields.
Here I show an example of an Exception class that has an error field.
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. Handle the new created Exception
What NOT to do
Unless you have business logic that implies you have to do something when the REST API call fails (or another very good reason), always let the Exception propagate.
Don’t do this:
SomeObject foo(){
try{
//business code
Something something = someClient.getSomething();
//more business code and eventually return SomeObject
}catch(Exception e){
return null;
}
}
For more info on why that is bad, I recommend this article on Fast Fail exception handling
What to do
Let the @RestControllerAdvice class take care of the propagated exception.
Once here you have two options:
- If you don’t care at all and is ok for it to be a 500 INTERNAL ERROR, then do nothing, skip to the next step.
- If you do care, handle the Exception.
Let’s go for scenario 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. Create an Error Decoder implementation
This is the simplest an Error Decoder implementation can be:
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());
}
}
You can get as creative as your business logic needs.
Here is an example of a more complex Error Decoder implementation. The error that you get from the REST API call gets mapped into an Error class that is part of an Exception, so it can be used somewhere else (most probably a @RestControllerAdvice class).
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. Add the URL value in application.yml
If by now you haven’t renamed application.properties, rename it to 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
- It is important that the name of the root keys (in this particular example, ‘jsonplaceholder’) is all lowercase.
- If not, later you’ll get the error “Prefix must be in canonical form”.
- Order in this file doesn’t matter. I like to have stuff alphabetically sorted.
5. Create a @ConfigurationProperties class
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 the 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
}
}
Replace the marked values using this image as a guide:
7. Create a new @Pointcut
Let’s log whatever goes in and out of the feignClient interface.
To do that:
- Create a new @Pointcut that matches the feignClient you are interested in.
- Add the Pointcut to the @Before and @AfterReturning methods.
After both steps, the class should look something like this:
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);
}
}
Next lecture
Pollito’s Opinion on Spring Boot Development 6: Business logic