Spring boot2.x-第03讲:数据校验


有啥子问题欢迎各路神仙指点迷津。

1.前言

在看本篇文章之前,推荐大家先温习一下以下内容:

2.数据校验

2.1 诉求

在web开发中,针对请求参数,一般需要进行参数合法化校验,原本写法是一个一个字段判断,这种方式不通用且繁琐了,Java的JSR 303: Bean Validation规范就是解决这个问题的。


上代码……

2.2 API接口上送模型

package com.tong.hao.online.api.model.test;

import com.tong.hao.common.entity.api.TongBaseRequest;
import com.tong.hao.online.api.model.RegexpConstant;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;

import javax.validation.constraints.Min;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;

@Setter
@Getter
@EqualsAndHashCode(callSuper = false)
@NoArgsConstructor
@AllArgsConstructor
public class DemoReq extends TongBaseRequest {

    private static final long serialVersionUID = 1865606619170433347L;

    @ApiModelProperty(value = "姓名", required = true, position = 0)
//    @NotBlank(message = "name不能为空")
    // 该注解不需要写message,在/resources/ValidationMessages.properties文件中统一配置
    @NotBlank
    private String name;

    @ApiModelProperty(value = "年龄", position = 1)
    @Min(value = 1, message = "{demoReq.age}")
    private String age;

    @ApiModelProperty(value = "性别", position = 2, allowableValues = "I|U|D")
    // 使用@Pattern可以实现属性枚举值校验
    @Pattern(regexp = RegexpConstant.TEST_DEMO_ENUM)
    private String sex;
}

【注意】:该模型不用@Date注解,因为TongBaseRequest类中重写了toString()方法,如果使用@Date注解的话,也会重写toString()方法。

DemoReq模型中@Min注解的message指的是当age<1时的异常信息,@NotBlank注解的默认message为"javax.validation.constraints.NotBlank.message"(查看源码可知,此处在配置文件中覆盖异常信息),demeReq.age在/resources/ValidationMessages.properties文件中配置,文件内容如下:

javax.validation.constraints.NotBlank.message=不能为空
demoReq.age=age年龄必须大于等于1

2.3 Controller测试类

@Api(tags = {"测试控制器"})
@RestController
@Slf4j
@RequestMapping(value = "/test")
public class TestController implements ITestController {
    /**
     * <pre>
     *     接口数据校验测试,post请求
     * </pre>
     */
    @Override
    @ApiOperation(value = "数据校验测试-POST")
    @RequestMapping(method = RequestMethod.POST, value = VALID_POST)
    public String demoValidOfPost(@Valid @RequestBody DemoReq demoReq) {
        System.err.println("数据校验测试-POST");
        Map<String, Object> result = new HashMap<>();
        result.put("姓名", demoReq.getName());
        result.put("年龄", demoReq.getAge());
        return result.toString();
    }
}

【注意】

  • 方法demoValidOfPost()的入参必须要有@Valid注解
  • 方法入参必须要有@RequestBody
  • 方法入参必须只有一个对象

2.4 第一次测试

请求报文如下

{
	"tongInHead":{
		"clientNo":"123",
		"reqSystemId":"TWX",
		"reqBusinessNo":"123456789",
		"reqTranDate":"2019-09-28",
		"reqControllerUrl":"/test/valid/post"
	},
	"name":"呵呵",
	"age":"-8"
}

响应报文如下

{
    "tongOutHead": {
        "exceptionResultList": [
            {
                "retCode": "999999",
                "retMsg": "业务执行异常!"
            }
        ],
        "timestamp": "2019-09-28 20:35:09.975"
    }
}

log日志如下

org.springframework.web.bind.MethodArgumentNotValidException: Validation failed for argument [0] in public java.lang.String com.tong.hao.online.service.controller.test.TestController.demoValidOfPost(com.tong.hao.online.api.model.test.DemoReq): [Field error in object 'demoReq' on field 'age': rejected value [-8]; codes [Min.demoReq.age,Min.age,Min.java.lang.String,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [demoReq.age,age]; arguments []; default message [age],1]; default message [age年龄必须大于等于1]] 
	at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:138) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:127) ~[spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:167) ~[spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134) ~[spring-web-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:104) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:892) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:797) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1039) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942) ~[spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005) [spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]
	at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908) [spring-webmvc-5.1.9.RELEASE.jar:5.1.9.RELEASE]

【Tip】响应报文貌似不太友好啊,改进……

2.5 进阶一

从2.3的日志中可以看出报错的异常类是org.springframework.web.bind.MethodArgumentNotValidException,再根据上一篇文章 Spring boot2.x-全局异常处理 可以考虑对该异常进行全局处理。对GlobalExceptionHandler类进行修改如下,添加代码2- 处理 @RequestBody注解方式的参数校验

package com.tong.hao.online.service.handler;

import com.tong.hao.common.entity.api.TongBaseResponse;
import com.tong.hao.common.exception.BusinessException;
import com.tong.hao.common.exception.BusinessResult;
import com.tong.hao.common.exception.constants.ExceptionEnum;
import com.tong.hao.common.utils.ThBeanUtils;
import com.tong.hao.common.utils.ThStringUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * @ClassName GlobalExceptionHandler
 * @Author 友野浩二
 * @Description 全局的统一异常处理类
 *
 * <pre>
 * controller增强器:
 *      @ControllerAdvice(controller增强器) :
 *          被@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法,都会作用在被 @RequestMapping注解的方法上。
 *      @ExceptionHandler(异常处理器) :
 *          此注解的作用是当出现其定义的异常时进行处理的方法。
 *
 *      RestControllerAdvice = @ControllerAdvice + @ResponseBody,该类中所有的方法都会返回json格式的数据。
 *      如果异常要返回也页面,则就使用@ControllerAdvice。
 * </pre>
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * ExceptionHandler拦截了异常,我们可以通过该注解实现自定义异常处理。其中,@ExceptionHandler 配置的 value 指定需要拦截的异常类型。
     */
    @ExceptionHandler(value = {Exception.class})
//    @ResponseBody
    public TongBaseResponse handlerException(Exception ex) {
        log.info("com.tong.hao.online.service.handler.GlobalExceptionHandler.handlerException");
        log.info(ex.getMessage(), ex);
        // 1- 处理 业务异常处理
        if (ex instanceof BusinessException) {
            BusinessException businessException = (BusinessException) ex;
            return BusinessResult.error(businessException.getExceptionCode(), businessException.getExceptionMessage());
        }

        // 2- 处理 @RequestBody注解方式的参数校验
        if (ex instanceof MethodArgumentNotValidException) {
            MethodArgumentNotValidException methodArgumentNotValidException = (MethodArgumentNotValidException) ex;
            BindingResult bindingResult = methodArgumentNotValidException.getBindingResult();
            FieldError fieldError = bindingResult.getFieldError();
            if (!ThBeanUtils.isNull(fieldError)) {
                log.info("MethodArgumentNotValidException, {}, {}", fieldError.getField(), fieldError.getDefaultMessage());
                return BusinessResult.error(ExceptionEnum.FILED_ERROR.getCode(), getMessage(fieldError));
            }
        }
        // 其他异常处理
        return BusinessResult.error(ExceptionEnum.FAILED.getCode(), ExceptionEnum.FAILED.getMessage());
    }

    private String getMessage(FieldError fieldError) {
        String message;
        try {
            // [字段名] + 错误消息
            message = "[".concat(fieldError.getField()).concat("]").concat(ThStringUtils.nvl(fieldError.getDefaultMessage(), ""));
        } catch (Exception e) {
            message = fieldError.getDefaultMessage();
        }
        return message;
    }
}

2.6 第二次测试

请求报文不变
响应报文如下

{
    "tongOutHead": {
        "exceptionResultList": [
            {
                "retCode": "TH000",
                "retMsg": "[age]age年龄必须大于等于1"
            }
        ],
        "timestamp": "2019-09-28 20:48:27.588"
    }
}

log日志如下
堆栈信息没变,可以看到代码(62行代码)中打印的日志如下

MethodArgumentNotValidException, age, age年龄必须大于等于1

【Tip】:响应报文是友好了,but,打印的日志还是很恶心的,继续改进……

2.7 进阶二

看到这里,就需要用到切面了,这里我们在切面的前置通知中对API接口的入参进行手动校验,切面代码如下

package com.tong.hao.online.service.aspect;

import com.google.common.base.Stopwatch;
import com.tong.hao.common.entity.api.TongBaseRequest;
import com.tong.hao.common.exception.constants.ExceptionEnum;
import com.tong.hao.common.utils.BusinessUtils;
import com.tong.hao.common.utils.ThBeanUtils;
import com.tong.hao.common.utils.ValidatorUtils;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;

import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * @Author 友野浩二
 * @ClassName ValidateRequestAspect
 * @Description // 切面: 接口数据校验
 * @Date 2019/9/25 21:46
 * @Version 1.0
 *
 * <pre>
 * @Lazy 注解: 容器一般都会在启动的时候实例化所有单实例bean,如果我们想要Spring在启动的时候延迟加载bean,需要用到这个注解。
 *              value值为true 或 false,默认true(延迟加载),@Lazy(value = false)表示对象会在初始化的时候创建。
 * </pre>
 **/
@Aspect
@Component
@Lazy(value = false)
@Slf4j
public class ValidateRequestAspect {

    private static ThreadLocal<Long> startTime = new ThreadLocal<>();// 记录API接口执行的开始时间

    /*
     * @Author 友野浩二
     * @Date 2019/9/25 22:13
     * @Param []
     * @return void
     * @Description // 定义切入点:要对哪些类中的哪些方法进行增强,进行切割,指的是被增强的方法。即要切哪些东西。如包、类。
     *
     * <pre>
     *      execution: 用来匹配执行方法的连接点。
     *      1- 语法结构: execution(方法修饰符        方法返回值类型[必填] 方法所属包和类     匹配方法名(方法中的形参) 方法申明抛出异常)
     *                   execution(modifiers-pattern?ret-type-pattern declaring-type-pattern?name-pattern(param-pattern) throw-pattern?)
     *      2- 各部分都支持通配符"*"来匹配全部(可以使用..表示当前包及子包);
     *         方法的形参支持两种通配符。
     *              "*": 代表[一个]任意类型的参数;
     *              "..": 代表零个或多个任意类型的参数.
     *
     *      当前方法的切入点是:com.tong.hao.online.service.controller包及子包下的所有类的 接收一个参数的方法
     *      【注意】:开发规范规定controller类的所有方法只能有一个参数。
     * </pre>
     **/
    @Pointcut("execution(* com.tong.hao.online.service.controller..*.*(*))")
//    @Pointcut("execution(public String com.tong.hao.online.service.controller.test.TestController.demoValidOfPost(..))")
    private void validator() {
        log.info("<======== entry ValidateRequestAspect validator ========>");
    }

    /**
     * @Author 友野浩二
     * @Description // 前置通知:在目标方法执行前调用,在该方法中完成API接口入参模型的数据校验(类似javax.validation.constraints.NotBlank这种校验)
     * @Date 2019/9/25 23:05
     **/
    @Before("validator()")
    public void doBeforeController(JoinPoint joinPoint) {
        log.info("<======== entry ValidateRequestAspect 前置通知 ========>");
        // 在前置通知中进行接口入参数据校验
        Stopwatch stopwatch = Stopwatch.createStarted();// 创建自动start的计时器
        Object[] args = joinPoint.getArgs();// 获取传入目标方法的参数对象
        TongBaseRequest tongBaseRequest = (TongBaseRequest) args[0];// 因为validator()方法的切入点配置的目标方法只有一个参数,所以只获取参数对象的第一个
        log.info("request msg: {}", tongBaseRequest);// 打印请求报文
        Validator validator = ValidatorUtils.getValidator();
        // 对入参对象作校验,如果校验成功,则返回的 ConstraintViolation 类型的集合为空,否则该集合为空;
        // 集合中的每一个元素(ConstraintViolation 类型)对应一个违反的约束。
        Set<ConstraintViolation<TongBaseRequest>> constraintViolationSet = validator.validate(tongBaseRequest);
        if (!ThBeanUtils.isNull(constraintViolationSet)) {
            ConstraintViolation<TongBaseRequest> violationInfo = constraintViolationSet.iterator().next();

            String message;
            try {
                ApiModelProperty apiModelProperty = violationInfo.getRootBeanClass()
                        .getDeclaredField(violationInfo.getPropertyPath().toString())
                        .getAnnotation(ApiModelProperty.class);
                message = "[".concat(violationInfo.getPropertyPath().toString())
                        .concat(", ").concat(apiModelProperty.value()).concat("]")
                        .concat(violationInfo.getMessage());// 属性名 + ApiModelProperty.value(属性说明) + 异常信息
            } catch (NoSuchFieldException e) {
                message = violationInfo.getMessage();
            }
            throw BusinessUtils.createBusinessException(ExceptionEnum.FILED_ERROR.getCode(), message);// API接口数据校验的错误码统一为TH000
        }
        log.info("<======== exit ValidateRequestAspect 前置通知, elapsed time is [{}] ms ========>",
                stopwatch.elapsed(TimeUnit.MILLISECONDS));
        startTime.set(System.currentTimeMillis());// 设置当前时间戳
    }

    /**
     * @Author 友野浩二
     * @Description // 后置通知:在目标方法执行后调用,若目标方法出现异常,则不执行。
     * @Date 2019/9/25 23:10
     *
     * <pre>
     *     @AfterReturning 注解属性说明:
     *          pointcut/value:这两个属性的作用是一样的,它们都属于指定切入点对应的表达式。当指定了
     *                          pointcut属性值后,value属性值会被覆盖;
     *          returning:该属性指定一个形参名,用于表示Advice方法中可定义与此同名的形参,该形参可用
     *                      于访问目标方法的返回值。除此之外,在Advice方法中定义该形参(代表目标方法的
     *                      返回值)时指定的类型,会限制目标方法必须返回指定类型的值或没有返回值。若声
     *                      明为Object,则意味着对目标方法的返回值不加限制。
     * </pre>
     **/
    @AfterReturning(pointcut = "validator()", returning = "responseValue")
    public void doAfterReturningController(Object responseValue) {
        log.info("<======== entry ValidateRequestAspect 后置通知: after returning ========>");
        log.info("response msg: {}", responseValue);
        Long costTime = System.currentTimeMillis() - startTime.get();
        log.info("<======== 当前API接口耗费时间:{} ms", costTime);
    }

    /**
     * @Author 友野浩二
     * @Description // 后置/最终通知:无论目标方法在执行过程中是否出现异常,都会在它之后调用
     * @Date 2019/9/25 23:13
     **/
    @After("validator()")
    public void doAfterController() {
        log.info("<======== entry ValidateRequestAspect 后置通知: finally returning ========>");
    }
}

【注意】

  • 2.5进阶一中全局异常处理类中关于MethodArgumentNotValidException的代码可以注释掉;
  • Controller测试类中的方法入参@Valid注解必须去掉,否则切面切不到的;
  • 启动类上需要加入注解@EnableAspectJAutoProxy(proxyTargetClass = true)开启增强代理(AOP切面)

ValidatorUtils工具类

package com.tong.hao.common.utils;

import org.hibernate.validator.HibernateValidator;

import javax.validation.Validation;
import javax.validation.Validator;

/**
 * @ClassName ValidatorUtils
 * @Author 友野浩二
 * @Description 接口controller数据校验工具类
 * @Version 1.0
 */
public class ValidatorUtils {
    // 使用HibernateValidator
    private static Validator validator = Validation.byProvider(HibernateValidator.class).configure()
            .failFast(true)
            .buildValidatorFactory()
            .getValidator();

    public static Validator getValidator() {
        return validator;
    }
}

2.8 第三次测试

请求报文不变
响应报文不变
log日志如下

com.tong.hao.common.exception.BusinessException: [{"exceptionCode" : "TH000", "exceptionMessage" : "[age, 年龄]age年龄必须大于等于1"}]
	at com.tong.hao.common.utils.BusinessUtils.createBusinessException(BusinessUtils.java:22) ~[classes/:na]
	at com.tong.hao.online.service.aspect.ValidateRequestAspect.doBeforeController(ValidateRequestAspect.java:96) ~[classes/:na]

可以看到异常信息已经是自定义异常了。

3. 参考资料

SpringBoot | 第八章:统一异常、数据校验处理

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
您好!对于使用Spring Boot 2.x和MyBatis集成MySQL,并实现微信授权登录的问题,我可以给您一些指导。 首先,您可以按照以下步骤进行操作: 1. 配置MySQL数据库:在`application.properties`或`application.yml`文件中设置MySQL数据库的连接信息,包括数据库URL、用户名和密码等。 2. 引入依赖:在您的项目的`pom.xml`文件中添加Spring Boot、MyBatis和MySQL的相关依赖。例如: ```xml <dependencies> <!-- Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <!-- MySQL --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <!-- 其他依赖... --> </dependencies> ``` 3. 创建实体类和Mapper:创建与数据库表对应的实体类,并使用MyBatis的注解或XML配置文件来定义Mapper接口和SQL语句。 4. 配置MyBatis:在`application.properties`或`application.yml`文件中配置MyBatis相关的属性,如Mapper接口的扫描路径、XML配置文件的位置等。 5. 编写业务逻辑:根据您的需求,编写相应的业务逻辑代码,包括微信授权登录的逻辑处理。 6. 实现微信授权登录:使用微信开放平台提供的API,获取用户的授权信息,并将相关信息保存到数据库中。您可以使用第三方开源库(如unapp)来简化微信授权登录的过程。 需要注意的是,以上只是一个大致的步骤,具体实现还需根据您的项目需求进行调整。同时,为了保证代码的安全性和可靠性,建议您进行适当的异常处理、参数校验等。 希望以上内容对您有所帮助!如果您有任何疑问,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值