Hessian与HTTP Header

    最近在开发一个RPC服务,在多种RPC框架之间选来选取,最终还是选择了hessian,整体上来说,hessian不仅简单而且优秀,还符合“微服务”的轻量级服务治理的理念;这个RPC框架优秀到简直没有升级和扩展的必要... 不过在实际开发中,遇到了一个小小的问题,就是如何通过hessian协议(框架)来发送一些“附属信息”,比如token等;这些附属信息,内容较小,但是可能条目个数较多,如果都封装成JAVA对象通过API传送,确实引入一些扩展性的问题。既然hessian底层基于HTTP协议,这些附属信息能否通过Header传递呢? 经过思考和验证,不仅可以,而且这也是最佳的策略;此后Client端如果需要额外传递更多的附属信息,则不需要升级API Client,透明易用。同时通过HEADER传送一些信息,这对WEB Proxy层也是良好的。

     设计要求:

     1、对于一个附属信息,比如TOKEN、备注等附属信息,不能通过hessian API传送。

     2、附属信息可能很多,不过需要支持动态增加,增加时不应该修改hessian API,以便给Client带来兼容性问题。

     3、性能问题需要兼顾。

     4、附属信息通过HTTP Headers发送。

     经过研究Hessian源码,发现Hessian请求默认会在header中添加一些标识信息,不过addHeader的操作是“关闭的”,不能通过简单的Spring配置或者Hessian API额外的增加其他的header;为了支持此特性,我们需要扩展Hessian API,本实例基于Spring容器。

     1、我们希望Headers可以通过Spring配置方式制定。

     2、我们希望可以在runtime期间,Hessian请求发送时开发者可以自定headers并通过Hessian协议发送。

     3、Hessian Remote Service应该可以解析出header,并能够在Service内部获得header的内容,以便使用。

     4、请求响应结束后,这些headers应该被Clear,即无状态。

     设计思路:

     1)那些可以通过Spring配置指定的headers,我们可以通过扩展Hessian FactoryBean来实现,将配置中指定的header(通常为全局headers)保存在Spring Bean属性中。

     2)那些需要在runtime期间动态添加的headers,为了避免耦合,我们可以使用ThreadLocal方式,将它们保存在Context中,并在Hessian请求实际发送之间,扩展其addHeader方法,并将它们添加在HTTP 请求中。

     3)Hessian Remote Service,通常为HTTP Servlet实例,那么我们可以基于Fitler的方式解析这些headers,这也要求Hessian Headers需要遵循一定的规则,比如header均已“x-rpc-hessian”开头等。

     5)对于Spring容器中的remote service,我们也可以扩展相应的FactoryBean,来解析这些header,并放置在ThreadLocal中,此后Service执行过程中,就可以获取这些headers内容。



 

一、HContext.java

    一个基于ThreadLocal实现的Context类,用于保存runtime期间开发者动态添加的headers。

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.
 *  如果header中,有中文,需要进行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();
    }
}

    需要注意,如果headers中包含中文,需要进行UrlEncoder之后才能发送,对应在Service端则需要UrlDecoder。

二、HessianProxy扩展类

    大家都知道,Hessian客户端必须创建一个HessianProxy代理类,才能进行RPC调用。HessianProxy是通过JAVA动态代理方式创建,其代理了Serivce API的接口。HessianProxy负责进行实际的RPC请求和响应处理,它在发送请求时总会调用内部的addHeader方法,那么我们即可围绕addHeader方法开展。

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);
    }
}

    对于那些global级别的headers,即通过Spring配置文件声明的、所有请求都公用的headers,我们应该在保存在HessianProxy实例的内部;对于那些runtime期间动态增加的headers,我们需要从HContext中获取,并将它们均通过addHeaders方法添加到connection中。考虑到ThreadLocal的容器的生命周期管理,我们必须在addHeaders方法调用结束后,进行clear。

    在考虑HContext.clear的时机时,本人曾经引入过一些问题。起初,本人在invoke方法返回之前调用clear,事实上经过测试发现,这是错误的。需要注意,invoke方法的执行时机时“调用API的任何方法都会执行”(包括toString、getClass等等),所以Client请求在发送之前,可能多次执行invoke,那些不是API方法的执行,会导致HContext数据被清除。所以,我们应该在临近请求发送的地方进行HContext操作。

三、HessianProxyFactoryBean扩展

    HessianProxy是有HessianProxyFactory创建的,我们通常可以在此FactoryBean中指定serviceUrl等等一些Hessian Client的配置信息,当然我们可以扩展它,并支持配置一些global级别的headers,这些headers,通常是一些常量,此Service Client的所有请求都可以通用;比如Token等。

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);
    }
}

    这个扩展类比较简单,只需要注意,在创建HessianProxy实例时,将配置的global headers作为参数传递过去。因为面向开发者或者Spring框架时,只有HessianProxyFactory是可见的,所以这些参数只能通过FactoryBean配置然后传递给HessianProxy。

四、基于Spring框架的ProxyFactoryBean扩展

    Spring支持Hessian框架,它基于HessianProxyFactoryBean实现,这个类名与Hessian原始的“HessianProxyFactory”比较类似,不过它是Spring容器的工厂bean,用于创建单例模式的HessianProxyFactory实例。注意此类是面向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);//强制修改
    }

    @Override
    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        localIp = getLocalIp();//本地IP地址
        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 {
            //一个主机有多个网络接口
            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;
    }
}

    没有特别之处,只是我们默认添加了一个全局header,表示Client端的本机IP地址,主要用于Remote Service跟踪请求的来源。

五、Remote Service端

    

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 {
        //处理请求
        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是Spring提供的机制,即可以将Spring Bean暴露并与Servlet容器结合(Spring-web),它只有一个主要的方法,用于处理Client发送的请求,那么我们可以在此方法中解析Client发送的headers,并保存在HContext中。

六、使用方式

    1、Client端配置

    <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端JAVA代码样例

HContext.put("comment","这是Hessian服务");
HContext.put("operator","zhangsan");
remotePortalService.send(target);
//不需要对HContext进行clear,HessianProxy会自动执行。

    3、Remote Service端配置(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配置

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

    你可能还需要在其他配置文件中声明“portalService”,这是一个Spring Bean。此外,你的Spring Controller的URI路径也需要以“/rpc”开头,即与web.xml配置保持一致。

    4、Remote Service端JAVA示例

public Object send(Object target) {
    String clientId = HContext.get("source_host");
    //HContext已经在Exportor中进行了数据准备,所以可以直接使用,也不需要clear。
}

猜你喜欢

转载自shift-alt-ctrl.iteye.com/blog/2373965