Spring全局异常处理的使用及源码分析

又是美好的一天呀~
个人博客地址: huanghong.top

本文预估阅读时长为20分钟左右~

ExceptionHanlder的使用

handler定义

package com.huang.handler;

import com.huang.controller.demo1.DemoController;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @Time 2023-02-28 17:25
 * Created by Huang
 * className: GlobalExceptionHandler
 * Description:
 */
//@RestControllerAdvice
//@RestControllerAdvice(basePackages = "com.huang.controller.demo1")
@RestControllerAdvice(basePackageClasses = DemoController.class)
public class GlobalExceptionHandler {

    @ExceptionHandler(value = RuntimeException.class)
    public String runtimeExceptionHandler(HttpServletRequest request, RuntimeException exception) {
        return exception.getMessage();
    }

    @ExceptionHandler(value = IOException.class)
    public String IOExceptionHandler(HttpServletRequest request, IOException exception) {
        return exception.getMessage();
    }

    @ExceptionHandler(value = Exception.class)
    public String exceptionHandler(HttpServletRequest request, Exception exception) {
        return exception.getMessage();
    }

    @ExceptionHandler(value = NullPointerException.class)
    public String nullPointerExceptionHandler(HttpServletRequest request, NullPointerException exception) {
        return exception.getMessage();
    }
}
  1. @RestControllerAdvice是一个组合注解,由@ControllerAdvice和@ResponseBody组成,相同于**@RestController@Controller之间的区别,增加@ResponseBody**注解则返回的结果直接写入 HTTP response body 中,反之直接返回view。
  2. 通过指定@RestControllerAdvice中的basePackagesbasePackageClasses的属性可以扫描指定包路径下或类的异常。

controller定义

package com.huang.controller.demo1;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;

/**
 * @Time 2023-02-28 17:20
 * Created by Huang
 * className: DemoController
 * Description:
 */
@RestController("demo1")
@RequestMapping
public class DemoController {

    @GetMapping("/demo1/normal")
    public String normal(){
        return "normal";
    }

    @GetMapping("/demo1/NullPointerException")
    public String NullPointerException(){
        throw new NullPointerException("demo1 NullPointerException");
    }

    @GetMapping("/demo1/RuntimeException")
    public String RuntimeException(){
        throw new RuntimeException("demo1 RuntimeException");
    }

    @GetMapping("/demo1/IOException")
    public String IOException() throws IOException {
        throw new IOException("demo1 IOException");
    }
}

package com.huang.controller.demo2;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;

/**
 * @Time 2023-02-28 17:20
 * Created by Huang
 * className: DemoController
 * Description:
 */
@RestController("demo2")
@RequestMapping
public class DemoController {

    @GetMapping("/demo2/normal")
    public String normal(){
        return "normal";
    }

    @GetMapping("/demo2/NullPointerException")
    public String NullPointerException(){
        throw new NullPointerException("demo2 NullPointerException");
    }

    @GetMapping("/demo2/RuntimeException")
    public String RuntimeException(){
        throw new RuntimeException("demo2 RuntimeException");
    }

    @GetMapping("/demo2/IOException")
    public String IOException() throws IOException {
        throw new IOException("demo2 IOException");
    }
}

结论

  1. 指定basePackages则只扫描处理对应路径下的异常内容。
  2. 指定basePackageClasses则只扫描处理对应类中的异常内容。
  3. 异常捕获由最下级异常至最高级,比如抛出NullPointerException:handler处理顺序为nullPointerExceptionHandler->runtimeExceptionHandler->exceptionHandler。
  4. 单个ExceptionHandler可指定多个异常,比如**@ExceptionHandler(value = {NullPointerException.class,IOException.class})**

全局异常处理源码分析

异常处理配置

方式一:配置默认视图
package com.huang.handler;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;

import java.util.Properties;

/**
 * @Time 2023-02-28 21:22
 * Created by Huang
 * className: GlobalExceptionViewHandler
 * Description:
 */
@Configuration
public class GlobalExceptionViewHandler {
    @Bean
    public SimpleMappingExceptionResolver getSimpleMappingExceptionResolver(){
        SimpleMappingExceptionResolver simpleMappingExceptionResolver = new SimpleMappingExceptionResolver();
        simpleMappingExceptionResolver.setDefaultErrorView("defaultErrorView");
        Properties properties = new Properties();
        properties.setProperty("java.lang.Exception", "ExceptionErrorView");
        properties.setProperty("java.io.IOException", "IOExceptionErrorView");
        properties.setProperty("java.lang.NullPointerException", "NullPointerExceptionErrorView");
        properties.setProperty("java.lang.RuntimeException", "RuntimeExceptionErrorView");
        properties.setProperty("java.lang.IllegalArgumentException", "IllegalArgumentExceptionErrorView");
        simpleMappingExceptionResolver.setExceptionMappings(properties);
        return simpleMappingExceptionResolver;
    }
}
方式二:配置ControllerAdvice
package com.huang.handler;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * @Time 2023-02-28 17:25
 * Created by Huang
 * className: GlobalExceptionHandler
 * Description:
 */
@RestControllerAdvice
//@RestControllerAdvice(basePackages = "com.huang.controller.demo1")
//@RestControllerAdvice(basePackageClasses = DemoController.class)
public class GlobalExceptionHandler {

    @ExceptionHandler(value = RuntimeException.class)
    public String exceptionHandler(HttpServletRequest request, RuntimeException exception) {
        return exception.getMessage();
    }

    @ExceptionHandler(value = IOException.class)
    public String exceptionHandler(HttpServletRequest request, IOException exception) {
        return exception.getMessage();
    }

    @ExceptionHandler(value = Exception.class)
    public String exceptionHandler(HttpServletRequest request, Exception exception) {
        return exception.getMessage();
    }

    @ExceptionHandler(value = NullPointerException.class)
    public String exceptionHandler(HttpServletRequest request, NullPointerException exception) {
        return exception.getMessage();
    }
}

注: 如果两者同时配置,则在请求访问的过程中,DispatcherServlet进行初始化执行initStrategies方法,在执行initHandlerExceptionResolvers中会获取所有异常解析器HandlerExceptionResolver然后加以排序操作**(AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers))**

以上述代码为例,根据ExceptionResolvers的PriorityOrdered、Ordered来进行排序

image-20230228222714501

  1. org.springframework.boot.web.servlet.error.DefaultErrorAttributes(@Order(Ordered.HIGHEST_PRECEDENCE))默认处理返回null。

    public ModelAndView resolveException(HttpServletRequest request,
          HttpServletResponse response, Object handler, Exception ex) {
       storeErrorAttributes(request, ex);
       return null;
    }
    
    private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
        request.setAttribute(ERROR_ATTRIBUTE, ex);
    }
    
  2. org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver(Ordered.LOWEST_PRECEDENCE)子类异常视图处理器org.springframework.web.servlet.handler.SimpleMappingExceptionResolver

  3. HandlerExceptionResolverComposite(Ordered=0)中的ExceptionHandlerExceptionResolver在初始化会解析ControllerAdvice中的异常处理方案,存在对应处理NullPointerException的处理器则进行处理返回,直接跳过GlobalExceptionViewHandler处理器。

image-20230228223048494

//org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
@Bean
public HandlerExceptionResolver handlerExceptionResolver(
    @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
    List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
    configureHandlerExceptionResolvers(exceptionResolvers);
    if (exceptionResolvers.isEmpty()) {
        addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
    }
    extendHandlerExceptionResolvers(exceptionResolvers);
    HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
    //设置Order=0
    composite.setOrder(0);
    composite.setExceptionResolvers(exceptionResolvers);
    return composite;
}

总结:异常解析处理器执行顺序DefaultErrorAttributes->HandlerExceptionResolverComposite->SimpleMappingExceptionResolver,同时配置异常视图处理器和ControllerAdvice,ControllerAdvice会优先处理异常。

构建HandlerExceptionResolverComposite

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport

@Bean
public HandlerExceptionResolver handlerExceptionResolver(
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
   List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
   //自动注入WebMvcConfigurerComposite中扩展resolvers,不存在
   configureHandlerExceptionResolvers(exceptionResolvers);
   if (exceptionResolvers.isEmpty()) {
      //添加异常解析器
      addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
   }
   extendHandlerExceptionResolvers(exceptionResolvers);
   HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
   //order设置为0
   composite.setOrder(0);
   //设置异常解析器
   composite.setExceptionResolvers(exceptionResolvers);
   return composite;
}
addDefaultHandlerExceptionResolvers

org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport#addDefaultHandlerExceptionResolvers

protected final void addDefaultHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers,
      ContentNegotiationManager mvcContentNegotiationManager) {
   //创建org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver实例
   ExceptionHandlerExceptionResolver exceptionHandlerResolver = createExceptionHandlerExceptionResolver();
   exceptionHandlerResolver.setContentNegotiationManager(mvcContentNegotiationManager);
   exceptionHandlerResolver.setMessageConverters(getMessageConverters());
   exceptionHandlerResolver.setCustomArgumentResolvers(getArgumentResolvers());
   exceptionHandlerResolver.setCustomReturnValueHandlers(getReturnValueHandlers());
   if (jackson2Present) {
      exceptionHandlerResolver.setResponseBodyAdvice(
            Collections.singletonList(new JsonViewResponseBodyAdvice()));
   }
   if (this.applicationContext != null) {
      exceptionHandlerResolver.setApplicationContext(this.applicationContext);
   }
   //执行afterPropertiesSet
   exceptionHandlerResolver.afterPropertiesSet();
   exceptionResolvers.add(exceptionHandlerResolver);
   //添加ResponseStatusExceptionResolver
   ResponseStatusExceptionResolver responseStatusResolver = new ResponseStatusExceptionResolver();
   responseStatusResolver.setMessageSource(this.applicationContext);
   exceptionResolvers.add(responseStatusResolver);
   //添加DefaultHandlerExceptionResolver
   exceptionResolvers.add(new DefaultHandlerExceptionResolver());
}
//小结:HandlerExceptionResolverComposite中ExceptionResolvers包含上述三个实例(ExceptionHandlerExceptionResolver/ResponseStatusExceptionResolver/DefaultHandlerExceptionResolver)
initExceptionHandlerAdviceCache

org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#initExceptionHandlerAdviceCache

private void initExceptionHandlerAdviceCache() {
   if (getApplicationContext() == null) {
      return;
   }
   //获取Spring容器中所有ControllerAdvice Bean
   List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
   for (ControllerAdviceBean adviceBean : adviceBeans) {
      Class<?> beanType = adviceBean.getBeanType();
      if (beanType == null) {
         throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
      }
      //构建ExceptionHandlerMethodResolver对象,解析ControllerAdvice中的ExceptionHandler
      ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
      if (resolver.hasExceptionMappings()) {
         //如果存在@ExceptionHandler,放入本地缓存中
         this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
      }
      // 如果该 beanType 类型是 ResponseBodyAdvice 子类,则添加到 responseBodyAdvice 中
      if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
         this.responseBodyAdvice.add(adviceBean);
      }
   }
}

//ExceptionHandlerMethodResolver初始化
//mappedMethods key:异常类型 value: ControllerAdvice中的ExceptionHandler方法
private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);

public ExceptionHandlerMethodResolver(Class<?> handlerType) {
    //遍历@ExceptionHandler的方法
    for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
        //遍历处理的异常集合
        for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
            //添加到mappedMethods中
            addExceptionMapping(exceptionType, method);
        }
    }
}

DispatcherServlet初始化

//org.springframework.web.servlet.DispatcherServlet
protected void onRefresh(ApplicationContext context) {
   initStrategies(context);
}

protected void initStrategies(ApplicationContext context) {
    ...
    initHandlerExceptionResolvers(context);
    ...
}

private void initHandlerExceptionResolvers(ApplicationContext context) {
    this.handlerExceptionResolvers = null;
	//默认检测所有异常解析器
    if (this.detectAllHandlerExceptionResolvers) {
        //查询上下文所有异常解析器
        Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
            .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
        //解析器不为空,根据ExceptionResolvers的PriorityOrdered、Ordered来进行排序
        if (!matchingBeans.isEmpty()) {
            this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
            // We keep HandlerExceptionResolvers in sorted order.
            AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
        }
    }
    else {
        try {
            HandlerExceptionResolver her =
                context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
            this.handlerExceptionResolvers = Collections.singletonList(her);
        }
        catch (NoSuchBeanDefinitionException ex) {
            // Ignore, no HandlerExceptionResolver is fine too.
        }
    }

    // Ensure we have at least some HandlerExceptionResolvers, by registering
    // default HandlerExceptionResolvers if no other resolvers are found.
    if (this.handlerExceptionResolvers == null) {
        this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
        if (logger.isTraceEnabled()) {
            logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
                         "': using default strategies from DispatcherServlet.properties");
        }
    }
}

Spring容器自动注入异常解析器

//org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
   return new DefaultErrorAttributes();
}

//org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
@Bean
public HandlerExceptionResolver handlerExceptionResolver(
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
   List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
   //自动注入WebMvcConfigurerComposite中扩展resolvers,不存在
   configureHandlerExceptionResolvers(exceptionResolvers);
   if (exceptionResolvers.isEmpty()) {
      //添加异常解析器
      addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
   }
   extendHandlerExceptionResolvers(exceptionResolvers);
   HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
   //order设置为0
   composite.setOrder(0);
   //设置异常解析器
   composite.setExceptionResolvers(exceptionResolvers);
   return composite;
}
processDispatchResult

org.springframework.web.servlet.DispatcherServlet#processDispatchResult

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
      @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
      @Nullable Exception exception) throws Exception {
   boolean errorView = false;
   if (exception != null) {
      //如果是ModelAndViewDefiningException,则获取对应的ModelAndView
      if (exception instanceof ModelAndViewDefiningException) {
         logger.debug("ModelAndViewDefiningException encountered", exception);
         mv = ((ModelAndViewDefiningException) exception).getModelAndView();
      }
      else {
         //获取对应mappedHandler进行异常处理
         Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
         //mv不为空则返回错误页面
         mv = processHandlerException(request, response, handler, exception);
         errorView = (mv != null);
      }
   }

   // Did the handler return a view to render?
   if (mv != null && !mv.wasCleared()) {
      //视图数据渲染
      render(mv, request, response);
      if (errorView) {
         WebUtils.clearErrorRequestAttributes(request);
      }
   }
   else {
      if (logger.isTraceEnabled()) {
         logger.trace("No view rendering, null ModelAndView returned.");
      }
   }

   if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
      // Concurrent handling started during a forward
      return;
   }

   if (mappedHandler != null) {
      // Exception (if any) is already handled..
      //异常处理完成后执行HandlerInterceptor的afterCompletion方法
      mappedHandler.triggerAfterCompletion(request, response, null);
   }
}
processHandlerException

org.springframework.web.servlet.DispatcherServlet#processHandlerException

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
      @Nullable Object handler, Exception ex) throws Exception {

   // Success and error responses may use different content types
   request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

   // Check registered HandlerExceptionResolvers...
   ModelAndView exMv = null;
   if (this.handlerExceptionResolvers != null) {
      //遍历handlerExceptionResolvers,使用逐个handlerExceptionResolver处理异常,如果exMv不为空则跳过后续处理
      for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
         //DefaultErrorAttributes 处理返回null
         //HandlerExceptionResolver 自主定义ControllerAdvice
         //如果仍定义了SimpleMappingExceptionResolver则会被跳过
         exMv = resolver.resolveException(request, response, handler, ex);
         if (exMv != null) {
            break;
         }
      }
   }
   if (exMv != null) {
      if (exMv.isEmpty()) {
         request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
         return null;
      }
      // We might still need view name translation for a plain error model...
      if (!exMv.hasView()) {
         String defaultViewName = getDefaultViewName(request);
         if (defaultViewName != null) {
            exMv.setViewName(defaultViewName);
         }
      }
      if (logger.isTraceEnabled()) {
         logger.trace("Using resolved error view: " + exMv, ex);
      }
      else if (logger.isDebugEnabled()) {
         logger.debug("Using resolved error view: " + exMv);
      }
      WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
      return exMv;
   }

   throw ex;
}
resolveException

ControllerAdvice对应的异常处理逻辑

org.springframework.web.servlet.handler.AbstractHandlerMethodExceptionResolver#doResolveException

protected final ModelAndView doResolveException(
      HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
   //对应controller
   HandlerMethod handlerMethod = (handler instanceof HandlerMethod ? (HandlerMethod) handler : null);
   return doResolveHandlerMethodException(request, response, handlerMethod, ex);
}
doResolveHandlerMethodException

org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver#doResolveHandlerMethodException

protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
      HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
   //根据controller和异常类型匹配到对应的ExceptionHandler方法
   ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
   if (exceptionHandlerMethod == null) {
      return null;
   }

   if (this.argumentResolvers != null) {
      //设置参数解析器
      exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
   }
   if (this.returnValueHandlers != null) {
      //设置返回值处理器
      exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
   }
   //根据原生request和response构建一个新的webRequest
   ServletWebRequest webRequest = new ServletWebRequest(request, response);
   ModelAndViewContainer mavContainer = new ModelAndViewContainer();

   ArrayList<Throwable> exceptions = new ArrayList<>();
   try {
      if (logger.isDebugEnabled()) {
         logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod);
      }
      // Expose causes as provided arguments as well
      Throwable exToExpose = exception;
      while (exToExpose != null) {
         exceptions.add(exToExpose);
         Throwable cause = exToExpose.getCause();
         exToExpose = (cause != exToExpose ? cause : null);
      }
      Object[] arguments = new Object[exceptions.size() + 1];
      //构建参数 exceptions + handlerMethod
      exceptions.toArray(arguments);  // efficient arraycopy call in ArrayList
      arguments[arguments.length - 1] = handlerMethod;
      //通过ExceptionHandler的异常处理逻辑来处理异常
      exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments);
   }
   catch (Throwable invocationEx) {
      // Any other than the original exception (or a cause) is unintended here,
      // probably an accident (e.g. failed assertion or the like).
      if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) {
         logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx);
      }
      // Continue with default processing of the original exception...
      return null;
   }
   //通过RequestResponseBodyMethodProcessor中handleReturnValue的方法设置mavContainer.setRequestHandled(true);
   //直接返回new ModelAndView();
   if (mavContainer.isRequestHandled()) {
      return new ModelAndView();
   }
   else {
      ModelMap model = mavContainer.getModel();
      HttpStatus status = mavContainer.getStatus();
      ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status);
      mav.setViewName(mavContainer.getViewName());
      if (!mavContainer.isViewReference()) {
         mav.setView((View) mavContainer.getView());
      }
      if (model instanceof RedirectAttributes) {
         Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes();
         RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
      }
      return mav;
   }
}

感谢阅读完本篇文章!!!
个人博客地址: huanghong.top

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

喜欢正常冰的冰美式

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值