最近在开发一个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。 }
相关推荐
Hessian 协议是一种轻量级的 remoting on http 工具,使用简单的方法提供了 RMI 的功能。采用的是二进制 RPC 协议,所以它很适合于发送二进制数据。Hessian 主要用作面向对象的消息通信。 Hessian 协议报文格式可以...
它定义了消息结构,包含Action、Envelope、Header和Body等元素,通过HTTP或HTTPS进行传输。SOAP允许服务提供商定义复杂的WSDL(Web Services Description Language)来描述服务接口,增强了服务的规范性和互操作性。...
通过在etcd服务注册实例中增加Label信息,实现了路径、body和header的适配,从而使得服务与基础设施层的数据平面以及控制平面之间的通信更加顺畅。 在实践中,有赞还探索了灰度控制系统,这是一种在线上小范围内...
序列化方面,Dubbo 2.6.0支持Hessian2、Fastjson等多种方式,用于将Java对象转换为网络传输的数据格式。 7. **监控(Monitor)** 监控模块用于收集服务调用的统计信息,帮助开发者分析服务性能和稳定性。`...
55links友情链接网址跟踪器,放在桌面,每次直接打开就可以访问55links友情链接交易平台,方便快捷。
AB PLC例程代码项目案例 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我!
moore_01_0909
FIBR English learning
AB PLC例程代码项目案例 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我!
OIF_IEEE802.3_liaison_19OCt09
做网络安全FTP内容的实验必备
nagarajan_01_1107
AB PLC例程代码项目案例 【备注】 1、该资源内项目代码都经过测试运行成功,功能ok的情况下才上传的,请放心下载使用!有问题请及时沟通交流。 2、适用人群:计算机相关专业(如计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网、自动化、电子信息等)在校学生、专业老师或者企业员工下载使用。 3、用途:项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 4、如果基础还行,或热爱钻研,亦可在此项目代码基础上进行修改添加,实现其他不同功能。 欢迎下载!欢迎交流学习!不清楚的可以私信问我!
mellitz_3cd_01_0318
PyQt6实战派 配套代码
陕西省省级非物质文化遗产经纬度数据统计表 统计内容包含以下字段: 1. 项目名称 2. 遗产类别 3. 入选批次 4. 所属地区 5. 申报地区/单位 6. 地理经度 7. 地理纬度 该统计表系统记录了陕西省省级非物质文化遗产的地理空间信息,为文化遗产的数字化保护与研究工作提供了重要的数据支撑。
ran_3ck_02a_0918
毕业设计_基于springboot+vue开发的汽车租赁管理系统【源码+sql+可运行】【50308】.zip 全部代码均可运行,亲测可用,尽我所能,为你服务; 1.代码压缩包内容 代码:springboo后端代码+vue前端页面代码; 脚本:数据库SQL脚本 效果图:运行结果请看资源详情效果图 2.环境准备: - JDK1.8+ - maven3.6+ - nodejs14+ - mysql5.6+ - redis 3.技术栈 - 后台:springboot+mybatisPlus+Shiro - 前台:vue+iview+Vuex+Axios - 开发工具: idea、navicate 4.功能列表 - 系统设置:用户管理、角色管理、资源管理、系统日志 - 业务管理:汽车管理、客户管理、租赁订单 3.运行步骤: 步骤一:修改数据库连接信息(ip、port修改) 步骤二:找到启动类xxxApplication启动 4.若不会,可私信博主!!!
# Runcorder - 跑步训练管理系统 Runcorder 是一款专为跑步爱好者、马拉松运动员及高校体育生设计的本地化跑步训练管理工具,基于 Python 开发,结合 Tkinter 图形界面与强大的数据处理能力,为用户提供从训练记录到数据分析的全方位支持。无论是初学者还是专业跑者,Runcorder 都能帮助你科学规划训练、精准追踪进度,并通过可视化图表直观呈现训练成果,让你的跑步训练更智能、更高效! - **多用户管理**:支持创建、加载和删除用户档案,每个用户的数据独立存储,确保隐私与安全。 - **科学训练记录**:全维度记录跑步数据,包括日期、里程、配速、自评和晨跑标记,支持智能输入校验,避免数据错误。 - **多维数据分析**:通过动态可视化图表展示跑步里程趋势、平均配速曲线,支持自定义 Y 轴范围,帮助用户深入理解训练效果。 - **高阶功能**:提供 4 种科学训练模式(有氧/无氧/混合),支持历史记录修改与删除,数据以 JSON 格式持久化存储,跨平台兼容。
paatzsch_01_0708