Implementation approach

In the recent project, the distributed RPC framework is not used. The communication between various services goes through HTTP and is distributed to each instance through load balancing configured by NGINx to achieve the purpose of distributed communication.

The scheduling of HTTP between services resulted in a large number of RestTemplate calls in the code. In order to avoid difficult management in the subsequent development process, I implemented a simple RestInterface based on the DYNAMIC proxy of JDK to manage all HTTP requests in a manner similar to SpringMVC.

Step 1: Create several annotations

GET and POST look a lot like PostMapping and GetMapping for SpringMVC.

package com.tydic.config.restconfig;

import java.lang.annotation.*;

/ * * *@author Gmw
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface GET {

    String path(a) default "";
}

Copy the code
package com.tydic.config.restconfig;

import java.lang.annotation.*;

/ * * *@author Gmw
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Inherited
@Documented
public @interface POST {

    String path(a) default "";
}

Copy the code

These two annotations are responsible for annotating the RestInterface interface method to indicate whether the method is called using the POST or GET method.

I’m lazy so I just use RequestBody annotations. It doesn’t make much difference. All annotations can be used in Spring.

Then we create a PARAM for the parameters of the GET request:

package com.tydic.config.restconfig;

import java.lang.annotation.*;

/**
 * RequestParam
 * @author Gmw
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@Inherited
@Documented
public @interface PARAM {

    String value(a);
}

Copy the code

Step 2: HTTP call interfaceRestInterface

/** * HTTP requests * <p> * <b> Note: </b> * GET requests only carry path parameters; A POST request can only carry a request body (V1.0) * </p> * *@author Gmw
 */
public interface RestInterface {

    /** * Get user information ** from work order ID@paramParameter Request body *@returnReturn to the body * /
    @POST(path = "/wm/api/sysmgr/users/info")
    JSONObject queryUserInfo(@RequestBody JSONObject parameter);

    /** * get order update **@paramParameter Request body *@returnReturn to the body * /
    @POST(path = "/wm/api/worksheets/receipts/order/updates")
    JSONObject queryOrderUpdates(@RequestBody JSONObject parameter);

    /** * submit the order **@paramParameter Request body *@returnReturn to the body * /
    @POST(path = "/wm/api/orders/submit")
    JSONObject orderSubmit(@RequestBody JSONObject parameter);
}

Copy the code

The interface is simple, with annotations annotated, and a lot of annotations, to make it clear what each method call does.

Step 3: Implement dynamic proxy classes

The implementation of dynamic proxy is simple, just implement the InvocationHandler interface:

    private static class RestInterfaceImpl implements InvocationHandler {

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // ...}}Copy the code

The three parameters are mainly method and args, representing the interface method and method input instance of the current proxy.

All HTTP interfaces similar to the RestInterface interface can use this RestInterfaceImpl as a proxy implementation, because the proxy logic for each Rest interface is the same, parsing annotations, assembling request parameters, sending HTTP requests, and returning.

We then provide the implementation of this proxy class through Spring’s configuration class and encapsulate it as a Spring component:

@Configuration
public class RestInterfaceConfig {

    private final RestInterfaceImpl proxyImpl;

    public RestInterfaceConfig(
            @Value("${nginx.protocol}") String protocol,
            @Value("${nginx.host}") String host,
            @Value("${nginx.port}") String port,
            LogServiceImpl logService) {
        restInterface = new RestInterfaceImpl(protocol, host, port, logService);
    }

    @Bean(name = "restInterface")
    public RestInterface createRestInterface(a) {
        return (RestInterface) Proxy.newProxyInstance(
                // Use the interface to load the proxy class
                RestInterface.class.getClassLoader(),
                // Define the proxy class to implement several interfaces
                new Class[]{RestInterface.class},
                // Proxy class instanceproxyImpl); }}Copy the code

If you need a Bean provision that implements multiple Rest interfaces, you only need:

    @Bean(name = "anotherRestInterface")
    public RestInterface createAnotherRestInterface(a) {
        return (AnotherRestInterface) Proxy.newProxyInstance(
                // Use the interface to load the proxy class
                RestInterface.class.getClassLoader(),
                // Define the proxy class to implement several interfaces
                new Class[]{AnotherRestInterface.class},
                // Proxy class instance
                proxyImpl);
    }

/ / /...
Copy the code

The RestInterfaceImpl interface is thread-safe (no state is saved), so this proxy implementation can be shared by multiple beans.

The complete code

Here is the full RestInterfaceConfig code:

package com.tydic.config.restconfig;

import com.alibaba.fastjson.JSONObject;
import com.tydic.business.RestInterface;
import com.tydic.constant.Constants;
import com.tydic.log.LogServiceImpl;
import com.tydic.utils.BaseUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.lang.annotation.Annotation;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.stream.Collectors;

/ * * *@author Gmw
 */
@Slf4j
@Configuration
public class RestInterfaceConfig {

    private final RestInterfaceImpl restInterface;

    public RestInterfaceConfig(
            @Value("${nginx.protocol}") String protocol,
            @Value("${nginx.host}") String host,
            @Value("${nginx.port}") String port,
            LogServiceImpl logService) {
        restInterface = new RestInterfaceImpl(protocol, host, port, logService);
    }

    @Bean(name = "restInterface")
    public RestInterface createRestInterface(a) {
        return (RestInterface) Proxy.newProxyInstance(
                // Use the interface to load the proxy class
                RestInterface.class.getClassLoader(),
                // Define the proxy class to implement several interfaces
                new Class[]{RestInterface.class},
                // Proxy class instance
                restInterface);
    }

    private static class RestInterfaceImpl implements InvocationHandler {

        private final String restPrefix;

        private final LogServiceImpl logService;

        public RestInterfaceImpl(String protocol, String host, String port, LogServiceImpl logService) {
            this.logService = logService;
            restPrefix = String.format("%s://%s:%s", protocol, host, port);
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                RequestMetadata requestMetadata = getRequestMedataFromMethod(method, restPrefix);
                ParamWrapper paramWrapper = getParamFromMethodAndArgs(method.getParameters(), args);
                urlComplete(requestMetadata, paramWrapper.reqParams);
                RestTemplate restTemplate = BaseUtils.newRestTemplateInstance();
                ResponseEntity<String> response = restInvoke(restTemplate, requestMetadata, paramWrapper);
                return JSONObject.parseObject(response.getBody(), method.getReturnType());
            } catch (Exception e) {
                logService.writeErrorLog(e, method.getDeclaringClass(), method, args, Constants.KeyName.restInterface);
                throwe; }}private void urlComplete(RequestMetadata requestMetadata, Map<String, String> reqParams) {
            if(reqParams ! =null && !reqParams.isEmpty()) {
                String url = requestMetadata.getUrl() + ("?" + reqParams
                        .keySet().stream().map(key -> String.format("%s={%s}", key, key)).collect(Collectors.joining("&"))); requestMetadata.setUrl(url); }}private ResponseEntity<String> restInvoke(RestTemplate rest, RequestMetadata request, ParamWrapper params) {
            log.debug("Request to be sent soon:");
            log.debug("request: {}", request);
            log.debug("params: {}", params);
            if ("get".equals(request.getMethod())) {
                return rest.getForEntity(request.url, String.class, params.reqParams);
            }
            HttpEntity<Object> requestEntity = new HttpEntity<>(params.requestBody, request.getHeaders());
            return rest.exchange(request.url, HttpMethod.POST, requestEntity, String.class);
        }

        private ParamWrapper getParamFromMethodAndArgs(Parameter[] parameters, Object[] args) {
            ParamWrapper wrapper = new ParamWrapper();
            for (int index = 0; index < parameters.length; index++) {
                Parameter parameter = parameters[index];
                Object parameterValue = args[index];
                if (parameter.isAnnotationPresent(RequestBody.class)) {
                    wrapper.setRequestBodyType(parameter.getType());
                    if (parameter.getAnnotation(RequestBody.class).required() && parameterValue == null) {
                        // The request body cannot be empty
                        throw new IllegalArgumentException("Request body cannot be empty");
                    }
                    wrapper.setRequestBody(parameterValue);
                } else if (parameter.isAnnotationPresent(PARAM.class)) {
                    PARAM paramAnno = parameter.getAnnotation(PARAM.class);
                    if (paramAnno.required() && parameterValue == null) {
                        // Request parameters cannot be empty
                        throw new IllegalArgumentException("Request parameter [" + paramAnno.value() + "] cannot be empty"); } wrapper.setReqParams(paramAnno.value(), parameterValue); }}return wrapper;
        }

        private RequestMetadata getRequestMedataFromMethod(Method method, String prefix) {
            RequestMetadata metadata = new RequestMetadata();
            List<Annotation> methodAnnotations = Arrays.asList(method.getAnnotations());
            metadata.setUrl(findUrl(methodAnnotations, prefix));
            metadata.setMethod(findMethod(methodAnnotations));
            metadata.setHeaders(generateCommonHttpHeaders());
            return metadata;
        }

        private String findMethod(List<Annotation> methodAnnotations) {
            Annotation reqPathAnno = methodAnnotations
                    .stream().filter(anno -> anno instanceof POST || anno instanceof GET)
                    .findFirst().orElseThrow(() -> new RuntimeException("HTTP method definition not found"));
            return reqPathAnno instanceof POST ? "post" : "get";
        }

        private String findUrl(List<Annotation> methodAnnotations, String prefix) {
            Annotation reqPathAnno = methodAnnotations
                    .stream().filter(anno -> anno instanceof POST || anno instanceof GET)
                    .findFirst().orElseThrow(() -> new RuntimeException("HTTP path definition not found"));
            String path;
            if (reqPathAnno instanceof POST) {
                path = ((POST) reqPathAnno).path();
            } else {
                path = ((GET) reqPathAnno).path();
            }
            return prefix + (path.startsWith("/")? path :"/" + path);
        }

        private HttpHeaders generateCommonHttpHeaders(a) {
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
            headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));
            Optional.ofNullable(RequestContextHolder.getRequestAttributes())
                    .filter(requestAttributes -> ServletRequestAttributes.class.isAssignableFrom(requestAttributes.getClass()))
                    .map(requestAttributes -> ((ServletRequestAttributes) requestAttributes).getRequest())
                    .ifPresent(req -> {
                        headers.set(Constants.CustomHeader.WM_TENANT_ID, req.getHeader(Constants.CustomHeader.WM_TENANT_ID));
                        headers.set(Constants.CustomHeader.WM_APP_ID, req.getHeader(Constants.CustomHeader.WM_APP_ID));
                        headers.set(Constants.CustomHeader.WM_TOUCH_POINT, req.getHeader(Constants.CustomHeader.WM_TOUCH_POINT));
                    });
            return headers;
        }

        private static class RequestMetadata {
            private String url;
            private String method;
            private HttpHeaders headers;

            public String getUrl(a) {
                return url;
            }

            public void setUrl(String url) {
                this.url = url;
            }

            public String getMethod(a) {
                return method;
            }

            public void setMethod(String method) {
                this.method = method;
            }

            public HttpHeaders getHeaders(a) {
                return headers;
            }

            public void setHeaders(HttpHeaders headers) {
                this.headers = headers;
            }

            @Override
            public String toString(a) {
                return new ToStringBuilder(this)
                        .append("url", url)
                        .append("method", method)
                        .append("headers", headers) .toString(); }}private static class ParamWrapper {
            private Map<String, String> reqParams = new HashMap<>();
            privateClass<? > requestBodyType;private Object requestBody;

            public Map<String, String> getReqParams(a) {
                return reqParams;
            }

            public void setReqParams(Map<String, String> reqParams) {
                this.reqParams = reqParams;
            }

            publicClass<? > getRequestBodyType() {return requestBodyType;
            }

            public void setRequestBodyType(Class
        requestBodyType) {
                this.requestBodyType = requestBodyType;
            }

            public Object getRequestBody(a) {
                return requestBody;
            }

            public void setRequestBody(Object requestBody) {
                this.requestBody = requestBody;
            }

            public void setReqParams(String paramName, Object paramValue) {
                if(paramValue ! =null) { reqParams.put(paramName, paramValue.toString()); }}@Override
            public String toString(a) {
                return new ToStringBuilder(this)
                        .append("reqParams", reqParams)
                        .append("requestBodyType", requestBodyType)
                        .append("requestBody", requestBody) .toString(); }}}}Copy the code

conclusion

In this way, unified HTTP management is achieved. The first version only supports JSON serialization, many of which are written dead, such as header fetching, etc. Request methods also support only GET and POST, and only POST can carry the request body, and only GET can carry the request parameters. Find time to expand this later.