文章目录
- 一、前言
- 二、设计思路
- 三、代码实现
- 四、启动测试
- 五、过滤器解码无效
- 六、源码跟踪
- 七、解决方案
- 八、再次重启测试
- 九、总结
一、前言
最近做的一个公司项目,因为客户需要对特殊字符做搜索,但是前端的请求参数无法传递到后端,所以前端对所有列表请求的请求参数做了统一的URL编码,那么后端也需要做一个统一的解码操作。
二、设计思路
既然是统一解码操作,那么只需要拦截所有的GET请求,然后对GET请求里的参数进行解码就可以了,这里就可以用到过滤器来解决。
三、代码实现
1.编写过滤器MonitorDataFilter
- 过滤器处于客户端和服务器端资源之间,对所有的请求或者响应进行拦截操作
- 我们这里的过滤器主要就是拦截所有GET请求,然后对GET请求的请求参数解码
package org.***.***.filter;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
/**
* 过滤器
* 修改请求体:对请求参数解码
*/
@Component
@WebFilter(urlPatterns = {"/*"}, filterName = "monitorDataFilter")
public class MonitorDataFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
//此处可设置http请求和响应的字符编码格式
servletRequest.setCharacterEncoding("utf-8");
servletResponse.setCharacterEncoding("utf-8");
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String method = request.getMethod();
if ("GET".equals(method)) {
Map<String, String[]> parameterMap = request.getParameterMap();
Map<String, Object> param = parameterMapDecode(parameterMap);
ParameterRequestWrapper parameterRequestWrapper = new ParameterRequestWrapper(request, param);
filterChain.doFilter(parameterRequestWrapper, response);
} else {
filterChain.doFilter(request, response);
}
}
@Override
public void destroy() {
}
/**
* 请求体解码
*/
private Map<String, Object> parameterMapDecode(Map<String, String[]> parameterMap) throws UnsupportedEncodingException {
Map<String, Object> formData = new HashMap<>();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String value = (entry.getValue())[0];
String decode = URLDecoder.decode(value, "UTF-8");
formData.put(entry.getKey(), decode);
}
return formData;
}
}
2.编写ParameterRequestWrapper拦截器
- ServletRequest和HttpServletRequest中的请求参数是不能进行修改的,因此有了ServletRequestWrapper和HttpServletRequestWrapper可以进行修改请求参数,controller中的请求参数,都是通过getParameter(String name) 或者 getParameterValues(String name)这两个方法类赋值转换的,因此重新定义类来修改请求参数,我们继承HttpServletRequestWrapper,然后重写getParameter(String name)、getParameterValues(String name)——取自(https://blog.csdn.net/qq_31289187/article/details/87097008/)
package org.***.***.filter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.Vector;
/**
* 请求参数拦截
*/
public class ParameterRequestWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> params = new HashMap<>();
public ParameterRequestWrapper(HttpServletRequest request) {
super(request);
this.params.putAll(request.getParameterMap());
}
/**
* 重写构造方法
*
* @param request
* @param extendParams
*/
public ParameterRequestWrapper(HttpServletRequest request, Map<String, Object> extendParams) {
this(request);
addAllParameters(extendParams);
}
/**
* 在获取所有参数名,必须重写此方法。
*
* @return
*/
@Override
public Enumeration<String> getParameterNames() {
return new Vector(params.keySet()).elements();
}
/**
* 根据params,修改请求体
* @param name
* @return
*/
@Override
public String getParameter(String name) {
String[] values = params.get(name);
if (values == null || values.length == 0) {
return null;
}
return values[0];
}
/**
* 根据params,修改请求体
* @param name
* @return
*/
@Override
public String[] getParameterValues(String name) {
String[] values = params.get(name);
if (values == null || values.length == 0) {
return null;
}
return values;
}
/**
* 添加多个参数
*
* @param otherParams
*/
public void addAllParameters(Map<String, Object> otherParams) {
for (Map.Entry<String, Object> entry : otherParams.entrySet()) {
addParameter(entry.getKey(), entry.getValue());
}
}
/**
* 添加参数
*
* @param name 参数名
* @param value 参数值
*/
public void addParameter(String name, Object value) {
if (value != null) {
if (value instanceof String[]) {
params.put(name, (String[]) value);
} else if (value instanceof String) {
params.put(name, new String[]{(String) value});
} else {
params.put(name, new String[]{String.valueOf(value)});
}
}
}
}
四、启动测试
经过以上几步,基本就算是写好了这个功能,就可以进入测试阶段了
当请求参数为’&‘,过滤器中解码前的参数为’%26’,解码后为’&‘。
接口的请求参数也是解码后的’&’
那么到这里,其实已经就可以发布到线上用起来了。但是,后面在测试的时候,遇到一个问题,使得过滤器的解码无效。
五、过滤器解码无效
公司项目是个微服务项目,当我把过滤器发布上线后,发现有的服务的列表,输入搜索条件后,无法找到数据,经过一段时间的查找,才发现问题所在,解码失效了。
这里可以看到dictValue的值还是编码后的值,这里唯一不同的点就是,上面是对象接收,这里是map接收,我尝试改为对象接收,发现是可以解码的,但为什么map接收就失效了呢?然后就开始断点调试,发现过滤器也走了,过滤器中的解码也解成功了,好像也没有哪儿出了问题,但接收参数始终是未解码前的,这我就百思不得其解了。既然找到不问题,就度娘嘛,但是查了半天,网上也没有遇到同样的问题。那就没办法了,那就只能走小白最困难的源码跟踪环节,既然是已经解码了,但是请求参数又变回解码前的,那么肯定就是在过滤器放行后到调这个接口的其中某个环节出了问题。这里提一嘴,源码跟踪一直是我比较弱的点,感觉源码方法调来调去,一个接口多个实现,你都不知道往哪儿断点,把人都给绕晕了,所以我很少看源码,这块也一直没有得到提升,这次我非得把这个问题找出来。
六、源码跟踪
其实源码跟踪很简单,你不需要完全了解源码到底写了什么,你只需要抓住你的问题,然后针对性的跟踪即可。
例如,我这里就是请求参数变为未解码前的了,那么只需要知道到底是哪一步把请求参数改变了即可。
1.在接口处打上断点
2.然后查看方法执行链路
小知识:之前我一直不知道如何断点源码,就是不清楚到底是咋执行的,也不知道怎么看,其实很简单,idea的断点有相应的执行链路,它可以显示调用了哪些方法,这里的doFilter()以及parentList()分别是我过滤器的继续执行方法以及接口方法。那么中间这些就是源码调用的方法(很明显,从下往上,就是方法的调用顺序)
3.接下来就是一个方法一个方法的点,找请求体是哪个方法改变的。
经过一番查找发现,在这里,请求参数还是’&’
但是经过这个方法后,就变为‘%26’
4.进入this.getMethodArgumentValues()方法
进入这个方法后,继续断点走,可以发现,经过第一次循环,参数就被更改了。
这里可以看出进入方法前,参数还是对的。
5.进入this.resolvers.resolveArgument()
这里直接再进入resolver.resolveArgument()方法
6.进入resolver.resolveArgument()方法
ok,这里到了源码跟踪最麻烦的一点,到底进入哪个实现?
小知识:其实这里也很简单,只需要再接口打上断点,它会自动跳往下一个实现的方法(之前一直不知道这块,这就是没经常源码断点调试吃的亏)
7.断点后,按f9,进入了resolveArgument()方法
8.继续往下执行
可以看到,当执行webRequest.getParameterMap()方法前,参数还是对的,继续往下。
噢 ~ 噢 ~噢 !参数变了,答案近在咫尺(别看我只写了几步就找到了,因为一次跟,我可跟了好久才发现是这里改变了)。
ok,继续往下,看看是为什么会被改变。
9.进入webRequest.getParameterMap()方法,打上断点
10.进入前面的方法,this.getRequest()。
发现这里的参数是对的,没被改变。接着进入后面的方法,getParameterMap()
11.在getParameterMap()方法,打上断点
12.继续往下执行,进入了getParameterMap()方法
哎嘿,可以看到,这个request的参数就是’%26’,所以返回的也就是未解码的,并没有用我们解码后的请求体,这是为什么呢?
经过我多次断点调试,发现代码会先执行ServletRequestWrapper这个类,这个ServletRequest会先初始化为前端传递的参数,所以它的参数是未解码前的。而我们用map类型作为接受参数,请求参数会从图8webRequest.getParameterMap()的方法获取,而这个方法又会从已经初始化好的ServletRequest里取,就造成了我们解码无效的情况。到这里,源码跟踪结束。
七、解决方案
方式一:最直接的方法就是将请求参数类型改为对象接收,但这种方式,如果接口多了,改起来特别麻烦。
方式二:重写源码,我们只需要重写参数被改变的哪个类即可,也就是图8哪个类。在parameterMap = webRequest.getParameterMap();这行代码之后调用解码的方法即可。
package org.springframework.web.method.annotation;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartRequest;
import org.springframework.web.multipart.support.MultipartResolutionDelegate;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.Part;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
public class RequestParamMapMethodArgumentResolver implements HandlerMethodArgumentResolver {
public RequestParamMapMethodArgumentResolver() {
}
public boolean supportsParameter(MethodParameter parameter) {
RequestParam requestParam = (RequestParam)parameter.getParameterAnnotation(RequestParam.class);
return requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) && !StringUtils.hasText(requestParam.name());
}
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
Class valueType;
HttpServletRequest servletRequest;
Collection parts;
Iterator var10;
Part part;
Map<String, String[]> parameterMap;
MultipartRequest multipartRequest;
if (!MultiValueMap.class.isAssignableFrom(parameter.getParameterType())) {
valueType = resolvableType.asMap().getGeneric(new int[]{1}).resolve();
if (valueType == MultipartFile.class) {
multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
return multipartRequest != null ? multipartRequest.getFileMap() : new LinkedHashMap(0);
} else if (valueType == Part.class) {
servletRequest = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
parts = servletRequest.getParts();
LinkedHashMap<String, Part> result = new LinkedHashMap(parts.size());
var10 = parts.iterator();
while(var10.hasNext()) {
part = (Part)var10.next();
if (!result.containsKey(part.getName())) {
result.put(part.getName(), part);
}
}
return result;
} else {
return new LinkedHashMap(0);
}
} else {
parameterMap = webRequest.getParameterMap();
parameterMapDecode(parameterMap);
Map<String, String> result = new LinkedHashMap(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
result.put(key, values[0]);
}
});
return result;
}
} else {
valueType = resolvableType.as(MultiValueMap.class).getGeneric(new int[]{1}).resolve();
if (valueType == MultipartFile.class) {
multipartRequest = MultipartResolutionDelegate.resolveMultipartRequest(webRequest);
return multipartRequest != null ? multipartRequest.getMultiFileMap() : new LinkedMultiValueMap(0);
} else if (valueType != Part.class) {
parameterMap = webRequest.getParameterMap();
MultiValueMap<String, String> result = new LinkedMultiValueMap(parameterMap.size());
parameterMap.forEach((key, values) -> {
String[] var3 = values;
int var4 = values.length;
for(int var5 = 0; var5 < var4; ++var5) {
String value = var3[var5];
result.add(key, value);
}
});
return result;
} else {
servletRequest = (HttpServletRequest)webRequest.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null && MultipartResolutionDelegate.isMultipartRequest(servletRequest)) {
parts = servletRequest.getParts();
LinkedMultiValueMap<String, Part> result = new LinkedMultiValueMap(parts.size());
var10 = parts.iterator();
while(var10.hasNext()) {
part = (Part)var10.next();
result.add(part.getName(), part);
}
return result;
} else {
return new LinkedMultiValueMap(0);
}
}
}
}
/**
* 请求体解码
*/
public void parameterMapDecode(Map<String, String[]> parameterMap) throws UnsupportedEncodingException {
Map<String, Object> formData = new HashMap<>();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String value = (entry.getValue())[0];
String decode = URLDecoder.decode(value, "UTF-8");
String[] result = new String[1];
result[0] = decode;
entry.setValue(result);
}
}
}
八、再次重启测试
再次重启,解码成功。
九、总结
- 学到了如何找接口的实现方法,直接再接口断点即可
- 对源码跟踪有了更深的认识,提高了定位问题的能力