赞
踩
前面的章节,系统雏形已经初步形成,前端项目的展示数据为固定数据活mock数据,今天我们来一起完善前后端项目数据交互。
日常工作中,我们开发接口时,一般都会涉及到参数校验、异常处理、封装结果返回等处理。如果每个后端开发人员在参数校验、异常处理等都是各写各的,没有统一处理的话,代码即不优雅,也不容易维护。前端也很难对数据统一操作。所以,作为一名合格的后端开发工程师,我们需要统一校验参数,统一异常处理、统一结果返回,让代码更加规范、可读性更强、更容易维护。
后端服务的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;
- }
- }
添加依赖
- <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实现嵌套验证。
前面咱们已经实现了前后交互的数据规范和基本流程,但是如果后端出现异常前端如何应对呢。
测试发现返回的数据和咱们制定的数据规范不一致,前端只能通过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,"测试自定义异常");
- @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);
- }
前后端交互接口遵循统一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);
- }
- }
重启后端服务,测试点击按钮发现成功返回后端数据。
- <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>
关注公众号”小猿架构“,发送 "前后分离架构" ,下载课程视频+课程源码+课件。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。