7

During the project development process, do you really want to define a global variable, the scope is for a single request request, and it can be obtained at any time during the entire request process. When using feign, dubbo, etc. for service calls, it would be better if the scope of the variable could be passed to the entire microservice chain. This is the effect that this article wants to achieve. When I was just working on Oracle ADF development, I could define variables based on the RequestScope scope.

In the previous article "Full-Link Log of Microservices (Sleuth+MDC)" , we implemented the full-link log based on the framework of spring cloud sleuth and MDC Full pass of values such as traceId. This article is a companion article, but some implementation frameworks are different. This article is based on spring's own RequestContextHolder and servlet's HttpServletRequest .

1. Single service single thread implementation

If you only want to implement the scope within a single service, and the logic of the entire API is single-threaded, then the easiest solution to think of is ThreadLocal . Define a ThreadLocal variable, assign it at each API request, and clear it after the request ends. Many of our frameworks handle this logic in AOP.

But now it's easier, the Spring framework comes with RequestContextHolder naturally supports this. There must be a container that provides the Getter/Setter method for storing variables. Let's introduce HttpServletRequest below.

1.1. HttpServletRequest

Everyone should be familiar with HttpServletRequest. In an API request, all client requests are encapsulated into an HttpServletRequest object. This object is created when an API request is made and destroyed after the response is complete, making it a very suitable container for the Request scope.

1. Attribute and Header, Parameter

The method of putting and getting variables into the container can be implemented by the setAttribute/getAttribute method of the HttpServletRequest object. Nowadays, everyone may be unfamiliar with Attribute . It was used more in the early JSP development, and it was used for value transfer between Servlets, but it is also very suitable for the current scene.

Some people say that why not use Header and Parameter? They are also containers within the Request scope. There are two simple points:

  1. Header , Parameter were not designed for server-side containers, so they can only be assigned values on the client side, and only Getter interface, and no Setter interface. But Attribute also provides Getter/Setter interface.
  2. Header , Parameter storage object Value all String string, but also facilitate convenient client data transfer protocol based on HTTP. But Attribute which stores objects Value is Object , which is more suitable for storing various types of objects.

So in web development, how do we get the HttpServletRequest object every day?

2. Three ways to get HttpServletRequest
  1. Write HttpServletRequest on the method parameters of the Controller, so that each time a request comes, the corresponding HttpServletRequest is obtained. When other layers such as Service need to be used, the controller starts to pass layer by layer. Obviously, insurance, but the code doesn't look pretty.

     @GetMapping("/req")
    public void req(HttpServletRequest request) {...}
  2. Using RequestContextHolder, you can use the following method to get HttpServletRequest directly where you need it:

     public static HttpServletRequest getRequestByContext() {
         HttpServletRequest request = null;
         RequestAttributes ra = RequestContextHolder.getRequestAttributes();
         if (ra instanceof ServletRequestAttributes) {
             ServletRequestAttributes sra = (ServletRequestAttributes) ra;
             request = sra.getRequest();
         }
         return request;
     }
  3. Get HttpServletRequest directly through @Autowired.

     @Autowired
    HttpServletRequest request;

Among them, the principles of the second and third methods are the same. This is because the Spring framework also obtains the value through RequestContextHolder.currentRequestAttributes() in the source code of dynamically generated HttpServletRequest Bean, so that it can be injected through @Autowired.

The following is a detailed introduction RequestContextHolder .

1.2. RequestContextHolder

1. RequestContextHolder tool class

Let's take a look at the source code of RequestContextHolder first:

 public abstract class RequestContextHolder {
    private static final boolean jsfPresent = ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
    private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");

    public RequestContextHolder() {
    }

    public static void resetRequestAttributes() {
        requestAttributesHolder.remove();
        inheritableRequestAttributesHolder.remove();
    }

    public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
        setRequestAttributes(attributes, false);
    }

    public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
        if (attributes == null) {
            resetRequestAttributes();
        } else if (inheritable) {
            inheritableRequestAttributesHolder.set(attributes);
            requestAttributesHolder.remove();
        } else {
            requestAttributesHolder.set(attributes);
            inheritableRequestAttributesHolder.remove();
        }

    }

    @Nullable
    public static RequestAttributes getRequestAttributes() {
        RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
        if (attributes == null) {
            attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
        }

        return attributes;
    }

    public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
        RequestAttributes attributes = getRequestAttributes();
        if (attributes == null) {
            if (jsfPresent) {
                attributes = RequestContextHolder.FacesRequestAttributesFactory.getFacesRequestAttributes();
            }

            if (attributes == null) {
                throw new IllegalStateException("No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.");
            }
        }

        return attributes;
    }

    private static class FacesRequestAttributesFactory {
        private FacesRequestAttributesFactory() {
        }

        @Nullable
        public static RequestAttributes getFacesRequestAttributes() {
            FacesContext facesContext = FacesContext.getCurrentInstance();
            return facesContext != null ? new FacesRequestAttributes(facesContext) : null;
        }
    }
}

Two important points can be paid attention to:

  1. RequestContextHolder is also based on ThreadLocal , which provides Getter/Setter method based on local threads, but loses variable values if it is cross-threaded.
  2. RequestContextHolder can be implemented based on InheritableThreadLocal , so that the implementation can also obtain the value of the current thread from the child thread.

This is very similar to what was said in the previous article MDC . The tool class of RequestContextHolder is very simple, so where does the Spring framework store the RequestContextHolder value, and where does it destroy it?

2. Spring MVC implementation

Let's take a look at the FrameworkServlet class, which has a processRequest method. According to the method name, we can also roughly understand that this method is used to process requests.
FrameworkServlet.java

 protected final void processRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        long startTime = System.currentTimeMillis();
        Throwable failureCause = null;
        LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
        LocaleContext localeContext = this.buildLocaleContext(request);
        RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
        ServletRequestAttributes requestAttributes = this.buildRequestAttributes(request, response, previousAttributes);
        WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
        asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new FrameworkServlet.RequestBindingInterceptor());
        this.initContextHolders(request, localeContext, requestAttributes);

        try {
            this.doService(request, response);
        } catch (IOException | ServletException var16) {
            failureCause = var16;
            throw var16;
        } catch (Throwable var17) {
            failureCause = var17;
            throw new NestedServletException("Request processing failed", var17);
        } finally {
            this.resetContextHolders(request, previousLocaleContext, previousAttributes);
            if (requestAttributes != null) {
                requestAttributes.requestCompleted();
            }

            this.logResult(request, response, (Throwable)failureCause, asyncManager);
            this.publishRequestHandledEvent(request, response, startTime, (Throwable)failureCause);
        }

    }

this.doService(request, response); is to execute specific business logic, and the two points we focus on are before and after this method:

  • Set the current request RequestContextHolder value, this.initContextHolders(request, localeContext, requestAttributes); corresponding method code is as follows:

     private void initContextHolders(HttpServletRequest request, @Nullable LocaleContext localeContext, @Nullable RequestAttributes requestAttributes) {
          if (localeContext != null) {
              LocaleContextHolder.setLocaleContext(localeContext, this.threadContextInheritable);
          }
    
          if (requestAttributes != null) {
              RequestContextHolder.setRequestAttributes(requestAttributes, this.threadContextInheritable);
          }
    
      }
  • When the execution is completed or an exception is thrown, you need to reset the RequestContextHolder value, that is, clear the current RequestContextHolder value and set it to the previous value, this.resetContextHolders(request, previousLocaleContext, previousAttributes); The corresponding method code is as follows:

     private void resetContextHolders(HttpServletRequest request, @Nullable LocaleContext prevLocaleContext, @Nullable RequestAttributes previousAttributes) {
          LocaleContextHolder.setLocaleContext(prevLocaleContext, this.threadContextInheritable);
          RequestContextHolder.setRequestAttributes(previousAttributes, this.threadContextInheritable);
      }

2. Single-service multi-thread implementation

In a single service, when we develop with multiple threads, what should we do if we want to obtain HttpServletRequest through RequestContextHolder in child threads?

Some people say that in the tool class of RequestContextHolder, the implementation method of InheritableThreadLocal is provided, and the demand can be realized.

It is clearly not recommended to use the implementation of InheritableThreadLocal here. In fact, in the previous article, it was mentioned that it is not recommended to use InheritableThreadLocal to implement multi-threaded delivery of MDC. Here too, it is recommended to use 线程池的装饰器模式 instead of InheritableThreadLocal . Let's do a comparison and explain why.

1. Limitations of InheritableThreadLocal

The limitation of ThreadLocal is that it cannot be passed between parent and child threads. i.e. thread-local variables set in the parent thread cannot be accessed in the child thread. Later to solve this problem, a new class InheritableThreadLocal was introduced.

After using this method, the child thread can access the local thread variable of the parent thread when the child thread is created . The implementation principle is to copy the local thread variable currently existing in the parent thread to the local thread variable of the child thread when the parent thread creates the child thread. middle.

Everyone pay attention to the bolded words " when creating a child thread " above, which is the limitation of InheritableThreadLocal .

As we all know, a major feature of thread pools is that threads can be recycled and reused after they are created. This means that if a thread pool is used to create a thread, when using InheritableThreadLocal, only the newly created thread can correctly inherit the value of the parent thread, and subsequent reused threads will not update the value.

2. Decoration mode of thread pool

The setTaskDecorator(TaskDecorator taskDecorator) method of the ThreadPoolTaskExecutor class does not have the above problem, because it is not linked to the thread Thread , but to Runnable . The official annotation for the method is:

Specify a custom TaskDecorator to be applied to any Runnable about to be executed.

Therefore, when you want to implement single-service multi-thread delivery, it is recommended to customize the thread pool in the following way (also combined with MDC's context inheritance):

 @Bean("customExecutor")
    public Executor getAsyncExecutor() {
        final RejectedExecutionHandler rejectedHandler = new ThreadPoolExecutor.CallerRunsPolicy() {
            @Override
            public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
                log.warn("LOG:线程池容量不够,考虑增加线程数量,但更推荐将线程消耗数量大的程序使用单独的线程池");
                super.rejectedExecution(r, e);
            }
        };
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        threadPoolTaskExecutor.setCorePoolSize(7);
        threadPoolTaskExecutor.setMaxPoolSize(42);
        threadPoolTaskExecutor.setQueueCapacity(11);
        threadPoolTaskExecutor.setRejectedExecutionHandler(rejectedHandler);
        threadPoolTaskExecutor.setThreadNamePrefix("Custom Executor-");
        threadPoolTaskExecutor.setTaskDecorator(runnable -> {
            try {
                Optional<RequestAttributes> requestAttributesOptional = ofNullable(RequestContextHolder.getRequestAttributes());
                Optional<Map<String, String>> contextMapOptional = ofNullable(MDC.getCopyOfContextMap());
                return () -> {
                    try {
                        requestAttributesOptional.ifPresent(RequestContextHolder::setRequestAttributes);
                        contextMapOptional.ifPresent(MDC::setContextMap);
                        runnable.run();
                    } finally {
                        MDC.clear();
                        RequestContextHolder.resetRequestAttributes();
                    }
                };
            } catch (Exception e) {
                return runnable;
            }
        });
        return threadPoolTaskExecutor;
    }
3. Does sleuth's LazyTraceThreadPoolTaskExecutor also pass thread values?

Remember the child thread transfer of MDC in the previous article. When the sleuth framework was introduced, Spring's default thread pool was replaced by LazyTraceThreadPoolTaskExecutor . At this time, there is no need to do the above decorator operations, and the child threads in the default thread pool can inherit the traceId and other values in the MDC.

Then LazyTraceThreadPoolTaskExecutor Can the child thread also inherit the value of the parent thread's RequestContextHolder?

Tried it myself, can't!

3. Full-link multi-threading implementation

The full link is for the scenario of microservice invocation, although in principle, HttpServletRequest should only be for a single service request to response. However, due to the popularity of microservices, the link of a service request often spans multiple services.

Based on the above approach, can we implement request-scoped variables that propagate across microservices?

1. Discussion on delivery methods

But there is a contradiction here, as we mentioned earlier when we compared Attribute and Header、Parameter . The former is suitable for passing values (Setter/Getter) inside the server-side container. The latter two should store the value on the client side (Setter) and get it on the server side (Getter).

So my understanding is: if you need to implement data transfer between services, it is recommended that a string with a small amount of data can be passed through Header (such as: traceId, etc.). The actual data should still be passed through the regular API parameters Parameter or the request body Body .

2. Example of Header transmission

Here is an example of Header passing through Feign interceptor. Feign supports custom interceptor configuration, you can read the value of the previous request in this configuration class, and then stuff it into the next request.

HelloFeign.java

 @FeignClient(
    name = "${hello-service.name}",
    url = "${hello-service.url}",
    path = "${hello-service.path}",
    configuration = {FeignRequestInterceptor.class}
)
public interface HelloFeign { ... }

FeignRequestInterceptor.java

 @ConditionalOnClass({RequestInterceptor.class})
public class FeignRequestInterceptor implements RequestInterceptor {
    private static final String[] HEADER_KEYS = new String[]{"demo-key-1", "demo-key-2", "demo-key-3"};

    @Override
    public void apply(RequestTemplate requestTemplate) {
        ofNullable(this.getRequestByContext())
                .ifPresent(request -> {
                    for (int i = 0; i < HEADER_KEYS.length; i++) {
                        String key = HEADER_KEYS[i];
                        String value = request.getHeader(key);
                        if (!Objects.isNull(value)) {
                            requestTemplate.header(key, new String[]{value});
                        }
                    }
                });
    }

    private HttpServletRequest getRequestByContext() {
        HttpServletRequest request = null;
        RequestAttributes ra = RequestContextHolder.getRequestAttributes();
        if (ra instanceof ServletRequestAttributes) {
            ServletRequestAttributes sra = (ServletRequestAttributes) ra;
            request = sra.getRequest();
        }
        return request;
    }
}

KerryWu
641 声望159 粉丝

保持饥饿


引用和评论

0 条评论