Hessian与HTTP Header

    Recently, I was developing an RPC service, and I chose between various RPC frameworks. In the end, hessian was chosen. On the whole, hessian is not only simple and excellent, but also conforms to the concept of lightweight service governance of "microservices"; this The RPC framework is so good that there is almost no need to upgrade and expand... However, in actual development, I encountered a small problem, that is, how to send some "affiliated information", such as tokens, etc. through the hessian protocol (framework); these subsidiary Information, the content is small, but the number of entries may be large. If they are encapsulated into JAVA objects and transmitted through the API, it does introduce some scalability problems. Since the bottom layer of Hessian is based on the HTTP protocol, can these auxiliary information be passed through Header? After thinking and verification, it is not only possible, but also the best strategy; after that, if the client side needs to transmit more additional information, there is no need to upgrade the API Client, which is transparent and easy to use. At the same time, some information is transmitted through HEADER, which is also good for the WEB Proxy layer.

 

     Design requirements:

     1. For an auxiliary information, such as TOKEN, remarks and other auxiliary information, it cannot be transmitted through the hessian API.

     2. There may be a lot of auxiliary information, but it needs to support dynamic addition, and the hessian API should not be modified when adding, so as to bring compatibility problems to the Client.

     3. Performance issues need to be taken into account.

     4. Ancillary information is sent through HTTP Headers.

 

     After researching the Hessian source code, it is found that the Hessian request will add some identification information to the header by default, but the operation of addHeader is "closed", and other headers cannot be added through simple Spring configuration or Hessian API; in order to support this feature, we Need to extend the Hessian API, this example is based on the Spring container.

     1. We hope that Headers can be formulated through Spring configuration.

     2. We hope that during runtime, when Hessian requests are sent, developers can customize headers and send them through the Hessian protocol.

     3. The Hessian Remote Service should be able to parse the header and obtain the content of the header inside the Service for use.

     4. After the request and response are over, these headers should be Clear, that is, stateless.

 

     Design ideas:

     1) Those headers that can be specified by Spring configuration, we can achieve by extending Hessian FactoryBean, and save the headers specified in the configuration (usually global headers) in Spring Bean properties.

     2) Those headers that need to be dynamically added during runtime, in order to avoid coupling, we can use ThreadLocal method, save them in Context, and extend its addHeader method between the actual sending of Hessian request and add them to HTTP request middle.

     3) Hessian Remote Service, usually an HTTP Servlet instance, then we can parse these headers based on Fitler, which also requires Hessian headers to follow certain rules, such as headers that start with "x-rpc-hessian", etc.

     5) For the remote service in the Spring container, we can also extend the corresponding FactoryBean to parse these headers and place them in ThreadLocal, after which the content of these headers can be obtained during the execution of the Service.



 

 

1. HContext.java

    A Context class implemented based on ThreadLocal, used to save headers dynamically added by developers during runtime.

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by liuguanqing on 17/4/12.
 * If there is Chinese in the header, it needs to be sent after URLEncoder
 */
public class HContext {

    public static final String KEY_PREFIX = "x-rpc-";
    public static final String UTF8 = "utf-8";
    public static final String SOURCE_HOST = "source_host";
    public static final String REQUEST_TYPE = "request_type";
    private static final ThreadLocal<Map<String,String>> context = new InheritableThreadLocal<>();

    public static void init() {
        Map<String, String> model = context.get();
        if(model == null) {
            context.set(new HashMap<>());
        }
    }

    public static void put(String key,String value) {
        if(value == null) {
            return;
        }
        Map<String,String> model = context.get();
        if(model == null) {
            init();
        }
        if(!key.startsWith(KEY_PREFIX)) {
            key = KEY_PREFIX + key;
        }
        context.get().put(key,value);
    }

    public static void putAll(Map<String,String> kvs) {
        if(kvs == null || kvs.isEmpty()) {
            return;
        }
        for(Map.Entry<String,String> entry : kvs.entrySet()) {
            String value = entry.getValue();
            if(value == null) {
                continue;
            }
            put(entry.getKey(),value);
        }
    }

    public static String get(String key) {
        key = KEY_PREFIX + key;
        Map<String,String> model = context.get();
        if(model == null) {
            return null;
        }
        return model.get(key);
    }

    public static Map<String,String> getAll() {
        Map<String,String> model = context.get();
        if(model == null) {
            return Collections.EMPTY_MAP;
        }
        return model;
    }

    public static void clear() {
        context.remove();
    }
}

    It should be noted that if the headers contain Chinese, it needs to be sent after UrlEncoder, and UrlDecoder is required on the Service side.

 

2. HessianProxy extension class

    As we all know, the Hessian client must create a HessianProxy proxy class to make RPC calls. HessianProxy is created by JAVA dynamic proxy, which proxy Serivce API interface. HessianProxy is responsible for the actual RPC request and response processing. It always calls the internal addHeader method when sending a request, so we can focus on the addHeader method.

import com.caucho.hessian.client.HessianConnection;
import com.caucho.hessian.client.HessianProxy;
import com.caucho.hessian.client.HessianProxyFactory;

import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by liuguanqing on 17/4/12.
 *
 */
public class HessianWithContextProxy extends HessianProxy {

    protected Map<String,String> globalHeaders = new HashMap<>();

    public HessianWithContextProxy(URL url, HessianProxyFactory factory) {
        super(url, factory);
        HContext.init();
    }

    public HessianWithContextProxy(URL url, HessianProxyFactory factory, Class type) {
        super(url, factory, type);
        HContext.init();
    }

    public HessianWithContextProxy(URL url, HessianProxyFactory factory, Class type,Map<String,String> headers) {
        super(url, factory, type);
        HContext.init();
        globalHeaders = headers;
    }

    protected void addRequestHeaders(HessianConnection conn) {
        super.addRequestHeaders(conn);
        try {
            HContext.putAll(globalHeaders);//global headers cant be replaced!
            Map<String, String> context = HContext.getAll();
            for (Map.Entry<String, String> entry : context.entrySet()) {
                try {
                    String value = entry.getValue();
                    if (value == null) {
                        continue;
                    }
                    conn.addHeader(entry.getKey(), URLEncoder.encode(value, HContext.UTF8));
                } catch (Exception e) {
                    //
                }
            }
        } finally {
            HContext.clear();
            //after send,we clear at once.
            //must clear context here.
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return super.invoke(proxy,method,args);
    }
}

 

    For those global-level headers, that is, the headers declared by the Spring configuration file and common to all requests, we should save them inside the HessianProxy instance; for those headers that are dynamically added during runtime, we need to get them from HContext and put They are all added to the connection through the addHeaders method. Considering the life cycle management of the ThreadLocal container, we must clear it after the addHeaders method is called.

    When considering the timing of HContext.clear, I have introduced some problems. At first, I called clear before the invoke method returned. In fact, after testing, it was found that this was wrong. It should be noted that when the invoke method is executed, "any method that calls the API will be executed" (including toString, getClass, etc.), so the Client request may execute invoke multiple times before sending it. Those that are not API methods will cause HContext. Data is cleared. Therefore, we should perform HContext operations close to where the request is sent.

 

Three, HessianProxyFactoryBean extension

    HessianProxy is created by HessianProxyFactory. We can usually specify serviceUrl and other Hessian Client configuration information in this FactoryBean. Of course, we can extend it and support the configuration of some global-level headers. These headers are usually constants. This Service All requests of the Client can be generic; such as Token, etc.

import com.caucho.hessian.client.HessianProxyFactory;
import com.caucho.hessian.io.HessianRemoteObject;

import java.lang.reflect.Proxy;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by liuguanqing on 17/4/12.
 */
public class HessianWithContextProxyFactory extends HessianProxyFactory {

    private Map<String,String> globalHeaders = new HashMap<>();

    public HessianWithContextProxyFactory() {
        super();
    }

    public void setGlobalHeaders(Map<String, String> globalHeaders) {
        this.globalHeaders = globalHeaders;
    }

    public Map<String, String> getGlobalHeaders() {
        return globalHeaders;
    }

    @Override
    public Object create(Class api, URL url, ClassLoader loader) {
        if (api == null)
            throw new NullPointerException("api must not be null for HessianProxyFactory.create()");
        HessianWithContextProxy handler = new HessianWithContextProxy(url, this, api,globalHeaders);
        return Proxy.newProxyInstance(loader, new Class[]{api, HessianRemoteObject.class}, handler);
    }
}

 

    This extension class is relatively simple, just note that when creating an instance of HessianProxy, pass the configured global headers as parameters. Because only the HessianProxyFactory is visible to developers or the Spring framework, these parameters can only be configured through the FactoryBean and then passed to the HessianProxy.

 

4. ProxyFactoryBean extension based on Spring framework

    Spring supports the Hessian framework, which is implemented based on HessianProxyFactoryBean. This class name is similar to Hessian's original "HessianProxyFactory", but it is the factory bean of the Spring container, which is used to create a HessianProxyFactory instance of the singleton mode. Note that this class is for the Hessian Client!

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

/**
 * Created by liuguanqing on 17/4/12.
 */
public class HessianWithContextProxyFactoryBean extends HessianProxyFactoryBean {

    protected HessianWithContextProxyFactory proxyFactory = new HessianWithContextProxyFactory();

    protected Map<String,String> headers = new HashMap<>();

    public void setHeaders(Map<String, String> headers) {
        this.headers = headers;
    }

    private String localIp;


    public HessianWithContextProxyFactoryBean() {
        super();
        super.setProxyFactory(proxyFactory);//Force modification
    }

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        localIp = getLocalIp();//local IP address
        headers.put(HContext.SOURCE_HOST,localIp);
        proxyFactory.getGlobalHeaders().putAll(headers);//append
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        //all the methods of hessian instances will be invoked here,
        //so we cant use threadLocal there.
        return super.invoke(invocation);
    }

    public String getLocalIp() {
        try {
            //One host has multiple network interfaces
            Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
            while (netInterfaces.hasMoreElements ()) {
                NetworkInterface netInterface = netInterfaces.nextElement ();
                Enumeration<InetAddress> addresses = netInterface.getInetAddresses();
                while (addresses.hasMoreElements()) {
                    InetAddress address = addresses.nextElement();
                    if (address.isSiteLocalAddress() && !address.isLoopbackAddress()) {
                         return address.getHostAddress();

                    }
                }
            }
        }catch (Exception e) {
            //
        }
        return null;
    }
}

 

    There is nothing special, but we add a global header by default, which represents the local IP address of the client, which is mainly used for the Remote Service to track the source of the request.

 

5. Remote Service side

    

import org.springframework.remoting.caucho.HessianServiceExporter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;
import java.util.Enumeration;

/**
 * Created by liuguanqing on 17/4/12.
 *
 */
public class HessianWithContextServiceExporter extends HessianServiceExporter {

    @Override
    public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // handle the request
        try {
            Enumeration<String> headers = request.getHeaderNames();
            while (headers.hasMoreElements()) {
                String key = headers.nextElement();
                if(key.startsWith(HContext.KEY_PREFIX)) {
                    try {
                        String value = request.getHeader(key);
                        if(value != null) {
                            HContext.put(key, URLDecoder.decode(value,HContext.UTF8));
                        }
                    } catch (Exception e) {
                        //
                    }
                }
            }
            HContext.put(HContext.REQUEST_TYPE,"SDK");
            super.handleRequest(request,response);
        } finally {
            HContext.clear();
        }
    }


}

 

    HessianServiceExporter is a mechanism provided by Spring, that is, Spring Bean can be exposed and combined with the Servlet container (Spring-web). It has only one main method to process the request sent by the Client, then we can parse the request sent by the Client in this method. headers, and stored in the HContext.

 

6. How to use

   1. Client configuration

    <bean id="remotePortalService" class="com.demo.hessian.spring.HessianWithContextProxyFactoryBean">
        <property name="serviceUrl" value="${portal.service.url}" />
        <property name="serviceInterface" value="com.demo.service.PortalService" />
        <property name="overloadEnabled" value="true"/>
        <property name="connectTimeout" value="3000"/>
        <property name="hessian2" value="true"/>
        <property name="readTimeout" value="3000"/>
        <property name="headers">
            <map>
                <entry key="project" value="${portal.service.project}"/>
                <entry key="token" value="${.portal.service.token}" />
            </map>
        </property>
    </bean>

 

   2. Client-side JAVA code sample

HContext.put("comment","This is the Hessian service");
HContext.put("operator","zhangsan");
remotePortalService.send(target);
//No need to clear HContext, HessianProxy will execute automatically.

 

    3. Remote Service configuration (web.xml)

    <servlet>
        <servlet-name>hessianService</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring-hessian.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>hessianService</servlet-name>
        <url-pattern>/rpc/*</url-pattern>
    </servlet-mapping>

 

    spring-hessian.xml configuration

    <bean name="/portal" class="com.vipkid.utils.hessian.spring.HessianWithContextServiceExporter">
        <property name="service" ref="portalService"/>
        <property name="serviceInterface" value="com.demo.service.PortalService"/>
    </bean>

    You may also need to declare "portalService" in other configuration files, which is a Spring Bean. In addition, the URI path of your Spring Controller also needs to start with "/rpc", which is consistent with the web.xml configuration.

    4. Remote Service side JAVA example

public Object send(Object target) {
    String clientId = HContext.get("source_host");
    //HContext has been data prepared in Exportor, so it can be used directly without clear.
}

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326353976&siteId=291194637