修改响应
既然我们能通过Filter修改HttpServletRequest
,自然也能修改HttpServletResponse
,因为这两者都是接口。
我们来看一下在什么情况下我们需要修改HttpServletResponse
。
假设我们编写了一个Servlet,但由于业务逻辑比较复杂,处理该请求需要耗费很长的时间:
@WebServlet(urlPatterns = "/slow/hello")
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
最合适:
@WebFilter("/slow/*")
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修改响应 (推荐使用IDE练习插件快速下载)
小结
借助HttpServletResponseWrapper
,我们可以在Filter中实现对原始HttpServletResponse
的修改。