手把手教你前后分离架构(四) 前后端数据交互

前面的章节,系统雏形已经初步形成,前端项目的展示数据为固定数据活mock数据,今天我们来一起完善前后端项目数据交互。

1、后台统一接口

日常工作中,我们开发接口时,一般都会涉及到参数校验、异常处理、封装结果返回等处理。如果每个后端开发人员在参数校验、异常处理等都是各写各的,没有统一处理的话,代码即不优雅,也不容易维护。前端也很难对数据统一操作。所以,作为一名合格的后端开发工程师,我们需要统一校验参数,统一异常处理、统一结果返回,让代码更加规范、可读性更强、更容易维护。

1.1、集成swagger

后端服务的API接口可以查看文档和调试,通过swagger可减少与前端人员沟通成本,也可帮助后端人员了解后端API详情。

添加pom依赖

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.7.0</version>
        </dependency>
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.1</version>
        </dependency>

添加启动类

@Component
@Configuration
@EnableSwagger2
public class Swagger2Config extends WebMvcConfigurationSupport {
    @Bean
    public Docket createRestApi(){

        ParameterBuilder tokenPar = new ParameterBuilder();
        List<Parameter> 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<T> {
    @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;
    }
}

1.3、统一参数校验

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency

添加参数实体类

@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实现嵌套验证。

1.4、统一异常

前面咱们已经实现了前后交互的数据规范和基本流程,但是如果后端出现异常前端如何应对呢。

测试发现返回的数据和咱们制定的数据规范不一致,前端只能通过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<FieldError> 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,"测试自定义异常");

1.5、验证测试

@ApiOperation(value = "测试查询")
@GetMapping("/test")
public Result test(@Valid TestParam testParam, BindingResult result) throws Exception{
    List<FieldError> 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);
}

1.6、接口规范

前后端交互接口遵循统一RESTful接口原则,构建优良的 REST API

避免在 URI 中使用动词

HTTP Method动词与 URI 的组合,比如 GET: / user/。一个端点可以被解释为对某种资源进行的某个动作。比如, POST: /user 代表“创建一个新的 user”而不是saveUser;查询用户:GET: /user而不是getUser。
HTTP Method: GET 代表查,POST代表增,PUT代表改, DELETE 代表删。

2、前后端交互

2.1前端整合axios

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可能都会不同了。这就是跨域问题。

2.2 、SpringBoot跨域支持

后端跨域有很多种方式,咱们这里采用过滤器的方式。

@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);
    }
}  

重启后端服务,测试点击按钮发现成功返回后端数据。

2.3、表单验证实践

<template>
<div>
  <el-form :inline="true" :model="formInline" :rules="rules" ref="formInline" class="demo-form-inline">
    <el-form-item label="姓名" prop="username">
      <el-input v-model="formInline.username" placeholder="姓名"></el-input>
    </el-form-item>
    <el-form-item label="年龄" prop="age">
      <el-input v-model="formInline.age" placeholder="年龄"></el-input>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="onSubmit('formInline')">查询</el-button>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="test" icon="el-icon-search">测试</el-button>
    </el-form-item>
  </el-form>
</div>
</template>

<script>
import axios from 'axios'
export default {
  name: "test1",
  data() {
    return {
      formInline: {
        username: '',
        age: ''
      },
      rules: {
        username: [
          {required: true, message: '请姓名', trigger: 'blur'},
          {min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur'}
        ],
        age: [
          { required: true, message: '年龄不能为空', trigger: 'blur'},
          { type: 'number', message: '年龄必须为数字值'}
        ]
      }
    }
  },
  methods:{
    test(){
      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.warning(data.info)
          }
        }).catch(error => { // 请求失败处理
          this.$message.error("系统异常,请稍后重试!");
        });
    },
    onSubmit(formName){
      this.$refs[formName].validate((valid) => {
        if (valid) {
          axios.get('http://127.0.0.1:8888/test', {
            params: this.formInline
          }).then(({data}) => {
            if (data && data.success) {
              this.$message.success(data.message)
            } else {
              this.$message.warning(data.info)
            }
          }).catch(error => { // 请求失败处理
            this.$message.error("系统异常,请稍后重试!");
          });
        }
      })
    },
  }
}
</script>

 

关注公众号”小猿架构“,发送 "前后分离架构" ,下载课程视频+课程源码+课件。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码

)">
< <上一篇
下一篇>>