共计 2827 个字符,预计需要花费 8 分钟才能阅读完成。
既然我们能通过 Filter 修改HttpServletRequest
,自然也能修改HttpServletResponse
,因为这两者都是接口。
我们来看一下在什么情况下我们需要修改HttpServletResponse
。
假设我们编写了一个 Servlet,但由于业务逻辑比较复杂,处理该请求需要耗费很长的时间:
public class HelloServlet extends HttpServlet {protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {resp.setContentType("text/html"); | |
// 模拟耗时 1 秒: | |
try {Thread.sleep(1000); | |
} catch (InterruptedException e) { } | |
PrintWriter pw = resp.getWriter(); | |
pw.write("<h1>Hello, world!</h1>"); | |
pw.flush();} | |
} |
好消息是每次返回的响应内容是固定的,因此,如果我们能使用缓存将结果缓存起来,就可以大大提高 Web 应用程序的运行效率。
缓存逻辑最好不要在 Servlet 内部实现,因为我们希望能复用缓存逻辑,所以,编写一个 CacheFilter
最合适:
public class CacheFilter implements Filter {// Path 到 byte[]的缓存: | |
private Map<String, byte[]> cache = new ConcurrentHashMap<>(); | |
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) | |
throws IOException, ServletException {HttpServletRequest req = (HttpServletRequest) request; | |
HttpServletResponse resp = (HttpServletResponse) response; | |
// 获取 Path: | |
String url = req.getRequestURI(); | |
// 获取缓存内容: | |
byte[] data = this.cache.get(url); | |
resp.setHeader("X-Cache-Hit", data == null ? "No" : "Yes"); | |
if (data == null) {// 缓存未找到, 构造一个伪造的 Response: | |
CachedHttpServletResponse wrapper = new CachedHttpServletResponse(resp); | |
// 让下游组件写入数据到伪造的 Response: | |
chain.doFilter(request, wrapper); | |
// 从伪造的 Response 中读取写入的内容并放入缓存: | |
data = wrapper.getContent(); | |
cache.put(url, data); | |
} | |
// 写入到原始的 Response: | |
ServletOutputStream output = resp.getOutputStream(); | |
output.write(data); | |
output.flush();} | |
} |
实现缓存的关键在于,调用 doFilter()
时,我们不能传入原始的 HttpServletResponse
,因为这样就会写入 Socket,我们也就无法获取下游组件写入的内容。如果我们传入的是“伪造”的HttpServletResponse
,让下游组件写入到我们预设的ByteArrayOutputStream
,我们就“截获”了下游组件写入的内容,于是,就可以把内容缓存起来,再通过原始的HttpServletResponse
实例写入到网络。
这个 CachedHttpServletResponse
实现如下:
class CachedHttpServletResponse extends HttpServletResponseWrapper {private boolean open = false; | |
private ByteArrayOutputStream output = new ByteArrayOutputStream(); | |
public CachedHttpServletResponse(HttpServletResponse response) {super(response); | |
} | |
// 获取 Writer: | |
public PrintWriter getWriter() throws IOException {if (open) {throw new IllegalStateException("Cannot re-open writer!"); | |
} | |
open = true; | |
return new PrintWriter(output, false, StandardCharsets.UTF_8); | |
} | |
// 获取 OutputStream: | |
public ServletOutputStream getOutputStream() throws IOException {if (open) {throw new IllegalStateException("Cannot re-open output stream!"); | |
} | |
open = true; | |
return new ServletOutputStream() {public boolean isReady() {return true; | |
} | |
public void setWriteListener(WriteListener listener) { } | |
// 实际写入 ByteArrayOutputStream: | |
public void write(int b) throws IOException {output.write(b); | |
} | |
}; | |
} | |
// 返回写入的 byte[]: | |
public byte[] getContent() {return output.toByteArray();} | |
} |
可见,如果我们想要修改响应,就可以通过 HttpServletResponseWrapper
构造一个“伪造”的HttpServletResponse
,这样就能拦截到写入的数据。
修改响应时,最后不要忘记把数据写入原始的 HttpServletResponse
实例。
这个 CacheFilter
同样是一个“可插拔”组件,它是否启用不影响 Web 应用程序的其他组件(Filter、Servlet)。
练习
通过 Filter 修改响应。
下载练习
小结
借助 HttpServletResponseWrapper
,我们可以在 Filter 中实现对原始HttpServletResponse
的修改。