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.