SpringMVC框架基础

本文详细介绍了SpringMVC框架的工作原理,包括核心组件DispatcherServlet的作用、配置方式以及如何使用注解实现请求映射。此外,还深入探讨了@RequestMapping注解的各种应用场景,并列举了多种处理器映射和拦截器的配置方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. SpringMVC框架简介

Spring的模型-视图-控制器(MVC)框架是围绕一个DispatcherServlet来设计的,这个Servlet会把请求分发给各个处理器,并支持可配置的处理器映射、视图渲染、本地化、时区与主题渲染等,甚至还能支持文件上传。处理器是你的应用中注解了@Controller和@RequestMapping的类和方法,Spring为处理器方法提供了极其多样灵活的配置。Spring 3.0以后提供了@Controller注解机制、@PathVariable注解以及一些其他的特性,你可以使用它们来进行RESTful web站点和应用的开发。

1.1 DispatcherServlet

mvc

DispatcherServlet应用其实就是一个“前端控制器”的设计模式,作为一个Servlet需要在web.xml配置文件中声明:

<web-app>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!--Web服务启动时加载-->
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
</web-app>

在Servlet 3.0+的环境下,你还可以用编程的方式配置Servlet容器。下面是一段这种基于代码配置的例子:

public class MyWebApplicationInitializer implements WebApplicationInitializer {

    @Override
    public void onStartup(ServletContext container) {
        ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet());
        registration.setLoadOnStartup(1);
        registration.addMapping("/*");
    }
}

Spring中的ApplicationContext实例是可以有范围(scope)的。在Spring MVC中,每个DispatcherServlet都持有一个自己的上下文对象WebApplicationContext,它又继承了根(root)WebApplicationContext对象中已经定义的所有bean。WebApplicationContext被绑定在ServletContext中,如果需要获取它,你可以通过RequestContextUtils工具类中的静态方法来拿到这个web应用的上下文WebApplicationContext。
dispatcher
DispatcherServlet的初始化过程中,Spring MVC会在你web应用的WEB-INF目录下查找一个名为[servlet-name]-servlet.xml的配置文件,并创建其中所定义的bean,如果在全局上下文中存在相同名字的bean,则它们将被新定义的同名bean覆盖。当你的应用中只需要一个DispatcherServlet时,只配置一个根context对象也是可行的。

singleRootContext

<web-app>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/root-context.xml</param-value>
    </context-param>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!--配置一个空值-->
            <param-value></param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
</web-app>

1.1.1 WebApplicationContext中特殊的bean

Spring的DispatcherServlet使用了特殊的bean来处理请求、渲染视图等,这些特定的bean是Spring MVC框架的一部分,在未指定情况下默认使用org.springframework.web.servlet包下的DispatcherServlet.properties作为默认配置

bean的类型作用
HandlerMapping处理器映射。它会根据某些规则将进入容器的请求映射到具体的处理器以及一系列前处理器和后处理器(即处理器拦截器)上。具体的规则视HandlerMapping类的实现不同而有所不同。其最常用的一个实现支持你在控制器上添加注解,配置请求路径。当然,也存在其他的实现
HandlerAdapter处理器适配器。拿到请求所对应的处理器后,适配器将负责去调用该处理器,这使得DispatcherServlet无需关心具体的调用细节。比方说,要调用的是一个基于注解配置的控制器,那么调用前还需要从许多注解中解析出一些相应的信息。因此,HandlerAdapter的主要任务就是对DispatcherServlet屏蔽这些具体的细节。
HandlerExceptionResolver处理器异常解析器。它负责将捕获的异常映射到不同的视图上去,此外还支持更复杂的异常处理代码
ViewResolver视图解析器。它负责将一个代表逻辑视图名的字符串(String)映射到实际的视图类型View上。
LocaleResolver
LocaleContextResolver
地区解析器 和 地区上下文解析器。它们负责解析客户端所在的地区信息甚至时区信息,为国际化的视图定制提供了支持。
ThemeResolver主题解析器。它负责解析你web应用中可用的主题,比如,提供一些个性化定制的布局等。
MultipartResolver解析multi-part的传输请求,比如支持通过HTML表单进行的文件上传等。
FlashMapManagerFlashMap管理器。它能够存储并取回两次请求之间的FlashMap对象。后者可用于在请求之间传递数据,通常是在请求重定向的情境下使用

1.2 控制器实现

/**
 * @Controller
 * @RequestMapping
 */
@Controller
@RequestMapping("/appointments")
public class AppointmentsController {

    private final AppointmentBook appointmentBook;

    @Autowired
    public AppointmentsController(AppointmentBook appointmentBook) {
        this.appointmentBook = appointmentBook;
    }

    @RequestMapping(method = RequestMethod.GET)
    public Map<String, Appointment> get() {
        return appointmentBook.getAppointmentsForToday();
    }

    @RequestMapping(path = "/{day}", method = RequestMethod.GET)
    public Map<String, Appointment> getForDay(@PathVariable @DateTimeFormat(iso=ISO.DATE) Date day, Model model) {
        return appointmentBook.getAppointmentsForDay(day);
    }

    @RequestMapping(path = "/new", method = RequestMethod.GET)
    public AppointmentForm getNewForm() {
        return new AppointmentForm();
    }

    @RequestMapping(method = RequestMethod.POST)
    public String add(@Valid AppointmentForm appointment, BindingResult result) {
        if (result.hasErrors()) {
            return "appointments/new";
        }
        appointmentBook.addAppointment(appointment);
        return "redirect:/appointments";
    }
}

1.2.1 @RequestMapping

Spring 3.1中新增了一组类用以增强@RequestMapping,分别是RequestMappingHandlerMappingRequestMappingHandlerAdapter,在MVC命名空间和MVC Java编程配置方式下,这组类及其新特性默认是开启的。在Spring 3.1之前,框架会在两个不同的阶段分别检查类级别和方法级别的请求映射——首先,DefaultAnnotationHanlderMapping会先在类级别上选中一个控制器,然后再通过AnnotationMethodHandlerAdapter定位到具体要调用的方法;Spring 3.1后引入的这组新类,RequestMappingHandlerMapping成为了这两个决策实际发生的唯一一个地方

URI模块

URI模板是一个类似于URI的字符串,只不过其中包含了一个或多个的变量名

    // GET /owners/yt

    @RequestMapping(path="/owners/{ownerId}", method=RequestMethod.GET)//ownerId=yt
    //如果参数名与变量名不同,@PathVariable("ownerId")
    public String findOwner(@PathVariable String ownerId, Model model) {
        Owner owner = ownerService.findOwner(ownerId);
        model.addAttribute("owner", owner);
        return "displayOwner";
    }

URI模板可以从类级别和方法级别的@RequestMapping注解获取数据。URI模板中还可以带正则表达式,语法是{varName:regex}

@RequestMapping("/spring-web/{symbolicName:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{extension:\\.[a-z]+}")
public void handle(@PathVariable String version, @PathVariable String extension) {

}
Path Pattern

除了URI模板外,@RequestMapping注解还支持Ant风格的路径模式(如/myPath/.do等)。不仅如此,还可以把URI模板变量和Ant风格的glob组合起来使用(比如/owners//pets/{petId}这样的用法等)
Ant风格:* 、**、?

矩阵变量

在URI规范RFC 3986中允许在路径段落中携带键值对,这些键值对被称为矩阵变量,矩阵变量可以在路径的任意位置用分号”;”隔开。

// GET /pets/42;q=11;r=22

@RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET)
public void findPet(@PathVariable String petId, @MatrixVariable int q) {

    // petId == 42
    // q == 11
}

/**
 * 存在多个相同的矩阵变量时,pathVar来指定精确的值
 */
// GET /owners/42;q=11/pets/21;q=22

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}", method = RequestMethod.GET)
public void findPet(
    @MatrixVariable(name="q", pathVar="ownerId") int q1,
    @MatrixVariable(name="q", pathVar="petId") int q2) {

    // q1 == 11
    // q2 == 22
}

/**
 * 设置默认值
 */
// GET /pets/42

@RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET)
public void findPet(@MatrixVariable(required=false, defaultValue="1") int q) {

    // q == 1
}

/**
 * Map存储所有的矩阵变量
 */
// GET /owners/42;q=11;r=12/pets/21;q=22;s=23

@RequestMapping(path = "/owners/{ownerId}/pets/{petId}", method = RequestMethod.GET)
public void findPet(
    @MatrixVariable Map<String, String> matrixVars,
    @MatrixVariable(pathVar="petId") Map<String, String> petMatrixVars) {

    // matrixVars: ["q" : [11,22], "r" : 12, "s" : 23]
    // petMatrixVars: ["q" : 11, "s" : 23]
}

如果要允许矩阵变量的使用,你必须把RequestMappingHandlerMapping类的removeSemicolonContent属性设置为false,该值默认是true的。启动矩阵变量要使用mvc的命名空间配置:

  <mvc:annotation-driven enable-matrix-variables="true"/>
可消费的媒体类型consumes

你可以指定一组可消费的媒体类型,缩小映射的范围。这样只有当请求头中 Content-Type 的值与指定可消费的媒体类型中有相同的时候,请求才会被匹配。

@Controller
@RequestMapping(path = "/pets", method = RequestMethod.POST, consumes="application/json")
public void addPet(@RequestBody Pet pet, Model model) {

}

可以使用否定如“ !text/plain”来匹配所有请求头 Accept 中不含 text/plain 的请求,媒体类型可以使用MediaType类中定义的常量。

可生产的媒体类型produces

你可以指定一组可生产的媒体类型,缩小映射的范围。这样只有当请求头中 Accept 的值与指定可生产的媒体类型中有相同的时候,请求才会被匹配。

@Controller
@RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseBody
public Pet getPet(@PathVariable String petId, Model model) {

}
请求参数params

你可以筛选请求参数的条件来缩小请求匹配范围,比如”myParam”、”!myParam”及”myParam=myValue”等;前两个条件用于筛选存在/不存在某些请求参数的请求,第三个条件筛选具有特定参数值的请求。

    @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET, params="myParam=myValue")
    public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {

    }
请求头headers

请求参数params作用类似

    @RequestMapping(path = "/pets", method = RequestMethod.GET, headers="content-type=text/plain")
    public void findPet(@PathVariable String ownerId, @PathVariable String petId, Model model) {
    }

尽管我们可以使用content-type和accept来匹配请求头Content-Type和Accept的值,但更推荐独立使用consumes和produces条件来筛选各自的请求。

1.2.2 RequestMapping注解的处理方法(handler method)

使用@RequestMapping注解的处理方法可以拥有非常灵活的方法签名,大多数参数都可以任意的次序出现,除了唯一的一个例外:BindingResult参数。

支持的方法参数类型:

  • 请求或响应对象(Servlet API):ServletRequest、HttpServletRequest、ServletResponse、HttpServletResponse对象
  • HttpSession类型的会话对象(Servlet API)
  • java.util.Locale:当前请求的地区信息,有LocaleResolver或LocaleContextResolver获取到
  • java.util.TimeZone(java 6以上的版本)/java.time.ZoneId(java 8):当前请求绑定的时区信息,有LocalContextResolver解析得到
  • java.io.InputStreamjava.io.Reader:该对象与通过Servlet API拿到的输入流/Reader是一样的
  • java.io.OutputStreamjava.io.Writer:该对象与通过Servlet API拿到的输出流/Writer是一样的
  • org.springframework.http.HttpMethod:可以拿到的HTTP请求方法
  • java.security.Principal:当前认证用户信息
  • HTTPEntity<?>:提供了对HTTP请求头和请求内容的存取
  • java.util.Map/org.springframework.io.Model/org.springframework.ui.ModelMap:用以增强默认暴露给视图层的模型(model)的功能
  • org.springframework.web.servlet.mvc.support.RedirectAttributes:用以指定重定向下要使用到的属性集以及添加flash属性(暂存在服务端的属性,它们会在下次重定向请求的范围中有效)
  • org.springframework.validation.Errors /org.springframework.validation.BindingResult:验证结果对象,用于存储前面的命令或表单对象的验证结果(紧接其前的第一个方法参数)
  • org.springframework.web.util.UriComponentsBuilder:构造器对象,用于构造当前请求URL相关的信息,比如主机名、端口号、资源类型(scheme)、上下文路径、servlet映射中的相对部分(literal part)等
  • @PathVariable@MatrixVariable@RequestParam@RequestHeader@RequestBody@RequestPart注解的方法参数
  • JavaBean:自动将参数通过set方法注入到对象属性中,支持级联属性

支持的方法返回类型:

  • ModelAndView:其中model隐含填充了命令对象,以及注解了@ModelAttribute字段的存取器被调用所返回的值
  • Model:视图名称默认由RequestToViewNameTranslator决定,model隐含填充了命令对象以及注解了@ModelAttribute字段的存取器被调用所返回的值
  • Map:用于暴露model,其中视图名称默认由RequestToViewNameTranslator决定,model隐含填充了命令对象以及注解了@ModelAttribute字段的存取器被调用所返回的值
  • model:隐含填充了命令对象,以及注解了@ModelAttribute字段的存取器被调用所返回的值。handler方法也可以增加一个Model类型的方法参数来增强model
  • String:其值会被解析成一个逻辑视图名。其中,model将默认填充了命令对象以及注解了@ModelAttribute字段的存取器被调用所返回的值。handler方法也可以增加一个Model类型的方法参数来增强model
  • void:如果处理器方法中已经对response响应数据进行了处理(比如在方法参数中定义一个ServletResponse或HttpServletResponse类型的参数并直接向其响应体中写东西),那么方法可以返回void。handler方法也可以增加一个Model类型的方法参数来增强model
  • HttpEntity<?>ResponseEntity<?>:用于提供对Servlet HTTP响应头和响应内容的存取,对象会被HttpMessageConverters转换成响应流
  • HttpHeaders:返回一个不含响应体的response
  • Callable<?>:当应用希望异步地返回方法值时使用,这个过程由Spring MVC自身的线程来管理
  • DeferredResult<?>:当应用希望方法的返回值交由线程自身决定时使用
  • ListenableFuture<?>:当应用希望方法的返回值交由线程自身决定时使用
  • ResponseBodyEmitter:可用它异步地向响应体中同时写多个对象,also supported as the body within a ResponseEntity
  • SseEmitter:可用它异步地向响应体中写服务器端事件(Server-Sent Events),also supported as the body within a ResponseEntity
  • StreamingResponseBody:可用它异步地向响应对象的输出流中写东西。also supported as the body within a ResponseEntity
  • 其他任何返回类型,都会被处理成model的一个属性并返回给视图,该属性的名称为方法级的@ModelAttribute所注解的字段名(或者以返回类型的类名作为默认的属性名)。model隐含填充了命令对象以及注解了@ModelAttribute字段的存取器被调用所返回的值
@RequestParam将请求参数绑定至方法参数
@RequestMapping(method = RequestMapping.GET)
public String setupForm(@RequestParam(value = "petId",required = "false",defaultValue = "pet") int petId, ModelMap model) {
    Pet pet = this.clinic.loadPet(petId);
    model.addAttribute("pet", pet);
    return "petForm";
}

当注解的参数类型是Map<String,String>或者MultiValueMap<String, String>,则该Map中会自动填充所有的请求参数。

@RequestBody注解映射请求体
@RequestMapping(path = "/something", method = RequestMethod.PUT)
public void handle(@RequestBody String body, Writer writer) throws IOException {
    writer.write(body);
}

请求体到方法参数的转换是由HttpMessageConverter完成的。HttpMessageConverter负责将HTTP请求信息转换成对象,以及将对象转换回一个HTTP响应体。对于@RequestBody注解,RequestMappingHandlerAdapter提供了以下几种默认的HttpMessageConverter支持:

  • ByteArrayHttpMessageConverter:用以转换字节数组
  • StringHttpMessageConverter:用以转换字符串
  • FormHttpMessageConverter:用以将表格数据转换成MultiValueMap<String, String>或从MultiValueMap<String, String>中转换出表格数据
  • SourceHttpMessageConverter:用于javax.xml.transform.Source类的互相转换
@ResponseBody注解映射响应体

@ResponseBody注解可被应用于方法上,标志该方法的return应该被直接写回到HTTP响应体中去,而不是被放置到Model中或被解释为一个视图名。

@RequestMapping(path = "/something", method = RequestMethod.PUT)
@ResponseBody
public String helloWorld() {
    return "Hello World"
}
HTTP实体HttpEntity
@RequestMapping("/something")
public ResponseEntity<String> handle(HttpEntity<byte[]> requestEntity) throws UnsupportedEncodingException {
    String requestHeader = requestEntity.getHeaders().getFirst("MyRequestHeader");
    byte[] requestBody = requestEntity.getBody();

    // do something with request header and body

    HttpHeaders responseHeaders = new HttpHeaders();
    responseHeaders.set("MyResponseHeader", "MyValue");
    return new ResponseEntity<String>("Hello World", responseHeaders, HttpStatus.CREATED);
}

与@RequestBody与@ResponseBody注解一样,Spring使用了HttpMessageConverter来对请求流和响应流进行转换

@ModelAttribute

@ModelAttribute注解可被应用在方法或方法参数上。
@ModelAttribute注解在方法上的作用:用于添加一个或多个属性到model上,这样的方法能接受与@RequestMapping注解相同的参数类型,只不过不能直接被映射到具体的请求上,在同一控制器中,注解了@ModelAttribute的方法实际上会在@RequestMapping方法之前被调用。

// Add one attribute
// The return value of the method is added to the model under the name "account"
// You can customize the name via @ModelAttribute("myAccount")

@ModelAttribute
public Account addAccount(@RequestParam String number) {
    return accountManager.findAccount(number);
}

// Add multiple attributes

@ModelAttribute
public void populateModel(@RequestParam String number, Model model) {
    model.addAttribute(accountManager.findAccount(number));
    // add more ...
}

@ModelAttribute注解在方法参数上的作用:用于数据绑定,数据来源的几种可能:

  • 它可能因为@SessionAttributes注解的使用已经存在于model中
  • 在同个控制器中使用了@ModelAttribute方法已经存在于model中
  • URI模板变量和类型转换中取得的
  • 调用了自身的默认构造器被实例化出来的
@RequestMapping(path = "/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@Valid @ModelAttribute("pet") Pet pet, BindingResult result) {
    if (result.hasErrors()) {
        return "petForm";
    }

    // ...
}
@SessionAttributes保存模型数据

类型级别的@SessionAttributes注解声明了某个特定处理器所使用的会话属性,通常它会列出该类型希望存储到session或converstaion中的model属性名或model的类型名,一般是用于在请求之间保存一些表单数据的bean。

@Controller
@SessionAttributes(value = {"user"},types = {String.class})
@RequestMapping("/appointments")
public class AppointmentsController {
    //...
}

保存model中指定的属性名或类型到session中。

@CookieValue注解映射cookie值

@CookieValue注解能将一个方法参数与一个HTTP cookie的值进行绑定。

@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@CookieValue("JSESSIONID") String cookie) {
    //...
}
@RequestHeader注解映射请求头属性

@RequestHeader注解能将一个方法参数与一个请求头属性进行绑定,@RequestHeader注解应用在Map<String, String>MultiValueMap<String, String>HttpHeaders类型的参数上,那么所有的请求头属性值都会被填充到map中

/**
 *  Host                    localhost:8080
 *  Accept                  text/html,application/xhtml+xml,application/xml;q=0.9
 *  Accept-Language         fr,en-gb;q=0.7,en;q=0.3
 *  Accept-Encoding         gzip,deflate
 *  Accept-Charset          ISO-8859-1,utf-8;q=0.7,*;q=0.7
 *  Keep-Alive              300
 */
@RequestMapping("/displayHeaderInfo.do")
public void displayHeaderInfo(@RequestHeader("Accept-Encoding") String encoding,
        @RequestHeader("Keep-Alive") long keepAlive) {
    //...
}

1.2.3 处理器映射(Handler Mappings)

RequestMappingHandlerMapping类会自动查找所有注解了@RequestMapping的@Controller控制器bean,同时所有继承自AbstractHandlerMapping的处理器方法映射HandlerMapping类都拥有下列的属性,你可以对它们进行定制:

  • interceptors:拦截器列表
  • defaultHandler:生效的默认处理器
  • order:根据order(见org.springframework.core.Ordered接口)属性的值,Spring会对上下文可用的所有处理器映射进行排序,并应用第一个匹配成功的处理器
  • alwaysUseFullPath:总是使用完整路径(默认为false)
  • urlDecode:默认设置为true(也是Spring 2.5的默认设置)。若你需要比较加密过的路径,则把此标志设为false。需要注意的是,HttpServletRequest永远以未加密的方式存储Servlet路径。此时,该路径将无法匹配到加密过的路径。

自定义拦截器

public class TimeBasedAccessInterceptor implements HandlerInterceptor {
    private int openingTime;
    private int closingTime;

    public void setOpeningTime(int openingTime) {
        this.openingTime = openingTime;
    }

    public void setClosingTime(int closingTime) {
        this.closingTime = closingTime;
    }
    /**
     * @desc 在处理器实际执行之前执行
     * 当方法返回 true时,处理器链会继续执行;
     * 若方法返回 false, DispatcherServlet即认为拦截器自身已经完成了
     * 对请求的处理(比如说,已经渲染了一个合适的视图),那么其余的拦
     * 截器以及执行链中的其他处理器就不会再被执行了
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return true;
    }
    /**
     * @desc 在处理器实际执行之后执行
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        Calendar cal = Calendar.getInstance();
        int hour = cal.get(HOUR_OF_DAY);
        if (openingTime <= hour && hour < closingTime) {
            return true;
        }
        response.sendRedirect("http://host.com/outsideOfficeHours.html");
        return false;
    }
    /**
     * @desc 在整个请求处理完成之后被执行
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

注册拦截器

<bean id="handlerMapping" class="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
    <property name="interceptors">
        <list>
            <ref bean="officeHoursInterceptor"/>
        </list>
    </property>
</bean>

<bean id="officeHoursInterceptor" class="com.yt.TimeBasedAccessInterceptor">
    <property name="openingTime" value="9"/>
    <property name="closingTime" value="18"/>
</bean>

所有被RequestMappingHandlerMapping处理的请求都会被TimeBasedAccessInterceptor拦截器所拦截。所有的拦截器都要实现HandlerInterceptor接口,也可以继承它的子类HandlerInterceptorAdapter更具有灵活性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值