从入门到精通:Spring MVC的矩阵参数、数据预处理与HTTP缓存实战

从入门到精通:Spring MVC的矩阵参数、数据预处理与HTTP缓存实战_第1张图片

肖哥弹架构 跟大家“弹弹” 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)

从入门到精通:Spring MVC的矩阵参数、数据预处理与HTTP缓存实战_第2张图片

作用:在URL路径中支持复杂参数的传递

解决的问题

  • 传统查询参数(?key=value)在表达复杂数据结构时的局限性
  • 需要保持URL语义清晰的同时传递多值参数

请求示例

GET /cars;color=red,blue;model=SUV/year=2023
Accept: 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);
        }
    }
}

/**
 * 汽车服务类(模拟实现)
 */
@Service
class 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
@AllArgsConstructor
class Car {
    private int id;
    private String model;
    private String color;
    private int year;
}

关键配置说明

  1. 必须的配置:在配置类中设置setRemoveSemicolonContent(false),否则Spring会默认移除分号后的内容

  2. 矩阵变量格式

    • 基本格式:/path;key1=value1;key2=value2
    • 多值格式:/path;colors=red,blue,green
  3. 参数绑定

    • @MatrixVariable注解的pathVar属性指定要解析的路径变量
    • 可以直接绑定到Map获取所有参数,或绑定到具体参数

2. 请求/响应体预处理

从入门到精通:Spring MVC的矩阵参数、数据预处理与HTTP缓存实战_第3张图片

【作用】

实现请求/响应体的全链路自动处理,包括:

  • 敏感数据自动加解密
  • 请求签名验证
  • 响应统一包装
  • 数据格式转换
【解决的问题】
  1. 避免在Controller中重复处理加解密逻辑
  2. 统一安全合规要求的数据处理
  3. 实现业务逻辑与安全逻辑的解耦
【请求示例】
POST /api/secure/user
Content-Type: application/json
X-Signature: sha256=abc123

{
  "encryptedData": "U2FsdGVkX1+7jJ7p5K2J3YI1h1Z5b7v8R9x0yA4C6D2E="
}
【响应示例】
{
  "code": 200,
  "message": "success",
  "encryptedData": "U2FsdGVkX1+9k8J7m6N5V2X3C1B4D7E0F8G2H5I6J=",
  "timestamp": 1689234567890
}
【核心实现代码】
1. 加解密配置类
@Configuration
public 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. 请求预处理切面
@ControllerAdvice
public 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. 响应后处理切面
@ControllerAdvice
public 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
@AllArgsConstructor
class 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. 加密服务实现
@Service
public 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);
        }
    }
}
【关键配置】
  1. application.yml中配置:
spring:
  http:
    converters:
      preferred-json-mapper: jackson # 确保使用Jackson处理JSON
  1. 启动类需添加:
@SpringBootApplication
@EnableWebMvc
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

3. HTTP 缓存控制

从入门到精通:Spring MVC的矩阵参数、数据预处理与HTTP缓存实战_第4张图片

【作用】

实现高效的HTTP缓存策略,包括:

  • 条件请求处理(ETag/Last-Modified)
  • 缓存过期控制(Cache-Control)
  • 协商缓存与强制缓存组合
  • 缓存再验证机制
【解决的问题】
  1. 减少重复数据传输,节省带宽
  2. 降低服务器负载
  3. 提高客户端响应速度
  4. 保证资源更新的及时性
【请求/响应示例】

首次请求

GET /api/products/123 HTTP/1.1

首次响应

HTTP/1.1 200 OK
Cache-Control: max-age=3600, must-revalidate
ETag: "a1b2c3d4"
Last-Modified: Wed, 21 Oct 2023 07:28:00 GMT
Content-Type: application/json

{
  "id": 123,
  "name": "无线耳机",
  "price": 199.99
}

二次请求

GET /api/products/123 HTTP/1.1
If-None-Match: "a1b2c3d4"
If-Modified-Since: Wed, 21 Oct 2023 07:28:00 GMT

资源未修改时的响应

HTTP/1.1 304 Not Modified
Cache-Control: max-age=3600, must-revalidate
ETag: "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
@EnableWebMvc
public 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. 静态资源缓存配置
@Configuration
public 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. 缓存服务工具类
@Service
public 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());
    }
}

你可能感兴趣的:(spring,mvc,java,SpringMVC)