修改响应

既然我们能通过Filter修改HttpServletRequest,自然也能修改HttpServletResponse,因为这两者都是接口。

我们来看一下在什么情况下我们需要修改HttpServletResponse

假设我们编写了一个Servlet,但由于业务逻辑比较复杂,处理该请求需要耗费很长的时间:

  1. @WebServlet(urlPatterns = "/slow/hello")
  2. public class HelloServlet extends HttpServlet {
  3. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  4. resp.setContentType("text/html");
  5. // 模拟耗时1秒:
  6. try {
  7. Thread.sleep(1000);
  8. } catch (InterruptedException e) {
  9. }
  10. PrintWriter pw = resp.getWriter();
  11. pw.write("<h1>Hello, world!</h1>");
  12. pw.flush();
  13. }
  14. }

好消息是每次返回的响应内容是固定的,因此,如果我们能使用缓存将结果缓存起来,就可以大大提高Web应用程序的运行效率。

缓存逻辑最好不要在Servlet内部实现,因为我们希望能复用缓存逻辑,所以,编写一个CacheFilter最合适:

  1. @WebFilter("/slow/*")
  2. public class CacheFilter implements Filter {
  3. // Path到byte[]的缓存:
  4. private Map<String, byte[]> cache = new ConcurrentHashMap<>();
  5. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
  6. throws IOException, ServletException {
  7. HttpServletRequest req = (HttpServletRequest) request;
  8. HttpServletResponse resp = (HttpServletResponse) response;
  9. // 获取Path:
  10. String url = req.getRequestURI();
  11. // 获取缓存内容:
  12. byte[] data = this.cache.get(url);
  13. resp.setHeader("X-Cache-Hit", data == null ? "No" : "Yes");
  14. if (data == null) {
  15. // 缓存未找到,构造一个伪造的Response:
  16. CachedHttpServletResponse wrapper = new CachedHttpServletResponse(resp);
  17. // 让下游组件写入数据到伪造的Response:
  18. chain.doFilter(request, wrapper);
  19. // 从伪造的Response中读取写入的内容并放入缓存:
  20. data = wrapper.getContent();
  21. cache.put(url, data);
  22. }
  23. // 写入到原始的Response:
  24. ServletOutputStream output = resp.getOutputStream();
  25. output.write(data);
  26. output.flush();
  27. }
  28. }

实现缓存的关键在于,调用doFilter()时,我们不能传入原始的HttpServletResponse,因为这样就会写入Socket,我们也就无法获取下游组件写入的内容。如果我们传入的是“伪造”的HttpServletResponse,让下游组件写入到我们预设的ByteArrayOutputStream,我们就“截获”了下游组件写入的内容,于是,就可以把内容缓存起来,再通过原始的HttpServletResponse实例写入到网络。

这个CachedHttpServletResponse实现如下:

  1. class CachedHttpServletResponse extends HttpServletResponseWrapper {
  2. private boolean open = false;
  3. private ByteArrayOutputStream output = new ByteArrayOutputStream();
  4. public CachedHttpServletResponse(HttpServletResponse response) {
  5. super(response);
  6. }
  7. // 获取Writer:
  8. public PrintWriter getWriter() throws IOException {
  9. if (open) {
  10. throw new IllegalStateException("Cannot re-open writer!");
  11. }
  12. open = true;
  13. return new PrintWriter(output, false, StandardCharsets.UTF_8);
  14. }
  15. // 获取OutputStream:
  16. public ServletOutputStream getOutputStream() throws IOException {
  17. if (open) {
  18. throw new IllegalStateException("Cannot re-open output stream!");
  19. }
  20. open = true;
  21. return new ServletOutputStream() {
  22. public boolean isReady() {
  23. return true;
  24. }
  25. public void setWriteListener(WriteListener listener) {
  26. }
  27. // 实际写入ByteArrayOutputStream:
  28. public void write(int b) throws IOException {
  29. output.write(b);
  30. }
  31. };
  32. }
  33. // 返回写入的byte[]:
  34. public byte[] getContent() {
  35. return output.toByteArray();
  36. }
  37. }

可见,如果我们想要修改响应,就可以通过HttpServletResponseWrapper构造一个“伪造”的HttpServletResponse,这样就能拦截到写入的数据。

修改响应时,最后不要忘记把数据写入原始的HttpServletResponse实例。

这个CacheFilter同样是一个“可插拔”组件,它是否启用不影响Web应用程序的其他组件(Filter、Servlet)。

练习

修改响应 - 图1下载练习:通过Filter修改响应 (推荐使用IDE练习插件快速下载)

小结

借助HttpServletResponseWrapper,我们可以在Filter中实现对原始HttpServletRequest的修改。

读后有收获可以支付宝请作者喝咖啡,读后有疑问请加微信群讨论:

修改响应 - 图2 修改响应 - 图3