6種常見的SpringBoot攔截器使用場景及實(shí)現(xiàn)方式
在構(gòu)建企業(yè)級Web應(yīng)用時,我們經(jīng)常需要在請求處理的不同階段執(zhí)行一些通用邏輯,如權(quán)限驗(yàn)證、日志記錄、性能監(jiān)控等。
Spring MVC的攔截器(Interceptor)機(jī)制提供了一種優(yōu)雅的方式來實(shí)現(xiàn)這些橫切關(guān)注點(diǎn),而不必在每個控制器中重復(fù)編寫相同的代碼。
本文將介紹SpringBoot中6種常見的攔截器使用場景及其實(shí)現(xiàn)方式。
攔截器基礎(chǔ)
什么是攔截器
攔截器是Spring MVC框架提供的一種機(jī)制,用于在控制器(Controller)處理請求前后執(zhí)行特定的邏輯。
攔截器與過濾器的區(qū)別
1. 歸屬不同:過濾器(Filter)屬于Servlet規(guī)范,攔截器屬于Spring框架。
2. 攔截范圍:過濾器能攔截所有請求,攔截器只能攔截Spring MVC的請求。
3. 執(zhí)行順序:請求首先經(jīng)過過濾器,然后才會被攔截器處理。
攔截器的生命周期方法
攔截器通過實(shí)現(xiàn)HandlerInterceptor
接口來定義,該接口包含三個核心方法:
1. preHandle() :在控制器方法執(zhí)行前調(diào)用,返回true表示繼續(xù)執(zhí)行,返回false表示中斷請求。
2. postHandle() :在控制器方法執(zhí)行后、視圖渲染前調(diào)用。
3. afterCompletion() :在整個請求完成后調(diào)用,無論是否有異常發(fā)生。
場景一:用戶認(rèn)證攔截器
使用場景
用戶認(rèn)證攔截器主要用于:
- 驗(yàn)證用戶是否已登錄
- 檢查用戶是否有權(quán)限訪問特定資源
- 實(shí)現(xiàn)無狀態(tài)API的JWT token驗(yàn)證
實(shí)現(xiàn)代碼
@Component public class AuthenticationInterceptor implements HandlerInterceptor { @Autowired private JwtTokenProvider jwtTokenProvider; @Autowired private UserService userService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 跳過非控制器方法的處理 if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; // 檢查是否有@PermitAll注解,有則跳過認(rèn)證 PermitAll permitAll = handlerMethod.getMethodAnnotation(PermitAll.class); if (permitAll != null) { return true; } // 從請求頭中獲取token String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("{"error": "未授權(quán),請先登錄"}"); return false; } token = token.substring(7); // 去掉"Bearer "前綴 try { // 驗(yàn)證token if (!jwtTokenProvider.validateToken(token)) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("{"error": "Token已失效,請重新登錄"}"); return false; } // 從token中獲取用戶信息并設(shè)置到請求屬性中 String username = jwtTokenProvider.getUsernameFromToken(token); User user = userService.findByUsername(username); if (user == null) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("{"error": "用戶不存在"}"); return false; } // 檢查方法是否有@RequireRole注解 RequireRole requireRole = handlerMethod.getMethodAnnotation(RequireRole.class); if (requireRole != null) { // 檢查用戶是否有所需角色 String[] roles = requireRole.value(); boolean hasRole = false; for (String role : roles) { if (user.hasRole(role)) { hasRole = true; break; } } if (!hasRole) { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write("{"error": "權(quán)限不足"}"); return false; } } // 將用戶信息放入請求屬性 request.setAttribute("currentUser", user); return true; } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("{"error": "Token驗(yàn)證失敗"}"); return false; } } }
配置注冊
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AuthenticationInterceptor authenticationInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(authenticationInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register"); } }
自定義注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface PermitAll { } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String[] value(); }
最佳實(shí)踐
1. 使用注解來標(biāo)記需要認(rèn)證或特定權(quán)限的接口
2. 將攔截器中的業(yè)務(wù)邏輯抽取到專門的服務(wù)類中
3. 為不同安全級別的API設(shè)計(jì)不同的路徑前綴
4. 添加詳細(xì)的日志記錄,便于問題排查
場景二:日志記錄攔截器
使用場景
日志記錄攔截器主要用于:
- 記錄API請求和響應(yīng)內(nèi)容
- 跟蹤用戶行為
- 收集系統(tǒng)使用統(tǒng)計(jì)數(shù)據(jù)
- 輔助問題排查
實(shí)現(xiàn)代碼
@Component @Slf4j public class LoggingInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 記錄請求開始時間 long startTime = System.currentTimeMillis(); request.setAttribute("startTime", startTime); // 記錄請求信息 String requestURI = request.getRequestURI(); String method = request.getMethod(); String remoteAddr = request.getRemoteAddr(); String userAgent = request.getHeader("User-Agent"); // 獲取當(dāng)前用戶(如果已通過認(rèn)證攔截器) Object currentUser = request.getAttribute("currentUser"); String username = currentUser != null ? ((User) currentUser).getUsername() : "anonymous"; // 記錄請求參數(shù) Map<String, String[]> paramMap = request.getParameterMap(); StringBuilder params = new StringBuilder(); if (!paramMap.isEmpty()) { for (Map.Entry<String, String[]> entry : paramMap.entrySet()) { params.append(entry.getKey()) .append("=") .append(String.join(",", entry.getValue())) .append("&"); } if (params.length() > 0) { params.deleteCharAt(params.length() - 1); } } // 記錄請求體(僅POST/PUT/PATCH請求) String requestBody = ""; if (HttpMethod.POST.matches(method) || HttpMethod.PUT.matches(method) || HttpMethod.PATCH.matches(method)) { // 使用包裝請求對象來多次讀取請求體 ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); // 為了觸發(fā)內(nèi)容緩存,我們需要獲取一次輸入流 if (wrappedRequest.getContentLength() > 0) { wrappedRequest.getInputStream().read(); requestBody = new String(wrappedRequest.getContentAsByteArray(), wrappedRequest.getCharacterEncoding()); } } log.info( "REQUEST: {} {} from={} user={} userAgent={} params={} body={}", method, requestURI, remoteAddr, username, userAgent, params, requestBody ); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 計(jì)算請求處理時間 long startTime = (Long) request.getAttribute("startTime"); long endTime = System.currentTimeMillis(); long processingTime = endTime - startTime; // 記錄響應(yīng)狀態(tài)和處理時間 int status = response.getStatus(); String requestURI = request.getRequestURI(); String method = request.getMethod(); if (ex != null) { log.error( "RESPONSE: {} {} status={} time={}ms error={}", method, requestURI, status, processingTime, ex.getMessage() ); } else { log.info( "RESPONSE: {} {} status={} time={}ms", method, requestURI, status, processingTime ); } } }
配置與使用
@Bean public FilterRegistrationBean<ContentCachingFilter> contentCachingFilter() { FilterRegistrationBean<ContentCachingFilter> registrationBean = new FilterRegistrationBean<>(); registrationBean.setFilter(new ContentCachingFilter()); registrationBean.addUrlPatterns("/api/*"); return registrationBean; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loggingInterceptor) .addPathPatterns("/**"); }
自定義內(nèi)容緩存過濾器
public class ContentCachingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { ContentCachingRequestWrapper wrappedRequest = new ContentCachingRequestWrapper(request); ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response); try { filterChain.doFilter(wrappedRequest, wrappedResponse); } finally { wrappedResponse.copyBodyToResponse(); } } }
最佳實(shí)踐
1. 對敏感信息(如密碼、信用卡號等)進(jìn)行脫敏處理
2. 設(shè)置合理的日志級別和輪轉(zhuǎn)策略
3. 針對大型請求/響應(yīng)體,考慮只記錄部分內(nèi)容或摘要
4. 使用MDC(Mapped Diagnostic Context)記錄請求ID,便于跟蹤完整請求鏈路
場景三:性能監(jiān)控?cái)r截器
使用場景
性能監(jiān)控?cái)r截器主要用于:
- 監(jiān)控API響應(yīng)時間
- 識別性能瓶頸
- 統(tǒng)計(jì)慢查詢
- 提供性能指標(biāo)用于系統(tǒng)優(yōu)化
實(shí)現(xiàn)代碼
@Component @Slf4j public class PerformanceMonitorInterceptor implements HandlerInterceptor { // 慢請求閾值,單位毫秒 @Value("${app.performance.slow-request-threshold:500}") private long slowRequestThreshold; @Autowired private MetricsService metricsService; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; String controllerName = handlerMethod.getBeanType().getSimpleName(); String methodName = handlerMethod.getMethod().getName(); request.setAttribute("controllerName", controllerName); request.setAttribute("methodName", methodName); request.setAttribute("startTime", System.currentTimeMillis()); } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { Long startTime = (Long) request.getAttribute("startTime"); if (startTime != null) { long processingTime = System.currentTimeMillis() - startTime; String controllerName = (String) request.getAttribute("controllerName"); String methodName = (String) request.getAttribute("methodName"); String uri = request.getRequestURI(); // 記錄性能數(shù)據(jù) metricsService.recordApiPerformance(controllerName, methodName, uri, processingTime); // 記錄慢請求 if (processingTime > slowRequestThreshold) { log.warn("Slow API detected: {} {}.{} - {}ms (threshold: {}ms)", uri, controllerName, methodName, processingTime, slowRequestThreshold); // 記錄慢請求到專門的監(jiān)控系統(tǒng) metricsService.recordSlowRequest(controllerName, methodName, uri, processingTime); } } } }
指標(biāo)服務(wù)實(shí)現(xiàn)
@Service @Slf4j public class MetricsServiceImpl implements MetricsService { // 使用滑動窗口記錄最近的性能數(shù)據(jù) private final ConcurrentMap<String, SlidingWindowMetric> apiMetrics = new ConcurrentHashMap<>(); // 慢請求記錄隊(duì)列 private final Queue<SlowRequestRecord> slowRequests = new ConcurrentLinkedQueue<>(); // 保留最近1000條慢請求記錄 private static final int MAX_SLOW_REQUESTS = 1000; @Override public void recordApiPerformance(String controller, String method, String uri, long processingTime) { String apiKey = controller + "." + method; apiMetrics.computeIfAbsent(apiKey, k -> new SlidingWindowMetric()) .addSample(processingTime); // 可以在這里添加Prometheus或其他監(jiān)控系統(tǒng)的指標(biāo)記錄 } @Override public void recordSlowRequest(String controller, String method, String uri, long processingTime) { SlowRequestRecord record = new SlowRequestRecord( controller, method, uri, processingTime, LocalDateTime.now() ); slowRequests.add(record); // 如果隊(duì)列超過最大容量,移除最早的記錄 while (slowRequests.size() > MAX_SLOW_REQUESTS) { slowRequests.poll(); } } @Override public List<ApiPerformanceMetric> getApiPerformanceMetrics() { List<ApiPerformanceMetric> metrics = new ArrayList<>(); for (Map.Entry<String, SlidingWindowMetric> entry : apiMetrics.entrySet()) { String[] parts = entry.getKey().split("\."); String controller = parts[0]; String method = parts.length > 1 ? parts[1] : ""; SlidingWindowMetric metric = entry.getValue(); metrics.add(new ApiPerformanceMetric( controller, method, metric.getAvg(), metric.getMin(), metric.getMax(), metric.getCount() )); } return metrics; } @Override public List<SlowRequestRecord> getSlowRequests() { return new ArrayList<>(slowRequests); } // 滑動窗口指標(biāo)類 private static class SlidingWindowMetric { private final LongAdder count = new LongAdder(); private final LongAdder sum = new LongAdder(); private final AtomicLong min = new AtomicLong(Long.MAX_VALUE); private final AtomicLong max = new AtomicLong(0); public void addSample(long value) { count.increment(); sum.add(value); // 更新最小值 while (true) { long currentMin = min.get(); if (value >= currentMin || min.compareAndSet(currentMin, value)) { break; } } // 更新最大值 while (true) { long currentMax = max.get(); if (value <= currentMax || max.compareAndSet(currentMax, value)) { break; } } } public long getCount() { return count.sum(); } public double getAvg() { long countValue = count.sum(); return countValue > 0 ? (double) sum.sum() / countValue : 0; } public long getMin() { return min.get() == Long.MAX_VALUE ? 0 : min.get(); } public long getMax() { return max.get(); } } }
實(shí)體類定義
@Data @AllArgsConstructor public class ApiPerformanceMetric { private String controllerName; private String methodName; private double avgProcessingTime; private long minProcessingTime; private long maxProcessingTime; private long requestCount; } @Data @AllArgsConstructor public class SlowRequestRecord { private String controllerName; private String methodName; private String uri; private long processingTime; private LocalDateTime timestamp; }
指標(biāo)服務(wù)接口
public interface MetricsService { void recordApiPerformance(String controller, String method, String uri, long processingTime); void recordSlowRequest(String controller, String method, String uri, long processingTime); List<ApiPerformanceMetric> getApiPerformanceMetrics(); List<SlowRequestRecord> getSlowRequests(); }
性能監(jiān)控控制器
@RestController @RequestMapping("/admin/metrics") public class MetricsController { @Autowired private MetricsService metricsService; @GetMapping("/api-performance") public List<ApiPerformanceMetric> getApiPerformanceMetrics() { return metricsService.getApiPerformanceMetrics(); } @GetMapping("/slow-requests") public List<SlowRequestRecord> getSlowRequests() { return metricsService.getSlowRequests(); } }
最佳實(shí)踐
1. 使用滑動窗口統(tǒng)計(jì),避免內(nèi)存無限增長
2. 為不同API設(shè)置不同的性能閾值
3. 將性能數(shù)據(jù)導(dǎo)出到專業(yè)監(jiān)控系統(tǒng)(如Prometheus)
4. 設(shè)置告警機(jī)制,及時發(fā)現(xiàn)性能問題
5. 只對重要接口進(jìn)行詳細(xì)監(jiān)控,避免過度監(jiān)控帶來的性能開銷
場景四:接口限流攔截器
使用場景
接口限流攔截器主要用于:
- 防止接口被惡意頻繁調(diào)用
- 保護(hù)系統(tǒng)資源,避免過載
- 實(shí)現(xiàn)API訪問量控制
- 防止DoS攻擊
實(shí)現(xiàn)代碼
@Component @Slf4j public class RateLimitInterceptor implements HandlerInterceptor { @Autowired private RedisTemplate<String, Object> redisTemplate; @Value("${app.rate-limit.enabled:true}") private boolean enabled; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!enabled) { return true; } if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; // 獲取限流注解 RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class); if (rateLimit == null) { // 沒有配置限流注解,不進(jìn)行限流 return true; } // 獲取限流類型 RateLimitType limitType = rateLimit.type(); // 根據(jù)限流類型獲取限流鍵 String limitKey = getLimitKey(request, limitType); // 獲取限流配置 int limit = rateLimit.limit(); int period = rateLimit.period(); // 執(zhí)行限流檢查 boolean allowed = checkRateLimit(limitKey, limit, period); if (!allowed) { // 超過限流,返回429狀態(tài)碼 response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.getWriter().write("{"error":"Too many requests","message":"請求頻率超過限制,請稍后再試"}"); return false; } return true; } private String getLimitKey(HttpServletRequest request, RateLimitType limitType) { String key = "rate_limit:"; switch (limitType) { case IP: key += "ip:" + getClientIp(request); break; case USER: // 從認(rèn)證信息獲取用戶ID Object currentUser = request.getAttribute("currentUser"); String userId = currentUser != null ? String.valueOf(((User) currentUser).getId()) : "anonymous"; key += "user:" + userId; break; case API: key += "api:" + request.getRequestURI(); break; case IP_API: key += "ip_api:" + getClientIp(request) + ":" + request.getRequestURI(); break; case USER_API: Object user = request.getAttribute("currentUser"); String id = user != null ? String.valueOf(((User) user).getId()) : "anonymous"; key += "user_api:" + id + ":" + request.getRequestURI(); break; default: key += "global"; } return key; } private boolean checkRateLimit(String key, int limit, int period) { // 使用Redis的原子操作進(jìn)行限流檢查 Long count = redisTemplate.execute(connection -> { // 遞增計(jì)數(shù)器 Long currentCount = connection.stringCommands().incr(key.getBytes()); // 如果是第一次遞增,設(shè)置過期時間 if (currentCount != null && currentCount == 1) { connection.keyCommands().expire(key.getBytes(), period); } return currentCount; }, true); return count != null && count <= limit; } private String getClientIp(HttpServletRequest request) { String ipAddress = request.getHeader("X-Forwarded-For"); if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("Proxy-Client-IP"); } if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getHeader("WL-Proxy-Client-IP"); } if (ipAddress == null || ipAddress.isEmpty() || "unknown".equalsIgnoreCase(ipAddress)) { ipAddress = request.getRemoteAddr(); if ("127.0.0.1".equals(ipAddress) || "0:0:0:0:0:0:0:1".equals(ipAddress)) { // 根據(jù)網(wǎng)卡取本機(jī)配置的IP try { InetAddress inet = InetAddress.getLocalHost(); ipAddress = inet.getHostAddress(); } catch (UnknownHostException e) { log.error("獲取本機(jī)IP失敗", e); } } } // 對于多個代理的情況,第一個IP為客戶端真實(shí)IP if (ipAddress != null && ipAddress.contains(",")) { ipAddress = ipAddress.substring(0, ipAddress.indexOf(",")); } return ipAddress; } }
限流注解
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { /** * 限流類型 */ RateLimitType type() default RateLimitType.IP; /** * 限制次數(shù) */ int limit() default 100; /** * 時間周期(秒) */ int period() default 60; } public enum RateLimitType { /** * 按IP地址限流 */ IP, /** * 按用戶限流 */ USER, /** * 按接口限流 */ API, /** * 按IP和接口組合限流 */ IP_API, /** * 按用戶和接口組合限流 */ USER_API, /** * 全局限流 */ GLOBAL }
使用示例
@RestController @RequestMapping("/api/products") public class ProductController { @Autowired private ProductService productService; @GetMapping @RateLimit(type = RateLimitType.IP, limit = 100, period = 60) public List<Product> getProducts() { return productService.findAll(); } @GetMapping("/{id}") @RateLimit(type = RateLimitType.IP, limit = 200, period = 60) public Product getProduct(@PathVariable Long id) { return productService.findById(id) .orElseThrow(() -> new ResourceNotFoundException("Product not found")); } @PostMapping @RequireRole("ADMIN") @RateLimit(type = RateLimitType.USER, limit = 10, period = 60) public Product createProduct(@RequestBody @Valid ProductRequest productRequest) { return productService.save(productRequest); } }
最佳實(shí)踐
1. 根據(jù)接口重要性和資源消耗設(shè)置不同的限流規(guī)則
2. 使用分布式限流解決方案,如Redis+Lua腳本
3. 為特定用戶群體設(shè)置不同的限流策略
4. 在限流響應(yīng)中提供合理的重試建議
5. 監(jiān)控限流情況,及時調(diào)整限流閾值
場景五:請求參數(shù)驗(yàn)證攔截器
使用場景
請求參數(shù)驗(yàn)證攔截器主要用于:
- 統(tǒng)一處理參數(shù)驗(yàn)證邏輯
- 提供友好的錯誤信息
- 防止非法參數(shù)導(dǎo)致的安全問題
- 減少控制器中的重復(fù)代碼
實(shí)現(xiàn)代碼
@Component @Slf4j public class RequestValidationInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } HandlerMethod handlerMethod = (HandlerMethod) handler; // 檢查方法參數(shù)是否需要驗(yàn)證 Parameter[] parameters = handlerMethod.getMethod().getParameters(); for (Parameter parameter : parameters) { // 檢查是否有@RequestBody注解 if (parameter.isAnnotationPresent(RequestBody.class) && parameter.isAnnotationPresent(Valid.class)) { // 該參數(shù)需要驗(yàn)證,在控制器方法中會自動驗(yàn)證 // 這里只需確保我們能處理驗(yàn)證失敗的情況 // 通過全局異常處理器處理MethodArgumentNotValidException // 記錄驗(yàn)證將要發(fā)生 log.debug("將對 {}.{} 的請求體參數(shù) {} 進(jìn)行驗(yàn)證", handlerMethod.getBeanType().getSimpleName(), handlerMethod.getMethod().getName(), parameter.getName()); } // 檢查是否有@RequestParam注解 RequestParam requestParam = parameter.getAnnotation(RequestParam.class); if (requestParam != null) { String paramName = requestParam.value().isEmpty() ? parameter.getName() : requestParam.value(); String paramValue = request.getParameter(paramName); // 檢查必填參數(shù) if (requestParam.required() && (paramValue == null || paramValue.isEmpty())) { response.setStatus(HttpStatus.BAD_REQUEST.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.getWriter().write( "{"error":"參數(shù)錯誤","message":"缺少必填參數(shù): " + paramName + ""}"); return false; } // 檢查參數(shù)格式(如果有注解) if (parameter.isAnnotationPresent(Pattern.class) && paramValue != null) { Pattern pattern = parameter.getAnnotation(Pattern.class); if (!paramValue.matches(pattern.regexp())) { response.setStatus(HttpStatus.BAD_REQUEST.value()); response.setContentType(MediaType.APPLICATION_JSON_VALUE); response.getWriter().write( "{"error":"參數(shù)錯誤","message":"參數(shù) " + paramName + " 格式不正確: " + pattern.message() + ""}"); return false; } } } // 檢查是否有@PathVariable注解 PathVariable pathVariable = parameter.getAnnotation(PathVariable.class); if (pathVariable != null) { // 對于PathVariable的驗(yàn)證主要依賴RequestMappingHandlerMapping的正則匹配 // 這里可以添加額外的驗(yàn)證邏輯,如數(shù)值范圍檢查等 } } return true; } }
全局異常處理
@RestControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 處理請求體參數(shù)驗(yàn)證失敗的異常 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<Map<String, Object>> handleValidationExceptions( MethodArgumentNotValidException ex) { Map<String, String> errors = new HashMap<>(); ex.getBindingResult().getAllErrors().forEach(error -> { String fieldName = ((FieldError) error).getField(); String errorMessage = error.getDefaultMessage(); errors.put(fieldName, errorMessage); }); Map<String, Object> body = new HashMap<>(); body.put("error", "參數(shù)驗(yàn)證失敗"); body.put("details", errors); return ResponseEntity.badRequest().body(body); } /** * 處理請求參數(shù)綁定失敗的異常 */ @ExceptionHandler(MissingServletRequestParameterException.class) public ResponseEntity<Map<String, Object>> handleMissingParams( MissingServletRequestParameterException ex) { Map<String, Object> body = new HashMap<>(); body.put("error", "參數(shù)錯誤"); body.put("message", "缺少必填參數(shù): " + ex.getParameterName()); return ResponseEntity.badRequest().body(body); } /** * 處理路徑參數(shù)類型不匹配的異常 */ @ExceptionHandler(MethodArgumentTypeMismatchException.class) public ResponseEntity<Map<String, Object>> handleTypeMismatch( MethodArgumentTypeMismatchException ex) { Map<String, Object> body = new HashMap<>(); body.put("error", "參數(shù)類型錯誤"); body.put("message", "參數(shù) " + ex.getName() + " 應(yīng)為 " + ex.getRequiredType().getSimpleName() + " 類型"); return ResponseEntity.badRequest().body(body); } }
使用示例
@RestController @RequestMapping("/api/users") public class UserController { @Autowired private UserService userService; @GetMapping public List<User> getUsers( @RequestParam(required = false) @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "只能包含字母和數(shù)字") String keyword, @RequestParam(defaultValue = "0") @Min(value = 0, message = "頁碼不能小于0") Integer page, @RequestParam(defaultValue = "10") @Min(value = 1, message = "每頁條數(shù)不能小于1") @Max(value = 100, message = "每頁條數(shù)不能大于100") Integer size) { return userService.findUsers(keyword, page, size); } @PostMapping public User createUser(@RequestBody @Valid UserCreateRequest request) { return userService.createUser(request); } @GetMapping("/{id}") public User getUser(@PathVariable @Positive(message = "用戶ID必須為正整數(shù)") Long id) { return userService.findById(id) .orElseThrow(() -> new ResourceNotFoundException("User not found")); } }
自定義驗(yàn)證請求類
@Data public class UserCreateRequest { @NotBlank(message = "用戶名不能為空") @Size(min = 4, max = 20, message = "用戶名長度必須在4-20之間") @Pattern(regexp = "^[a-zA-Z0-9_]+$", message = "用戶名只能包含字母、數(shù)字和下劃線") private String username; @NotBlank(message = "密碼不能為空") @Size(min = 6, max = 20, message = "密碼長度必須在6-20之間") @Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$", message = "密碼必須包含大小寫字母和數(shù)字") private String password; @NotBlank(message = "郵箱不能為空") @Email(message = "郵箱格式不正確") private String email; @NotBlank(message = "手機(jī)號不能為空") @Pattern(regexp = "^1[3-9]\d{9}$", message = "手機(jī)號格式不正確") private String phone; @NotNull(message = "年齡不能為空") @Min(value = 18, message = "年齡必須大于或等于18歲") @Max(value = 120, message = "年齡必須小于或等于120歲") private Integer age; @NotEmpty(message = "角色不能為空") private List<String> roles; @Valid private Address address; } @Data public class Address { @NotBlank(message = "省份不能為空") private String province; @NotBlank(message = "城市不能為空") private String city; @NotBlank(message = "詳細(xì)地址不能為空") private String detail; @Pattern(regexp = "^\d{6}$", message = "郵編必須為6位數(shù)字") private String zipCode; }
最佳實(shí)踐
1. 結(jié)合Spring Validation框架進(jìn)行深度驗(yàn)證
2. 為常用驗(yàn)證規(guī)則創(chuàng)建自定義注解
3. 提供清晰、具體的錯誤信息
4. 記錄驗(yàn)證失敗的情況,發(fā)現(xiàn)潛在的問題
5. 對敏感API進(jìn)行更嚴(yán)格的參數(shù)驗(yàn)證
場景六:國際化處理攔截器
使用場景
國際化處理攔截器主要用于:
- 根據(jù)請求頭或用戶設(shè)置確定語言
- 切換應(yīng)用的本地化資源
- 提供多語言支持
- 增強(qiáng)用戶體驗(yàn)
實(shí)現(xiàn)代碼
@Component public class LocaleChangeInterceptor implements HandlerInterceptor { @Autowired private MessageSource messageSource; private final List<Locale> supportedLocales = Arrays.asList( Locale.ENGLISH, // en Locale.SIMPLIFIED_CHINESE, // zh_CN Locale.TRADITIONAL_CHINESE, // zh_TW Locale.JAPANESE, // ja Locale.KOREAN // ko ); // 默認(rèn)語言 private final Locale defaultLocale = Locale.ENGLISH; // 語言參數(shù)名 private String paramName = "lang"; // 用于檢測語言的HTTP頭 private List<String> localeHeaders = Arrays.asList( "Accept-Language", "X-Locale" ); @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 嘗試從請求參數(shù)中獲取語言設(shè)置 String localeParam = request.getParameter(paramName); Locale locale = null; if (localeParam != null && !localeParam.isEmpty()) { locale = parseLocale(localeParam); } // 如果請求參數(shù)中沒有有效的語言設(shè)置,嘗試從HTTP頭中獲取 if (locale == null) { for (String header : localeHeaders) { String localeHeader = request.getHeader(header); if (localeHeader != null && !localeHeader.isEmpty()) { locale = parseLocaleFromHeader(localeHeader); if (locale != null) { break; } } } } // 如果無法確定語言,使用默認(rèn)語言 if (locale == null) { locale = defaultLocale; } // 將解析出的語言設(shè)置到LocaleContextHolder中 LocaleContextHolder.setLocale(locale); // 將語言信息放入請求屬性中,便于在視圖中使用 request.setAttribute("currentLocale", locale); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 請求結(jié)束后清除語言設(shè)置 LocaleContextHolder.resetLocaleContext(); } /** * 解析語言參數(shù) */ private Locale parseLocale(String localeParam) { Locale requestedLocale = Locale.forLanguageTag(localeParam.replace('_', '-')); // 檢查請求的語言是否在支持的語言列表中 for (Locale supportedLocale : supportedLocales) { if (supportedLocale.getLanguage().equals(requestedLocale.getLanguage())) { // 如果語言匹配,但國家可能不同,使用完整的支持語言 return supportedLocale; } } return null; } /** * 從Accept-Language頭解析語言 */ private Locale parseLocaleFromHeader(String headerValue) { // 解析Accept-Language頭,格式如: "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7" String[] parts = headerValue.split(","); for (String part : parts) { String[] subParts = part.split(";"); String localeValue = subParts[0].trim(); Locale locale = Locale.forLanguageTag(localeValue.replace('_', '-')); // 檢查是否是支持的語言 for (Locale supportedLocale : supportedLocales) { if (supportedLocale.getLanguage().equals(locale.getLanguage())) { return supportedLocale; } } } return null; } }
國際化配置
@Configuration public class LocaleConfig { @Bean public LocaleResolver localeResolver() { SessionLocaleResolver resolver = new SessionLocaleResolver(); resolver.setDefaultLocale(Locale.ENGLISH); return resolver; } @Bean public MessageSource messageSource() { ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource(); messageSource.setBasename("classpath:i18n/messages"); messageSource.setDefaultEncoding("UTF-8"); messageSource.setCacheSeconds(3600); // 刷新緩存的周期(秒) return messageSource; } }
國際化工具類
@Component public class I18nUtil { @Autowired private MessageSource messageSource; /** * 獲取國際化消息 * * @param code 消息代碼 * @return 本地化后的消息 */ public String getMessage(String code) { return getMessage(code, null); } /** * 獲取國際化消息 * * @param code 消息代碼 * @param args 消息參數(shù) * @return 本地化后的消息 */ public String getMessage(String code, Object[] args) { Locale locale = LocaleContextHolder.getLocale(); try { return messageSource.getMessage(code, args, locale); } catch (NoSuchMessageException e) { return code; } } }
資源文件示例
# src/main/resources/i18n/messages_en.properties greeting=Hello, {0}! login.success=Login successful login.failure=Login failed: {0} validation.username.notEmpty=Username cannot be empty validation.password.weak=Password is too weak, must be at least 8 characters # src/main/resources/i18n/messages_zh_CN.properties greeting=你好,{0}! login.success=登錄成功 login.failure=登錄失敗:{0} validation.username.notEmpty=用戶名不能為空 validation.password.weak=密碼強(qiáng)度不夠,至少需要8個字符
在控制器中使用
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private AuthService authService; @Autowired private I18nUtil i18nUtil; @PostMapping("/login") public ResponseEntity<?> login(@RequestBody LoginRequest request) { try { String token = authService.login(request.getUsername(), request.getPassword()); Map<String, Object> response = new HashMap<>(); response.put("token", token); response.put("message", i18nUtil.getMessage("login.success")); return ResponseEntity.ok(response); } catch (AuthenticationException e) { Map<String, Object> response = new HashMap<>(); response.put("error", i18nUtil.getMessage("login.failure", new Object[]{e.getMessage()})); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); } } @GetMapping("/greeting") public Map<String, String> greeting(@RequestParam String name) { Map<String, String> response = new HashMap<>(); response.put("message", i18nUtil.getMessage("greeting", new Object[]{name})); return response; } }
在前端獲取當(dāng)前語言
@RestController @RequestMapping("/api/locale") public class LocaleController { @GetMapping("/current") public Map<String, Object> getCurrentLocale(HttpServletRequest request) { Locale currentLocale = (Locale) request.getAttribute("currentLocale"); if (currentLocale == null) { currentLocale = LocaleContextHolder.getLocale(); } Map<String, Object> response = new HashMap<>(); response.put("locale", currentLocale.toString()); response.put("language", currentLocale.getLanguage()); response.put("country", currentLocale.getCountry()); return response; } @GetMapping("/supported") public List<Map<String, String>> getSupportedLocales() { List<Map<String, String>> locales = new ArrayList<>(); locales.add(createLocaleMap(Locale.ENGLISH, "English")); locales.add(createLocaleMap(Locale.SIMPLIFIED_CHINESE, "簡體中文")); locales.add(createLocaleMap(Locale.TRADITIONAL_CHINESE, "繁體中文")); locales.add(createLocaleMap(Locale.JAPANESE, "日本語")); locales.add(createLocaleMap(Locale.KOREAN, "???")); return locales; } private Map<String, String> createLocaleMap(Locale locale, String displayName) { Map<String, String> map = new HashMap<>(); map.put("code", locale.toString()); map.put("language", locale.getLanguage()); map.put("displayName", displayName); return map; } }
最佳實(shí)踐
1. 使用標(biāo)準(zhǔn)的國際化資源文件組織方式
2. 緩存消息資源,避免頻繁加載資源文件
3. 為所有用戶可見的字符串提供國際化支持
4. 允許用戶在界面中切換語言
5. 在會話中保存用戶語言偏好
6. 使用參數(shù)化消息,避免字符串拼接
攔截器的最佳實(shí)踐
1. 攔截器注冊順序
攔截器的執(zhí)行順序非常重要,通常應(yīng)該遵循:
- 認(rèn)證/授權(quán)攔截器優(yōu)先執(zhí)行
- 日志攔截器盡量靠前,記錄完整信息
- 性能監(jiān)控?cái)r截器包裹整個請求處理過程
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private AuthenticationInterceptor authInterceptor; @Autowired private LoggingInterceptor loggingInterceptor; @Autowired private PerformanceMonitorInterceptor performanceInterceptor; @Autowired private RateLimitInterceptor rateLimitInterceptor; @Autowired private RequestValidationInterceptor validationInterceptor; @Autowired private LocaleChangeInterceptor localeInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { // 1. 國際化攔截器 registry.addInterceptor(localeInterceptor) .addPathPatterns("/**"); // 2. 日志攔截器 registry.addInterceptor(loggingInterceptor) .addPathPatterns("/**"); // 3. 性能監(jiān)控?cái)r截器 registry.addInterceptor(performanceInterceptor) .addPathPatterns("/api/**"); // 4. 限流攔截器 registry.addInterceptor(rateLimitInterceptor) .addPathPatterns("/api/**"); // 5. 參數(shù)驗(yàn)證攔截器 registry.addInterceptor(validationInterceptor) .addPathPatterns("/api/**"); // 6. 認(rèn)證攔截器 registry.addInterceptor(authInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/auth/register"); } }
2. 避免攔截器中的重量級操作
將復(fù)雜邏輯抽取到專門的服務(wù)類中
使用異步方式處理日志記錄等非關(guān)鍵路徑操作
緩存頻繁使用的數(shù)據(jù)
避免在攔截器中進(jìn)行數(shù)據(jù)庫操作
3. 異常處理
攔截器中的異??赡軐?dǎo)致整個請求處理鏈中斷
總是使用try-catch捕獲并正確處理異常
關(guān)鍵攔截器(如認(rèn)證)中的異常應(yīng)返回適當(dāng)?shù)腍TTP狀態(tài)碼和錯誤信息
4. 路徑模式配置
精確指定需要攔截的路徑,避免不必要的性能開銷
合理使用excludePathPatterns排除不需要攔截的路徑
對靜態(tài)資源路徑通常應(yīng)該排除
5. 攔截器的組合使用
設(shè)計(jì)獨(dú)立、職責(zé)單一的攔截器
通過組合使用實(shí)現(xiàn)復(fù)雜功能
避免在一個攔截器中實(shí)現(xiàn)多種不相關(guān)的功能
總結(jié)
通過合理使用這些攔截器,可以極大地提高代碼復(fù)用性,減少重復(fù)代碼,使應(yīng)用架構(gòu)更加清晰和模塊化。
在實(shí)際應(yīng)用中,可以根據(jù)具體需求選擇或組合使用這些攔截器,甚至擴(kuò)展出更多類型的攔截器來滿足特定業(yè)務(wù)場景。
以上就是6種常見的SpringBoot攔截器使用場景及實(shí)現(xiàn)方式的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot攔截器的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
30分鐘入門Java8之lambda表達(dá)式學(xué)習(xí)
本篇文章主要介紹了30分鐘入門Java8之lambda表達(dá)式學(xué)習(xí),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-04-04Spring源碼學(xué)習(xí)之動態(tài)代理實(shí)現(xiàn)流程
這篇文章主要給大家介紹了關(guān)于Spring源碼學(xué)習(xí)之動態(tài)代理實(shí)現(xiàn)流程的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03一文帶你掌握J(rèn)ava8中函數(shù)式接口的使用和自定義
函數(shù)式接口是?Java?8?引入的一種接口,用于支持函數(shù)式編程,下面我們就來深入探討函數(shù)式接口的概念、用途以及如何創(chuàng)建和使用函數(shù)式接口吧2023-08-08java Class文件結(jié)構(gòu)解析常量池字節(jié)碼
這篇文章主要為大家介紹了java Class文件的整體結(jié)構(gòu)解析常量池字節(jié)碼詳細(xì)講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07詳解Spring Boot 項(xiàng)目啟動時執(zhí)行特定方法
這篇文章主要介紹了詳解Spring Boot 項(xiàng)目啟動時執(zhí)行特定方法,Springboot給我們提供了兩種“開機(jī)啟動”某些方法的方式:ApplicationRunner和CommandLineRunner。感興趣的小伙伴們可以參考一下2018-06-06Java中Comparable與Comparator的區(qū)別解析
這篇文章主要介紹了Java中Comparable與Comparator的區(qū)別解析,實(shí)現(xiàn)Comparable接口,重寫compareTo方法,一般在實(shí)體類定義的時候就可以選擇實(shí)現(xiàn)該接口,提供一個默認(rèn)的排序方式,供Arrays.sort和Collections.sort使用,需要的朋友可以參考下2024-01-01springboot集成JWT之雙重token的實(shí)現(xiàn)
本文主要介紹了springboot集成JWT之雙重token的實(shí)現(xiàn),前端使用accessToken進(jìn)行登錄和驗(yàn)證,后端使用refreshToken定期更新accessToken,具有一定的參考價值,感興趣的可以了解一下2025-03-03