SpringMVC-原理

1. MVC

1.1 什么是MVC

MVC分别是:模型Model(javabean)、视图View(jsp)、控制器Controller(servlet)。

模型(Model):负责数据逻辑的处理和实现数据操作。

视图(View):负责格式化数据并把它们呈现给用户,包括数据展示、用户交互、数据验证、页面设计等。

控制器(Controller):负责接收转发请求,对请求进行处理后,指定试图并将响应结果发送给客户端。

MVC 框架提供了模型-视图-控制的体系结构和可以用来开发灵活、松散耦合的 web 应用程序的组件。MVC 模式导致了应用程序的不同方面(输入逻辑、业务逻辑和 UI 逻辑)的分离,同时提供了在这些元素之间的松散耦合。

1.2 JSP+JavaBean

在以往,我们用到的是JSP+JavaBean设计模式,适合一些业务流程比较简单的Web程序。

  • JSP(视图):用于处理用户请求
  • JavaBean(模型):用于封装和处理数据
image-20211012185124472

工作流程: 浏览器端发出request请求,JSP从请求中获取需要的数据,交由JavaBean进行业务处理,JavaBean通过与数据库的交互获取相关返回值。JavaBean将结果以response形式返还给JSP,最终解析在浏览器。

JSP+JavaBean弊端:

  1. JSP与JavaBean间严重耦合,维护困难
  2. JSP责任太多,结构较为混乱,无法实现业务流程复杂的应用

1.3 Servlet+JSP+JavaBean

而现在,我们用到的是Servlet+JSP+JavaBean设计模式,也就是MVC模式。

  • Servlet(Controller):处理用户请求
  • JSP(View):数据显示
  • JavaBean(Model):数据封装
image-20211012192029940

该模式将控制层单独划分出来负责业务流程的控制,接收request请求,实例化所需要的JavaBean,并将处理后的数据以response返回给JSP惊醒页面数据展示。

1.4 MVC优缺点

优点:

  1. 多视图可共享同一个模型,提高代码的重用性
  2. 业务划分清晰,MVC模块相互独立,松耦合框架
  3. 易于维护,易于扩展大型复杂的web应用

缺点:

  1. 提高了系统的负责度
  2. 试图对模型数据的低效率访问

2. Spring MVC

2.1 什么是Spring MVC

Spring MVC是Spring Framework的一部分,是基于Java实现MVC的轻量级Web框架,本质上是Servlet。

Spring MVC是Servlet+JSP+JavaBean的实现,与Spring无缝对接,是如今主流的Web开发框架。

Spring MVC框架曹勇松耦合可拔插的组件结构,具有高度可配置性,相对其其他的框架更具扩展性和灵活性。

2.2 Spring MVC 特点

  1. 角色划分清晰,Model、View、Controller
  2. 配置灵活,可把类当作Bean通过xml配置
  3. 与Spring无缝对接

2.3 DispatcherServlet

DispatcherServlet(前置控制器):Spring MVC框架是围绕DispatcherServlet设计的,这种Servlet用来处理所有的HTTP请求和响应,支持配置处理器映射、试图渲染、本地化与文件上传等功能。

  • 通过 HandlerMapping(处理器映射器)将请求映射到处理器(返回一个HandlerExecutionChain,它包括一个处理器、多个HandlerInterceptor拦截器)。
  • 通过 HandlerAdapter(适配器) 支持多种类型的处理器(HandlerExecutionChain中的处理器)。
  • 通过 ViewResolver(视图解析器) 解析逻辑视图名到具体视图实现。
image-20211012202832724

2.4 核心组件

DispatcherServlet通过与Spring MVC核心组件的相互协作,完成了请求和响应的处理。暂且就叫核心组件吧,但是在官网上定义为 Special Bean Types,因为在程序中我们需要以bean的方式配置它们。

🌈 处理流程相当繁琐,不如先搞懂每一各组件的功能,工作流程放后面再说。

2.4.1 HandlerMapping

2.4.1.1 HandlerMapping功能

🌟 功能:根据请求匹配到对应的 Handler,然后将找到的 Handler 和所有匹配的 HandlerInterceptor(拦截器)绑定到创建的 HandlerExecutionChain 对象上并返回。

🌟 返回值:Handler

image-20211012221742391
2.4.1.2 HandlerMapping接口

该接口内容如下,其中主要包含getHandler抽象方法,该方法最终返回了一个HandlerExecutionChain实例。

image-20211012223459229
2.4.1.3 HandlerMapping实现类

该接口的实现类如下(选中的是下文中我们要用到的):

image-20211012222600069

可以看到,大致上分为两大类 AbstractUrlHandlerMappingAbstractHandlerMethodMapping,且都继承自 AbstractHandlerMapping 抽象类。

⚠️ 注意:不同的映射处理器(HandlerMapping) 映射出来的 handler 对象是不一样的,AbstractUrlHandlerMapping 映射器映射出来的是 handlerController 对象,AbstractHandlerMethodMapping 映射器映射出来的 handlerHandlerMethod 对象。

2.4.2 HandlerExecutionChain

2.4.2.1 HandlerExecutionChain功能

HandlerExecutionChainHanderMapping关系非常紧密,HandlerExecutionChain只能通过HanderMapping接口中的唯一方法来获得。

🌟 功能:处理程序执行链,由处理程序对象和任何处理程序拦截器组成。根据url查找控制器,下文中url被查找控制器为:hello

🌟组成:实例封装了一个handler处理对象和一些interceptors

当用户请求到到DispaterServlet中后,配置的HandlerMapping会根据用户请求(也就是handler)会将它与所有的interceptors封装为HandlerExecutionChain对象。

2.4.2.2 HandlerExecutionChain类

在该类中是这样定义HandlerExecutionChain的:

Handler execution chain, consisting of handler object and any handler interceptors. Returned by HandlerMapping’s HandlerMapping.getHandler method.

该类的内容如下:

image-20211012225835674
2.4.2.3 HandlerExecutionChain实例

HandlerExecutionChain实例的获取写在AbstractHandlerMapping类中,由下图的getHandlerExecutionChain方法获取该实例。

image-20211012231719874

getHandlerExecutionChain方法如下:

image-20211012232559501

2.4.3 HandlerAdapter

在上文中,提到HandlerMapping不同实现类映射的handler不同,各种处理器中的处理方法各不相同,Spring为了解决适应多种处理器,定义了处理器适配器的概念,也就是我们所说的HandlerAdapter

在Spring MVC中可以支持多种处理器(处理器也就是处理用户请求的程序)。

Spring实现的处理器类型有Servlet、Controller、HttpRequestHandler以及注解类型的处理器。

2.4.3.1 HandlerAdapter功能

🌟 功能:帮助DispatcherServlet调用映射到请求的处理程序,而不管处理程序实际是如何调用的(规则要求)。 通过HandlerAdapter对处理器进行执行。

🌟 返回值:返回一个ModelAndView对象(包含模型数据、逻辑视图名);

image-20211013100402263
2.4.3.2 HandlerAdapter接口

HandlerAdapter接口的内容如下:

public interface HandlerAdapter {

    boolean supports(Object handler);

    @Nullable
    ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;

    @Deprecated
    long getLastModified(HttpServletRequest request, Object handler);

}

🌟 handle方法:使用给定的处理程序来处理此请求,返回ModelAndView。

2.4.3.3 HandlerAdapter实现类

HandlerAdapter接口的实现类如下:

image-20211013000033046

  • SimpleServletHandlerAdapter 适配Servlet处理器
  • HttpRerquestHandlerAdapter 适配HttpRequestHandler处理器
  • RequestMappingHandlerAdapter 适配注解处理器
  • SimpleControllerHandlerAdapter 适配Controller处理器(非默认使用)

在下文中,我们用到的是第四种,来具体看下SimpleControllerHandlerAdapter 实现类里面的内容:

@Override
@Nullable
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
    throws Exception {

    return ((Controller) handler).handleRequest(request, response);
}

🌟handle方法:该实现类实现了接口的方法,同样返回ModelAndView

⚠️ 注意:该方法的返回值实际上是Controller执行的handler方法!!

2.4.4 Controller

Controller接口被显式设计为操作HttpServletRequest和HttpServletResponse对象。

🌈 就像HttpServlet一样。

2.4.4.1 Controller功能

🌟 功能:具体实现handler方法,

🌟 返回值:返回一个ModelAndView对象(包含模型数据、逻辑视图名);

2.4.4.2 Controller接口

Controller接口基本没有上面内容(也没找到实现类),只有一个handleRequest方法:

public interface Controller {
    @Nullable
    ModelAndView handleRequest(HttpaServletRequest request, HttpServletResponse response) throws Exception;

}

🌟 handleRequest方法:处理请求并返回一个ModelAndView对象,DispatcherServlet将呈现该对象。

⚠️ 注意:返回值为null不是错误,它表明该对象本身完成了请求处理,因此没有ModelAndView来呈现。

2.4.4.3 Controller实现类

Controller接口的实现类如下(最下面选中的是下文中自己写的实现类):

image-20211013095808045

2.4.4.4 ModelAndView对象

ModelAndView对象中有两个主要的方法:

public class ModelAndView {
    /**
     * 向模型添加一个属性。
     * @param attributeName:要添加到模型中的对象的名称(永远不要为空)
     * @param attributeValue:添加到模型中的对象(可以为空)
     * @return
     */
    public ModelAndView addObject(String attributeName, @Nullable Object attributeValue)
    {
        getModelMap().addAttribute(attributeName, attributeValue);
        return this;
    }

    /**
     * 为这个ModelAndView设置一个视图名,由DispatcherServlet通过ViewResolver解析。将覆盖任何已存在的视图名或视图。
     * @viewName:视图名(具体的jsp文件名,去除前缀后缀)
     */
    public void setViewName(@Nullable String viewName) {
        this.view = viewName;
    }
}

🌈 addObject可以看作一个session,就像之前定义session.setAttribute那样,name、value就对应session的key-value。

2.4.5 ViewResolver

2.4.5.1 ViewResolver功能

🌟 功能:解析DispatcherServlet传递的逻辑视图名,解析为具体的View,并将解析结果传回给DispatcherServlet。

image-20211013100559610

2.4.5.2 ViewResolver接口

ViewResolver接口中只有一个返回视图名的resolveViewName抽象方法。

public interface ViewResolver {
    @Nullable
    View resolveViewName(String viewName, Locale locale) throws Exception;
}

🌟 resolveViewName方法:按名称解析给定的视图。

  • viewName:要解析的视图的名称
  • locale:用于解析视图的区域设置。
2.4.5.3 ViewResolver实现类

ViewResolver接口的实现类如下(选中的是下文中我们要用到的):

image-20211013083210793

  • ResourceBundleViewResolver:采用bundle根路径所指定的ResourceBundle中的bean定义作为配置。
  • XmlViewResolver:该类接受一个XML格式的配置文件。
  • URLBasedViewResolver:它直接使用URL来解析到逻辑视图名,除此之外不需要其他任何显式的映射声明。
  • FreeMarkerViewResolver :最终会解析逻辑视图配置,返回 freemarker 模板。不需要指定 viewClass 属性。
  • ContentNegotiatingViewResolver:它会根据所请求的文件名或请求的Accept头来解析一个视图。

InternalResourceViewResolver 为“内部资源视图解析器”,是日常开发中最常用的视图解析器类型。它是 URLBasedViewResolver 的子类,拥有 URLBasedViewResolver 的一切特性。

InternalResourceViewResolver 能自动将返回的视图名称解析为 InternalResourceView 类型的对象。InternalResourceView 会把 Controller 处理器方法返回的模型属性都存放到对应的 request 属性中,然后通过 RequestDispatcher 在服务器端把请求 forword 重定向到目标 URL。也就是说,使用 InternalResourceViewResolver 视图解析时,无需再单独指定 viewClass 属性。

2.4.5.4 InternalResourceViewResolver

InternalResourceViewResolver 是实现类的主要内容如下:

public class InternalResourceViewResolver extends UrlBasedViewResolver {

    //将默认视图类设置为requiredViewClass:默认情况下是InternalResourceView,如果JSTL API存在,则默认是JstlView。
    public InternalResourceViewResolver() {
        Class<?> viewClass = requiredViewClass();
        if (InternalResourceView.class == viewClass && jstlPresent) {
            viewClass = JstlView.class;
        }
        setViewClass(viewClass);
    }

    /**
     * 一个方便的构造函数,允许指定前缀和后缀作为构造函数参数
     * @param prefix 在构建URL时附加到视图名称的前缀
     * @param suffix 在构建URL时附加到视图名称的后缀
     */
    public InternalResourceViewResolver(String prefix, String suffix) {
        this();
        setPrefix(prefix);
        setSuffix(suffix);
    }

    @Override
    protected AbstractUrlBasedView buildView(String viewName) throws Exception {
        InternalResourceView view = (InternalResourceView) super.buildView(viewName);
        if (this.alwaysInclude != null) {
            view.setAlwaysInclude(this.alwaysInclude);
        }
        view.setPreventDispatchLoop(true);
        return view;
    }

}

在其父类URLBasedViewResolver 中,buildView方法如下:

protected AbstractUrlBasedView buildView(String viewName) throws Exception{..}

AbstractUrlBasedView:基于url的视图的抽象基类。以“URL”bean属性的形式提供一致的方式来保存View包装的URL。

2.4.6 View

View是最后一个处理环节了!!

2.4.6.1 View功能

🌟 功能:View会根据传进来的Model模型数据进行渲染,此处的Model实际是一个Map数据结构,因此很容易支持其他视图技术,实现类支持不同的View类型(jsp、freemarker、pdf…)

image-20211013101242574
2.4.6.1 View接口

view接口的抽象内容如下:

void render(@Nullable Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
    throws Exception;

可以看到,通过Map数据结构支持给定模型呈现视图。

2.4.6.1 View实现类

该接口的实现类如下:

image-20211013093308729

3. DispatcherServlet

通过对Spring MVC中核心组件的认识,我们应该看到了DispatcherServlet在这之中扮演的角色。

下面根据源码,将DispatcherServlet的交互过程再梳理一遍。

1️⃣ Step1:request -> HandlerMapping.getHandler() -> HandlerExecutionChain对象(Handler)

@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    if (this.handlerMappings != null) {
        for (HandlerMapping mapping : this.handlerMappings) {
            HandlerExecutionChain handler = mapping.getHandler(request);
            if (handler != null) {
                return handler;
            }
        }
    }
    return null;
}

2️⃣ Step2:Handler -> HandlerAdapter -> ModelAndView

@Nullable
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) {
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            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;
}

3️⃣ Step3:ModelAndView -> View -> Response

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
    // Determine locale for request and apply it to the response.
    Locale locale =
        (this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
    response.setLocale(locale);

    View view;
    String viewName = mv.getViewName();
    if (viewName != null) {
        // We need to resolve the view name.
        view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
        if (view == null) {
            throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
                                       "' in servlet with name '" + getServletName() + "'");
        }
    }
    else {
        // No need to lookup: the ModelAndView object contains the actual View object.
        view = mv.getView();
        if (view == null) {
            throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
                                       "View object in servlet with name '" + getServletName() + "'");
        }
    }

    // Delegate to the View object for rendering.
    if (logger.isTraceEnabled()) {
        logger.trace("Rendering view [" + view + "] ");
    }
    try {
        if (mv.getStatus() != null) {
            response.setStatus(mv.getStatus().value());
        }
        view.render(mv.getModelInternal(), request, response);
    }
    catch (Exception ex) {
        if (logger.isDebugEnabled()) {
            logger.debug("Error rendering view [" + view + "]", ex);
        }
        throw ex;
    }
}

4. Spring MVC 执行原理

通过上文描述,应该对Spring MVC有了简单的认识,下面梳理下整体的工作流程。

image-20211013124718580

1️⃣ Step1:DispatcherServlet接受浏览器请求、拦截请求;

2️⃣ Step2:DispatcherServlet调用HandlerMapping,HandlerMapping根据请求URL查找Handler;

3️⃣ Step3:HandlerExecutionChain处理程序执行链,根据URL查找控制器;

4️⃣ Step4:HandlerExecutionChain将查找到的具体的Handler解析后返回Handler;

5️⃣ Step5:DispatcherServlet调用HandlerAdapter处理器适配器,按照特定的规则去执行Handler。

6️⃣ Step6:HandlerAdapter经过适配调用具体的处理器Controller;

7️⃣ Step7:Handler由Controller执行,执行后的具体信息(ModelAndView)返回给HandlerAdapter;

8️⃣ Step8:HandlerAdapter将视图逻辑名或模型传递给DispatcherServlet;

9️⃣ Step9: DispatcherServlet将ModelAndView传给ViewReslover视图解析器;

🔟 Step10:视图解析器将解析的逻辑视图名(具体的View)返回给DispatcherServlet;

1️⃣1️⃣ Step11:DispatcherServlet根据视图解析器解析的视图结果,调用具体的视图;

1️⃣2️⃣ Step12:将View作为Request返回给历览器。

5. Spring MVC 模型

看完理论部分,不如来实际写一个Model。

官方文档:spring-web

JDK:1.8

Maven:3.8.2

IDEA:2021.2

Tomcat:9.0.46

5.1 前期准备

新建一个Maven项目,删去src目录,再项目下新建一个空的Model。

导入核心依赖:

  • spring-webmvcorg.springframework
  • servlet-apijavax.servlet
  • jsp-apijavax.servlet
  • jstljavax.servlet

5.2 配置Web/Tomcat

在Model名称右键Add Frameworl Support...,勾选Web Application

配置Tomcat参考:IDEA配置Tomcat

5.3 配置web.xml

5.3.1 配置Servlet

我们知道DispatcherServlet本质是一个Servlet,所以需要像往常一样在web.xml文件中配置Servlet映射。

<servlet>
    <servlet-name>SpringMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!--关联一个xml配置文件-->
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:SpringMVC-servlet.xml</param-value>
    </init-param>
    <!--启动级别-->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>SpringMVC</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>

在这里面,出现了不认识的内容,下面展开看看。

5.3.2 配置Spring MVC 配置文件

<init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:SpringMVC-servlet.xml</param-value>
</init-param>

这里是配置一个contextConfigLocation,在<init-param>标签内如果没有提供值,默认会去找WEB-INF/*-servlet.xml

如果是 标签内的,如果没有提供值,默认会去找/WEB-INF/applicationContext.xml

5.3.3 配置启动级别

<load-on-startup> 元素标记容器是否应该在web应用程序启动的时候就加载这个servlet,(实例化并调用其init()方法)。

  • 如果该元素的值为负数或者没有设置,则容器会当Servlet被请求时再加载。
  • 如果值为正整数或者0时,表示容器在应用启动时就加载并初始化这个servlet,值越小,servlet的优先级越高,就越先被加载。值相同时,容器就会自己选择顺序来加载。
<load-on-startup>1</load-on-startup>

5.3.4 匹配JSP

/ 匹配所有的请求;(不包括.jsp)

/* 匹配所有的请求;(包括.jsp)

<url-pattern>/</url-pattern>

5.4 编写Spring MVC 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd">

</beans>

5.4.1 处理映射器

<bean class="org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping"/>

5.4.2 处理器适配器

<bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter"/>

5.4.3 视图解析器

  • prefix:前缀
  • suffix:后缀
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver" id="internalResourceViewResolver">
    <property name="prefix" value="/jsp/"/>
    <property name="suffix" value=".jsp"/>
</bean>

5.5 Controller

前面提到,相当于一个Servlet的组件是Controller。我们的操作业务写在这里面。

public class HelloController implements Controller {
    @Override
    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //ModelAndView 实例化模型和视图
        ModelAndView modelAndView = new ModelAndView();
        // 封装对象,放在ModelAndView中
        modelAndView.addObject("username", "welcome");
        // 跳转页面,全路径为:/jsp/test.jsp
        modelAndView.setViewName("test");
        return modelAndView;
    }
}

5.6 注册Controller

有了Control后,我们需要将它交给SpringIOC容器,所以我们需要将其注册为Bean。

在SpringMVC-servlet.xml中注册Bean。

<bean id="/test" class="com.controller.HelloController"/>

5.7 JSP页面

写要跳转的jsp页面,显示ModelandView存放的数据,以及我们的正常页面。

注意之前定义的前缀,JSP路径为web/jsp/test.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
        <head>
            <title>Title</title>
        </head>
        <body>
            ${username}
        </body>
    </html>

🌈 username:这里利用EL表达式取到的username,相当于之前的session.setAttribute

5.8 测试

启动Tomcat,在修改地址栏为http://localhost:8080/test

image-20211013133840254

6. 写在最后

上面Spring MVC Model的代码量其实并不多,但在实际应用中我们也并不会写这么代码,后期通过简单的注解就能实现相同的功能。重要的是理解Spring MVC 的执行过程,这也是面试点。

Spring MVC 中的各个组件容易搞混淆,下面将Model中到的实现类以及父类接口再梳理一遍。

接口实现类说明
NullDispatcherServlet前置控制器
HandlerMappingBeanNameUrlHandlerMapping处理映射器
HandlerAdapterSimpleControllerHandlerAdapter处理适配器
ViewResolverInternalResourceViewResolver视图解析器

DispatcherServlet:org.springframework.web.servlet.DispatcherServlet

BeanNameUrlHandlerMapping:org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping

SimpleControllerHandlerAdapter:org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter

InternalResourceViewResolver:org.springframework.web.servlet.view.InternalResourceViewResolver

 


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

JOEL-T99

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

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

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

打赏作者

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

抵扣说明:

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

余额充值