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.
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.
Based on the prejudgment of the business, we reconstructed the glue layer and mainly solved two problems using SWAK:
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.
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.
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:
@SwakInterface
?@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.
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);
}
}
}
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();
}
}
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:
@SwakSessionAop
is executed?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();
}
}
}
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.
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.
A Few Tips on Large-Scale Real-Time Data Warehouse Construction
Selection and Exploration of Scheme for Flutter Automated UI Testing
56 posts | 4 followers
FollowXianYu Tech - November 22, 2021
XianYu Tech - December 24, 2021
XianYu Tech - December 27, 2021
XianYu Tech - December 13, 2021
XianYu Tech - December 24, 2021
XianYu Tech - May 13, 2021
56 posts | 4 followers
FollowExplore Web Hosting solutions that can power your personal website or empower your online business.
Learn MoreA low-code development platform to make work easier
Learn MoreExplore how our Web Hosting solutions help small and medium sized companies power their websites and online businesses.
Learn MoreHelp enterprises build high-quality, stable mobile apps
Learn MoreMore Posts by XianYu Tech