Microservices and exception handling in Java with Feign and reflection
Microservices architecture
When it comes to building a complex application in the cloud, microservices architecture is the newest and coolest kid in town. It has numerous advantages over the more traditional monolithic整体 architecture such as :
- modularization模块 and isolation, which makes the development easier in a big team;
- more efficient scaling缩放 of the critical paths of the application;(用项目管理上的说法就是,因为你的模块都独立了,因此任务关键路径全被拆掉了,而关键路径缩短就意味着交付时间的提前)
- possibility to upgrade only a microservice at a time, making the deployments less risky and less prone to unexpected side effects;
- technology independance技术独立 : by exposing an API with a clearly defined contract with a set of common rules shared by all microservices, you don’t have to care which language or database is used by the microservice.
I could go on for a while on this, microservices are a great way to build applications in the cloud. There are lots of awesome OSSOpen-source software (OSS) projects from our friends at Netflix and Spring that will help you doing this, from service discovery to mid-tier load balancing and dynamic configuration, there’s a library for most requirements you’ll have to meet. It’s also great to see Spring coming aboard with Spring Cloud collaborating合作 and integrating整合some of the Netflix librairies into a very useful and simple library to use with your new or existing Spring application!
Caveats警告,附加说明
It wouln’t be fair to avoid talking about the downsides负面 of microservices as they do present some challenges and are not suited to everyone and every application out there. Splitting an application into microservices bring some additional concerns like :
- complex configuration management : 10 microservices? 10 configuration profiles, 10 Logback configurations, etc. (using a centralized configuration server can help you on this though);
- performance hit : you need to validate this token? No problem, just make a POST to this endpoint with the token in the body and you’ll get the response in no time! While this is true for most cases, the network overhead网络开销, serialization/deserialization process can become a bottleneck瓶颈 and you always have to be resilient有弹性的 for network outages中断 or congestion拥挤;
- Interacting with other microservices brings a lot of boilerplate code样板代码 : whereas然而 a single additional method to a class was needed in a monolithic architecture, in a microservices you need a resource implementing an API, a client, some authorization mechanism, exception handling, etc.
Dynamic exception handling using Feign and reflection
It now also supports :
- an optional dependency on Spring to support classpath scanning路径扫描 for abstract exception classes;
- a customizable
Decoder
; - a customizable fallback后退
ErrorDecoder
- better algorithm to instantiate实例化 exceptions (it now supports empty and all combinations组合 of
String
andThrowable
constructors); - injection注入 of the
Throwable
message field via reflection反射; - customization of many of the library’s aspects.
All the details and examples are available in the readme of the project on Github. The rest of the article below is still relevant相关 to show the big picture but bear in mind that it’s not accurate with regards to the published code.
In a monolithic application, handling exceptions is a walk in the park. You try, you catch. However, if something goes wrong during an inter-service call, most of the times you’ll want to propagate扩散 this exception or handle it gracefully优雅的. The problem is, you don’t get an exception from the client, you get an HTTP code and a body describing the error or you may get a generic exception depending on the client used.
For some of our applications at Coveo, we use Feign to build our clients across services. It allows us to easily build clients by just writing an interface with the parameters, the endpoint and the thrown exceptions like this :
interface GitHub {
@RequestLine("GET /users/{user}/repos")
List<Repo> getUserRepos(@Param("user") String user) throws UserDoesNotExistException;
}
When using the client, you are able to easily decode errors using the ErrorDecoder
interface with the received Response
object when the HTTP code is not in the 200 range. Now, we only need a way to map the errors to the proper exception.
Required base exception
Most of our exceptions here at Coveo inherit from a base exception which defines a readable errorCode
that is unique per exception :
public abstract class ServiceException extends Exception
{
private String errorCode;
//Constructors omitted
public String getErrorCode()
{
return errorCode;
}
}
This allows us to translate exceptions on the API into a RestException
object with a consistent一致的 error code and message like this :
{
"errorCode": "INVALID_TOKEN",
"message": "The provided token is invalid or expired."
}
Using the errorCode
as the key, we can use the reflection API of Java to build up a map of thrown exceptions at runtime and rethrow them like there was no inter-service call!
Using reflection to create a dynamic ErrorDecoder
Alright, let’s dive into the code. First, we need a little POJO to hold the information for instantiation :
public class ThrownServiceExceptionDetails
{
private Class<? extends ServiceException> clazz;
private Constructor<? extends ServiceException> emptyConstructor;
private Constructor<? extends ServiceException> messageConstructor;
//getters and setters omitted
}
Then, we use reflection to get the thrown exceptions from the client in the constructor by passing the Feign interface as a parameter :
public class FeignServiceExceptionErrorDecoder implements ErrorDecoder
{
private static final Logger logger = LoggerFactory.getLogger(FeignServiceExceptionErrorDecoder.class)
private Class<?> apiClass;
private Map<String, ThrownServiceExceptionDetails> exceptionsThrown = new HashMap<>();
public FeignServiceExceptionErrorDecoder(Class<?> apiClass) throws Exception
{
this.apiClass = apiClass;
for (Method method : apiClass.getMethods()) {
if (method.getAnnotation(RequestLine.class) != null) {
for (Class<?> clazz : method.getExceptionTypes()) {
if (ServiceException.class.isAssignableFrom(clazz)) {
if (Modifier.isAbstract(clazz.getModifiers())) {
extractServiceExceptionInfoFromSubClasses(clazz);
} else {
extractServiceExceptionInfo(clazz);
}
} else {
logger.info("Exception '{}' declared thrown on interface '{}' doesn't inherit from
ServiceException, it will be skipped.", clazz.getName(), apiClass.getName())
}
}
}
}
}
With the thrown exceptions in hand, knowing that they inherit from ServiceException
, we extract the errorCode
and the relevant constructors. It supports empty constructor and single String
parameter constructor :
private void extractServiceExceptionInfo(Class<?> clazz)
throws Exception
{
ServiceException thrownException = null;
Constructor<?> emptyConstructor = null;
Constructor<?> messageConstructor = null;
for (Constructor<?> constructor : clazz.getConstructors()) {
Class<?>[] parameters = constructor.getParameterTypes();
if (parameters.length == 0) {
emptyConstructor = constructor;
thrownException = (ServiceException) constructor.newInstance();
} else if (parameters.length == 1 && parameters[0].isAssignableFrom(String.class)) {
messageConstructor = constructor;
thrownException = (ServiceException) constructor.newInstance(new String());
}
}
if (thrownException != null) {
exceptionsThrown.put(thrownException.getErrorCode(),
new ThrownServiceExceptionDetails()
.withClazz((Class<? extends ServiceException>) clazz)
.withEmptyConstructor((Constructor<? extends ServiceException>) emptyConstructor)
.withMessageConstructor((Constructor<? extends ServiceException>) messageConstructor));
} else {
logger.warn("Couldn't instantiate the exception '{}' for the interface '{}', it needs an empty or String
only *public* constructor.", clazz.getName(), apiClass.getName());
}
}
Bonus feature, when the scanned exception is abstract, we use the Spring ClassPathScanningCandidateComponentProvider
to get all the subclasses and add them to the map :
private void extractServiceExceptionInfoFromSubClasses(Class<?> clazz)
throws Exception
{
Set<Class<?>> subClasses = getAllSubClasses(clazz);
for (Class<?> subClass : subClasses) {
extractServiceExceptionInfo(subClass);
}
}
private Set<Class<?>> getAllSubClasses(Class<?> clazz) throws ClassNotFoundException
{
ClassPathScanningCandidateComponentProvider provider =
new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(clazz));
Set<BeanDefinition> components = provider.findCandidateComponents("your/base/package/here");
Set<Class<?>> subClasses = new HashSet<>();
for (BeanDefinition component : components) {
subClasses.add(Class.forName(component.getBeanClassName()));
}
return subClasses;
}
Finally, we need to implement Feign ErrorDecoder
. We deserialize反序列化 the body into the RestException
object who holds the message
and the errorCode
used to map to the proper exception :
@Override
public Exception decode(String methodKey,
Response response)
{
private JacksonDecoder jacksonDecoder = new JacksonDecoder();
try {
RestException restException = (RestException) jacksonDecoder.decode(response, RestException.class);
if (restException != null && exceptionsThrown.containsKey(restException.getErrorCode())) {
return getExceptionByReflection(restException);
}
} catch (IOException e) {
// Fail silently here, irrelevant as a new exception will be thrown anyway
} catch (Exception e) {
logger.error("Error instantiating the exception to be thrown for the interface '{}'",
apiClass.getName(), e);
}
return defaultDecode(methodKey, response, restException); //fallback not presented here
}
private ServiceException getExceptionByReflection(RestException restException)
throws Exception
{
ServiceException exceptionToBeThrown = null;
ThrownServiceExceptionDetails exceptionDetails = exceptionsThrown.get(restException.getErrorCode());
if (exceptionDetails.hasMessageConstructor()) {
exceptionToBeThrown = exceptionDetails.getMessageConstructor().newInstance(restException.getMessage());
} else {
exceptionToBeThrown = exceptionDetails.getEmptyConstructor().newInstance();
}
return exceptionToBeThrown;
}
Success!
Now that既然 wasn’t so hard was it? By using this ErrorDecoder
, all the exceptions declared声明 thrown, even the subclasses of abstract base exceptions in our APIs, will get a chance to live by生存 and get thrown on both sides of an inter-service call, with no specific treatment无需特殊处理, just some reflection magic反射魔法!
Hopefully this will come in handy for you, thanks for reading!