最近,使用thymeleaf
模板语言的时候,发现输出的html
网页中会出现很多的空行,不像JSP
那样可以通过在web.xml
里配置一下就能去掉多余的空行。
Github
上作者好像也不愿做此功能,需要使用者自己去实现。
于是找到Google
的htmlcompressor
,使用很简单,就是自定义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
仔细查看错误日志不难发现报错的都是css
、js
、图片什么的请求。
跟进错误,发现抛异常的代码:
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
的状态为已提交。