SpringBoot深入简出之篇五

SpringBoot深入简出之篇五

  1. 错误处理机制
    1)、SpringBoot默认的错误处理机制
    默认效果:
    Web 端返回一个错误的页面
    在这里插入图片描述
    移动端返回的是json 数据
    在这里插入图片描述
    原理:
    可以参照ErrorMvcAutoConfiguration;错误处理的自动配置类;这个类给容器中添加了以下组件:
    DefaultErrorAttributes:帮我们获取页面的共享数据

    	@Override
    	public Map<String, Object> getErrorAttributes(WebRequest webRequest,
    			boolean includeStackTrace) {
    		Map<String, Object> errorAttributes = new LinkedHashMap<>();
    		errorAttributes.put("timestamp", new Date());//设置报错时间戳
    		addStatus(errorAttributes, webRequest);//设置报错状态码
    		addErrorDetails(errorAttributes, webRequest, includeStackTrace);//设置报错原因说明
    		addPath(errorAttributes, webRequest);//设置报错的文件路径
    		return errorAttributes;//把报错数据对象返回
    	}
    

    BasicErrorController:处理默认的/error 请求(通过请求头判断到底执行哪一个方法)

    ##BasicErrorController 类的源码
    @Controller
    @RequestMapping("${server.error.path:${error.path:/error}}")
    public class BasicErrorController extends AbstractErrorController {
    
    	@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)//产生HTML类型的数据(web端)
    	public ModelAndView errorHtml(HttpServletRequest request,
    			HttpServletResponse response) {
    		HttpStatus status = getStatus(request);
    		Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
    				request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    		response.setStatus(status.value());
    
    		//去哪个页面作为错误页面;包含页面地址和页面内容
    		ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    		return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    	}
    
    	@RequestMapping//产生JSON格式的数据(移动端)
    	public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    		Map<String, Object> body = getErrorAttributes(request,
    				isIncludeStackTrace(request, MediaType.ALL));
    		HttpStatus status = getStatus(request);
    		return new ResponseEntity<>(body, status);
    	}
    }
    

    ErrorPageCustomizer:

    	/**
    	 * Path of the error controller.
    	 */
    	@Value("${error.path:/error}")
    	private String path = "/error";//系统出现错误以后来到/error 请求进行处理;(web.xml 注册的错误页面规则)
    

    DefaultErrorViewResolver:(SpringBoot 默认报错视图解析器)

    @Override
    	public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status,
    			Map<String, Object> model) {
    		ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
    		if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
    			modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    		}
    		return modelAndView;
    	}
    
    	private ModelAndView resolve(String viewName, Map<String, Object> model) {
    		//默认SpringBoot可以轻易找到一个页面? error/404
    		String errorViewName = "error/" + viewName;
    		
    		//默认引擎可以解析这个页面就用模板引擎解析(就是我们没有自定义报错页面的时候)
    		TemplateAvailabilityProvider provider = this.templateAvailabilityProviders
    				.getProvider(errorViewName, this.applicationContext);
    		if (provider != null) {
    			//模板引擎可用的情况下返回errorViewName 指定的视图地址
    			return new ModelAndView(errorViewName, model);
    		}
    		//模板引擎不可用,就在静态资源文件下找errorViewName 对应的页面  error/404.html
    		return resolveResource(errorViewName, model);
    	}
    

    步骤:
    一旦系统出现4xx、5xx之类的错误;ErrorPageCustomizer 就会生效(定制错误的响应规则);就会来到 /error 请求;就会被 BasicErrorController 处理;
    1)、响应页面:去哪个页面是由 DefaultErrorViewResolver 类决定的

    	protected ModelAndView resolveErrorView(HttpServletRequest request,
    			HttpServletResponse response, HttpStatus status, Map<String, Object> model) {
    		for (ErrorViewResolver resolver : this.errorViewResolvers) {
    			ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
    			if (modelAndView != null) {
    				return modelAndView;
    			}
    		}
    		return null;
    	}
    

    2)、定制错误页面响应:
    ①有模板引擎的情况下;error/状态码;【将错误页面命名为“错误状态码.html ”放置模板引擎的error文件夹下】,发送此状态码的错误就会来到对应的页面;
    我们可以使用4xx或者5xx作为错误页面的文件名匹配这种错误,精确优先(即有自定义对应的报错状态码页面会优先被调用)
    页面能获取的信息:
    timestamp:时间戳
    status:状态码
    error:错误提示
    exception:异常对象
    message:异常信息
    errors:JSR303 数据校验的错误提示信息

    //自定义报错页面代码
    	<!DOCTYPE html>
    	<html lang="en" xmlns:th="http://www.thymeleaf.org">
    		<head>
    		    <meta charset="UTF-8">
    		    <title>登录失败页面</title>
    		</head>
    		<body>
    		    <h1>status:[[${status}]]</h1>
    		    <h2>timestamp:[[${timestamp}]]</h2>
    		    <h3>error:[[]${error}]</h3>
    		</body>
    	</html>
    

在这里插入图片描述
在这里插入图片描述
②没有模板引擎,静态资源文件夹下找(但是动态数据就没办解析了)
③以上都没有错误页面,就是默认来到SpringBoot 默认的错误提示页面

3)、定制JSON格式的返回数据

①自定义异常处理&返回定制json 数据

	@ResponseBody
   @ExceptionHandler(UserNotFoundException.class)//异常处理器
   public Map<String, Object> Handler(Exception e){
       Map<String, Object> map = new HashMap<>();
       map.put("code", "[user] param is not found!");
       map.put("message", e.getMessage());
       map.put("cause", e.getCause());
       return map;
   }

在这里插入图片描述
②转发到/error 进行自适应效果处理

	@ExceptionHandler(UserNotFoundException.class)//异常处理器
    public String Handler(Exception e){
        Map<String, Object> map = new HashMap<>();
        request.setAttribute("javax.servlet.error.status_code", 500);//设置报错状态码
        map.put("code", "[user] param is not found!");
        map.put("message", e.getMessage());
        map.put("cause", e.getCause());
        return "forward:/error";//转发到系统默认报错配置处理机制(BasicErrorController类会自动帮我们判断要响应哪种数据格式)
    }

在这里插入图片描述
③将我们的定制数据携带出来
出现报错后,回来到/error 请求会被BasicErrorController 处理,响应出去可以获取的数据是由 getErrorAttributes 得到的(是AbstractErrorController(ErrorController)规定的方法);
I、完全来编写一个ErrorController 的实现类【或者是编写AbstractErrorController 的子类】,放在容器中;
II、页面上能用的数据,或者是json 返回能用的数据都是通过errorAttributes.getErrorAttributes得到的;
容器中DefaultErrorAttributes.getErrorAttributes();默认进行数据处理;
自定义 ErrorAttributes

@Component//给容器中添加我们自定义的ErrorAttributes
public class MyErrorAttributes extends DefaultErrorAttributes {
    //返回值的map就是web端的页面数据或者移动端的json数据信息
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, includeStackTrace);
        map.put("exception", "我去你大爷!");//我们自定义异常处理器添加的数据
        return map;
    }
}

在这里插入图片描述

  1. 配置嵌入式Servlet 容器
    SpringBoot默认使用Tomcat作为嵌入式的Servlet 容器;
    在这里插入图片描述
    1)、修改内嵌的Servlet 容器(如:Tomcat)的相关配置参数(可查看 ServerProperties 类的源码)
    I、通过配置文件的方式修改参数
##设置内嵌Servlet 容器的相关参数
server.port=8200
server.servlet.context-path=/hello
#通用的servlet容器设置
server.xxx=
#修改Tomcat的设置
server.tomcat.xxx=

II、通过编写一个EmbeddedServletContainerCustomer:嵌入式的Servlet 容器定制类;来修改Servlet容器的配置参数

@Configuration
public class MyServerConfig implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {
        factory.setPort(8090);
    }
}

III、注册三大组件:Servlet、Filter、Listener
由于SpringBoot 默认是以jar包的方式嵌入式的Servlet容器来启动SpringBoot 的web应用;没有web.xml 配置文件
注册方式:
ServletRegistrationBean

//自定义MyServlet组件
public class MyServlet extends HttpServlet {
    @Override//处理get请求
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    @Override//处理post请求
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().write("Welcome to MyServlet");
    }
}

//把自定义的MyServlet组件注册到Servlet容器中
@Bean//注册Servlet组件
    public ServletRegistrationBean myServlet(){
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean<>(new MyServlet(), "/myServlet");
        servletRegistrationBean.setLoadOnStartup(1);//第一个添加的组件
        return servletRegistrationBean;
    }

在这里插入图片描述

FilterRegistrationBean

//自定义MyFilter组件
public class MyFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("MyFilter 初始化方法");
    }

    @Override
    public void destroy() {
        System.out.println("MyFilter 销毁方法");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("MyFilter 执行了.....");
        filterChain.doFilter(servletRequest, servletResponse);//拦截器放行
    }
}

//把自定义的MyFilter组件注册到Servlet容器中
@Bean//注册Filter组件
   public FilterRegistrationBean myFilter(){
       FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
       filterRegistrationBean.setFilter(new MyFilter());
       filterRegistrationBean.setUrlPatterns(Arrays.asList("/myFilter", "/hello"));
       return filterRegistrationBean;
   }

在这里插入图片描述
在这里插入图片描述

ServletListenerRegistrationBean

//自定义MylListener 组件
public class MylListener implements ServletContextListener {
    @Override
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("contextInitialized 初始化,web应用启动了...");
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("contextDestroyed 初始化,web应用销毁了...");
    }
}

//把自定义的MylListener 组件注册到Servlet容器中
@Bean//注册Listener组件
    public ServletListenerRegistrationBean myListener(){
        ServletListenerRegistrationBean servletListenerRegistrationBean = new ServletListenerRegistrationBean(new MylListener());
        return servletListenerRegistrationBean;

    }

在这里插入图片描述

2)、SpringBoot 支持的其他Servlet容器

I、Tomcat(SpringBoot默认使用)

<dependency>
  	<groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

II、Jetty(长连接)

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
     <exclusions>
         <!-- 注销了Tomcat服务器 -->
         <exclusion>
             <artifactId>spring-boot-starter-tomcat</artifactId>
             <groupId>org.springframework.boot</groupId>
         </exclusion>
     </exclusions>
 </dependency>

 <!-- 引入其他的Servlet容器 -->
 <dependency>
     <artifactId>spring-boot-starter-jetty</artifactId>
     <groupId>org.springframework.boot</groupId>
 </dependency>

III、Undertow(不支持JSP)

<dependency>
   <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
     <exclusions>
         <!-- 注销了Tomcat服务器 -->
         <exclusion>
             <artifactId>spring-boot-starter-tomcat</artifactId>
             <groupId>org.springframework.boot</groupId>
         </exclusion>
     </exclusions>
 </dependency>

 <!-- 引入其他的Servlet容器 -->
 <dependency>
     <artifactId>spring-boot-starter-undertow</artifactId>
     <groupId>org.springframework.boot</groupId>
 </dependency>

3)、嵌入式Servlet容器自动配置原理
EmbeddedWebServerFactoryCustomizerAutoConfiguration:嵌入式的Servlet容器自动配置(源码如下)

@Configuration
@ConditionalOnWebApplication
@EnableConfigurationProperties(ServerProperties.class)//ServerProperties 类为配置嵌入式Servlet容器提供参数对象(包含了Tomcat、Jetty、Undertow的各种配置参数)
public class EmbeddedWebServerFactoryCustomizerAutoConfiguration {

	/**
	 * Nested configuration if Tomcat is being used.
	 */
	@Configuration
	@ConditionalOnClass({ Tomcat.class, UpgradeProtocol.class })//判断当前pom是否引入了Tomcat依赖
	public static class TomcatWebServerFactoryCustomizerConfiguration {

		@Bean//完成对Tomcat的配置传递参数对象
		public TomcatWebServerFactoryCustomizer tomcatWebServerFactoryCustomizer(
				Environment environment, ServerProperties serverProperties) {
			return new TomcatWebServerFactoryCustomizer(environment, serverProperties);
		}

	}

TomcatWebServerFactoryCustomizer 类

public class TomcatWebServerFactoryCustomizer implements
		WebServerFactoryCustomizer<ConfigurableTomcatWebServerFactory>, Ordered {

	private final Environment environment;

	private final ServerProperties serverProperties;

	public TomcatWebServerFactoryCustomizer(Environment environment,
			ServerProperties serverProperties) {//完成配置参数对象的赋值
		this.environment = environment;
		this.serverProperties = serverProperties;
	}
	@Override
	public void customize(ConfigurableTomcatWebServerFactory factory) {//在customize 方法下的操作
		ServerProperties properties = this.serverProperties;
		ServerProperties.Tomcat tomcatProperties = properties.getTomcat();//在参数对象中获取Tomcat 

ServerProperties(源码如下)

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {

	/**
	 * Server HTTP port.
	 */
	private Integer port;//端口号

	/**
	 * Network address to which the server should bind.
	 */
	private InetAddress address;//ip地址

	private final Tomcat tomcat = new Tomcat();//创建Tomcat

	private final Jetty jetty = new Jetty();

	private final Undertow undertow = new Undertow();

	public Integer getPort() {
		return this.port;
	}

	public void setPort(Integer port) {
		this.port = port;
	}

	public InetAddress getAddress() {
		return this.address;
	}

	public void setAddress(InetAddress address) {
		this.address = address;
	}

	public Tomcat getTomcat() {
		return this.tomcat;
	}
	
	public Jetty getJetty() {
		return this.jetty;
	}

	public Undertow getUndertow() {
		return this.undertow;
	}

Tomcat 类源码如下

public class Tomcat {
    private static final StringManager sm = StringManager.getManager(Tomcat.class);
    private final Map<String, Logger> pinnedLoggers = new HashMap();
    protected Server server;
    protected int port = 8080;
    protected String hostname = "localhost";
    protected String basedir;
....................

步骤:
①SpringBoot根据导入的依赖情况,给容器中添加相应的WebServerFactoryCustomizer【TomcatWebServerFactoryCustomizer】
②容器中某个组件要创建对象就会惊动后置处理器【WebServerFactoryCustomizerBeanPostProcessor】;只要是嵌入式的Servlet容器工厂初始化对象,后置处理就开始执行
③后置处理器【WebServerFactoryCustomizerBeanPostProcessor】;从容器中获取ServerProperties 定制器中的配置参数以及配置方法。

4)、嵌入式Servlet容器启动原理
什么时候创建嵌入式Servlet容器工厂;什么时候获取嵌入式的Servlet容器并启动Tomcat;获取嵌入式的Servlet容器工厂;
①、SpringBoot应用启动运行run方法
②、refreshContext(context);SpringBoot刷新IOC容器【创建IOC容器对象,并初始化容器,创建容器中每一个组件】;如果是web应用创建AnnotationConfigReactiveWebServerApplicationContext容器,否则就创建默认AnnotationConfigApplicationContext容器
③、refresh(context);刷新刚才创建好的Servlet容器
④、onRefresh();web的ioc容器重写了onRefresh方法
⑤、web的IOC 容器会创建嵌入式的Servlet容器;用createWebServer()方法
⑥、获取嵌入式的Servlet容器工厂:ServletWebServerFactory factory = getWebServerFactory();从ioc容器中获取 ServletWebServerFactory组件;TomcatWebServerFactoryCustomizer 创建对象,后置处理器获取这个对象,并利用定制器来定制Servlet容器的相关配置。
⑦、使用容器工厂获取嵌入式的Servlet容器:(源码如下)

private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			ServletWebServerFactory factory = getWebServerFactory();//获取容器工厂
			this.webServer = factory.getWebServer(getSelfInitializer());//从容器工厂中获取嵌入式Servlet容器
		}
		else if (servletContext != null) {
			try {
				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context",
						ex);
			}
		}
		initPropertySources();
	}

⑧、嵌入式的Servlet容器创建对象并启动Servlet容器
先启动嵌入式的Servlet容器,再将ioc容器中剩下没有创建出的对象获取出来;

  1. 使用外置的Servlet容器
    嵌入式Servlet容器:应用以jar包的方式打包;
    优点:简单、便捷
    缺点:默认不支持JSP,优化定制比较复杂(使用定制器【ServletProperties、自定义WebServerFactoryCustomizer】,自己编写嵌入式Servlet容器的创建工厂)
    外置式Servlet容器:外面安装Tomcat—应用war 包的方式打包;
    步骤:
    1)、必须创建一个war项目;
    在这里插入图片描述
    配置外部Tomcat可参考

2)、将嵌入式的Tomcat指定为provided;

<dependency>
   <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>

3)、必须编写一个SpringBootServletInitializer 的子类,并重写configure 方法

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
    	//传入SpringBoot应用的主程序
        return application.sources(Demo05Application.class);
    }

}

4)、启动服务器就可以使用了;
在这里插入图片描述

原理:
jar 包:执行SpringBoot煮了的main方法,启动ioc 容器,创建嵌入式的Servlet容器;
war 包:启动服务器,服务器启动SpringBoot 应用【SpringBootServletInitializer 】,启动ioc 容器;

外置容器启动规则:
1)、服务器启动(web应用启动)会创建当前web应用里面每一个jar 包里面的SpringBootServletInitializer实例
2)、SpringBootServletInitializer 的实例放在jar包的META-INF/services 文件夹下,有一个名为:javax.servlet.SpringBootServletInitializer的文件,内容就是SpringBootServletInitializer的实现类的全类名
3)、还可以使用@HandlesTypes,在应用启动的时候加载我们感兴趣的类;

启动流程:
1)、启动Tomcat
2)、org\springframework\spring-web\5.1.7.RELEASE\spring-web-5.1.7.RELEASE.jar!\META-INF\services\javax.servlet.ServletContainerInitializer:Spring的web模块里面有这个文件:org.springframework.web.SpringServletContainerInitializer
3)、SpringServletContainerInitializer 将@HandlesTypes(WebApplicationInitializer.class);标志的所有这个类型的类都传染到onStartup()方法的Set<Class<?>;为这些WebApplicationInitializer类型的类创建实例;
在这里插入图片描述
4)、每一个WebApplicationInitializer 都调用的自己的onStartup 方法;
5)、相当于我们的SpringBootServletInitializer 的类会被创建对象,并执行onStartup方法
在这里插入图片描述
6)、SpringBootServletInitializer 示例执行onStartup 方法的时候会createRootApplicationContext;创建容器(源码如下)

protected WebApplicationContext createRootApplicationContext(
			ServletContext servletContext) {
		//1、创建createSpringApplicationBuilder
		SpringApplicationBuilder builder = createSpringApplicationBuilder();
		builder.main(getClass());
		ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
		if (parent != null) {
			this.logger.info("Root context already created (using as parent).");
			servletContext.setAttribute(
					WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
			builder.initializers(new ParentContextApplicationContextInitializer(parent));
		}
		builder.initializers(
				new ServletContextApplicationContextInitializer(servletContext));
		builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
		//2、调用了configurea方法,子类重写了这个方法,将SpringBoot的主程序传入进来
		builder = configure(builder);
		builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
		//3、使用builder 创建一个Spring应用
		SpringApplication application = builder.build();
		if (application.getAllSources().isEmpty() && AnnotationUtils
				.findAnnotation(getClass(), Configuration.class) != null) {
			application.addPrimarySources(Collections.singleton(getClass()));
		}
		Assert.state(!application.getAllSources().isEmpty(),
				"No SpringApplication sources have been defined. Either override the "
						+ "configure method or add an @Configuration annotation");
		// Ensure error pages are registered
		if (this.registerErrorPageFilter) {
			application.addPrimarySources(
					Collections.singleton(ErrorPageFilterConfiguration.class));
		}
		//4、启动Spring 应用
		return run(application);
	}

7)、Spring 的应用就启动并且创建IOC容器(run 方法的源码如下)

public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch();
		
		//启动Servlet 容器
		stopWatch.start();
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		SpringApplicationRunListeners listeners = getRunListeners(args);
		listeners.starting();
		try {
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
					
			//刷新IOC容器
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass)
						.logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

显示启动Servlet容器(Tomcat),在启动SpringBoot应用

后续请查看第六篇

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值