问题背景:
插入一个:spring boot 如何利用log4j2 打印日志
工程源码 : spring-boot-package
公司在接收其他平台数据的时候,走的是restful api接口,但是有两个特殊要求。
- 需要对整个业务参数块进行加密
- 开发的接口需要 提供swagger ui
- post请求 入参示例如下
- 业务入参如下
{
"token":"accessToken",
"securityKey":"ss212313",
"data":
{
"IMEI":"123",
"state":"off"
}
}
- 实际传参则是直接将data 中的业务数据整块加密
{
"token":"accessToken",
"securityKey":"ss212313",
"data": "加密后的数据"
}
swagger要能展示入参数据结构,接收参数的对象需要与 业务入参的数据结构一致。但实际http
请求传入的参数却是加密后的字符串
问题点:如何让spring 帮我们封装解密后的数据?
如何让spring 对我们新增的属性,自动完成对象封装
先处理一个简单的,比如get请求,如何在请求处理过程中添加属性,并让spring自动将封装到入参对象中。
- 如下设计2个入参对象
@Data
public class CommonVo {
private String name;
private Integer age;
}
@Data
public class CustomerVo {
private String name;
private Integer age;
private String key;
private Son son;
@Data
public class Son{
private String fieldOne;
private String fieldTwo;
}
}
- 如下图所示,在代码中为其新增属性,并在转发后的请求中,能够将新增的属性自动封装到CustomerVo对象中
- 比较常用的操作,通常是 通过Wrapper 对ServletRequest进行封装,这种封装,要获取新增的属性,需要通过 req.getParameter(key) 来获取,spring并不会对添加到Wrapper的属性,进行自动封装
public class RequestParameterWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> params = new HashMap();
public RequestParameterWrapper(HttpServletRequest request) {
super(request);
//将现有parameter传递给params
this.params.putAll(request.getParameterMap());
}
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)});
}
}
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
@Override
public String getParameter(String name) {
String[] values = params.get(name);
if (values == null || values.length == 0) {
return null;
}
return values[0];
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
}
- 只有知道spring 封装数据的原理,才能知道如何让spring将新增的属性封装到对象中
从这里开始需要注意跟踪了
如果你在上一步直接重写 getParameterNames() 方法也可以
我继续debug下去了,然后找的wrapper中的封装的HttpServletRequest中的 parameterNames了。因此走了弯路。
整理出来,对于parameter 的修改需要重写如下4个方法
private Map<String, String[]> params = new HashMap();
public RequestParameterWrapper(HttpServletRequest request) {
super(request);
//将现有parameter传递给params
this.params.putAll(request.getParameterMap());
}
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)});
}
}
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
@Override
public String getParameter(String name) {
String[] values = params.get(name);
if (values == null || values.length == 0) {
return null;
}
return values[0];
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(params.keySet());
}
效果
接下来看一下post请求中,json参数的封装。
post请求,@RequestBody 的数据封装,会报如下的错误
I/O error while reading input message; nested exception is java.io.IOException: Stream closed
在这里出现的原因是,@RequestBody?
先到controller中转发之前,进行调试
直接到 DispatcherServlet
插入一点
GET请求 :
POST请求 :
inputBuffer.streamClosed
回到上一步,也就是说这个InputStream是不能再次读取的。
因为buffer 不执行 flip() 方法时,只能读取一次。转发前将HttpServletRequest封装到Controller里面的时候,已经进行过一次读取。现在封装到wrapper里面又到Request里面去读取一次,肯定要报错。
也是说,对于json数据的封装,wrapper要有自己的inputStream。回到上一步继续分析
如何重写 getInputStream(),我的想法是看一下代码里面有没有已重写过的例子可以参考。
最后的wrapper如下
public class RequestParameterWrapper extends HttpServletRequestWrapper {
private Map<String, String[]> params = new HashMap();
public RequestParameterWrapper(HttpServletRequest request) {
super(request);
//将现有parameter传递给params
this.params.putAll(request.getParameterMap());
}
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)});
}
}
}
@Override
public Map<String, String[]> getParameterMap() {
return params;
}
@Override
public String getParameter(String name) {
String[] values = params.get(name);
if (values == null || values.length == 0) {
return null;
}
return values[0];
}
@Override
public String[] getParameterValues(String name) {
return params.get(name);
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(params.keySet());
}
private JSONObject jsons = new JSONObject();
public void addJson(String key, Object obj) {
jsons.put(key, obj);
}
@Override
public ServletInputStream getInputStream() {
MyServletInputStream coyoteInputStream = new MyServletInputStream(new ByteArrayInputStream(jsons.toJSONString().getBytes()));
return coyoteInputStream;
}
public class MyServletInputStream extends ServletInputStream {
private final InputStream sourceStream;
private boolean finished = false;
public MyServletInputStream(InputStream sourceStream) {
Assert.notNull(sourceStream, "Source InputStream must not be null");
this.sourceStream = sourceStream;
}
@Override
public int read() throws IOException {
int data = this.sourceStream.read();
if (data == -1) {
this.finished = true;
}
return data;
}
@Override
public int available() throws IOException {
return this.sourceStream.available();
}
@Override
public void close() throws IOException {
super.close();
this.sourceStream.close();
}
@Override
public boolean isFinished() {
return this.finished;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
throw new UnsupportedOperationException();
}
}
}
汇总
其实将controller中转发前的数据封装放到filer中,也是完全可以的,只需要判断一下,对于json参数,则按ServletInputStream中封装,其他的按parameters中封装就可以了。
我之所以写着controller层封装,是因为我的接口格式,对外只提供一个请求,然后通过字段 action进行业务转发。
其实总的来说:还是对于 HttpServletRequestWrapper 不够熟悉,所以才碰到这么多问题。
下面是对 封装请求能够重写的方法进行了一个汇总
public class RequestParameterWrapperTest extends HttpServletRequestWrapper {
public RequestParameterWrapperTest(HttpServletRequest request) {
super(request);
}
/**
* 重写的方法,主要来自两大类
* HttpServletRequestWrapper
* ServletRequestWrapper
*
* @return
*/
/**
* parameter 相关,原本是 封装在RequestFacade 中的Request中的coyoteRequest[Request]中的 ParameterMap<String, String[]> parameterMap中
* @param name
* @return
*/
@Override
public String getParameter(String name) {
return super.getParameter(name);
}
@Override
public Map<String, String[]> getParameterMap() {
return super.getParameterMap();
}
@Override
public Enumeration<String> getParameterNames() {
return super.getParameterNames();
}
@Override
public String[] getParameterValues(String name) {
return super.getParameterValues(name);
}
/**
* 默认是保存在 Request 中的 Map<String, Object> attributes中
* @param name
* @param o
*/
@Override
public void setAttribute(String name, Object o) {
super.setAttribute(name, o);
}
@Override
public void removeAttribute(String name) {
super.removeAttribute(name);
}
@Override
public Object getAttribute(String name) {
return super.getAttribute(name);
}
@Override
public Enumeration<String> getAttributeNames() {
return super.getAttributeNames();
}
// @Override
// public ServletInputStream getInputStream() throws IOException {
// return super.getInputStream();
// }
/**
* header 相关
{
"cookie": "JSESSIONID=BEC4AAAD417148E4AA60A98AA425439C",
"postman-token": "36f065f0-7ec0-4908-8189-4943c690b52b",
"host": "localhost:8080",
"connection": "keep-alive",
"cache-control": "no-cache",
"accept-encoding": "gzip, deflate, br",
"user-agent": "PostmanRuntime/7.25.0",
"accept": "*//*"
}
* @param name
* @return
*/
@Override
public String getHeader(String name) {
return super.getHeader(name);
}
@Override
public Enumeration<String> getHeaders(String name) {
return super.getHeaders(name);
}
@Override
public Enumeration<String> getHeaderNames() {
return super.getHeaderNames();
}
@Override
public int getIntHeader(String name) {
return super.getIntHeader(name);
}
@Override
public long getDateHeader(String name) {
return super.getDateHeader(name);
}
/**
* session相关
* @return
*/
@Override
public String getRequestedSessionId() {
return super.getRequestedSessionId();
}
@Override
public HttpSession getSession(boolean create) {
return super.getSession(create);
}
@Override
public HttpSession getSession() {
return super.getSession();
}
@Override
public String changeSessionId() {
return super.changeSessionId();
}
@Override
public boolean isRequestedSessionIdValid() {
return super.isRequestedSessionIdValid();
}
@Override
public boolean isRequestedSessionIdFromCookie() {
return super.isRequestedSessionIdFromCookie();
}
@Override
public boolean isRequestedSessionIdFromURL() {
return super.isRequestedSessionIdFromURL();
}
@Override
public boolean isRequestedSessionIdFromUrl() {
return super.isRequestedSessionIdFromUrl();
}
// UTF-8
@Override
public String getCharacterEncoding() {
return super.getCharacterEncoding();
}
@Override
public void setCharacterEncoding(String enc) throws UnsupportedEncodingException {
super.setCharacterEncoding(enc);
}
// [{"httpOnly":false,"maxAge":-1,"name":"JSESSIONID","secure":false,"value":"BEC4AAAD417148E4AA60A98AA425439C","version":0}]
@Override
public Cookie[] getCookies() {
return super.getCookies();
}
// name=lisi&age=18
@Override
public String getQueryString() {
return super.getQueryString();
}
// /demo/getAppendFieldForwoard
@Override
public String getRequestURI() {
return super.getRequestURI();
}
// http://localhost:8080/demo/getAppendFieldForwoard
@Override
public StringBuffer getRequestURL() {
return super.getRequestURL();
}
// /demo
@Override
public String getContextPath() {
return super.getContextPath();
}
// /getAppendFieldForwoard
@Override
public String getServletPath() {
return super.getServletPath();
}
// localhost
@Override
public String getServerName() {
return super.getServerName();
}
// 8080
@Override
public int getServerPort() {
return super.getServerPort();
}
@Override
public String getRemoteAddr() {
return super.getRemoteAddr();
}
@Override
public String getRemoteHost() {
return super.getRemoteHost();
}
@Override
public int getRemotePort() {
return super.getRemotePort();
}
@Override
public String getRemoteUser() {
return super.getRemoteUser();
}
@Override
public Locale getLocale() {
return super.getLocale();
}
@Override
public int getLocalPort() {
return super.getLocalPort();
}
@Override
public String getLocalName() {
return super.getLocalName();
}
@Override
public String getLocalAddr() {
return super.getLocalAddr();
}
@Override
public Enumeration<Locale> getLocales() {
return super.getLocales();
}
@Override
public String getPathTranslated() {
return super.getPathTranslated();
}
@Override
public Principal getUserPrincipal() {
return super.getUserPrincipal();
}
@Override
public String getProtocol() {
return super.getProtocol();
}
@Override
public String getScheme() {
return super.getScheme();
}
@Override
public String getRealPath(String path) {
return super.getRealPath(path);
}
@Override
public String getPathInfo() {
return super.getPathInfo();
}
@Override
public String getAuthType() {
return super.getAuthType();
}
@Override
public HttpServletMapping getHttpServletMapping() {
return super.getHttpServletMapping();
}
@Override
public String getMethod() {
return super.getMethod();
}
@Override
public boolean isUserInRole(String role) {
return super.isUserInRole(role);
}
@Override
public boolean authenticate(HttpServletResponse response) throws IOException, ServletException {
return super.authenticate(response);
}
@Override
public void login(String username, String password) throws ServletException {
super.login(username, password);
}
@Override
public void logout() throws ServletException {
super.logout();
}
@Override
public Collection<Part> getParts() throws IOException, ServletException {
return super.getParts();
}
@Override
public Part getPart(String name) throws IOException, ServletException {
return super.getPart(name);
}
@Override
public <T extends HttpUpgradeHandler> T upgrade(Class<T> httpUpgradeHandlerClass) throws IOException, ServletException {
return super.upgrade(httpUpgradeHandlerClass);
}
@Override
public PushBuilder newPushBuilder() {
return super.newPushBuilder();
}
@Override
public Map<String, String> getTrailerFields() {
return super.getTrailerFields();
}
@Override
public boolean isTrailerFieldsReady() {
return super.isTrailerFieldsReady();
}
@Override
public ServletRequest getRequest() {
return super.getRequest();
}
@Override
public void setRequest(ServletRequest request) {
super.setRequest(request);
}
@Override
public int getContentLength() {
return super.getContentLength();
}
@Override
public long getContentLengthLong() {
return super.getContentLengthLong();
}
@Override
public String getContentType() {
return super.getContentType();
}
@Override
public BufferedReader getReader() throws IOException {
return super.getReader();
}
@Override
public boolean isSecure() {
return super.isSecure();
}
@Override
public RequestDispatcher getRequestDispatcher(String path) {
return super.getRequestDispatcher(path);
}
@Override
public ServletContext getServletContext() {
return super.getServletContext();
}
@Override
public AsyncContext startAsync() throws IllegalStateException {
return super.startAsync();
}
@Override
public AsyncContext startAsync(ServletRequest servletRequest, ServletResponse servletResponse) throws IllegalStateException {
return super.startAsync(servletRequest, servletResponse);
}
@Override
public boolean isAsyncStarted() {
return super.isAsyncStarted();
}
@Override
public boolean isAsyncSupported() {
return super.isAsyncSupported();
}
@Override
public AsyncContext getAsyncContext() {
return super.getAsyncContext();
}
@Override
public boolean isWrapperFor(ServletRequest wrapped) {
return super.isWrapperFor(wrapped);
}
@Override
public boolean isWrapperFor(Class<?> wrappedType) {
return super.isWrapperFor(wrappedType);
}
@Override
public DispatcherType getDispatcherType() {
return super.getDispatcherType();
}
}