在日常 Web 开发中,我们经常需要获取 HTTP 请求头和 Cookie 中的信息,比如获取用户的设备类型、语言偏好或是用户标识。每次手动解析这些数据不仅繁琐,还容易出错。Spring 框架提供的@RequestHeader 和@CookieValue 注解恰好解决了这个痛点,让我们事半功倍。
@RequestHeader 注解详解
基本概念
@RequestHeader注解用于将 HTTP 请求头信息绑定到控制器方法的参数上。当 Spring 接收到请求时,会自动从请求头中提取对应的值并转换为方法参数。
基本语法
@RequestMapping("/user-agent")
public String userAgent(@RequestHeader("User-Agent") String userAgent) {
return "当前User-Agent: " + userAgent;
}
参数说明
@RequestHeader注解支持的属性:
@RequestHeader(
value = "User-Agent", // 请求头名称
required = false, // 是否必需,默认true
defaultValue = "Unknown" // 默认值
)
下面通过流程图了解@RequestHeader的工作原理:
实战案例
案例一:处理多个请求头
@GetMapping("/browser-info")
public Map<String, String> getBrowserInfo(
@RequestHeader("User-Agent") String userAgent,
@RequestHeader("Accept-Language") String acceptLanguage,
@RequestHeader(
value = "Referer",
required = false
) String referer) {
Map<String, String> info = new HashMap<>();
info.put("userAgent", userAgent);
info.put("language", acceptLanguage);
info.put("referer", referer != null ? referer : "直接访问");
return info;
}
案例二:使用 MultiValueMap 接收所有请求头
@GetMapping("/all-headers")
public Map<String, List<String>> getAllHeaders(@RequestHeader MultiValueMap<String, String> headers) {
// MultiValueMap能够处理多值请求头(如Set-Cookie、Accept)
return headers;
}
注意:@RequestHeader 默认使用 Map 存储单值头,遇到多值头(如 Accept-Language: en-US, en)时需用 MultiValueMap 来保留所有值,避免只保留最后一个值的问题。
案例三:类型转换
@GetMapping("/content-length")
public String getContentLength(
@RequestHeader(
value = "Content-Length",
required = false,
defaultValue = "0"
) long contentLength) {
return "请求内容长度: " + contentLength + " 字节";
}
注意:对于非字符串类型(如 int、long、boolean 等),Spring 会尝试自动转换。如果请求头的值无法转换(例如 API-Version="1.1"无法转为 int),会抛出
TypeMismatchException。应当添加全局异常处理或确保客户端传递符合预期格式的值。
@CookieValue 注解详解
基本概念
@CookieValue注解用于将 HTTP 请求中的 Cookie 值绑定到控制器方法的参数上。这让我们能够更方便地获取和处理 Cookie 数据。
基本语法
@GetMapping("/get-user-id")
public String getUserId(@CookieValue("userId") String userId) {
return "当前用户ID: " + userId;
}
参数说明
@CookieValue注解支持的属性:
@CookieValue(
value = "sessionId", // Cookie名称
required = false, // 是否必需,默认true
defaultValue = "" // 默认值
)
下面通过流程图了解 Cookie 处理过程:
flowchart TD
A[HTTP请求] --> B[带有Cookie的请求]
B --> C{Spring Controller}
C --> D["@CookieValue注解"]
D --> E[解析Cookie值]
E --> F[转换为方法参数]
F --> G[业务处理]
E -- "通过HttpServletRequest.getCookies()获取,并通过ConversionService转换类型" --> E
实战案例
案例一:基本 Cookie 处理
@GetMapping("/welcome")
public String welcome(
@CookieValue(
value = "username",
required = false,
defaultValue = "游客"
) String username) {
return "欢迎, " + username + "!";
}
案例二:类型转换
@GetMapping("/last-visit")
public String lastVisit(
@CookieValue(
value = "lastVisitTime",
required = false
)
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
Date lastVisit) {
// 底层使用Formatter进行格式化和解析,支持本地化
if (lastVisit == null) {
return "这是您的第一次访问";
}
return "您上次访问时间: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(lastVisit);
}
为确保日期格式转换的可靠性,可在配置类中注册日期格式化器:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatterForFieldType(Date.class, new DateFormatter("yyyy-MM-dd HH:mm:ss"));
}
}
案例三:处理复杂 Cookie 对象
// 定义POJO提升类型安全性
record UserPreference(String theme, String language, boolean notifications) {}
@GetMapping("/user-preferences")
public String userPreferences(
@CookieValue(
value = "preferences",
required = false
) String preferencesJson) {
if (preferencesJson.isBlank()) { // 使用isBlank代替null检查,更符合默认值逻辑
return "未设置偏好";
}
try {
ObjectMapper mapper = new ObjectMapper();
// 使用具体POJO替代Map,提高类型安全
UserPreference preferences = mapper.readValue(preferencesJson, UserPreference.class);
return "您的偏好设置: " + preferences;
} catch (Exception e) {
return "解析偏好设置失败: " + e.getMessage();
}
}
两者结合的实际应用
多语言支持实现
@GetMapping("/hello")
public String hello(
@RequestHeader(
value = "Accept-Language",
required = false,
defaultValue = "zh-CN"
) String language,
@CookieValue(
value = "preferredLanguage",
required = false
) String preferredLanguage) {
// 优先使用Cookie中存储的语言偏好
String userLanguage = preferredLanguage != null ? preferredLanguage : language;
if (userLanguage.startsWith("en")) {
return "Hello, welcome!";
} else if (userLanguage.startsWith("fr")) {
return "Bonjour, bienvenue!";
} else {
return "你好,欢迎!";
}
}
接口版本控制
// 定义响应POJO并添加Builder模式支持
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse {
private String data;
private String metadata;
private String error;
private String debugInfo;
// 静态工厂方法
public static ApiResponse v1(String data) {
return builder().data(data).build();
}
public static ApiResponse v2(String data, String metadata) {
return builder().data(data).metadata(metadata).build();
}
public static ApiResponse error(String error) {
return builder().error(error).build();
}
// 添加调试信息的方法
public ApiResponse withDebugInfo(String info) {
return builder()
.data(this.data)
.metadata(this.metadata)
.error(this.error)
.debugInfo(info)
.build();
}
}
@RestController
@RequestMapping("/api")
public class VersionedApiController {
@GetMapping("/data")
public ApiResponse getData(
@RequestHeader(
value = "API-Version",
defaultValue = "1"
) int apiVersion,
@CookieValue(
value = "debug-mode",
required = false,
defaultValue = "false"
) boolean debugMode) {
ApiResponse response;
if (apiVersion == 1) {
response = ApiResponse.v1("版本1的数据结构");
} else if (apiVersion == 2) {
response = ApiResponse.v2("版本2的数据结构", "额外的元数据");
} else {
response = ApiResponse.error("不支持的API版本");
}
// 使用构建器模式添加调试信息,避免重复参数
if (debugMode) {
return response.withDebugInfo("这里是调试信息");
}
return response;
}
}
下面用图表展示不同版本的 API 请求处理流程:
类型转换原理与高级应用
Spring 类型转换机制
Spring 通过ConversionService为@RequestHeader和@CookieValue提供类型转换功能,支持将字符串转换为各种 Java 类型:
- 转换原理与处理流程:
-
Spring 的参数解析顺序:
- 从请求中提取原始值(header/cookie)
- 应用
required校验(若为 false 且值不存在,使用 defaultValue) - 使用
ConversionService转换为目标类型 - 应用
@Valid校验(若存在)
-
转换机制区别:
Converter:一对一直接转换(如 String→Integer),不涉及本地化Formatter:支持格式化和解析,依赖Locale信息(如@DateTimeFormat底层用 Formatter)
- 枚举类型转换示例:
public enum ThemeType {
DARK, LIGHT, SYSTEM
}
@GetMapping("/theme")
public String getTheme(
@RequestHeader(
value = "Theme",
required = false,
defaultValue = "SYSTEM"
) ThemeType theme) {
return "当前主题: " + theme;
}
- 自定义类型转换器:
// 自定义用户信息类
public class UserInfo {
private String id;
private String name;
private String email;
// 默认构造函数
public UserInfo() {
this.id = "guest";
this.name = "访客";
this.email = "guest@example.com";
}
public UserInfo(String id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
// getter和setter省略...
@Override
public String toString() {
return String.format("UserInfo[id=%s, name=%s, email=%s]", id, name, email);
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToUserInfoConverter());
}
}
class StringToUserInfoConverter implements Converter<String, UserInfo> {
@Override
public UserInfo convert(String source) {
// 更健壮的边界条件处理
if (source == null || !source.contains(":")) {
return new UserInfo(); // 返回默认用户
}
try {
// 限制分割次数,避免多余空值和数组越界
String[] parts = source.split(":", 3);
if (parts.length < 3) {
return new UserInfo(); // 数据不完整,返回默认值
}
return new UserInfo(parts[0], parts[1], parts[2]);
} catch (Exception e) {
return new UserInfo(); // 出现异常返回默认值
}
}
}
@GetMapping("/user-cookie")
public String getUserCookie(@CookieValue(required = false) UserInfo userInfo) {
return "用户信息: " + userInfo;
}
框架版本兼容性
Spring 不同版本对这两个注解的支持略有不同:
- Spring Boot 2.x+ 增强了对这些注解的支持:
- 支持
jakarta.servlet包(Spring Boot 3.0+) - 响应式编程模型(WebFlux)中可使用对应的
ServerWebExchange - 自动配置了更多默认转换器
- 支持
// Spring Boot 3.0+ WebFlux中的用法
@GetMapping("/reactive-header")
public Mono<String> getReactiveHeader(
@RequestHeader("User-Agent") String userAgent) {
return Mono.just("Reactive User-Agent: " + userAgent);
}
异常处理与最佳实践
处理缺失的请求头和 Cookie
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({MissingRequestHeaderException.class, MissingRequestCookieException.class})
public ResponseEntity<ErrorResponse> handleMissingHeaderOrCookie(Exception ex) {
String message = ex instanceof MissingRequestHeaderException ?
"缺少必要请求头: " + ((MissingRequestHeaderException) ex).getHeaderName() :
"缺少必要Cookie: " + ((MissingRequestCookieException) ex).getCookieName();
return ResponseEntity.badRequest().body(new ErrorResponse("400", message));
}
@ExceptionHandler(TypeMismatchException.class)
public ResponseEntity<ErrorResponse> handleTypeMismatch(TypeMismatchException ex) {
return ResponseEntity.badRequest().body(
new ErrorResponse("400", "参数类型不匹配: " + ex.getMessage())
);
}
// 定义错误响应类
record ErrorResponse(String code, String message) {}
}
实用技巧
- 为必需参数提供默认值:避免直接报错,提升用户体验
@GetMapping("/user-info")
public String getUserInfo(
@RequestHeader(
value = "User-Agent",
defaultValue = "Unknown"
) String userAgent,
@CookieValue(
value = "userId",
required = false,
defaultValue = "anonymous"
) String userId) {
// 即使header或cookie不存在,代码也能正常运行
return "User: " + userId + ", Agent: " + userAgent;
}
- 与其他注解对比使用
@GetMapping("/user/{id}")
public String getUserDetails(
@PathVariable("id") long userId, // 从URL路径获取
@RequestParam(required = false) String detail, // 从查询参数获取
@RequestHeader("User-Agent") String userAgent, // 从请求头获取
@CookieValue(
value = "token",
required = false
) String token // 从Cookie获取
) {
// 各种参数来源的综合使用
return String.format("用户ID: %d, 详情: %s, 浏览器: %s, 令牌: %s",
userId, detail, userAgent, token);
}
- 安全最佳实践
@GetMapping("/secure")
public String getSecureInfo(
@CookieValue(
value = "sessionId",
required = false
) String sessionId) {
// 安全相关:敏感Cookie需妥善保护
if (sessionId == null) {
return "未登录";
}
// 验证sessionId...
return "安全信息";
}
在设置 Cookie 时应遵循安全最佳实践:
@GetMapping("/set-secure-cookie")
public ResponseEntity<String> setSecureCookie(HttpServletResponse response) {
Cookie cookie = new Cookie("secureData", "sensitive-value");
cookie.setHttpOnly(true); // 防止JavaScript通过document.cookie获取Cookie,抵御XSS攻击
cookie.setSecure(true); // 仅在HTTPS连接中发送Cookie,防止明文传输被截获
cookie.setPath("/");
cookie.setMaxAge(3600); // 1小时有效期
response.addCookie(cookie);
return ResponseEntity.ok("已设置安全Cookie");
}
- 性能注意事项
对于频繁访问的请求头或 Cookie,可以考虑缓存或直接使用HttpServletRequest:
@GetMapping("/performance")
public String getPerformanceOptimized(HttpServletRequest request) {
// 对于频繁访问的请求头,直接从request获取可减少反射开销
String userAgent = request.getHeader("User-Agent");
// 对于Cookie,可以遍历一次并缓存结果
Cookie[] cookies = request.getCookies();
String sessionId = null;
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("sessionId".equals(cookie.getName())) {
sessionId = cookie.getValue();
break;
}
}
}
return "UA: " + userAgent + ", Session: " + sessionId;
}
总结
| 注解 | 主要用途 | 常用属性 | 常见应用场景 | 属性兼容性 |
|---|---|---|---|---|
| @RequestHeader | 获取 HTTP 请求头信息 | value, required, defaultValue | 获取 User-Agent、语言偏好、API 版本 | 与@CookieValue 完全一致 |
| @CookieValue | 获取 HTTP 请求 Cookie 值 | value, required, defaultValue | 用户会话管理、个性化设置、记住用户偏好 | 与@RequestHeader 完全一致 |
Spring 的这两个注解简化了 Web 开发中对请求头和 Cookie 的处理,让我们能够更专注于业务逻辑。它们与@RequestParam、@PathVariable一起构成了 Spring MVC 参数绑定体系,方便获取不同来源的数据。
使用时需注意几个关键点:
- 类型转换配置,特别是自定义类型时需注册相应转换器
- 多值头的处理应使用
MultiValueMap而非普通Map - 边界条件处理(如缺失值、格式不正确的值)
- 安全因素,特别是敏感 Cookie 需遵循 Web 安全最佳实践
- 高频访问场景下的性能优化