Controller 主要的工作有以下几项:
接收请求并解析参数
调用 Service 执行具体的业务代码(可能包含参数校验)
捕获业务逻辑异常做出反馈
业务逻辑执行成功做出响应
不应该存在以下几种:
参数校验过多地耦合了业务代码,违背单一职责原则
可能在多个业务中都抛出同一个异常,导致代码重复
各种异常反馈和成功响应格式不统一,接口对接不友好
改造 Controller 层逻辑
统一返回结构
统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此)。
使用一个状态码、状态信息就能清楚地了解接口调用情况:
//定义返回数据结构
public interface IResult {
Integer getCode();
String getMessage();
}
//常用结果的枚举
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
private Integer code;
private String message;
//省略get、set方法和构造方法
}
//统一返回数据结构
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
}
public static <T> Result<T> success(String message, T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
}
public static Result<?> failed() {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
}
public static Result<?> failed(String message) {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
}
public static Result<?> failed(IResult errorResult) {
return new Result<>(errorResult.getCode(), errorResult.getMessage(), null);
}
public static <T> Result<T> instance(Integer code, String message, T data) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
return result;
}
}
参数校验
Java API 的规范 JSR303 定义了校验的标准 validation-api ,其中一个比较出名的实现是 hibernate validation。
spring validation 是对其的二次封装,常用于 SpringMVC 的参数自动校验,参数校验的代码就不需要再与业务逻辑代码进行耦合了。
①@PathVariable 和 @RequestParam 参数校验
Get 请求的参数接收一般依赖这两个注解,但是处于 url 有长度限制和代码的可维护性,超过 5 个参数尽量用实体来传参。
对 @PathVariable 和 @RequestParam 参数进行校验需要在入参声明约束的注解。
如果校验失败,会抛出 MethodArgumentNotValidException 异常。
二者参数的使用区别如下:
@RequestParam 和 @PathVariable 都是 Spring MVC 用于获取请求参数的注解,它们的使用方法和场景有一些不同。
@RequestParam
和 @PathVariable
都是 Spring MVC 用于获取请求参数的注解,它们的使用方法和场景有一些不同。
@RequestParam
用于获取请求参数,必须放在方法参数上。它可以指定参数的名称和默认值,并且可以设置参数的必填性。示例代码如下:
@GetMapping("/users")
public List<User> getUsers(@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "10") int size) {
// 方法体
}
@PathVariable
用于获取 URL 路径上的参数值。它可以将路径参数直接绑定到方法的参数上。示例代码如下:
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// 方法体
}
@RequestParam
和 @PathVariable
的使用场景不同。如果需要从请求参数中获取参数值,使用 @RequestParam
;如果需要从 URL 路径中获取参数值,则使用 @PathVariable
。
例如,我们需要查询某个用户的信息,那么用户的 id 可以放在 URL 路径中,所以 @PathVariable
是最好的选择。
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// 方法体
}
而如果我们需要根据条件查询用户列表,则可以使用 @RequestParam
。
@GetMapping("/users")
public List<User> getUsers(@RequestParam(required = false, defaultValue = "0") int page,
@RequestParam(required = false, defaultValue = "10") int size) {
// 方法体
}
@RequestParam
是 Spring 框架中用于获取请求参数值的注解。可以使用如下方式进行使用:
- 在方法参数中使用
@RequestParam
,指定参数名和默认值
@RequestMapping("/user")
public String getUser(@RequestParam("id") int userId, @RequestParam(value="name", defaultValue="张三") String userName) {
// 方法体
return "user";
}
@RequestParam
的参数name
可以省略
@RequestMapping("/user")
public String getUser(@RequestParam int id, @RequestParam(value="name", defaultValue="张三") String userName) {
// 方法体
return "user";
}
@RequestParam
可以设置参数的必填性
@RequestMapping("/user")
public String getUser(@RequestParam(required=true) int id) {
// 方法体
return "user";
}
@RequestParam
还可以设置参数的正则表达式限制
@RequestMapping("/user")
public String getUser(@RequestParam(name="email", required=true) @Pattern(regexp=".+@.+\\..+", message="邮箱格式不正确") String email) {
// 方法体
return "user";
}
@RequestParam
还可以设置请求参数为数组
@RequestMapping(value="/students", method=RequestMethod.GET)
public String getStudents(@RequestParam String[] name) {
// 方法体
return "students";
}
自定义校验规则
有些时候 JSR303 标准中提供的校验规则不满足复杂的业务需求,也可以自定义校验规则。
自定义校验规则需要做两件事情:
自定义注解类,定义错误信息和一些其他需要的内容
注解校验器,定义判定规则
//自定义注解类
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
/**
* 是否允许为空
*/
boolean required() default true;
/**
* 校验不通过返回的提示信息
*/
String message() default "不是一个手机号码格式";
/**
* Constraint要求的属性,用于分组校验和扩展,留空就好
*/
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
//注解校验器
public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
private boolean required = false;
private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$"); // 验证手机号
/**
* 在验证开始前调用注解里的方法,从而获取到一些注解里的参数
*
* @param constraintAnnotation annotation instance for a given constraint declaration
*/
@Override
public void initialize(Mobile constraintAnnotation) {
this.required = constraintAnnotation.required();
}
/**
* 判断参数是否合法
*
* @param value object to validate
* @param context context in which the constraint is evaluated
*/
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if (this.required) {
// 验证
return isMobile(value);
}
if (StringUtils.hasText(value)) {
// 验证
return isMobile(value);
}
return true;
}
private boolean isMobile(final CharSequence str) {
Matcher m = pattern.matcher(str);
return m.matches();
}
}
自动校验参数真的是一项非常必要、非常有意义的工作。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());
}
}
[附上一篇springboot统一处理的文章:]
一、用户登录权限效验(通过拦截器去实现)
二、统一异常处理
三、统一数据返回格式
本文参考:
https://mp.weixin.qq.com/s/m97tr7Ag-dMgg5RFVyb7Mg