Integrating Microservices With a Monolithic Application
Problem Statement
The technical requirement is to weave a microservice with a non-Spring monolith service layer.
Monolith modules expose SOAP-based web services via WebService facade classes, which are an internally invoking method of different service classes (e.g. StoreService
, ProductService
, PaymentService
, etc.). These web services return essential responses with the help of different utils, application services, and other business services (there is inter-communication between multiple business services). In the roadmap, we have a requirement to convert these legacy services into microservices. The associated necessity is to integrate these microservices with the monolithic backbone without impacting the consumer, and with minimal changes to the monolith’s existing code base.
High-Level Implementation Details
As per the best practice, to break a monolith into a microservice based architecture, first, I identified a candidate piece of functionality (e.g. PaymentServiceImpl:getCustomerPanInfo()
); second, I created a microservice for it. Now it’s time to integrate the microservice with the monolith’s façade. At this point, I have deviated a little from the best practice steps, as we already have a façade for SOAP services (OnlineBusinessService
), which is accessed by different tenants (Web, Mobile, etc.). I have not introduced any separate façade for microservices, and correspondingly, there is no on-the-fly toggler for the incoming traffic. Instead, I have implemented an aspect-based compile time toggler. Once the developer is done developing the microservice for a particular functionality, he/she can annotate the service in the monolith to instruct the system to toggle to the corresponding microservice. Also, there is the option of a fallback method, which can enable the system to fall back into an alternate execution flow if a microservice is not available. The fallback method can be a separate tracking method or can be an existing legacy service.
Implementation Details
To realize the above approach, I have introduced the following components:
com.abccorp.online.business.annotations.MicroServiceWeaver
com.abccorp.online.business.aspect.MicroServiceWeaverAspect
resources
folder and an aspect configuration file aop.xml
Dependency |
Plugin |
|
|
To weave aspects, I need to state aspectjweaver-1.8.13.jar
as a Java agent and weaving mode as compile in VM options:
-javaagent:"<Project path>\lib\aspectjweaver-1.8.13
.jar" -DweavingMode=compile
Currently, I provide an absolute path in the VM option. Going forward, we have to pass this file in a relative manner.
com.abccorp.online.business.annotations.MicroServiceWeaver
A Java annotation with three parameters:
serviceClass
: The full class name, which contains the microservice caller (msMethod
) and fallback (fallbackMethod
) method. It is an optional parameter. By default, it is same as the class where the actual legacy service has been defined.msMethod
: The method to invoke a microservice.fallbackMethod
: The fallback method which will be invoked if a microservice invocation returnsInvocationTargetException
.
com.abccorp.online.business.aspect.MicroServiceWeaverAspect
This is an actual aspect implementation. This class has a single pointcut which is implemented as an around (@Around
) aspect. If any method annotated with the MicroServiceWeaver
annotation instruction pointer first enters into this pointcut and try to invoke the microservice caller method defined as msMethod
and return the corresponding response. If during the microservice invocation, the system receives a InvocationTargetException
, it will invoke fallbackMethod
.
aop.xml
This configuration file declares the aspects and weaver. In this proof of concept, the only aspect is com.abccorp.online.business.aspect.MicroServiceWeaverAspect
, and the below packages are woven by the weaver.
-
com.abccorp.online.business.services..*
-
com.abccorp.online.business.aspect.*
Annotating Monolith Services
I chose the following service methods to test this implementation:
-
com.abccorp.online.business.services.Echo:echo()
-
com.abccorp.online.business.services.StoreService:getStoreImpl()
Why Not Hystrix for the Fallback?
I have found that HystrixCommandAspect
has a bug when invoking a static method, and most of our monolith service class's methods are static. There is a class called AopUtils
inside hystrix-javanica
which extracts the fallbackMethod
method dynamically by invoking the joinPoint.getTarget()
method using a Java reflection. For static methods, this invocation is returning null
, i.e. why I am getting NullPointerException
. To get rid of this issue, we have to use the signature.getDeclaringType()
method to fetch the fallback method name dynamically. It was fixed in issue # 1631 but is not yet available in the repository. I tried with the latest version of hystrix-javanica (i.e. 1.5.13) but the bug is present there.
Note: For non-static service methods, HystrixCommandAspect will work fine to implement the fallback functionality.
All the implementations are in the appendix below.
Appendix
com.abccorp.online.business.annotations.MicroServiceWeaver
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface MicroServiceWeaver {
String serviceClass() default "";
String msMethod() default "";
String fallbackMethod() default "";
}
com.abccorp.online.business.aspect.MicroServiceWeaverAspect
@Aspect
public class MicroServiceWeaverAspect {
private static Logger logger = Logger.getLogger(MicroServiceWeaverAspect.class);
@Pointcut("@annotation(com.abccorp.online.business.annotations.MicroServiceWeaver)")
public void microServiceWeaverPointcut() {
}
@Around("microServiceWeaverPointcut()")
public Object microServiceWeaverAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
Object returnObject = null;
try {
logger.debug("MicroServiceWeaverAspect:microServiceWeaverPointcut - entry into aroundAdvice");
returnObject = invokeMethod(joinPoint);
} catch (Throwable throwable) {
throw throwable;
} finally {
logger.debug("MicroServiceWeaverAspect:microServiceWeaverPointcut - exit from aroundAdvice");
}
return returnObject;
}
private Object invokeMethod(ProceedingJoinPoint joinPoint) throws Throwable {
Object returnObject = null;
Signature signature = joinPoint.getSignature();
Object obj = null;
Method method = null;
if (signature instanceof MethodSignature) {
MethodSignature methodSignature = (MethodSignature) signature;
String methodName = methodSignature.getMethod().getName();
Class<?>[] parameterTypes = methodSignature.getMethod().getParameterTypes();
Annotation[] annotations = methodSignature.getMethod().getAnnotations();
Class<MicroServiceWeaver> microServiceWeaverClass = MicroServiceWeaver.class;
for (Annotation annotation : annotations) {
if (microServiceWeaverClass.isAssignableFrom(annotation.annotationType())) {
Class cls;
if (((MicroServiceWeaver) annotation).serviceClass().isEmpty()){
cls = signature.getDeclaringType();
}else {
cls = Class.forName(((MicroServiceWeaver) annotation).serviceClass());
}
obj = cls.newInstance();
method = cls.getDeclaredMethod(((MicroServiceWeaver) annotation).msMethod(), parameterTypes);
try {
returnObject = method.invoke(obj, joinPoint.getArgs());
}catch (InvocationTargetException ite){
String fallbackMethodName = ((MicroServiceWeaver) annotation).fallbackMethod();
if (fallbackMethodName.contentEquals(methodName)){
returnObject = joinPoint.proceed();
}else {
method = cls.getDeclaredMethod(((MicroServiceWeaver) annotation).fallbackMethod(), parameterTypes);
returnObject = method.invoke(obj, joinPoint.getArgs());
}
}
}
break;
}
}
return returnObject;
}
}
aop.xml
<aspectj>
<weaver options="-verbose -showWeaveInfo -debug">
<include within="com.abccorp.online.business.services..*" />
<include within="com.abccorp.online.business.aspect.*"/>
</weaver>
<aspects>
<aspect name="com.abccorp.online.business.aspect.MicroServiceWeaverAspect"/>
</aspects>
</aspectj>
com.abccorp.online.business.services.Echo:echo()
public EchoResponse echo2(String ping) {
RestTemplate restTemplate = new RestTemplate();
URI uri = URI.create("http://localhost:2222/payment/types");
String payload = restTemplate.getForObject(uri, String.class);
return new EchoResponse("I am echo2:: " + payload);
}
@MicroServiceWeaver(msMethod = "echo2", fallbackMethod = "echo")
public EchoResponse echo(String ping) {
return new EchoResponse("I am echo:: " + ping);
}
com.abccorp.online.business.services.StoreService:getStoreImpl()
public static StoresResponse getStoresImpl2(final StoresRequest request, final Affiliate affiliate, final StoreEntityAccess storeEntityAccess, final PaymentProcessorEntityAccess paymentProcessorEntityAccess, final ProductEntityAccess productEntityAccess) {
RestTemplate restTemplate = new RestTemplate();
URI uri = URI.create("http://localhost:2222/payment/paninfo");
String payload = restTemplate.getForObject(uri, String.class);
log.debug("PAYLOAD:: "+payload);
return new StoresResponse(StoresResponseCode.INVALID_CALL_TYPE_ERROR);
}
public static StoresResponse msFallbackMethod(final StoresRequest request, final Affiliate affiliate, final StoreEntityAccess storeEntityAccess, final PaymentProcessorEntityAccess paymentProcessorEntityAccess, final ProductEntityAccess productEntityAccess){
log.debug("Microservice is not working");
return null;
}
/**
* Retrieve store objects for a given request
*/
@MicroServiceWeaver(msMethod = "getStoresImpl2", fallbackMethod = "msFallbackMethod")
public static StoresResponse getStoresImpl(final StoresRequest request, final Affiliate affiliate, final StoreEntityAccess storeEntityAccess, final PaymentProcessorEntityAccess paymentProcessorEntityAccess, final ProductEntityAccess productEntityAccess) {
if (request == null) return new StoresResponse(StoresResponseCode.MISSING_REQUEST_ERROR);
if (request.getCallType() == null) return new StoresResponse(StoresResponseCode.MISSING_CALL_TYPE_ERROR);
StoresResponse response = new StoresResponse(StoresResponseCode.OK_INFO);
.
.
.