1、背景:
公司基础设施部门推出了自己的微服务框架(以下简称M),要求所有业务应用都要接入进去,但坑爹的是M只提供了SpringMVC工程的support,对于采用Stripes作为MVC框架的应用并不支持,所以就必须重构这个Stripes应用。
M虽然代替Tomcat自己实现了IO,工作线程池,服务注册发现等等,但还是提供了对Servlet规范的简单支持(移除了web.xml,filter,listener, 打包方式也改成了tar.gz,保留了ServletContext,request,response等等)。
2、思路与分析:
由于现有应用的业务逻辑还是比较复杂(沉淀了3,4年),且涉及到多个APP端和其他第三方的交互,所以如果想对应用做完全的重构是一件非常头痛的事情,不仅要投入大量的后端,测试等人力成本,而且整个重构周期也会比较长,影响到对其他需求的响应。
于是在想:既然SpringMVC和Stripes都遵循Servlet规范,而M又能支持SpringMVC,是否可以通过SpringMVC拦截到所有请求后,把之前的老接口的请求转发给Stripes处理,以后新开发的接口就直接走SpringMVC,这样既兼容了公司的M框架也对原有应用的改动较小,开发周期短,测试人员只需要验证一些系统边界和主要流程,不用再挨个挨个验证已有的业务逻辑,这样可以节省大量人力成本,缩短开发周期。
SpringMVC是目前最流行的MVC框架,是Spring庞大家族的一员,而Stripes也是一个非常轻量的MVC框架,但是SpringMVC的Controller跟Stripes的ActionBean有个很明显的区别就是前者是单例的,跟状态无关,所有的请求参数都是封装在ServletRequest中的,而后者是prototype,每次请求都会重新生成一个ActionBean对象。
Spring有很强的可扩展性,SpringMVC也不例外,下面简单列了一下扩展的几个思路:
1.扩展mvc名称空间:扩展RequestMappingHandlerAdapter等
优点是:可以将整个请求的处理流程纳入到SpringMVC
缺点是:侵入性较大,SpringMVC版本升级后需要再做适配,技术实现比较麻烦
2.扩展DispatcherServlet:重写onRefresh或者doService等
M的spring-support是直接创建DispatcherServlet的,需要M的同事开回调入口,不太现实
3.增加拦截器,没有侵入性,不影响后续SpringMVC升级版本等
缺点是扩展性不是很好,不过对桥接需求来说已经完全足够
4.自定义Handlermapping,没有侵入性,理论上可以保证现有拦截器不需要改造,
但是会导致SpringMVC的拦截器和Stripes的拦截器定义两套,后期维护麻烦(两个地方都需要修改)
经过对比和讨论,我选择了第三点,新增一个SpringMVC拦截器,下图是一个简单的流程示意图:
3、动手实现原型:
新建桥接工程XXX-bridge,所有新增代码均放在新工程,对老工程零侵入(老板特别强调,如果桥接失败了,一定不能影响现在核心工程,要能马上回退回去),所以老工程仅新增了pom文件里的两个plugin,可以实现Stripes和SpringMVC两种框架模式下的并行开发,(原来的老工程持续有需求进来)。
/**
* 拦截所有请求,如果能映射到Stripes则直接派发给Stripes并return false,如果不能映射则回退给SpringMVC
*/
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
if (handlerMapping.isEmpty()) {
return true;
}
String url = httpServletRequest.getRequestURI();
String contextPath = httpServletRequest.getContextPath();
url = url.replace(contextPath, "").replaceAll("/+", "/");
LOGGER.info("当前请求路径: {}", url);
if (!StripesContextHandlerInterceptor.handlerMapping.containsKey(url)) {
LOGGER.info("请求由SpringMVC处理: {}", url);
return true;
}
try {
LOGGER.info("请求由Stripes处理: {}", url);
StripesContextHolder.doService(httpServletRequest.getServletContext(), getApplicationContext(),
httpServletRequest, httpServletResponse);
return false;
} catch (Exception e) {
LOGGER.error("处理请求异常:", e);
return false;
}
}
关键点在于要解析到所有的老接口地址,然后通过拦截器来判断当前请求是否是老接口,是的话就转发给Stripes来处理,其他详细的代码托管在github,欢迎访问。
https://github.com/hiccup234/web-advance/tree/master/bridge
4、问题与验证:
1、因为初始化Stripes需要 ServletContext,但是应用启动时却没办法获取到 ServletContext 对象,所以在 StripesContextHolder 里做了懒加载,只有当第一个请求进来后才加载和初始化Stripes框架。
2、Stripes是可以支持queryString为空的查询的,类似:http://127.0.0.1/server/test?list 而M会在解析请求参数的时候对这种情况直接抛异常,经过与基础设施同事协商,他们做出让步,对这种请求的情况直接放行给应用,从而避免了前端和第三方做改造。
今晚上线成功,目前跟Tomcat并行发布,5%的流量灰度,观察日志一切正常。前期的技术验证加上近两周的开发联调到今天的上线,算是向着架构的方向迈出了小小一步(同时:也感谢测试同学的自动化,让验证和回归省去了很多工作量)