之前进行filter层的cache操作时,并不能完成对后端调用的续命操作,即每次的缓存都是被动触发。当存在调用频率很高的请求时,如果能在缓存快过期时主动地触发后端的重新请求,那就能保证前端的请求始终均为命中缓存层,在性能上会有大大地提高。相应的策略如下:
这里面的第3步,并没有直接将旧有缓存进行重新设置ttl,而是再次请求。主要的考虑在于潜在的旧缓存可能不一致的问题,并且因为使用了异步调用,因此对实际上的前端访问是没有任何影响的。
本文的主要思路参考于(原文为nginx+lua,这里同样适用):
https://segmentfault.com/a/1190000003874328
本文的缓存工作点为Filter层,因此对后端的访问也同样起始于此点,可以理解为从此处继续后续的流程。但如果直接调用 FilterChain.doFilter,则会因为之前的请求已经结束了,此调用将直接报 类似 NPE这种错误。即一个 filterChain 不能即返回前端数据,同时又在新线程时继续原来的处理逻辑。
这里使用了spring-test 中的 mockFilterChain来完成相应的处理。整个思路来源于 AutoConfigureMockMvc 中创建起mockMvc的处理过程。
本文只考虑 GET 请求的处理.
参考的伪代码如下:
//构建起模拟请求 val newRequest = builder.buildRequest(getServletContext()); val newResponse = new MockHttpServletResponse(); //当前项目中绑定的servlet以及filter链, 以构建起调用链条 val bean = getMockHolder(); //模拟调用链 MockFilterChain newFilterChain = new MockFilterChain(bean.getDispatcherServlet(), bean.getFilterList().toArray(new Filter[0])); newFilterChain.doFilter(newRequest, newResponse);
上面的模拟调用链中, 会通过特定的标记标明这是一个更新缓存的请求,那么在重新执行整个调用链之后。会通过之前的逻辑触发缓存的save操作,这样即达到了更新缓存的目的.
在spring-test 中已经提供类 MockMvcRequestBuilders 来完成request的构建,我们只需要将以下信息赋值上去即可。关注信息如下:
以上信息均可以从原来的request中获取到,同时特殊属性根据实际情况直接绑定 requestAttribute. 如 代码 builder.requestAttr(CACHE_RENEW, “1”) 即设置特殊业务调用标识
模拟调用链依赖于项目中已有的filter列表以及servlet。在标识的Spring boot 项目中, DispatcherServlet即是统一的servlet入口,而其它的filter,则是通过注册 FilterRegistration 来完成。这意味着,一切均可以通过spring的容器拿到信息.
DispatcherServlet 对象由spring boot 项目初始化时自动注入到容器。因此,我们可以从容器中直接获取.
filter列表可通过对象 ServletContextInitializerBeans 迭代来获取. 从里面迭代 FilterRegistrationBean 即可。为什么可以拿到,可查看其源码查看工作机制.
这里面需要注意的是,filter地实际工作时有一个 pattern 的概念,即针对一个请求,并不是每个filter都要调用,只有命中 pattern 的请求才过filter。 从spring-test的参考代码中,可以查看到 PatternMappingFilterProxy 对象,从 FilterRegistrationBean 中可以拿到 pattern,根据其是否存在pattern 来决定是否构建 proxy 对象。默认情况下,无pattern即意味着过滤所有请求.
MockFilterChain 即模拟filter链的调用,即 filter从 first 到 last 依次调用,并且到最后1个时,触发servlet的调用。 这是一种典型的用列表模拟 链式调用的经典例子,并且在循环结束之后,即可直接触发servlet的调用。在spring-test 中,servlet的调用是通过伪装成filter来完成调用,效果一样.
在标准的spring mvc执行时,可以通过 RequestContextHolder.getRequestAttributes 来获取绑定在当前线程中的 request 和 response。 但这个信息并不是通过 filter 或 interceptor 来进行绑定的。而是通过 servlet 体系的 ServletRequestListener 来完成。此listener的工作模式早于filter,因此在实际处理时,我们还需要自己来完成这一步操作。参考于 RequestContextListener 实现,我们只需要在进行 filterChain 执行前,手动绑定 mockRequest, mockRespose即可。同时,在调用结束时,reset 来完成线程资源的释放。通过try finally 可以完成此次操作. 参考代码如下:
try{ RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(newRequest, newResponse)); filterChain.doFilter(newRequest, newResponse); } finally { RequestContextHolder.resetRequestAttributes(); }
至此,整个模拟调用结束。从整个调用上来看,可以理解为 一个请求信息经过web容器,项目路由,参数解析这些都已经完成了,按照正常的调用,它将继续调用 filter链,然后是 servlet, 在整个过程中,传递的都是 request/response 对象。 而模拟调用,就是通过构建一个与原来的request有相同作用的对象体系,然后再重新走一遍filter链,servlet即可。