👉 這是一個或許對你有用的社群
《專案實戰(影片)》:從書中學,往事上“練” 《網際網路高頻面試題》:面朝簡歷學習,春暖花開 《架構 x 系統設計》:摧枯拉朽,掌控面試高頻場景題 《精進 Java 學習指南》:系統學習,網際網路主流技術棧 《必讀 Java 原始碼專欄》:知其然,知其所以然

👉這是一個或許對你有用的開源專案國產 Star 破 10w+ 的開源專案,前端包括管理後臺 + 微信小程式,後端支援單體和微服務架構。功能涵蓋 RBAC 許可權、SaaS 多租戶、資料許可權、商城、支付、工作流、大屏報表、微信公眾號、ERP、CRM、AI 大模型等等功能:
Boot 多模組架構:https://gitee.com/zhijiantianya/ruoyi-vue-pro Cloud 微服務架構:https://gitee.com/zhijiantianya/yudao-cloud 影片教程:https://doc.iocoder.cn 【國內首批】支援 JDK 17/21 + SpringBoot 3.3、JDK 8/11 + Spring Boot 2.7 雙版本

一個優秀的 Controller 層邏輯
基於 Spring Boot + MyBatis Plus + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/ruoyi-vue-pro 影片教程:https://doc.iocoder.cn/video/
從現狀看問題
-
接收請求並解析引數 -
呼叫 Service 執行具體的業務程式碼(可能包含引數校驗) -
捕獲業務邏輯異常做出反饋 -
業務邏輯執行成功做出響應
//DTO
@Data
publicclassTestDTO
{
private
Integer num;
private
String type;
}
//Service
@Service
publicclassTestService
{
public Double service(TestDTO testDTO)throws Exception
{
if
(testDTO.getNum() <=
0
) {
thrownew
Exception(
"輸入的數字需要大於0"
);
}
if
(testDTO.getType().equals(
"square"
)) {
return
Math.pow(testDTO.getNum(),
2
);
}
if
(testDTO.getType().equals(
"factorial"
)) {
double
result =
1
;
int
num = testDTO.getNum();
while
(num >
1
) {
result = result * num;
num -=
1
;
}
return
result;
}
thrownew
Exception(
"未識別的演算法"
);
}
}
//Controller
@RestController
publicclassTestController
{
private
TestService testService;
@PostMapping
(
"/test"
)
public Double test(@RequestBody TestDTO testDTO)
{
try
{
Double result =
this
.testService.service(testDTO);
return
result;
}
catch
(Exception e) {
thrownew
RuntimeException(e);
}
}
@Autowired
public DTOid setTestService(TestService testService)
{
this
.testService = testService;
}
}
-
引數校驗過多地耦合了業務程式碼,違背單一職責原則 -
可能在多個業務中都丟擲同一個異常,導致程式碼重複 -
各種異常反饋和成功響應格式不統一,介面對接不友好
基於 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實現的後臺管理系統 + 使用者小程式,支援 RBAC 動態許可權、多租戶、資料許可權、工作流、三方登入、支付、簡訊、商城等功能
專案地址:https://github.com/YunaiV/yudao-cloud 影片教程:https://doc.iocoder.cn/video/
改造 Controller 層邏輯
統一返回結構
//定義返回資料結構
publicinterfaceIResult
{
Integer getCode()
;
String getMessage()
;
}
//常用結果的列舉
publicenum
ResultEnum implements IResult {
SUCCESS(
2001
,
"介面呼叫成功"
),
VALIDATE_FAILED(
2002
,
"引數校驗失敗"
),
COMMON_FAILED(
2003
,
"介面呼叫失敗"
),
FORBIDDEN(
2004
,
"沒有許可權訪問資源"
);
private
Integer code;
private
String message;
//省略get、set方法和構造方法
}
//統一返回資料結構
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclassResult<T>
{
private
Integer code;
private
String message;
private
T data;
publicstatic
<T>
Result<T> success(T data)
{
returnnew
Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
}
publicstatic
<T>
Result<T> success(String message, T data)
{
returnnew
Result<>(ResultEnum.SUCCESS.getCode(), message, data);
}
publicstatic
Result<?> failed() {
returnnew
Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(),
null
);
}
publicstatic
Result<?> failed(String message) {
returnnew
Result<>(ResultEnum.COMMON_FAILED.getCode(), message,
null
);
}
publicstatic
Result<?> failed(IResult errorResult) {
returnnew
Result<>(errorResult.getCode(), errorResult.getMessage(),
null
);
}
publicstatic
<T>
Result<T> instance(Integer code, String message, T data)
{
Result<T> result =
new
Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
return
result;
}
}
統一包裝處理
publicinterfaceResponseBodyAdvice<T>
{
booleansupports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType)
;
@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response)
;
}
-
supports: 判斷是否要交給 beforeBodyWrite 方法執行,ture:需要;false:不需要 -
beforeBodyWrite: 對 response 進行具體的處理
// 如果引入了swagger或knife4j的文件生成元件,這裡需要僅掃描自己專案的包,否則文件無法正常生成
@RestControllerAdvice
(basePackages =
"com.example.demo"
)
publicclassResponseAdviceimplementsResponseBodyAdvice<Object>
{
@Override
publicbooleansupports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType)
{
// 如果不需要進行封裝的,可以新增一些校驗手段,比如新增標記排除的註解
returntrue
;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response)
{
// 提供一定的靈活度,如果body已經被包裝了,就不進行包裝
if
(body
instanceof
Result) {
return
body;
}
return
Result.success(body);
}
}
引數校驗
①@PathVariable 和 @RequestParam 引數校驗
@RestController
(value =
"prettyTestController"
)
@RequestMapping
(
"/pretty"
)
publicclassTestController
{
private
TestService testService;
@GetMapping
(
"/{num}"
)
public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num)
{
return
num * num;
}
@GetMapping
(
"/getByEmail"
)
public TestDTO getByAccount(@RequestParam @NotBlank @Email String email)
{
TestDTO testDTO =
new
TestDTO();
testDTO.setEmail(email);
return
testDTO;
}
@Autowired
publicvoidsetTestService(TestService prettyTestService)
{
this
.testService = prettyTestService;
}
}
校驗原理
-
用於解析 @RequestBody 標註的引數 -
處理 @ResponseBody 標註方法的返回值
publicclassRequestResponseBodyMethodProcessorextendsAbstractMessageConverterMethodProcessor
{
/**
* Throws MethodArgumentNotValidException if validation fails.
*
@throws
HttpMessageNotReadableException if {
@link
RequestBody#required()}
* is {
@code
true} and there is no body content or if there is no suitable
* converter to read the content with.
*/
@Override
public Object resolveArgument
(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
throws Exception
{
parameter = parameter.nestedIfOptional();
//把請求資料封裝成標註的DTO物件
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);
if
(binderFactory !=
null
) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if
(arg !=
null
) {
//執行資料校驗
validateIfApplicable(binder, parameter);
//如果校驗不透過,就丟擲MethodArgumentNotValidException異常
//如果我們不自己捕獲,那麼最終會由DefaultHandlerExceptionResolver捕獲處理
if
(binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
thrownew
MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if
(mavContainer !=
null
) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return
adaptArgumentIfNecessary(arg, parameter);
}
}
publicabstractclassAbstractMessageConverterMethodArgumentResolverimplementsHandlerMethodArgumentResolver
{
/**
* Validate the binding target if applicable.
* <p>The default implementation checks for {
@code@javax
.validation.Valid},
* Spring's {
@link
org.springframework.validation.annotation.Validated},
* and custom annotations whose name starts with "Valid".
*
@param
binder the DataBinder to be used
*
@param
parameter the method parameter descriptor
*
@since
4.1.5
*
@see
#isBindExceptionRequired
*/
protectedvoidvalidateIfApplicable(WebDataBinder binder, MethodParameter parameter)
{
//獲取引數上的所有註解
Annotation[] annotations = parameter.getParameterAnnotations();
for
(Annotation ann : annotations) {
//如果註解中包含了@Valid、@Validated或者是名字以Valid開頭的註解就進行引數校驗
Object[] validationHints = ValidationAnnotationUtils.determineValidationHints(ann);
if
(validationHints !=
null
) {
//實際校驗邏輯,最終會呼叫Hibernate Validator執行真正的校驗
//所以Spring Validation是對Hibernate Validation的二次封裝
binder.validate(validationHints);
break
;
}
}
}
}
②@RequestBody 引數校驗
//DTO
@Data
publicclassTestDTO
{
@NotBlank
private
String userName;
@NotBlank
@Length
(min =
6
, max =
20
)
private
String password;
@NotNull
@Email
private
String email;
}
//Controller
@RestController
(value =
"prettyTestController"
)
@RequestMapping
(
"/pretty"
)
publicclassTestController
{
private
TestService testService;
@PostMapping
(
"/test-validation"
)
publicvoidtestValidation(@RequestBody @Validated TestDTO testDTO)
{
this
.testService.save(testDTO);
}
@Autowired
publicvoidsetTestService(TestService testService)
{
this
.testService = testService;
}
}
校驗原理
publicclassMethodValidationPostProcessorextendsAbstractBeanFactoryAwareAdvisingPostProcessorimplementsInitializingBean
{
//指定了建立切面的Bean的註解
private
Class<? extends Annotation> validatedAnnotationType = Validated
.class
;
@Override
publicvoidafterPropertiesSet()
{
//為所有@Validated標註的Bean建立切面
Pointcut pointcut =
new
AnnotationMatchingPointcut(
this
.validatedAnnotationType,
true
);
//建立Advisor進行增強
this
.advisor =
new
DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(
this
.validator));
}
//建立Advice,本質就是一個方法攔截器
protected Advice createMethodValidationAdvice(@Nullable Validator validator)
{
return
(validator !=
null
?
new
MethodValidationInterceptor(validator) :
new
MethodValidationInterceptor());
}
}
publicclassMethodValidationInterceptorimplementsMethodInterceptor
{
@Override
public Object invoke(MethodInvocation invocation)throws Throwable
{
//無需增強的方法,直接跳過
if
(isFactoryBeanMetadataMethod(invocation.getMethod())) {
return
invocation.proceed();
}
Class<?>[] groups = determineValidationGroups(invocation);
ExecutableValidator execVal =
this
.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
try
{
//方法入參校驗,最終還是委託給Hibernate Validator來校驗
//所以Spring Validation是對Hibernate Validation的二次封裝
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch
(IllegalArgumentException ex) {
...
}
//校驗不透過丟擲ConstraintViolationException異常
if
(!result.isEmpty()) {
thrownew
ConstraintViolationException(result);
}
//Controller方法呼叫
Object returnValue = invocation.proceed();
//下面是對返回值做校驗,流程和上面大概一樣
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if
(!result.isEmpty()) {
thrownew
ConstraintViolationException(result);
}
return
returnValue;
}
}
③自定義校驗規則
-
自定義註解類,定義錯誤資訊和一些其他需要的內容 -
註解校驗器,定義判定規則
//自定義註解類
@Target
({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention
(RetentionPolicy.RUNTIME)
@Documented
@Constraint
(validatedBy = MobileValidator
.
class
)
public
@
interfaceMobile
{
/**
* 是否允許為空
*/
booleanrequired()defaulttrue
;
/**
* 校驗不透過返回的提示資訊
*/
String message()default "不是一個手機號碼格式"
;
/**
* Constraint要求的屬性,用於分組校驗和擴充套件,留空就好
*/
Class<?>[] groups()
default
{};
Class<? extends Payload>[] payload()
default
{};
}
//註解校驗器
publicclassMobileValidatorimplementsConstraintValidator<Mobile, CharSequence>
{
privateboolean
required =
false
;
privatefinal
Pattern pattern = Pattern.compile(
"^1[34578][0-9]{9}$"
);
// 驗證手機號
/**
* 在驗證開始前呼叫註解裡的方法,從而獲取到一些註解裡的引數
*
*
@param
constraintAnnotation annotation instance for a given constraint declaration
*/
@Override
publicvoidinitialize(Mobile constraintAnnotation)
{
this
.required = constraintAnnotation.required();
}
/**
* 判斷引數是否合法
*
*
@param
value object to validate
*
@param
context context in which the constraint is evaluated
*/
@Override
publicbooleanisValid(CharSequence value, ConstraintValidatorContext context)
{
if
(
this
.required) {
// 驗證
return
isMobile(value);
}
if
(StringUtils.hasText(value)) {
// 驗證
return
isMobile(value);
}
returntrue
;
}
privatebooleanisMobile(final CharSequence str)
{
Matcher m = pattern.matcher(str);
return
m.matches();
}
}
自定義異常與統一攔截異常
-
丟擲的異常不夠具體,只是簡單地把錯誤資訊放到了 Exception 中 -
丟擲異常後,Controller 不能具體地根據異常做出反饋 -
雖然做了引數自動校驗,但是異常返回結構和正常返回結構不一致
//自定義異常
publicclassForbiddenExceptionextendsRuntimeException
{
publicForbiddenException(String message)
{
super
(message);
}
}
//自定義異常
publicclassBusinessExceptionextendsRuntimeException
{
publicBusinessException(String message)
{
super
(message);
}
}
//統一攔截異常
@RestControllerAdvice
(basePackages =
"com.example.demo"
)
publicclassExceptionAdvice
{
/**
* 捕獲 {
@code
BusinessException} 異常
*/
@ExceptionHandler
({BusinessException
.
class
})
publicResult
<?>
handleBusinessException
(
BusinessExceptionex
)
{
return
Result.failed(ex.getMessage());
}
/**
* 捕獲 {
@code
ForbiddenException} 異常
*/
@ExceptionHandler
({ForbiddenException
.
class
})
publicResult
<?>
handleForbiddenException
(
ForbiddenExceptionex
)
{
return
Result.failed(ResultEnum.FORBIDDEN);
}
/**
* {
@code@RequestBody
} 引數校驗不透過時丟擲的異常處理
*/
@ExceptionHandler
({MethodArgumentNotValidException
.
class
})
publicResult
<?>
handleMethodArgumentNotValidException
(
MethodArgumentNotValidExceptionex
)
{
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb =
new
StringBuilder(
"校驗失敗:"
);
for
(FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(
":"
).append(fieldError.getDefaultMessage()).append(
", "
);
}
String msg = sb.toString();
if
(StringUtils.hasText(msg)) {
return
Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
}
return
Result.failed(ResultEnum.VALIDATE_FAILED);
}
/**
* {
@code@PathVariable
} 和 {
@code@RequestParam
} 引數校驗不透過時丟擲的異常處理
*/
@ExceptionHandler
({ConstraintViolationException
.
class
})
publicResult
<?>
handleConstraintViolationException
(
ConstraintViolationExceptionex
)
{
if
(StringUtils.hasText(ex.getMessage())) {
return
Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
}
return
Result.failed(ResultEnum.VALIDATE_FAILED);
}
/**
* 頂級異常捕獲並統一處理,當其他異常無法處理時候選擇使用
*/
@ExceptionHandler
({Exception
.
class
})
publicResult
<?>
handle
(
Exceptionex
)
{
return
Result.failed(ex.getMessage());
}
}
總結





