×
Community Blog How Is SWAK, the Decoupling Tool for Idle Fish Business Code, Implemented?

How Is SWAK, the Decoupling Tool for Idle Fish Business Code, Implemented?

This article focuses on illustrating the principles of SWAK while posting some key code implementation using abridged code to help readers understand.

By Chenxiao from Idle Fish Technology

Three years ago, we published an article introducing the Service Code Deconstruction Tool – SWAK. SWAK is the abbreviation for Swiss Army Knife. For those that don’t know, a Swiss Army Knife is a small, multi-tool pocket knife suitable for various scenarios. On the Idle Fish server side, the SWAK framework is also a compact and flexible technical framework that applies to a variety of scenarios. It can solve the problem of decoupling the platform code from business code for splitting businesses. Previously, we applied it to the business decoupling of the commodity domain. Now, we have encountered this problem once again in the glue layer of search. So, we used SWAK to solve it.

The Application of SWAK in Idle Fish Search

Idle Fish search adopted a new development mode featuring end-cloud unification and introduced a glue layer to move part of the client logic to the server. This improves the dynamic capability of the end side, eliminates some requirements of version updates, and speeds up the release of requirements. During the process of using this new mode, the code structure design of the glue layer part is relatively simple at the beginning, resulting in the serious coupling of the glue layer code and the expansion of if-else logic.

1

Based on the prejudgment of the business, we reconstructed the glue layer and mainly solved two problems using SWAK:

  • Due to the increasing number of vertical services in search, the existing structure cannot support business customization. Therefore, SWAK is used to decouple businesses and index to the corresponding page orchestration logic according to different bizType (business type). This ensures that the page orchestration logic of different businesses does not affect each other. Small businesses can reuse the page logic of commodity search or customize it themselves.
  • There are more types of cards, which leads to the expansion of if-else code in the card parser. Therefore, we introduced the second layer SWAK, removed the if-else part of the card parser, and changed it to the mode of indexing to the corresponding parser through cardType (card type). Thus, other people will not feel confused when searching for the real logic among if-else code scripts.

2

Currently, the search glue layer is still in the overall architecture upgrade. Let's give you a brief introduction. After the architecture upgrade is completed, we will write an article to introduce the research and development mode featuring end-cloud unification of Idle Fish search. This article will focus on the implementation principle of SWAK.

The Usage of SWAK

Before explaining the principle of SWAK, let's briefly review the use of SWAK to understand its principle. Let's first look at what problems SWAK solves. For example, when performing a search, we need to judge different search types and return different page arrangements. At this time, we need to go to different branches of code according to the type. If the code is coupled in one place, the file will become more difficult to maintain in the future.

if(Search product) {
    if(Search product A) {
        doSomething1();
    }else if(Search product B) {
        doSomething2();
    }
} else if(Search under Huiwan) {
    doSomething3();
} else if(Search User) {
    if(Search user A) {
        doSomething4();
    }else if(Search user B) {
        doSomething5();
    }
}

SWAK aims to handle the situation above. We can tile all the logic corresponding to if-else, change it to TAG, and route through SWAK.

/**
 * 1. First, define an interface.
 */
@SwakInterface(desc = "Component parsing") // Use annotations to declare that this is a multi-implementation interface.
public interface IPage {
    SearchPage parse(Object data);
}

/**
 * 2. Then, write the corresponding implementation. There can be many implementations, which are identified by TAG.
 */
@Component
@SwakTag(tags = {ComponentTypeTag.COMMON_SEARCH})
public class CommonSearchPage implements IPage {
    @Override
    public SearchPage parse(Object data) {
        return null;
    }
}

/**
 * 3. Write the entry of the SWAK route.
 */
@Component
public class PageService {
    @Autowired
    private IPage iPage;

    @SwakSessionAop(tagGroupParserClass = PageTypeParser.class,
        instanceClass = String.class)
    public SearchPage getPage(String pageType, Object data) {
        return iPage.parse(data);
    }
}

/**
 * 4. Write the corresponding parsing class.
 */
public class PageTypeParser implements SwakTagGroupParser<String> {
    @Override
    public SwakTagGroup parse(String pageType) {
            // pageType = ComponentTypeTag.COMMON_SEARCH
        return new SwakTagGroup.Builder().buildByTags(pageType);
    }
}

Although there are only a few lines of code, it covers all the core processes of SWAK. The core problem is, how can SWAK find the corresponding interface implementation? This problem needs to be answered from two aspects: registration process and execution process.

3

Registration Process

Since applications in Idle Fish servers are mostly based on the Spring framework, SWAK borrowed many Spring features in its design. Spring-related features can be researched elsewhere if you are unfamiliar. We won't introduce them in detail here.

Let’s take the section above as an example. The main purpose of the registration phase is to find the IPage class labeled by @SwakInterface and give it to the Spring container for hosting. Thus, the dependency injection capability of Spring can be used naturally when using the class. At the same time, in order to replace the interface implementation dynamically in the future, we cannot register the found class into the Spring container directly. We need to hook it into a proxy class and return instances of different @SwakTag in the proxy class according to the situation.

There may be several questions when reading these sentences, which we will answer one by one:

  1. How do we find Bean labeled by @SwakInterface?
  2. How do we make a change when Spring registers Bean?
  3. How does the proxy class dynamically replace the interface implementation?

How Do We Find Bean Labeled by @SwakInterface?

In Java, we usually use reflection to obtain custom annotations. We need to scan all the classes and obtain custom annotations through reflection. You can optimize the scanning range here and only scan classes under a specific path. It is possible to write the code logic for scanning the library, but it is also a good choice to use the open-source framework. I would like to recommend the reflections library (GitHub) of ronmamo. The implementation principle of the library will not be introduced in detail here. The usage method is also very simple. Let's view the code directly:

    public Set<Class<?>> getSwakInterface() {
        Reflections reflections = new Reflections(new ConfigurationBuilder()
            .addUrls(ClasspathHelper.forPackage(this.packagePath))
            .setScanners(new TypeAnnotationsScanner(), new SubTypesScanner())
        );
        return reflections.getTypesAnnotatedWith(SwakInterface.class);
    }

In addition to scanning the @SwakInterface, we should scan out classes corresponding to @SwakTag and store them in a map to ensure that we can find a Class through tags later.

How Do We Make a Change When Spring Registers Bean?

Spring provides a method for us. Spring will get all beans of BeanDefinitionRegistryPostProcessor type in the container and call the postProcessBeanDefinitionRegistry method in the bean registration phase. So, we can inherit this class and rewrite the corresponding methods to hook this process directly. In this method, we can create a new BeanDefinition and set the prepared proxy class as BeanClass. This way, we will use the prepared proxy class directly when the corresponding Bean is generated. (The principle here involves the registration process of Spring Bean. You can check the information by yourself.)

@Configuration
public class ProxyBeanDefinitionRegister implements BeanDefinitionRegistryPostProcessor {
  
    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefinitionRegistry) throws BeansException {
        Set<Class> typesAnnotatedWith = getSwakInterface();

        for (Class superClass : typesAnnotatedWith) {
            if (!superClass.isInterface()) {
                continue;
            }

            RootBeanDefinition beanDefinition = new RootBeanDefinition();
            beanDefinition.setBeanClass(SwakInterfaceProxyFactoryBean.class);

            beanDefinition.getPropertyValues().addPropertyValue("swakInterfaceClass", superClass);
            String beanName = superClass.getName();
            beanDefinition.setPrimary(true);
            beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinition);
        }
    }
}

How Does the Proxy Class Dynamically Replace the Interface Implementation?

In the previous step, we prepared a SwakInterfaceProxyFactoryBean to be registered in the BeanDefinitionMap as a proxy class, but SwakInterfaceProxyFactoryBean is not a proxy class in the strict sense. As its name describes, it is a FactoryBean, which is a class used in Spring to create a more complex bean. In the getObject() method of this class, we use dynamic proxy to create the corresponding object.

In the choice of dynamic proxy mode, we use CGLIB to implement dynamic proxy because the dynamic proxy mechanism in JDK can only proxy classes that implement interfaces. CGLIB can provide proxies for classes that do not implement interfaces and can provide better performance. There are many introductions to CGLIB on the Internet, so I won't introduce them in detail here. Set a CallBack in Enhancer. When the proxy class calls the method, it will call back the SwakInterfaceProxy.intercept() method we set to intercept it. We will introduce the intercept() method in detail in the following execution process. Let's look at the code first:

public class SwakInterfaceProxyFactoryBean implements FactoryBean {
    @Override
    public Object getObject() {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this);
        this.clazz = clazz;
        // In general, new is not used here, and the SwakInterfaceProxy can be handed over to Spring for hosting. In order to express clearly, new is used to refer to the enhancer.
        enhancer.setCallback(new SwakInterfaceProxy());
        // Return a proxy object. The returned object initially is a proxy class that encapsulates the “entity class” is an instance of the implementation class.
        return enhancer.create();
    }
}

Execution Process

In the execution process, our main goal is to parse the corresponding implementation class CommonSearchPage of the member variable IPage iPage by SwakTagGroupParser, according to parameters before the method body marked by the @SwakSessionAop is executed. Then, the call of ipage.parse() in this method body will call the CommonSearchPage.parse() method directly.

The simple process above may lead to some additional questions:

  1. How can we insert the parsed code before the method body labeled by @SwakSessionAop is executed?
  2. After the corresponding implementation class is parsed, how does it assign the value to the iPage variable?

How Can We Insert Code in Front of the Method?

When reading this question, it is natural to think that an AOP is needed to be designed. Spring has helped us with it. The AOP of Spring is a JVM-based dynamic proxy implemented by CGLIB and has been well-encapsulated. We can use the @Around annotation to perform a layer of facets before the method to execute our code. First, we use the SwakTagGroupParser to parse the tagGroup and save the parsed tagGroup. Then, we can call the jointPoint.proceed() to continue executing the method body, so the iPage used in the method body will be implemented accordingly.

Some people may doubt that it just saved tagGroup here. Why will the iPage use the corresponding implementation later? We will explain this in the next question.

@Component
@Aspect
public class SwakSessionInterceptor {

    @Pointcut("@annotation(com.taobao.idle.swak.core.aop.SwakSessionAop)")
    public void sessionAop() {
    }


    @Around("sessionAop()&&@annotation(swakSessionAop)")
    public Object execute(ProceedingJoinPoint jointPoint, SwakSessionAop swakSessionAop) {
          // Obtain the parameters of Parser based on the type.
        Class instanceClass = swakSessionAop.instanceClass();
          Object sessionInstance;
          for (Object object : args) {
            if (instanceClass.isAssignableFrom(object.getClass())) {
                sessionInstance = object;
            }
        }
      
          // Parse the corresponding tagGroup by using Parser.
        Class parserClass = swakSessionAop.tagGroupParserClass();
        SwakTagGroupParser swakTagGroupParser = (SwakTagGroupParser)(parserClass.newInstance());
        SwakTagGroup tagGroup = swakTagGroupParser.parse(sessionInstance);
      
        try {
            // SwakSessionHolder is a place to store tagGroup, which can be implemented at will.
            SwakSessionHolder.hold(tagGroup);
            Object object = jointPoint.proceed();
            return object;
        } finally {
            SwakSessionHolder.clear();
        }
    }
}

How Is the Value of the iPage Variable "Assigned"?

First of all, I need to explain why I always put quotation marks for "assign," because it does not really assign value to iPage, but the effect is the same. I still remember that we made a layer of dynamic proxy for the classes marked by @SwakInterface before, so the objects corresponding to iPage will call the intercept() method before calling the method. In this method, we can find the SwakTag to be called through the tagGroup saved before, find the instance of the corresponding implementation class through SwakTag, and call its instance through the method.invoke() method.

The API related to reflection will not be introduced in detail here. Here is the explanation of the method from Liao Xuefeng. “Calling invoke on a Method instance is equivalent to calling the method. The first parameter of invoke is the object instance, which refers to the instance where the method is called. Subsequent variable parameters should be consistent with the method parameters. Otherwise, an error will be reported.”

public class SwakInterfaceProxy implements MethodInterceptor {
    @Override
    public Object intercept(Object o, Method method, Object[] parameters, MethodProxy methodProxy) throws Throwable {
            String interfaceName = clazz.getName();
        SwakTagGroup tagGroup = SwakSessionHolder.getTagGroup();
        
        // You can also adjust the execution sequence based on the priority configuration of the tag. A random sequence is used here.
        List<String> tags = tagGroup.getTags();
        Object retResult = null;
        try {
            // Execute according to the priority.
            for (String tag : tags) {
                // Obtain the instance of the implementation class based on the TAG.
// No instance but only Class is obtained here. It may be because that TAG is used for the first time. Find the corresponding instance in Spring container by using the Class.
                Object tagImplInstance = getInvokeInstance(tag);
                retResult = method.invoke(tagImplInstance, parameters);
            }
            return retResult;
        } catch (Throwable t) {
            throw t;
        }
    }
}

At this point, the complete process of using SWAK to call the method has been completed.

Summary

This article focuses on illustrating the principles of SWAK while posting some key code implementation. Some code involved in this article has been cut down to a certain extent to help you understand and save space, so do not copy it directly, but you can refer to this article to implement it yourself. If you have any questions after reading the article, we will continue to write corresponding articles to answer them.

0 0 0
Share on

XianYu Tech

56 posts | 4 followers

You may also like

Comments