在基于spring框架的项目开发中,必然会遇到controller层,它可以很方便的对外提供数据接口服务,也是非常关键的出口,所以非常有必要进行规范统一,使其既简洁又优雅。
controller层的职责为负责接收和响应请求,一般不负责具体的逻辑业务的实现。controller主要工作如下:
目前controller层代码会存在的问题:
优雅写法一:统一返回结构
统一返回值类型,无论项目前后端是否分离都是非常必要的,方便对接接口的前端开发人员更加清晰地知道这个接口的调用是否成功,不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此。
统一返回结构,通过状态码就能清楚的知道接口的调用情况:
@Data
public class ResponseData {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
这样即能实现对controller返回的数据进行统一,又不需要对原有代码进行大量的改动了。
优雅写法三:参数校验
Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation。
@RestController
@RequestMapping("/test")
public class TestController {private TestService testService;@Autowiredpublic 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;}
}
@Data
public class TestDTO {@NotBlankprivate String userName;@NotBlank@Length(min = 6, max = 20)private String password;@NotNull@Emailprivate String email;
}@RestController
@RequestMapping("/test")
public class TestController {private TestService testService;@Autowiredpublic void setTestService(TestService testService) {this.testService = testService;}@PostMapping("/testValidation")public void testValidation(@RequestBody @Validated TestDTO testDTO) {this.testService.save(testDTO);}}
优雅写法四:自定义异常与统一拦截异常
原来抛出的异常会有如下问题:
自定义异常是为了后面统一拦截异常时,对业务中的异常有更加细颗粒度的区分,拦截时针对不同的异常作出不同的响应。
统一拦截异常的是为了可以与前面定义下来的统一包装返回结构能对应上,还有就是希望无论系统发生什么异常,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的校验规则,可以明确返回的结构,包括异常情况。