Preface
This article will introduce several methods to obtain request objects in the web system developed by Spring MVC and discuss its thread safety. I won’t say much below, let’s take a look at the detailed introduction together.
Overview
When developing a web system using Spring MVC, you often need to use a request object when processing requests, such as obtaining the client IP address, the requested URL, attributes in the header (such as cookies, authorization information), data in the body, etc. Since in Spring MVC, the Controller, Service and other objects that handle requests are singletons, the most important issue to be paid attention to when obtaining request objects is whether the request object is thread-safe: when there are a large number of concurrent requests, can it be guaranteed that different request objects are used in different requests/threads?
There is another question to note here: where do I use the request object "when processing a request" mentioned earlier? Considering that there are slight differences in the methods of obtaining request objects, they can be roughly divided into two categories:
1) Use request objects in Spring beans: include both MVC beans such as Controller, Service, Repository, and ordinary Spring beans such as Component. For convenience of explanation, the beans in Spring in the following text are all referred to as beans for short.
2) Use request objects in non-beans: such as in methods of ordinary Java objects, or in static methods of classes.
In addition, this article discusses around the request object representing the request, but the method used is also applicable to the response object, InputStream/Reader, OutputStream/Writer, etc.; where InputStream/Reader can read the data in the request, and OutputStream/Writer can write data to the response.
Finally, the method of obtaining the request object is also related to the version of Spring and MVC; this article is discussed based on Spring 4, and the experiments performed are all using version 4.1.1.
How to test thread safety
Since the thread safety issues of the request object need special attention, in order to facilitate the discussion below, let’s first explain how to test whether the request object is thread-safe.
The basic idea of testing is to simulate a large number of concurrent requests on the client, and then determine whether the requests are used on the server.
The most intuitive way to determine whether the request object is the same is to print out the address of the request object. If it is the same, it means that the same object is used. However, in almost all web server implementations, thread pools are used, which leads to two requests that arrive in succession, which may be processed by the same thread: after the previous request is processed, the thread pool reclaims the thread and reassigns the thread to the subsequent request. In the same thread, the request object used is likely to be the same (the address is the same, the attributes are different). Therefore, even for thread-safe methods, the request object addresses used by different requests may be the same.
To avoid this problem, one method is to let the thread sleep for a few seconds during the request processing process, which can make each thread work long enough to avoid the same thread allocating to different requests; the other method is to use other attributes of the request (such as parameters, header, body, etc.) as the basis for whether the request is thread-safe, because even if different requests use the same thread one after another (the address of the request object is the same), as long as the request object is constructed twice using different attributes, the use of the request object is thread-safe. This paper uses the second method for testing.
The client test code is as follows (create 1000 threads to send requests separately):
public class Test { public static void main(String[] args) throws Exception { String prefix = UUID.randomUUID().toString().replaceAll("-", "") + "::"; for (int i = 0; i < 1000; i++) { final String value = prefix + i; new Thread() { @Override public void run() { try { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet("http://localhost:8080/test?key=" + value); httpClient.execute(httpGet); httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } } } } }.start(); } }}The Controller code in the server is as follows (the code for obtaining the request object is temporarily omitted):
@Controllerpublic class TestController { // Store existing parameters to determine whether the parameters are duplicated, thereby determining whether the thread is safe public static Set<String> set = new HashSet<>(); @RequestMapping("/test") public void test() throws InterruptedException { // ……………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………………… "/t appears repeatedly, request concurrency is not safe!"); } else { System.out.println(value); set.add(value); } // The simulation program has been executed for a period of time, Thread.sleep(1000); }}If the request object is thread-safe, the print result in the server is as follows:
If there is a thread safety issue, the print result in the server may look like this:
If there is no special description, the test code will be omitted from the codes later in this article.
Method 1: Add parameters to the Controller
Code Example
This method is the simplest to implement, and directly enter the Controller code:
@Controllerpublic class TestController { @RequestMapping("/test") public void test(HttpServletRequest request) throws InterruptedException { // The simulation program has been executed for a period of time Thread.sleep(1000); }}The principle of this method is that when the Controller method starts processing the request, Spring will assign the request object to the method parameters. In addition to the request object, there are many parameters that can be obtained through this method. For details, please refer to: https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods
After obtaining the request object in the Controller, if you want to use the request object in other methods (such as service methods, tool-class methods, etc.), you need to pass the request object as a parameter when calling these methods.
Thread safety
Test results: thread safety
Analysis: At this time, the request object is a method parameter, which is equivalent to a local variable, and is undoubtedly thread-safe.
Pros and cons
The main disadvantage of this method is that the request object is too redundant to write, which is mainly reflected in two points:
1) If the request object is required in multiple controller methods, then the request parameter needs to be added to each method.
2) The acquisition of the request object can only start from the controller. If the place where the request object is used is in a deeper place of the function call level, then all methods on the entire call chain need to add the request parameter.
In fact, during the entire request processing process, the request object runs through the entire request; that is, except for special cases such as timers, the request object is equivalent to a global variable inside the thread. This method is equivalent to passing this global variable around.
Method 2: Automatic injection
Code Example
First upload the code:
@Controllerpublic class TestController{ @Autowired private HttpServletRequest request; //Autowired request @RequestMapping("/test") public void test() throws InterruptedException{ //The simulation program has been executed for a period of time Thread.sleep(1000); }} Thread safety
Test results: thread safety
Analysis: In Spring, the scope of the Controller is singleton (singleton), which means that in the entire web system, there is only one TestController; but the injected request is thread-safe, because:
In this way, when the bean (TestController in this example) is initialized, Spring does not inject a request object, but a proxy; when the bean needs to use the request object, the request object is obtained through the proxy.
The following is a description of this implementation through specific code.
Add breakpoints to the above code and view the properties of the request object as shown in the figure below:
As can be seen in the figure, request is actually a proxy: the implementation of the proxy is shown in the internal class of AutowireUtils
ObjectFactoryDelegatingInvocationHandler: /** * Reflective InvocationHandler for lazy access to the current target object. */@SuppressWarnings("serial")private static class ObjectFactoryDelegatingInvocationHandler implements InvocationHandler, Serializable { private final ObjectFactory<?> objectFactory; public ObjectFactoryDelegatingInvocationHandler(ObjectFactory<?> objectFactory) { this.objectFactory = objectFactory; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // ...Omit irrelevant code try { return method.invoke(this.objectFactory.getObject(), args); // Agent implementation core code} catch (InvocationTargetException ex) { throw ex.getTargetException(); } }}In other words, when we call the method of the request, we actually call the method method of the object generated by objectFactory.getObject(); the object generated by objectFactory.getObject() is the real request object.
Continue to observe the above figure and find that the objectFactory type is the internal class RequestObjectFactory of WebApplicationContextUtils; and the RequestObjectFactory code is as follows:
/** * Factory that exposes the current request object on demand. */@SuppressWarnings("serial")private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable { @Override public ServletRequest getObject() { return currentRequestAttributes().getRequest(); } @Override public String toString() { return "Current HttpServletRequest"; }}Among them, to obtain the request object, you need to call the currentRequestAttributes() method to obtain the RequestAttributes object. The implementation of this method is as follows:
/** * Return the current RequestAttributes instance as ServletRequestAttributes. */private static ServletRequestAttributes currentRequestAttributes() { RequestAttributes requestAttr = RequestContextHolder.currentRequestAttributes(); if (!(requestAttr instanceof ServletRequestAttributes)) { throw new IllegalStateException("Current request is not a servlet request"); } return (ServletRequestAttributes) requestAttr;}The core code that generates the RequestAttributes object is in the class RequestContextHolder, where the relevant code is as follows (the unrelated code in the class is omitted):
public abstract class RequestContextHolder { public static RequestAttributes currentRequestAttributes() throws IllegalStateException { RequestAttributes attributes = getRequestAttributes(); // irrelevant logic is omitted here...... return attributes; } public static RequestAttributes getRequestAttributes() { RequestAttributes attributes = requestAttributesHolder.get(); if (attributes == null) { attributes = inheritableRequestAttributesHolder.get(); } return attributes; } private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal<RequestAttributes>("Request attributes"); private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal<RequestAttributes>("Request context");}From this code, we can see that the generated RequestAttributes object is a thread local variable (ThreadLocal), so the request object is also a thread local variable; this ensures the thread safety of the request object.
Pros and cons
The main advantages of this method:
1) Injection is not limited to Controller: In Method 1, only the request parameter can be added to Controller. For method 2, it can not only be injected in the Controller, but also in any bean, including Service, Repository and ordinary beans.
2) The injected object is not limited to request: In addition to injecting the request object, this method can also inject other objects with scope as request or session, such as response objects, session objects, etc.; and ensure thread safety.
3) Reduce code redundancy: Just inject the request object into the bean that requires the request object, and it can be used in various methods of the bean, which greatly reduces code redundancy compared with method 1.
However, this method also has code redundancy. Consider this scenario: there are many controllers in the web system, and each controller uses a request object (this scenario is actually very frequent). At this time, you need to write code to inject request many times; if you also need to inject response, the code will be even more cumbersome. The following describes the improvement of the automatic injection method and analyzes its thread safety and advantages and disadvantages.
Method 3: Automatic injection into the base class
Code Example
Compared with method 2, put the injected part of the code into the base class.
Base class code:
public class BaseController { @Autowired protected HttpServletRequest request; }The Controller code is as follows; here are two derived classes of BaseController. Since the test code will be different at this time, the server test code is not omitted; the client also needs to make corresponding modifications (send a large number of concurrent requests to the two URLs at the same time).
@Controllerpublic class TestController extends BaseController { // Store existing parameters to determine whether the parameter value is repeated, thereby determining whether the thread is safe public static Set<String> set = new HashSet<>(); @RequestMapping("/test") public void test() throws InterruptedException { String value = request.getParameter("key"); // Check thread safety if (set.contains(value)) { System.out.println(value + "/t appears repeatedly, request concurrency is not safe!"); } else { System.out.println(value); set.add(value); } // The simulation program has been executed for a period of time Thread.sleep(1000); }} @Controllerpublic class Test2Controller extends BaseController { @RequestMapping("/test2") public void test2() throws InterruptedException { String value = request.getParameter("key"); // Judge thread safety (use a set with TestController to judge) if (TestController.set.contains(value)) { System.out.println(value + "/t appears repeatedly, request concurrency is not safe!"); } else { System.out.println(value); TestController.set.add(value); } // The simulation program has been executed for a period of time, Thread.sleep(1000); }} Thread safety
Test results: thread safety
Analysis: Based on understanding the thread safety of method 2, it is easy to understand that method 3 is thread-safe: when creating different derived class objects, the domains in the base class (here is the injected request) will occupy different memory space in different derived class objects, that is, putting the code injected request in the base class has no impact on thread safety; the test results also prove this.
Pros and cons
Compared with method 2, repeated injection of requests in different controllers is avoided; however, considering that java only allows inheritance of one base class, if the controller needs to inherit other classes, this method is no longer easy to use.
Whether it is method 2 or method 3, you can only inject requests into the bean; if other methods (such as static methods in the tool class) need to use request objects, you need to pass the request parameters when calling these methods. Method 4 introduced below can be used directly in static methods such as tool classes (of course, it can also be used in various beans).
Method 4: Call manually
Code Example
@Controllerpublic class TestController { @RequestMapping("/test") public void test() throws InterruptedException { HttpServletRequest request = ((ServletRequestAttributes) (RequestContextHolder.currentRequestAttributes())).getRequest(); // The simulation program has been executed for a period of time Thread.sleep(1000); }} Thread safety
Test results: thread safety
Analysis: This method is similar to Method 2 (automatic injection), except that it is implemented through automatic injection in Method 2, and this method is implemented through manual method call. Therefore, this method is also thread-safe.
Pros and cons
Advantages: Can be obtained directly in non-beans. Disadvantages: If you use more places, the code is very cumbersome; therefore, it can be used in conjunction with other methods.
Method 5: @ModelAttribute method
Code Example
The following method and its variants (mutation: put request and bindRequest in subclasses) are often seen online:
@Controllerpublic class TestController { private HttpServletRequest request; @ModelAttribute public void bindRequest(HttpServletRequest request) { this.request = request; } @RequestMapping("/test") public void test() throws InterruptedException { // The simulation program has been executed for a period of time Thread.sleep(1000); }} Thread safety
Test result: thread is not safe
Analysis: When @ModelAttribute annotation is used to modify the method in the Controller, its function is that the method will be executed before each @RequestMapping method in the Controller is executed. Therefore, in this example, the function of bindRequest() is to assign a value to the request object before test() is executed. Although the parameter request in bindRequest() itself is thread-safe, since TestController is singleton, request, as a field of TestController, cannot guarantee thread-safety.
Summarize
To sum up, adding parameters (Method 1), automatic injection (Method 2 and Method 3), and manual calls (Method 4) in the Controller are all thread-safe and can all be used to obtain request objects. If the request object is used less in the system, either method can be used; if it is used more, it is recommended to use automatic injection (Method 2 and Method 3) to reduce code redundancy. If you need to use a request object in a non-bean, you can either pass it in through parameters when calling the upper layer, or you can directly obtain it through manual calls (Method 4).
Okay, the above is the entire content of this article. I hope that the content of this article has certain reference value for everyone's study or work. If you have any questions, you can leave a message to communicate. Thank you for your support to Wulin.com.
References