【java学习】Spring MVC(Model View Controller)、ApplicationRunner

1,概念

SpringMVC:
Spring推出的基于Servlet标准的MVC框架实现

1)Spring MVC特性

  1. Spring MVC提供了一种绑定机制(请求参数名称与Java类的属性相匹配即可),通过该机制可以从用户请求中提取数据,然后将数据转换为预定义的数据格式,最后映射到一个模型类,从而创建一个对象。
  2. Spring MVC还是非侵入式的,因为业务逻辑代码与框架本身是分离的。

2,Spring MVC Annotation

注解场景说明备注
@EnableWebMvc在配置类中开启Web MVC的配置支持。
@Controller
@RequestMapping用于映射web请求,包括访问路径和参数。
@ResponseBody支持将返回值放到response内,而不是一个页面,通常用户返回json数据。
@RequestBody允许request的参数在request体中,而不是在直接连接的地址后面。(放在参数前)
@PathVariable用于接收路径参数
@RestController组合注解,相当于@Controller和@ResponseBody的组合,注解在类上,意味着,该Controller的所有方法都默认加上了@ResponseBody。Spring4之后加入的注解
@ControllerAdvice全局异常处理;全局数据绑定;全局数据预处理
@ExceptionHandler用于全局处理控制器里的异常。
@InitBinder用来设置WebDataBinder,WebDataBinder用来自动绑定前台请求参数到Model中。
@ModelAttribute用于定义Controller方法执行之前,对数据模型的操作。
@ResponseStatus在接口使用@ResponseStatus,在处理方法正确执行的前提下,后台返回HTTP响应的状态码为@ResponseStatus指定的状态码,但是浏览器依然可以正常渲染视图(在不使用@ResponseStatus的reson属性情况下)。加在@ExceptionHandler下方,该方法在捕获异常后,后台返回HTTP响应的状态码为@ResponseStatus指定的状态码,但是浏览器依然可以正常渲染视图(在不使用@ResponseStatus的reson属性情况下)。@ResponseStatus(code = HttpStatus.BAD_REQUEST, reason = “这是一个异常捕获”)

1)@Controller(控制组件)

用于标记在一个类上,使用它标记的类就是一个springmcv Controller对象,分发处理器将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping注解。@Controller只是定义了一个控制器类,而使用了@RequestMapping注解的方法才是真正处理请求的处理器。

1>与@RestControlle区别

@Controller:它把用户请求的数据经过业务处理层处理之后封装成一个Model ,然后再把该Model返回给对应的View进行展示,即该注解返回的是一个页面。
@RestControlle:返回的是json数据。

2>跨域:@CrossOrigin

跨域:
请求url的协议、域名、端口三者之间任意一个与当前页面url不同。
浏览器支持的是同源策略,不允许跨域是浏览器基本的安全策略。

@CrossOrigin中的2个参数:

  • origins : 允许可访问的域列表
  • maxAge:准备响应前的缓存持续的最大时间(以秒为单位)。

如果您正在使用Spring Security,请确保在Spring安全级别启用CORS,并允许它利用Spring MVC级别定义的配置。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and()...
    }
}
//为整个controller启用@CrossOrigin
@CrossOrigin(origins = "http://domain2.com", maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {

    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }
}


@CrossOrigin(maxAge = 3600)
@RestController
@RequestMapping("/account")
public class AccountController {
		//为单个方法启用跨域
    @CrossOrigin(origins = "http://domain2.com")
    @GetMapping("/{id}")
    public Account retrieve(@PathVariable Long id) {
        // ...
    }

    @DeleteMapping("/{id}")
    public void remove(@PathVariable Long id) {
        // ...
    }
}

2)@RequestMapping

用于标记在一个方法或类上,用来处理请求地址映射的注解,用于类上,表示类中所有响应请求处理的方法都是以该地址作为父路径,返回值会通过视图解析器解析为实际的物理视图,然后做转发操作。

属性说明备注
value指定请求的实际地址多个url映射:@GetMapping(value = {"/check-list/{type}", "/check-map/{type}"})
method指定请求的method类型
consumes指定处理请求的内容提交类型(Content-Type),例如application/josn,text/html
produces指定返回的内容类型,仅当request请求头中的(Accept)类型中包含该指定类型才返回
param指定request中必须包含的参数值
headers指定request中必须包含某些指定的header值,才能让改方法处理请求

spring4.3引入:@GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@PatchMapping,@RequestMapping是他们的父类注解。

org.springframework.web.bind.annotation

@GetMapping相当于@RequestMapping(method = RequestMethod.GET)
@PostMapping相当于@RequestMapping(method = RequestMethod.POST)

同时又引入了三个参数注解:

1>@PathVaribale

对应http访问的路径参数。

@GetMapping(value = "/user/{id}")
public ResponseMessage getUser(@PathVariable Long id){
}

2>@RequestParam

适合参数较少,参数固定的情况。
对应http访问的query参数。

属性说明
value请求参数名(必须配置)
required是否必需,默认为 true,即 请求中必须包含该参数,如果没有包含,将会抛出异常(可选配置)
defaultValue默认值,如果设置了该值,required 将自动设为 false,无论你是否配置了required,配置了什么值,都是 false(可选配置)

3>@RequestBody

org.springframework.web.bind.annotation

适用于复杂的数据结构,比如用xml或者json定义的复杂对象。
对应的是http request payload,数据在http请求的body上,也就是在HttpServletRequest.getInputStream里面。

@PostMapping(value = "")
public ResponseMessage modifyUser(@RequestBody User user){
}

4>@RequestHeader

org.springframework.web.bind.annotation
@RequestMapping("/getSingleHeader")
public Map<String, Object> getSingleHeader(@RequestHeader("user-id") String userId){
        Map<String, Object> result = new HashMap<>();
        result.put("code", 0);
        result.put("msg", "success");
        result.put("userId", userId);
        return result;
}

//一次性获取所有请求头--MultiValueMap
@RequestMapping("/listHeaders")
public Map<String, Object> listHeaders(@RequestHeader MultiValueMap<String, String> headers) {
        Map<String, Object> result = new HashMap<>();
        headers.forEach((key, value) -> {
             // 日志中输出所有请求头
            System.out.println(String.format("Header '%s' = %s", key, value));
        });
        result.put("code", 0);
        result.put("msg", "success");
        result.put("headers", headers);
        return result;
}

//一次性获取所有请求头--HttpHeaders (底层就是MultiValueMap实现的)
@RequestMapping("/getAllHttpHeaders")
public Map<String, Object> getAllHttpHeaders(@RequestHeader HttpHeaders headers) {
        headers.forEach((key, value) -> {
             // 日志中输出所有请求头
            System.out.println(String.format("getAllHttpHeaders '%s' = %s", key, value));
        });
        Map<String, Object> result = new HashMap<>();
        result.put("code", 0);
        result.put("msg", "success");
        result.put("headers", headers);
        return result;
}

5>@CookieValue

作用:用来获取Cookie中的值;

@RequestMapping("/testCookieValue")
    public String testCookieValue(@CookieValue("JSESSIONID") String sessionId) {
        System.out.println("JSESSIONID = " + sessionId);
        return "success";
    }

6>@SessionAttributes

将值放到session作用域中,写在class上面。

@SessionAttributes(value = {"user"}, types = {String.class})

3)@ModelAttribute

  1. 通常被用来填充一些公共需要的属性或数据;
    比如权限的验证(也可以使用Interceptor)等。
  2. 先执行@ModelAttribute方法,再执行Controller方法。

1>实现

  1. 注释方法并定义模型数据
    在没有@RequestMapping标注的方法上建立模型数据,抽象一个公共BaseController。
abstract public class BaseController{
    @ModelAttribute("account")
    public User newUser(@RequestHeader HttpHeaders header){
        User user = new User();
        user.setName("myUser");
        user.setId(1);
        return user;
    }
}

  1. 注释参数引用模型数据
@Controller
public class MyController extends BaseController {  
    @RequestMapping("user")
    @ResponseBody
    public void getUser(@ModelAttribute("account")User user){
        System.out.println(user.toString());
    }
}

4)@Transactional

5)@ControllerAdvice及@ExceptionHandler

@ControllerAdvice就是@Controller 的增强版。@ControllerAdvice主要用来处理全局数据,一般搭配@ExceptionHandler、@ModelAttribute以及@InitBinder使用。

6)@InitBinder

用于在@Controller中标注于方法上,表示为当前控制器注册一个属性编辑器,只对当前的Controller有效。@InitBinder标注的方法必须有一个参数WebDataBinder。所谓的属性编辑器可以理解就是帮助我们完成参数绑定。

		//例如当请求是/test?name=%20zero%20&date=2018-05-22时,会把zero绑定到name,再把时间串格式化为Date类型,再绑定到date。
    @ResponseBody
    @RequestMapping(value = "/test")
    public String test(@RequestParam String name,@RequestParam Date date) throws Exception {
        System.out.println(name);
        System.out.println(date);
        return name;
    }

    @InitBinder
    public void initBinder(WebDataBinder binder){
    		//把String类型的参数先trim再绑定
        binder.registerCustomEditor(String.class,
                new StringTrimmerEditor(true));
			//对于Date类型的参数会先格式化在绑定
        binder.registerCustomEditor(Date.class,
                new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));

    }

这里的@InitBinder方法只对当前Controller生效,要想全局生效,可以使用@ControllerAdvice。通过@ControllerAdvice可以将对于控制器的全局配置放置在同一个位置,注解了@ControllerAdvice的类的方法可以使用@ExceptionHandler,@InitBinder,@ModelAttribute注解到方法上,这对所有注解了@RequestMapping的控制器内的方法有效。

@ControllerAdvice
public class GlobalControllerAdvice {

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(String.class,
                new StringTrimmerEditor(true));

        binder.registerCustomEditor(Date.class,
                new CustomDateEditor(new SimpleDateFormat("yyyy-MM-dd"), false));

    }
}

3,Dispatcher Servlet(前端控制器)

Dispatcher Servlet是Spring web的核心元素,跟之前的Servlet作用相似,都是用来接收用户请求,然后去后台匹配合适的handler。

Tomcat:
The Apache Tomcat software is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies.

Servlet:
1.Servlet不是框架,它是java库里面的一个类,Servlet是服务器端运行的一个程序。
2.当web容器启动的时候并且执行的时候,Servlet类就会被初始化。
3.用户通过浏览器输入url时,请求到达Servlet来接收并且根据servlet配置去处理。
通常项目中会用到不同的web容器,我这里用到是比较常见的Tomcat。在eclipse里面创建一个java web项目,会有一个WEB-INF的文件夹,为了不轻易被外界访问到,这个文件夹底下的文件都是受保护的。文件夹中包括了一个很重要的配置文件,web.xml,我们要实现的不同Servlet也要在这里配置才能使用。

1)主要工作流

Controller接受了一个Request,分发给了一个RequestHandler,并返回一个Response对象。
在这里插入图片描述

①前端请求到达DispatcherServlet

②DispatcherServlet请求HandlerMappering(处理器映射) 查找Handler

DispatcherServlet 对请求URL进行解析,得到请求资源标识符(URI),然后根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括一个Handler处理器对象、多个HandlerInterceptor拦截器对象),最后以HandlerExecutionChain对象的形式返回。

Dispatcher Servlet扫描被@Controller注解的类,从而将web请求映射到被@RequestMapping注解的方法上。
@PathVariable注解相关参数,将一个方法参数绑定到一个url模板。

//@PathVariable用法
@RequestMapping(value = "/test/{id}",method = RequestMethod.DELETE)
public Result test(@PathVariable("id")String id) 

//@RequestParam用法,注意这里请求后面没有添加参数
@RequestMapping(value = "/test",method = RequestMethod.POST)
public Result test(@RequestParam(value="id",required=false,defaultValue="0")String id) 
注意上面@RequestParam用法当中的参数。

@ExceptionHandler 可以定义方法来处理控制器类中发生的异常。

③DispatcherServlet 执行handler

根据获得的Handler,选择一个合适的HandlerAdapter。提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)。 在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:

HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
数据转换:对请求消息进行数据转换。如String转换成IntegerDouble等
数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResultError

④Handler执行完成后,返回ModelAndView对象

根据返回的ModelAndView,选择一个适合的ViewResolver返回给DispatcherServlet;
ViewResolver(视图解析器) 结合Model和View,来渲染视图,最后将渲染结果返回给客户端。

⑦DispatcherServlet将模型传递给视图,并在浏览器中展示视图

4,过滤器(Filter)

1)概念

过滤器是JavaWeb的三大组件之一,是实现Filter接口的Java类。

过滤器是实现对请求资源(jsp、servlet、html)的过滤功能,是一个运行在服务器的程序,优先于请求资源(jsp、servlet、html)之前执行。

当浏览器发送请求给服务器的时候,先执⾏过滤器,然后才访问Web的资源。服务器响应Response,从Web资源抵达浏览器之前,也会途径过滤器。

在很多Web开发中,都会用到过滤器(Filter),如参数过滤、防止SQL注入、防止页面攻击、过滤敏感字符、解决网站乱码、空参数矫正、Token验证、Session验证、点击率统计等。

2)场景

针对所有接口做全局过滤。

3)方法

Filter抽象类包含三个方法:

1> init()

该方法在容器启动初始化过滤器时被调用,它在Filter的整个生命周期只会被调用一次,这个方法必须执行成功,否则过滤器会不起作用。

2>doFilter()

容器中的每一次请求都会调用该方法,FilterChain用来调用下一个过滤器Filter。

@Override
public void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3) throws IOException, ServletException{
  //请求(request)处理逻辑
  //请求(request)封装逻辑
  //chain重新写回request和response
}

其中,参数FilterChain是一个接口,里面又定义了doFilter()方法,这是因为在Java中使⽤了链式结构。把所有的过滤器都放在FilterChain⾥边,如果符合条件,就执⾏下⼀个过滤器(如果没有过滤器了,就执⾏⽬标资源)。

3>destroy()

容器销毁时被调用。一般在方法中销毁或关闭资源,也只会被调用一次。

4>demo

@Slf4j
@Order(1)  //如果有多个Filter,则序号越小,越早被执行
//@Component//无需添加此注解,在启动类添加@ServletComponentScan注解后,会自动将带有@WebFilter的注解进行注入!
//这里的urlPatterns为接口里的路径过滤条件
@WebFilter(filterName = "timeFilter", urlPatterns = "/api/filter/*")
public class TimeFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("初始化过滤器:{}", filterConfig.getFilterName());
    }
    @Override
    public void destroy() {
        log.info("销毁过滤器");
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        log.info("开始执行");
        long startTime = System.currentTimeMillis();
        filterChain.doFilter(servletRequest, servletResponse);
        long endTime = System.currentTimeMillis();
        log.info("请求:{},耗时:{}ms", getUrlFrom(servletRequest), (endTime - startTime));
        log.info("结束执行");
    }
    private String getUrlFrom(ServletRequest servletRequest) {
        if (servletRequest instanceof HttpServletRequest) {
            return ((HttpServletRequest) servletRequest).getRequestURL().toString();
        }
        return "";
    }
}

在启动类添加一个注解,找到定义的拦截器:

@ServletComponentScan(basePackages = "com.luo.filter")

测试接口:

@RestController
@Slf4j
@RequestMapping("/api/filter")
public class FilterUserController {
    @GetMapping("/getUserList")
    public List<String> getUser() {
        log.info("开始业务逻辑处理。");
        List<String> list = new ArrayList<>();
        list.add("张三");
        list.add("李四");
        list.add("王五");
        log.info("业务逻辑处理结束。");
        return list;
    }
}

5,拦截器(Interceptor)

1)概念

SpringMVC的处理器拦截器类似于Servlet开发中的过滤器Filter,用于对处理器进行预处理和后处理。开发者可以自己定义一些拦截器来实现特定的功能。

拦截器它是链式调用,一个应用中可以同时存在多个拦截器Interceptor,一个请求也可以触发多个拦截器,而每个拦截器的调用会依据它的声明顺序依次执行。

2)场景

3)api

HandlerInterceptor 抽象类;包含三个方法:

1>preHandle()

这个方法将在请求处理之前进行调用。「注意」:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。

2>postHandle()

只有在 preHandle() 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。「有意思的是」:postHandle() 方法被调用的顺序跟 preHandle() 是相反的,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。

3>afterCompletion()

只有在 preHandle() 方法返回值为true 时才会执行,在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。

4)demo

SpringBoot实现一个登录拦截器:用户在访问首页接口,先判断一下session,如果session中有user的信息,说明用户已经登录过了,能正常访问首页接口,否则跳转到登录页面,让用户进行登录。

拦截器类:实现handlerInterceptor接口。

@Component
@Slf4j
public class UserInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //业务拦截相关规则
        //从session中获取用户的信息
        InterceptorUserEntity user = (InterceptorUserEntity) request.getSession().getAttribute("user");
        //判断用户是否登录
        if (null == user) {
            response.sendRedirect(request.getContextPath() + "/api/interceptor/login");
            return false;
        }
        //需要返回true,否则请求不会被控制器处理
        return true;
    }
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后),如果异常发生,则该方法不会被调用");
    }
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("在整个请求结束之后被调用,也就是在DispatcherServlet渲染了对应的视图之后执行(主要是用于进行资源清理工作)");
    }
}

使用@Configuration注解写一个拦截器的配置文件:

//.addPathPatterns表示作用范围。(只在这个interceptor下的所有接口进行拦截)
//.excludePathPatterns表示放行。这里把登录页面和已登录完成(setSession)放行。
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Autowired
    private UserInterceptor userInterceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(userInterceptor).addPathPatterns("/api/interceptor/**").excludePathPatterns("/**/login", "/**/setSession");
    }
}

测试代码:

@Data
public class InterceptorUserEntity {
    private Integer id;
    private String name;
}

@RestController
@Slf4j
@RequestMapping("/api/interceptor")
public class InterceptorUserController {
    @GetMapping("/setSession")
    @ResponseBody
    public Object setSession(HttpServletRequest request) {
        //将用户信息存放到session中
        InterceptorUserEntity user = new InterceptorUserEntity();
        user.setId(001);
        user.setName("张三");
        request.getSession().setAttribute("user", user);
        return "已进行登录!";
    }
    /**
     * 用户登录后跳转到首页
     *
     * @return
     */
    @GetMapping("/index")
    public Object index() {
        return "这里是首页!";
    }
    /**
     * 登录页面
     *
     * @return
     */
    @GetMapping("/login")
    public Object login() {
        return "请进行登录!";
    }
}

6,过滤器与拦截器的区别

1)相同点

过滤器与拦截器都体现了AOP的编程思想,都可以实现例如日志、登录鉴权等功能。

2)不同点

①:拦截器是基于java的反射机制(动态代理)的实现,而过滤器是基于函数的回调。
②:拦截器不依赖于servlet容器,而过滤器依赖于servlet容器。
③:拦截器只对Controller请求起作用,而过滤器则可以对几乎所有的请求起作用。
④:拦截器可以访问Controller上下文、值、栈里面的对象,而过滤器不可以。
⑤:在spring容器的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
⑥:拦截器可以获取IOC容器中的各个bean,而过滤器不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。

3)触发机制不同

在这里插入图片描述过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

7,ApplicationRunner

@FunctionalInterface
public interface ApplicationRunner {
    void run(ApplicationArguments args) throws Exception;
}
  1. spring程序启动后,run方法会被自动调用。
  2. 执行顺序:
  3. 场景:初始化操作。
    实例化Kafka客户端,异步加载缓存。

1)使用


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;

@Component
public class StartRegisterSchedule implements ApplicationRunner {

    @Autowired
    RestTemplate restTemplate;

    @Override
    public void run(ApplicationArguments args) {
			 // 获取命令行参数
        String[] sourceArgs = args.getSourceArgs();
        List<String> nonOptionArgs = args.getNonOptionArgs();
        Set<String> optionNames = args.getOptionNames();
    }
}

2)问题

  1. ApplicationRunner没有调用?
    可能其他启动任务非异步调用被阻塞了。

8,幂等性

1>概念

某个操作,任意多次执行所产生的影响均与一次执行的影响相同。
举例:多次支付同一笔订单。

2>场景及处理方式

  1. 前端未做限制,导致用户重复提交
  2. 使用浏览器后退,或者按F5刷新,或者使用历史记录,重复提交表单
  3. 网络波动,引起重复请求
  4. 超时重试,引起接口重复调用
  5. 定时任务设置不合理,导致数据重复处理
  6. 使用消息队列时,消息重复消费

4>解决方案

  1. 前端重定向
    页面重定向(PRG),当用户进行表单提交时,会重定向到另外一个提交成功页面,而不是停留在原先的表单页面。这样就避免了用户刷新导致重复提交。同时防止了通过浏览器按钮前进/后退导致表单重复提交。
  2. 后端一锁二判三更新
    约定一个唯一ID作为幂等号。
    如主键、操作流水、状态机等。
    另外,在返回参数中标记是因为幂等成功(responseCode=DUPLICATED)。
    1)加锁
    对幂等号加锁。
    用redis分布式锁:非阻塞高效互斥锁。
    2)状态校验
    基于状态机、流水表、唯一索引等等进行重复操作的判断。
    3)更新时将数据进行持久化。
    数据库要定义好唯一约束,控制脏数据入库。
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值