从入门到精通:Spring MVC的矩阵参数、数据预处理与HTTP缓存实战_springmvc 如何实现一个数据的缓存策略
肖哥弹架构 跟大家“弹弹” Spring MVC设计与实战应用,需要代码关注
欢迎 点赞,点赞,点赞。
关注公号Solomon肖哥弹架构获取更多精彩内容
历史热点文章
- MyCat应用实战:分布式数据库中间件的实践与优化(篇幅一)
- 图解深度剖析:MyCat 架构设计与组件协同 (篇幅二)
- 一个项目代码讲清楚DO/PO/BO/AO/E/DTO/DAO/ POJO/VO
- 写代码总被Dis:5个项目案例带你掌握SOLID技巧,代码有架构风格
- 里氏替换原则在金融交易系统中的实践,再不懂你咬我
为什么你的Spring API不够优雅? 可能是因为缺少这些进阶技能:
你是否遇到过这些痛点?
❌ URL参数过于复杂难以维护
❌ 重复的请求体校验逻辑遍布各Controller
❌ 客户端频繁请求相同数据却无法有效缓存
🔧 矩阵变量
▸ 解析/products;category=book;author=Rowling
这类结构化URL
▸ 对比@RequestParam
与@MatrixVariable
的性能差异
🔧 数据预处理
▸ 用@InitBinder
自动转换日期格式
▸ 实现RequestBodyAdvice
对加密请求体自动解密
🔧 HTTP缓存
▸ 配置Cache-Control: max-age=3600
强制浏览器缓存
▸ 结合Last-Modified
实现条件性GET请求
一、请求处理增强
1. 矩阵变量(Matrix Variables)
作用:在URL路径中支持复杂参数的传递
解决的问题:
- 传统查询参数(?key=value)在表达复杂数据结构时的局限性
- 需要保持URL语义清晰的同时传递多值参数
请求示例:
GET /cars;color=red,blue;model=SUV/year=2023Accept: application/json
返回内容:
{ \"cars\": [ { \"model\": \"SUV\", \"colors\": [\"red\",\"blue\"], \"year\": 2023 } ]}
核心代码:
import org.springframework.web.bind.annotation.*;import org.springframework.http.ResponseEntity;import java.util.Map;import java.util.List;import java.util.stream.Collectors;import java.util.Arrays;@RestController@RequestMapping(\"/api\")public class CarController { /** * 处理矩阵变量的完整示例 * @param matrixVars 从路径中提取的矩阵变量Map * @param year 常规查询参数 * @return 过滤后的汽车列表 * * 示例请求:GET /api/cars;color=red,blue;model=SUV/year=2023 */ @GetMapping(\"/cars/{carParams}\") public ResponseEntity<List<Car>> getCars( @MatrixVariable(pathVar = \"carParams\") Map<String, Object> matrixVars, @RequestParam int year) { // 1. 打印接收到的矩阵变量(调试用) System.out.println(\"Received matrix variables: \" + matrixVars); // 2. 从矩阵变量中提取颜色(支持多值逗号分隔) List<String> colors = extractColors(matrixVars); // 3. 从矩阵变量中提取车型 String model = (String) matrixVars.getOrDefault(\"model\", \"\"); // 4. 调用服务层进行查询 List<Car> filteredCars = carService.findCars(year, model, colors); return ResponseEntity.ok(filteredCars); } /** * 从矩阵变量中提取颜色列表 * @param matrixVars 矩阵变量Map * @return 颜色列表(可能为空) */ private List<String> extractColors(Map<String, Object> matrixVars) { Object colorObj = matrixVars.get(\"color\"); if (colorObj == null) { return List.of(); } // 处理逗号分隔的多值情况(如:color=red,blue) String colorStr = colorObj.toString(); return Arrays.stream(colorStr.split(\",\")) .map(String::trim) .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); } /** * 启用矩阵变量支持的配置类 */ @Configuration public static class MatrixVariableConfig implements WebMvcConfigurer { @Override public void configurePathMatch(PathMatchConfigurer configurer) { // 必须开启矩阵变量支持 configurer.setRemoveSemicolonContent(false); } }}/** * 汽车服务类(模拟实现) */@Serviceclass CarService { public List<Car> findCars(int year, String model, List<String> colors) { // 实际业务中这里会是数据库查询 return List.of( new Car(1, \"SUV\", \"red\", 2023), new Car(2, \"SUV\", \"blue\", 2023) ).stream() .filter(c -> model.isEmpty() || c.getModel().equals(model)) .filter(c -> colors.isEmpty() || colors.contains(c.getColor())) .collect(Collectors.toList()); }}/** * 汽车实体类 */@Data // Lombok注解,自动生成getter/setter@AllArgsConstructorclass Car { private int id; private String model; private String color; private int year;}
关键配置说明
-
必须的配置:在配置类中设置
setRemoveSemicolonContent(false)
,否则Spring会默认移除分号后的内容 -
矩阵变量格式:
- 基本格式:
/path;key1=value1;key2=value2
- 多值格式:
/path;colors=red,blue,green
- 基本格式:
-
参数绑定:
@MatrixVariable
注解的pathVar
属性指定要解析的路径变量- 可以直接绑定到
Map
获取所有参数,或绑定到具体参数
2. 请求/响应体预处理
【作用】
实现请求/响应体的全链路自动处理,包括:
- 敏感数据自动加解密
- 请求签名验证
- 响应统一包装
- 数据格式转换
【解决的问题】
- 避免在Controller中重复处理加解密逻辑
- 统一安全合规要求的数据处理
- 实现业务逻辑与安全逻辑的解耦
【请求示例】
POST /api/secure/userContent-Type: application/jsonX-Signature: sha256=abc123{ \"encryptedData\": \"U2FsdGVkX1+7jJ7p5K2J3YI1h1Z5b7v8R9x0yA4C6D2E=\"}
【响应示例】
{ \"code\": 200, \"message\": \"success\", \"encryptedData\": \"U2FsdGVkX1+9k8J7m6N5V2X3C1B4D7E0F8G2H5I6J=\", \"timestamp\": 1689234567890}
【核心实现代码】
1. 加解密配置类
@Configurationpublic class CryptoConfig { @Bean public AesCryptoService aesCryptoService() { // 从配置中心或环境变量获取密钥 String secretKey = System.getenv(\"CRYPTO_SECRET_KEY\"); return new AesCryptoService(secretKey); } @Bean public SignatureService signatureService() { return new HmacSha256SignatureService(); }}
2. 请求预处理切面
@ControllerAdvicepublic class RequestBodyPreprocessor implements RequestBodyAdvice { @Autowired private AesCryptoService cryptoService; @Autowired private SignatureService signatureService; // 只处理带有@Encrypted注解的方法 @Override public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { return methodParameter.hasMethodAnnotation(Encrypted.class); } @Override public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) { // 1. 验证请求签名 String signature = ((ServletServerHttpRequest) inputMessage).getServletRequest() .getHeader(\"X-Signature\"); if (!signatureService.verifySignature(inputMessage.getBody(), signature)) { throw new InvalidSignatureException(\"请求签名无效\"); } // 2. 返回解密后的输入流 return new DecryptedHttpInputMessage(inputMessage, cryptoService); } // 其他必须实现的空方法 @Override public Object afterBodyRead(...) { return body; } @Override public Object handleEmptyBody(...) { return body; }}/** * 解密输入流包装类 */class DecryptedHttpInputMessage implements HttpInputMessage { private final HttpInputMessage inputMessage; private final AesCryptoService cryptoService; public DecryptedHttpInputMessage(HttpInputMessage inputMessage, AesCryptoService cryptoService) { this.inputMessage = inputMessage; this.cryptoService = cryptoService; } @Override public InputStream getBody() throws IOException { // 读取加密数据 String encrypted = StreamUtils.copyToString(inputMessage.getBody(), StandardCharsets.UTF_8); // 解密数据 String decrypted = cryptoService.decrypt(encrypted); // 返回解密后的流 return new ByteArrayInputStream(decrypted.getBytes()); } @Override public HttpHeaders getHeaders() { return inputMessage.getHeaders(); }}
3. 响应后处理切面
@ControllerAdvicepublic class ResponseBodyPostprocessor implements ResponseBodyAdvice<Object> { @Autowired private AesCryptoService cryptoService; @Autowired private SignatureService signatureService; // 只处理带有@Encrypted注解的方法 @Override public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) { return returnType.hasMethodAnnotation(Encrypted.class); } @Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { // 1. 统一响应结构包装 ApiResponse<Object> responseWrapper = new ApiResponse<>( 200, \"success\", System.currentTimeMillis(), body); // 2. 序列化为JSON字符串 String jsonBody = JsonUtils.toJson(responseWrapper); // 3. 加密响应体 String encrypted = cryptoService.encrypt(jsonBody); // 4. 生成响应签名 String signature = signatureService.generateSignature(encrypted); response.getHeaders().add(\"X-Signature\", signature); return encrypted; }}/** * 统一响应结构 */@Data@AllArgsConstructorclass ApiResponse<T> { private int code; private String message; private long timestamp; private T data;}
4. 控制器使用示例
@RestController@RequestMapping(\"/api/secure\")public class SecureUserController { @Encrypted // 启用加解密处理 @PostMapping(\"/user\") public UserInfo updateUser(@Valid @RequestBody UserInfo user) { // 这里获取到的已经是解密后的对象 return userService.update(user); }}/** * 加解密注解 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Encrypted {}
5. 加密服务实现
@Servicepublic class AesCryptoService { private final SecretKeySpec secretKey; public AesCryptoService(String secretKey) { // 密钥处理逻辑 byte[] key = secretKey.getBytes(StandardCharsets.UTF_8); this.secretKey = new SecretKeySpec(key, \"AES\"); } public String encrypt(String data) { try { Cipher cipher = Cipher.getInstance(\"AES/GCM/NoPadding\"); cipher.init(Cipher.ENCRYPT_MODE, secretKey); byte[] encrypted = cipher.doFinal(data.getBytes()); return Base64.getEncoder().encodeToString(encrypted); } catch (Exception e) { throw new CryptoException(\"加密失败\", e); } } public String decrypt(String encrypted) { try { Cipher cipher = Cipher.getInstance(\"AES/GCM/NoPadding\"); cipher.init(Cipher.DECRYPT_MODE, secretKey); byte[] decoded = Base64.getDecoder().decode(encrypted); return new String(cipher.doFinal(decoded)); } catch (Exception e) { throw new CryptoException(\"解密失败\", e); } }}
【关键配置】
- 在
application.yml
中配置:
spring: http: converters: preferred-json-mapper: jackson # 确保使用Jackson处理JSON
- 启动类需添加:
@SpringBootApplication@EnableWebMvcpublic class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); }}
3. HTTP 缓存控制
【作用】
实现高效的HTTP缓存策略,包括:
- 条件请求处理(ETag/Last-Modified)
- 缓存过期控制(Cache-Control)
- 协商缓存与强制缓存组合
- 缓存再验证机制
【解决的问题】
- 减少重复数据传输,节省带宽
- 降低服务器负载
- 提高客户端响应速度
- 保证资源更新的及时性
【请求/响应示例】
首次请求:
GET /api/products/123 HTTP/1.1
首次响应:
HTTP/1.1 200 OKCache-Control: max-age=3600, must-revalidateETag: \"a1b2c3d4\"Last-Modified: Wed, 21 Oct 2023 07:28:00 GMTContent-Type: application/json{ \"id\": 123, \"name\": \"无线耳机\", \"price\": 199.99}
二次请求:
GET /api/products/123 HTTP/1.1If-None-Match: \"a1b2c3d4\"If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT
资源未修改时的响应:
HTTP/1.1 304 Not ModifiedCache-Control: max-age=3600, must-revalidateETag: \"a1b2c3d4\"
【核心实现代码】
1. 基础缓存控制(Controller层)
@RestController@RequestMapping(\"/api/products\")public class ProductController { @GetMapping(\"/{id}\") public ResponseEntity<Product> getProduct( @PathVariable Long id, WebRequest request) { // 1. 查询产品信息 Product product = productService.findById(id); // 2. 计算ETag(根据内容哈希) String etag = calculateETag(product); // 3. 检查资源是否修改 if (request.checkNotModified(etag)) { // 返回304 Not Modified return ResponseEntity.status(HttpStatus.NOT_MODIFIED) .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS)) .eTag(etag) .build(); } // 4. 返回完整响应 return ResponseEntity.ok() .cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS) .eTag(etag) .body(product); } private String calculateETag(Product product) { // 使用内容哈希作为ETag(实际项目可结合版本号) return \"\"\" + DigestUtils.md5DigestAsHex( (product.getId() + product.getUpdatedAt()).getBytes() ) + \"\"\"; }}
2. 高级缓存策略配置
@Configuration@EnableWebMvcpublic class CacheConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { // 全局缓存拦截器 registry.addInterceptor(new CacheControlInterceptor()); } @Bean public FilterRegistrationBean<ShallowEtagHeaderFilter> etagFilter() { // ETag生成过滤器(备选方案) FilterRegistrationBean<ShallowEtagHeaderFilter> filter = new FilterRegistrationBean<>(); filter.setFilter(new ShallowEtagHeaderFilter()); filter.addUrlPatterns(\"/api/*\"); return filter; }}/** * 自定义缓存控制拦截器 */public class CacheControlInterceptor implements HandlerInterceptor { @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { if (!(handler instanceof HandlerMethod)) return; HandlerMethod method = (HandlerMethod) handler; // 1. 检查方法上的缓存注解 if (method.hasMethodAnnotation(Cache.class)) { Cache cache = method.getMethodAnnotation(Cache.class); applyCachePolicy(response, cache.value()); } // 2. 默认策略(可根据URL模式设置不同策略) else if (request.getRequestURI().startsWith(\"/api/\")) { response.setHeader(\"Cache-Control\", \"public, max-age=60, must-revalidate\"); } } private void applyCachePolicy(HttpServletResponse response, String policy) { switch (policy) { case \"no-store\": response.setHeader(\"Cache-Control\", \"no-store\"); break; case \"immutable\": response.setHeader(\"Cache-Control\", \"public, max-age=31536000, immutable\"); break; default: // \"standard\" response.setHeader(\"Cache-Control\", \"public, max-age=3600, must-revalidate\"); } }}/** * 自定义缓存注解 */@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Cache { String value() default \"standard\"; // no-store|immutable|standard}
3. 静态资源缓存配置
@Configurationpublic class StaticResourceConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(\"/static/**\") .addResourceLocations(\"classpath:/static/\") .setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS)) .resourceChain(true) .addResolver(new VersionResourceResolver() .addContentVersionStrategy(\"/**\")); }}
4. 缓存服务工具类
@Servicepublic class CacheService { // 适用于动态内容的缓存控制 public <T> ResponseEntity<T> wrapCacheResponse( T body, String etag, Instant lastModified) { CacheControl cacheControl = CacheControl.maxAge(1, TimeUnit.HOURS) .mustRevalidate() .cachePublic(); return ResponseEntity.ok() .cacheControl(cacheControl) .eTag(etag) .lastModified(lastModified.toEpochMilli()) .body(body); } // 检查条件请求 public boolean checkNotModified( WebRequest request, String etag, Instant lastModified) { return request.checkNotModified(etag, lastModified.toEpochMilli()); }}