Public search “code road mark”, point of concern not lost!

background

Recently, the company’s project needs to be transformed, and the project architecture and technology selection are redesigned. The original intention is to transform the back-end service only under the condition that the front-end call is not affected, and then realize the smooth migration of the front-end flow from 0-100 through the “middleware + gray strategy”. Therefore, the new application retains all interface and access parameter definitions of the old application to ensure interface compatibility for migration.

However, at the end of the development work, it was suddenly recognized that the company’s existing interface proxy service middleware did not support cross-application gray configuration, so testing and joint adjustment could not be carried out. Considering that front-end resources cannot be matched quickly, the final evaluation adopts the method of “gray scale + old application interface forwarding”. That is, all traffic goes to the old interface first, and then determines whether to go to the old interface or the new interface according to the gray level. A simple illustration is as follows:In this way, the old application will turn into an empty shell of process forwarding, but it can not be offline. If it is to be offline, front-end adaptation is needed, but the new service can be baptized online in advance, and problems in the code can be found in time, and the entrance can be directly switched when the front-end adaptation.

thinking

From the whole scheme, gray configuration is simple, and the new application interface is easy to call encapsulation, but the problem is that there are dozens of interfaces, too many, if one encapsulation is simple brainless handling, pure physical work; Also, the code would quickly become obsolete, so I wondered if there was an easy way to resolve all interface forwarding at once.

Let’s analyze it:

  • The new and old interfaces belong to different interface classes, but the method names are identical, and there is no method overloading: one-to-one mapping can be implemented through simple configuration;
  • The old and new interface methods have exactly the same input and return value structure: this provides an opportunity for serialization/deserialization (or assigning values to fields by reflection, using BeanUtils#copyProperties, etc.).

The problem is to forward a request for a service to another service that has almost the same signature information as the current service. When we process the request in the old service, we can get the class name, method name, parameter type and parameter information in various ways. If we can find a target service instance and method based on this information, we can complete the request forwarding.

This looks like the RPC framework’s process of service discovery through the registry, intercepting requests to find execution objects, and then making interface calls. However, the process of service discovery is “full match”, now we only need to find the corresponding method given the interface name (no overloading is required); The interface calls in RPC framework use reflection technology.

Can we also refer to the implementation of RPC? Registry, service discovery, request interception, reflection call, if we can solve these few points, our problems seem to be solved. To sort out the initial ideas:

  • Registry: The new and old interfaces belong to different interface classes. You can use Map to Map interface classes. We need to let the program know that when a request is made to interface A1# Method, it can forward the request to interface A2# Method. Since the method name is the same, we can configure it by default and set it dynamically at run time. We only need to complete the mapping from A1 to A2.
  • Service discovery: Service discovery in RPC requires a service provider instance to be registered with the registry, and then the service consumer creates a local service reference object through which we can make remote calls to service methods. Our service is based on the RPC framework, and as long as we configure the reference, this consumer object can be generated locally. All we need to do is take this object and associate it with the target service interface class. In Spring, it’s just a Bean, so it’s a snap.
  • Request interception: Depending on the way the request entry is adopted, we can choose a different interceptor or Filter, or use AOP to slice the class or method that needs to forward the request, and use annotations to achieve precise control over the forward target. Get method metadata information, such as class name, method name, parameter list and type, return value type, etc.
  • Reflection invocation: In the previous two steps we can get the target service interface class and the local reference object of the target service interface class, providing the target object for reflection. All that is left is to use reflection techniques to find the list of methods on the target object, then convert the parameters and execute the call.

Through the above analysis, combing, the feasibility of this idea basically no problem, and for each problem to be solved proposed solutions, I believe that you also have a general understanding of the process of implementation. All that remains is implementation and proof

Practice and Demonstration

There is no way to share the project code of the company, so I will use a Demo to implement it again. The technical framework is Dubbo+Nacos. The sample project uses some public packages that I personally packaged, which you can download from Github. For simplicity, the example uses only one method.

The transition problem is that the old service had a GreetingController#sayHello method and now needs to forward its request to the new Dubbo service implementation GreetingService#sayHello. The old service code looks like this:

@RestController
@RequestMapping(value = "/greeting")
@Slf4j
public class GreetingController {

    @RequestMapping(value = "hello", method = RequestMethod.POST)
    public SingleResponse<HelloResponseCO> sayHello(@RequestBody HelloRequestCmd helloRequestCmd) {
        log.info("sayHello params={}", helloRequestCmd);
        HelloResponseCO responseCO = new HelloResponseCO();
        responseCO.setName("abcd");
        responseCO.setGreeting("Hello," + helloRequestCmd.getName() + ". --from old service.");
        returnSingleResponse.of(responseCO); }}Copy the code

That is, an interception is performed when the front end requests /greeting/hello, the parameter information is retrieved, the GreetingService#sayHello method is called through reflection, and the result is converted and returned to the front end.

Simulate registry and service discovery

A mapping between GreetingController and GreetingService is implemented by simulating the registry with RegistryUtils to implement a simple route lookup function. In order to be able to associate directly with the consumer instance object of GreetingService, the Bean obtained through Spring is stored directly in the mapping structure, and the Spring container automatically performs the matching and initialization logic after initialization. The code is as follows:


/** * Emulates registries and service object stores **@author raysonxin
 * @since2021/1/19 * /
@Slf4j
@Component
public class RegistryUtils implements ApplicationContextAware {

    /** * mapping */
    private static Map<String, ServiceDesc> serviceMap = new HashMap<>(1);


    static {
        / / initialization
        serviceMap.put(GreetingController.class.getName(), new ServiceDesc(GreetingService.class, null));
    }

    /** * Gets the target service's Bean object ** based on the request source class name@paramSourceClass sourceClass name *@return bean
     */
    public static Object getBeanObject(String sourceClass) {
        if (serviceMap.containsKey(sourceClass)) {
            return serviceMap.get(sourceClass).getServiceBeanObject();
        }
        return null;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        for (Map.Entry<String, ServiceDesc> entry : serviceMap.entrySet()) {
            try {
                // Get the bean object by type and set it to the registry.
                Object bean = applicationContext.getBean(entry.getValue().getServiceType());
                entry.getValue().setServiceBeanObject(bean);
            } catch (Exception ex) {
                log.error("RegistryUtils getBean for {}->{} failed.", entry.getKey(), entry.getValue().getServiceType().getName()); }}}@Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class ServiceDesc {

        /** * Service type */
        privateClass<? > serviceType;/** * Service object instance */
        privateObject serviceBeanObject; }}Copy the code

Request interception and reflection calls

Define an annotation EnableTransfer that modifies the interface or method to be forwarded for AOP weaving. Define the aspect class TransferAspect and set the execution logic of the pointcut and surrounding weaving.

To complete interface forwarding, we need to get the class name, method name, parameter, return value type of the current interface through reflection, and then find the target service Bean instance corresponding to the current class name through RegistryUtils. Once you have a Bean instance, you can use reflection to process it. You can just look at the code.

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EnableTransfer {
}


/** ** implement **@author raysonxin
 * @since2021/1/19 * /
@Component
@Slf4j
@Aspect
public class TransferAspect {

    @Pointcut("@within(com.rsxtech.demo.consumer.transfer.EnableTransfer)||@annotation(com.rsxtech.demo.consumer.transfer.En ableTransfer)")
    public void pointcut(a) {}@Around("pointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        // Request class name
        String className = joinPoint.getTarget().getClass().getName();
        // Request method name
        String methodName = joinPoint.getSignature().getName();
        // Return value typeClass<? > returnType = ((MethodSignature) joinPoint.getSignature()).getMethod().getReturnType();// the method is added
        Object[] args = joinPoint.getArgs();

        try {
            // Determine whether to forward, gray logic
            if (this.needTransfer(args)) {
                // Perform the call
                return this.invokeProxy(className, methodName, args, returnType); }}catch (Exception ex) {
            log.error("TransferAspect execute {}#{} transfer failed",className,methodName,ex);
        }
        // If there is no gray level or an exception is encountered, use the old processing method, the new interface does not work, but also to ensure that the system is available.
        return joinPoint.proceed();
    }

    /** * analog grayscale logic */
    private boolean needTransfer(Object[] args) {
        int rand = new Random().nextInt(100) % 2;
        return rand == 0;
    }

    private Object invokeProxy(String className, String methodName, Object[] args, Class
        returnType) throws Exception {
        // Get the target service instance object
        Object bean = RegistryUtils.getBeanObject(className);
        if (null == bean) {
            throw new NoSuchMethodException("no bean found");
        }

        // The method matches
        Method targetMethod = null;
        Method[] methods = bean.getClass().getDeclaredMethods();
        for (Method method : methods) {
            if (method.getName().equals(methodName)) {
                targetMethod = method;
                break; }}if (null == targetMethod) {
            throw new NoSuchMethodException("no matched method found");
        }
        
        // Parameter type judgment and conversion
        Object result = null; Class<? >[] targetParamTypes = targetMethod.getParameterTypes();if (targetParamTypes.length == 0) {
            result = targetMethod.invoke(bean);
        } else if (targetParamTypes.length == args.length) {
            Object[] targetParams = new Object[args.length];
            for (int i = 0; i < args.length; i++) {
                // Convert parameters here
                targetParams[i] = ObjectConverter.convert(args[i],targetParamTypes[i]);
            }

            result = targetMethod.invoke(bean, targetParams);
        } else {
            throw new NoSuchMethodException("no matched method found");
        }


        // Before returning, type conversion is required according to returnType
        returnObjectConverter.convert(result,returnType); }}Copy the code

Object type conversions are involved in the processing of parameters and return values. I use ObjectConverter and Jackson to add support for complex types. ObjectConverter found Balusc code from Github, link attached.

argument

Well, after a bit of work, it’s finally time to argue. The rest is as simple as adding the @enableTransfer annotation to the GreetingController class. When we request /greeting/hello, we’ll find TransferAspect#around. The code for the new service can be accessed directly at github.

/** * Old service implementation **@author raysonxin
 * @since2021/1/17 * /
@RestController
@RequestMapping(value = "/greeting")
@Slf4j
@EnableTransfer
public class GreetingController {

    @RequestMapping(value = "hello", method = RequestMethod.POST)
    public SingleResponse<HelloResponseCO> sayHello(@RequestBody HelloRequestCmd helloRequestCmd) {
        log.info("sayHello params={}", helloRequestCmd);
        HelloResponseCO responseCO = new HelloResponseCO();
        responseCO.setName("abcd");
        responseCO.setGreeting("Hello," + helloRequestCmd.getName() + ". --from old service.");
        returnSingleResponse.of(responseCO); }}Copy the code

conclusion

This is the end of the article, or a brief summary.

  • The problem described in this paper is a real case encountered in my project. It took about half a day from the beginning to the completion. In fact, the actual coding time is about an hour. When I started working on this problem, I didn’t want to write interface by interface and do the repetitive and useless work. I thought about proxy, reflection, THOUGHT about THE RPC framework, and put it into action as the idea matured. There was a bit of a twist in the middle type conversion, but it worked out.
  • As mentioned at the beginning of the article, I think it is a design flaw to recognize such a big risk just before the development is completed. Although, at the beginning of the project, I confirmed to the person in charge that smooth gray switching could be guaranteed, but it was still cool. Therefore, we must carefully evaluate and demonstrate the feasibility of the solution in the actual development, otherwise we will suffer the consequences.

The attached

  • Rsx-demo-projects, github.com/raysonxin/r…
  • Rsx-common-parent, github.com/raysonxin/r…
  • The Generic object converter “balusc.omnifaces.org/2007/08/gen…
  • The ObjectConverter. Java “gist.github.com/maxd/1d8a5f…

Public search “code road mark”, point of concern not lost!