Backend Development 7 min read

How SpringBoot Injects HttpServletRequest via ThreadLocal: A Deep Dive

This article explores how SpringBoot 2.6.12 injects HttpServletRequest into controllers using JDK dynamic proxies and ThreadLocal, detailing the underlying ObjectFactoryDelegatingInvocationHandler, RequestObjectFactory, RequestContextHolder, and DispatcherServlet processing flow with code examples and explains why injecting HttpServletRequest and HttpServletResponse in a controller is safe.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How SpringBoot Injects HttpServletRequest via ThreadLocal: A Deep Dive

Environment: SpringBoot 2.6.12

Test Controller class:

<code>@RestController
@RequestMapping("/message")
public class MessageController {

    @Resource
    private HttpServletRequest request;

    @PostMapping("/resolver")
    public Object resolver(@RequestBody Users user) {
        System.out.println(request);
        return user;
    }
}
</code>

When debugging, the injected request object is actually a proxy created by JDK dynamic proxy. The proxy class ObjectFactoryDelegatingInvocationHandler holds an ObjectFactory that provides the real request.

<code>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 {
        String methodName = method.getName();
        if (methodName.equals("equals")) {
            return (proxy == args[0]);
        } else if (methodName.equals("hashCode")) {
            return System.identityHashCode(proxy);
        } else if (methodName.equals("toString")) {
            return this.objectFactory.toString();
        }
        try {
            return method.invoke(this.objectFactory.getObject(), args);
        } catch (InvocationTargetException ex) {
            throw ex.getTargetException();
        }
    }
}
</code>

The objectFactory is actually RequestObjectFactory , whose getObject() method returns the real HttpServletRequest from a ThreadLocal holder.

<code>@SuppressWarnings("serial")
private static class RequestObjectFactory implements ObjectFactory<ServletRequest>, Serializable {
    @Override
    public ServletRequest getObject() {
        return currentRequestAttributes().getRequest();
    }
    @Override
    public String toString() {
        return "Current HttpServletRequest";
    }
}
</code>

The helper method currentRequestAttributes() obtains the current request attributes from RequestContextHolder :

<code>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;
}
</code>

RequestContextHolder.currentRequestAttributes() retrieves the thread‑bound attributes, creating them if necessary:

<code>public static RequestAttributes currentRequestAttributes() throws IllegalStateException {
    RequestAttributes attributes = getRequestAttributes();
    if (attributes == null) {
        if (jsfPresent) {
            attributes = FacesRequestAttributesFactory.getFacesRequestAttributes();
        }
        if (attributes == null) {
            throw new IllegalStateException("No thread-bound request found: ...");
        }
    }
    return attributes;
}
</code>

The underlying storage is a ThreadLocal that holds a ServletRequestAttributes instance containing both HttpServletRequest and HttpServletResponse .

During the servlet processing flow, DispatcherServlet (a subclass of FrameworkServlet ) calls processRequest :

<code>protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);
    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
    initContextHolders(request, localeContext, requestAttributes);
    try {
        doService(request, response);
    } finally {
        // cleanup omitted for brevity
    }
}
</code>

The two lines that create ServletRequestAttributes are:

<code>RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
</code>

buildRequestAttributes() simply wraps the request and response into a new ServletRequestAttributes object when no previous attributes exist:

<code>protected ServletRequestAttributes buildRequestAttributes(HttpServletRequest request,
        @Nullable HttpServletResponse response, @Nullable RequestAttributes previousAttributes) {
    if (previousAttributes == null || previousAttributes instanceof ServletRequestAttributes) {
        return new ServletRequestAttributes(request, response);
    } else {
        return null; // preserve the pre‑bound RequestAttributes instance
    }
}
</code>

Afterwards, initContextHolders() stores the newly created attributes in RequestContextHolder :

<code>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);
    }
}
</code>

The static method RequestContextHolder.setRequestAttributes() finally puts the ServletRequestAttributes (which holds the request and response) into a ThreadLocal variable, making them accessible throughout the processing of the current request.

<code>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();
        }
    }
}
</code>

Thus, injecting HttpServletRequest or HttpServletResponse into a Spring MVC controller is safe because the real objects are stored in a thread‑local holder and retrieved via the proxy whenever they are accessed.

End of analysis.

BackendJavaSpringBootHttpServletRequestthreadlocalDispatcherServlet
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.