一個優(yōu)秀的Controller層邏輯
說到
Controller
,相信大家都不陌生,它可以很方便地對外提供數(shù)據(jù)接口。它的定位,我認為是「不可或缺的配角」,說它不可或缺是因為無論是傳統(tǒng)的三層架構(gòu)還是現(xiàn)在的COLA架構(gòu),Controller
層依舊有一席之地,說明他的必要性;說它是配角是因為Controller
層的代碼一般是不負責具體的邏輯業(yè)務(wù)邏輯實現(xiàn),但是它負責接收和響應(yīng)請求
從現(xiàn)狀看問題
Controller 主要的工作有以下幾項
- 接收請求并解析參數(shù)
- 調(diào)用 Service 執(zhí)行具體的業(yè)務(wù)代碼(可能包含參數(shù)校驗)
- 捕獲業(yè)務(wù)邏輯異常做出反饋
- 業(yè)務(wù)邏輯執(zhí)行成功做出響應(yīng)
//DTO
@Data
publicclassTestDTO{
privateIntegernum;
privateStringtype;
}
//Service
@Service
publicclassTestService{
publicDoubleservice(TestDTOtestDTO)throwsException{
if(testDTO.getNum()<=?0){
thrownewException("輸入的數(shù)字需要大于0");
}
if(testDTO.getType().equals("square")){
returnMath.pow(testDTO.getNum(),2);
}
if(testDTO.getType().equals("factorial")){
doubleresult=1;
intnum=testDTO.getNum();
while(num>1){
result=result*num;
num-=1;
}
returnresult;
}
thrownewException("未識別的算法");
}
}
//Controller
@RestController
publicclassTestController{
privateTestServicetestService;
@PostMapping("/test")
publicDoubletest(@RequestBodyTestDTOtestDTO){
try{
Doubleresult=this.testService.service(testDTO);
returnresult;
}catch(Exceptione){
thrownewRuntimeException(e);
}
}
@Autowired
publicDTOidsetTestService(TestServicetestService){
this.testService=testService;
}
}
如果真的按照上面所列的工作項來開發(fā) Controller
代碼會有幾個問題
- 參數(shù)校驗過多地耦合了業(yè)務(wù)代碼,違背單一職責原則
- 可能在多個業(yè)務(wù)中都拋出同一個異常,導致代碼重復
- 各種異常反饋和成功響應(yīng)格式不統(tǒng)一,接口對接不友好
改造 Controller 層邏輯
統(tǒng)一返回結(jié)構(gòu)
統(tǒng)一返回值類型無論項目前后端是否分離都是非常必要的,方便對接接口的開發(fā)人員更加清晰地知道這個接口的調(diào)用是否成功(不能僅僅簡單地看返回值是否為 null 就判斷成功與否,因為有些接口的設(shè)計就是如此),使用一個狀態(tài)碼、狀態(tài)信息就能清楚地了解接口調(diào)用情況
//定義返回數(shù)據(jù)結(jié)構(gòu)
publicinterfaceIResult{
IntegergetCode();
StringgetMessage();
}
//常用結(jié)果的枚舉
publicenumResultEnumimplementsIResult{
SUCCESS(2001,"接口調(diào)用成功"),
VALIDATE_FAILED(2002,"參數(shù)校驗失敗"),
COMMON_FAILED(2003,"接口調(diào)用失敗"),
FORBIDDEN(2004,"沒有權(quán)限訪問資源");
privateIntegercode;
privateStringmessage;
//省略get、set方法和構(gòu)造方法
}
//統(tǒng)一返回數(shù)據(jù)結(jié)構(gòu)
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclassResult<T>{
privateIntegercode;
privateStringmessage;
privateTdata;
publicstaticResultsuccess(Tdata) {
returnnewResult<>(ResultEnum.SUCCESS.getCode(),ResultEnum.SUCCESS.getMessage(),data);
}
publicstaticResultsuccess(Stringmessage,Tdata) {
returnnewResult<>(ResultEnum.SUCCESS.getCode(),message,data);
}
publicstaticResultfailed(){
returnnewResult<>(ResultEnum.COMMON_FAILED.getCode(),ResultEnum.COMMON_FAILED.getMessage(),null);
}
publicstaticResultfailed(Stringmessage){
returnnewResult<>(ResultEnum.COMMON_FAILED.getCode(),message,null);
}
publicstaticResultfailed(IResulterrorResult){
returnnewResult<>(errorResult.getCode(),errorResult.getMessage(),null);
}
publicstaticResultinstance(Integercode,Stringmessage,Tdata) {
Resultresult=newResult<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
returnresult;
}
}
統(tǒng)一返回結(jié)構(gòu)后,在 Controller
中就可以使用了,但是每一個 Controller
都寫這么一段最終封裝的邏輯,這些都是很重復的工作,所以還要繼續(xù)想辦法進一步處理統(tǒng)一返回結(jié)構(gòu)
統(tǒng)一包裝處理
Spring 中提供了一個類 ResponseBodyAdvice
,能幫助我們實現(xiàn)上述需求
ResponseBodyAdvice
是對 Controller 返回的內(nèi)容在 HttpMessageConverter
進行類型轉(zhuǎn)換之前攔截,進行相應(yīng)的處理操作后,再將結(jié)果返回給客戶端。那這樣就可以把統(tǒng)一包裝的工作放到這個類里面。
publicinterfaceResponseBodyAdvice<T>{
booleansupports(MethodParameterreturnType,Class>converterType);
@Nullable
TbeforeBodyWrite(@NullableTbody,MethodParameterreturnType,MediaTypeselectedContentType,Class>selectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse);
}
-
supports
:判斷是否要交給beforeBodyWrite
方法執(zhí)行,ture:需要;false:不需要 -
beforeBodyWrite
:對response
進行具體的處理
//如果引入了swagger或knife4j的文檔生成組件,這里需要僅掃描自己項目的包,否則文檔無法正常生成
@RestControllerAdvice(basePackages="com.example.demo")
publicclassResponseAdviceimplementsResponseBodyAdvice<Object>{
@Override
publicbooleansupports(MethodParameterreturnType,Class>converterType){
//如果不需要進行封裝的,可以添加一些校驗手段,比如添加標記排除的注解
returntrue;
}
@Override
publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,Class>selectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){
//提供一定的靈活度,如果body已經(jīng)被包裝了,就不進行包裝
if(bodyinstanceofResult){
returnbody;
}
returnResult.success(body);
}
}
經(jīng)過這樣改造,既能實現(xiàn)對 Controller
返回的數(shù)據(jù)進行統(tǒng)一包裝,又不需要對原有代碼進行大量的改動
參數(shù)校驗
Java API 的規(guī)范 JSR303 定義了校驗的標準 validation-api
,其中一個比較出名的實現(xiàn)是 hibernate validation
,spring validation
是對其的二次封裝,常用于 SpringMVC
的參數(shù)自動校驗,參數(shù)校驗的代碼就不需要再與業(yè)務(wù)邏輯代碼進行耦合了
@PathVariable 和 @RequestParam 參數(shù)校驗
Get 請求的參數(shù)接收一般依賴這兩個注解,但是處于 url 有長度限制和代碼的可維護性,超過 5 個參數(shù)盡量用實體來傳參對 @PathVariable
和 @RequestParam
參數(shù)進行校驗需要在入?yún)⒙暶骷s束的注解
如果校驗失敗,會拋出 MethodArgumentNotValidException
異常
@RestController(value="prettyTestController")
@RequestMapping("/pretty")
@Validated
publicclassTestController{
privateTestServicetestService;
@GetMapping("/{num}")
publicIntegerdetail(@PathVariable("num")@Min(1)@Max(20)Integernum){
returnnum*num;
}
@GetMapping("/getByEmail")
publicTestDTOgetByAccount(@RequestParam@NotBlank@EmailStringemail){
TestDTOtestDTO=newTestDTO();
testDTO.setEmail(email);
returntestDTO;
}
@Autowired
publicvoidsetTestService(TestServiceprettyTestService){
this.testService=prettyTestService;
}
}
校驗原理
在 SpringMVC 中,有一個類是 RequestResponseBodyMethodProcessor
,這個類有兩個作用(實際上可以從名字上得到一點啟發(fā))
-
用于解析
@RequestBody
標注的參數(shù) -
處理
@ResponseBody
標注方法的返回值
解析 @RequestBoyd
標注參數(shù)的方法是 resolveArgument
publicclassRequestResponseBodyMethodProcessorextendsAbstractMessageConverterMethodProcessor{ /** *ThrowsMethodArgumentNotValidExceptionifvalidationfails. *@throwsHttpMessageNotReadableExceptionif{@linkRequestBody#required()} *is{@codetrue}andthereisnobodycontentorifthereisnosuitable *convertertoreadthecontentwith. */ @Override publicObjectresolveArgument(MethodParameterparameter,@NullableModelAndViewContainermavContainer, NativeWebRequestwebRequest,@NullableWebDataBinderFactorybinderFactory)throwsException{ parameter=parameter.nestedIfOptional(); //把請求數(shù)據(jù)封裝成標注的DTO對象 Objectarg=readWithMessageConverters(webRequest,parameter,parameter.getNestedGenericParameterType()); Stringname=Conventions.getVariableNameForParameter(parameter); if(binderFactory!=null){ WebDataBinderbinder=binderFactory.createBinder(webRequest,arg,name); if(arg!=null){ //執(zhí)行數(shù)據(jù)校驗 validateIfApplicable(binder,parameter); //如果校驗不通過,就拋出MethodArgumentNotValidException異常 //如果我們不自己捕獲,那么最終會由DefaultHandlerExceptionResolver捕獲處理 if(binder.getBindingResult().hasErrors()&&isBindExceptionRequired(binder,parameter)){ thrownewMethodArgumentNotValidException(parameter,binder.getBindingResult()); } } if(mavContainer!=null){ mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX+name,binder.getBindingResult()); } } returnadaptArgumentIfNecessary(arg,parameter); } } publicabstractclassAbstractMessageConverterMethodArgumentResolverimplementsHandlerMethodArgumentResolver{ /** *Validatethebindingtargetifapplicable. *
Thedefaultimplementationchecksfor{@code@javax.validation.Valid}, *Spring's{@linkorg.springframework.validation.annotation.Validated}, *andcustomannotationswhosenamestartswith"Valid". *@parambindertheDataBindertobeused *@paramparameterthemethodparameterdescriptor *@since4.1.5 *@see#isBindExceptionRequired */ protectedvoidvalidateIfApplicable(WebDataBinderbinder,MethodParameterparameter){ //獲取參數(shù)上的所有注解 Annotation[]annotations=parameter.getParameterAnnotations(); for(Annotationann:annotations){ //如果注解中包含了@Valid、@Validated或者是名字以Valid開頭的注解就進行參數(shù)校驗 Object[]validationHints=ValidationAnnotationUtils.determineValidationHints(ann); if(validationHints!=null){ //實際校驗邏輯,最終會調(diào)用HibernateValidator執(zhí)行真正的校驗 //所以SpringValidation是對HibernateValidation的二次封裝 binder.validate(validationHints); break; } } } }
@RequestBody 參數(shù)校驗
Post、Put 請求的參數(shù)推薦使用 @RequestBody
請求體參數(shù)
對 @RequestBody
參數(shù)進行校驗需要在 DTO 對象中加入校驗條件后,再搭配 @Validated
即可完成自動校驗如果校驗失敗,會拋出 ConstraintViolationException
異常
//DTO
@Data
publicclassTestDTO{
@NotBlank
privateStringuserName;
@NotBlank
@Length(min=6,max=20)
privateStringpassword;
@NotNull
@Email
privateStringemail;
}
//Controller
@RestController(value="prettyTestController")
@RequestMapping("/pretty")
publicclassTestController{
privateTestServicetestService;
@PostMapping("/test-validation")
publicvoidtestValidation(@RequestBody@ValidatedTestDTOtestDTO){
this.testService.save(testDTO);
}
@Autowired
publicvoidsetTestService(TestServicetestService){
this.testService=testService;
}
}
校驗原理
聲明約束的方式,注解加到了參數(shù)上面,可以比較容易猜測到是使用了 AOP 對方法進行增強
而實際上 Spring 也是通過 MethodValidationPostProcessor
動態(tài)注冊 AOP 切面,然后使用 MethodValidationInterceptor
對切點方法進行織入增強
publicclassMethodValidationPostProcessorextendsAbstractBeanFactoryAwareAdvisingPostProcessorimplementsInitializingBean{
//指定了創(chuàng)建切面的Bean的注解
privateClassvalidatedAnnotationType=Validated.class;
@Override
publicvoidafterPropertiesSet(){
//為所有@Validated標注的Bean創(chuàng)建切面
Pointcutpointcut=newAnnotationMatchingPointcut(this.validatedAnnotationType,true);
//創(chuàng)建Advisor進行增強
this.advisor=newDefaultPointcutAdvisor(pointcut,createMethodValidationAdvice(this.validator));
}
//創(chuàng)建Advice,本質(zhì)就是一個方法攔截器
protectedAdvicecreateMethodValidationAdvice(@NullableValidatorvalidator){
return(validator!=null?newMethodValidationInterceptor(validator):newMethodValidationInterceptor());
}
}
publicclassMethodValidationInterceptorimplementsMethodInterceptor{
@Override
publicObjectinvoke(MethodInvocationinvocation)throwsThrowable{
//無需增強的方法,直接跳過
if(isFactoryBeanMetadataMethod(invocation.getMethod())){
returninvocation.proceed();
}
Class[]groups=determineValidationGroups(invocation);
ExecutableValidatorexecVal=this.validator.forExecutables();
MethodmethodToValidate=invocation.getMethod();
Set>result;
try{
//方法入?yún)⑿r?,最終還是委托給HibernateValidator來校驗
//所以SpringValidation是對HibernateValidation的二次封裝
result=execVal.validateParameters(
invocation.getThis(),methodToValidate,invocation.getArguments(),groups);
}
catch(IllegalArgumentExceptionex){
...
}
//校驗不通過拋出ConstraintViolationException異常
if(!result.isEmpty()){
thrownewConstraintViolationException(result);
}
//Controller方法調(diào)用
ObjectreturnValue=invocation.proceed();
//下面是對返回值做校驗,流程和上面大概一樣
result=execVal.validateReturnValue(invocation.getThis(),methodToValidate,returnValue,groups);
if(!result.isEmpty()){
thrownewConstraintViolationException(result);
}
returnreturnValue;
}
}
自定義校驗規(guī)則
有些時候 JSR303 標準中提供的校驗規(guī)則不滿足復雜的業(yè)務(wù)需求,也可以自定義校驗規(guī)則
自定義校驗規(guī)則需要做兩件事情
- 自定義注解類,定義錯誤信息和一些其他需要的內(nèi)容
- 注解校驗器,定義判定規(guī)則
//自定義注解類
@Target({ElementType.METHOD,ElementType.FIELD,ElementType.ANNOTATION_TYPE,ElementType.CONSTRUCTOR,ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy=MobileValidator.class)
public@interfaceMobile{
/**
*是否允許為空
*/
booleanrequired()defaulttrue;
/**
*校驗不通過返回的提示信息
*/
Stringmessage()default"不是一個手機號碼格式";
/**
*Constraint要求的屬性,用于分組校驗和擴展,留空就好
*/
Class[]groups()default{};
Class[]payload()default{};
}
//注解校驗器
publicclassMobileValidatorimplementsConstraintValidator<Mobile,CharSequence>{
privatebooleanrequired=false;
privatefinalPatternpattern=Pattern.compile("^1[34578][0-9]{9}$");//驗證手機號
/**
*在驗證開始前調(diào)用注解里的方法,從而獲取到一些注解里的參數(shù)
*
*@paramconstraintAnnotationannotationinstanceforagivenconstraintdeclaration
*/
@Override
publicvoidinitialize(MobileconstraintAnnotation){
this.required=constraintAnnotation.required();
}
/**
*判斷參數(shù)是否合法
*
*@paramvalueobjecttovalidate
*@paramcontextcontextinwhichtheconstraintisevaluated
*/
@Override
publicbooleanisValid(CharSequencevalue,ConstraintValidatorContextcontext){
if(this.required){
//驗證
returnisMobile(value);
}
if(StringUtils.hasText(value)){
//驗證
returnisMobile(value);
}
returntrue;
}
privatebooleanisMobile(finalCharSequencestr){
Matcherm=pattern.matcher(str);
returnm.matches();
}
}
自動校驗參數(shù)真的是一項非常必要、非常有意義的工作。JSR303 提供了豐富的參數(shù)校驗規(guī)則,再加上復雜業(yè)務(wù)的自定義校驗規(guī)則,完全把參數(shù)校驗和業(yè)務(wù)邏輯解耦開,代碼更加簡潔,符合單一職責原則。
更多關(guān)于 Spring 參數(shù)校驗請參考:
https://juejin.cn/post/6856541106626363399
自定義異常與統(tǒng)一攔截異常
原來的代碼中可以看到有幾個問題
-
拋出的異常不夠具體,只是簡單地把錯誤信息放到了
Exception
中 -
拋出異常后,
Controller
不能具體地根據(jù)異常做出反饋 - 雖然做了參數(shù)自動校驗,但是異常返回結(jié)構(gòu)和正常返回結(jié)構(gòu)不一致
自定義異常是為了后面統(tǒng)一攔截異常時,對業(yè)務(wù)中的異常有更加細顆粒度的區(qū)分,攔截時針對不同的異常作出不同的響應(yīng)
而統(tǒng)一攔截異常的目的一個是為了可以與前面定義下來的統(tǒng)一包裝返回結(jié)構(gòu)能對應(yīng)上,另一個是我們希望無論系統(tǒng)發(fā)生什么異常,Http 的狀態(tài)碼都要是 200 ,盡可能由業(yè)務(wù)來區(qū)分系統(tǒng)的異常
//自定義異常
publicclassForbiddenExceptionextendsRuntimeException{
publicForbiddenException(Stringmessage){
super(message);
}
}
//自定義異常
publicclassBusinessExceptionextendsRuntimeException{
publicBusinessException(Stringmessage){
super(message);
}
}
//統(tǒng)一攔截異常
@RestControllerAdvice(basePackages="com.example.demo")
publicclassExceptionAdvice{
/**
*捕獲{@codeBusinessException}異常
*/
@ExceptionHandler({BusinessException.class})
publicResulthandleBusinessException(BusinessExceptionex){
returnResult.failed(ex.getMessage());
}
/**
*捕獲{@codeForbiddenException}異常
*/
@ExceptionHandler({ForbiddenException.class})
publicResulthandleForbiddenException(ForbiddenExceptionex){
returnResult.failed(ResultEnum.FORBIDDEN);
}
/**
*{@code@RequestBody}參數(shù)校驗不通過時拋出的異常處理
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
publicResulthandleMethodArgumentNotValidException(MethodArgumentNotValidExceptionex){
BindingResultbindingResult=ex.getBindingResult();
StringBuildersb=newStringBuilder("校驗失敗:");
for(FieldErrorfieldError:bindingResult.getFieldErrors()){
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage()).append(",");
}
Stringmsg=sb.toString();
if(StringUtils.hasText(msg)){
returnResult.failed(ResultEnum.VALIDATE_FAILED.getCode(),msg);
}
returnResult.failed(ResultEnum.VALIDATE_FAILED);
}
/**
*{@code@PathVariable}和{@code@RequestParam}參數(shù)校驗不通過時拋出的異常處理
*/
@ExceptionHandler({ConstraintViolationException.class})
publicResulthandleConstraintViolationException(ConstraintViolationExceptionex){
if(StringUtils.hasText(ex.getMessage())){
returnResult.failed(ResultEnum.VALIDATE_FAILED.getCode(),ex.getMessage());
}
returnResult.failed(ResultEnum.VALIDATE_FAILED);
}
/**
*頂級異常捕獲并統(tǒng)一處理,當其他異常無法處理時候選擇使用
*/
@ExceptionHandler({Exception.class})
publicResulthandle(Exceptionex){
returnResult.failed(ex.getMessage());
}
}
總結(jié)
做好了這一切改動后,可以發(fā)現(xiàn) Controller
的代碼變得非常簡潔,可以很清楚地知道每一個參數(shù)、每一個 DTO 的校驗規(guī)則,可以很明確地看到每一個 Controller
方法返回的是什么數(shù)據(jù),也可以方便每一個異常應(yīng)該如何進行反饋
這一套操作下來后,我們能更加專注于業(yè)務(wù)邏輯的開發(fā),代碼簡潔、功能完善,何樂而不為呢?
審核編輯:湯梓紅
-
Controller
+關(guān)注
關(guān)注
0文章
398瀏覽量
57372 -
代碼
+關(guān)注
關(guān)注
30文章
4876瀏覽量
69960
原文標題:Controller層代碼這么寫,簡潔又優(yōu)雅!
文章出處:【微信號:AndroidPush,微信公眾號:Android編程精選】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
請問一下lcd controller.c如何調(diào)用s3c2440 lcd controller.c的?
嵌入式系統(tǒng)的硬件層
列舉一個memory controller的示例
邏輯層網(wǎng)絡(luò)拓撲發(fā)現(xiàn)方法研究

網(wǎng)絡(luò)層和傳輸層的區(qū)別

評論