在基于spring框架的项目开发中,必然会遇到controller层,它可以很方便的对外提供数据接口服务,也是非常关键的出口,所以非常有必要进行规范统一,使其既简洁又优雅。
controller层的职责为负责接收和响应请求,一般不负责具体的逻辑业务的实现。controller主要工作如下:
- 接收请求并解析参数;
- 调用service层执行具体的业务逻辑(可能包含参数校验);
- 捕获业务异常做出反馈;
- 业务逻辑执行成功做出响应;
目前controller层代码会存在的问题:
- 参数校验过多地耦合了业务代码,违背了单一职责原则;
- 可能在多个业务逻辑中抛出同一个异常,导致代码重复;
- 各种异常反馈和成功响应格式不统一,接口对接不友好;
优雅写法一:统一返回结构
统一返回值类型,无论项目前后端是否分离都是非常必要的,方便对接接口的前端开发人员更加清晰地知道这个接口的调用是否成功,不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此。
统一返回结构,通过状态码就能清楚的知道接口的调用情况:
@Data
public class ResponseData<T> {
private Boolean status = true;
private int code = 200;
private String message;
private T data;
public static ResponseData ok(Object data) {
return new ResponseData(data);
}
public static ResponseData ok(Object data,String message) {
return new ResponseData(data,message);
}
public static ResponseData fail(String message,int code) {
ResponseData responseData= new ResponseData();
responseData.setCode(code);
responseData.setMessage(message);
responseData.setStatus(false);
responseData.setData(null);
return responseData;
}
public ResponseData() {
super();
}
public ResponseData(T data) {
super();
this.data = data;
}
public ResponseData(T data,String message) {
super();
this.data = data;
this.message=message;
}
}
@AllArgsConstructor
@Data
public enum ResponseCode {
SYS_FAIL(1, "操作失败"),
SYS_SUCESS(200, "操作成功"),
SYSTEM_ERROR_CODE_403(403, "权限不足"),
SYSTEM_ERROR_CODE_404(404, "未找到请求资源"),
;
private int code;
private String msg;
}
统一返回结构后,就可以在controller中使用了,但是每个controller都这么写,都是很重复的工作,所以还可以继续想办法处理统一返回结构。
优雅写法二:统一包装处理
Spring 中提供了一个类 ResponseBodyAdvice ,能帮助我们实现上述需求:
ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。这样就可以把统一包装处理的工作放到这个类里面,其中supports判断是否要交给beforeBodyWrite 方法执行,true为需要,false为不需要,beforeBodyWrite 是对response的具体处理。
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
// 如果不需要进行封装的,可以添加一些校验手段,比如添加标记排除的注解
return true;
}
@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);
}
}
这样即能实现对controller返回的数据进行统一,又不需要对原有代码进行大量的改动了。
优雅写法三:参数校验
Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation。
- @PathVariable 和 @RequestParam 参数校验:get请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参;
对 @PathVariable 和 @RequestParam 参数进行校验需要在入参处声明约束的注解,如果校验失败,会抛出 MethodArgumentNotValidException 异常。
@RestController
@RequestMapping("/test")
public class TestController {
private TestService testService;
@Autowired
public void setTestService(TestService prettyTestService) {
this.testService = prettyTestService;
}
@GetMapping("/{num}")
public Integer num(@PathVariable("num") @Min(1) @Max(20) Integer num) {
return num * num;
}
@GetMapping("/email")
public String email(@RequestParam @NotBlank @Email String email) {
return email;
}
}
- @RequestBody 参数校验:post和put 请求的参数推荐使用 @RequestBody 请求体参数;
对 @RequestBody 参数进行校验需要在 DTO 对象中加入校验条件后,再搭配 @Validated 即可完成自动校验。如果校验失败,会抛出 ConstraintViolationException 异常。
@Data
public class TestDTO {
@NotBlank
private String userName;
@NotBlank
@Length(min = 6, max = 20)
private String password;
@NotNull
@Email
private String email;
}
@RestController
@RequestMapping("/test")
public class TestController {
private TestService testService;
@Autowired
public void setTestService(TestService testService) {
this.testService = testService;
}
@PostMapping("/testValidation")
public void testValidation(@RequestBody @Validated TestDTO testDTO) {
this.testService.save(testDTO);
}
}
- 自定义校验规则:有些时候 JSR303 标准中提供的校验规则不满足复杂的业务需求,也可以自定义校验规则;
优雅写法四:自定义异常与统一拦截异常
原来抛出的异常会有如下问题:
- 抛出的异常不够具体,只是简单地把错误信息放到了 Exception 中;
- 抛出异常后,Controller 不能具体地根据异常做出反馈;
- 虽然做了参数自动校验,但是异常返回结构和正常返回结构不一致;
自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应。
统一拦截异常的是为了可以与前面定义下来的统一包装返回结构能对应上,还有就是希望无论系统发生什么异常,Http 的状态码都要是 200 ,尽可能由业务来区分系统的异常。
//自定义异常
public class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) {
super(message);
}
}
//自定义异常
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
//统一拦截异常
@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {
/**
* 捕获 {@code BusinessException} 异常
*/
@ExceptionHandler({BusinessException.class})
public Result<?> handleBusinessException(BusinessException ex) {
return Result.failed(ex.getMessage());
}
/**
* 捕获 {@code ForbiddenException} 异常
*/
@ExceptionHandler({ForbiddenException.class})
public Result<?> handleForbiddenException(ForbiddenException ex) {
return Result.failed(ResultEnum.FORBIDDEN);
}
/**
* {@code @RequestBody} 参数校验不通过时抛出的异常处理
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
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})
public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
if (StringUtils.hasText(ex.getMessage())) {
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
}
return Result.failed(ResultEnum.VALIDATE_FAILED);
}
/**
* 顶级异常捕获并统一处理,当其他异常无法处理时候选择使用
*/
@ExceptionHandler({Exception.class})
public Result<?> handle(Exception ex) {
return Result.failed(ex.getMessage());
}
}
通过上述写法,可以发现 Controller 的代码变得非常简洁优雅,可以清楚知道每个参数、每个DTO的校验规则,可以明确返回的结构,包括异常情况。