background
AREX is an open source automated regression testing platform based on real requests and data. It uses Java Agent technology and comparison technology to achieve fast and effective regression testing through traffic recording and playback capabilities.
Dubbo is a high-performance distributed service framework. It is based on RPC service invocation and service governance. It has the characteristics of transparent remote invocation, load balancing, service registration and discovery, high scalability, and service governance.
Currently AREX supports the recording of interface requests using the Dubbo2x and Dubbo3x protocols, and supports playback of use cases using these two native versions of the Dubbo protocol in the Schedule Service developed based on the Dubbo3x version. However, the user-defined extended Dubbo protocol, serialization methods, and even many personalized transformations based on Dubbo (such as dubbox) are currently not supported.
For example, a user in the community gave us feedback that the Dubbo protocol used by his company is based on the transformation made by Dubbox. The package structure and serialization and deserialization methods of the protocol have been modified. It can be understood as a brand new protocol. A specific type of Dubbo protocol cannot use AREX for recording and playback operations. In order to solve this problem, we forked a customized version on the main branch to realize the recording and playback of this protocol, but this method is only applicable to this specific protocol and lacks universality.
Considering that many community users may have similar problems in the future, we need a general solution that allows users to re-develop themselves to adapt to various custom Dubbo protocols. This article records the specific implementation ideas and details, hoping to provide reference for other users in future use.
plan
For the convenience of understanding, let's first introduce the workflow of each service component of AREX.
When using the AREX traffic recording function, the AREX Java Agent will record the data traffic and request information of the Java application in the production environment, and send this information to the AREX data access service (Storage Service), which will be imported into the Mongodb database to store. When replay testing is required, the AREX Schedule Service will extract the recorded data (request) of the application under test from the database through the data access service according to the user's configuration and needs, and then send an interface request to the target verification service . At the same time, the Java Agent will return the recorded external dependency (external request/DB) response to the application under test, and the target service will return a response message after processing the request logic. Then the scheduling service will compare the recorded response message with the playback response message to verify the correctness of the system logic, and push the comparison result to the analysis service (Report Service), which will generate a playback report for testers to check .
The difficulty of the problem is that since the scheduling service implements playback through generalized calls, when the Dubbo version, protocol, and serialization method of a service are inconsistent with the service provider (Provider), various problems will occur and the call will fail. .
AREX hopes to reduce the interference to user services as much as possible, so it does not want to care about the Dubbo configuration of user services. In order to solve the problem of inconsistency in Dubbo version, protocol, and serialization method, you can consider sending the playback request to the user service itself, and let the user service perform adaptation by itself.
With this in mind, we consider using Java SPI (Service Provider Interface, Service Provider Interface) to solve the problem. The Java SPI is a standard service discovery mechanism in Java. It helps developers extend and replace components in a program without modifying the code.
Users only need to implement the playback interface defined by the scheduling service, and then use the same framework as the recording to perform the playback operation. During playback, the scheduling service dynamically loads user-implemented interfaces through the Java SPI mechanism. In this way, the user service can adapt itself to the Dubbo version, protocol, and serialization method. For the native Dubbo protocol, AREX implements its extension and loads it in by default.
code analysis
SPI definition
Invoker
public interface ReplayExtensionInvoker {
/**
* check caseType by InvokerConstants#XXXCaseType.
*/
boolean isSupported(String caseType);
ReplayInvokeResult invoke(ReplayInvocation invocation);
default int order() {
return 1;
}
}
Referring to the wording of Invoker in the Dubbo source code, an interface named ReplayExtensionInvoker is defined. This interface defines two methods: and , which are isSupported
used invoke
to check the supported caseType
types and perform playback operations respectively. invoke
The parameter of the method Invocation
is a packaged request, which contains various information of the request, such as method name, parameter value and so on. invoke
The return value of the method is an InvokeResult
object, which contains the playback result information. A default method is also defined in the interface order
, which is used to specify the execution order of the implementing class.
- isSupported()
caseType
Indicates the protocol type of the playback use case, which can be represented by the InvokerConstant
predefined constants in the class . XXXCaseType
Users need to bind (match) the playback extension they implement with the specified by implementing the method of ReplayExtensionInvoker
the interface . In this way, when AREX performs a playback operation, it will automatically match the corresponding playback extension according to the protocol type in the playback use case, so as to realize the playback function of the corresponding protocol type.isSupported
caseType
- order()
caseType
Multiple extensions may be implemented for the same , here the order is specified to load first, in descending order.
Invocation
public interface ReplayInvocation {
/**
* eg: dubbo:127.0.0.1:20880
* protocol + host + port
*/
String getUrl();
Map<String, Object> getAttributes();
ReplayExtensionInvoker getInvoker();
void put(String key, Object value);
/**
* Get specified class item from attributes.
* key: refer to InvokerConstants.
*/
<T> T get(String key, Class<T> clazz);
/**
* Get object item from attributes.
* key: refer to InvokerConstants.
*/
Object get(String key);
}
Invocation
It is defined as an interface, and users need to implement some methods in this interface in order to obtain request parameters during playback.
- getUrl()
Returns the requested protocol, host and port number, referring to other protocols, such as gRPC, Triple, Rest, etc. Url is essential. It is hoped that it can support the expansion of other protocols in the future, and Url is used as a parameter alone, which is composed of protocol name + IP address + port number. For example: dubbo:127.0.0.1:20880
.
- getAttributes()
The scheduling service packages other parameters of the request in a Map object of key-value pair type, where the key of type String needs to be InvokerConstants
consistent with the name agreed in . When implementing playback extension, the user can getAttributes()
obtain the Map object by calling the method, and then obtain the corresponding parameter value InvokerConstant
from for processing during playback.
Result
public class ReplayInvokeResult {
/**
* invoke result.
*/
private Object result;
private Map<String, String> responseProperties;
/**
* if invoke failed.
*/
privdate String errorMsg;
/**
* if invoke failed.
*/
private Exception exception;
}
The ReplayInvokeResult class represents the result of the request invocation, and it contains the following fields:
- result: The result of the request call, stored in a variable of type Object.
- responseProperties: Some implicit parameters that need to be passed by the provider are stored in a Map.
- type variable.
- errorMsg: If the request call fails, the string storing the error message.
- exception: An object that stores exception information if the request call fails.
Extended loading
In the extension loading process, first ClassLoader
obtain an URLClassLoader
object by getting the of the current class, and then call its method through reflection addURL()
to load the extension implementation class.
loadJar()
The method accepts a jarPath
parameter, which indicates the jar package path where the extended implementation class is located, and then ClassLoader
obtains a reference to URLClassLoader
the object and addURL()
the method by judging the current JDK version and obtaining . Then, judge ClassLoader
whether the current is of URLClassLoader
type, if so, call the method through reflection addURL()
to load the jar package, and add the classes in the jar package to ClassLoader
, so that the method of the extended implementation class can be dynamically called when the program is running. If loading fails, an error message is output.
public void loadJar(String jarPath) {
try {
int javaVersion = getJavaVersion();
ClassLoader classLoader = this.getClassLoader();
File jarFile = new File(jarPath);
if (!jarFile.exists()) {
LOGGER.error("JarFile doesn't exist! path:{}", jarPath);
}
Method addURL = Class.forName("java.net.URLClassLoader").getDeclaredMethod("addURL", URL.class);
addURL.setAccessible(true);
if (classLoader instanceof URLClassLoader) {
addURL.invoke(classLoader, jarFile.toURI().toURL());
}
} catch (Exception e) {
LOGGER.error("loadJar failed, jarPath:{}, message:{}", jarPath, e.getMessage());
}
}
how to use
Step 1: Develop SPI
1. Add pom dependencies
To develop SPI extensions, you first need to pom.xml
add dependencies to the project's file arex-schedule-extension
.
<dependency>
<groupId>com.arextest</groupId>
<artifactId>arex-schedule-extension</artifactId>
</dependency>
2. Implement SPI
To implement the SPI extension, it needs to be implemented com.arextest.schedule.extension.invoker.ReplayExtensionInvoker
.
It should be noted that in the provider, it is judged whether the current traffic is playback traffic through Dubbo's implicit parameter attachments
, Arex-Record-Id
and the generated is Arex-Record-Id
also attachments
passed back through .
Therefore, when implementing ReplayExtensionInvoker
the interface, you need to ReplayInvocation#attributes
get it from the property Arex-Record-Id
and add it to the attachments
; at the same time, add attachments
the from to the property so that these parameters can be used correctly in the playback traffic.Arex-Record-Id
ReplayInvokeResult#responseProperties
The following is a default dubboInvoker
example for reference:
package com.arextest.dubboInvoker;
import com.arextest.schedule.extension.invoker.InvokerConstants;
import com.arextest.schedule.extension.invoker.ReplayExtensionInvoker;
import com.arextest.schedule.extension.invoker.ReplayInvocation;
import com.arextest.schedule.extension.model.ReplayInvokeResult;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ReferenceConfig;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.service.GenericService;
import java.util.List;
import java.util.Map;
public class DefaultDubboInvoker implements ReplayExtensionInvoker {
[@Override](https://my.oschina.net/u/1162528)
public boolean isSupported(String caseType) {
return InvokerConstants.DUBBO_CASE_TYPE.equalsIgnoreCase(caseType);
}
[@Override](https://my.oschina.net/u/1162528)
public ReplayInvokeResult invoke(ReplayInvocation replayInvocation) {
ReplayInvokeResult replayInvokeResult = new ReplayInvokeResult();
try {
RpcContext.getServiceContext().setAttachments(replayInvocation.get(InvokerConstants.HEADERS, Map.class));
ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
reference.setApplication(new ApplicationConfig("defaultDubboInvoker"));
reference.setUrl(replayInvocation.getUrl());
reference.setInterface(replayInvocation.get(InvokerConstants.INTERFACE_NAME, String.class));
reference.setGeneric(true);
GenericService genericService = reference.get();
if (genericService == null) {
return replayInvokeResult;
}
Object result = genericService.$invoke(replayInvocation.get(InvokerConstants.METHOD_NAME, String.class),
(String[]) replayInvocation.get(InvokerConstants.PARAMETER_TYPES, List.class).toArray(new String[0]),
(replayInvocation.get(InvokerConstants.PARAMETERS, List.class)).toArray());
replayInvokeResult.setResult(result);
replayInvokeResult.setResponseProperties(RpcContext.getServerContext().getAttachments());
} catch (Exception e) {
replayInvokeResult.setException(e);
replayInvokeResult.setErrorMsg(e.getMessage());
}
return replayInvokeResult;
}
}
3. Create the SPI extension file
Create a text file in the project's resources/META-INF/services directory named com.arextest.schedule.extension.invoker.ReplayExtensionInvoker
. ReplayExtensionInvoker
The content of the file is the fully qualified name of the extended implementation class, that is, the Reference of the class that implements the interface.
4. Make jar package
Package the implemented SPI extension into a jar package, pay attention to include all related dependencies.
Step 2: Import AREX project
1. jar package mapping Docker image
First, you need to map the jar package to the Docker image, which can be docker-compose.yml
achieved by modifying the file, mapping the local directory to the Docker image, and adding the jar package to the corresponding directory on the machine.
2. Modify the scheduling service startup parameters
Next, to modify the startup parameters of the Schedule Service, it is also docker-compose.yml
realized by modifying the file. Add the following startup parameters to Schedule's startup parameters.
-Dreplay.sender.extension.jarPath=/usr/local/tomcat/custom-dubbo/xxx.jar
Where xxx.jar
is the name of the jar package packaged in the previous step, /usr/local/tomcat/custom-dubbo/
and is the storage path of the jar package in the Docker image. The function of this startup parameter is to tell AREX where to find the jar package of SPI extension.
Outlook
There are three areas that can be improved in the future:
The loading mode of the jar package
At present, users need to manually manage and configure the path of the jar package, and need to manually modify docker-compose.yml
the file, which is a little cumbersome to operate. In the future, I hope to be able to configure relevant parameters and upload jar packages on the AREX platform.
Parameter convention
At present, the management of parameters is implemented in the code in the form of hardcode, and new access parties need to make an agreement in advance, and then update the SPI package. In the future, we hope to configure parameters on the AREX platform, so that new access parties can realize their needs through simple configuration.
Support for other protocols
Currently, the invoker framework can directly support various personalized extensions of the Dubbo protocol. If users need to access other protocols, such as gRPC, Triple, Rest, etc., they can also raise [issue] ( https://github.com/arextest/arex/issues) and discuss in the community to expand the invoker framework scope of application.
- AREX Documentation: http://arextest.com/zh-Hans/docs/intro/
- AREX official website: http://arextest.com/
- AREX GitHub:https://github.com/arextest
- AREX official QQ communication group: 656108079