admin

htmlcompressor 压缩 html 报错 UT010006: Cannot call getWriter(), getOutputStream() already called 解决办法
最近,使用thymeleaf模板语言的时候,发现输出的html网页中会出现很多的空行,不像JSP那样可以通过在we...
扫描右侧二维码阅读全文
07
2020/01

htmlcompressor 压缩 html 报错 UT010006: Cannot call getWriter(), getOutputStream() already called 解决办法

最近,使用thymeleaf模板语言的时候,发现输出的html网页中会出现很多的空行,不像JSP那样可以通过在web.xml里配置一下就能去掉多余的空行。

Github上作者好像也不愿做此功能,需要使用者自己去实现。

于是找到Googlehtmlcompressor,使用很简单,就是自定义Filter,在Filter里将html的空行去掉,实现如下,自定义CompressResponseFilter.kt:

@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

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包:

<dependency>
    <groupId>com.yahoo.platform.yui</groupId>
    <artifactId>yuicompressor</artifactId>
    <version>2.4.6</version>
</dependency>

<dependency>
    <groupId>com.googlecode.htmlcompressor</groupId>
    <artifactId>htmlcompressor</artifactId>
    <version>1.5.2</version>
</dependency>

如果是SpringBoot项目,需要开启Filter的扫描:

@ServletComponentScan(basePackages = ["com.bde4.v2.filter"])

可能你Google到的也就是我上面的实现方法,但是,运行后会发现报错:

UT010006: Cannot call getWriter(), getOutputStream() already called

仔细查看错误日志不难发现报错的都是cssjs、图片什么的请求。

跟进错误,发现抛异常的代码:

if (this.responseState == HttpServletResponseImpl.ResponseState.STREAM) {
   throw UndertowServletMessages.MESSAGES.getOutputStreamAlreadyCalled();
}

this.responseState是个私有属性,然而并没有getter或者setter方法,最多也只能找到一个reset方法将this.responseState重置:

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的状态还是未提交的)添加判断条件:

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的状态为已提交。

Last modification:January 7th, 2020 at 02:10 pm
If you think my article is useful to you, please feel free to appreciate

Leave a Comment