前面的章节,系统雏形已经初步形成,前端项目的展示数据为固定数据活mock数据,今天我们来一起完善前后端项目数据交互。
日常工作中,我们开发接口时,一般都会涉及到参数校验、异常处理、封装结果返回等处理。如果每个后端开发人员在参数校验、异常处理等都是各写各的,没有统一处理的话,代码即不优雅,也不容易维护。前端也很难对数据统一操作。所以,作为一名合格的后端开发工程师,我们需要统一校验参数,统一异常处理、统一结果返回,让代码更加规范、可读性更强、更容易维护。
后端服务的API接口可以查看文档和调试,通过swagger可减少与前端人员沟通成本,也可帮助后端人员了解后端API详情。
添加pom依赖
io.springfox
springfox-swagger2
2.7.0
io.springfox
springfox-swagger-ui
2.7.0
com.github.xiaoymin
swagger-bootstrap-ui
1.9.1
添加启动类
@Component
@Configuration
@EnableSwagger2
public class Swagger2Config extends WebMvcConfigurationSupport {
@Bean
public Docket createRestApi(){
ParameterBuilder tokenPar = new ParameterBuilder();
List pars = new ArrayList<>();
tokenPar.name(Constant.token).description("令牌").
modelRef(new ModelRef("string")).
parameterType("header").required(false).build();
pars.add(tokenPar.build());
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.xgg"))
.paths(PathSelectors.any())
.build().globalOperationParameters(pars);
}
private ApiInfo apiInfo(){
return new ApiInfoBuilder().build();
}
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("swagger-ui.html")
.addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("doc.html").
addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
//将所有/static/** 访问都映射到classpath:/static/ 目录下
registry.addResourceHandler("/static/**")
.addResourceLocations(ResourceUtils.CLASSPATH_URL_PREFIX +"/static/");
super.addResourceHandlers(registry);
}
}
1.2、统一返回格式
success 接口返回是否成功状态;code返回的状态码;message状态码说明,info返回数据对象。
{
"success": true, //是否成功 true 成功, false 失败
"code": 200, //返回状态码
"message": "成功", //返回状态说明
"info": "" //返回数据对象
}
返回实体定义
@Slf4j
@ApiModel(value = "统一返回结果")
public class Result {
@ApiModelProperty("是否成功")
private boolean success;
@ApiModelProperty("返回码")
private Integer code;
@ApiModelProperty("返回码说明")
private String message;
@ApiModelProperty("返回对象数据")
private T info;
/**
* 成功
*/
public static Result ok() {
Result r = new Result();
r.setSuccess(true);
r.setCode(ResultEnum.OK.getCode());
r.setMessage(ResultEnum.OK.getName());
return r;
}
/**
* 错误
*/
public static Result error() {
Result r = new Result();
r.setSuccess(false);
r.setCode(ResultEnum.ERROR.getCode());
r.setMessage(ResultEnum.ERROR.getName());
return r;
}
/**
* 无权限
*/
public static Result noAccess() {
Result r = new Result();
r.setSuccess(false);
r.setCode(ResultEnum.SIGNATURE_NOT_MATCH.getCode());
r.setMessage(ResultEnum.SIGNATURE_NOT_MATCH.getName());
return r;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public T getInfo() {
return info;
}
public void setInfo(T info) {
this.info = info;
}
public Result code(Integer value) {
this.setCode(value);
return this;
}
public Result message(String value) {
this.setMessage(value);
return this;
}
public Result info(T value) {
this.setInfo(value);
return this;
}
}
添加依赖
org.springframework.boot
spring-boot-starter-validation
添加参数实体类
@Data
public class TestParam {
@NotBlank(message = "姓名不能为空")
@ApiModelProperty(value = "姓名",required = true)
private String username;
@NotNull(message = "年龄不能为空")
@ApiModelProperty(value = "年龄",required = true)
private Long age;
@ApiModelProperty(value = "毕业学校")
private String school;
}
@NotNull适用于基本数据类型(Integer,Long,Double等等),当 @NotNull 注解被使用在 String 类型的数据上,则表示该数据不能为 Null(但是可以为 Empty)
@NotBlank适用于 String 类型的数据上,加了@NotBlank 注解的参数不能为 Null 且 trim() 之后 size > 0
@NotEmpty适用于 String、Collection集合、Map、数组等等,加了@NotEmpty 注解的参数不能为 Null 或者 长度为 0
Spring Validation验证框架对参数的验证机制提供了@Validated(Spring's JSR-303规范,是标准JSR-303的一个变种),javax提供了@Valid(标准JSR-303规范),配合BindingResult可以直接提供参数验证结果。
@Valid属于javax.validation包下,是jdk给提供的 是使用Hibernate validation的时候使用
@Validated是org.springframework.validation.annotation包下的,是spring提供的 是只用Spring Validator校验机制使用
说明:java的JSR303声明了@Valid这类接口,而Hibernate-validator对其进行了实现
@Validation对@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明。
在检验Controller的入参是否符合规范时,使用@Validated或者@Valid在基本验证功能上没有太多区别。但是在分组、注解地方、嵌套验证等功能上两个有所不同:
分组
@Validated:提供了一个分组功能,可以在入参验证时,根据不同的分组采用不同的验证机制,这个网上也有资料,不详述。@Valid:作为标准JSR-303规范,还没有吸收分组的功能。
注解位置
@Validated:用在类型、方法和方法参数上。但不能用于成员属性(field)
@Valid:可以用在方法、构造函数、方法参数和成员属性(field)上 所以可以用@Valid实现嵌套验证。
前面咱们已经实现了前后交互的数据规范和基本流程,但是如果后端出现异常前端如何应对呢。
测试发现返回的数据和咱们制定的数据规范不一致,前端只能通过catch来捕获异常,这显然不利于前端同学研发的便利性。
所以我们需要再后端建立统一异常规范,全局捕获已知和未知异常,按照我们之前制定的数据交互规范来统一返回数据。
添加全局异常捕获
@Slf4j
@RestControllerAdvice
public class ExceptionHandlerAdvice {
@ExceptionHandler({HttpMessageNotReadableException.class, ConstraintViolationException.class,MissingServletRequestParameterException.class})
public Result messageExceptionHandler(Exception ex) {
log.error(ResultEnum.BODY_NOT_MATCH.getName(), ex);
return Result.error().code(ResultEnum.BODY_NOT_MATCH.getCode()).message(ResultEnum.BODY_NOT_MATCH.getName());
}
@ExceptionHandler(value = {MethodArgumentNotValidException.class, BindException.class})
public Result controllerException(Exception e, BindingResult bindingResult) {
List listErrors = bindingResult.getFieldErrors();
if (!listErrors.isEmpty()) {
FieldError fieldError = listErrors.get(0);
return Result.error().code(ResultEnum.BODY_NOT_MATCH.getCode()).message(ResultEnum.BODY_NOT_MATCH.getName()).info(fieldError.getField()+""+fieldError.getDefaultMessage());
}
return Result.error().code(ResultEnum.BODY_NOT_MATCH.getCode()).message(ResultEnum.BODY_NOT_MATCH.getName());
}
@ExceptionHandler({HttpRequestMethodNotSupportedException.class, MethodArgumentTypeMismatchException.class})
public Result requestExceptionHandler(Exception ex ) {
log.error(ResultEnum.BODY_NOT_MATCH.getName(), ex);
return Result.error().code(ResultEnum.BODY_NOT_MATCH.getCode()).message(ResultEnum.BODY_NOT_MATCH.getName());
}
/**
* 自定义异常
*/
@ExceptionHandler({ BizException.class })
public Result bizException(BizException e) {
log.error("自定义系统异常", e);
return Result.error().message(e.getMsg()).code(e.getCode());
}
/**
* 全局异常
*/
@ExceptionHandler({ Exception.class })
// @ResponseStatus(HttpStatus.BAD_REQUEST)
public Result sysException(Exception e) {
log.error("系统异常", e);
return Result.error();
}
添加自定义异常
@Data
public class BizException extends RuntimeException {
private Integer code;
private String msg;
public BizException(Integer code, String msg) {
this.msg = msg;
this.code = code;
}
public BizException(String msg) {
this.msg = msg;
this.code = ResultEnum.ERROR.getCode();
}
public BizException(String msg, Throwable t) {
super(t);
this.msg = msg;
this.code = ResultEnum.ERROR.getCode();
}
}
使用自定义异常
throw new SqException(201,"测试自定义异常");
@ApiOperation(value = "测试查询")
@GetMapping("/test")
public Result test(@Valid TestParam testParam, BindingResult result) throws Exception{
List fieldErrors = result.getFieldErrors();
if (!fieldErrors.isEmpty()) {
return Result.error().info(fieldErrors.get(0).getDefaultMessage());
}
return Result.ok().info(testParam);
}
@ApiOperation(value = "测试接口1")
@PostMapping("/test1")
public Result test1(@Valid TestParam testParam) throws Exception{
throw new BizException(201,"测试自定义异常");
testService.test("哈哈哈哈哈哈哈");
return Result.ok().info(testParam);
}
前后端交互接口遵循统一RESTful接口原则,构建优良的 REST API。
避免在 URI 中使用动词
HTTP Method动词与 URI 的组合,比如 GET: / user/。一个端点可以被解释为对某种资源进行的某个动作。比如, POST: /user 代表“创建一个新的 user”而不是saveUser;查询用户:GET: /user而不是getUser。
HTTP Method: GET 代表查,POST代表增,PUT代表改, DELETE 代表删。
axios时目前最流行的ajax封装库之一,用于很方便地实现ajax请求的发送。
添加依赖
npm install axios
使用axios
我们使用之前的test.vue页面和SpringBoot后台的测试接口做测试。
axios
.get('http://127.0.0.1:8888/test', {
params: {
username: 12345,
age: 20
}
}).then(({data}) => {
if(data && data.success){
this.$message.success(data.message)
}else{
this.$message.error(data.info)
}
}).catch(error => { // 请求失败处理
this.$message.error("系统异常,请稍后重试!");
});
}
点击测试按钮会发现浏览器控制台报错了
这是跨域问题引起的,因为我们浏览器访问的是9081端口,而访问后台的端口是8888端口,而且部署到服务器以后可能不光两者端口不同,连ip可能都会不同了。这就是跨域问题。
后端跨域有很多种方式,咱们这里采用过滤器的方式。
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
//1,允许任何来源
corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
//2,允许任何请求头
corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
//3,允许任何方法
corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
//4,允许凭证
corsConfiguration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsFilter(source);
}
}
重启后端服务,测试点击按钮发现成功返回后端数据。
查询
测试
关注公众号”小猿架构“,发送 "前后分离架构" ,下载课程视频+课程源码+课件。