先说一下出现这种情况的场景。该场景在使用jetty,tomcat作为容器时是可以正常运行的。首先是表单提交。
springmvc控制器代码如下:
@RequestMapping("test")
@Controller
public class TestController {
@RequestMapping("aa.html")
public String aa(Model model, RedirectAttributes redirectAttributes) {
redirectAttributes.addAttribute("a","李四");
return "redirect:bb.html";
}
@RequestMapping("bb.html")
public String bb(Model model, HttpServletRequest request) {
String a=request.getParameter("a");
return "login";
}
}
aa方法做了一次重定向,重定向时传参是中文,到进入bb方法时,获取到的string a则为乱码。
查看项目已使用了spring提供的字符集设置过滤器CharacterEncodingFilter。强制设置字符集为utf-8。却依然出现乱码问题。
没办法,只能查看springmvc的源码。跟踪源码后发现,在RedirectView类的createTargetUrl方法里,会去为重定向拼装url字符串:
protected final String createTargetUrl(Map model, HttpServletRequest request)
throws UnsupportedEncodingException {
// Prepare target URL.
StringBuilder targetUrl = new StringBuilder();
if (this.contextRelative && getUrl().startsWith("/")) {
// Do not apply context path to relative URLs.
targetUrl.append(request.getContextPath());
}
targetUrl.append(getUrl());
String enc = this.encodingScheme;
if (enc == null) {
enc = request.getCharacterEncoding();
}
if (enc == null) {
enc = WebUtils.DEFAULT_CHARACTER_ENCODING;
}
if (this.expandUriTemplateVariables && StringUtils.hasText(targetUrl)) {
Map variables = getCurrentRequestUriVariables(request);
targetUrl = replaceUriTemplateVariables(targetUrl.toString(), model, variables, enc);
}
if (isPropagateQueryProperties()) {
appendCurrentQueryParams(targetUrl, request);
}
if (this.exposeModelAttributes) {
appendQueryProperties(targetUrl, model, enc);
}
return targetUrl.toString();
}
debug发现,在使用post做form表单提交时,request.getCharacterEncoding()获得的是null,故导致执行到了enc = WebUtils.DEFAULT_CHARACTER_ENCODING这行代码
public static final String DEFAULT_CHARACTER_ENCODING = "ISO-8859-1";
使用了ISO-8859-1,故导致重定向的url字符串里没有使用utf-8字符集,导致了乱码。
那为什么request.getCharacterEncoding()获得的是null呢。在CharacterEncodingFilter过滤器里,
@Override
protected void doFilterInternal(
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String encoding = getEncoding();
if (encoding != null) {
if (isForceRequestEncoding() || request.getCharacterEncoding() == null) {
request.setCharacterEncoding(encoding);
}
if (isForceResponseEncoding()) {
response.setCharacterEncoding(encoding);
}
}
filterChain.doFilter(request, response);
}
该过滤器会强制使用utf-8。在debug之后,发现request.setCharacterEncoding(encoding)这行代码确实有执行到。再看request.setCharacterEncoding方法里的实现,io.undertow.servlet.spec.HttpServletRequestImpl:
@Override
public void setCharacterEncoding(final String env) throws UnsupportedEncodingException {
if (readStarted) {
return;
}
try {
characterEncoding = Charset.forName(env);
final ManagedServlet originalServlet = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY).getOriginalServletPathMatch().getServletChain().getManagedServlet();
final FormDataParser parser = originalServlet.getFormParserFactory().createParser(exchange);
if (parser != null) {
parser.setCharacterEncoding(env);
}
} catch (UnsupportedCharsetException e) {
throw new UnsupportedEncodingException();
}
}
debug发现,readStarted为true。故CharacterEncodingFilter过滤器设置request.setCharacterEncoding无效。
注意,HttpServletRequestImpl是undertow容器的实现,springboot集成了undertow作为web服务器。
在HttpServletRequestImpl的private FormData parseFormData()方法上设置断点,发现在进入CharacterEncodingFilter过滤器前,undertow容器会先进入parseFormData方法,调用堆栈如下:
at io.undertow.servlet.spec.HttpServletRequestImpl.parseFormData(HttpServletRequestImpl.java:750)
at io.undertow.servlet.spec.HttpServletRequestImpl.getParameter(HttpServletRequestImpl.java:636)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:70)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:131)
at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:285)
at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:264)
at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:81)
at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:175)
at io.undertow.server.Connectors.executeRootHandler(Connectors.java:207)
at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:802)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
parseFormData()如下:
private FormData parseFormData() {
if (parsedFormData == null) {
if (readStarted) {
return null;
}
final ManagedServlet originalServlet = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY).getCurrentServlet().getManagedServlet();
final FormDataParser parser = originalServlet.getFormParserFactory().createParser(exchange);
if (parser == null) {
return null;
}
readStarted = true;
try {
return parsedFormData = parser.parseBlocking();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return parsedFormData;
}
注意,该方法把readStarted设置为true,所以在进入CharacterEncodingFilter过滤器后,调用request.setCharacterEncoding时,其实什么都没有处理,则导致springmvc在拼装重定向url字符串时,没有使用utf-8字符集,导致出现了乱码。至此出现乱码的原因分析完毕。至于为什么容器会去调用 parseFormData(),大家可以查看调用堆栈里每一步的实现,看看是怎么一步步调用到parseFormData(),我觉得这是undertow容器的一个bug。
分析完原因,给出解决方案,这样才完整。我这里的解决方案是添加一个web过滤器,利用装饰器模式,改写request.getCharacterEncoding()方法,当该方法获得为空时,默认返回utf-8。
Utf8EncodingFilter:
package com.onlyou.superp2b;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/**
* 默认使用utf-8字符集
* Created by cd_huang on 2017/7/25.
*/
public class Utf8EncodingFilter extends GenericFilterBean {
private static Logger logger = LoggerFactory.getLogger(Utf8EncodingFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
Utf8EncodingRequest request =new Utf8EncodingRequest((HttpServletRequest)servletRequest);
filterChain.doFilter(request,servletResponse);
}
protected void initFilterBean() throws ServletException {
logger.info("---加载Utf8EncodingFilter!---");
}
}
Utf8EncodingRequest:
package com.onlyou.superp2b;
import org.apache.commons.lang3.StringUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
/**
* 默认使用utf-8字符集
* Created by cd_huang on 2017/7/25.
*/
public class Utf8EncodingRequest extends HttpServletRequestWrapper {
public Utf8EncodingRequest(HttpServletRequest request) {
super(request);
}
@Override
public String getCharacterEncoding() {
String characterEncoding =this.getRequest().getCharacterEncoding();
return StringUtils.isBlank(characterEncoding)?"utf-8":characterEncoding;
}
}
最后,测试了下,确实解决了使用undertow作为web容器带来的中文乱码的问题。