Spring MVC 如何直接访问 HttpServletRequest, HttpServletResponse, HttpSession 等 Servlet API 对象?

Spring MVC 提供了许多高级抽象(如 @RequestParam, @PathVariable, @RequestBody, Model, ResponseEntity 等)来简化开发,避免直接操作底层的 Servlet API,但在某些特定场景下,可能仍然需要直接访问 HttpServletRequest, HttpServletResponse, HttpSession 等对象。

Spring MVC 提供了非常方便的方式来实现这一点,主要是通过方法参数注入

方法参数注入 (推荐方式)

这是最常用、最推荐、也是最简洁的方式。只需要在Controller 处理方法(Handler Method)的参数列表中声明需要的 Servlet API 对象类型,Spring MVC 在调用该方法时会自动将当前请求对应的实例注入进来。

支持注入的主要 Servlet API 类型包括:

  • javax.servlet.ServletRequest / javax.servlet.http.HttpServletRequest
  • javax.servlet.ServletResponse / javax.servlet.http.HttpServletResponse
  • javax.servlet.http.HttpSession
  • java.security.Principal (用于获取当前认证的用户信息)
  • java.util.Locale (当前请求的区域设置)
  • java.util.TimeZone (从 Java 8 / Servlet 3.1 开始) / java.time.ZoneId (Spring 5)
  • java.io.InputStream / javax.servlet.ServletInputStream (用于读取请求体)
  • java.io.OutputStream / javax.servlet.ServletOutputStream (用于写入响应体)
  • java.io.Reader (用于读取请求体)
  • java.io.Writer (用于写入响应体)
  • org.springframework.web.context.request.WebRequest / org.springframework.web.context.request.NativeWebRequest (Spring 提供的更丰富的请求封装,可以访问请求和会话属性,但与 Servlet API 解耦)
  • org.springframework.http.HttpMethod (当前请求的 HTTP 方法)

示例代码:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;
import java.security.Principal;
import java.util.Locale;

@Controller
public class ServletApiController {

    @GetMapping("/servlet-access")
    @ResponseBody // 使用 @ResponseBody 简化,直接返回字符串;也可以返回视图名或 ModelAndView
    public String handleRequest(
            HttpServletRequest request,          // 注入 HttpServletRequest
            HttpServletResponse response,        // 注入 HttpServletResponse
            HttpSession session,               // 注入 HttpSession
            Principal principal,               // 注入 Principal (如果配置了安全认证)
            Locale locale) {                   // 注入 Locale

        // 1. 使用 HttpServletRequest
        String remoteAddr = request.getRemoteAddr();
        String userAgent = request.getHeader("User-Agent");
        String queryParam = request.getParameter("info"); // 不推荐,优先用 @RequestParam

        // 2. 使用 HttpServletResponse
        // 可以设置响应头、状态码等,但如果方法有返回值且使用了 @ResponseBody 或 ResponseEntity,
        // Spring 会接管大部分响应处理,直接操作 response 可能导致冲突或意外行为。
        // 更适合在需要直接写入响应流或精细控制时使用(通常配合 void 返回类型)。
        // response.setHeader("X-Custom-Info", "From Servlet API");
        // response.setStatus(HttpServletResponse.SC_ACCEPTED);

        // 3. 使用 HttpSession
        Integer visitCount = (Integer) session.getAttribute("visitCount");
        if (visitCount == null) {
            visitCount = 1;
        } else {
            visitCount++;
        }
        session.setAttribute("visitCount", visitCount);

        // 4. 使用 Principal
        String username = (principal != null) ? principal.getName() : "anonymous";

        // 5. 使用 Locale
        String language = locale.getLanguage();

        // 构建返回信息
        return String.format(
                "Remote Address: %s<br>" +
                "User-Agent Snippet: %s...<br>" +
                "Query Param 'info': %s<br>" +
                "Visit Count (Session): %d<br>" +
                "Username: %s<br>" +
                "Locale Language: %s",
                remoteAddr,
                (userAgent != null && userAgent.length() > 10) ? userAgent.substring(0, 10) : userAgent,
                queryParam,
                visitCount,
                username,
                language
        );
    }

    // 示例:直接写入响应流 (方法返回类型为 void)
    @GetMapping("/direct-write")
    public void directWriteResponse(HttpServletResponse response) throws IOException {
        response.setContentType("text/plain");
        response.setCharacterEncoding("UTF-8");
        PrintWriter writer = response.getWriter();
        writer.println("This response is written directly using HttpServletResponse's writer.");
        writer.flush();
        // 注意:当直接操作 response 时,不要再返回 String、ModelAndView 或使用 @ResponseBody
    }
}

优点:

  • 简单直观: 声明即可用。
  • 解耦: Controller 方法本身不依赖特定的获取方式。
  • 易于测试: 在单元测试中,可以轻松地模拟这些接口(如使用 Mockito 创建 Mock 对象)并传递给方法。
  • 符合 Spring MVC 设计: 这是框架推荐的标准方式。

RequestContextHolder (非 Controller 组件中可能使用)

Spring 还提供了一个 RequestContextHolder 类,它使用 ThreadLocal 来持有当前线程的请求属性(包括 HttpServletRequestHttpServletResponse)。在请求处理流程中的任何地方(只要还是在同一个请求处理线程内)访问这些对象。

但是,在 Controller 中,强烈不推荐使用 RequestContextHolder,因为方法参数注入是更清晰、更易测试的方式。

RequestContextHolder 主要用于那些不在 Controller 层,但又需要访问当前请求信息的组件(例如某些 Service、Aspect 或 Filter 中,尽管在这些地方通常也应该优先考虑依赖注入或传递参数)。

示例 (仅作演示,不推荐在 Controller 中使用):

import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

// ... 在某个方法内部 ...
public void someMethodNotInController() {
    RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
    if (requestAttributes instanceof ServletRequestAttributes) {
        HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest();
        HttpSession session = request.getSession(false); // 获取 session,如果不存在则返回 null

        // 现在可以使用 request 和 session 对象了
        String userAgent = request.getHeader("User-Agent");
        if (session != null) {
            Object userAttribute = session.getAttribute("user");
            // ...
        }
        System.out.println("Accessed request outside controller via RequestContextHolder. User-Agent: " + userAgent);
    } else {
         System.out.println("Not in a web request context or context is not Servlet-based.");
    }
}

缺点(尤其是在 Controller 中):

  • 代码耦合: 严重依赖 Spring 的静态方法,不易单元测试(需要模拟静态方法或设置 RequestContextHolder)。
  • 可读性差: 不如方法参数明确。
  • 可能出错: 如果在非请求处理线程中调用,会返回 null 或抛出异常。

不推荐的方式:直接 @Autowired

虽然技术上可能通过配置 request/session scope 的代理 Bean 来实现将 HttpServletRequest 等直接 @Autowired 到 Controller 字段,但这通常被认为是不好的实践

  • 线程安全问题: Controller 默认是单例(Singleton)的,而 HttpServletRequest, HttpServletResponse, HttpSession 是与每个请求相关的(Request/Session Scoped)。直接注入需要 Spring 创建线程安全的代理对象,增加了复杂性。
  • 可测试性差: 字段注入比方法注入更难模拟。
  • 不清晰: 不如方法参数那样明确地表明方法对这些对象的依赖。
// 不推荐的示例
@Controller
public class BadPracticeController {

    @Autowired // 不推荐!需要特殊配置(如 request scope proxy)才能工作,且有缺点
    private HttpServletRequest request;

    @GetMapping("/bad-autowire")
    @ResponseBody
    public String handle() {
        // 使用注入的 request 字段
        return "Request URI from autowired field: " + request.getRequestURI();
    }
}

总结与建议

  • 首选且推荐的方式: 在 Controller 方法签名中直接声明 HttpServletRequest, HttpServletResponse, HttpSession 等所需参数,利用 Spring MVC 的方法参数注入
  • 优先使用 Spring MVC 抽象: 尽可能使用 @RequestParam, @PathVariable, @RequestHeader, @CookieValue, @RequestBody, Model, RedirectAttributes, ResponseEntity 等高级抽象,它们能覆盖大部分场景,使代码更简洁、更专注于业务逻辑,并与 Servlet API 解耦。
  • 仅在必要时访问 Servlet API: 当 Spring 的抽象无法满足特定需求时(例如需要非常底层的控制、访问非标准 Header/Cookie 属性、直接操作输入/输出流等),才考虑直接注入 Servlet API 对象。
  • 避免 RequestContextHolder: 在 Controller 中尽量避免使用 RequestContextHolder
  • 避免 @Autowired Servlet API 对象: 不要将请求/会话作用域的对象直接自动装配到单例 Controller 字段中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰糖心书房

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值