如何将多Java控制器中的提前返回校验逻辑提取为共享方法?
如何将多Java控制器中的提前返回校验逻辑提取为共享方法?
这个问题我太有共鸣了——重复的校验代码散在几十个控制器里,不仅维护麻烦,还容易写错!给你几个从简单到优雅的解决方案,按需选择:
1. 工具类快速落地(最直接)
先搞一个静态工具类把校验逻辑封装起来,虽然不是最优雅,但改动最小,能快速解决重复代码问题。
工具类实现
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import java.util.Map; import java.util.Objects; import java.util.Optional; public class UserValidationUtils { private static final String SUCCESS = "success"; private static final String ERROR = "error"; // 封装校验逻辑,返回Optional:校验不通过时返回封装好的响应,否则返回空 public static Optional<ResponseEntity<?>> validateUserMatch(Authentication authentication, Long requestUserId) { User user = (User) authentication.getPrincipal(); if (Objects.isNull(user) || !user.getId().equals(requestUserId)) { return Optional.of(ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of( SUCCESS, false, ERROR, ErrorCode.RETRIEVING_ANOTHER_USER_DATA_PROHIBITED.getMessage() ))); } return Optional.empty(); } }
控制器中使用
原来的校验代码可以替换成这两行:
@PostMapping("results") @ResponseBody public ResponseEntity<?> results( Authentication authentication, @RequestBody ResultsRequest request ) { try { // 调用工具类做校验,不通过直接返回 Optional<ResponseEntity<?>> validationResult = UserValidationUtils.validateUserMatch(authentication, request.getUserId()); if (validationResult.isPresent()) { return validationResult.get(); } // 校验通过,继续业务逻辑 return resultsService.getResults(request.getUserId()); } catch (Exception e) { LOGGER.warn(ErrorCode.ERROR_GETTING_RESULTS.getMessage(), e); } return ResponseEntity.ok(List.of()); }
2. 自定义注解+AOP(最优雅,无侵入)
如果想彻底把校验逻辑从控制器里剥离,用AOP是最优解——只需要给目标方法加个注解,就能自动触发校验,完全消除重复代码。
步骤1:定义自定义注解
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckUserIdMatch { }
步骤2:统一请求DTO的接口
让所有包含userId的请求DTO实现一个通用接口,方便切面统一获取userId:
public interface HasUserId { Long getUserId(); }
比如你的ResultsRequest就改成:
public class ResultsRequest implements HasUserId { // 原有的字段和方法 @Override public Long getUserId() { return this.userId; } }
步骤3:编写切面类
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import java.util.Map; import java.util.Objects; @Aspect @Component public class UserValidationAspect { private static final String SUCCESS = "success"; private static final String ERROR = "error"; // 拦截所有加了@CheckUserIdMatch的方法 @Around("@annotation(com.yourpackage.CheckUserIdMatch)") public Object validateUserMatch(ProceedingJoinPoint joinPoint) throws Throwable { Authentication authentication = null; HasUserId request = null; // 从方法参数中找到Authentication和请求DTO for (Object arg : joinPoint.getArgs()) { if (arg instanceof Authentication) { authentication = (Authentication) arg; } else if (arg instanceof HasUserId) { request = (HasUserId) arg; } } // 参数合法性校验(避免方法参数不符合要求的情况) if (authentication == null || request == null) { throw new IllegalArgumentException("目标方法必须包含Authentication参数和实现HasUserId的请求DTO"); } // 核心校验逻辑 User user = (User) authentication.getPrincipal(); if (Objects.isNull(user) || !user.getId().equals(request.getUserId())) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of( SUCCESS, false, ERROR, ErrorCode.RETRIEVING_ANOTHER_USER_DATA_PROHIBITED.getMessage() )); } // 校验通过,继续执行原方法 return joinPoint.proceed(); } }
步骤4:控制器中使用
只需要给方法加个注解,原来的校验代码可以全部删掉:
@PostMapping("results") @ResponseBody @CheckUserIdMatch // 加这个注解就自动校验了 public ResponseEntity<?> results( Authentication authentication, @RequestBody ResultsRequest request ) { try { // 直接走业务逻辑,不用再写校验 return resultsService.getResults(request.getUserId()); } catch (Exception e) { LOGGER.warn(ErrorCode.ERROR_GETTING_RESULTS.getMessage(), e); } return ResponseEntity.ok(List.of()); }
这种方式彻底解耦了校验逻辑和业务代码,以后新增控制器方法只要加个注解就行,维护成本极低。
3. Spring Security表达式(适合权限体系结合)
如果你的项目已经在用Spring Security,也可以用@PreAuthorize结合自定义SpEL方法来实现,能和现有权限控制体系无缝融合。
步骤1:编写权限校验服务
import org.springframework.security.core.Authentication; import org.springframework.stereotype.Service; import java.util.Objects; @Service("userSecurityService") public class UserSecurityService { public boolean matchesUserId(Authentication authentication, Long requestUserId) { User user = (User) authentication.getPrincipal(); return user != null && user.getId().equals(requestUserId); } }
步骤2:控制器中使用@PreAuthorize
import org.springframework.security.access.prepost.PreAuthorize; @PostMapping("results") @ResponseBody // 用SpEL调用自定义的校验方法 @PreAuthorize("@userSecurityService.matchesUserId(authentication, #request.userId)") public ResponseEntity<?> results( Authentication authentication, @RequestBody ResultsRequest request ) { try { return resultsService.getResults(request.getUserId()); } catch (Exception e) { LOGGER.warn(ErrorCode.ERROR_GETTING_RESULTS.getMessage(), e); } return ResponseEntity.ok(List.of()); }
步骤3:全局异常处理
默认情况下,校验不通过会抛出AccessDeniedException,需要写一个全局异常处理器把它转换成你要的响应格式:
import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.AccessDeniedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import java.util.Map; @ControllerAdvice public class GlobalExceptionHandler { private static final String SUCCESS = "success"; private static final String ERROR = "error"; @ExceptionHandler(AccessDeniedException.class) public ResponseEntity<?> handleAccessDenied(AccessDeniedException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(Map.of( SUCCESS, false, ERROR, ErrorCode.RETRIEVING_ANOTHER_USER_DATA_PROHIBITED.getMessage() )); } }
总结
- 如果想快速改造,选工具类方案,改动最小;
- 如果想彻底解耦、代码更干净,选AOP+自定义注解方案;
- 如果项目已经用了Spring Security,选SpEL表达式方案,和权限体系结合更紧密。
备注:内容来源于stack exchange,提问作者lenny
相关产品推荐
相关产品推荐

