Exception handling across microservices can be tedious, let’s see how the Java reflection API can help us ease the pain!
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 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
In a monolithic application, handling exceptions is a walk in the park. 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 :
interfaceGitHub{@RequestLine("GET /users/{user}/repos")List<Repo>getUserRepos(@Param("user")Stringuser)throwsUserDoesNotExistException;}
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 :
publicabstractclassServiceExceptionextendsException{privateStringerrorCode;//Constructors omittedpublicStringgetErrorCode(){returnerrorCode;}}
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 :
publicclassThrownServiceExceptionDetails{privateClass<?extendsServiceException>clazz;privateConstructor<?extendsServiceException>emptyConstructor;privateConstructor<?extendsServiceException>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 :
publicclassFeignServiceExceptionErrorDecoderimplementsErrorDecoder{privatestaticfinalLoggerlogger=LoggerFactory.getLogger(FeignServiceExceptionErrorDecoder.class)privateClass<?>apiClass;privateMap<String,ThrownServiceExceptionDetails>exceptionsThrown=newHashMap<>();publicFeignServiceExceptionErrorDecoder(Class<?>apiClass)throwsException{this.apiClass=apiClass;for(Methodmethod: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 :
privatevoidextractServiceExceptionInfo(Class<?>clazz)throwsException{ServiceExceptionthrownException=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();}elseif(parameters.length==1&¶meters[0].isAssignableFrom(String.class)){messageConstructor=constructor;thrownException=(ServiceException)constructor.newInstance(newString());}}if(thrownException!=null){exceptionsThrown.put(thrownException.getErrorCode(),newThrownServiceExceptionDetails().withClazz((Class<?extendsServiceException>)clazz).withEmptyConstructor((Constructor<?extendsServiceException>)emptyConstructor).withMessageConstructor((Constructor<?extendsServiceException>)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 :
privatevoidextractServiceExceptionInfoFromSubClasses(Class<?>clazz)throwsException{Set<Class<?>>subClasses=getAllSubClasses(clazz);for(Class<?>subClass:subClasses){extractServiceExceptionInfo(subClass);}}privateSet<Class<?>>getAllSubClasses(Class<?>clazz)throwsClassNotFoundException{ClassPathScanningCandidateComponentProviderprovider=newClassPathScanningCandidateComponentProvider(false);provider.addIncludeFilter(newAssignableTypeFilter(clazz));Set<BeanDefinition>components=provider.findCandidateComponents("your/base/package/here");Set<Class<?>>subClasses=newHashSet<>();for(BeanDefinitioncomponent:components){subClasses.add(Class.forName(component.getBeanClassName()));}returnsubClasses;}
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 :
@OverridepublicExceptiondecode(StringmethodKey,Responseresponse){privateJacksonDecoderjacksonDecoder=newJacksonDecoder();try{RestExceptionrestException=(RestException)jacksonDecoder.decode(response,RestException.class);if(restException!=null&&exceptionsThrown.containsKey(restException.getErrorCode())){returngetExceptionByReflection(restException);}}catch(IOExceptione){// Fail silently here, irrelevant as a new exception will be thrown anyway}catch(Exceptione){logger.error("Error instantiating the exception to be thrown for the interface '{}'",apiClass.getName(),e);}returndefaultDecode(methodKey,response,restException);//fallback not presented here}privateServiceExceptiongetExceptionByReflection(RestExceptionrestException)throwsException{ServiceExceptionexceptionToBeThrown=null;ThrownServiceExceptionDetailsexceptionDetails=exceptionsThrown.get(restException.getErrorCode());if(exceptionDetails.hasMessageConstructor()){exceptionToBeThrown=exceptionDetails.getMessageConstructor().newInstance(restException.getMessage());}else{exceptionToBeThrown=exceptionDetails.getEmptyConstructor().newInstance();}returnexceptionToBeThrown;}
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!