SpringBoot 的入门学习(4): 拦截器,文件上传,错误处理,原生组件注解,嵌入式容器定制化

一、 拦截器

编写拦截器

具体步骤如下:

  • 编写一个拦截器实现 HandlerInterceptor 接口
  • 拦截器注册到容器中(实现 WebMvcConfigureraddInterceptors()
  • 指定拦截规则(注意,如果是拦截所有,静态资源也会被拦截】

具体代码如下:

  • 编写一个实现HandlerInterceptor接口的拦截器:

    @Slf4j
    public class LoginInterceptor implements HandlerInterceptor {
    
        /**
         * 目标方法执行之前
         */
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
            String requestURI = request.getRequestURI();
            log.info("preHandle拦截的请求路径是{}",requestURI);
    
            //登录检查逻辑
            HttpSession session = request.getSession();
    
            Object loginUser = session.getAttribute("loginUser");
    
            if(loginUser != null){
                //放行
                return true;
            }
    
            //拦截住。未登录。跳转到登录页
            request.setAttribute("msg","请先登录");
    //        re.sendRedirect("/");
            request.getRequestDispatcher("/").forward(request,response);
            return false;
        }
    
        /**
         * 目标方法执行完成以后
         */
        @Override
        public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
            log.info("postHandle执行{}",modelAndView);
        }
    
        /**
         * 页面渲染以后
         */
        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            log.info("afterCompletion执行异常{}",ex);
        }
    }
    
  • 拦截器注册到容器中 && 指定拦截规则:

    @Configuration
    public class AdminWebConfig implements WebMvcConfigurer{
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(new LoginInterceptor())//拦截器注册到容器中
                    .addPathPatterns("/**")  //所有请求都被拦截包括静态资源
                    .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
                            "/js/**","/aa/**"); //放行的请求
    }
    
    

源码分析-拦截器的执行时机和原理

  • 1、根据当前请求,找到 HandlerExecutionChain(可以处理请求的 handler 以及 handler 的所有 拦截器)
  • 2、 先来顺序执行 所有拦截器的 preHandle()方法。
    • 如果当前拦截器 preHandle() 返回为true。则执行下一个拦截器的preHandle()
    • 如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion();
  • 3、如果任何一个拦截器返回 false,直接跳出不执行目标方法。
  • 4、所有拦截器都返回true,才执行目标方法。
  • 5、倒序执行所有拦截器的postHandle()方法
  • 6、前面的步骤有任何异常都会直接倒序触发 afterCompletion()
  • 7、页面成功渲染完成以后,也会倒序触发 afterCompletion()

在这里插入图片描述

二、文件上传

单文件与多文件上传的使用

  • 页面代码 /static/form/form_layouts.html

  • 当个就加入一个 file,多个就外加一个 multiple

    <form role="form" th:action="@{/upload}" method="post" enctype="multipart/form-data">
        <div class="form-group">
            <label for="exampleInputEmail1">邮箱</label>
            <input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
        </div>
        
        <div class="form-group">
            <label for="exampleInputPassword1">名字</label>
            <input type="text" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
        </div>
        
        <div class="form-group">
            <label for="exampleInputFile">头像</label>
            <input type="file" name="headerImg" id="exampleInputFile">
        </div>
        
        <div class="form-group">
            <label for="exampleInputFile">生活照</label>
            <input type="file" name="photos" multiple>
        </div>
        
        <div class="checkbox">
            <label>
                <input type="checkbox"> Check me out
            </label>
        </div>
        <button type="submit" class="btn btn-primary">提交</button>
    </form>
    
    
  • 控制层代码:保存图片

    @Slf4j
    @Controller
    public class FormTestController {
    
        @GetMapping("/form_layouts")
        public String form_layouts(){
            return "form/form_layouts";
        }
    
        @PostMapping("/upload")
        public String upload(@RequestParam("email") String email,
                             @RequestParam("username") String username,
                             @RequestPart("headerImg") MultipartFile headerImg,
                             @RequestPart("photos") MultipartFile[] photos) throws IOException {
    
            log.info("上传的信息:email={},username={},headerImg={},photos={}",
                     email,username,headerImg.getSize(),photos.length);
    
            if(!headerImg.isEmpty()){
                //保存到文件服务器,OSS服务器
                String originalFilename = headerImg.getOriginalFilename();
                headerImg.transferTo(new File("H:\\cache\\"+originalFilename));
            }
    
            if(photos.length > 0){
                for (MultipartFile photo : photos) {
                    if(!photo.isEmpty()){
                        String originalFilename = photo.getOriginalFilename();
                        photo.transferTo(new File("H:\\cache\\"+originalFilename));
                    }
                }
            }
    
    
            return "main";
        }
    }
    
    

文件上传相关的配置类:

  • org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
  • org.springframework.boot.autoconfigure.web.servlet.MultipartProperties

文件大小相关配置项:

spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB

源码流程-文件上传参数解析器

文件上传相关的自动配置类MultipartAutoConfiguration有创建文件上传参数解析器StandardServletMultipartResolver通过名字就可以看出该解析器支持servlet解析器
在这里插入图片描述

  • 从注解 condtional 就可以知道,和其他解析器的读取相同,只有在没有自定义的文件上传解析器的情况下,springboot 才会使用默认的文件解析器。

具体步骤:

  • 1、 还是从 dispatchServlet 类中的 doDispatch 开始,doDispatch 存在一个flag,用于标记是否该Request 为文件上传请求。如果是文件上传请求,会对该请求进行一次包装。
    在这里插入图片描述
  • 2、进入 CheckPart函数可以发现,判断请求是否为文件上传请求,使用的就是 文件上传解析器 multipartResolver
    在这里插入图片描述
    再深入的话,会发现判断request是否为文件上传请求的方法就是查看request的消息头,如果消息头是 multipart/ 则该request 为文件上传
    在这里插入图片描述
  1. 如果判断出该request为文件上传请求,那么在check函数的最后,我们会使用文件上传解析器对请求进行解析 resolvePartList

三、错误处理

默认错误处理规则

Spring Boot官方文档 - Error Handling

By default, Spring Boot provides an /error mapping that handles all errors in a sensible way, and it is registered as a “global” error page in the servlet container. For machine clients, it produces a JSON response with details of the error, the HTTP status, and the exception message. For browser clients, there is a “whitelabel” error view that renders the same data in HTML format (to customize it, add a View that resolves to error).
There are a number of server.error properties that can be set if you want to customize the default error handling behavior. See the “Server Properties” section of the Appendix

SpringBoot默认错误处理机制:

  • 默认情况下,Spring Boot遇到错误,回转到1 /error 处理所有错误的映射
    • 对于浏览器客户端,会响应一个“ whitelabel” 错误视图,以HTML格式呈现相同的数据
      在这里插入图片描述
    • 对于非浏览器客户端,又叫机器客户端,它将生成JSON响应,其中包含时间戳信息,错误信息,HTTP状态和异常消息的详细信息。如下:
      {
        "timestamp": "2020-11-22T05:53:28.416+00:00",
        "status": 404,
        "error": "Not Found",
        "message": "No message available",
        "path": "/asadada"
      }
      

对于自定义视图,官方文档给出了下面几种方法:

  • /templates/error/下的 格式为 4xx,5xx 的页面会被自动解析为错误页面。(所以把错误页面放到该文件夹中就可以使用了)
    在这里插入图片描述
  • 上面的json错误信息也会随之返回到错误页面,因此可以在错误页面中添加一些读操作,显示错误信息。
    在这里插入图片描述

源码流程-默认规则原理

MVC 自动配置包中有一个error文件,里面存在着 错误处理的相关类:
在这里插入图片描述
其中的 ErrorMVCAutoCOnfiguration 就是用来自动配置错误处理的,即自动配置异常处理规则。
该方法导入了一些server 和webMVC的配置文件:
在这里插入图片描述

== 该自动配置包存在两个重要的组件:
BasicErrorController 和 DefaultErrorAttributes 。==

第一个重要组件BasicErrorController --> id:basicErrorController

浏览器端: 返回 默认错误视图(白页)的逻辑

  • 根据命名规则可以看住这是一个,controller,点开这个controller可以看到:
    在这里插入图片描述
    如果我们没有配置 sever.error.path 和 error.path 这两个变量是,它们就为null,则错误的默认request请求为 /error

  • 在BasicErrorController 的处理方法中,有一个返回 ModelAndViewer的方法:new ModelAndView("error", model); 即返回一个 error 页面
    在这里插入图片描述

  • 而这个error 页面的定义也在该方法中:该方法通过error 字符,返回error页面。
    在这里插入图片描述

  • 这个static veiw 里面,描写了默认错误页面的html
    在这里插入图片描述

非浏览器端: 返回一个json 数据

  • 在这个controller中还存着这一个返回 json 的方法,如下:
    在这里插入图片描述
第二个重要组件DefaultErrorAttributes->id:errorAttributes

在这里插入图片描述
进入 DefaultErrorAttributes 类:

  • 该DefaultErrorAttributes 实现了两个接口: ErrorAttributes 和 HandlerExceptionResolver
  • DefaultErrorAttributes:定义错误页面中可以包含数据(异常明细,堆栈信息等)。
    在这里插入图片描述
第三个已过时组件:DefaultErrorViewResolver -> id:conventionErrorViewResolver

该组件中可以看到:如果发生异常错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面(主要作用)。

  • 该组件中存在 “4xx”和“5xx” 两个常用状态码
  • 并且在返回是会把状态码与 “/error” 进行拼接。即只能识别error中的“5xx”和”4xx“。
  • 如果想要返回页面,就会找error视图(StaticView默认是一个白页)。

源码流程- 出现异常时的错误页面跳转流程

如果控制层出现除以0的操作:

@Slf4j
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String handle01(){

        int i = 1 / 0;//将会抛出ArithmeticException

        log.info("Hello, Spring Boot 2!");
        return "Hello, Spring Boot 2!";
    }
}

①:异常的捕捉与解析
  • 1、当浏览器发出/hello请求,DispatcherServlet的doDispatch()的mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); 将会抛出 ArithmeticException。

  • 2、catch 捕捉到异常后,会调用 processDispatchResult函数。该函数会判断是否存在异常,如果无异常则返回结果。有异常调用 processHandlerException 处理异常

  • 3、 processHandlerException 方法会遍历所有的集成了 handlerExceptionResolvers 的异常解析器,并且利用匹配的哪一个进行异常解析与处理。

  • 上面这些流程的代码如下:

    public class DispatcherServlet extends FrameworkServlet {
        ...
    	protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    		...
    				// Actually invoke the handler.
                	//将会抛出ArithmeticException
    				mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
    
    				applyDefaultViewName(processedRequest, mv);
    				mappedHandler.applyPostHandle(processedRequest, response, mv);
    			}
    			catch (Exception ex) {
                    //将会捕捉ArithmeticException
    				dispatchException = ex;
    			}
    			catch (Throwable err) {
    				...
    			}
        		//捕捉后,继续运行
    			processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    		}
    		catch (Exception ex) {
    			triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    		}
    		catch (Throwable err) {
    			triggerAfterCompletion(processedRequest, response, mappedHandler,
    					new NestedServletException("Handler processing failed", err));
    		}
    		finally {
    			...
    		}
    	}
    
    	private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
    			@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
    			@Nullable Exception exception) throws Exception {
    
    		boolean errorView = false;
    
    		if (exception != null) {
    			if (exception instanceof ModelAndViewDefiningException) {
    				...
    			}
    			else {
    				Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
    				//ArithmeticException将在这处理
                    mv = processHandlerException(request, response, handler, exception);
    				errorView = (mv != null);
    			}
    		}
    		...
    	}
    
    	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处理器异常解析器
    			for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
    				exMv = resolver.resolveException(request, response, handler, ex);
    				if (exMv != null) {
    					break;
    				}
    			}
    		}
    		...
    	
            //若只有系统的自带的异常解析器(没有自定义的),异常还是会抛出
    		throw ex;
    	}
    
    }
    
    
  • 系统自带的异常解析器:
    在这里插入图片描述

②:异常的解析

1、 DefaultErrorAttributes 先来处理异常,它主要功能把异常信息保存到 request 域,并且返回null。
2、如果没有没有默认的解析器(上图的HandlerExceptionResolverComposite)能处理异常,最后异常会被抛出。
3、最终对底层就会转发 \error 请求,如图BasicErrorController 就会处理这个请求,并根据设备情况以及错误信息返回具体的内容。

③:几种异常处理原理

1、自定义错误页

  • 请求格式为 error/404.html error/5xx.html
  • 有精确的错误状态码页面就匹配精确,没有就找 4xx.html;
  • 如果都没有就触发白页

2、使用@ControllerAdvice+@ExceptionHandler处理全局异常;

  • 该方法底层是 ExceptionHandlerExceptionResolver

    @Slf4j
    @ControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler({ArithmeticException.class,NullPointerException.class})  //处理异常
        public String handleArithException(Exception e){
    
            log.error("异常是:{}",e);
            return "login"; //视图地址
        }
    }
    

3、@ResponseStatus+自定义异常 ;

  • 底层是 ResponseStatusExceptionResolver ,把responseStatus注解的信息底层调用reponse.sendError(statusCode, resolvedReason),tomcat发送的/error

    @ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多")
    public class UserTooManyException extends RuntimeException {
    
        public  UserTooManyException(){
    
        }
        public  UserTooManyException(String message){
            super(message);
        }
    }
    
    
    @Controller
    public class TableController {
        
    	@GetMapping("/dynamic_table")
        public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
            //表格内容的遍历
    	     List<User> users = Arrays.asList(new User("zhangsan", "123456"),
                    new User("lisi", "123444"),
                    new User("haha", "aaaaa"),
                    new User("hehe ", "aaddd"));
            model.addAttribute("users",users);
    
            if(users.size()>3){
                throw new UserTooManyException();//抛出自定义异常
            }
            return "table/dynamic_table";
        }
        
    }
    

4、Spring自家异常如 org.springframework.web.bind.MissingServletRequestParameterException,DefaultHandlerExceptionResolver 处理Spring自家异常。

  • response.sendError(HttpServletResponse.SC_BAD_REQUEST/400/, ex.getMessage());

5、自定义实现 HandlerExceptionResolver 处理异常;

  • 可以作为默认的全局异常处理规则

    @Order(value= Ordered.HIGHEST_PRECEDENCE)  //优先级,数字越小优先级越高
    @Component
    public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
        @Override
        public ModelAndView resolveException(HttpServletRequest request,
                                             HttpServletResponse response,
                                             Object handler, Exception ex) {
    
            try {
                response.sendError(511,"我喜欢的错误");
            } catch (IOException e) {
                e.printStackTrace();
            }
            return new ModelAndView();
        }
    }
    
    

6、自定义ErrorViewResolver 实现处理异常

  • 代码如下:

    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class BasicErrorController extends AbstractErrorController {
    
        ...
        
    	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    	public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    		HttpStatus status = getStatus(request);
    		Map<String, Object> model = Collections
    				.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    		response.setStatus(status.value());
    		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    	}
        
        protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
    			Map<String, Object> model) {
            //这里用到ErrorViewResolver接口
    		for (ErrorViewResolver resolver : this.errorViewResolvers) {
    			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
    			if (modelAndView != null) {
    				return modelAndView;
    			}
    		}
    		return null;
    	}
        
        ...
        
    }
    
    
  • response.sendError(),error请求就会转给controller。

  • 你的异常没有任何人能处理,tomcat底层调用response.sendError(),error请求就会转给controller。

  • basicErrorController 要去的页面地址是 ErrorViewResolver 。

    @FunctionalInterface
    public interface ErrorViewResolver {
    
    	ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);
    
    }
    
    

四:原生组件的注解与注入:servlet,filter和 LIstener

官方文档

使用注解

  • 使用原生注解 servlet 指定请求

    @WebServlet(urlPatterns = "/my")
    public class MyServlet extends HttpServlet {
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            resp.getWriter().write("66666");
        }
    }
    
    
  • 注册过滤器

    @Slf4j
    @WebFilter(urlPatterns={"/css/*","/images/*"}) //my
    public class MyFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
            log.info("MyFilter初始化完成");
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            log.info("MyFilter工作");
            chain.doFilter(request,response);
        }
    
        @Override
        public void destroy() {
            log.info("MyFilter销毁");
        }
    }
    
    
  • 注册监听器

    @Slf4j
    @WebListener
    public class MyServletContextListener implements ServletContextListener {
    
    
        @Override
        public void contextInitialized(ServletContextEvent sce) {
            log.info("MySwervletContextListener监听到项目初始化完成");
        }
    
        @Override
        public void contextDestroyed(ServletContextEvent sce) {
            log.info("MySwervletContextListener监听到项目销毁");
        }
    }
    
  • 最后还要在主启动类添加注解@ServletComponentScan,用以扫描所有注解

    @ServletComponentScan(basePackages = "com.lun")//
    @SpringBootApplication(exclude = RedisAutoConfiguration.class)
    public class Boot05WebAdminApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(Boot05WebAdminApplication.class, args);
        }
    }
    
    

使用注入

  • ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean

    @Configuration(proxyBeanMethods = true)//保证所有的bean都为单例
    public class MyRegistConfig {
    
        @Bean
        public ServletRegistrationBean myServlet(){
            MyServlet myServlet = new MyServlet();
    
            return new ServletRegistrationBean(myServlet,"/my","/my02");
        }
    
    
        @Bean
        public FilterRegistrationBean myFilter(){
    
            MyFilter myFilter = new MyFilter();
    //        return new FilterRegistrationBean(myFilter,myServlet());
            FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
            filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
            return filterRegistrationBean;
        }
    
        @Bean
        public ServletListenerRegistrationBean myListener(){
            MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
            return new ServletListenerRegistrationBean(mySwervletContextListener);
        }
    }
    
    

五、嵌入式Servlet容器

官网文档

切换web服务器与定制化

默认支持的WebServer

  • Tomcat, Jetty, or Undertow。

  • ServletWebServerApplicationContext容器启动寻找ServletWebServerFactory 并引导创建服务器。

  • Boot默认使用Tomcat服务器,若需更改其他服务器,则修改工程pom.xml:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jetty</artifactId>
    </dependency>
    
    

原理

  • SpringBoot应用启动发现当前是Web应用,web场景包-导入tomcat。

  • web应用会创建一个web版的IOC容器 ServletWebServerApplicationContext

  • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂——>Servlet 的web服务器)。
    SpringBoot底层默认有很多的WebServer工厂(ServletWebServerFactoryConfiguration内创建Bean),如:
    TomcatServletWebServerFactory、JettyServletWebServerFactory、UndertowServletWebServerFactory

  • 底层直接会有一个自动配置类 ServletWebServerFactoryAutoConfiguration

  • ServletWebServerFactoryAutoConfiguration 导入了 ServletWebServerFactoryConfiguration(配置类)

  • ServletWebServerFactoryConfiguration 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory

  • TomcatServletWebServerFactory创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize——this.tomcat.start();

  • 内嵌服务器,与以前手动把启动服务器相比,改成现在使用代码启动(tomcat核心jar包存在)。

定制Servlet容器

  • 实现WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
  • 把配置文件的值和 ServletWebServerFactory 进行绑定
  • 修改配置文件 server.xxx
  • 直接自定义 ConfigurableServletWebServerFactory
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

    @Override
    public void customize(ConfigurableServletWebServerFactory server) {
        server.setPort(9000);
    }

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值