肖哥弹架构 跟大家“弹弹” Spring MVC设计与实战应用,需要代码关注
欢迎 点赞,点赞,点赞。
关注公号Solomon肖哥弹架构获取更多精彩内容
为什么你的Spring API不够优雅? 可能是因为缺少这些进阶技能:
你是否遇到过这些痛点?
❌ URL参数过于复杂难以维护
❌ 重复的请求体校验逻辑遍布各Controller
❌ 客户端频繁请求相同数据却无法有效缓存
矩阵变量
▸ 解析/products;category=book;author=Rowling
这类结构化URL
▸ 对比@RequestParam
与@MatrixVariable
的性能差异
数据预处理
▸ 用@InitBinder
自动转换日期格式
▸ 实现RequestBodyAdvice
对加密请求体自动解密
HTTP缓存
▸ 配置Cache-Control: max-age=3600
强制浏览器缓存
▸ 结合Last-Modified
实现条件性GET请求
作用:在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;
}
关键配置说明
必须的配置:在配置类中设置setRemoveSemicolonContent(false)
,否则Spring会默认移除分号后的内容
矩阵变量格式:
/path;key1=value1;key2=value2
/path;colors=red,blue,green
参数绑定:
@MatrixVariable
注解的pathVar
属性指定要解析的路径变量Map
获取所有参数,或绑定到具体参数实现请求/响应体的全链路自动处理,包括:
POST /api/secure/user
Content-Type: application/json
X-Signature: sha256=abc123
{
"encryptedData": "U2FsdGVkX1+7jJ7p5K2J3YI1h1Z5b7v8R9x0yA4C6D2E="
}
{
"code": 200,
"message": "success",
"encryptedData": "U2FsdGVkX1+9k8J7m6N5V2X3C1B4D7E0F8G2H5I6J=",
"timestamp": 1689234567890
}
@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();
}
}
@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();
}
}
@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;
}
@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 {
}
@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);
}
}
}
application.yml
中配置:spring:
http:
converters:
preferred-json-mapper: jackson # 确保使用Jackson处理JSON
@SpringBootApplication
@EnableWebMvc
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
实现高效的HTTP缓存策略,包括:
首次请求:
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"
@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()
) + """;
}
}
@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
}
@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("/**"));
}
}
@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());
}
}