spring项目《接口响应体格式统一封装》

    xiaoxiao2022-07-07  161

    前言 在之前的文章中我们有介绍过,如何更好、更简单的写好一个接口(接口返回值篇),今天的这篇文章我们主要介绍,怎么统一处理下接口的返回格式问题。 问题分析 我们先来分析下我们所面临的问题在哪里,然后接着给出解决方案。在写一个接口时,我们通常会先统一定义一下接口的返回格式是什么,然后在跟前端去对接,通常的返回格式大体两种(我们以保存用户为例): 1. 成功/失败响应格式不一致(此种方式作为我们默认的接口响应方式)

    保存用户成功,响应体

    {     "id": 10000,     "pwd": "123456",     "nickname": "小竹马",     "img": "http://avatar.csdn.net/0/E/9/1_aiyaya_.jpg",     "status": "NORMAL",     "createTime": 1515075974540 } 12345678

    失败响应体(下面的格式是spring boot默认的错误响应格式,只不过我们在其基础上增加了一个code字段用于解释更详细的错误码)

    {     "status": 400,     "error": "Bad Request",     "message": "参数无效",     "code": 10001,     "path": "/zhuma-demo/users",     "exception": "org.springframework.web.bind.MethodArgumentNotValidException",     "errors": [         {             "fieldName": "status",             "message": "值是无效的"         }     ],     "timestamp": 1515076067369 } 123456789101112131415 2.成功/失败响应体格式一致

    保存用户成功,响应体

    {     "code": 1,     "msg": "成功",     "data": {         "id": 10000,         "pwd": "123456",         "nickname": "小竹马",         "img": "http://avatar.csdn.net/0/E/9/1_aiyaya_.jpg",         "status": "NORMAL",         "createTime": 1515076287882     } } 123456789101112

    失败响应体

    {     "code": 10001,     "msg": "参数无效",     "data": [         {             "fieldName": "status",             "message": "值是无效的"         }     ] } 12345678910 那么如果我们想要的响应体格式是第二种,我们该如何写我们的代码呢?你可能想是这样么? @RestController @RequestMapping("/users") public class UserController {

        @PostMapping     @ResponseStatus(HttpStatus.CREATED)     public PlatformResult addUser(@Validated @RequestBody User user) {         user.setId(10000L);         user.setCreateTime(new Date());         return PlatformResult.success(user);     }

    } 12345678910111213 PlatformResult.success()这段逻辑显然很多余,每个方法都要这样写一遍,所以上述方式并不是我们想要的,我们要的是 @ResponseResult(PlatformResult.class) @RestController @RequestMapping("/users") public class UserController {

        @PostMapping     @ResponseStatus(HttpStatus.CREATED)     public User addUser(@Validated @RequestBody User user) {         user.setId(10000L);         user.setCreateTime(new Date());         return user;     }

    } 1234567891011121314 我们加了一个自定义的注解@ResponseResult(PlatformResult.class),参数PlatformResult.class告诉这个Controller类下的所有方法都以这个类PlatformResult的格式进行返回,这个注解可以标记在类或方法上,好了,我们的目的明朗了许多,要做的就是标记这个注解让它实现接口返回值格式控制这个功能,下面我们给出具体的实现方式。 实现思路 首先介绍下完成我们这次主要功能的几个类:

    Result 是返回格式类的父接口(所有返回格式类都需要继承它) PlatformResult 通用返回结果格式(我们上面说的第二种返回结果) DefaultErrorResult 全局错误返回结果(我们上面说的第一种错误时的返回结果) GlobalExceptionHandler全局异常处理 ResponseResult 注解类(用于在Controller上指定返回值格式类) ResponseResultInterceptor 拦截器(主要用于将ResponseResult注解类的标记信息传入ResponseResultHandler中) ResponseResultHandler 响应体格式处理器(主要转换逻辑都在这里)

    代码实现 下面将有一大片代码袭来,要顶住!O(∩_∩)O哈哈~ 1. Result 接口类 package com.zhuma.demo.comm.result;

    import java.io.Serializable;

    /**  * @desc 响应格式父接口  *  * @author zhumaer  * @since 4/1/2018 3:00 PM  */ public interface Result extends Serializable { }

    12345678910111213 说明 理论上所有的返回格式类都需要实现该接口才能被使用 2. PlatformResult 通用返回结果 package com.zhuma.demo.comm.result;

    import com.zhuma.demo.enums.ResultCode; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor;

    /**  * @desc 平台通用返回结果  *   * @author zhumaer  * @since 10/9/2017 3:00 PM  */ @Builder @NoArgsConstructor @AllArgsConstructor @Data public class PlatformResult implements Result {

        private static final long serialVersionUID = 874200365941306385L;

        private Integer code;

        private String msg;

        private Object data;

        public static PlatformResult success() {         PlatformResult result = new PlatformResult();         result.setResultCode(ResultCode.SUCCESS);         return result;     }

        public static PlatformResult success(Object data) {         PlatformResult result = new PlatformResult();         result.setResultCode(ResultCode.SUCCESS);         result.setData(data);         return result;     }

        public static PlatformResult failure(ResultCode resultCode) {         PlatformResult result = new PlatformResult();         result.setResultCode(resultCode);         return result;     }

        public static PlatformResult failure(ResultCode resultCode, Object data) {         PlatformResult result = new PlatformResult();         result.setResultCode(resultCode);         result.setData(data);         return result;     }

        public static PlatformResult failure(String message) {         PlatformResult result = new PlatformResult();         result.setCode(ResultCode.PARAM_IS_INVALID.code());         result.setMsg(message);         return result;     }

        private void setResultCode(ResultCode code) {         this.code = code.code();         this.msg = code.message();     }

    }

    1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768 3. DefaultErrorResult 默认全局错误返回格式 package com.zhuma.demo.comm.result;

    import java.util.Date;

    import com.zhuma.demo.enums.ExceptionEnum; import com.zhuma.demo.exception.BusinessException; import com.zhuma.demo.util.RequestContextHolderUtil; import com.zhuma.demo.util.StringUtil; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes;

    import com.zhuma.demo.enums.ResultCode; import org.springframework.http.HttpStatus;

    /**  * @desc 默认全局错误返回结果  *       备注:该返回信息是spring boot的默认异常时返回结果{@link DefaultErrorAttributes},目前也是我们服务的默认的错误返回结果  *   * @author zhumaer  * @since 9/29/2017 3:00 PM  */ @Builder @AllArgsConstructor @NoArgsConstructor @Data public class DefaultErrorResult implements Result {

        private static final long serialVersionUID = 1899083570489722793L;

        /**      * HTTP响应状态码 {@link org.springframework.http.HttpStatus}      */     private Integer status;

        /**      * HTTP响应状态码的英文提示      */     private String error;

        /**      * 异常堆栈的精简信息      *       */     private String message;

        /**      * 我们系统内部自定义的返回值编码,{@link ResultCode} 它是对错误更加详细的编码      *       * 备注:spring boot默认返回异常时,该字段为null      */     private Integer code;

        /**      * 调用接口路径      */     private String path;

        /**      * 异常的名字      */     private String exception;

        /**      * 异常的错误传递的数据      */     private Object errors;

        /**      * 时间戳      */     private Date timestamp;

        public static DefaultErrorResult failure(ResultCode resultCode, Throwable e, HttpStatus httpStatus, Object errors) {         DefaultErrorResult result = DefaultErrorResult.failure(resultCode, e, httpStatus);         result.setErrors(errors);         return result;     }

        public static DefaultErrorResult failure(ResultCode resultCode, Throwable e, HttpStatus httpStatus) {         DefaultErrorResult result = new DefaultErrorResult();         result.setCode(resultCode.code());         result.setMessage(resultCode.message());         result.setStatus(httpStatus.value());         result.setError(httpStatus.getReasonPhrase());         result.setException(e.getClass().getName());         result.setPath(RequestContextHolderUtil.getRequest().getRequestURI());         result.setTimestamp(new Date());         return result;     }

        public static DefaultErrorResult failure(BusinessException e) {         ExceptionEnum ee = ExceptionEnum.getByEClass(e.getClass());         if (ee != null) {             return DefaultErrorResult.failure(ee.getResultCode(), e, ee.getHttpStatus(), e.getData());         }

            DefaultErrorResult defaultErrorResult = DefaultErrorResult.failure(e.getResultCode() == null ? ResultCode.SUCCESS : e.getResultCode(), e, HttpStatus.OK, e.getData());         if (StringUtil.isNotEmpty(e.getMessage())) {             defaultErrorResult.setMessage(e.getMessage());         }         return defaultErrorResult;     }

    }

    123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108 4. GlobalExceptionHandler 全局错误异常处理器 package com.zhuma.demo.handler;

    import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolationException;

    import com.zhuma.demo.comm.handler.BaseGlobalExceptionHandler; import com.zhuma.demo.comm.result.DefaultErrorResult; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController;

    import com.zhuma.demo.exception.BusinessException;

    /**  * @desc 统一异常处理器  *   * @author zhumaer  * @since 8/31/2017 3:00 PM  */ @RestController @ControllerAdvice public class GlobalExceptionHandler extends BaseGlobalExceptionHandler {

        /* 处理400类异常 */     @ResponseStatus(HttpStatus.BAD_REQUEST)     @ExceptionHandler(ConstraintViolationException.class)     public DefaultErrorResult handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {         return super.handleConstraintViolationException(e, request);     }

        @ResponseStatus(HttpStatus.BAD_REQUEST)     @ExceptionHandler(HttpMessageNotReadableException.class)     public DefaultErrorResult handleConstraintViolationException(HttpMessageNotReadableException e, HttpServletRequest request) {         return super.handleConstraintViolationException(e, request);     }

        @ResponseStatus(HttpStatus.BAD_REQUEST)     @ExceptionHandler(BindException.class)     public DefaultErrorResult handleBindException(BindException e, HttpServletRequest request) {         return super.handleBindException(e, request);     }

        @ResponseStatus(HttpStatus.BAD_REQUEST)     @ExceptionHandler(MethodArgumentNotValidException.class)     public DefaultErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {         return super.handleMethodArgumentNotValidException(e, request);     }

        /* 处理自定义异常 */     @ExceptionHandler(BusinessException.class)     public ResponseEntity<DefaultErrorResult> handleBusinessException(BusinessException e, HttpServletRequest request) {         return super.handleBusinessException(e, request);     }

        /** 处理运行时异常 */     @Override     @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)     @ExceptionHandler(Throwable.class)     public DefaultErrorResult handleThrowable(Throwable e, HttpServletRequest request) {         //TODO 可通过邮件、微信公众号等方式发送信息至开发人员、记录存档等操作         return super.handleThrowable(e, request);     }      }

    1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071 BaseGlobalExceptionHandler 全局异常处理基础类 package com.zhuma.demo.comm.handler;

    import java.util.List;

    import javax.servlet.http.HttpServletRequest; import javax.validation.ConstraintViolationException;

    import com.zhuma.demo.comm.result.DefaultErrorResult; import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.validation.BindException; import org.springframework.web.bind.MethodArgumentNotValidException;

    import com.zhuma.demo.comm.result.ParameterInvalidItem; import com.zhuma.demo.enums.ResultCode; import com.zhuma.demo.exception.BusinessException; import com.zhuma.demo.util.ConvertUtil;

    /**  * @desc 全局异常处理基础类  *   * @author zhumaer  * @since 10/10/2017 9:54 AM  */ @Slf4j public class BaseGlobalExceptionHandler {

        /**      * 违反约束异常      */     protected DefaultErrorResult handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {         log.info("handleConstraintViolationException start, uri:{}, caused by: ", request.getRequestURI(), e);         List<ParameterInvalidItem> parameterInvalidItemList = ConvertUtil.convertCVSetToParameterInvalidItemList(e.getConstraintViolations());         return DefaultErrorResult.failure(ResultCode.PARAM_IS_INVALID, e, HttpStatus.BAD_REQUEST, parameterInvalidItemList);     }

        /**      * 处理验证参数封装错误时异常      */     protected DefaultErrorResult handleConstraintViolationException(HttpMessageNotReadableException e, HttpServletRequest request) {         log.info("handleConstraintViolationException start, uri:{}, caused by: ", request.getRequestURI(), e);         return DefaultErrorResult.failure(ResultCode.PARAM_IS_INVALID, e, HttpStatus.BAD_REQUEST);     }

        /**      * 处理参数绑定时异常(反400错误码)      */     protected DefaultErrorResult handleBindException(BindException e, HttpServletRequest request) {         log.info("handleBindException start, uri:{}, caused by: ", request.getRequestURI(), e);         List<ParameterInvalidItem> parameterInvalidItemList = ConvertUtil.convertBindingResultToMapParameterInvalidItemList(e.getBindingResult());         return DefaultErrorResult.failure(ResultCode.PARAM_IS_INVALID, e, HttpStatus.BAD_REQUEST, parameterInvalidItemList);     }

        /**      * 处理使用@Validated注解时,参数验证错误异常(反400错误码)      */     protected DefaultErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {         log.info("handleMethodArgumentNotValidException start, uri:{}, caused by: ", request.getRequestURI(), e);         List<ParameterInvalidItem> parameterInvalidItemList = ConvertUtil.convertBindingResultToMapParameterInvalidItemList(e.getBindingResult());         return DefaultErrorResult.failure(ResultCode.PARAM_IS_INVALID, e, HttpStatus.BAD_REQUEST, parameterInvalidItemList);     }

        /**      * 处理通用自定义业务异常      */     protected ResponseEntity<DefaultErrorResult> handleBusinessException(BusinessException e, HttpServletRequest request) {         log.info("handleBusinessException start, uri:{}, exception:{}, caused by: {}", request.getRequestURI(), e.getClass(), e.getMessage());

            DefaultErrorResult defaultErrorResult = DefaultErrorResult.failure(e);         return ResponseEntity                 .status(HttpStatus.valueOf(defaultErrorResult.getStatus()))                 .body(defaultErrorResult);     }

        /**      * 处理运行时系统异常(反500错误码)      */     protected DefaultErrorResult handleRuntimeException(RuntimeException e, HttpServletRequest request) {         log.error("handleRuntimeException start, uri:{}, caused by: ", request.getRequestURI(), e);         return DefaultErrorResult.failure(ResultCode.SYSTEM_INNER_ERROR, e, HttpStatus.INTERNAL_SERVER_ERROR);     }

    }

    12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788 说明 上面用到了一些自定义异常基类(BusinessException),对于这个类它会有很多子类去继承它,例如: 参数异常、数据已存在异常、无权限异常等等,这些类我们会在后面的文章给大家分享下 5. ResponseResult 注解类 package com.zhuma.demo.annotation;

    import com.zhuma.demo.comm.result.PlatformResult; import com.zhuma.demo.comm.result.Result;

    import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target;

    /**  * @desc 接口返回结果增强  会通过拦截器拦截后放入标记,在ResponseResultHandler 进行结果处理  *  * @author zhumaer  * @since 4/1/2018 3:00 PM  */ @Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface ResponseResult {

        Class<? extends Result>  value() default PlatformResult.class;

    }

    1234567891011121314151617181920212223242526 说明 这里我们默认PlatformResult使用这个类作为返回格式,所以@ResponseResult 等价于@ResponseResult(PlatformResult.class) 6. ResponseResultInterceptor 拦截器 package com.zhuma.demo.interceptor;

    import com.zhuma.demo.annotation.ResponseResult; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView;

    import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.lang.reflect.Method;

    /**  * @desc 接口响应体控制拦截器  *   * @author zhumaer  * @since 4/1/2018 3:00 PM  */ @Component public class ResponseResultInterceptor implements HandlerInterceptor {

        public static final String RESPONSE_RESULT = "RESPONSE-RESULT";

        @Override     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {         if (handler instanceof HandlerMethod) {             final HandlerMethod handlerMethod = (HandlerMethod) handler;             final Class<?> clazz = handlerMethod.getBeanType();             final Method method = handlerMethod.getMethod();             if (clazz.isAnnotationPresent(ResponseResult.class)) {                 request.setAttribute(RESPONSE_RESULT, clazz.getAnnotation(ResponseResult.class));             } else if (method.isAnnotationPresent(ResponseResult.class)) {                 request.setAttribute(RESPONSE_RESULT, method.getAnnotation(ResponseResult.class));             }         }

            return true;     }

        @Override     public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {         // nothing to do     }

        @Override     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {         // nothing to do     }

    } 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051 开启拦截器 package com.zhuma.demo.config.web;

    import com.zhuma.demo.interceptor.ResponseResultInterceptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

    @Configuration public class InterceptorConfig extends WebMvcConfigurerAdapter {

        @Autowired     private ResponseResultInterceptor responseResultInterceptor;

        @Override     public void addInterceptors(InterceptorRegistry registry) {         String apiUri = "/**";         //响应结果控制拦截         registry.addInterceptor(responseResultInterceptor).addPathPatterns(apiUri);     }

    }

    1234567891011121314151617181920212223 7. ResponseResultHandler 响应体格式处理器 package com.zhuma.demo.handler;

    import com.zhuma.demo.annotation.ResponseResult; import com.zhuma.demo.comm.result.DefaultErrorResult; import com.zhuma.demo.comm.result.Result; import com.zhuma.demo.interceptor.ResponseResultInterceptor; import com.zhuma.demo.util.RequestContextHolderUtil; import org.springframework.core.MethodParameter; import org.springframework.http.MediaType; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.server.ServerHttpRequest; import org.springframework.http.server.ServerHttpResponse; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

    import com.zhuma.demo.comm.result.PlatformResult;

    /**  * @desc 接口响应体处理器  *   * @author zhumaer  * @since 4/1/2018 3:00 PM  */ @ControllerAdvice public class ResponseResultHandler implements ResponseBodyAdvice<Object> {

        @Override     public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {         HttpServletRequest request = RequestContextUtil.getRequest();         ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(ResponseResultInterceptor.RESPONSE_RESULT);         return responseResultAnn != null && !ApiStyleEnum.NONE.name().equalsIgnoreCase(request.getHeader(HeaderConstants.API_STYLE));     }

        @Override     public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {         ResponseResult responseResultAnn = (ResponseResult) RequestContextHolderUtil.getRequest().getAttribute(ResponseResultInterceptor.RESPONSE_RESULT);

            Class<? extends Result> resultClazz = responseResultAnn.value();

            if (resultClazz.isAssignableFrom(PlatformResult.class)) {             if (body instanceof DefaultErrorResult) {                 DefaultErrorResult defaultErrorResult = (DefaultErrorResult) body;                 return PlatformResult.builder()                         .code(Integer.valueOf(defaultErrorResult.getCode()))                         .msg(defaultErrorResult.getMessage())                         .data(defaultErrorResult.getErrors())                         .build();             } else if (body instanceof String) {                 return JsonUtil.object2Json(PlatformResult.success(body));             }

                return PlatformResult.success(body);         }

            return body;     }

    }

    1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859 说明

    上述代码用到了一个工具类(RequestContextHolderUtil)用于获取request对象,如果感兴趣可以看下这篇文章 工具类分享之《RequestContextHolderUtil》 @ControllerAdvice、ResponseBodyAdvice这两个类是本功能的关键使用类,用于接口的响应体增强,其中supports方法用于判断是否需要做增强转化,beforeBodyWrite方法用于增加逻辑实现 supports方法中,加了个小功能,当调用人员不想要封装结果时,可以在header上设置参数Api-Style=none

    最后我们写一个创建用户的Controller层方法: package com.zhuma.demo.web.user;

    import java.util.Date;

    import com.zhuma.demo.annotation.ResponseResult; import com.zhuma.demo.comm.result.DefaultErrorResult; import com.zhuma.demo.comm.result.PlatformResult; import com.zhuma.demo.comm.result.Result; import com.zhuma.demo.exception.BusinessException; import com.zhuma.demo.exception.UserNotLoginException; import org.springframework.http.HttpStatus; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController;

    import com.zhuma.demo.model.po.User;

    import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse;

    /**  * @desc 用户管理控制器  *   * @author zhumaer  * @since 6/20/2017 16:37 PM  */ @ResponseResult @RestController @RequestMapping("/users") public class UserController {

        @PostMapping     @ResponseStatus(HttpStatus.CREATED)     public User addUser(@Validated @RequestBody User user) {         user.setId(10000L);         user.setCreateTime(new Date());         return user;     }

    } 12345678910111213141516171819202122232425262728293031323334353637383940414243 结束语 接口响应体统一格式转化,这个功能解说完啦,因为本次贴出来的基本算是完整的代码,如果你能看完也真心不容易,哈哈,哪里不懂的可以直接留言或关注微信公众号,来和我一起讨论吧O(∩_∩)O~ 源码github地址:https://github.com/zhumaer/zhuma ---------------------  作者:筑码-井哥  来源:  原文:https://blog.csdn.net/aiyaya_/article/details/78976759  版权声明:本文为博主原创文章,转载请附上博文链接!

    最新回复(0)