在日常得接口开发中,为了防止非法参数对业务造成影响,经常需要对接口得参数进行校验,例如注册用户得时候需要检验用户名密码是否为空,手机号邮箱格式是否正确。靠代码对接口参数一个个校验得话就太过于繁琐,代码可读性极差。
由此spring框架提供了一个参数校验得框架validator框架
validator框架就是为了解决开发人员在开发代码得时候仅用少于代码完成参数校验得工作,提升开发效率,validator框架是专门用来进行校验接口参数得,例如:
- 必填项校验
- email格式校验
- 手机号格式校验
- 参数值长度校验
- 正则表达式校验
接下来直接开始使用validator框架
org.springframework.boot
spring-boot-starter-validation
package org.init.model.business;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
@NotBlank(message = "账号不能为空")
@Length(min = 5,max = 16,message = "账号长度为5-16位")
private String userName;
@NotBlank(message = "密码不能为空")
@Length(min = 4,max = 16,message = "密码长度为4-16位")
private String passWord;
@NotBlank(message = "账号不能为空")
@Email(message = "邮箱格式出现错误")
private String email;
}
常见的约束注解如下:
注解 |
功能 |
@AssertFalse |
可以为null,如果不为null的话必须为false |
@AssertTrue |
可以为null,如果不为null的话必须为true |
@DecimalMax |
设置不能超过最大值 |
@DecimalMin |
设置不能超过最小值 |
@Digits |
设置必须是数字且数字整数的位数和小数的位数必须在指定范围内 |
@Future |
日期必须在当前日期的未来 |
@Past |
日期必须在当前日期的过去 |
@Max |
最大不得超过此最大值 |
@Min |
最大不得小于此最小值 |
@NotNull |
不能为null,可以是空 |
@Null |
必须为null |
@Pattern |
必须满足指定的正则表达式 |
@Size |
集合、数组、map等的size()值必须在指定范围内 |
|
必须是email格式 |
@Length |
长度必须在指定范围内 |
@NotBlank |
字符串不能为null,字符串trim()后也不能等于"" |
@NotEmpty |
不能为null,集合、数组、map等size()不能为0;字符串trim()后可以等于"" |
@Range |
值必须在指定范围内 |
@URL |
必须是一个URL |
package org.init.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.init.annotation.NotControllerResponseAdvice;
import org.init.common.BaseResponse;
import org.init.common.ResultUtils;
import org.init.model.business.User;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Api(tags = "测试控制器")
@RestController
@RequestMapping("/hello")
public class HelloController {
@ApiOperation(value = "测试参数控制器")
@PostMapping("/testParam")
public BaseResponse testParam(@Validated @RequestBody User user){
return ResultUtils.success(user);
}
}
这里我们定义了一个方法testParam函数使用@RequestBody注解,用于接受前端发送得json数据@Validated注解开启校验
我们故意输入一个错误得邮箱号
点击发送请求返回
报错400是因为 由于邮箱格式出现错误被拦截了
这时我们打开后台查看日志发现后台日志中确实出现了邮箱格式出现错误得提示
前端小姐姐如果是这原样展示给用户看,估计代码上午提交得下午产品经理就得找过来找前端小姐姐one by one,显然这样返回给前端肯定是不行滴,于是就要咱们站出来跟前端小姐姐说没问题我来修复包在我身上,没办法谁让咱们是呢!
首先先分析一下问题message跑哪里去了,为什么只有在服务端日志才成查看到,并没有包含在响应体中呢?
也是查看了spring boot 版本日志在知道,2.5.x起,默认的异常信息中的message属性被移除了。
如果仍然希望异常响应时显示详细的提示信息,则需要增加如下配置:server: error: include-message: always
有了上述配置,message果然就回来了
既然我们可以通过配置在异常响应体中增加 message ,那么还有什么其他可配置的信息么?
基于 Spring Boot 2.7.8 ,将异常响应信息做如下梳理:
属性名 |
属性说明 |
固定 / 可配置 |
配置项及默认值 |
timestamp |
异常发生时的时间 |
固定 |
|
status |
http 响应状态码 |
固定 |
|
error |
与状态码对应的异常原因 |
固定 |
|
path |
异常发生时的请求路径 |
固定 |
|
message |
异常的提示信息 |
可配置 |
server.error.include-message = never |
exception |
异常的类名 |
可配置 |
server.error.include-exception = false |
trace |
异常跟踪堆栈信息 |
可配置 |
server.error.include-stacktrace = never |
errors |
BindingResult 中的错误信息 |
可配置 |
server.error.include-binding-errors = never |
除了 server.error.include-exception 是布尔值外,其它三项配置可选值如下:
可选值 |
配置说明 |
never |
异常响应体中不会包含对应的信息 |
always |
异常响应体中包含对应的信息 |
on-param |
当请求参数中包含相应的参数名(message、trace、errors),且参数值不为 false 时,异常响应体中将包含对应的信息 |
等等message虽然回来了,但是并没有看到咱们想看到得提示,不行得赶紧处理毕竟牛已经吹出去了
package org.init.exception;
import org.init.common.BaseResponse;
import org.init.common.ErrorCode;
import org.init.common.ResultUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.sql.SQLException;
import java.util.List;
/**
* spring boot 捕获全局异常
*/
@RestControllerAdvice
public class GlobExceptionHandler {
@ExceptionHandler(Exception.class)
public BaseResponse exception(Exception e) {
Throwable sourceCause = this.getSourceCause(e, e.getCause());
if (null != sourceCause) {
if ((sourceCause instanceof SQLException)) {
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "执行后台sql语句出现异常!");
}
if ((sourceCause instanceof NullPointerException)) {
return ResultUtils.error(ErrorCode.SYSTEM_ERROR, "空指针异常!");
}
}
return ResultUtils.error(ErrorCode.SYSTEM_ERROR);
}
//直接监听BindException类就行MethodArgumentNotValidException都是继承得BindException类
@ExceptionHandler(BindException.class)
public BaseResponse methodArgumentNotValidException(BindException ex){
StringBuffer sb = new StringBuffer();
//遍历所有的错误结果
for (ObjectError allError : ex.getAllErrors()) {
sb.append(allError.getDefaultMessage()+"/n");
}
return ResultUtils.error(ErrorCode.NULL_ERROR,sb.toString(),"参数不能为空!");
}
/**
* 取得当前异常最初引发来源方
*/
private Throwable getSourceCause(Exception e, Throwable ex) {
if ((e instanceof SQLException)) {
return e;
}
if ((e instanceof NullPointerException)) {
return e;
}
if (ex == null)
return null;
if (ex.getCause() == null)
return ex;
else
return getSourceCause(e, ex.getCause());
}
}
增加了全局异常处理这个时候重新发起再看,果然变成了前端小姐姐想要得大功告成,可以向前端小姐姐交差了!
到这这就是完整得validator框架使用教程了,以为结束了!!!!
然而并没有,现在由于一个项目是多人协作开发得,要是没有标准开发文档显然是不行滴,要是有文档有些小糊涂蛋还是忘记按照规范做返回值咋办呢,毕竟我也是小糊涂蛋中的一员为了弥补有时写错得风险,好!我决定心里暗暗立下falg 如果写错一次返回值给前端小姐姐增加麻烦就,我就饿一天一天不吃饭,人是铁饭是钢,总不吃饭也不行,毕竟身体是革命的本钱,总不能饿着自己吧,为了我能吃上饭,也为了给前端小姐姐留下好印象,我决定把代码给优化一下!!!
往下看!
ResponseBodyAdvice 快速使用
在实际项目中,我们经常需要在请求前后进行一些操作,比如:参数解密/返回结果加密、返回值封装,打印请求参数和返回结果的日志等。这些与业务无关的东西,我们不希望写在controller方法中,造成代码重复可读性变差。这里,我们经常使用@ControllerAdvice和RequestBodyAdvice、ResponseBodyAdvice来对请求前后进行处理(本质上就是AOP),来实现日志记录每一个请求的参数和返回结果。
package org.init.common;
import java.io.Serializable;
/**
* 通用返回处理对象
*/
public class BaseResponse implements Serializable {
private int code;
private T data;
private String message;
private String description;
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public BaseResponse(int code, T data, String message, String description) {
this.code = code;
this.data = data;
this.message = message;
this.description = description;
}
public BaseResponse(int code, T data) {
this(code, data, "", "");
}
public BaseResponse (int code, T data,String message){
this(code,data,message,"");
}
public BaseResponse(ErrorCode errorCode){
this(errorCode.getCode(), null, errorCode.getMessage(), errorCode.getMessage());
}
}
统一状态码
package org.init.common;
/**
* 错误码
*/
public enum ErrorCode {
SUCCESS(0, "ok", ""),
PARAM_ERROR(4000, "参数错误", ""),
NULL_ERROR(4001, "请求参数为空", ""),
FORBIDDEN(4002, "无权限", ""),
SYSTEM_ERROR(5000, "系统内部异常", "");
private final int code;
private final String message;
private final String description;
ErrorCode(int code, String message, String description) {
this.code = code;
this.message = message;
this.description = description;
}
public int getCode() {
return this.code;
}
public String getMessage() {
return this.message;
}
public String getDescription() {
return this.description;
}
}
package org.init.common;
/**
* 返回工具类
*/
public class ResultUtils {
/**
* 成功
*
* @param data
* @param
* @return
*/
public static BaseResponse success(T data) {
return new BaseResponse<>(0, data, "ok");
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode) {
return new BaseResponse<>(errorCode);
}
/**
* 失败
*
* @param code
* @param message
* @param description
* @return
*/
public static BaseResponse error(int code, String message, String description) {
return new BaseResponse(code, null, message, description);
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode, String message, String description) {
return new BaseResponse(errorCode.getCode(), null, message, description);
}
/**
* 失败
*
* @param errorCode
* @return
*/
public static BaseResponse error(ErrorCode errorCode, String description) {
return new BaseResponse(errorCode.getCode(), errorCode.getMessage(), description);
}
}
1、使用方式:自定义类实现 ResponseBodyAdvice 接口,然后在类上标记 @RestControllerAdvice 注解即可自动识别并进行功能增强。
2、下面以对返回数据封装统一格式为例进行演示(注意仅对是有@ResponseBody 注解的控制器方法进行拦截,@RestController 相当于是类中的所有方法上都加了 @ResponseBody)。
3、注意如果控制层目标方法往外抛出了异常,则不再进入 ResponseBodyAdvice(需要使用@ExceptionHandler(value = Exception.class))
package org.init.config;
import org.init.annotation.NotControllerResponseAdvice;
import org.init.common.BaseResponse;
import org.init.common.ResultUtils;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* spring boot 统一返回值
*/
@RestControllerAdvice
public class ControllerResponseAdvice implements ResponseBodyAdvice
package org.init.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.init.annotation.NotControllerResponseAdvice;
import org.init.common.BaseResponse;
import org.init.common.ResultUtils;
import org.init.model.business.User;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@Api(tags = "测试控制器")
@RestController
@RequestMapping("/hello")
public class HelloController {
@ApiOperation(value = "测试参数控制器")
@PostMapping("/testParam1")
public User testParam1(@Validated @RequestBody User user){
return user;
}
}
我们ControllerResponseAdvice类给注释掉看一下效果
如果是这样看 还是吃不上饭!
我们加上ControllerResponseAdvice再看一下效果
哇哦,这就是我们想要的!
思考一个问题,这样增加了ControllerResponseAdvice类确实是比较方便,再也不用担心返回值得问题,随之而来又出现了新的问题,假如前端小姐姐就是要我返回一个字符串怎么办,就不能说自己不行,没办法谁让前端是小姐姐呢,干吧!
这个自定义注解是标记在controller 中得方法上的
package org.init.annotation;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {
}
修改这个方法supports,判断如果返回类型不是咱们得通用返回对象BaseResponse并且方法上没有标注着NotControllerResponseAdvice注解,那么就返回值进行自动包装,如果增加着NotControllerResponseAdvice注解那么就不进行包装,直接返回,完美解决前端小姐姐提出得各种要求
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return !returnType.getParameterType().isAssignableFrom(BaseResponse.class) && !returnType.hasMethodAnnotation(NotControllerResponseAdvice.class);
}
至此,本次要给大家分享就全部结束了,同时也欢迎业界大佬进行指正!