企业实战之Spring项目《hibernate validator+Assert参数校验》

前言

在企业开发过程中,我们比较烦的也就是参数校验这一环节了,但是这一步又是不能省略掉的,我看过很多的企业开发者,他们对自己的接口参数校验都是很马虎的,以为校参这一步放在前端,后端校验就可以稍微省略很多了,其实是很错误的,我们打个比方,你的接口可能会被前端很多平台去调用,例如:ios、android、pc、web端, 如果某一端校验有检验遗漏的,就很可能导致后端接口因为参数传递的不合法导致500错误,这其实并不能把问题归结给前端,所有接口返回的500错误,都应该是后端系统的错误,所以我们一定要想办法解决这种问题。

正题

我们在企业开发中时如何校验参数的呢?接下来就给大家推荐一种校验参数的方式:hibernate validator + org.springframework.util.Assert 两种方式结合。
为什么是使用两种方式结合校验的呢?这里我们首先要明确的是,你的参数校验想要提示的人群是谁,对于api我们会分内部api和外部api,对应的接口使用者也是不一样的,内部api(也就是我们在项目中命名XxxSerivce类下的方法)使用者是后端开发团队的人,而外部api(也就是我们在项目中命名XxxController类下方法)更多的是给前端团队的开发人员使用的。

hibernate validator的使用

首先让我们看下需要用到的jar包:

这里写图片描述

validation-api-1.0.0.Final.jar是JDK的接口,是一套校验参数的统一规范;
hibernate-validator-5.3.5.Final.jar是对上述接口的实现。

如果你的项目使用的是spring boot开发的话,spring boot会默认引入相关的hibernate校验包,如果你只是使用spring mvc可以参考这篇文章的配置。

接下来我们以用户添加这个业务功能来说下,如何更优美的进行参数校验:

用户的PO类的写法:

package com.zhuma.demo.model.po;

import java.io.Serializable;
import java.util.Date;

import javax.validation.constraints.Pattern;

import org.hibernate.validator.constraints.Length;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.Range;

import com.zhuma.demo.annotation.EnumValueAnn;
import com.zhuma.demo.validator.CreateGroup;

/**
 * @desc 用户PO
 *
 * @author zhumaer
 * @since 6/15/2017 2:48 PM
 */
public class User implements Serializable {

	private static final long serialVersionUID = 2594274431751408585L;

	/**
	 * 用户ID
	 */
	private Long id;

	/**
	 * 登录密码
	 */
	@NotBlank
	@Pattern(regexp = "^[a-zA-Z][a-zA-Z0-9_-]{5,19}$", groups = CreateGroup.class, message = "{custom.pwd.invalid}")
	private String pwd;

	/**
	 * 昵称
	 */
	@NotBlank
	@Length(min = 1, max = 64, groups = CreateGroup.class)
	private String nickname;

	/**
	 * 头像
	 */
	@Length(min = 0, max = 256, groups = CreateGroup.class)
	private String img;

	/**
	 * 电话
	 */
	@Pattern(regexp = "^1[3-9]\\d{9}$", message = "{custom.phone.invalid}", groups = CreateGroup.class)
	private String phone;

	/**
	 * 性别 {@link} 0 男 1 女
	 */
	@Range(min = 0, max = 1, groups = CreateGroup.class)
	private Integer gender;

	/**
	 * 最新的登录时间
	 */
	private Date latestLoginTime;

	/**
	 * 最新的登录IP
	 */
	private String latestLoginIp;

	/**
	 * 状态 {@link StatusEnum}
	 */
	@EnumValueAnn(enumClass = StatusEnum.class, enumMethod = "isValidCode", groups = CreateGroup.class)
	private Integer status;

	/**
	 * 创建时间
	 */
	private Date createTime;

	/**
	 * 更新时间
	 */
	private Date updateTime;

	/**
	 * 账号状态枚举
	 */
	public enum StatusEnum {
		NORMAL(0, "正常"),
		SUSPENDED(1, "停用"),
		DELETED(2, "已删除");

		private Integer code;
		private String desc;

		StatusEnum(Integer code, String desc) {
			this.code = code;
			this.desc = desc;
		}

		public Integer getCode() {
			return code;
		}

		public String getDesc() {
			return desc;
		}

		public static boolean isValidCode(Integer code) {

			if (code == null) {
				return false;
			}

			for (StatusEnum status : StatusEnum.values()) {
				if (status.getCode().equals(code)) {
					return true;
				}
			}
			return false;
		}

	}

	//省略getter、setter方法(这里你可以引入lombok来简化类的getter、setter)。

}

说明

  • @Length(min = 0, max = 64) 在校验字符串参数类型的数据长度时,可能你会和我有一样的纠结,这个64和数据库字段 varchar(64)保持一致,还是要根据数据库编码(比如utf8mb4,一个字符占4个字节)去计算一下呢,这里说明下,在mysql5.0版本之后,字段varchar(64)就是代表着该字段最多能存储64个字符,你不用去考虑编码换算的问题了,所以我们校验是只要根据数据库字段定义的长度保持一致就可以啦O(∩_∩)O。

  • @EnumValueAnn(enumClass = StatusEnum.class, enumMethod = “isValidCode”, groups = CreateGroup.class) ,这里使用的是一个自定义的校验注解(如果想了解请跳转hibernate validator自定义注解实战之《枚举值校验》),功能就是校验某一个参数的值是否在枚举的定义值内,这个枚举值的校验功能相信在你的系统也会特别的常用的。

  • @NotNull @NotEmpty @NotBlank 三者之间你可能会分的不是特别清楚,这里给出个说明:
    1.@NotNull:不能为null,但可以为空, (""," “,” “)都是可以的
    2.@NotEmpty:不能为null,而且长度必须大于0,(” “,” ")是可以的,需要标记在String类型的字段上
    3.@NotBlank:不能为null,而且调用trim()后,长度必须大于0,(“test”)是可以的,只能作用在String类型的字段上。

  • 校验组的使用是很有必要的(指明校验组:groups = CreateGroup.class),这里我们定义了一个创建时用的校验组,如果你的一个校验对象上有多种校验参数的方式(比如:添加和更新的时候,参数校验逻辑就不一样了),那么你就要考虑使用校验组了。

  • 错误提示信息是放在ValidationMessages.properties里面的,默认的注解都是有它的默认提示信息的,如果你对默认注解的提示信息不满意就可以在ValidationMessages.properties这个文件里去修改或自定义你自己的提示信息啦,该文件你可以直接放到src/main/resources目录下就可以起作用了,如果你想更改目录或涉及到多语言切换,因为篇幅有限这里就不讲那么多配置相关的事情啦,就请自行百度下吧。

下面我们看下UserController的写法:

package com.zhuma.demo.web.user;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.zhuma.demo.model.po.User;
import com.zhuma.demo.service.UserService;
import com.zhuma.demo.validator.CreateGroup;

/**
 * @desc 用户管理控制器
 * 
 * @author zhumaer
 * @since 6/20/2017 16:37 PM
 */
@RestController
@RequestMapping("/users")
public class UserController {

	@Autowired
	private UserService userService;

	@PostMapping
	@ResponseStatus(HttpStatus.CREATED)
	public User addUser(@Validated(CreateGroup.class) @RequestBody User user) {
		return userService.insertAndReturn(user);
	}

}

说明

  • @Validated注解就是开启我们校验功能的标识啦,@Validated(CreateGroup.class)这样写也就是指明是根据CreateGroup组下的注解去校验,支持同时指定多个组的校验。
  • 如果校验逻辑不通过时,就会抛出MethodArgumentNotValidException这个异常,异常携带有一个BindingResult对象这里面封装了出错的详细信息,企业开发中我们通常会把这个异常统一的处理下,也就是通过@ControllerAdvice注解建立统一异常处理类,统一处理异常,为了能够更好的理解代码的调用链路,下面我们会给出一个异常处理类的代码简单版本,后面我们也会有专门的文章去说明下该如何统一处理系统的异常。
  • 如果不想像上面所提到的使用统一异常处理机制的话,你也可以在controller方法中,这样写: public User addUser(@Validated(CreateGroup.class) @RequestBody User user, BindingResult result),去接收检验出错时的详细信息,然后自己去做处理。

统一异常处理器(去掉多余功能的版本):

package com.zhuma.demo.handler;

import java.util.List;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.google.common.collect.Lists;
import com.zhuma.demo.comm.handler.BaseAggregationLayerGlobalExceptionHandler;
import com.zhuma.demo.comm.result.ParameterInvalidItem;
import com.zhuma.demo.comm.result.Result;
import com.zhuma.demo.enums.ResultCode;
import com.zhuma.demo.exception.BusinessException;

/**
 * @desc 统一异常处理器
 * 
 * @author zhumaer
 * @since 8/31/2017 3:00 PM
 */
@RestController
@ControllerAdvice
public class GlobalExceptionHandler extends BaseAggregationLayerGlobalExceptionHandler {

	private static final Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionHandler.class);

	@ResponseStatus(HttpStatus.BAD_REQUEST)
	@ExceptionHandler(MethodArgumentNotValidException.class)
	public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
		LOGGER.info("handleMethodArgumentNotValidException start, uri:{}, caused by: ", request.getRequestURI(), e);
		List<ParameterInvalidItem> parameterInvalidItemList = Lists.newArrayList();

		List<FieldError> fieldErrorList = e.getBindingResult().getFieldErrors();
		for (FieldError fieldError : fieldErrorList) {
			ParameterInvalidItem parameterInvalidItem = new ParameterInvalidItem();
			parameterInvalidItem.setFieldName(fieldError.getField());
			parameterInvalidItem.setMessage(fieldError.getDefaultMessage());
			parameterInvalidItemList.add(parameterInvalidItem);
		}

		return Result.failure(ResultCode.PARAM_IS_INVALID, parameterInvalidItemList);
	}
}

说明

  • 上面的你可能不太清楚接口的返回结果到底是怎么定义出来的,如果你想了解可以看下这篇文章《接口返回参数》

最后我们使用PostMan调用接口看下最终我们想要的返回结果吧:

这里写图片描述

Assert的使用

上面介绍的是对外接口我们的参数校验方式,下面我们说下,对于我们写的Xxxservice我们是使用org.springframework.util.Assert
这个工具类做检验的,我们看下他都有哪些方法:

这里写图片描述

我们接着上面的例子以保存用户的UserService为例:

package com.zhuma.demo.service.impl;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.Assert;

import com.zhuma.demo.comm.mapper.BaseMapper;
import com.zhuma.demo.comm.service.impl.BaseServiceImpl;
import com.zhuma.demo.model.po.User;
import com.zhuma.demo.service.UserService;

public class UserServiceImpl extends BaseServiceImpl<User> implements UserService {

	@Autowired
	private BaseMapper<User> baseMapper;

	@Override
	public User insertAndReturn(User user) {
		Assert.notNull(user, "user is not null.");
		Assert.isNull(user.getId(), "user' id must be null");

		this.insert(user);

		return baseMapper.selectByPrimaryKey(user.getId());
	}

}

我们看下它的源码:

	/**
	 * Assert that an object is not {@code null}.
	 * <pre class="code">Assert.notNull(clazz, "The class must not be null");</pre>
	 * @param object the object to check
	 * @param message the exception message to use if the assertion fails
	 * @throws IllegalArgumentException if the object is {@code null}
	 */
	public static void notNull(Object object, String message) {
		if (object == null) {
			throw new IllegalArgumentException(message);
		}
	}

说明

  • 你可以看到这个工具校验类写法其实很简单,只是简单的封装了一下,底层抛出IllegalArgumentException无效参数异常,对于这个异常我们的统一异常处理器是不会处理的,当遇到IllegalArgumentException这个异常时会被认为是500系统错误异常反馈给前端开发人员,为什么要这样做而不是处理掉呢?这是因为报出这个错误,其实就是系统代码内部出现了问题才会导致的(因为XxxService调用者都是我们内部开发人员),所以这个异常的解决是应该后端开发人员解决的,也是系统不应该出现的,后端开发人员调用其他人的service时应该让自己的代码更合理,满足调用方法参数的合法性。

  • 这里说明一下校验参数的规则,如果方法是public公共的,那么必须要校验方法参数的合法性,如果不去校验,出现的一些未知错误,责任就是写这个方法的开发人员的,加上了校验,如果程序抛出IllegalArgumentException参数无效异常时, 责任就在这个接口的调用者身上了,private私有方法我们通常不做参数校验。

  • 我经常看到一些开发人员,甚至是比较有经验的开发,校验方法参数时会这样写:

	@Override
	public User insertAndReturn(User user) {
		if (user == null) {
			return null;
		}

		this.insert(user);

		return baseMapper.selectByPrimaryKey(user.getId());
	}

这样写法其实是很不好的,也是很不负责的表现,为什么这么说呢,因为接口调用者在他的代码中可能已经出现了他没有预料到的错误,导致了user=null,但是你的逻辑却没有及时的通知给他这种错误的出现。

最后

若上述文字有描述不清楚,或者你有哪里不太懂的可以评论留言或者关注我的公众号,公众号里也会不定期的发送一些关于企业实战相关的文章哈!

源码github地址:https://github.com/zhumaer/zhuma
QQ群号:629446754(欢迎加群)

欢迎关注我们的公众号或加群,等你哦!

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值