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

1、后台统一接口

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

1.1、集成swagger

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

添加pom依赖

io.springfoxspringfox-swagger22.7.0io.springfoxspringfox-swagger-ui2.7.0com.github.xiaoyminswagger-bootstrap-ui1.9.1

添加启动类

@Component@Configuration@EnableSwagger2public class Swagger2Config extends WebMvcConfigurationSupport {@Beanpublic 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();}@Overrideprotected 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;}}

1.3、统一参数校验

添加依赖

org.springframework.bootspring-boot-starter-validation</dependency

添加参数实体类

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

添加自定义异常

@Datapublic 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 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跨域支持

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

@Configurationpublic class CorsConfig {@Beanpublic 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、表单验证实践

查询测试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("系统异常,请稍后重试!");});}})},}}

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