[Servlet]请求封装器、响应封装器

本文介绍了如何使用Servlet的请求封装器和响应封装器来解决过滤请求内容、设置GET请求编码以及实现响应数据压缩等问题。通过自定义的请求和响应包装类,实现了对HttpServletRequest和HttpServletResponse的适配,例如过滤不文明词汇、设置GET请求编码以及生成压缩数据的响应。文章详细阐述了封装器的工作原理和实际操作的关键点,并给出了留言板过滤HTML字符、GET请求编码设置以及响应压缩的实例应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 单纯的过滤器所做不到的事情:

    1) 前面章节介绍的仅仅是没有任何辅助之下的过滤器,但即使这样简单的过滤器也可以实现一些简单的过滤功能,比如测service时长,重设一下编码等等;

    2) 但是上节讲的过滤器却无法满足下列需求:

         i. 直接修改请求request中的内容(比如将请求内容中的一些不文明词汇替换成?#@!等和谐掉),这就需要修改请求Body中的内容,但是HttpServletRequest的API中只有getParameter却没有setParameter;

         ii. 设置GET请求的编码,虽然可以使用HttpServletRequest的setCharacterEncoding来设置编码,但该方法只能修改请求Body的编码,但GET方法的请求参数是在URL中的,之前讲过这只能却请求参数的字节序列再进行转码,因此这也涉及到修改请求request中的内容的问题;

         iii. 输出压缩数据:由于直接在service中使用PrintWriter进行输出了,也就意味着在service服务过程中就已经响应给客户端了,因此无法在service返回后再对输出进行压缩,除非使PrintWriter的输出具有压缩功能,否则PrintWriter并没有提供任何API让你修改输出流中的内容;

    3) 以上这些问题的解决方法看似只能修改HttpServletRequest和HttpServletResponse的源码了,但真的有那么麻烦吗?直接修改源码是非常繁琐的,而且容易产生严重漏洞,还好Servlet/JSP提供了解决方法,那就是请求封装器和响应封装器;


2. 封装器的效果以及如何实现:

    1) 从问题的解决思路出发:适配器思想

        i. 比如上例中要和谐掉request中的不文明词汇,在不做任何修改之前,使用getParameter获取的内容是不文明词汇,但是我们总希望getParameter发生奇迹,可以返回和谐后的词汇,因此我们的思路就是修改getParameter的源码使之具有和谐功能,但遗憾的是我们不希望修改源码;

        ii. 其实方法很简单,我们可以用一个自定义的类,比如MyHttpServletRequest将原来的HttpServletReqeust封装起来,然后在自定义一个getParameter方法,在该方法中调用HttpServletRequest的getParameter得到原始数据,然后在对原始数据进行和谐,再将和谐后的内容返回不就行了吗?

        iii. 也就是MyHttpServletRequest的getParameter将HttpServletRequest的getParameter包装了起来,同时返回了过滤后的数据,那么MyHttpServletRequest不就相当于修改了源码的HttpServletRequest了吗?并且HttpServletRequest的源码一点儿也没修改!

        iv. 接着只要将MyHttpServletRequest对象传给service就行了,在service中调用的都将是MyHttpServletRequest的getParameter,得到的都是和谐过的数据,这就完全解决了问题;

    2) 以上就是一种典型的适配器思想,那就是为了改变一个类的行为,我们不必直接去修改该类的各个方法,而是用另一个类来封装它,即分别定义同名方法(当然方法签名也必须一模一样),然后在各方法中定义自己需要的行为(当然会用到原类各方法得到原始的数据,然后在进行加工返回处理过的数据),而MyHttpServletRequest就是一个封装器(也叫适配器);

    3) 实际操作中的三个关键点:

        i. 并不是直接将HttpServletRequest和HttpServletResponse“封装”起来,一般意义上的封装使之将一个类的对象作为另一个类的数据成员,但是Request和Response并不是类而是接口,直接"封装"接口并不能获得其中的数据;

        ii. 定义相同方法签名的方法在面向对象中叫做“覆盖”,在Java中要使用@Override标记来标注一个方法,在调用方式时才会调用覆盖的版本而不是原来的版本;

        iii. 既然是覆盖,那么就只能继承,也就是说我们的MyHttpServletRequest必须继承(extends)HttpServletRequest了,但是问题又来了,HttpServletRequest只是一个接口,接口只能实现(implements)而不能直接继承(extends),况且接口只有方法没有数据,关键是我们需要得到Request中的数据!

!!!幸好J2EE标准提供了两个类,一个是HttpServletRequestWrapper,另一个是HttpServletResponseWrapper,这两个类分别实现了HttpServletRequest接口和HttpServletResponse接口,由于它们都是类,因此都分别封装了Request和Response中的数据,因此我们自己的封装器只要继承这两个就行了!

    4) Wrapper封装器:

         i. 正如上面所说,它们也是J2EE的API,但仅仅就是实现了一下Request和Response接口,并没有改动里面方法的效果,因此可以在自定义的封装器中运用原始的方法得到原始的数据并进行处理;

         ii. 其实J2EE提供了四种封装器,分别是:

ServletRequest  ->  ServletRequestWrapper

ServletResponse  ->  ServletResponseWrapper

HttpServletRequest  ->  HttpServletRequestWrapper

HttpServletResponse  ->  HttpServletResponseWrapper

!!我们自定义的封装器必须从这4中Wrapper中继承;


3. 请求封装器的应用——留言板过滤HTML字符:

    1) 留言板允许用户在上面写自己的话,但有些用户可能不老实,比如写一个<a href="一个广告地址或者一个危险站点">XXX</a>,如果将这串字符原封不动的响应出去就会成为一个超链接"XXX",其它用户点了之后会进入广告页面也可能会中毒;

    2) 因此我们需要将留言板中的HTML字符替换成纯文本,让其响应时仅仅是一串纯文本"<a href="一个广告地址或者一个危险站点">XXX</a>"而不是一个超链接"XXX";

    3) 可以这样实现:

         i. 首先是请求封装器MyRequest:

class MyRequest extends HttpServletRequestWrapper {
	public MyRequest(HttpServletRequest request) {
		super(request);
	}

	@Override
	public String getParameter(String name) {
		// TODO Auto-generated method stub
		String value = getRequest().getParameter(name);
		return StringEscapeUtils.escapeHtml(value);
	}	
}
!!所有的Wrapper API类都具有getRequest方法或者是getResponse方法来获取封装的请求或响应的数据,要多多利用!

!!构造器一定要调用基类的构造器,并且参数一定要有ServletRequest或HttpServletRequest或ServletResponse或HttpServletResponse(视具体是什么封装器而定);

!!从构造角度看也是将一个请求或响应“封装”了起来,例如:MyRequest myReq = new MyRequest(req);  // 这里就将req封装成了myReq

!!!这里的StringEscapeUtils类来自Apache Commons Lang库中,其方法String escapeHtml(String value);可以将value中的HTML符号替换成纯文本符号并返回,该程序包需要到http://commons.apache.org/lang/上下载,下载解压后将里面的JAR包包括到WEB-INF/lib目录中即可;

         ii. 过滤器:很简单,就是简单的一封装,然后一传参就行了

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	// TODO Auto-generated method stub
	HttpServletRequest wrapper = new MyRequest((HttpServletRequest)request);
	chain.doFilter(wrapper, response);
}


4. 请求封装器的应用——完整的编码设置:

    1) 前面讲到了POST的编码设置不需要封装器,直接用setCharacterEncoding一下就行了,但是GET方法的编码设置就必须要使用封装器了;

    2) 封装器:

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) {
		// TODO Auto-generated method stub
		String value = getRequest().getParameter(name);
		if (value != null) {
			try {
				byte[] b = value.getBytes("ISO-8859-1");
				value = new String(b, ENCODING);
			} catch (UnsupportedEncodingException e) {
				throw new RuntimeException(e);
			}
		}
		return value;
	}
}
    3) 过滤器:

private String ENCODING;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
	// TODO Auto-generated method stub
	ENCODING = filterConfig.getInitParameter("ENCODING");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	// TODO Auto-generated method stub
	HttpServletRequest req = (HttpServletRequest)request;
	if ("GET".equals(req.getMethod())) { // GET方法需要包装
		req = new EncodingWrapper(req, ENCODING);
	}
	else { // POST方法直接设置编码即可,完全可以省去封装的过程增加效率
		req.setCharacterEncoding(ENCODING);
	}
	chain.doFilter(req, response);
}


5. 响应封装器的应用:压缩响应

    1) 前面已经讲过了,想要让响应出的数据是经过压缩的就必须要让HttpServletResponse具有压缩功能,而该类显然不具备这样的功能,因此需要用响应封装器对HttpServletResponse进行封装,并对其getPrinter、getOutputStream等方法进行适配,使其具有压缩输出的功能;

    2) J2EE为ServletResponse和HttpServletResponse分别提供了封装器API类ServletResponseWrapper和HttpServletResponseWrapper可供用户继承;

    3) 首先对ServletOutputstream进行扩展:

         i. getWriter获取的PrintWriter底层是通过ServletOutputstream进行输出的,所以要先对ServletOutputStream进行扩展使其具有压缩功能;

         ii. 这里会用到J2SE的API类GZIPOutputStream,该类可以直接输出GZIP格式的压缩输出流;

         iii. 代码:

public class GZIPServletOS extends ServletOutputStream {
	private GZIPOutputStream gzipOS;

	public GZIPServletOS(ServletOutputStream servletOS) throws IOException {
		gzipOS = new GZIPOutputStream(servletOS);
	}

	// 由于OutputStream的所有输出底层都是调用wirte(int b)方法的,因此
	// 只有让write(int b)具有压缩功能,那么OutputStream的所有输出就都具有压缩功能了
	@Override
	public void write(int b) throws IOException {
		// TODO Auto-generated method stub
		gzipOS.write(b); // 输出时用封装的GZIPOutputStream的write压缩输出
	}
	
	// 对于压缩输出流,字节流中前后数据是相互依赖压缩的,传输只能按照压缩快一块一块传输
	// 不能将一块分成多个部分传输,因此GZIPOutputStream提供finish方法手动控制缓冲区的输出
	// finish类似flush方法,但不过finish并不是将整个缓冲区中所有内容输出
	// 而是将缓冲区中现有的所有完整的块输出,末尾不完整的块不输出,继续接受压缩字节
	// 记住!压缩流只能以压缩块为单位进行输出
	public void finish() {
		if (gzipOS != null) // 必须在非空的时候才能使用finish,否则会抛出异常
			gzipOS.finish();
	}
}
    4) 接着是响应封装器:

public class CompressWrapper extends HttpServletResponseWrapper {
	// 基于OutputStream和PrintWriter的规则设计封装器
	// 在J2SE标准下PrintWriter用封装的OutputStream进行输出
	// PrintWriter在创建时也是利用OutputStream的:伪代码
		// OutputStream os = new OutputStream
		// PrintWriter out = new PrintWriter(os)
	// 因此J2SE标准规定:如果out封装了os,那么输出时就只能其中一个
		// 用os输出时就不得使用out输出,用out输出的时候就不能用os输出
		// 混用就直接抛出IllegalStateException

	// 这两个成员的设计就符合J2SE标准
	private GZIPServletOS gzipServletOS; // OutputStream
	private PrintWriter out; // PrintWriter

	public CompressWrapper(HttpServletResponse resp) {
		super(resp);
	}

	@Override
	public ServletOutputStream getOutputStream() throws IOException {
		// TODO Auto-generated method stub
		if (out != null) { // 用os进行输出时out不能占用os
			throw new IllegalStateException();
		}
		if (gzipServletOS == null) {
			gzipServletOS = new GZIPServletOS(getResponse().getOutputStream());
		}
		return gzipServletOS; // 多态返回,向上隐式转换
	}

	@Override
	public PrintWriter getWriter() throws IOException {
		// TODO Auto-generated method stub
		if (gzipServletOS != null) { // os已经被占用就不能在使用out了
			throw new IllegalStateException();
		}
		if (out == null) {
			gzipServletOS = new GZIPServletOS(getResponse().getOutputStream());
			OutputStreamWriter osw = new OutputStreamWriter(
					gzipServletOS, getResponse().getCharacterEncoding());
			out = new PrintWriter(osw);
		}	
		return out;
	}

	@Override
	public void setContentLength(int len) {
		// TODO Auto-generated method stub
		// 不实现此方法内容,因为真正的输出会被压缩
	}
	
	public void finish() { // 再对finish进行包装
		gzipServletOS.finish();
	}
}
!!PrintWriter的构建需要OutputStreamWriter,OutputStreamWriter的构造器:OutputStreamWriter(OutputStream out, Charset cs);

        a. 当然也有只有一个out参数的版本,但是这样的话cs就会使用默认的字符集,但是在网络应用中编码集是非常重要的,因此在网络应用中基本上都要使用有cs的版本;

        b. HttpServletResponse的getCharacterEncoding可以获取响应编码集,因此可以将该方法的结果作为cs来构造OuputStreamWriter;

    5) 最后是过滤器:

         i. 压缩过滤的标准步骤:

            a. 检查请求的accept-encoding标头是否有gzip字符串,即判断浏览器是否有压缩响应的需求;

            b. 如果有这样的需求,就得设置响应的content-encoding标头为gzip;

            c. 接着就是压缩响应封装、doFilter、手动冲刷压缩缓冲区了;

         ii. 代码:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
		throws IOException, ServletException {
	String encodings = ((HttpServletRequest)request).getHeader("accept-encoding");
	if (encodings != null && encodings.indexOf("gzip") > -1) {
		CompressWrapper respWrapper = new CompressWrapper((HttpServletResponse)response);
		respWrapper.setHeader("content-encoding", "gzip");
		
		chain.doFilter(request, respWrapper);
		
		respWrapper.finish();
	}
	else {
		chain.doFilter(request, response);
	}
}



评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值