htmlcompressor 压缩 html 报错 UT010006: Cannot call getWriter(), getOutputStream() already called 解决办法 时间: 2020-01-07 14:10 分类: JAVA Web 最近,使用`thymeleaf`模板语言的时候,发现输出的`html`网页中会出现很多的空行,不像`JSP`那样可以通过在`web.xml`里配置一下就能去掉多余的空行。 `Github`上作者好像也不愿做此功能,需要使用者自己去实现。 于是找到`Google`的`htmlcompressor`,使用很简单,就是自定义`Filter`,在`Filter`里将`html`的空行去掉,实现如下,自定义`CompressResponseFilter.kt`: ```java @WebFilter(filterName = "CompressResponseFilter", urlPatterns = ["/*"]) class CompressResponseFilter : Filter { private var compressor: HtmlCompressor? = null @Throws(IOException::class, ServletException::class) override fun doFilter(req: ServletRequest, resp: ServletResponse, chain: FilterChain) { val responseWrapper = CharResponseWrapper( resp as HttpServletResponse) chain.doFilter(req, responseWrapper) val servletResponse = responseWrapper.toString() resp.getWriter().write(compressor!!.compress(servletResponse)) } @Throws(ServletException::class) override fun init(config: FilterConfig) { compressor = HtmlCompressor() compressor!!.isCompressCss = true compressor!!.isCompressJavaScript = false } override fun destroy() {} } ``` 我这里是`Kotlin`代码,相信大家也能看得懂。 里面的`CharResponseWrapper.kt`: ```java class CharResponseWrapper(response: HttpServletResponse) : HttpServletResponseWrapper(response) { private val output: CharArrayWriter = CharArrayWriter() override fun toString(): String { return output.toString() } override fun getWriter(): PrintWriter { return PrintWriter(output) } } ``` 需要引入的`jar`包: ```xml com.yahoo.platform.yui yuicompressor 2.4.6 com.googlecode.htmlcompressor htmlcompressor 1.5.2 ``` 如果是`SpringBoot`项目,需要开启`Filter`的扫描: ``` @ServletComponentScan(basePackages = ["com.bde4.v2.filter"]) ``` 可能你`Google`到的也就是我上面的实现方法,但是,运行后会发现报错: > UT010006: Cannot call getWriter(), getOutputStream() already called 仔细查看错误日志不难发现报错的都是`css`、`js`、图片什么的请求。 跟进错误,发现抛异常的代码: ```java if (this.responseState == HttpServletResponseImpl.ResponseState.STREAM) { throw UndertowServletMessages.MESSAGES.getOutputStreamAlreadyCalled(); } ``` `this.responseState`是个私有属性,然而并没有`getter`或者`setter`方法,最多也只能找到一个`reset`方法将`this.responseState`重置: ```java public void reset() { if (this.servletOutputStream != null) { this.servletOutputStream.resetBuffer(); } this.writer = null; this.responseState = HttpServletResponseImpl.ResponseState.NONE; this.exchange.getResponseHeaders().clear(); this.exchange.setStatusCode(200); this.treatAsCommitted = false; } ``` 于是在`resp.getWriter().write(compressor!!.compress(servletResponse))`之前调用`resp.reset()`重启项目,此时报错`Response already commited`。 所以很明显,对于静态资源,在调用`chain.doFilter(req, responseWrapper)`时,它就已经将数据刷到输出流里面去了。 所以再次调用`resp.getWriter().write(compressor!!.compress(servletResponse))`就会报错: > UT010006: Cannot call getWriter(), getOutputStream() already called 解决办法就是: 在`chain.doFilter(req, responseWrapper)`之后(注意,必须是之后,之前的话`response`的状态还是未提交的)添加判断条件: ```java override fun doFilter(req: ServletRequest, resp: ServletResponse, chain: FilterChain) { val responseWrapper = CharResponseWrapper( resp as HttpServletResponse) chain.doFilter(req, responseWrapper) val servletResponse = responseWrapper.toString() if (!resp.isCommitted) resp.getWriter().write(compressor!!.compress(servletResponse)) } ``` `if (!resp.isCommitted)`只有当`response`未提交的时候我们才去压缩`html`。 对于这个错误,网上搜了很多文章,都没有找到解决办法,如有遇到相同问题的朋友,希望这篇文章能够帮到你。 总的来说就是过滤掉静态资源文件,可能有人会说过滤静态资源用`URL`的后缀就行了,但是为了保险起见还是用上面的方法比较合适,因为报错的根本原因就是`response`的状态为已提交。 标签: 无