引出问题
项目中参数校验作为常规编码操作是很正常的一件事,一般会想到至少两种解决方案:
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】