The preface
Recently xiao Ming took over the former colleague’s code, unexpected, reasonable encountered pit.
In order to avoid falling into the same pit twice, Xiao Ming decided to write down the pit, and set up a big sign in front of the pit, to avoid other partners falling in.
HTTPClient emulated the call
To illustrate this, let’s start with the simplest OF HTTP calls.
Set up the body
The service side
The server code is as follows:
@Controller
@RequestMapping("/")
public class ReqController {
@PostMapping(value = "/body")
@ResponseBody
public String body(HttpServletRequest httpServletRequest) {
try {
String body = StreamUtil.toString(httpServletRequest.getInputStream());
System.out.println("Requested body:" + body);
// From the parameter
return body;
} catch (IOException e) {
e.printStackTrace();
returne.getMessage(); }}}Copy the code
How does the Java client request that the server read the body passed in?
The client
This problem is certainly not difficult for you, there are many ways to do this.
Let’s take Apache HttpClient as an example:
// Post request with set parameters
public static String post(String url, String body) {
try {
// Send a POST request via HttpPost
HttpPost httpPost = new HttpPost(url);
StringEntity stringEntity = new StringEntity(body);
// Pass our Entity object through setEntity
httpPost.setEntity(stringEntity);
return execute(httpPost);
} catch (UnsupportedEncodingException e) {
throw newRuntimeException(e); }}// Execute the request and return the response data
private static String execute(HttpRequestBase http) {
try {
CloseableHttpClient client = HttpClients.createDefault();
// Invoke the execute method through the client
CloseableHttpResponse Response = client.execute(http);
// Get the response data
HttpEntity entity = Response.getEntity();
// Convert the data to a string
String str = EntityUtils.toString(entity, "UTF-8");
/ / close
Response.close();
return str;
} catch (IOException e) {
throw newRuntimeException(e); }}Copy the code
You can find httpClient is still very convenient after encapsulation.
We can set setEntity to the StringEntity of the incoming parameter.
test
To verify the correctness, Xiao Ming implements a validation method locally.
@Test
public void bodyTest(a) {
String url = "http://localhost:8080/body";
String body = buildBody();
String result = HttpClientUtils.post(url, body);
Assert.assertEquals("body", result);
}
private String buildBody(a) {
return "body";
}
Copy the code
Very relaxed, Xiao Ming let out the dragon King’s smile.
Set the parameter
The service side
Xiaoming also saw that there is a server side code implementation as follows:
@PostMapping(value = "/param")
@ResponseBody
public String param(HttpServletRequest httpServletRequest) {
// From the parameter
String param = httpServletRequest.getParameter("id");
System.out.println("param: " + param);
return param;
}
private Map<String,String> buildParamMap(a) {
Map<String,String> map = new HashMap<>();
map.put("id"."123456");
return map;
}
Copy the code
All parameters are fetched by the getParameter method. How do we do that?
The client
This is not difficult, Xiao Ming thought.
Since a lot of code has been done this way before, CTRL +CV does the following:
// Post request with set parameters
public static String post(String url, Map<String, String> paramMap) {
List<NameValuePair> nameValuePairs = new ArrayList<>();
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue());
nameValuePairs.add(pair);
}
return post(url, nameValuePairs);
}
// Post request with set parameters
private static String post(String url, List<NameValuePair> list) {
try {
// Send a POST request via HttpPost
HttpPost httpPost = new HttpPost(url);
// The Entity is found to be an interface, so we can only find the implementation class. The implementation class needs a collection. The generic type of the collection is NameValuePair
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(list);
// Pass our Entity object through setEntity
httpPost.setEntity(formEntity);
return execute(httpPost);
} catch (Exception exception) {
throw newRuntimeException(exception); }}Copy the code
This is the most common paramMap, easy to build; And the specific implementation of the way away, also facilitate the later expansion.
The servlet standard
UrlEncodedFormEntity looks bland and indicates that this is a post form request.
The following criteria must be met before the parameter collection of a post form can be used.
1. The request is HTTP or HTTPS 2. The request method is POST 3. Application /x-www-form-urlencoded 4. The servlet has called the relevant getParameter method on the request object.Copy the code
If the above conditions are not met, the data from the POST form is not set to the parameter set, but can still be obtained through the InputStream of the Request object.
When these conditions are met, the data from the POST form is no longer available in the InputStream of the Request object.
This is a very important agreement, resulting in a lot of small partners more obscure circle.
test
Therefore, Xiao Ming also wrote the corresponding test case:
@Test
public void paramTest(a) {
String url = "http://localhost:8080/param";
Map<String,String> map = buildParamMap();
String result = HttpClientUtils.post(url, map);
Assert.assertEquals("123456", result);
}
Copy the code
If only dating could be like programming.
Xiao Ming thought, but could not help but frown, found that things are not simple.
Set parameter and body
The service side
One of the request’s input parameters is large, so it is put in body. The other parameters are still put in paramter.
@PostMapping(value = "/paramAndBody")
@ResponseBody
public String paramAndBody(HttpServletRequest httpServletRequest) {
try {
// From the parameter
String param = httpServletRequest.getParameter("id");
System.out.println("param: " + param);
String body = StreamUtil.toString(httpServletRequest.getInputStream());
System.out.println("Requested body:" + body);
// From the parameter
return param+"-"+body;
} catch (IOException e) {
e.printStackTrace();
returne.getMessage(); }}Copy the code
StreamUtil#toString is a utility class for simple flow handling.
/** * Convert to string *@paramInputStream flow *@returnResults *@since1.0.0 * /
public static String toString(final InputStream inputStream) {
if (inputStream == null) {
return null;
}
try {
int length = inputStream.available();
final Reader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
final CharArrayBuffer buffer = new CharArrayBuffer(length);
final char[] tmp = new char[1024];
int l;
while((l = reader.read(tmp)) ! = -1) {
buffer.append(tmp, 0, l);
}
return buffer.toString();
} catch (Exception exception) {
throw newRuntimeException(exception); }}Copy the code
The client
How do you set parameters and body in HttpClient?
You can try it for yourself.
Xiao Ming tried a variety of methods, found a cruel reality – httpPost can only set an Entity, also tried to see a variety of subclasses, but LUAN.
Just when Ming wanted to give up, he suddenly thought that paramter could be realized by splicing URLS.
So we’re going to set the parameter and the URL to a new URL, and the body is going to be set the same way as before.
The implementation code is as follows:
// Post request with set parameters
public static String post(String url, Map
paramMap, String body)
,> {
try {
List<NameValuePair> nameValuePairs = new ArrayList<>();
for (Map.Entry<String, String> entry : paramMap.entrySet()) {
NameValuePair pair = new BasicNameValuePair(entry.getKey(), entry.getValue());
nameValuePairs.add(pair);
}
/ / build the url
// Construct the request path and add parameters
URI uri = new URIBuilder(url).addParameters(nameValuePairs).build();
/ / structure HttpClient
CloseableHttpClient httpClient = HttpClients.createDefault();
// Send a POST request via HttpPost
HttpPost httpPost = new HttpPost(uri);
httpPost.setEntity(new StringEntity(body));
// Get the response
// Invoke the execute method through the client
CloseableHttpResponse Response = httpClient.execute(httpPost);
// Get the response data
HttpEntity entity = Response.getEntity();
// Convert the data to a string
String str = EntityUtils.toString(entity, "UTF-8");
/ / close
Response.close();
return str;
} catch (URISyntaxException | IOException | ParseException e) {
throw newRuntimeException(e); }}Copy the code
Here’s how to build a new URL by using new URIBuilder(URL).addParameters(nameValuePairs).build(), of course you can use &key=value to concatenate it yourself.
The test code
@Test
public void paramAndBodyTest(a) {
String url = "http://localhost:8080/paramAndBody";
Map<String,String> map = buildParamMap();
String body = buildBody();
String result = HttpClientUtils.post(url, map, body);
Assert.assertEquals("123456-body", result);
}
Copy the code
The test passed. It was perfect.
A new journey
Of course, the general article should end here.
But that’s not the point of this article. Our story has just begun.
Log demand
Geese fly over, the sky will leave his traces.
The process should be even more so.
In order to easily trace the problem, we usually log the incoming parameters of the call.
In order to facilitate code expansion and maintainability, Xiao Ming of course adopts the interceptor approach.
Log interceptor
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.util.StreamUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
/** * log interceptor@authorAn old horse whistles against the west wind@since1.0.0 * /
@Component
public class LogHandlerInterceptor implements HandlerInterceptor {
private Logger logger = LoggerFactory.getLogger(LogHandlerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
// Get parameter information
Enumeration<String> enumeration = httpServletRequest.getParameterNames();
while (enumeration.hasMoreElements()) {
String paraName = enumeration.nextElement();
logger.info("Param name: {}, value: {}", paraName, httpServletRequest.getParameter(paraName));
}
// Get the body information
String body = StreamUtils.copyToString(httpServletRequest.getInputStream(), StandardCharsets.UTF_8);
logger.info("body: {}", body);
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {}@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {}}Copy the code
Very simple to understand, input and input parameter parameters and body information.
Then specify the scope to take effect:
@Configuration
public class SpringMvcConfig extends WebMvcConfigurerAdapter {
@Autowired
private LogHandlerInterceptor logHandlerInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(logHandlerInterceptor)
.addPathPatterns("/ * *");
super.addInterceptors(registry); }}Copy the code
All requests will take effect.
Where’s my inputStream?
Is there anything wrong with the log interceptor?
If so, what should be done about it?
Xiao Ming thought everything would go well after he finished writing. As soon as he ran the test case, he was torn apart.
All Controller methods of it. GetInputStream () the contents of all become empty.
Who is it? Stole my inputStream?
On second thought, Xiao Ming found the problem.
A stream can only be read once, and once a stream is read, it cannot be read again.
But the log must output, then what should be done?
It is not definitely
Technology to Google, gossip to twitter.
So Xiaoming went to check, the solution is more direct, rewrite.
Rewrite HttpServletRequestWrapper
Rewrite HttpServletRequestWrapper first, every time to read the flow of information stored, easy to repeat reading.
/ * * *@author binbin.hou
* @since1.0.0 * /
public class MyHttpServletRequestWrapper extends HttpServletRequestWrapper {
private byte[] requestBody = null;// To save the stream
public MyHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
requestBody = StreamUtils.copyToByteArray(request.getInputStream());
}
@Override
public ServletInputStream getInputStream(a) {
final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
return new ServletInputStream() {
@Override
public int read(a) {
return bais.read(); // Read the data in requestBody
}
@Override
public boolean isFinished(a) {
return false;
}
@Override
public boolean isReady(a) {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {}}; }@Override
public BufferedReader getReader(a) throws IOException {
return new BufferedReader(newInputStreamReader(getInputStream())); }}Copy the code
To realize the Filter
We rewrite the above MyHttpServletRequestWrapper when take effect?
We can implement a Filter to replace the original request:
import org.springframework.stereotype.Component;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
/ * * *@author binbin.hou
* @since1.0.0 * /
@Component
public class HttpServletRequestReplacedFilter implements Filter {
@Override
public void destroy(a) {}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
// Make the substitution
if(request instanceof HttpServletRequest) {
requestWrapper = new MyHttpServletRequestWrapper((HttpServletRequest) request);
}
if(requestWrapper == null) {
chain.doFilter(request, response);
} else{ chain.doFilter(requestWrapper, response); }}@Override
public void init(FilterConfig arg0) throws ServletException {}}Copy the code
Then you can see that everything is fine, xiao Ming mouth leaked out of the dragon king smile.
summary
In this article, the original problem is simplified. When you actually encounter this problem, it is simply an interceptor + parameter and body request.
So it’s a bit of a waste of time trying to figure out the whole problem.
But time wasted without reflection is time wasted.
The two core points are:
(1) Understanding of the Servlet standard.
(2) Understanding of stream reading and some knowledge about Spring.
I am an old horse, looking forward to the next reunion with you.