关注我的公众号:【编程朝花夕拾】,可获取首发内容。
01 引言
你的Controller(控制层)还在通过if-else
校验参数?还在通过try-catch
捕捉异常么?还在因为参数校验不通过,给客户端响应错误信息么......
我们看看优雅的控制层是怎么写的?
02 传统控制层的写法
2.1 参数校验
@RequestMapping("/foo")
public JsonResult login(String cellphone,String name) {
String cellphone = C2BLoginUtil.getLoginCellphone();
if (StringUtils.isBlank(cellphone)) {
return new JsonResult(false,"手机号不能为空!");
}
if (StringUtils.isBlank(name)) {
return new JsonResult(false,"姓名不能为空!");
}
// ......
return new JsonResult(true);
}
在控制层内实现参数的校验,如果参数较多,就会出现大量的校验,代码就会显的很臃肿。如果多个方法复用参数,每个方法都要去写一遍,造成代码冗余。
2.2 异常的捕捉
@RequestMapping("/foo")
public JsonResult login(String cellphone,String name) {
try {
// ...
} catch (Exception e) {
log.error("服务调用异常", e);
return new JsonResult(false, "服务调用异常");
}
// ......
return new JsonResult(true);
}
每一个方法都需要异常捕获,这也是代码冗余的体现。
03 优雅的写法
统一参数校验, 统一异常捕获。整合控制才能重复的逻辑,减少代码冗余。
3.1 统一参数处理
假如有一个Book
类需要校验,我们需要将参数的校验,统一处理。然后服务端只需要一个注解就可轻松解决,还可以复用。
@Data
public class Book implements Serializable {
private static final long serialVersionUID = -4746928436252252305L;
/**
* ID
*/
private Integer bookId;
/**
* 名称
*/
@Length(min = 5, max = 20, message = "书名的长度必须在5~20之间")
private String bookName;
/**
* 定价
*/
@Min(value = 15, message = "书的价格不能低于15元")
@Max(value = 100, message = "书的价格不能超过100元")
private BigDecimal bookPrice;
/**
* 发布日期
*/
@NotNull(message = "publishDate 不能为空")
private Date publishDate;
/**
* 编写完成日期
*/
@NotNull(message = "finishDateTime 不能为空")
private LocalDateTime finishDateTime;
/**
* script
*/
private String script;
}
服务端的处理
只需要一个@Valid
注解就可完美的解决参数校验的问题。
@RequestMapping("/testFormData")
public String testFormData(@Valid Book book){
System.out.println(book);
return "success";
}
是不是少了好多代码量。
3.2 统一异常捕获
将所有的异常放在一起处理,控制层就不需要处理了,如果需要只需要抛出异常即可。
@RestControllerAdvice
public class GlobalExceptionHandler
public static String LOG_TEMPLATE = "请求路径[url]={},[%s] 异常信息:";
public static String REQUEST_PARAMS_EX_TEMPLATE = "参数[%s]:%s";
public static String REQUEST_PARAMS_MISS_TEMPLATE = "请求参数缺失,缺失的参数:%s";
@Autowired(required = false)
private GlobalExceptionResponseResolver globalExceptionResponseResolver;
/**
* 校验参数绑定异常
*
* @author ws
* @date 2024/1/29 11:13
* @param e
*/
@ExceptionHandler(BindException.class)
public Object bindExceptionHandler(HttpServletRequest request, BindException e) {
log.error(String.format(LOG_TEMPLATE, "bindExceptionHandler"), request.getRequestURL(), e);
return doGenerateResult(e, getErrorMsgDefault(e.getBindingResult().getFieldErrors(), e.getMessage()));
}
/**
* 方法参数校验异常处理
*
* @author ws
* @date 2024/1/29 13:25
* @param e
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public Object methodArgumentNotValidExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException e) {
log.error(String.format(LOG_TEMPLATE, "methodArgumentNotValidExceptionHandler"), request.getRequestURL(), e);
return doGenerateResult(e, getErrorMsgDefault(e.getBindingResult().getFieldErrors(), e.getMessage()));
}
/**
* 参数校验配合@Requestparam的异常
*
* @author ws
* @date 2024/3/7 16:29
*/
@ExceptionHandler(ConstraintViolationException.class)
public Object constraintViolationExceptionExceptionHandler(HttpServletRequest request, ConstraintViolationException e) {
log.error(String.format(LOG_TEMPLATE, "constraintViolationExceptionExceptionHandler"), request.getRequestURL(), e);
return doGenerateResult(e, getConstraintViolationExceptionMsg(e));
}
/**
* 请求参数缺失异常
*
* @author ws
* @date 2024/1/29 13:33
* @param e
*/
@ExceptionHandler(MissingServletRequestParameterException.class)
public Object missingServletRequestParameterExceptionHandler(HttpServletRequest request, MissingServletRequestParameterException e) {
log.error(String.format(LOG_TEMPLATE, "missingServletRequestParameterExceptionHandler"), request.getRequestURL(), e);
return doGenerateResult(e, String.format(REQUEST_PARAMS_MISS_TEMPLATE, e.getParameterName()));
}
/**
* 重复的请求拦截异常
*
* @author ws
* @date 2024/1/30 16:51
* @param request
* @param e
*/
@ExceptionHandler(WebConfigException.class)
public Object requestLimitingInterceptorExceptionHandler(HttpServletRequest request, WebConfigException e) {
log.error(String.format(LOG_TEMPLATE, "requestLimitingInterceptorExceptionHandler"), request.getRequestURL(), e);
return doGenerateResult(e, e.getMessage());
}
/**
* 兜底的异常处理
*
* @author ws
* @date 2024/1/29 13:35
* @param e
*/
@ExceptionHandler(Exception.class)
public Object exceptionHandler(HttpServletRequest request, Exception e) {
log.error(String.format(LOG_TEMPLATE, "exceptionHandler"), request.getRequestURL(), e);
return doGenerateResult(e, e.getMessage());
}
/**
* 处理ConstraintViolationException的参数
*
* @author ws
* @date 2024/3/7 16:34
*/
private String getConstraintViolationExceptionMsg(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
if (CollectionUtils.isEmpty(violations)) {
return e.getMessage();
}
return violations.stream().filter(Objects::nonNull)
.map(cv -> String.format(REQUEST_PARAMS_EX_TEMPLATE, cv.getPropertyPath(), cv.getMessage()))
.collect(Collectors.joining("\n"));
}
/**
* 处理公用的参数校验异常的信息
*
* @author ws
* @date 2024/1/31 10:45
*/
private String getErrorMsgDefault(List<FieldError> fieldErrors, String detaultMsg) {
String errorMsg = null;
if (!CollectionUtils.isEmpty(fieldErrors)) {
List<String> msgs = new ArrayList<>(fieldErrors.size());
fieldErrors.forEach(error -> msgs.add(showErrorMsg(error)));
errorMsg = String.join("\n", msgs);
}
return Optional.ofNullable(errorMsg).orElse(detaultMsg);
}
private String showErrorMsg(FieldError fieldError) {
return String.format(REQUEST_PARAMS_EX_TEMPLATE, fieldError.getField(), fieldError.getDefaultMessage());
}
/**
* 处理通用结果
*
* @author ws
* @date 2024/1/31 10:43
*/
private Object doGenerateResult(Exception e, String errorMsg) {
disposeExceptionTrace(errorMsg);
if (globalExceptionResponseResolver != null) {
globalExceptionResponseResolver.pushExceptionNotice(e, errorMsg);
return globalExceptionResponseResolver.resolveExceptionResponse(e, errorMsg);
}
return JsonResult.ofFail(errorMsg);
}
}
服务端的处理
理论上服务端已经不需要处理异常了。但是如果需要校验方法内部的异常,只需要抛出异常即可。
@RequestMapping("/testEx")
public String testEx() {
Assert.notNull(obj, "obj 不能为空");
return "success";
}
这里如果obj
如果为空,就会抛异常,异常会被统一处理。
3.3 统一Web的日期处理
Form
表单的如果传递日期格式,需要控制层处理。如果日期的格式都不相同,处理起来就会麻烦。我们可以使用统一处理日期格式,兼容所有的场景。
@ControllerAdvice
public class GlobalWebHandler {
/**
* 处理web的data数据,这里主要处理form表单的日期
*
* @author ws
* @date 2024/1/29 14:13
* @param binder
*/
@InitBinder
public void globalWebDataBinder(WebDataBinder binder){
DateFormatter dateFormatter = new DateFormatter(DateFormatConstant.STANDARD_DATE_TIME);
dateFormatter.setFallbackPatterns(DateFormatConstant.FORMAT_PATTERNS);
binder.addCustomFormatter(dateFormatter, Date.class);
//兼容jdk8 LocalDateTime
binder.addCustomFormatter(new LocalDateTimeFormatter(), LocalDateTime.class);
}
}
该方法兼容常用的日志格式以及LocalDateTime
, 只要传递对应的字符串,就可以正常解析。支持的格式:
04 开源项目
控制层的优雅写法,技术文章中多次被各个大佬讲过,但是都只是教你如何处理,却没有现成的工具封装。为了能够更好的使用优雅的写法,我自己从总结了常用的类型、以及以及处理方案并开源,欢迎大家使用。
只要引入对应的依赖,就可体验这种写法,不需要繁琐的配置,开箱即用。
GitHub地址:github.com/simonking-w…
Gitee地址: gitee.com/simonkingws…
该项目不仅仅处理了通用的参数,还增加常用的拦截器、重复提交、链路追踪、在线运维等方法: