Servlet过滤器与封装器
来源:互联网 发布:学校网络管理制度大全 编辑:程序博客网 时间:2024/06/03 22:14
在Servlet容器调用某个Servlet的service()方法前,Servlet并不会知道有请求的到来,而在Servlet的service()方法运行之后,容器真正对浏览器进行HTTP响应之前,浏览器也不会知道Servlet真正的响应是什么。过滤器正如其名称所示,它介于Servlet之前,可拦截过滤浏览器对Servlet的请求,也可以改变Servlet对浏览器的响应。本文将介绍过滤器的运用,了解如何实现Filter接口来编写过滤器,以及如何使用请求封装器及响应封装器,将容器产生的请求与响应对象加以包装,针对某些请求信息或响应进行加工处理。
1、过滤器的概念
想象已经开发好应用程序的主要商务功能了,但现在有几个需求出现:
(1)针对所有的servlet,产品经理想要了解从请求到响应之间的时间差。
(2)针对某些特定的页面,客户希望只有特定的几个用户有权浏览。
(3)基于安全的考量,用户输入的特定字符必须过滤并替换为无害的字符。
(4)请求与响应的编码从Big5改用UTF-8。
在修改源代码之前,先分析一下这些需求:
(1)在运行Servlet的service()方法“前”,记录起始时间,Servlet的service()方法运行“后”,记录结束时间并计算时间差。
(2)在运行Servlet的service()方法“前”,验证是否为允许的用户。
(3)在运行Servlet的service()方法“前”,对请求参数进行字符过滤与替换。
(4)在运行Servlet的service()方法“前”,对请求与响应对象设置编码。
经过以上分析,可以发现这些需求,可以在真正运行Servlet的service方法“前”与Servlet的service()方法“后”中间进行实现。如下图所示:
性能评测、用户验证、字符替换、编码设置等需求,基本上与应用程序的业务逻辑没有直接的关系,只是应用程序额外的元件服务之一。因此,这些需求应该设计为独立的元件,使之随时可以加入到应用程序中,也随时可以移除,或随时可以修改设置而不用修改原有的业务代码。这类元件就像是一个过滤器,安插在浏览器与Servlet中间,可以过滤请求与响应而作进一步的处理,如下图所示。
Servlet/JSP提供了过滤器机制让你实现这些元件服务,可以视 需求抽换过滤器或调整过滤器的顺序,也可以针对不同的URL应用不同的过滤器。甚至在不同的Servlet间请求转发或包含时应用过滤器。
2、实现并设置过滤器
在Servlet中要实现过滤器,必须实现Filter接口,并使用@WebFilter标注或在web.xml中定义过滤器,让容器知道该加载哪些过滤器类。Filter接口有三个要实现的方法:init()、doFilter()与destroy()。
package javax.servlet;import java.io.IOException;public interface Filter { public void init(FilterConfig filterConfig) throws ServletException; public void doFilter(ServletRequest request, ServletResponse response,FilterChain chain) throws IOException, ServletException; public void destroy();}
FilterConfig类似于Servlet接口init()方法参数上的ServletConfig,FilterConfig是实现Filter接口的类上使用标注或web.xml中过滤器设置信息的代表对象。如果在定义过滤器时设置了初始参数,则可以通过FilterConfig的getInitParameter()方法来取得初始参数。
Filter接口的doFilter()方法则类似于Servlet接口的service()方法。当请求来到容器,而容器发现调用Servlet的service()方法前,可以应用某过滤器时,就会调用该过滤器的doFilter()方法。可以在doFilter()方法中进行service()方法的前置处理,而后决定是否调用FilterChain的doFilter()方法。如果调用了FilterChain的doFilter()方法,就会运行下一个过滤器,如果没有下一个过滤器,就调用请求目标Servlet的service()方法(这里实际上用到了责任链模式)。如果没有调用FilterChain的doFilter()方法,则请求就不会继续交给接下来的过滤器或目标Servlet,这就是所谓的拦截请求(从Servlet的角度来看,根本不知道浏览器有发出请求)。
以下是一个简单的性能评测过滤器,用来记录请求与响应的时间差。
@WebFilter( filterName="PerformanceFilter", urlPatterns={"/*"}, dispatcherTypes={ DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST, DispatcherType.ERROR,DispatcherType.ASYNC }, initParams={@WebInitParam(name="Site", value="菜鸟教程")} )public class PerformanceFilter implements Filter { private FilterConfig config; public PerformanceFilter() { } public void destroy() { } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long begin = System.currentTimeMillis(); chain.doFilter(request, response); config.getServletContext().log("Performance process in " + (System.currentTimeMillis() - begin) + " milliseconds"); // 输出站点名称 System.out.println("站点网址:http://www.runoob.com"); } public void init(FilterConfig fConfig) throws ServletException { // 获取初始化参数 this.config = fConfig; String site = config.getInitParameter("Site"); // 输出初始化参数 System.out.println("PerformanceFilter init done! 网站名称: " + site); }}
当过滤器类被载入容器并实例化后,容器会运行其init()方法并传入FilterConfig对象作为参数。过滤器的设置与Servlet的设置很类似,@WebFilter中的filterName设置过滤器名称,urlPatterns设置哪些URL请求必须应用哪个过滤器,可应用的URL模式与Servlet基本上相同,而”/*“表示应用在所有的URL请求上。除了指定URL模式外,也可以指定Servlet名称,这可以通过@WebFilter的servletNames来设置:
@WebFilter(filterName="PerformanceFilter", servletNames={"Servlet1","Servlet2"})
如果想一次符合所有的Servlet名称,可以使用星号(*)。如果在过滤器初始化时,想要读取一些参数,可以在@WebFilter中使用@WebInitParam来设置initParams,例如:
@WebFilter( filterName="EncodingFilter", urlPatterns={"/encoding"}, initParams={ @WebInitParam(name="ENCODING", value="UTF-8") })public class EncodingFilter implements Filter { private String ENCODING; private FilterConfig config; public EncodingFilter() { } public void init(FilterConfig fConfig) throws ServletException { // TODO Auto-generated method stub config = fConfig; ENCODING = config.getInitParameter("ENCODING"); // 输出初始化参数 System.out.println("EncodingFilter init done! ENCODING = " + ENCODING); } ...}
触发过滤器的时机,默认是浏览器直接发出请求时。如果是那些通过RequestDispatcher的forward()或include()发出的请求,需要设置@WebFilter的dispatcherTypes,例如:
@WebFilter( filterName="some", urlPatterns={"/some"}, dispatcherTypes={ DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST, DispatcherType.ERROR,DispatcherType.ASYNC })
如果不设置任何dispatcherTypes,则默认为REQUEST。FORWARD就是指通过RequestDispatcher的forward()方法而来的请求可以套用过滤器,INCLUDE是指通过RequestDispatcher的include方法而来的请求可以套用过滤器,ERROR是指由容器处理例外而转发过来的请求可以套用过滤器,ASYNC是指异步处理器的请求可以触发过滤器。
3、实现请求封装器
以下通过两个例子,来说明请求封装器的实现与应用,分别是特殊字符替换过滤器与编码设置过滤器。
1、实现字符替换过滤器
假设有个留言板程序已经上线并正常运行中,但是发现,有些用户会在留言中输入一些HTML标签。基于安全性的考虑,不希望用户输入的HTML标签直接出现在留言中而被一些浏览器当作HTML的一部分来解释。例如,并不希望用户在留言中输入<a href=”http://openhome.cc”>OpenHome.cc</a>这样的信息。不希望在留言显示中有超链接,希望将一些HTML字符过滤掉,如将<、>这样的角括号置换为HTML实体字符,可以使用过滤器的方式。但问题在于,虽然可以使用HttpServletRequest的getParameter()取得请求参数值,但是没有一个像setParameter()的方法,可以将处理过后的参数值重新设置给HttpServletRequest。
所幸,有个HttpServletRequestWrapper帮我们实现了HttpServletRequest接口,只要继承这个类,并编写想要重新定义的方法即可。相对应于ServletRequest接口,也有个ServletRequestWrapper类可以使用。
以下范例通过继承HttpServletRequestWrapper实现一个请求封装器,可以将请求参数中的HTML字符替换为HTML实体字符。
public class EscapeWrapper extends HttpServletRequestWrapper { public EscapeWrapper(HttpServletRequest request) { super(request);//必须调用父类构造器,将HttpServletRequest实例传入 } @Override public String getParameter(String name) { String value = getRequest().getParameter(name); return StringEscapeUtils.escapeHtml(value); //将请求参数值进行字符替换 }}
之后若有Servlet想取得请求参数值,都会调用getParameter()方法,所以这里重新定义这个方法,在此方法中,进行字符替换动作。可以使用这个请求封装器搭配过滤器,以进行字符过滤的服务。例如:
@WebFilter( filterName="EscapeFilter", urlPatterns={"/guestbook"}, dispatcherTypes={ DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST, DispatcherType.ERROR,DispatcherType.ASYNC })public class EscapeFilter implements Filter { private FilterConfig config; public EscapeFilter() { } public void destroy() { System.out.println("EscapeFilter calling done!"); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { long begin = System.currentTimeMillis(); HttpServletRequest requestWrapper = new EscapeWrapper((HttpServletRequest)request); chain.doFilter(requestWrapper, response); config.getServletContext().log("Request escaping HTML tags in " + (System.currentTimeMillis() - begin) + " milliseconds"); } public void init(FilterConfig fConfig) throws ServletException { this.config = fConfig; System.out.println("EscapeFilter init done!"); }}
2、实现编码设置过滤器
在之前的范例中,如果要设置请求字符编码,都是在个别Servlet中处理。可以在过滤器中进行字符编码的统一设置,如果日后想要改变编码,就不用每个Servlet逐一修改了。
由于HttpServletRequest的setCharacterEncoding()方法针对的是请求的Body内容,对于GET请求,必须在取得请求参数的字节阵列后,重新指定编码来解析。这个需求与上一个范例类似,可搭配请求封装器来实现。
public class EncodingWrapper extends HttpServletRequestWrapper { private String ENCODING; public EncodingWrapper(HttpServletRequest request, String ENCODING) { super(request); this.ENCODING = ENCODING; } @Override public String getParameter(String name){ String value = getRequest().getParameter(name); if(value != null) { try { //Web容器默认使用ISO-8859-1编码格式 byte[] b = value.getBytes("ISO-8859-1"); value = new String(b, ENCODING); } catch(UnsupportedEncodingException e) { throw new RuntimeException(e); } } return value; }}
编码过滤器的实现如下:
@WebFilter( filterName="EncodingFilter", urlPatterns={"/encoding"}, dispatcherTypes={ DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.REQUEST, DispatcherType.ERROR,DispatcherType.ASYNC }, initParams={ @WebInitParam(name="ENCODING", value="UTF-8") })public class EncodingFilter implements Filter { private String ENCODING; private FilterConfig config; public EncodingFilter() { } public void destroy() { } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest)request; if("GET".equals(req.getMethod())) { long begin = System.currentTimeMillis(); req = new EncodingWrapper(req, ENCODING); chain.doFilter(req, response); config.getServletContext().log("GET Method Request Encoding process in " + (System.currentTimeMillis() - begin) + " milliseconds"); } else { req.setCharacterEncoding(ENCODING); chain.doFilter(req, response); } } public void init(FilterConfig fConfig) throws ServletException { config = fConfig; ENCODING = config.getInitParameter("ENCODING"); // 输出初始化参数 System.out.println("EncodingFilter init done! ENCODING = " + ENCODING); }}
请求参数的编码设置是通过过滤器初始参数来设置的,并在过滤器初始化方法init()中读取,过滤器仅在GET请求以创建EncodingWrapper实例,其他方法则通过HttpServletRequest的setCharacterEncoding()来设置编码,最后都调用FilterChain的doFilter()方法传入EncodingWrapper实例或原请求对象。
3、实现响应封装器
在Servlet中,是通过HttpServletResponse对象来对浏览器进行响应的,如果想要对响应的内容进行压缩处理,就要想办法让HttpServletResponse对象具有压缩处理的功能。前面介绍过请求封装器的实现,而在响应封装器的部分,可以继承HttpServletResponseWrapper类来对HttpServletResponse对象进行封装。
若要对浏览器进行输出响应,必须通过getWriter()取得PrintWriter,或是通过getOutputStream()取得ServletOutputStream。 所以针对压缩输出的需求,主要就是继承HttpServletResponseWrapper类之后,通过重新定义这两个方法来达成。
在下面例子中,压缩的功能采用GZIP格式,这是浏览器可以授受的压缩格式,可以使用GZIPOutputStream类来实现。由于getWriter()的PrintWriter在创建时,也是必须使用到ServletOutputStream,所以在这里先扩展ServletOutputStream类,让它具有压缩的功能。
public class GZipServletOutputStream extends ServletOutputStream { private GZIPOutputStream gzipOutputStream; public GZipServletOutputStream(ServletOutputStream servletOutputStream) throws IOException { this.gzipOutputStream = new GZIPOutputStream(servletOutputStream); } @Override public boolean isReady() { return false; } @Override public void setWriteListener(WriteListener listener) { } public GZIPOutputStream getGzipOutputStream(){ return gzipOutputStream; } @Override public void write(int b) throws IOException { gzipOutputStream.write(b); //输出时通过gzipOutputStream来压缩输出 }}
在HttpServletResponse对象传入Servlet的service()方法前,必须先封装它,使得调用getOutputStream()时,可以取得这里所实现的GZipServletOutputStream对象,而调用getWriter()时,也可以利用GZipServletOutputStream对象来构造PrintWriter对象。
public class CompressionWrapper extends HttpServletResponseWrapper { private GZipServletOutputStream gzServletOutputStream; private PrintWriter printWriter; public CompressionWrapper(HttpServletResponse response) { super(response); } @Override public ServletOutputStream getOutputStream() throws IOException { //响应中已经调用过getWriter,再调用getOutputStream就抛出异常 if(printWriter != null) { throw new IllegalStateException(); } if(null == gzServletOutputStream) { gzServletOutputStream = new GZipServletOutputStream(getResponse().getOutputStream()); } return gzServletOutputStream; } @Override public PrintWriter getWriter() throws IOException { //响应中已经调用过getOutputStream,再调用getWriter就抛出异常 if(gzServletOutputStream != null) { throw new IllegalStateException(); } if(null == printWriter) { gzServletOutputStream = new GZipServletOutputStream(getResponse().getOutputStream()); OutputStreamWriter osw = new OutputStreamWriter( gzServletOutputStream, getResponse().getCharacterEncoding()); printWriter = new PrintWriter(osw); } return printWriter; } //不实现此方法,因为真正的输出会被压缩,忽略原来的内容长度设置 @Override public void setContentLength(int len){ } public GZIPOutputStream getGZIPOutputStream() { if(this.gzServletOutputStream == null) return null; return this.gzServletOutputStream.getGzipOutputStream(); }}
在上例中要注意,由于Servlet规范中规定,在同一个请求期间,getWriter()与getOutputStream()只能择一调用,否则必抛出IllegalStateException,因此建议在实现响应封装器时,也遵循这个规范。因此在重新定义getOutputStream()与getWriter()方法时,分别要检查是否已经存在PrintWriter与ServletOutputStream实例。
接下来就实现一个压缩过滤器,使用上面开发的CompressionWrapper来封装原HttpServletResponse。
@WebFilter( filterName="CompressionFilter", urlPatterns = { "/*" })public class CompressionFilter implements Filter { private FilterConfig config; public CompressionFilter() { } public void destroy() { } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest)request; HttpServletResponse res = (HttpServletResponse)response; String encodings = req.getHeader("accept-encoding"); //检查是否接受压缩 if((encodings != null) && (encodings.indexOf("gzip") > -1)) { long begin = System.currentTimeMillis(); CompressionWrapper responseWrapper = new CompressionWrapper(res); responseWrapper.setHeader("content-encoding", "gzip"); //设置响应内容编码为gzip chain.doFilter(request, responseWrapper); GZIPOutputStream gzipOutputStream = responseWrapper.getGZIPOutputStream(); if(gzipOutputStream != null) { gzipOutputStream.finish(); //调用GZIPOutputStream的finish方法完成压缩输出 } config.getServletContext().log("gzip compression process in " + (System.currentTimeMillis() - begin) + " milliseconds"); } else { chain.doFilter(request, response); //不接受压缩直接进行下一个过滤器 } } public void init(FilterConfig fConfig) throws ServletException { this.config = fConfig; System.out.println("CompressionFilter init done!"); }}
浏览器是否接受GZIP压缩格式,可以通过检查accept-encoding请求标头中是否包括gzip字符串来判断。如果可以接受GZIP压缩,创建CompressionWrapper封装原响应对象,并设置content-encoding响应标头为gzip,这样浏览器就会知道响应内容是GZIP压缩格式。接着调用FilterChain的doFilter()时,传入响应对象为CompressionWrapper对象。当FilterChain的doFilter()结束时,必须调用GZIPOutputStream的finish()方法,这才会将GZIP后的资料从缓冲区全部移出并进行响应。
如果浏览器不接受GZIP压缩格式,则直接调用FilterChain的doFilter(),这样就可以让不接受GZIP压缩格式的客户端也可以收到原有的响应内容。
- Servlet过滤器与封装器
- Servlet监听器、过滤器与Spring拦截器
- jsp 与servlet 过滤器
- 过滤器,监听器与Servlet
- servlet 过滤器与监听器
- Servlet过滤器创建与配置
- Servlet之监听器与过滤器
- web入门-Servlet与过滤器
- Servlet过滤器与Struts拦截器的区别
- Servlet的过滤器与Spring拦截器详解
- 【框架学习】springMVC过滤器与servlet拦截器区别
- servlet/过滤器/拦截器/监听器
- 拦截器、过滤器、监听器、servlet
- Servlet---请求封装器
- Servlet过滤器简介与字符串过滤
- 自定义JSP与Servlet中文乱码过滤器
- servlet 过滤器
- Servlet 过滤器
- JavaScript RegExp.$1...$9 属性详解
- Mysql创建、删除用户
- bitmap与file之间转换使用
- java socketNIO demo
- nodejs base64 编解码
- Servlet过滤器与封装器
- .NET索引器
- 以checked选中作为判断条件的各种写法
- 基于dragonboard410c智能网关
- HBase简介
- js对全角与半角介绍及相互转化
- CSS3 渐变 线性渐变
- 解决echarts 坐标轴太长无法显示的问题
- IDEA快捷键