异常处理
在上一篇文章中,我们找到了RRExceptionHandler
/**
* 异常处理器
*
* @author chenshun
* @email sunlightcs@gmail.com
* @date 2016年10月27日 下午10:16:19
*/
@RestControllerAdvice
public class RRExceptionHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
/**
* 处理自定义异常
*/
@ExceptionHandler(RRException.class)
public R handleRRException(RRException e){
R r = new R();
r.put("code", e.getCode());
r.put("msg", e.getMessage());
return r;
}
@ExceptionHandler(NoHandlerFoundException.class)
public R handlerNoFoundException(Exception e) {
logger.error(e.getMessage(), e);
return R.error(404, "路径不存在,请检查路径是否正确");
}
@ExceptionHandler(DuplicateKeyException.class)
public R handleDuplicateKeyException(DuplicateKeyException e){
logger.error(e.getMessage(), e);
return R.error("数据库中已存在该记录");
}
@ExceptionHandler(AuthorizationException.class)
public R handleAuthorizationException(AuthorizationException e){
logger.error(e.getMessage(), e);
return R.error("没有权限,请联系管理员授权");
}
@ExceptionHandler(Exception.class)
public R handleException(Exception e){
logger.error(e.getMessage(), e);
return R.error();
}
}
该类用@RestControllerAdvice
修饰,这个注解修饰的类,里面的方法作用于@ResponseBody
修饰的方法返回结果。从handleRRException(RRException e)
方法看得出,系统中的业务逻辑异常都是直接往上抛,最后由RRExceptionHandler
处理并且返回对应的提示信息。
查看R
类可以发现,该项目 RESTful 接口返回结果都是一个统一格式的 map 转成的 json
/**
* 返回数据
*
* @author chenshun
* @email sunlightcs@gmail.com
* @date 2016年10月27日 下午9:59:27
*/
public class R extends HashMap<String, Object> {
private static final long serialVersionUID = 1L;
public R() {
put("code", 0);
put("msg", "success");
}
public static R error() {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, "未知异常,请联系管理员");
}
public static R error(String msg) {
return error(HttpStatus.SC_INTERNAL_SERVER_ERROR, msg);
}
public static R error(int code, String msg) {
R r = new R();
r.put("code", code);
r.put("msg", msg);
return r;
}
public static R ok(String msg) {
R r = new R();
r.put("msg", msg);
return r;
}
public static R ok(Map<String, Object> map) {
R r = new R();
r.putAll(map);
return r;
}
public static R ok() {
return new R();
}
public R put(String key, Object value) {
super.put(key, value);
return this;
}
}
校验
项目中使用的是 Hibernate-validator 做输入的校验
以登录表单为例
LoginForm.java
/**
* 登录表单
*
* @author Mark sunlightcs@gmail.com
* @since 3.1.0 2018-01-25
*/
@ApiModel(value = "登录表单")
public class LoginForm {
@ApiModelProperty(value = "手机号")
@NotBlank(message="手机号不能为空")
private String mobile;
@ApiModelProperty(value = "密码")
@NotBlank(message="密码不能为空")
private String password;
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
AppLoginController.java
/**
* APP登录授权
*
* @author chenshun
* @email sunlightcs@gmail.com
* @date 2017-03-23 15:31
*/
@RestController
@RequestMapping("/app")
@Api("APP登录接口")
public class AppLoginController {
@Autowired
private UserService userService;
@Autowired
private JwtUtils jwtUtils;
/**
* 登录
*/
@PostMapping("login")
@ApiOperation("登录")
public R login(@RequestBody LoginForm form){
//表单校验
ValidatorUtils.validateEntity(form);
//用户登录
long userId = userService.login(form);
//生成token
String token = jwtUtils.generateToken(userId);
Map<String, Object> map = new HashMap<>();
map.put("token", token);
map.put("expire", jwtUtils.getExpire());
return R.ok(map);
}
}
ValidatorUtils.java
/**
* hibernate-validator校验工具类
*
* 参考文档:http://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/
*
* @author chenshun
* @email sunlightcs@gmail.com
* @date 2017-03-15 10:50
*/
public class ValidatorUtils {
private static Validator validator;
static {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
/**
* 校验对象
* @param object 待校验对象
* @param groups 待校验的组
* @throws RRException 校验不通过,则报RRException异常
*/
public static void validateEntity(Object object, Class<?>... groups)
throws RRException {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
StringBuilder msg = new StringBuilder();
for(ConstraintViolation<Object> constraint: constraintViolations){
msg.append(constraint.getMessage()).append("<br>");
}
throw new RRException(msg.toString());
}
}
}
参照 hibernate-validator文档 ,得知,在AppLoginController
的 login
方法中,调用ValidatorUtils.validateEntity(form);
通过 LoginForm
声明的属性对应的@NotBlank
注解,来校验输入是否符合约束。若不符合,根据 validator 返回的message拼接成异常信息,然后抛出 RRException
,提示对应信息,这就连接上异常处理功能了。
另
异常处理和校验,如果只是看怎么使用,为什么要这么用的话是挺简单的。项目中使用@RestControllerAdvice
作为异常处理的方式,并且用 hibernate-validator 作为校验工具,可以减少大量的输入校验和异常捕获的代码(当然有些时候还是需要自己在代码中捕获不希望抛出的异常的)。
但是只看怎么用实在是太没有意思了。因此我们来试着读一下 validator 的源码~
首先我们找到 Validator
的实现类 ValidatorImpl
,找到validate(T object, Class<?>... groups)
方法
@Override
public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) {
Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() );
sanityCheckGroups( groups );
ValidationContext<T> validationContext = getValidationContextBuilder().forValidate( object );
if ( !validationContext.getRootBeanMetaData().hasConstraints() ) {
return Collections.emptySet();
}
ValidationOrder validationOrder = determineGroupValidationOrder( groups );
ValueContext<?, Object> valueContext = ValueContext.getLocalExecutionContext(
validatorScopedContext.getParameterNameProvider(),
object,
validationContext.getRootBeanMetaData(),
PathImpl.createRootPath()
);
return validateInContext( validationContext, valueContext, validationOrder );
}
根据官网文档指出,groups 不传的话应该是对所有属性进行校验,否则只对对应 groups 的属性进行校验。那我们就来看看它是怎么做到的.
在这个方法中主要做了四件事:
- 根据 object 参数获得
ValidationContext<T>
- 根据 groups 参数获得
ValidationOrder
- 根据 object, validationContext 等获得
ValueContext<?, Object>
- 调用
validateInContext( validationContext, valueContext, validationOrder );
这个方法的主干就完成了,接下来一个个方法看看里面都做了什么
根据 object 参数获得 ValidationContext<T>
查看ValidationContext<T>
源码,类的注释上有这么一段话
/**
* Context object keeping track of all required data for a validation call.
*
* We use this object to collect all failing constraints, but also to have access to resources like
* constraint validator factory, message interpolator, traversable resolver, etc.
*/
也就说,这个类是用来做 validation call 的时候传入的,并且会把调用时产生的所有需要的数据都追踪。这个类是通过ValidationContextBuilder.forValidate(T rootBean)
创建的。看看都是怎么创建的吧。
public <T> ValidationContext<T> forValidate(T rootBean) {
@SuppressWarnings("unchecked")
Class<T> rootBeanClass = (Class<T>) rootBean.getClass();
return new ValidationContext<>(
ValidationOperation.BEAN_VALIDATION,
constraintValidatorManager,
constraintValidatorFactory,
validatorScopedContext,
traversableResolver,
constraintValidatorInitializationContext,
rootBean,
rootBeanClass,
beanMetaDataManager.getBeanMetaData( rootBeanClass ),
null, //executable
null, //executable parameters
null, //executable return value
null //executable metadata
);
}
已知传入的参数有待校验的 bean,这个 bean 所属的 class,还有一堆杂七杂八的参数,先放着。目前已知这个类是用来存放所有校验时的数据的就行了。
根据 groups 参数获得 ValidationOrder
跟踪 determineGroupValidationOrder( groups )
会找到getValidationOrder(Collection<Class<?>> groups)
/**
* Generates a order of groups and sequences for the specified validation groups.
*
* @param groups the groups specified at the validation call
*
* @return an instance of {@code ValidationOrder} defining the order in which validation has to occur
*/
public ValidationOrder getValidationOrder(Collection<Class<?>> groups) {
if ( groups == null || groups.size() == 0 ) {
throw LOG.getAtLeastOneGroupHasToBeSpecifiedException();
}
// HV-621 - if we deal with the Default group we return the default ValidationOrder. No need to
// process Default as other groups which saves several reflection calls (HF)
if ( groups.size() == 1 && groups.contains( Default.class ) ) {
return ValidationOrder.DEFAULT_GROUP;
}
for ( Class<?> clazz : groups ) {
if ( !clazz.isInterface() ) {
throw LOG.getGroupHasToBeAnInterfaceException( clazz );
}
}
DefaultValidationOrder validationOrder = new DefaultValidationOrder();
for ( Class<?> clazz : groups ) {
if ( Default.class.equals( clazz ) ) { // HV-621
validationOrder.insertGroup( Group.DEFAULT_GROUP );
}
else if ( isGroupSequence( clazz ) ) {
insertSequence( clazz, clazz.getAnnotation( GroupSequence.class ).value(), true, validationOrder );
}
else {
Group group = new Group( clazz );
validationOrder.insertGroup( group );
insertInheritedGroups( clazz, validationOrder );
}
}
return validationOrder;
}
然后我们看看默认的 ValidationOrder
class DefaultGroupValidationOrder implements ValidationOrder {
private final List<Group> defaultGroups;
private DefaultGroupValidationOrder() {
defaultGroups = Collections.singletonList( Group.DEFAULT_GROUP );
}
@Override
public Iterator<Group> getGroupIterator() {
return defaultGroups.iterator();
}
@Override
public Iterator<Sequence> getSequenceIterator() {
return Collections.<Sequence>emptyIterator();
}
@Override
public void assertDefaultGroupSequenceIsExpandable(List<Class<?>> defaultGroupSequence) throws GroupDefinitionException {
}
}
可以看出ValidationOrder
是用来存放校验的顺序的。里面可以取得一个校验迭代器,估计后面校验的时候是根据这个迭代器来比较属性的 group 的,这其实看类名就可以猜到了。
根据 object, validationContext 等获得 ValueContext<?, Object>
我们看看ValueContext<?, Object>
是个什么东西
/**
* An instance of this class is used to collect all the relevant information for validating a single class, property or
* method invocation.
*/
根据类注释可知这个类是用来存放待校验的 property 和 method invocation 的。也就是说这个类是校验时用来遍历的。
那么这三个类是做什么的都知道了,我们看看校验逻辑
调用validateInContext( validationContext, valueContext, validationOrder );
/**
* Validates the given object using the available context information.
* @param validationContext the global validation context
* @param valueContext the current validation context
* @param validationOrder Contains the information which and in which order groups have to be executed
*
* @param <T> The root bean type
*
* @return Set of constraint violations or the empty set if there were no violations.
*/
private <T, U> Set<ConstraintViolation<T>> validateInContext(ValidationContext<T> validationContext, ValueContext<U, Object> valueContext,
ValidationOrder validationOrder) {
if ( valueContext.getCurrentBean() == null ) {
return Collections.emptySet();
}
BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
if ( beanMetaData.defaultGroupSequenceIsRedefined() ) {
validationOrder.assertDefaultGroupSequenceIsExpandable( beanMetaData.getDefaultGroupSequence( valueContext.getCurrentBean() ) );
}
// process first single groups. For these we can optimise object traversal by first running all validations on the current bean
// before traversing the object.
Iterator<Group> groupIterator = validationOrder.getGroupIterator();
while ( groupIterator.hasNext() ) {
Group group = groupIterator.next();
valueContext.setCurrentGroup( group.getDefiningClass() );
validateConstraintsForCurrentGroup( validationContext, valueContext );
if ( shouldFailFast( validationContext ) ) {
return validationContext.getFailingConstraints();
}
}
groupIterator = validationOrder.getGroupIterator();
while ( groupIterator.hasNext() ) {
Group group = groupIterator.next();
valueContext.setCurrentGroup( group.getDefiningClass() );
validateCascadedConstraints( validationContext, valueContext );
if ( shouldFailFast( validationContext ) ) {
return validationContext.getFailingConstraints();
}
}
// now we process sequences. For sequences I have to traverse the object graph since I have to stop processing when an error occurs.
Iterator<Sequence> sequenceIterator = validationOrder.getSequenceIterator();
while ( sequenceIterator.hasNext() ) {
Sequence sequence = sequenceIterator.next();
for ( GroupWithInheritance groupOfGroups : sequence ) {
int numberOfViolations = validationContext.getFailingConstraints().size();
for ( Group group : groupOfGroups ) {
valueContext.setCurrentGroup( group.getDefiningClass() );
validateConstraintsForCurrentGroup( validationContext, valueContext );
if ( shouldFailFast( validationContext ) ) {
return validationContext.getFailingConstraints();
}
validateCascadedConstraints( validationContext, valueContext );
if ( shouldFailFast( validationContext ) ) {
return validationContext.getFailingConstraints();
}
}
if ( validationContext.getFailingConstraints().size() > numberOfViolations ) {
break;
}
}
}
return validationContext.getFailingConstraints();
}
这个方法主要调用了validateConstraintsForCurrentGroup( validationContext, valueContext );
,那么继续看源码
private void validateConstraintsForCurrentGroup(ValidationContext<?> validationContext, ValueContext<?, Object> valueContext) {
// we are not validating the default group there is nothing special to consider. If we are validating the default
// group sequence we have to consider that a class in the hierarchy could redefine the default group sequence.
if ( !valueContext.validatingDefault() ) {
validateConstraintsForNonDefaultGroup( validationContext, valueContext );
}
else {
validateConstraintsForDefaultGroup( validationContext, valueContext );
}
}
看看默认 group 的校验
private <U> void validateConstraintsForDefaultGroup(ValidationContext<?> validationContext, ValueContext<U, Object> valueContext) {
final BeanMetaData<U> beanMetaData = valueContext.getCurrentBeanMetaData();
final Map<Class<?>, Class<?>> validatedInterfaces = new HashMap<>();
// evaluating the constraints of a bean per class in hierarchy, this is necessary to detect potential default group re-definitions
for ( Class<? super U> clazz : beanMetaData.getClassHierarchy() ) {
BeanMetaData<? super U> hostingBeanMetaData = beanMetaDataManager.getBeanMetaData( clazz );
boolean defaultGroupSequenceIsRedefined = hostingBeanMetaData.defaultGroupSequenceIsRedefined();
// if the current class redefined the default group sequence, this sequence has to be applied to all the class hierarchy.
if ( defaultGroupSequenceIsRedefined ) {
Iterator<Sequence> defaultGroupSequence = hostingBeanMetaData.getDefaultValidationSequence( valueContext.getCurrentBean() );
Set<MetaConstraint<?>> metaConstraints = hostingBeanMetaData.getMetaConstraints();
while ( defaultGroupSequence.hasNext() ) {
for ( GroupWithInheritance groupOfGroups : defaultGroupSequence.next() ) {
boolean validationSuccessful = true;
for ( Group defaultSequenceMember : groupOfGroups ) {
validationSuccessful = validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz,
metaConstraints, defaultSequenceMember );
}
if ( !validationSuccessful ) {
break;
}
}
}
}
// fast path in case the default group sequence hasn't been redefined
else {
Set<MetaConstraint<?>> metaConstraints = hostingBeanMetaData.getDirectMetaConstraints();
validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz, metaConstraints,
Group.DEFAULT_GROUP );
}
validationContext.markCurrentBeanAsProcessed( valueContext );
// all constraints in the hierarchy has been validated, stop validation.
if ( defaultGroupSequenceIsRedefined ) {
break;
}
}
}
这下应该就一目了然了,如果这个 class 被重新定义成 default group 那么遍历所有的这个类的层次结构。然后获取到这个类相关的所有MetaConstraint
,传给下一个方法validateConstraintsForSingleDefaultGroupElement( validationContext, valueContext, validatedInterfaces, clazz, metaConstraints, defaultSequenceMember );
。看MetaConstraint
这个类可知,它是对应的一种约束,比如 Email, String等等。
private <U> boolean validateConstraintsForSingleDefaultGroupElement(ValidationContext<?> validationContext, ValueContext<U, Object> valueContext, final Map<Class<?>, Class<?>> validatedInterfaces,
Class<? super U> clazz, Set<MetaConstraint<?>> metaConstraints, Group defaultSequenceMember) {
boolean validationSuccessful = true;
valueContext.setCurrentGroup( defaultSequenceMember.getDefiningClass() );
for ( MetaConstraint<?> metaConstraint : metaConstraints ) {
// HV-466, an interface implemented more than one time in the hierarchy has to be validated only one
// time. An interface can define more than one constraint, we have to check the class we are validating.
final Class<?> declaringClass = metaConstraint.getLocation().getDeclaringClass();
if ( declaringClass.isInterface() ) {
Class<?> validatedForClass = validatedInterfaces.get( declaringClass );
if ( validatedForClass != null && !validatedForClass.equals( clazz ) ) {
continue;
}
validatedInterfaces.put( declaringClass, clazz );
}
boolean tmp = validateMetaConstraint( validationContext, valueContext, valueContext.getCurrentBean(), metaConstraint );
if ( shouldFailFast( validationContext ) ) {
return false;
}
validationSuccessful = validationSuccessful && tmp;
}
return validationSuccessful;
}
读validateConstraintsForSingleDefaultGroupElement()
方法,了解方法主要是遍历metaConstraints
,每个metaConstraint
都对这个 group 进行一次校验。
往下读得知,它先是逐步创建约束对应的 constraintTree
,每个 constraintTree
用一个工厂类创建了 ConstraintValidator<A, ?>
,然后调用 isValid()
方法,完成了校验。最后如果校验失败了,将不符合约束的地方写到了ValidationContext<T>
中。每个constraintTree
会校验目标对象的所有指定约束的属性。比如@Email
protected final <T, V> Set<ConstraintViolation<T>> validateSingleConstraint(ValidationContext<T> executionContext,
ValueContext<?, ?> valueContext,
ConstraintValidatorContextImpl constraintValidatorContext,
ConstraintValidator<A, V> validator) {
boolean isValid;
try {
@SuppressWarnings("unchecked")
V validatedValue = (V) valueContext.getCurrentValidatedValue();
isValid = validator.isValid( validatedValue, constraintValidatorContext );
}
catch (RuntimeException e) {
if ( e instanceof ConstraintDeclarationException ) {
throw e;
}
throw LOG.getExceptionDuringIsValidCallException( e );
}
if ( !isValid ) {
//We do not add these violations yet, since we don't know how they are
//going to influence the final boolean evaluation
return executionContext.createConstraintViolations(
valueContext, constraintValidatorContext
);
}
return Collections.emptySet();
}
总结一下校验方法做了这些事:
- 如果传 groups ,就根据 groups 获取 group sequence,否则就是获取 default group sequence,也就是所有属性都要遍历都要校验。
- 每个 group 都进行一次 metaConstraints 的遍历,metaConstraint 对应一种约束,比如 Email 类型,或者 String 类型等等
- metaConstraint 拿到 constraintTree,然后创建 Validator,进行校验,如果校验不通过写到 ValidationContext 中
- 从 ValidationContext 中可以获取到校验不通过的所有信息
大概的校验机制就是这些了。
总结
读源码经常会被一层又一层的调用难倒,以我浅薄的经验,读源码首先要明确要读的是什么模块什么功能,带着目的性读源码,然后多从方法名和变量名去理解含义,多看类和接口的注释。逐步看懂细节再看整体,这样比较能对整个功能模块有全面的了解。