SpringMVC笔记

文章内容输出来源:拉勾教育Java高薪训练营;
本文为SpringMVC模块学习笔记

简介

SpringMVC是什么?

在说SpringMVC是什么之前,我们先讲一下,系统标准的三层架构:表现层,业务成,持久层。

  • 表现层:(WEB层)字面意思就是表现给用户看的一层,他承接着和用户交互的功能,他有包括展示层和控制层。
    • 展示层:用户接受用户意图和结果展示
    • 控制层:负责接受展示层发过来的请求,转发请求给业务层,获取处理结果转发给展示层。这个流程可以理解为MVC模式。
  • 业务层:也就是我们说的Service层。他负责程序具体业务逻辑处理,对外(web层)提供接口调用,依赖持久层。
  • 持久层:也即是我们所的dao层(data access object),负责数据持久化,主要和数据库交互,基本的增删改查都通过他。

上面说的程序开发的三层架构,其中表现层用到了MVC模式

MVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller) 的缩写, 是一种用于设计创建 Web 应用程序表现层的模式。MVC 中每个部分各司其职:

  • Model(模型):包括业务模型和数据模型,用于数据传输;
  • View(视图):通常指JSP或者HTML,对于后端来说,整个前端工程都是View,用于数据的展示和与用户交互。
  • Controller(控制器):具体处理用户交互的一层,用于接收视图传过来的用户请求,转发请求到对应的业务层处理,返回处理结果。

上面说了系统的三层架构和MVC模式,那个SpringMVC是什么呢?他其实是MVC模式的一种实现,用于系统三层架构中的表现层。

SpringMVC 已经成为目前最主流的 MVC 框架之一,并且随着 Spring3.0 的发布,全面超越Struts2, 成为最优秀的 MVC 框架。

与Struts2对比有什么优势呢?
servlet、struts实现接口、springmvc中要让一个java类能够处理请求只需要添加注解就ok

SpringMVC通过一套注解,让一个简单的 Java 类成为处理请求的控制器,而无须实现任何接口。同时它还支持 RESTful 编程⻛格的请求。

Spring MVC 本质可以认为是对servlet的封装,简化了我们serlvet的开发。

SpringMVC和Spring什么关系?

SpringMVC是Spring家族下一个WEB项目,用来实现MVC模式,它依赖Spring的IOC和AOP特性。

MVC容器和IOC容器什么关系?

MVC容器其实也是一个IOC容器,它独立与Spring项目配置的IOC容器。MVC容器可以获取到IOC容器管理的Bean,但是IOC容器获取不到MVC容器管理的Bean,他们关系可以认为是父子容器,MVC容器是子,IOC容器是父。

SpringMVC工作原理

整体工作流程:
SpringMVC处理请求流程

流程说明

  1. 用户发送请求至前端控制器DispatcherServlet
  2. DispatcherServlet收到请求调用HandlerMapping处理器映射器
  3. 处理器映射器根据请求Url找到具体的Handler(后端控制器),生成处理器对象及处理器拦截器(如果有则生成)一并返回DispatcherServlet
  4. DispatcherServlet调用HandlerAdapter处理器适配器去调用Handler
  5. 处理器适配器执行Handler
  6. Handler执行完成给处理器适配器返回ModelAndView
  7. 处理器适配器向前端控制器返回 ModelAndView,ModelAndView是SpringMVC框架的一个底层对象,包括 Model 和 View
  8. 前端控制器请求视图解析器去进行视图解析,根据逻辑视图名来解析真正的视图。
  9. 视图解析器向前端控制器返回View
  10. 前端控制器进行视图渲染,就是将模型数据(在ModelAndView对象中)填充到 request 域
  11. 前端控制器向用户响应结果

SPringMVC 9大组件

  • HandlerMapping (处理器映射器)
    用于根据当前请求uri查找对应的Handler处理器的,处理器具体表现可以是类,也可以是方法。他返回的是HandlerInterceptor
  • HandlerAdapter(处理器适配器)
    它是一个适配器,用于适配不同形式的HandlerHandler的形式是任意的(类,方法,方法参数不同),但是HandlerServlet获取请求是固定的doService(HttpServletRequest req,HttpServletResponse resp)形式,所以需要HandlerAdapter来让固定的Servlet请求适应不同的Handler
  • ViewResolver(视图解析器)
    用于将String类型的视图名和Locale解析为View类型的视图,只有一个resolveViewName()方法。从方法的定义可以看出,Controller层返回的String类型视图名 viewName 最终会在这里被解析成为View。View是用来渲染⻚面的,也就是说,它会将程序返回的参数和数据填入模板中,生成html文件。默认情况下,Spring MVC会自动为我们配置一个 InternalResourceViewResolver,是针对 JSP 类型视图的。ViewResolver 在这个过程主要完成两件事情:找到渲染所用的模板和所用的技术(如JSP)并填入参数。
  • RequestToViewNameTranslator
    它的作用是从请求中获取 ViewName.因为 ViewResolver 根据 ViewName 查找 View,但有的 Handler 处理完成之后,没有设置 View,也没有设置 ViewName, 便要通过这个组件从请求中查找 ViewName。
  • LocaleResolver
    ViewResolver 组件的 resolveViewName 方法需要两个参数,一个是视图名,一个是 Locale。 LocaleResolver 用于从请求中解析出 Locale,比如中国 Locale 是 zh-CN,用来表示一个区域。这 个组件也是 i18n 的基础。
  • ThemeResolver
    ThemeResolver 组件是用来解析主题的。主题是样式、图片及它们所形成的显示效果的集合。
  • MultipartResolver
    MultipartResolver 用于上传请求,通过将普通的请求包装成 MultipartHttpServletRequest 来实 现。MultipartHttpServletRequest 可以通过 getFile() 方法 直接获得文件。如果上传多个文件,还 可以调用 getFileMap()方法得到Map<FileName,File>这样的结构,MultipartResolver 的作用就 是封装普通的请求,使其拥有文件上传的功能。
  • FlashMapManager
    FlashMap 用于重定向时的参数传递。FlashMapManager 就是用来管理 FalshMap 的。

使用实战

注意:spring-mvc的配置可以和spring的配置文件在一起,这样其实就是,将所有Bean都交由IOC容器管理,如果将mvc配置独立出来,可以将contoller单独交由MVC容器管理。

项目整合

这里在spring已经整合基础上讲解

  1. 引入依赖坐标
  2. 添加springmvc.xml配置文件
  3. 配置web.xml启动入口
  4. 编写Controller

引入依赖坐标:

<!--spring-mvc-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-webmvc</artifactId>
    <version>5.1.12.RELEASE</version>
</dependency>

<!--jsp-api&servlet-api-->
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>jsp-api</artifactId>
    <version>2.0</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <scope>provided</scope>
</dependency>
<!--⻚面使用jstl表达式-->
<dependency>
    <groupId>jstl</groupId>
    <artifactId>jstl</artifactId>
    <version>1.2</version>
</dependency>
<dependency>
    <groupId>taglibs</groupId>
    <artifactId>standard</artifactId>
    <version>1.1.2</version>
</dependency>

添加springmvc.xml配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       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
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd

">

    <!--扫描controller-->
    <context:component-scan base-package="com.wjy.ssm.controller"/>

    <!--配置springmvc注解驱动,自动注册合适的组件handlerMapping和handlerAdapter-->
    <mvc:annotation-driven/>

</beans>

配置web.xml启动入口

<!--springmvc启动-->
<servlet>
    <servlet-name>springmvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:springmvc.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>

编写Controller

// 表示当前类是Controller类,交由容器管理
@Controller
// RequestMapping的值最终会作为url发布
@RequestMapping("/base")
public class LoginContoller {

    // 发布地址: localhost:port/base/login
    @RequestMapping("/login")
    public ModelAndView login(String username, String password, HttpSession session) {
        final String admin = "admin";
        if (admin.equals(username) && admin.equals(password)) {
            // 登录成功,服务器存储登录状态
            session.setAttribute("login", username);
            return new ModelAndView("redirect:/resume/list");
        }
        return new ModelAndView("index");
    }

    // 发布地址: localhost:port/base/logout
    @RequestMapping("/logout")
    public ModelAndView logout(HttpSession session) {
        session.invalidate();
        return new ModelAndView("index");
    }
}

使用servlet容器部署后,就可以通过RequestMapping指定的地址访问了。

url-pattern配置说明

SpringMVC本质上是一个,Servlet,他需要配置url-pattern指定哪些path下请求被SpringMVC处理

<servlet-mapping>
<servlet-name>springmvc</servlet-name>

<!--
    方式一:带后缀,比如*.action  *.do *.aaa
            该种方式比较精确、方便,在以前和现在企业中都有很大的使用比例
    方式二:/ 不会拦截 .jsp,但是会拦截.html等静态资源(静态资源:除了servlet和jsp之外的js、css、png等)

        为什么配置为/ 会拦截静态资源???
            因为tomcat容器中有一个web.xml(父),你的项目中也有一个web.xml(子),是一个继承关系
                    父web.xml中有一个DefaultServlet,  url-pattern 是一个 /
                    此时我们自己的web.xml中也配置了一个 / ,覆写了父web.xml的配置
        为什么不拦截.jsp呢?
            因为父web.xml中有一个JspServlet,这个servlet拦截*.jsp文件,而我们并没有覆写这个配置,
            所以springmvc此时不拦截jsp,jsp的处理交给了tomcat

    方式三:/* 拦截所有,包括.jsp
-->
<!--拦截匹配规则的url请求,进入springmvc框架处理-->
<url-pattern>/</url-pattern>
</servlet-mapping>

静态资源访问配置

使用SpringMVC提供的Handler解析转发静态资源请求
  1. 配置SpringMVC的url-pattern为/拦截除了*.jsp的所有请求
  2. 配置文件添加静态资源处理Handler
    <!--静态资源配置-->
    <!--
        原理:添加该标签配置之后,会在SpringMVC上下文中定义一个DefaultServletHttpRequestHandler对象
             这个对象如同一个检查人员,对进入DispatcherServlet的url请求进行过滤筛查,如果发现是一个静态资源请求
             那么会把请求转由web应用服务器(tomcat)默认的DefaultServlet来处理,如果不是静态资源请求,那么继续由
             SpringMVC框架处理
    -->
    <mvc:default-servlet-handler/>
    
使用SpringMVC管理静态资源
  1. 配置url-pattern为/*拦截所有请求
  2. 将所有静态资源放到webapp下一个目录,如webapp/WEB-INF/static
  3. 配置文件添加配置
    <!-- 
        location:服务器存放文件路径
        mapping:用户请求路径
        如下配置,用户对 localhost:port/static/...下的所有请求,都会转发到对应的服务器/WEB-INF/static/...路径下
     -->
    <mvc:resources location="/WEB-INF/static/" mapping="/static/**"/>
    

传统Controller跳转页面方式

  1. 通过ModelAndView方式
  2. 直接Controller返回页面名称字符串

本质上是使用InternalResourceViewResolver视图解析器,跳转页面,可以通过配置文件,配置InternalResourceViewResolver的具体参数,如果视图前缀后缀

<!--视图解析器-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix" value="/"/>
    <property name="suffix" value=".jsp"/>
</bean>

传统Controller往JSP页面传输数据

  1. 通过形参声明Model,ModelMap,Map,ModelAndView对象,然后调用他们提供的方法addXXX(String,Object)等,可以将数据放入Request作用域中。
  2. JSP页面通过EL表达式从Request作用域取出数据使用。

web参数绑定

参数绑定只需要,在@RequestMapping修饰的方法上声明形参就可以收到web传递的参数

简单参数绑定

简单数据类型:八种基本数据类型及其包装类型
参数类型推荐使用包装数据类型,因为基础数据类型不可以为null
整型:Integer、int
字符串:String
单精度:Float、float
双精度:Double、double
布尔型:Boolean、boolean (对于布尔类型的参数,请求的参数值为true或false。或者1或0)

绑定简单数据类型参数,只需要直接声明形参即可(形参参数名和传递的参数名要保持一 致,建议 使用包装类型,当形参参数名和传递参数名不一致时可以使用@RequestParam注解进行 手动映射)

pojo类接收参数

url中id=1&username=zhangsan会注入到user
Controller:

/*
* SpringMVC接收pojo类型参数  url:/demo/handle04?id=1&username=zhangsan
*
* 接收pojo类型参数,直接形参声明即可,类型就是Pojo的类型,形参名无所谓
* 但是要求传递的参数名必须和Pojo的属性名保持一致
*/
@RequestMapping("/handle04")
public ModelAndView handle04(User user) {
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.setViewName("success");
    return modelAndView;
}

实体类

public class User {

    private Integer id;
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
指定参数接收别名

通过@RequestParam注解指定参数别名,下面例子,ids=1会注入到id形参中

 
/*
* SpringMVC 接收简单数据类型参数 url:/demo/handle03?ids=1
*/
@RequestMapping("/handle03")
public ModelAndView handle03(@RequestParam("ids") Integer id,Boolean flag) {
    ModelAndView modelAndView = new ModelAndView();
    modelAndView.setViewName("success");
    return modelAndView;
}
日期类型参数接收(定制类型参数接收)

使用自定义类型转换器处理日期类型,同时也可以使用自定义类型转换器处理自定义类型

  1. 创建类型转换器
  2. 配置文件中配置注册类型转换器
  3. 从行参中获取转换后的类型参数
  • 创建类型转换器
    /**=
     *  自定义类型转换器
     * S:source,源类型
     * T:target:目标类型
     */
    public class DateConverter implements Converter<String, Date> {
        @Override
        public Date convert(String source) {
            // 完成字符串向日期的转换
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
            try {
                Date parse = simpleDateFormat.parse(source);
                return parse;
            } catch (ParseException e) {
                e.printStackTrace();
            }
            return null;
        }
    }
    
  • 注册类型转换器
    <!-- 自动注册最合适的处理器映射器,处理器适配器(调用handler方法) -->
    <mvc:annotation-driven conversion-service="conversionServiceBean"/>
    
    <!--注册自定义类型转换器-->
    <bean id="conversionServiceBean" class="org.springframework.format.support.FormattingConversionServiceFactoryBean">
        <property name="converters">
            <set>
                <bean class="com.lagou.edu.converter.DateConverter"></bean>
            </set>
        </property>
    </bean>
    
  • 后台Handler方法
    /**
     * 绑定日期类型参数
     * 定义一个SpringMVC的类型转换器  接口,扩展实现接口接口,注册你的实现
     * @param date
     * @return
     */
    @RequestMapping("/handle06")
    public voie handle06(Date date) {
        System.out.println(date);
    }
    
    输出Tue Sep 22 00:00:00 CST 2020
  • 前端jsp
    <a href="/demo/handle06?birthday=2020-09-22">点击测试</a>
    
Servlet对象获取方式

获取Servlet对象,只需要在Handler方法行参中声明想要获取的对象即可,SpringMVC会将Servlet对象注入;

@RequestMapping("/test")
public ModelAndView test(HttpServletRequest request, HttpServletResponse response, HttpSession session) {
    session.invalidate();
    return new ModelAndView("index");
}

以上代码中request,response,session即为Servlet对象

REST风格支持

获取URL路径参数

使用@PathVariable注解获取URL路径参数

/**
 *  /demo/handle/15 这个请求接收后,id的值为15
 **/
@RequestMapping(value = "/handle/{id}")
public String handleDelete(@PathVariable("id") Integer id) {
    return "success";
}
Handler处理指定类型请求

通过@RequestMapping(method = {RequestMethod.GET})的method属性指定特定类型请求处理,只可以为:

public enum RequestMethod {
    GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE;
    private RequestMethod() { /* compiled code */ }
}

以下Handler只会拦截DELETE请求

@RequestMapping(value = "/handle/{id}",method = {RequestMethod.DELETE})
public String handleDelete(@PathVariable("id") Integer id) {
    return "success";
}

JSON格式数据交互支持

  1. 引入依赖包
    <!--json数据交互所需jar,start-->
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-core</artifactId>
      <version>2.9.0</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-databind</artifactId>
      <version>2.9.0</version>
    </dependency>
    <dependency>
      <groupId>com.fasterxml.jackson.core</groupId>
      <artifactId>jackson-annotations</artifactId>
      <version>2.9.0</version>
    </dependency>
    <!--json数据交互所需jar,end-->
    
  2. 使用注解指定获取JSON数据
    • @RequestBody 指定参数从请求体中获取数据,数据格式为JSON字符串,并序列化为pojo类
    • @ResponseBody 指定返回值序列化为JSON字符串,并response到web页面
示例
  • 前端代码
    // 发送ajax请求
    $.ajax({
        url: '/demo/handle07',
        type: 'POST',
        data: '{"id":"1","name":"李四"}',
        contentType: 'application/json;charset=utf-8',
        dataType: 'json',
        success: function (data) {
            alert(data.name);
        }
    })
    
  • 后台Handler方法
    // 添加@ResponseBody之后,不再走视图解析器那个流程,而是等同于response直接输出数据
    @RequestMapping("/handle07")
    public @ResponseBody User handle07(@RequestBody User user) {
        // 业务逻辑处理,修改name为张三丰
        user.setName("张三丰");
        return user;
    }
    
  • 最终,后端会收到数据为{"id":"1","name":"李四"}的User对象,返回页面数据为{"id":"1","name":"张三丰"}的JSON对象

其他特性

拦截器

监听器、过滤器和拦截器对比
  • Servlet:处理Request请求和Response响应
  • 过滤器(Filter):Servlet执行之前执行,如果配置为/*可以对所有的资源访问(servlet、js/css静态资源等)进行过滤处理
  • 监听器(Listener):实现了javax.servlet.ServletContextListener接口的服务器端组件,它随 Web应用的启动而启动,只初始化一次,然后会一直运行监视,随Web应用的停止而销毁
    • 作用一:做一些初始化工作,web应用中spring容器启动ContextLoaderListener
    • 作用二:监听web中的特定事件,比如HttpSession,ServletRequest的创建和销毁;变量的创建、 销毁和修改等。可以在某些动作前后增加处理,实现监控,比如统计在线人数,利用 HttpSessionLisener等。
  • 拦截器(Interceptor):SpringMVC提供的特性,不会拦截 jsp/html/css/image的访问等,只拦截Handler
拦截器执行流程
  1. Handler执行前执行一次,如果返回true则进行下一步,否则直接返回
  2. Handler执行完成后执行一次
  3. 然后到前端页面前执行一次(与上一次之间,还有视图渲染操作如:JSP渲染)

拦截器执行流程

多个拦截器执行流程

会基于配置文件配置执行:

  • preHandler:顺序执行(与配置文件配置顺序一致)
  • postHandler:倒序执行
  • afterCompletion:倒序执行

文件上传支持

  1. 引入依赖
  2. 注册文件解析器
  3. 编写代码

引入依赖

<!--文件上传所需坐标-->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>

注册文件解析器

<!--
    多元素解析器
    id固定为multipartResolver
-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
    <!--设置上传文件大小上限,单位是字节,-1代表没有限制也是默认的-->
    <property name="maxUploadSize" value="5000000"/>
</bean>

编写代码

  • 前端代码
    <!--
        1 method="post"
        2 enctype="multipart/form-data"
        3 type="file"
    -->
    <form method="post" enctype="multipart/form-data" action="/demo/upload">
        <input type="file" name="uploadFile"/>
        <input type="submit" value="上传"/>
    </form>
    
  • 后端代码
    @RequestMapping(value = "/upload")
    public ModelAndView upload(MultipartFile uploadFile,HttpSession session)
    }
    
    其中uploadFile就是本次上传文件使用uploadFile.transferTo(new File(服务器目录,文件名));保存文件到服务器

全局Controller异常处理

可以通过指定行参来获取Servlet对象

@ControllerAdvice
public class GlobalExceptionResolver {

    /**
     * 传统处理方式
     * @param exception 拦截异常
     * @param response HttpServletResponse对象
     * @return
     */
    @ExceptionHandler(ArithmeticException.class)
    public ModelAndView handleException(ArithmeticException exception, HttpServletResponse response) {
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("msg", exception.getMessage());
        modelAndView.setViewName("error");
        return modelAndView;
    }


    /**
     * restful处理方式
     * @param exception 拦截异常
     * @return
     */
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Object handleException(Exception exception) {
        return exception.getMessage();
    }
}

安全的重定向参数传递方式Flash属性

我们普通重定向传递参数方式为:

return "redirect:handle01?name=" + name;

但是上面方式是基于GET传参,明文,有长度限制,安全性不高,此时我们可以使用SpringMVC提供的flash属性来在上下文传递参数。框架会在session中记录该属性值,当 跳转到⻚面之后框架会自动删除flash属性。

@RequestMapping("/handle01")
public ModelAndView handle01(@ModelAttribute("name") String name) {
    // 封装了数据和页面信息的 ModelAndView
    ModelAndView modelAndView = new ModelAndView();
    // 视图信息(封装跳转的页面信息) 逻辑视图名
    modelAndView.setViewName("success");
    return modelAndView;
}

@RequestMapping("/handleRedirect")
public String handleRedirect(String name,RedirectAttributes redirectAttributes) {
    // addFlashAttribute方法设置了一个flash类型属性,该属性会被暂存到session中,在跳转到页面之后该属性销毁
    redirectAttributes.addFlashAttribute("name",name);
    return "redirect:handle01";

}

以上代码,最终在handle01跳转的success页面内能从request中获取name属性;
handle01方法的行参中也可以获取name参数。

乱码问题解决

  • Post请求乱码,web.xml中加入过滤器
    <!--springmvc提供的针对post请求的编码过滤器-->
    <filter>
        <filter-name>encoding</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>encoding</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    
  • Get请求乱码(Get请求乱码需要修改tomcat下server.xml的配置)
    <Connector URIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
    
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值