背景
web应用的项目:分表现层portal端和业务服务层service端,使用dubbo框架,rpc使用hessian。dubbo里的dubbo协议因为是socket长连接可以attachment隐式带参数,hessian协议则不能attachment参数。现有需求需要在每个服务调用都多传一个参数,于是考虑不在改动接口的情况下,也在hessian协议下进行隐式传参。
同事说hessian也是走http,可以在发起请求的时候把参数添加到http头信息,服务端接收的时候再解析出来。还有就是hack一下hessian的二进制协议在序列化和反序列化的时候做点手脚。于是源码看起、google百度相关的hessian源码搭配更快地看懂源码。途中就找在远在2007年的一个帖子:Hessian源码分析和Hack --让Hessian携带远程调用端的信息,里面就说到隐式传参的解决方案。所以本文不算原创。
方案1:使用http头信息传参
-
在Hessian客户端HessianProxy调用方法发送请求中添加http头信息:
public Object invoke(Object proxy, Method method, Object []args)
-->protected HessianConnection sendRequest(String methodName, Object []args)
-->protected void addRequestHeaders(HessianConnection conn)
里添加头信息protected void addRequestHeaders(HessianConnection conn) { conn.addHeader("Content-Type", "x-application/hessian"); conn.addHeader("Accept-Encoding", "deflate"); //从ThreadLocal里获取参数 Map<String,String> context=HessianContext.getContext(); if(context!=null) { for(Map.Entry<String, String> entry:context.entrySet()) conn.addHeader(entry.getKey(), entry.getValue()); } String basicAuth = _factory.getBasicAuth(); if (basicAuth != null) conn.addHeader("Authorization", basicAuth); }
2.在dubbo的HessianProtocol中,skeleton.invoke(request.getInputStream(), response.getOutputStream());
前提取http头信息
private class HessianHandler implements HttpHandler
{
public void handle(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
String uri = request.getRequestURI();
HessianSkeleton skeleton = skeletonMap.get(uri);
if (! request.getMethod().equalsIgnoreCase("POST")) {
response.setStatus(500);
} else {
RpcContext.getContext().setRemoteAddress(request.getRemoteAddr(), request.getRemotePort());
try {
Enumeration paramName=request.getHeaderNames();
String name;
Map<String,String> data=new HashMap<String, String>();
while(paramName.hasMoreElements())
{
name=(String)paramName.nextElement();
data.put(name, request.getHeader(name));
}
HessianContext.setContext(data);
skeleton.invoke(request.getInputStream(), response.getOutputStream());
HessianContext.removeContext();
} catch (Throwable e) {
throw new ServletException(e);
}
}
}
}
3.这种用法需要同时升级hessian和dubbo包,但是不破坏hessian协议可以和原有的hessian兼容。在portal端调用service端方法前后需要HessianContext.setContext(...)
和HessianContext.removeContext()
,service端用HessianContext.getContext()
获取参数。
方案2:hessian多序列化反序列化一个参数
-
hessian的HessianOutput和Hessian2Output在HessianProxy的sendRequest方法里用call方法拼接请求body。在call方法的拼接参数后再write一个参数
public void call(String method, Object []args) throws IOException { int length = args != null ? args.length : 0; startCall(method, length); for (int i = 0; i < length; i++) writeObject(args[i]); //再write一个参数 writeObject(HessianContext.getContext()); completeCall(); }
-
在HessianSkeleton里
invoke(Object service,AbstractHessianInput in,AbstractHessianOutput out)
里读取参数后再读取一个参数for (int i = 0; i < args.length; i++) { // XXX: needs Marshal object values[i] = in.readObject(args[i]); } HessianContext.setContext((Map<String, Object>) in.readObject()); Object result = null; try { result = method.invoke(service, values); } catch (Exception e) { Throwable e1 = e;
-
这个方案只需升级hessian,但是破坏hessian的二进制规则,不能兼容原有的hessian,需要所有项目都升级,且不能外部使用。
HessianContext
HessianContext用ThreadLocal存放参数,代码简单直接上代码:
public class HessianContext {
private static final ThreadLocal<HessianContext> _THREADLOCAL = new ThreadLocal<HessianContext>();
private Map<String,Object> data;
private HessianContext(){}
public static void setContext(Map<String,Object> data)
{
if(data==null)
return;
HessianContext context=_THREADLOCAL.get();
if(context==null)
{
context=new HessianContext();
_THREADLOCAL.set(context);
}
context.data=data;
}
public static Map<String,Object> getAndRemoveContext()
{
Map<String,Object> data=getContext();
removeContext();
return data;
}
public static Map<String,Object> getContext()
{
HessianContext context=_THREADLOCAL.get();
return context==null?null:context.data;
}
public static void removeContext()
{
_THREADLOCAL.remove();
}
}