架构实战篇(五):Spring Boot 表单验证和异常处理

代码界的吴彦祖 2018-03-22 14:30:51 ⋅ 219 阅读


为了让API 能够更好的提供服务,表单数据验证和异常的处理是必不可少的,让我们来看看怎么处理才能让代码能够更好的解耦和扩展维护

为了节省篇幅,本节的代码是在《架构实战篇(二):Spring Boot 整合Swagger2》的基础上做的添加

1. 使用 @Valid 验证数据

为了接收前端传过来的请求参数(表单)我们创建一个Form结尾的数据类,并在必要的字段上增加 hibernate 的验证注解

package com.example.model;
import io.swagger.annotations.ApiModelProperty;
import org.hibernate.validator.constraints.NotEmpty;
public class UserForm {    
@ApiModelProperty(value = "用户名", required = true, example = "admin")  
@NotEmpty(message = "用户名不能为空")    
private String username;    
@ApiModelProperty(value = "密码", required = true, example = "000000")  
@NotEmpty(message = "密码不能为空")    
private String password;    
// get set

}

接下来我们在 Rest API的入参地方增加上 @Valid 注解告诉Spring 这个入参的对象是需要验证的

一般情况下我们都会把数据的验证和正常逻辑都放在这里代码上会过于冗余,这里我们把数据验证和异常分离了出来,按照正常的逻辑返回一个用户(User)对象给请求端

package com.example.controller;
import com.example.exception.BusinessException;
import com.example.exception.UserNotFoundException;
import com.example.model.User;
import com.example.model.UserForm;
import io.swagger.annotations.*;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;@Api(value = "用户", description = "用户")
@RequestMapping("/user")
@RestController
public class UserController {    
@ApiOperation(value = "登录", notes = "输入用户名和密码登录")    
@ApiResponses(value = {            
@ApiResponse(code = 200, message = "OK", response = User.class, responseContainer = "user"),            
@ApiResponse(code = 405, message = "用户名或者密码不存在")    })  
 @RequestMapping(value = "/login", produces = {"application/json"}, method = RequestMethod.POST)    
 public User login(@Valid @RequestBody UserForm form) {        
 if (!form.getPassword().equalsIgnoreCase("000000")) {          
  // 使用默认的业务异常类,需要每次都填入消息            throw new BusinessException(405, "用户名或者密码不存在");        }        User user = new User();        user.setId(1L);        user.setUsername(form.getUsername());        user.setFirstName("小");        user.setLastName("明");        user.setEmail("xiaoming@mail.com");        user.setUserStatus(1);        return user;    }    
@ApiOperation(value = "获取用户信息", notes = "获取用户信息")    
@ApiResponses(value = {          
@ApiResponse(code = 200, message = "OK", response = User.class, responseContainer = "user"),            
@ApiResponse(code = 406, message = "用户不能存在")    })    
@GetMapping(value = "/info/{userId}")    
public User getUserInfo(            @ApiParam(name = "userId", value = "用户编号", type = "path", defaultValue = "1000")            @PathVariable("userId") Integer userId) {        
if (userId != 1000) {            
// 自定义异常类可以出入需要的参数然后在异常处理里面做统一的处理            throw new UserNotFoundException(userId);        }        User user = new User();        user.setId(1000L);        user.setUsername("1000");        user.setFirstName("小");        user.setLastName("明");        user.setEmail("xiaoming@mail.com");        user.setUserStatus(1);        
       return user;    } }

在上面代码中我们定义了两个异常
BusinessException(使用默认的业务异常类,需要每次都填入消息)
UserNotFoundException(自定义异常类可以出入需要的参数然后在异常处理里面做统一的处理)

2. 使用 @RestControllerAdvice 解耦并处理异常

这里我们要分清'请求状态码'和'业务状态码'
HttpStatus 是请求状态码,是为了告诉请求端本次请求的一个状态
而我们下面 ErrorBody 的Code 是业务上自定义的状态吗,是为了告诉用户你请求成功了,但是业务处理上出错了

package com.example.exception;
import com.example.model.ErrorBody;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.List;
/** * Api 异常处理 */

@RestControllerAdvice
public class ApiExceptionHandler {    
// 对表单验证时抛出的 MethodArgumentNotValidException 异常做统一处理

@ExceptionHandler(MethodArgumentNotValidException.class)    
public ResponseEntity<?> validException(MethodArgumentNotValidException e) {        BindingResult bindingResult = e.getBindingResult();        List<ObjectError> errors = bindingResult.getAllErrors();                
if (!errors.isEmpty()) {            
       // 只显示第一个错误信息        ErrorBody body = new ErrorBody(HttpStatus.BAD_REQUEST.value(), errors.get(0).getDefaultMessage());            
       return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);        }        
       return new ResponseEntity<>("valid error", HttpStatus.BAD_REQUEST);    }  
 
// 业务异常的默认处理方式@ExceptionHandler(BusinessException.class)    
public ResponseEntity<?> businessException(BusinessException e) {        ErrorBody body = new ErrorBody(e.getCode(), e.getMessage());        
       return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);    }    

// 代码异常的处理方式
@ExceptionHandler(Exception.class)    
public ResponseEntity<?> defaultHandler(Exception e) {        ErrorBody body = new ErrorBody(HttpStatus.INTERNAL_SERVER_ERROR.value(), e.getMessage());        
   return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);    } }
package com.example.exception;
/** * 业务异常 */
public class BusinessException extends RuntimeException {    
private Integer code;    
public BusinessException(Integer code) {        
   this.code = code;    }    
public BusinessException(Integer code, String message) {        
   super(message);        
   this.code = code;    }  
// get set

}

3. 自定义带参数异常

package com.example.exception;
public class UserNotFoundException extends BusinessException {    
public UserNotFoundException(Integer userId) {        
       super(406, "用户'" + userId + "'不存在");    } }
package com.example.model;
/** * 异常消息体 */
public class ErrorBody {    
private Integer code;    
private String message;    
private long timestamp = System.currentTimeMillis();    
public ErrorBody(Integer code, String message) {        
   this.code = code;        
   this.message = message;    }  
// get set
}

4. 测试异常处理

访问本地服务
http://localhost:8081/swagger/swagger-ui.html

  • 测试下用户登录服务
    输入一个正确的内容,点击“Try it out"

正确的返回

输入一个错误的内容,点击“Try it out"


错误的返回

这里我们注意看下Response Code 是 400 ,Response Body 里面的Code 是405
400 是告诉我们请求出错了,不会返回正确的User 对象了
405 是我们自定义的业务编码,调用端可以根据这个编码做一些处理

  • 在测试下我们的带参数异常
    输入一个错误的内容,点击“Try it out"


    错误的返回

这里我们看到返回的message 中把我们请求时的用户名也包含在内了,减少了我们每次都拼接字符串的时间

预告下一节我们将分享如何使用Spring boot 调用其他Spring boot 提供的API 服务并友好的处理异常

关注我们

想要了解更多内容请关注“IT实战联盟”微信公众号!也可以留言和作者交流 获取源码哦!!!



全部评论: 0

    我有话说:

    架构实战(十):Spring Boot 解耦之事件驱动

    通过使用spring 事件来解决业务代码的耦合

    架构实战(三)-Spring Boot架构搭建RESTful API案例

    之前分享了Spring Boot 整合Swagger 让API可视化前后端分离架构 受到了大家一致好评 ,本节就接着上节的代码做了详细的查询代码的补充完善并搭建RESTful API架构案例。

    架构实战(十七):Spring Boot Assembly 整合 thymeleaf

    如何让服务器上的 sprig boot 项目升级变的方便快捷

    微服务架构实战(六):Spring boot2.x 集成阿里大鱼短信接口详解与Demo

    Spring boot2.x 集成阿里大鱼短信接口,发送短信验证码及短信接口详解。

    架构实战(七):Spring Boot Data JPA 快速入门

    Spring Data JPA 是Spring Data 的一个子项目,它通过提供基于JPA的Repository极大了减少了操作JPA的代码。

    架构实战(六):Spring Boot RestTemplate的使用

    RestTemplate是Spring提供的用于访问Rest服务的客户端,RestTemplate提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率。

    架构实战(十):Spring Boot 集成 Dubbo

    Dubbo是阿里巴巴SOA服务化治理方案的核心框架,一个分布式服务框架,致力于提供高性能透明化的RPC远程服务调用方案。

    架构实战(一)-Spring Boot+MyBatis基础架构搭建

    Spring的追求一定是简单点简单点,让java的开发变得更加简单、容易。瞧瞧的告诉你们直接copy就能用哦~~~

    架构实战(十一):Spring Boot 集成企业级搜索引擎 SolrCloud

    Solr是以Lucene为基础实现的文本检索应用服务。Solr部署方式有单机方式、多机Master-Slaver方式、Cloud方式。

    码云推荐:一个优秀的分布式spring boot/Spring Cloud API限流框架,特别适合微服务架构

    一个优秀的分布式spring boot/Spring Cloud API限流框架,特别适合微服务架构.

    架构实战(九):Spring Boot 集成 RocketMQ

    快速集成阿里开源消息队列 RocketMQ

    架构实战(十四):Spring Boot 多缓存实战

    多场景下的不同缓存策略解决方案

    微服务架构学习笔记:gRPC Spring Boot Starter 2.2.0 发布,及使用步骤

    gRPC Spring Boot Starter 项目是一个 gRPC 的 Spring Boot 模块。内嵌一个 gRPC Server 对外提供服务,并支持 Spring Cloud 的服务发现

    架构实战(十三):Spring Boot Logback 邮件通知

    日志对于应用程序来说是非常重要的,当你的程序报错了,而你又不知道是多么可怕的一件事情,本文使用logback把程序报错信息邮件到开发者

    架构实战(四):Spring Boot整合 Thymeleaf

    Thymeleaf 是一种模板语言。那模板语言或模板引擎是什么?

    SpringBoot+zk+dubbo架构实践(二):SpringBoot 集成 zookeeper

    不啰嗦,本完成两件事:1、搭建SpringBoot 框架;2、基于spring boot框架访问zookeeper。

    架构实战(十二):Spring Boot 分布式Session共享Redis

    分布式Web网站一般都会碰到集群session共享问题,小编整理了一套解决方案,内附GitHub 源码地址哦~~~