0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

Controller层代码就该这么写

jf_ro2CN3Fa 来源:掘金 gelald 作者:掘金 gelald 2022-11-21 10:28 次阅读

一个优秀的 Controller 层逻辑

从现状看问题

改造 Controller 层逻辑

统一返回结构

统一包装处理

参数校验

自定义异常与统一拦截异常

总结

一个优秀的 Controller 层逻辑

说到 Controller,相信大家都不陌生,它可以很方便地对外提供数据接口。它的定位,我认为是「不可或缺的配角」。

说它不可或缺是因为无论是传统的三层架构还是现在的 COLA 架构,Controller 层依旧有一席之地,说明他的必要性。

说它是配角是因为 Controller 层的代码一般是不负责具体的逻辑业务逻辑实现,但是它负责接收和响应请求。

从现状看问题

Controller 主要的工作有以下几项:

接收请求并解析参数

调用 Service 执行具体的业务代码(可能包含参数校验)

捕获业务逻辑异常做出反馈

业务逻辑执行成功做出响应

//DTO
@Data
publicclassTestDTO{
privateIntegernum;
privateStringtype;
}


//Service
@Service
publicclassTestService{

publicDoubleservice(TestDTOtestDTO)throwsException{
if(testDTO.getNum()<= 0) {
            throw new 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;
}
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;
}
}

如果真的按照上面所列的工作项来开发 Controller 代码会有几个问题:

参数校验过多地耦合了业务代码,违背单一职责原则

可能在多个业务中都抛出同一个异常,导致代码重复

各种异常反馈和成功响应格式不统一,接口对接不友好

改造 Controller 层逻辑

统一返回结构

统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此)。

使用一个状态码、状态信息就能清楚地了解接口调用情况:

//定义返回数据结构
publicinterfaceIResult{
IntegergetCode();
StringgetMessage();
}

//常用结果的枚举
publicenumResultEnumimplementsIResult{
SUCCESS(2001,"接口调用成功"),
VALIDATE_FAILED(2002,"参数校验失败"),
COMMON_FAILED(2003,"接口调用失败"),
FORBIDDEN(2004,"没有权限访问资源");

privateIntegercode;
privateStringmessage;

//省略get、set方法和构造方法
}

//统一返回数据结构
@Data
@NoArgsConstructor
@AllArgsConstructor
publicclassResult{
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;
}
}

统一返回结构后,在 Controller 中就可以使用了,但是每一个 Controller 都写这么一段最终封装的逻辑,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构。

统一包装处理

Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求:

publicinterfaceResponseBodyAdvice{
booleansupports(MethodParameterreturnType,Class>converterType);

@Nullable
TbeforeBodyWrite(@NullableTbody,MethodParameterreturnType,MediaTypeselectedContentType,Class>selectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse);
}

ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。

那这样就可以把统一包装的工作放到这个类里面:

supports: 判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要

beforeBodyWrite: 对 response 进行具体的处理

//如果引入了swagger或knife4j的文档生成组件,这里需要仅扫描自己项目的包,否则文档无法正常生成
@RestControllerAdvice(basePackages="com.example.demo")
publicclassResponseAdviceimplementsResponseBodyAdvice{
@Override
publicbooleansupports(MethodParameterreturnType,Class>converterType){
//如果不需要进行封装的,可以添加一些校验手段,比如添加标记排除的注解
returntrue;
}


@Override
publicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,Class>selectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){
//提供一定的灵活度,如果body已经被包装了,就不进行包装
if(bodyinstanceofResult){
returnbody;
}
returnResult.success(body);
}
}

经过这样改造,既能实现对 Controller 返回的数据进行统一包装,又不需要对原有代码进行大量的改动。

参数校验

Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation。

spring validation 是对其的二次封装,常用于 SpringMVC 的参数自动校验,参数校验的代码就不需要再与业务逻辑代码进行耦合了。

①@PathVariable 和 @RequestParam 参数校验

Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参。

对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解。

如果校验失败,会抛出 MethodArgumentNotValidException 异常。

@RestController(value="prettyTestController")
@RequestMapping("/pretty")
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,这个类有两个作用(实际上可以从名字上得到一点启发)

用于解析 @RequestBody 标注的参数

处理 @ResponseBody 标注方法的返回值

解析 @RequestBoyd 标注参数的方法是 resolveArgument。

publicclassRequestResponseBodyMethodProcessorextendsAbstractMessageConverterMethodProcessor{
/**
*ThrowsMethodArgumentNotValidExceptionifvalidationfails.
*@throwsHttpMessageNotReadableExceptionif{@linkRequestBody#required()}
*is{@codetrue}andthereisnobodycontentorifthereisnosuitable
*convertertoreadthecontentwith.
*/
@Override
publicObjectresolveArgument(MethodParameterparameter,@NullableModelAndViewContainermavContainer,
NativeWebRequestwebRequest,@NullableWebDataBinderFactorybinderFactory)throwsException{

parameter=parameter.nestedIfOptional();
//把请求数据封装成标注的DTO对象
Objectarg=readWithMessageConverters(webRequest,parameter,parameter.getNestedGenericParameterType());
Stringname=Conventions.getVariableNameForParameter(parameter);

if(binderFactory!=null){
WebDataBinderbinder=binderFactory.createBinder(webRequest,arg,name);
if(arg!=null){
//执行数据校验
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){ //获取参数上的所有注解 Annotation[]annotations=parameter.getParameterAnnotations(); for(Annotationann:annotations){ //如果注解中包含了@Valid、@Validated或者是名字以Valid开头的注解就进行参数校验 Object[]validationHints=ValidationAnnotationUtils.determineValidationHints(ann); if(validationHints!=null){ //实际校验逻辑,最终会调用HibernateValidator执行真正的校验 //所以SpringValidation是对HibernateValidation的二次封装 binder.validate(validationHints); break; } } } }

②@RequestBody 参数校验

Post、Put 请求的参数推荐使用 @RequestBody 请求体参数。

对 @RequestBody 参数进行校验需要在 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;
}
}

校验原理

声明约束的方式,注解加到了参数上面,可以比较容易猜测到是使用了 AOP 对方法进行增强。

而实际上 Spring 也是通过 MethodValidationPostProcessor 动态注册 AOP 切面,然后使用 MethodValidationInterceptor 对切点方法进行织入增强。

publicclassMethodValidationPostProcessorextendsAbstractBeanFactoryAwareAdvisingPostProcessorimplementsInitializingBean{

//指定了创建切面的Bean的注解
privateClassvalidatedAnnotationType=Validated.class;

@Override
publicvoidafterPropertiesSet(){
//为所有@Validated标注的Bean创建切面
Pointcutpointcut=newAnnotationMatchingPointcut(this.validatedAnnotationType,true);
//创建Advisor进行增强
this.advisor=newDefaultPointcutAdvisor(pointcut,createMethodValidationAdvice(this.validator));
}

//创建Advice,本质就是一个方法拦截器
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{
//方法入参校验,最终还是委托给HibernateValidator来校验
//所以SpringValidation是对HibernateValidation的二次封装
result=execVal.validateParameters(
invocation.getThis(),methodToValidate,invocation.getArguments(),groups);
}
catch(IllegalArgumentExceptionex){
...
}
//校验不通过抛出ConstraintViolationException异常
if(!result.isEmpty()){
thrownewConstraintViolationException(result);
}
//Controller方法调用
ObjectreturnValue=invocation.proceed();
//下面是对返回值做校验,流程和上面大概一样
result=execVal.validateReturnValue(invocation.getThis(),methodToValidate,returnValue,groups);
if(!result.isEmpty()){
thrownewConstraintViolationException(result);
}
returnreturnValue;
}
}

③自定义校验规则

有些时候 JSR303 标准中提供的校验规则不满足复杂的业务需求,也可以自定义校验规则。

自定义校验规则需要做两件事情:

自定义注解类,定义错误信息和一些其他需要的内容

注解校验器,定义判定规则

//自定义注解类
@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{

privatebooleanrequired=false;

privatefinalPatternpattern=Pattern.compile("^1[34578][0-9]{9}$");//验证手机号

/**
*在验证开始前调用注解里的方法,从而获取到一些注解里的参数
*
*@paramconstraintAnnotationannotationinstanceforagivenconstraintdeclaration
*/
@Override
publicvoidinitialize(MobileconstraintAnnotation){
this.required=constraintAnnotation.required();
}

/**
*判断参数是否合法
*
*@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();
}
}
,>

自动校验参数真的是一项非常必要、非常有意义的工作。JSR303 提供了丰富的参数校验规则,再加上复杂业务的自定义校验规则,完全把参数校验和业务逻辑解耦开,代码更加简洁,符合单一职责原则。

自定义异常与统一拦截异常

原来的代码中可以看到有几个问题:

抛出的异常不够具体,只是简单地把错误信息放到了 Exception 中

抛出异常后,Controller 不能具体地根据异常做出反馈

虽然做了参数自动校验,但是异常返回结构和正常返回结构不一致

自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应。

而统一拦截异常的目的一个是为了可以与前面定义下来的统一包装返回结构能对应上,另一个是我们希望无论系统发生什么异常,Http 的状态码都要是 200 ,尽可能由业务来区分系统的异常。

//自定义异常
publicclassForbiddenExceptionextendsRuntimeException{
publicForbiddenException(Stringmessage){
super(message);
}
}

//自定义异常
publicclassBusinessExceptionextendsRuntimeException{
publicBusinessException(Stringmessage){
super(message);
}
}

//统一拦截异常
@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}参数校验不通过时抛出的异常处理
*/
@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}参数校验不通过时抛出的异常处理
*/
@ExceptionHandler({ConstraintViolationException.class})
publicResulthandleConstraintViolationException(ConstraintViolationExceptionex){
if(StringUtils.hasText(ex.getMessage())){
returnResult.failed(ResultEnum.VALIDATE_FAILED.getCode(),ex.getMessage());
}
returnResult.failed(ResultEnum.VALIDATE_FAILED);
}

/**
*顶级异常捕获并统一处理,当其他异常无法处理时候选择使用
*/
@ExceptionHandler({Exception.class})
publicResulthandle(Exceptionex){
returnResult.failed(ex.getMessage());
}

}

总结

做好了这一切改动后,可以发现 Controller 的代码变得非常简洁,可以很清楚地知道每一个参数、每一个 DTO 的校验规则,可以很明确地看到每一个 Controller 方法返回的是什么数据,也可以方便每一个异常应该如何进行反馈。这一套操作下来后,我们能更加专注于业务逻辑的开发,代码简介、功能完善,何乐而不为呢

,>
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • Controller
    +关注

    关注

    0

    文章

    397

    浏览量

    56995
  • 代码
    +关注

    关注

    30

    文章

    4557

    浏览量

    66835
  • spring
    +关注

    关注

    0

    文章

    333

    浏览量

    14163

原文标题:Controller层代码就该这么写,简洁又优雅!

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    如何在w_bt_firmware_controller.c中转换CYW43012 hcd文件c代码

    [],射频 TX 信号很弱 我还有其他来自模块制造商的 BT hcd 文件。 我可以知道如何在 w_bt_firmware_controller.c 中转换 CYW43012 hcd 文件 c代码 arrary brcm_patchram_buf[]
    发表于 03-01 06:51

    如何做到每天代码

    代码必须是开源的,且要放到Github上(这样强迫自己小心代码,并会考虑代码重用性及怎么创建项目前期的模块)。当然,以上这些规则是灵活的。John Resig之所以制定
    发表于 04-25 19:16

    例说FPGA连载90:多分辨率HDMI显示驱动设计之hdmi_controller.v模块代码解析

    `例说FPGA连载90:多分辨率HDMI显示驱动设计之hdmi_controller.v模块代码解析特权同学,版权所有配套例程和更多资料下载链接:http://pan.baidu.com/s
    发表于 04-12 21:45

    Sensor Controller Studio烧录程序?

    请问Sensor Controller Studio 编辑的代码是怎么烧录的?还是debug之后就已经下载成功了?如果没有,那么要怎么烧录能和CCS编辑的代码怎么结合的呢?
    发表于 06-24 07:34

    Linux专业入门-《Linux就这么学》

    《Linux就这么学》是一本面向初学者的入门书籍,注重操作实用性。
    发表于 09-26 09:10

    I2C程序代码为什么能够赋值

    先附上一段代码(摘自老师I2C部分代码)typedef struct i2c_controller { int (*init)(void); int (*master_xfer)(p_i2c_msg
    发表于 04-18 04:42

    请问这个代码怎么

    用了匿名科创的6.0版上位机 但是没买他们的产品 所以没有他们的代码现在不知道怎么代码 他们一帧数据是这么构成的 帧名称+帧头+发送设备地址+目标设备地址+功能字+数据长度+数据+和
    发表于 06-26 04:25

    请问裸机中断控制器代码的问题怎么解决?

    这段代码是中断的引脚的配置,和中断使能,不太明白的是优先级的设置,按照老大的代码解释说 ARBMODE0 要设置为0 , ARB_SEL0 也要设置为00,其他都不用管。那为什么要这么
    发表于 07-19 05:45

    请问OLED驱动代码怎么

    。肯定是我自己代码没写好,但是现在我不知道怎么写了,现在急用,哪位大神有啊求一份,谢谢了。[C] 纯文本查看 复制代码void OLED_WR_Byte(u8 dat,u8 cmd){u8 i;OLED_RS=cmd; //
    发表于 08-28 04:35

    怎么记住这么代码格式?

    我记得刚开始接触编程的时候,觉得太难了。也很好奇,代码的那些人也太厉害了吧?全是英文的,他们的英文水平一定很好吧?他们是怎么记住这么代码格式的?而且错了一个标点符号,整个程序都会有
    发表于 07-15 08:56

    蓝桥杯中模块相应代码如何

    本文不说底层原理,只说蓝桥杯中模块相应代码如何,一定要自己敲出来,测试出相应的结果。模块练熟了再去敲往年的赛题,毕竟九尺之台,起于累土main主函数部分,这个把各个模块融合在一起,可以最后再看
    发表于 01-12 07:06

    用shell脚本curl命令调用自己代码接口

    正文要求在页面查询到5000条数据,为了方便插入,用shell脚本curl命令调用自己代码接口;**脚本如下:#!/bin/basha=0while [ $a -le 10
    发表于 10-19 14:48

    UITableVew代码量的优化

    帮他们修改。然后本人见识了各种风生水起的代码。比如说在Controller中写上不计其数的代码,再比如说在TableView的协议方法中协上数百行的代码。更有甚者,每个页面只有一个
    发表于 10-12 11:47 0次下载
    UITableVew<b class='flag-5'>代码</b>量的优化

    如何把Controller代码写的更优雅?

    本篇主要要介绍的就是 controller 层的处理,一个完整的后端请求由4部分组成。
    的头像 发表于 11-01 10:09 641次阅读

    优秀的代码都是如何分层的?看了直呼NB!

    的确在这些人眼中分层只是一个形式,前辈们的代码这么写的,其他项目代码这么写的,那么我也这么跟着写。但是在真正的团队开发中每个人的习惯都不同,
    的头像 发表于 06-09 14:39 380次阅读
    优秀的<b class='flag-5'>代码</b>都是如何分层的?看了直呼NB!