如需转载,请根据 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 许可,附上本文作者及链接。
本文作者: 执笔成念
作者昵称: zbcn
本文链接: https://1363653611.github.io/zbcn.github.io/2020/10/13/web_03Filter%E5%92%8CListener/
Filter
Filter可以对请求进行预处理,因此,我们可以把很多公共预处理逻辑放到Filter中完成。
需求
我们在Web应用中经常需要处理用户上传文件,例如,一个UploadServlet可以简单地编写如下:
1 | "/upload/file") ( |
2 | public class UploadServlet extends HttpServlet { |
3 | |
4 | public void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { |
5 | // 读取Request Body: |
6 | InputStream input = req.getInputStream(); |
7 | ByteArrayOutputStream output = new ByteArrayOutputStream(); |
8 | byte[] buffer = new byte[1024]; |
9 | for (;;) { |
10 | int len = input.read(buffer); |
11 | if (len == -1) { |
12 | break; |
13 | } |
14 | output.write(buffer, 0, len); |
15 | } |
16 | // TODO: 写入文件: |
17 | // 显示上传结果: |
18 | String uploadedText = output.toString(StandardCharsets.UTF_8.name()); |
19 | PrintWriter pw = resp.getWriter(); |
20 | pw.write("<h1>Uploaded:</h1>"); |
21 | pw.write("<pre><code>"); |
22 | pw.write(uploadedText); |
23 | pw.write("</code></pre>"); |
24 | pw.flush(); |
25 | } |
26 | } |
但是要保证文件上传的完整性怎么办?
如果在上传文件的同时,把文件的哈希也传过来,服务器端做一个验证,就可以确保用户上传的文件一定是完整的。
这个验证逻辑非常适合写在
ValidateUploadFilter
中,因为它可以复用。
1 | /** |
2 | * 自定义过滤器 |
3 | * <br/> |
4 | * @author zbcn8 |
5 | * @since 2020/10/3 14:20 |
6 | */ |
7 | "/upload/*") ( |
8 | public class ValidateUploadFilter implements Filter { |
9 | |
10 | public void init(FilterConfig filterConfig) throws ServletException { |
11 | |
12 | } |
13 | |
14 | |
15 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { |
16 | |
17 | HttpServletRequest req = (HttpServletRequest) request; |
18 | HttpServletResponse resp = (HttpServletResponse) response; |
19 | // 获取客户端传入的签名方法和签名: |
20 | String digest = req.getHeader("Signature-Method"); |
21 | String signature = req.getHeader("Signature"); |
22 | if (digest == null || digest.isEmpty() || signature == null || signature.isEmpty()) { |
23 | sendErrorPage(resp, "Missing signature."); |
24 | return; |
25 | } |
26 | // 读取Request的Body并验证签名: |
27 | MessageDigest md = getMessageDigest(digest); |
28 | InputStream input = new DigestInputStream(request.getInputStream(), md); |
29 | ByteArrayOutputStream output = new ByteArrayOutputStream(); |
30 | byte[] buffer = new byte[1024]; |
31 | for (;;) { |
32 | int len = input.read(buffer); |
33 | if (len == -1) { |
34 | break; |
35 | } |
36 | output.write(buffer, 0, len); |
37 | } |
38 | String actual =toHexString(md.digest()); |
39 | if (!signature.equals(actual)) { |
40 | sendErrorPage(resp, "Invalid signature."); |
41 | return; |
42 | } |
43 | // 验证成功后继续处理: |
44 | //filterChain.doFilter(request, response); |
45 | //ReReadableHttpServletRequest的构造方法,它保存了ValidateUploadFilter读取的byte[]内容,并在调用getInputStream()时通过byte[]构造了一个新的ServletInputStream。 |
46 | //然后,我们在ValidateUploadFilter中,把doFilter()调用时传给下一个处理者的HttpServletRequest替换为我们自己“伪造”的ReReadableHttpServletRequest |
47 | filterChain.doFilter(new ReReadableHttpServletRequest(req, output.toByteArray()), response); |
48 | } |
49 | |
50 | |
51 | public void destroy() { |
52 | |
53 | } |
54 | |
55 | // 将byte[]转换为hex string: |
56 | private String toHexString(byte[] digest) { |
57 | StringBuilder sb = new StringBuilder(); |
58 | for (byte b : digest) { |
59 | sb.append(String.format("%02x", b)); |
60 | } |
61 | return sb.toString(); |
62 | } |
63 | |
64 | // 根据名称创建MessageDigest: |
65 | private MessageDigest getMessageDigest(String name) throws ServletException { |
66 | try { |
67 | return MessageDigest.getInstance(name); |
68 | } catch (NoSuchAlgorithmException e) { |
69 | throw new ServletException(e); |
70 | } |
71 | } |
72 | |
73 | // 发送一个错误响应: |
74 | private void sendErrorPage(HttpServletResponse resp, String errorMessage) throws IOException { |
75 | resp.setStatus(HttpServletResponse.SC_BAD_REQUEST); |
76 | PrintWriter pw = resp.getWriter(); |
77 | pw.write("<html><body><h1>"); |
78 | pw.write(errorMessage); |
79 | pw.write("</h1></body></html>"); |
80 | pw.flush(); |
81 | } |
82 | } |
UploadServlet 无法获取到流数据
ValidateUploadFilter
对签名进行验证的逻辑是没有问题的,但是,细心的童鞋注意到,UploadServlet
并未读取到任何数据!
这里的原因是对HttpServletRequest
进行读取时,只能读取一次。如果Filter调用getInputStream()
读取了一次数据,后续Servlet处理时,再次读取,将无法读到任何数据。怎么办?
这个时候,我们需要一个“伪造”的HttpServletRequest
,具体做法是使用代理模式,对getInputStream()
和getReader()
返回一个新的流:
1 | class ReReadableHttpServletRequest extends HttpServletRequestWrapper { |
2 | private byte[] body; |
3 | private boolean open = false; |
4 | |
5 | public ReReadableHttpServletRequest(HttpServletRequest request, byte[] body) { |
6 | super(request); |
7 | this.body = body; |
8 | } |
9 | |
10 | // 返回InputStream: |
11 | public ServletInputStream getInputStream() throws IOException { |
12 | if (open) { |
13 | throw new IllegalStateException("Cannot re-open input stream!"); |
14 | } |
15 | open = true; |
16 | return new ServletInputStream() { |
17 | private int offset = 0; |
18 | |
19 | public boolean isFinished() { |
20 | return offset >= body.length; |
21 | } |
22 | |
23 | public boolean isReady() { |
24 | return true; |
25 | } |
26 | |
27 | public void setReadListener(ReadListener listener) { |
28 | } |
29 | |
30 | public int read() throws IOException { |
31 | if (offset >= body.length) { |
32 | return -1; |
33 | } |
34 | int n = body[offset] & 0xff; |
35 | offset++; |
36 | return n; |
37 | } |
38 | }; |
39 | } |
40 | |
41 | // 返回Reader: |
42 | public BufferedReader getReader() throws IOException { |
43 | if (open) { |
44 | throw new IllegalStateException("Cannot re-open reader!"); |
45 | } |
46 | open = true; |
47 | return new BufferedReader(new InputStreamReader(getInputStream(), "UTF-8")); |
48 | } |
49 | } |
ReReadableHttpServletRequest
的构造方法,它保存了ValidateUploadFilter
读取的byte[]
内容,并在调用getInputStream()
时通过byte[]
构造了一个新的ServletInputStream
。
然后,我们在ValidateUploadFilter
中,把doFilter()
调用时传给下一个处理者的HttpServletRequest
替换为我们自己“伪造”的ReReadableHttpServletRequest
:
1 | public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) |
2 | throws IOException, ServletException { |
3 | ... |
4 | chain.doFilter(new ReReadableHttpServletRequest(req, output.toByteArray()), response); |
5 | } |
编写ReReadableHttpServletRequest
时,是从HttpServletRequestWrapper
继承,而不是直接实现HttpServletRequest
接口。这是因为,Servlet的每个新版本都会对接口增加一些新方法,从HttpServletRequestWrapper
继承可以确保新方法被正确地覆写了,因为HttpServletRequestWrapper
是由Servlet的jar包提供的,目的就是为了让我们方便地实现对HttpServletRequest
接口的代理。
总结一下对HttpServletRequest
接口进行代理的步骤:
- 从
HttpServletRequestWrapper
继承一个XxxHttpServletRequest
,需要传入原始的HttpServletRequest
实例; - 覆写某些方法,使得新的
XxxHttpServletRequest
实例看上去“改变”了原始的HttpServletRequest
实例; - 在
doFilter()
中传入新的XxxHttpServletRequest
实例
总结
借助HttpServletRequestWrapper
,我们可以在Filter中实现对原始HttpServletRequest
的修改。
Listener
除了Servlet和Filter外,JavaEE的Servlet规范还提供了第三种组件:Listener。Listener顾名思义就是监听器,有好几种Listener,其中最常用的是ServletContextListener
,我们编写一个实现了ServletContextListener
接口的类如下:
1 |
|
2 | public class AppListener implements ServletContextListener { |
3 | // 在此初始化WebApp,例如打开数据库连接池等: |
4 | public void contextInitialized(ServletContextEvent sce) { |
5 | System.out.println("WebApp initialized."); |
6 | } |
7 | |
8 | // 在此清理WebApp,例如关闭数据库连接池等: |
9 | public void contextDestroyed(ServletContextEvent sce) { |
10 | System.out.println("WebApp destroyed."); |
11 | } |
12 | } |
任何标注为@WebListener
,且实现了特定接口的类会被Web服务器自动初始化。上述AppListener
实现了ServletContextListener
接口,它会在整个Web应用程序初始化完成后,以及Web应用程序关闭后获得回调通知。
我们可以把初始化数据库连接池等工作放到contextInitialized()
回调方法中,把清理资源的工作放到contextDestroyed()
回调方法中,因为Web服务器保证在contextInitialized()
执行后,才会接受用户的HTTP请求。
很多第三方Web框架都会通过一个ServletContextListener
接口初始化自己。
除了ServletContextListener
外,还有几种Listener:
- HttpSessionListener:监听HttpSession的创建和销毁事件;
- ServletRequestListener:监听ServletRequest请求的创建和销毁事件;
- ServletRequestAttributeListener:监听ServletRequest请求的属性变化事件(即调用
ServletRequest.setAttribute()
方法); - ServletContextAttributeListener:监听ServletContext的属性变化事件(即调用
ServletContext.setAttribute()
方法);
ServletContext
一个Web服务器可以运行一个或多个WebApp,对于每个WebApp,Web服务器都会为其创建一个全局唯一的ServletContext
实例,我们在AppListener
里面编写的两个回调方法实际上对应的就是ServletContext
实例的创建和销毁:
1 | public void contextInitialized(ServletContextEvent sce) { |
2 | System.out.println("WebApp initialized: ServletContext = " + sce.getServletContext()); |
3 | } |
ServletRequest
、HttpSession
等很多对象也提供getServletContext()
方法获取到同一个ServletContext
实例。ServletContext
实例最大的作用就是设置和共享全局信息。
ServletContext
还提供了动态添加Servlet、Filter、Listener等功能,它允许应用程序在运行期间动态添加一个组件,虽然这个功能不是很常用。
总结
- 通过Listener我们可以监听Web应用程序的生命周期,获取
HttpSession
等创建和销毁的事件; ServletContext
是一个WebApp运行期的全局唯一实例,可用于设置和共享配置信息。