我们正处于一个信息大暴发的时代,每天都能产生数以百万计的新闻资讯!
虽然有大数据推荐,但面对海量数据,通过我们的调研发现,在一个小时的时间里,您通常无法真正有效地获取您感兴趣的资讯!
头条新闻资讯订阅,旨在帮助您收集感兴趣的资讯内容,并且在第一时间通知到您。可以有效节约您获取资讯的时间,避免错过一些关键信息。
控制器层代码就应该这么写,简洁优雅!
出色的控制器层逻辑
看动画,轻松学习23种C++设计模式
download:https://www.sisuoit.com/3611.html
说起控制器,相信大家都不陌生,它可以很方便的对外提供数据接口。它的定位,在我看来,是不可或缺的配角。
不可或缺是因为无论是传统的三层架构还是现在的可乐架构,控制器层还是有一席之地的,可见其必要性。
之所以说是配角,是因为控制器层的代码一般不负责具体的逻辑业务逻辑实现,而是负责接收和响应请求。
从现状看问题
控制器的主要工作如下:
接收请求并解析参数。
调用服务来执行特定的业务代码(可能包括参数验证)
捕捉业务逻辑异常并给出反馈。
逻辑成功执行以做出响应。
//DTO
@数据
公共类TestDTO <{p>私有整数num
私有字符串类型;
}
//服务
@服务
公共类测试服务<{p>
公共双服务(TestDTO testDTO)引发异常<{p>if (testDTO.getNum() 1) <{p>结果=结果*数量;
num-= 1;
}
返回结果;
}
抛出新异常(“无法识别的算法”);
}
}
//控制器
@RestController
公共类TestController <{p>
私有TestService testService
@PostMapping("/test ")
公共双重测试(@RequestBody TestDTO testDTO) <{p>尝试<{p>double result = this . test service . service(test dto);
返回结果;
} catch(异常e) <{p>抛出新的runtime exception(e);
}
}
@自动连线
公共DTOid setTestService(TestService TestService)<{p>this.testService = testService
}
}
如果你真的按照上面列出的工作项来开发控制器代码,会有几个问题:
参数检查与业务代码耦合过多,违背了单一责任原则。
同一个异常可能在多个服务中抛出,导致代码重复。
各种异常反馈和成功响应格式不统一,界面对接不友好。
转换控制器层逻辑
统一回报结构
无论项目前后是否分离,都需要统一返回值类型,让对接接口的开发者更清楚的知道这个接口的调用是否成功(我们不能简单的判断返回值是否为null,因为有些接口就是这样设计的)。
推荐一个开源免费的Spring Boot最全教程:
https://github.com/javastacks/spring-boot-best-practice
使用状态代码和状态信息来清楚地理解接口调用:
//定义返回数据结构
公共接口IResult <{p>integer getCode();
string getMessage();
}
//常用结果的枚举
公共枚举ResultEnum实现IResult <{p>成功(2001,“接口调用成功”),
VALIDATE_FAILED(2002,“参数验证失败”),
COMMON_FAILED(2003,“接口调用失败”),
禁止(2004年《无权限访问资源》);
私有整数代码;
私有字符串消息;
//省略get、set方法和构造方法
}
//统一返回数据结构
@数据
@NoArgsConstructor
@AllArgsConstructor
公共类结果<{p>私有整数代码;
私有字符串消息;
私人测试数据;
公共静态结果成功(测试数据)<{p>返回新结果(ResultEnum。SUCCESS.getCode(),ResultEnum。SUCCESS.getMessage(),数据);
}
公共静态结果成功(字符串消息,测试数据)<{p>返回新结果(ResultEnum。SUCCESS.getCode(),message,data);
}
公共静态结果失败()<{p>返回新结果(ResultEnum。COMMON_FAILED.getCode(),ResultEnum。COMMON_FAILED.getMessage(),null);
}
公共静态结果失败(字符串消息)<{p>返回新结果(ResultEnum。COMMON_FAILED.getCode(),message,null);
}
公共静态结果失败(IResult errorResult) <{p>返回新结果(errorResult.getCode()、errorResult.getMessage()、null);
}
公共静态结果实例(整数代码、字符串消息、测试数据)<{p>结果Result = new Result();
result.setCode(代码);
result.setMessage(消息);
result.setData(数据);
返回结果;
}
}
统一结构返回后,可以在控制器中使用。但是,每个控制器都要编写这样一段最终封装的逻辑,这是一个非常重复的工作。因此,我们应继续寻找进一步处理统一回报结构的方法。
统一包装处理
Spring中提供了一个类ResponseBodyAdvice,可以帮助我们实现上述需求:
公共接口响应加载Advice <{p>布尔支持(MethodParameter returnType,Class > converter type);
@Nullable
T before body write(@ Nullable T body,MethodParameter returnType,MediaType selectedContentType,Class> selectedConverterType,ServerHttpRequest,server httpresponse response);
}
Body Advice在HttpMessageConverter执行类型转换之前截获控制器返回的内容,然后在相应的处理操作之后将结果返回给客户端。
然后你可以把统一打包的工作放到这个类中:
支持:确定是否交给beforeBodyWrite方法执行,true:yes;False:不是必需的。
BeforeBodyWrite:响应的特殊处理
//如果引入了swagger或者knife4j的文档生成组件,在这里只需要扫描自己项目的包,否则无法正常生成文档。
@ RestControllerAdvice(base packages = " com . example . demo ")
公共类ResponseAdvice实现ResponseBodyAdvice <{p>@覆盖
公共布尔支持(MethodParameter returnType,Class> converterType) <{p>//如果不需要封装,可以添加一些验证手段,比如添加标记排除的注释。
返回true
}
@覆盖
public Object beforeBodyWrite(Object body,MethodParameter returnType,MediaType selectedContentType,Class> selectedConverterType,ServerHttpRequest,ServerHttpResponse response) <{p>//提供一定程度的灵活性。如果几何体已经打包,则不会打包。
if(结果的正文实例)<{p>返回正文;
}
返回result . success(body);
}
}
经过这种转换,控制器返回的数据可以统一打包,不需要对原代码做大量的改动。
参数验证
Java API的规范JSR303定义了用于验证的标准验证API,其中一个众所周知的实现是hibernate验证。
Spring validation是SpringMVC的二次封装,经常用来自动验证Spring MVC的参数,所以参数验证的代码不需要耦合业务逻辑代码。
①@ path variable和@RequestParam的参数验证
Get请求的参数接收一般依赖于这两个注释,但是url长度有限,代码可维护。如果实体尽可能传递五个以上的参数。
@PathVariable和@RequestParam参数的验证需要参数中约束声明的批注。
如果验证失败,将引发methoalgumentnotvalidieexception异常。
@ rest controller(value = " pretty test controller ")
@RequestMapping("/pretty ")
公共类TestController <{p>
私有TestService testService
@GetMapping("/{num} ")
公共整数细节(@ path variable(" num ")@ Min(1)@ Max(20)Integer num)<{p>返回num * num
}
@GetMapping("/getByEmail ")
public test to getby account(@ request param @ not blank @ Email String Email)<{p>test dto test dto = new test dto();
testDTO.setEmail(电子邮件);
返回testDTO
}
@自动连线
public void setTestService(TestService pretty TestService)<{p>this . testservice = pretty testservice;
}
}
证实原则
在SpringMVC中,有一个类叫做RequestResponseBodyMethodProcessor,它有两个功能(其实可以从名字中得到启发)。
用于解析@RequestBody批注的参数
处理@ResponseBody批注方法的返回值
解析@RequestBoyd批注参数的方法是resolveArgument。
公共类RequestResponseBodyMethodProcessor扩展了AbstractMessageConverterMethodProcessor <{p>/**
*如果验证失败,将引发MethodArgumentNotValidException。
* @如果{@link RequestBody#required()}则抛出httpmessagenoretreadableexception
* is {@code true}并且没有正文内容,或者如果没有合适的
*用于读取内容的转换器。
*/
@覆盖
公共对象resolve argument(method parameter参数,@ Nullable ModelAndViewContainer MAV container,
NativeWebRequest webRequest,@ Nullable WebDataBinderFactory binder factory)引发异常<{p>
parameter = parameter . nestedifoptional();
//将请求数据封装到标记的DTO对象中
object arg = readWithMessageConverters(webRequest,parameter,parameter . getnestedgenericparametertype());
string name = conventions . getvariablenameforparameter(参数);
if (binderFactory!= null) <{p>WebDataBinder binder = binder factory . create binder(webRequest,arg,name);
如果(arg!= null) <{p>//执行数据验证
validateifapplable(binder,parameter);
//如果验证失败,则抛出methodgargumentnotvalidieexception异常。
//如果我们自己不捕捉,最终会被DefaultHandlereXceptionResolver捕捉并处理。
if (binder.getBindingResult()。has errors()& & isBindExceptionRequired(binder,parameter)) <{p>抛出新方法argumentnotvaliexception(parameter,binder . getbinding result());
}
}
if (mavContainer!= null) <{p>MAV container . add attribute(binding result。MODEL_KEY_PREFIX + name,binder . getbinding result());
}
}
返回adaptArgumentIfNecessary,parameter);
}
}
公共抽象类AbstractMessageConverterMethodArgumentResolver实现HandlerMethodArgumentResolver <{p>/**
*如果适用,验证绑定目标。
*默认实现检查{@code @javax.validation.Valid},
* Spring的{ @ link org . Spring framework . validation . annotation . validated },
*以及名称以“Valid”开头的自定义标注。
* @param binder要使用的数据绑定器
* @param parameter方法参数描述符
* @从4.1.5开始
* @请参阅#isBindExceptionRequired
*/
受保护的void validateifapplable(web data binder binder,MethodParameter参数)<{p>//获取参数的所有注释
annotation[]annotations = parameter . getparameter annotations();
for(Annotation ann:annotations)<{p>//如果批注包含@Valid、@Validated或名称以Valid开头的批注,请检查参数。
object[]validation hints = validationannotationutils . determinevalidationhints(ann);
if (validationHints!= null) <{p>//实际的验证逻辑最终会调用Hibernate Validator来执行真正的验证。
//所以Spring验证是Hibernate验证的二次封装。
binder . validate(validation hints);
打破;
}
}
}
}
②@RequestBody参数验证
对于Post和Put请求的参数,建议使用@RequestBody请求体参数。
要验证@RequestBody参数,需要给d to对象添加一个验证条件,然后与@Validated进行匹配,完成自动验证。
如果验证失败,将引发ConstraintViolationException异常。
//DTO
@数据
公共类TestDTO <{p>@NotBlank
私有字符串用户名;
@NotBlank
@长度(最小值= 6,最大值= 20)
私有字符串密码;
@NotNull
@电子邮件
私人字符串电子邮件;
}
//控制器
@ rest controller(value = " pretty test controller ")
@RequestMapping("/pretty ")
公共类TestController <{p>
私有TestService testService
@PostMapping("/test-validation ")
public void test validation(@ request body @ Validated test dto test dto)<{p>this . testservice . save(test dto);
}
@自动连线
public void setTestService(TestService TestService)<{p>this.testService = testService
}
}
证实原则
很容易猜到AOP是通过声明方式和给参数添加注释来增强方法的。
其实Spring也是通过MethodValidationPostProcessor动态注册AOP切面,然后用MethodValidationInterceptor编织增强切点方法。
公共类MethodValidationPostProcessor扩展abstractbeanfactoryawaredispostingpostprocessor实现InitializingBean <{p>
//指定创建切面的Bean的注释。
private Class[]groups = determineValidationGroups(invocation);
executable validator exec val = this . validator . forexecutables();
method to validate = invocation . get method();
设置结果;
尝试<{p>//方法参数验证最终委托给Hibernate验证器。
//所以Spring验证是Hibernate验证的二次封装。
result = execval . validate parameters(
invocation.getThis()、methodToValidate、invocation.getArguments()、groups);
}
catch(IllegalArgumentException ex)<{p>...
}
//如果验证失败,则引发ConstraintViolationException异常。
如果(!result.isEmpty()) <{p>抛出新的ConstraintViolationException(结果);
}
//控制器方法调用
object return value = invocation . proceed();
//下面是验证返回值,流程大致如上。
result = exec val . validatereturnvalue(invocation . getthis()、methodToValidate、ReturnValue、groups);
如果(!result.isEmpty()) <{p>抛出新的ConstraintViolationException(结果);
}
返回returnValue
}
}
③用户自定义的验证规则
有时候JSR303标准提供的验证规则不能满足复杂的业务需求,你也可以自定义验证规则。
要自定义验证规则,您需要做两件事:
自定义注释类,定义错误消息和一些其他必需的内容。
批注检查器,定义决策规则
//自定义批注类
@Target({ElementType。方法,ElementType。字段,元素类型。ANNOTATION_TYPE,ElementType。构造函数,ElementType。参数})
@保留(RetentionPolicy。运行时间)
@已记录
@ Constraint(validated by = mobile validator . class)
公共@界面移动<{p>/**
*允许空白?
*/
boolean必选()默认值为true
/**
*检查失败返回的消息。
*/
Message()默认“不是手机号码格式”;
/**
*约束所需的属性,用于分组校验和扩展,将其留空即可。
*/
Class[] groups()默认{ };
类别handleBusinessException(business exception ex)<{p>返回result . failed(ex . getmessage());
}
/**
*捕捉{@code ForbiddenException}异常
*/
@ exception handler({ forbiddenexception . class })
公共结果handleForbiddenException(ForbiddenException ex)<{p>返回Result.failed(ResultEnum。禁止);
}
/**
* {@code @RequestBody}参数验证失败时引发的异常处理。
*/
@ exception handler({ method argumentnotvalidexception . class })
公共结果handlemethodgargumentnotvaliexception(methodgargumentnotvaliexception ex)<{p>binding result binding result = ex . getbinding result();
StringBuilder SB = New StringBuilder("验证失败:");
for(field error field error:binding result . getfield errors())<{p>sb.append(fieldError.getField())。追加(":")。append(field error . getdefaultmessage())。追加(",");
}
string msg = sb . tostring();
if (StringUtils.hasText(msg)) <{p>返回Result.failed(ResultEnum。VALIDATE_FAILED.getCode(),msg);
}
返回Result.failed(ResultEnum。VALIDATE _失败);
}
/**
* {@code @PathVariable}和{@code @RequestParam}参数验证失败,并引发异常处理。
*/
@ exception handler({ constraintviolationexception . class })
公共结果handleConstraintViolationException(ConstraintViolationException ex)<{p>if(string utils . hastext(ex . getmessage())<{p>返回Result.failed(ResultEnum。VALIDATE_FAILED.getCode(),ex . getmessage());
}
返回Result.failed(ResultEnum。VALIDATE _失败);
}
/**
*顶层异常捕获和统一处理,当其他异常不能处理时,选择使用。
*/
@ exception handler({ exception . class })
公共结果句柄(异常)<{p>返回result . failed(ex . getmessage());
}
}
摘要
在做了所有这些改动后,可以发现控制器的代码变得非常简洁,你可以清楚地知道每个参数和DTO的验证规则,你可以清楚地看到每个控制器方法返回什么数据,你也可以方便地反馈每个异常。
返回搜狐,查看更多责任编辑:
以上内容为资讯信息快照,由td.fyun.cc爬虫进行采集并收录,本站未对信息做任何修改,信息内容不代表本站立场。
快照生成时间:2022-12-20 05:15:52
本站信息快照查询为非营利公共服务,如有侵权请联系我们进行删除。
信息原文地址: