引出问题

项目中参数校验作为常规编码操作是很正常的一件事,一般会想到至少两种解决方案:

1、硬编码

参数做编码校验

2、校验框架

比如JSR303 Validation

解决方法

下面我们主要就JSR303 Validation来说说在一个方法内,不同情况下同一个bean做参数校验的问题。
先放上需要校验的实体类

package com.xxx.gateway.bean;

import com.xxx.gateway.common.jsr303.VehiclePushDataGroupSequenceProvider;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.group.GroupSequenceProvider;

import javax.validation.constraints.Pattern;
import java.io.Serializable;

/**
 * Created by IntelliJ IDEA 2019.
 * User: shingmoyeung.
 * Date: 2019/9/20.
 * Time: 08:12.
 * To change this template use File Or Preferences | Settings | Editor | File and Code Templates.
 * File Description: 位置对象
 */
@Setter
@Getter
@GroupSequenceProvider(value = VehiclePushDataGroupSequenceProvider.class)
public class VehiclePushData implements Serializable {
    /**
     * 用户ID,必需
     */
    @NotBlank(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, message = "userId不可为空")
    @Pattern(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, regexp = "^[0-9a-z]{1,32}$", message = "userId长度必须介于(含)1-32位之间")
    private String userId;
    /**
     * 群组ID,必需
     */
    @NotBlank(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, message = "groupId不可为空")
    @Pattern(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, regexp = "^[0-9a-z]{1,32}$", message = "groupId长度必须介于(含)1-32位之间")
    private String groupId;
    /**
     * 群组CODE,必需
     */
    @NotBlank(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, message = "groupCode不可为空")
    @Pattern(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, regexp = "^1[0-9]{5,}$", message = "groupCode长度最小6位")
    private String groupCode;
    /**
     * 行程结束标识 0:未结束 1:结束,必需
     */
    @NotBlank(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, message = "isOver不可为空")
    @Pattern(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, regexp = "^[0-1]{1}$", message = "isOver值必须是0或1")
    private String isOver;
    /**
     * 车辆位置(逗号分隔经纬度),必需
     */
    @NotBlank(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, message = "vehiclePosition不可为空")
    @Pattern(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, regexp = "^([0-9]*.{0,1}[0-9]{1,}),([0-9]*.{0,1}[0-9]{1,})$", message = "vehiclePosition经纬度格式不正确,正确格式:123.21321,12.13213")
    private String vehiclePosition;
    /**
     * 汽车速度,必需
     */
    @NotBlank(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, message = "vehicleSpeed不可为空")
    @Pattern(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, regexp = "^[0-9]*.{0,1}[0-9]{1,}$", message = "vehicleSpeed的值格式不正确")
    private String vehicleSpeed;
    /**
     * 终端标识(1-小程序,2-车机,3-安卓手机,4-IOS手机),必需
     */
    @NotBlank(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, message = "terminalType不可为空")
    @Pattern(groups = {WechatGroup.class, AppGroup.class, CarGroup.class}, regexp = "^[1-4]{1}$", message = "terminalType的值必须在(含)1-4之间")
    private String terminalType;
    /**
     * 路况,非必需 unknown = 0 light = 1 medium = 2 heavy = 3 blocked = 4 none = 5
     */
    @NotBlank(groups = {AppGroup.class, CarGroup.class}, message = "roadCondition不可为空")
    @Pattern(groups = {AppGroup.class, CarGroup.class}, regexp = "^[0-5]{1}$", message = "roadCondition的值必须在(含)0-5之间")
    private String roadCondition;
    /**
     * 剩余里程,非必需
     */
    @NotBlank(groups = {AppGroup.class, CarGroup.class}, message = "surplusDistance不可为空")
    @Pattern(groups = {AppGroup.class, CarGroup.class}, regexp = "^[0-9]*.{0,1}[0-9]{1,}$", message = "surplusDistance的值格式不正确")
    private String surplusDistance;
    /**
     * 剩余时间,非必需
     */
    @NotBlank(groups = {AppGroup.class, CarGroup.class}, message = "surplusTime不可为空")
    @Pattern(groups = {AppGroup.class, CarGroup.class}, regexp = "^[0-9]*.{0,1}[0-9]{1,}$", message = "surplusTime的值格式不正确")
    private String surplusTime;
    /**
     * 行驶里程,非必需
     */
    @NotBlank(groups = {AppGroup.class, CarGroup.class}, message = "travDistance不可为空")
    @Pattern(groups = {AppGroup.class, CarGroup.class}, regexp = "^[0-9]*.{0,1}[0-9]{1,}$", message = "travDistance的值格式不正确")
    private String travDistance;
    /**
     * 行驶时间,非必需
     */
    @NotBlank(groups = {AppGroup.class, CarGroup.class}, message = "travTime不可为空")
    @Pattern(groups = {AppGroup.class, CarGroup.class}, regexp = "^[0-9]*.{0,1}[0-9]{1,}$", message = "travTime的值格式不正确")
    private String travTime;
    /**
     * 导航状态,非必需 1:导航中 2:未导航 3:导航中止
     */
    @NotBlank(groups = {CarGroup.class}, message = "naviStatus不可为空")
    @Pattern(groups = {CarGroup.class}, regexp = "^[1-3]{1}$", message = "naviStatus的值必须在(含)1-3之间")
    private String naviStatus;

    /**
     * 微信小程序数据组
     */
    public interface WechatGroup {
    }

    /**
     * 手机APP数据组
     */
    public interface AppGroup {
    }

    /**
     * 车机数据组
     */
    public interface CarGroup {
    }
}

针对这种情况,目前我在项目中提供两种实现方式。

1、实现Validation的DefaultGroupSequenceProvider接口,自定义分组策略

package com.xxx.gateway.common.jsr303;

import com.xxx.gateway.bean.VehiclePushData;
import com.xxx.gateway.common.Constant;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.validator.spi.group.DefaultGroupSequenceProvider;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by IntelliJ IDEA 2019.
 * User: shingmoyeung.
 * Date: 2019/10/15.
 * Time: 16:45.
 * To change this template use File Or Preferences | Settings | Editor | File and Code Templates.
 * File Description: 自定义Validation的分组GroupSequenceProvider
 */
@Slf4j
public class VehiclePushDataGroupSequenceProvider implements DefaultGroupSequenceProvider<VehiclePushData>, Constant {
    /**
     * 自定义Validation的分组GroupSequenceProvider
     *
     * @param vehiclePushData {@link VehiclePushData} 对象
     * @return
     */
    @Override
    public List<Class<?>> getValidationGroups(VehiclePushData vehiclePushData) {
        List<Class<?>> defaultGroupSequence = new ArrayList<>();
        //这一步不能省,否则Default分组都不会执行了,会抛错的
        defaultGroupSequence.add(VehiclePushData.class);
        //这块判空请务必要做
        if (vehiclePushData != null) {
            String terminalType = vehiclePushData.getTerminalType();
            log.info("terminalType为 {} 执行对应校验逻辑", terminalType);
            if (TERMINAL_TYPE_WECHAT.equals(terminalType)) {
                defaultGroupSequence.add(VehiclePushData.WechatGroup.class);
            } else if (TERMINAL_TYPE_CAR_MACHINE.equals(terminalType)) {
                defaultGroupSequence.add(VehiclePushData.CarGroup.class);
            } else {
                defaultGroupSequence.add(VehiclePushData.AppGroup.class);
            }
        }
        return defaultGroupSequence;
    }
}

然后,在通过如下代码完成校验

package com.xxx.gateway.controller;

import com.alibaba.fastjson.JSON;
import com.xxx.gateway.bean.VehiclePushData;
import com.xxx.gateway.common.Constant;
import com.xxx.gateway.common.ResponseBean;
import com.xxx.gateway.common.ResultEnum;
import com.xxx.gateway.feign.GroupFeign;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Scope;
import org.springframework.http.MediaType;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

/**
 * Created by IntelliJ IDEA 2019.
 * User: shingmoyeung.
 * Date: 2019/9/20.
 * Time: 10:04.
 * To change this template use File Or Preferences | Settings | Editor | File and Code Templates.
 * File Description: 设备发送数据
 */
@RefreshScope
@RestController
@Scope(value = "prototype")
public class MessageController implements Constant {
    private final static Logger logger = LoggerFactory.getLogger(MessageController.class);
    /**
     * {@link KafkaTemplate}
     */
    @Autowired
    private KafkaTemplate kafkaTemplate;
    /**
     * Group服务 Feign接口
     */
    @Autowired
    private GroupFeign groupFeign;
    /**
     * topic名称
     */
    @Value(value = "${gateway.producer.topic:push_data_input}")
    private String producerTopic;

    /**
     * 网关推送数据
     *
     * @param vehiclePushData 推送数据对象
     * @return
     */
    @PostMapping(value = "/sendLocationData", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    public ResponseBean sendLocationData(@Validated @RequestBody VehiclePushData vehiclePushData) {
        Map<String, Object> map = null;
        ResponseBean responseBean = null;
        try {
            if (TERMINAL_TYPE_WECHAT.equals(vehiclePushData.getTerminalType())) {
                //微信小程序点位
                responseBean = this.uploadData(vehiclePushData);
            } else {
                //非微信,即车机、手机app传入-检查群组服务,网关过滤上传的位置点数据
                responseBean = this.groupFeign.checkUpload(vehiclePushData.getGroupId(), vehiclePushData.getUserId());
                if (responseBean.getCode() == ResultEnum.SUCCESS.getCode()) {
                    map = (Map<String, Object>) responseBean.getData();
                    int ifUpload = (int) map.get("ifUpload");
                    //不使用手机上传的点且非车机点位
                    if (ifUpload == IF_UPLOAD_CAR_MACHINE && !TERMINAL_TYPE_CAR_MACHINE.equals(vehiclePushData.getTerminalType())) {
                        logger.warn("无效位置点信息");
                    } else {
                        //其他情况上传点位
                        responseBean = this.uploadData(vehiclePushData);
                    }
                } else if (responseBean.getCode() == ResultEnum.NO_RESULT.getCode()) {
                    logger.warn("无无历史记录点,首次上传点位");
                    //首次点位上传
                    responseBean = this.uploadData(vehiclePushData);
                } else {
                    logger.error("网关调用群组服务出现异常 状态码 {} 响应信息 {}", responseBean.getCode(), responseBean.getMessage());
                }
            }
        } catch (Exception e) {
            logger.error("网关推送数据出现异常 {}", e.getMessage());
            responseBean = new ResponseBean(ResultEnum.FAILURE, null);
        }
        return responseBean;
    }

    /**
     * 上传数据至kafka,等待天眼平台个群组服务消费
     *
     * @param vehiclePushData 推送数据对象
     * @return
     * @throws Exception
     */
    private ResponseBean uploadData(VehiclePushData vehiclePushData) throws Exception {
        //发送设备位置数据至kafka
        this.sendDataToKafka(this.producerTopic, vehiclePushData);
        return new ResponseBean(ResultEnum.SUCCESS, null);
    }

    /**
     * 发送数据到kafka队列中
     *
     * @param topic  topic
     * @param object 数据对象
     * @throws Exception
     */
    private void sendDataToKafka(String topic, Object object) throws Exception {
        //发送消息,topic不存在将自动创建新的topic
        ListenableFuture<SendResult<String, String>> future = this.kafkaTemplate.send(topic, JSON.toJSONString(object));
        //添加成功发送消息的回调和失败的回调
        future.addCallback(new ListenableFutureCallback<SendResult<String, String>>() {
            @Override
            public void onFailure(Throwable throwable) {
                logger.warn("Producer: The message failed to be sent {}", throwable.getMessage());
            }

            @Override
            public void onSuccess(SendResult<String, String> stringObjectSendResult) {
                logger.info("Producer->result {}", stringObjectSendResult.getProducerRecord());
            }
        });
    }
}

这里注意,一定是@Validated注解,才会走分组策略

2、工具类实现

package com.xxx.gateway.utils;

import com.xxx.gateway.bean.VehiclePushData;
import lombok.extern.slf4j.Slf4j;

import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import java.util.Set;

/**
 * Created by IntelliJ IDEA 2019.
 * User: shingmoyeung.
 * Date: 2019/10/15.
 * Time: 17:11.
 * To change this template use File Or Preferences | Settings | Editor | File and Code Templates.
 * File Description: JSR303校验工具类
 */
@Slf4j
public class ValidationUtil {
    /**
     * JSR303校验
     *
     * @param vehiclePushData {@link VehiclePushData} 对象
     * @param groups          Validation分组
     */
    public static void customValidationBean(VehiclePushData vehiclePushData, Class<?>... groups) {
        Set<ConstraintViolation<VehiclePushData>> result = Validation.buildDefaultValidatorFactory().getValidator().validate(vehiclePushData, groups);
        for (ConstraintViolation<VehiclePushData> vehiclePushDataConstraintViolation : result) {
            log.error("JSR303校验出现问题 {}, {}, {}", vehiclePushDataConstraintViolation.getPropertyPath(), vehiclePushDataConstraintViolation.getInvalidValue(), vehiclePushDataConstraintViolation.getMessage());
        }
    }
}

然后,在代码中通过如下访问

ValidationUtil.customValidationBean('这里是需要校验的实体类', '这里是实体类校验的分组’);

到这里,就实现了上述校验要求。

本文参考以下文章
分组序列@GroupSequenceProvider、@GroupSequence控制数据校验顺序,解决多字段联合逻辑校验问题【享学Spring MVC】