background

MPaaS -RPC is alipay’s native RPC call library.

In the process of client development, it is only necessary to simply call the functions encapsulated in the library to complete a data request process, but when encountering abnormal situations, we often face all kinds of exception codes and have no idea about them. Therefore, this article leads you to understand the call process of mPaaS-RPC and the causes of various exception codes.

1. Use method

On the Android side, the call of RPC is very simple, which is roughly divided into the following processes:

1.1 Defining RPC Interfaces

First, you need to define the RPC interface, including the interface name and interface features (whether login is required and whether signature is required). The interface definition method is as follows:

public interface LegoRpcService {
    @CheckLogin
    @SignCheck
    @OperationType("alipay.lego.card.style")
    LegoCardPB viewCard(MidPageCardRequestPB var1);
}
Copy the code

When the client calls the viewCard method, the framework will request the RPC interface corresponding to Alipay. Lego.card. style, and the parameters of the interface are defined in MidPageCardRequestPB.

This interface needs to check whether you are logged in and the client is signed correctly before calling, so this simple interface defines an RPC request.

1.2 Invoking an RPC Request

After the interface is defined, the RPC request needs to be called as follows:

// Create a parameter object
MidPageCardRequestPB request = new MidPageCardRequestPB();
// Fill in the parameters./ / get RpcServiceRpcService rpcService = (RpcService)LauncherApplicationAgent.getInstance().getMicroApplicationContext().findServiceByInterface(RpcService.class. getName());// Get the Rpc request proxy class
LegoRpcService service = rpcService.getRpcProxy(LegoRpcService.class);
// Call the method
LegoCardPB result = service.viewFooter(request);
Copy the code

The call process is roughly divided into the above steps.

It is important to note that we did not implement LegoRpcService. Instead, we obtained a proxy through rpcService.getrpcProxy, which uses the Java dynamic proxy concept, which will be covered later.

2. Source code analysis

Let’s take a look at how the framework works.

2.1 Creating Parameter Objects

The parameter object is a petabyte object whose serialization and deserialization processes need to correspond to the server side. In simple terms, this parameter is serialized on the client side, the request is sent as the parameter of the request, and then the server receives the request and deserializes it, executes the request based on the parameter, and returns the result.

MidPageCardRequestPB request = new MidPageCardRequestPB();
Copy the code

2.2 get RPCService

Git Bundle RPCServiceImpl mpaas-CommonService git Bundle RPCServiceImpl

This is done by calling the Load method of CommonServiceLoadAgent when the mPaaS starts.

        @Override
    public final void load(a) {... registerLazyService(RpcService.class.getName(), RpcServiceImpl.class.getName()); . }Copy the code

The getRpcProxy method in RpcServiceImpl invokes the getRpcProxy method of RpcFactory.

    @Override
    public <T> T getRpcProxy(Class<T> clazz) {
        return mRpcFactory.getRpcProxy(clazz);
    }
Copy the code

2.3 Obtaining the RPC Request Proxy Class

The mRpcFactory object is in the MPAas-RPC Bundle.

     public <T> T getRpcProxy(Class<T> clazz) {
        LogCatUtil.info("RpcFactory"."clazz=["+clazz.getName()+"]");
        return (T) Proxy.newProxyInstance(clazz.getClassLoader(), new Class[] { clazz},
            new RpcInvocationHandler(mConfig,clazz, mRpcInvoker));
    }
Copy the code

This is the process of creating dynamic proxies based on interfaces, a feature that Java native supports.

To put it simply, dynamic proxy means that the JVM generates a proxy class based on the interface. When invoking an interface method, the invoke method of the proxy class is called. You can perform operations on the invoke method to implement dynamic proxy.

2.4 Calling methods

The dynamic proxy class calls the RpcInvocationHandler method: the invoke method of RpcInvocationHandler is invoked when the defined RPC interface is called:

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws RpcException {
        return mRpcInvoker.invoke(proxy, mClazz, method, args, buildRpcInvokeContext(method));
    }
Copy the code

The Invoke method invokes the Invoke method of mRpcInvoker, which is passed by the RpcInvocationHandler created. The invoke method passes mClazz and buildRpcInvokeContext(method) in addition to the original arguments:

MClazz makes sense because RpcInvocationHandler corresponds to a class, whereas mRpcInvoker is a singleton: it doesn’t know which class method it is proiding, so it needs to be told explicitly.

BuildRpcInvokeContext (method) is a context object by name that holds the requested context information.

Next comes RpcInvoker’s Invoke method: An introduction to the design philosophy of the RPC framework, in which the Invoke method only defines processes, not actions. There are many interceptors registered in the framework, and each process is handled by the Interceptor. This idea is common in network-layer design architectures such as the famous Spring. Unfortunately, I think this idea got a little tweaked in the late development of the RPC framework, and the Invoke method was optimized for some of the details.

Without further ado, back to the Invoke method, I summarized some of the call flow, as shown below:

In simple terms, preHandle, singleCall, and postHandle are called in sequence. Let’s look at the code below:

     public Object invoke(Object proxy, Class
        clazz, Method method, Object[] args, InnerRpcInvokeContext invokeContext) throws RpcException {... preHandle(proxy, clazz, method, args, method.getAnnotations(),invokeContext);// Front intercept.try{
                response = singleCall(method, args, RpcInvokerUtil.getOperationTypeValue(method, args), id, invokeContext,protoDesc);
                returnObj = processResponse(method,response,protoDesc);
        } catch(RpcException exception) { exceptionHandle(proxy, response! =null? response.getResData():null, clazz, method, args, method.getAnnotations(), exception, invokeContext);// Exception interception
        }
          ...
        postHandle(proxy, response!=null? response.getResData():null, clazz, method, args, method.getAnnotations(), invokeContext);// backintercept.return returnObj;
    }
Copy the code

2.5 Front Interception

Let’s start with the preHandle method:

private void preHandle(final Object proxy, finalClass<? > clazz,final Method method,
                           final Object[] args, Annotation[] annotations, InnerRpcInvokeContext invokeContext) throws RpcException {
        handleAnnotations(annotations, new Handle() {
            @Override
            public boolean handle(RpcInterceptor rpcInterceptor, Annotation annotation)
                    throws RpcException {
                if(! rpcInterceptor.preHandle(proxy, RETURN_VALUE,new byte[]{}, clazz, method,
                        args, annotation, EXT_PARAM)) {
                    throw new RpcException(RpcException.ErrorCode.CLIENT_HANDLE_ERROR,
                            rpcInterceptor + "preHandle stop this call.");
                }
                return true; }}); RpcInvokerUtil.preHandleForBizInterceptor(proxy, clazz, method, args, invokeContext, EXT_PARAM, RETURN_VALUE);/ / the mock RPC current limit
        RpcInvokerUtil.mockRpcLimit(mRpcFactory.getContext(),method, args);
    }
Copy the code

The pre-intercept consists of three steps: first, handleanannotations are processed, then interceptors defined by the business layer are executed, and finally RPC limiting is simulated.

2.6 Handling Annotations

This is the most important step in the pre-intercept, calling handleAnnotations. This method gives us a callback, taking the annotations and the corresponding RpcInterceptor as parameters, and calls the RpcInterceptor preHandle method when we return.

Before we introduce handleanannotations, we’ll briefly mention the RpcInterceptor, which is called the interceptor in the framework:

public interface RpcInterceptor {
    public boolean preHandle(Object proxy,ThreadLocal<Object> retValue,  byte[] retRawValue, Class<? > clazz, Method method, Object[] args, Annotation annotation,ThreadLocal<Map<String,Object>> extParams) throws RpcException;

    public boolean postHandle(Object proxy,ThreadLocal<Object> retValue,  byte[] retRawValue, Class<? > clazz, Method method, Object[] args, Annotation annotation)
                                                                                            throws RpcException;

    public boolean exceptionHandle(Object proxy,ThreadLocal<Object> retValue,  byte[] retRawValue, Class<? > clazz, Method method, Object[] args, RpcException exception, Annotation annotation) throws RpcException;
}
Copy the code

In simple terms, the program starts by registering several interceptors, one annotation for each interceptor. When an annotation is processed in the Invoke method, the corresponding interceptor is found and the corresponding method of the interceptor is invoked. As mentioned in the previous flowchart, if the interceptor returns true, proceed, and if it returns false, the appropriate exception is thrown.

HandleAnnotations: handleAnnotations are just methods to find interceptors, so let’s see how this is implemented:

private boolean handleAnnotations(Annotation[] annotations, Handle handle) throws RpcException {
            for(Annotation annotation : annotations) { Class<? extends Annotation> c = annotation.annotationType(); RpcInterceptor rpcInterceptor = mRpcFactory.findRpcInterceptor(c); ret = handle.handle(rpcInterceptor, annotation); }}Copy the code

We call the mRpcFactory. FindRpcInterceptor (c) method to locate interceptor: mRpcFactory. FindRpcInterceptor (c) to find the two places:

  • One is mInterceptors;
  • The other is in GLOBLE_INTERCEPTORS.
public RpcInterceptor findRpcInterceptor(Class<? extends Annotation> clazz) {
        RpcInterceptor rpcInterceptor = mInterceptors.get(clazz);
        if(rpcInterceptor ! =null) {
            return rpcInterceptor;
        }
        return GLOBLE_INTERCEPTORS.get(clazz);
    }
Copy the code

The interceptors in these two places are actually the same, because the addRpcInterceptor adds one interceptor to both places.

public void addRpcInterceptor(Class<? extends Annotation> clazz, RpcInterceptor rpcInterceptor) {
        mInterceptors.put(clazz, rpcInterceptor);
        addGlobelRpcInterceptor(clazz,rpcInterceptor);
    }
Copy the code

The Spring approach mentioned above is that each case is handled in turn by multiple interceptors, which is more scalable.

The interceptor is added in the afterBootLoad method of CommonServiceLoadAgent in the CommonBiz Bundle, which is also called when the mPaaS framework is started.

rpcService.addRpcInterceptor(CheckLogin.class, new LoginInterceptor());
rpcService.addRpcInterceptor(OperationType.class, new CommonInterceptor());
rpcService.addRpcInterceptor(UpdateDeviceInfo.class, new CtuInterceptor(mMicroAppContext.getApplicationContext()));
Copy the code

Three interceptors have been added for three types of annotations, and at the end of the article we analyze what each interceptor does:

We’ll call the preHandle method when we find the corresponding interceptor annotations in handleanannotations.

PreHandle processed in the annotations of front intercept, could bring in preHandleForBizInterceptor processing context object of interceptor;

An interceptor in a Context object is the same as an annotation interceptor. In this stage I looked at the Context object and there is no interceptor set in the Context. If there is one, pull it out and call the corresponding methods in turn.

The final step in preHandle is to simulate network traffic limiting, which is used in tests where RPC access is restricted to simulate traffic limiting if the test turns on RPC traffic limiting.

2.7 singlecall

After processing the pre-intercept, we return to RpcInvoker’s Invoke method, which then calls singleCall to initiate the network request.

private Response singleCall(Method method, Object[] args,
                                String operationTypeValue, int id, InnerRpcInvokeContext invokeContext,RPCProtoDesc protoDesc) throws RpcException {
        checkLogin(method,invokeContext);
        Serializer serializer = getSerializer(method, args, operationTypeValue,id,invokeContext,protoDesc);
    if(EXT_PARAM.get() ! =null) {
        serializer.setExtParam(EXT_PARAM.get());
    }
    byte[] body = serializer.packet();
    HttpCaller caller = new HttpCaller(mRpcFactory.getConfig(), method, id, operationTypeValue, body,
    serializerFactory.getContentType(protoDesc), mRpcFactory.getContext(),invokeContext);
        addInfo2Caller(method, serializer, caller, operationTypeValue, body, invokeContext);
        Response response = (Response) caller.call();/ / synchronize
        return response;
    }
Copy the code

SingleCall starts with checkLogin, then obtains Serializer according to the parameter type, and performs the parameter serialization operation. The serialized parameter is used as the body of the entire request. Then an HttpCaller is created. HttpCaller is the wrapper code for the network transport layer, which is not focused on in this article.

Before actually sending the request, you need to call addInfo2Caller to add some general information to the HttpCaller, such as serialized version, contentType, timestamp, and whether to add a signature to the request according to the SignCheck annotation.

In my opinion, this annotation should be handled in each Intercepter as well. Otherwise, the code will not look good if the SignCheck annotation is handled separately.

Finally, we can actually call the caller.call method to send the request and receive a reply from the server.

2.8 ProcessResponse

This completes the singleCall method and gets a reply from the server.

The procedure goes back to invoke, gets a response from the server, and calls processResponse to process the response. The process of processing the reply is actually to deserialize the return result of the server, which is a reverse process of sending the request above, the code is as follows:

private Object processResponse(Method method, Response response,RPCProtoDesc protoDesc) {
        Type retType = method.getGenericReturnType();
        Deserializer deserializer = this.serializerFactory.getDeserializer(retType,response,protoDesc);
        Object object = deserializer.parser();
        if(retType ! = Void.TYPE) {/ / not void
            RETURN_VALUE.set(object);
        }
        return object;
    }
Copy the code

In the preHandle, singleCall, and processResponse procedures, if an RpcException is thrown (all exceptions in the process are thrown as rpcExceptions), ExceptionHandle is called in invoke.

2.9 Exception Handling

ExceptionHandle also finds the corresponding annotated Interceptor from the three interceptors and calls exceptionHandle. If it returns true, exceptionHandle needs to continue processing and is then thrown out to the business side for processing. If false is returned, the exception is eaten and does not need to be processed.

private void exceptionHandle(final Object proxy, final byte[] rawResult, finalClass<? > clazz,final Method method, final Object[] args,
                                 Annotation[] annotations, final RpcException exception,InnerRpcInvokeContext invokeContext)
            throws RpcException {
        boolean processed = handleAnnotations(annotations, new Handle() {
            @Override
            public boolean handle(RpcInterceptor rpcInterceptor, Annotation annotation)
                    throws RpcException {
                if (rpcInterceptor.exceptionHandle(proxy, RETURN_VALUE, rawResult, clazz, method,
                        args, exception, annotation)) {
                    LogCatUtil.error(TAG, exception + " need process");
                    // throw exception;
                    return true;
                } else {
                    LogCatUtil.error(TAG, exception + " need not process");
                    return false; }}});if (processed) {
            throwexception; }}Copy the code

2.10 Post-processing

After handling the exception, the Invoke method continues to execute, and the next step is to call postHandle for post-interception. The process is exactly the same as the previous interceptor, first going to the three default interceptors and then going to invokeContext to find the business custom interceptor, which currently has no implementation.

Handle preHandle, singeCall, exceptionHandle and postHandle. Invoke asyncNotifyRpcHeaderUpdateEvent will call to inform concerned with the Response headers of the Listener, then print the information, after the end of the entire process, request return results.

3. Default interceptor implementation

There are three default interceptors for three different annotations:

rpcService.addRpcInterceptor(CheckLogin.class, new LoginInterceptor());
rpcService.addRpcInterceptor(OperationType.class, new CommonInterceptor());
rpcService.addRpcInterceptor(UpdateDeviceInfo.class, new CtuInterceptor(mMicroAppContext.getApplicationContext()));
Copy the code

3.1 LoginInterceptor

The first LoginInterceptor, as its name suggests, is the interceptor that checks the login. Here we only implement the preHandle method:

public boolean preHandle(Object proxy, ThreadLocal<Object> retValue, byte[] retRawValue, Class<? > clazz, Method method, Object[] args, Annotation annotation,ThreadLocal<Map<String,Object>> extParams) throws RpcException {
        AuthService authService = AlipayApplication.getInstance().getMicroApplicationContext().getExtServiceByInterface(AuthService.class.getName());
        if(! authService.isLogin() && ! ActivityHelper.isBackgroundRunning()) {/ / not logged in
            LoggerFactory.getTraceLogger().debug("LoginInterceptor"."start login:" + System.currentTimeMillis());

            Bundle params = prepareParams(annotation);
            checkLogin(params);
            LoggerFactory.getTraceLogger().debug("LoginInterceptor"."finish login:" + System.currentTimeMillis());
            fail(authService);
        }
        return true;
    }
Copy the code

Check whether you are logged in:

  • If no login is performed, CLIENT_LOGIN_FAIL_ERROR = 11 is thrown.

3.2 CommonInterceptor

A CommonInterceptor interceptor intercepts the OperationType annotation. The value of this annotation is the name of the RPC request method, so you can see that the CommonInterceptor will handle all RPC requests.

public boolean preHandle(Object proxy, ThreadLocal<Object> retValue, byte[] retRawValue, Class<? > clazz, Method method, Object[] args, Annotation annotation, ThreadLocal<Map<String, Object>> extParams)
            throws RpcException { checkWhiteList(method, args); checkThrottle(); . writeMonitorLog(ACTION_STATUS_RPC_REQUEST, clazz, method, args);for (RpcInterceptor i : RpcCommonInterceptorManager.getInstance().getInterceptors()) {
            i.preHandle(proxy, retValue, retRawValue, clazz, method, args, annotation, extParams);
        }

        return true;
    }
Copy the code
  • Step 1: Check the whitelist:

During the first 3s of startup, only whitelisted requests can be sent to ensure performance. If not, CLIENT_NOTIN_WHITELIST = 17 is thrown

  • Step 2: Check whether the current is restricted:

Traffic limit is specified by the server. If traffic limit is specified on the server, the server returns a SERVER_INVOKEEXCEEDLIMIT=1002 exception on a request. In this case, the CommonInterceptor obtains the traffic limit expiration time from the return result

if(exception.getCode() == RpcException.ErrorCode.SERVER_INVOKEEXCEEDLIMIT){
            String control = exception.getControl();
            if(control! =null){
                mWrite.lock();
                try{
                    JSONObject jsonObject = new JSONObject(control);
                    if(jsonObject.getString("tag").equalsIgnoreCase("overflow")){
                        mThrottleMsg = exception.getMsg();
                        mControl = control;

                        // If it is an exception of OWN, update the end time of traffic limiting
                        if ( exception.isControlOwn() ){
                            mEndTime = System.currentTimeMillis()+jsonObject.getInt("waittime") *1000; }}}}Copy the code
  • Step 3: Write a monitoring log
writeMonitorLog(ACTION_STATUS_RPC_REQUEST, clazz, method, args);
Copy the code
  • Step 4: Handle the business custom interceptor
for (RpcInterceptor i : RpcCommonInterceptorManager.getInstance().getInterceptors()) {
            i.preHandle(proxy, retValue, retRawValue, clazz, method, args, annotation, extParams);
        }
Copy the code

We mentioned earlier that an interceptor in the RPC framework is bound to an annotation, for example CommonIntercetor is bound to the operatorType annotation. But if the business wants to customize interceptors for operatorType annotations, it needs to bind the list of interceptors under CommonIntercetor. It’s not currently implemented here, so you can ignore it.

Exception Interception exceptionHandle is used to handle exceptions in the result returned by the server. The service side can customize the exception based on the result returned by the server. For example, if your local session fails, the server returns SESSIONSTATUS_FAIL after requesting the result. After receiving this exception, the business side can handle it accordingly, for example, whether to use the locally saved account and password for automatic login, or pop up a login box to request user login, or directly return to the business side for processing, etc.

One thing the post-intercept postHandle does is log the results returned by the server.

3.3 CtuInterceptor

The CtuInterceptor interceptor corresponds to the UpdateDeviceInfo annotation. This annotation indicates that the RPC request requires device information. So the pre-intercept will write the device information into the request parameters.

        RpcDeviceInfo rpcDeviceInfo = new RpcDeviceInfo();
        DeviceInfo deviceInfo = DeviceInfo.getInstance();
        // Add some device information to deviceInfo.Copy the code

ExceptionHandle and postHandle were not handled.

These are the system’s default three interceptors, and the entire process in the MPAas-RPC Bundle. In fact, it seems that MPAAS-RPC is only responsible for the encapsulation and sending of network requests, the whole process is very simple. However, the real complex part is that the network request is processed differently according to different error codes, which is supposed to be handled by specific business parties.

However, conscience Alipay also provides a layer of encapsulation RPC-Beehive components, this layer is a layer of encapsulation between the network layer framework and the business side, some of the general exception code for processing, such as, the request is to switch chrysanthemum, or return abnormal display general exception interface.

The above is for MPAAS-RPC source code analysis, welcome to feedback ideas or suggestions, discuss corrections.

Past reading

The opening | modular and decoupling type development in ant gold mPaaS theorypractice probing depth,

Dependency Analysis Guide between Bundles of Word-of-mouth App

Follow our official account for first-hand mPaaS technology practices