Spring-boot特性(2)

SpringApplication

在使用Spring-boot时,永远要记住它仅仅是Spring Framework的延伸(或者说整合),其底层还是基于Spring Framework(core、contest、bean)。所以Spring该有的特性Spring Boot中都会存在。

启动异常

Spring在启动时需要初始化容器、向容器在注入类等等操作,如果在启动过程中发生任何异常,我们可以通过 FailureAnalyzers 特性来获取异常启动的信息,结构如下:

***************************
APPLICATION FAILED TO START
***************************

Description:

Embedded servlet container failed to start. Port 8080 was already in use.

Action:

Identify and stop the process that's listening on port 8080 or configure this application to listen on another port.

如果需要获取更详细的信息,我们可以开打DEBUG模式。请参看总结的第一章。

自定义Banner

默认情况下Spring Boot启动时日志会自带一个Banner,如下:

2018-01-22 10:49:43.865 DEBUG 4510 --- [  restartedMain] .b.l.ClasspathLoggingApplicationListener : Application started with classpath: [file:/work/demo2/target/classes/]

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.9.RELEASE)

2018-01-22 10:49:44.007  INFO 4510 --- [  restartedMain] o.p.springframe.boot.demo.webmvc.Demo    : Starting Demo on chkui with PID 4510 (/work/demo2/target/classes started by chenkui in /work/demo2)

绝大部分情况我们直接关闭它,但是在某些场景下我们需要自定义我们的Banner(比如做为一个产品部分给客户时)。

可以在ClassPath中添加一个名为"banner.txt"的文件,然后在JVM中设定"banner.location"的属性来指向他,还可以通过 banner.charset 来指定文件的编码。除了文本之外,还可以将Banner设定为图片——在banner.txt中通过banner.gif、banner.jpg、banner.png来指定,或者直接设定JVM的banner.image.location属性。图片会被转换成ASCII并在文本中输出它。

可以在banner.txt文件描述中通过${}方式引入各种参数,详情见Banner的参数描述部分

SpringApplication.setBanner()方法或org.springframework.boot.Banner 设置为false可以直接关闭Banner的输出。

设置启动参数

通常情况下,我们使用SpringApplication.run(args)的静态方法就可以启动Boot。其实静态的run方法也是在代码中创建了一个SpringApplication的实例。我们可以可以像下面这样自己创建SpringApplication实例并设置参数,最后启动它:

public static void main(String[] args) {
    SpringApplication app = new SpringApplication(MySpringConfiguration.class);
    app.setBannerMode(Banner.Mode.OFF);//不输出Banner
    app.run(args);
}

传递给run方法的args参数可以用于Boot的外部配置,也可以直接使用@Configuration的方式而什么都不传递,关于外部化配置的说明请见后续配置部分说明。

"函数式"Builder代码

Boot还提供了更加“函数式”方法来构建SpringApplication:

new SpringApplicationBuilder()
        .sources(Parent.class)
        .child(Application.class)
        .bannerMode(Banner.Mode.OFF)
        .run(args);

SpringApplicationBuilder可以很方便的用于创建多个Context,不过创建多个Context时也有一些限制,详情请看 SpringApplicationBuilder 的 API

事件以及监听

除了Spring Framework原有的事件外,Boot还额外增加了一些必要的事件。我们可以通过调用SpringApplication.addListeners(​)来增加事件。Boot特有的事件包括:

  1. ApplicationStartingEvent:在Application开始运行(这个时候仅仅完成初始化工具的生成和监听器的生成,其他任何context或者bean都不存在)时触发这个事件。
  2. ApplicationEnvironmentPreparedEvent:这个事件被触发的时机是,上下文环境已经确定但是还未创建context(即容器还未开始创建)。
  3. ApplicationPreparedEvent:触发的时机是所有的Bean(Class类)已经被读取,但是还未执行Context的refresh方法(refresh用于Spring初始化一个Context)。
  4. ApplicationReadyEvent:在完成上下文初始化、Beans加载,所有的功能都准备就绪时触发。
  5. ApplicationFailedEvent:在启动初始化过程中出现异常时触发。

使用监听器需要注意的是某些事件会在Context初始化之前就被创建,所以我们无法将这些监听器著注册成一个@Bean使用,除了通过SpringApplication.addListeners(​)和SpringApplicationBuilder.listeners()方法,还可以在META-INF/spring.factories文件中创建一个org.springframework.context.ApplicationListener字段指向监听类列表,例如:

org.springframework.context.ApplicationListener=com.example.project.MyListener

SpringBoot的事件是通过Spring Framework的事件机制传递的,这个事件的一个特点是当我们向子context发送事件时候,它的所有祖先context都会收到这个事件,所以我们在使用多个层级的SpringBoot应用时监听器必须对收到的事件来源加以区分,我们可以通过ApplicationContextAware来获取当前的Context,或者如果监听器是一个Bean,可以直接使用@Autowired注入。

Web环境

我们知道Spring有各种各样的ApplicationContext,而Boot的自动配置推导功能会根据ClassPath中所包含的内容来确定使用哪个ApplicationContext。通常情况下,如果你使用的是一个web环境工程(例如依赖了spring-boot-starter-web),Boot会使用AnnotationConfigEmbeddedWebApplicationContext,否则会使用AnnotationConfigApplicationContext。确定当前环境是否为Web环境的算法十分简单,通过判断几个类是否存在而确定,我们可以setWebEnvironment(boolean)方法来手工指定当前的环境。当然如果你对各类ApplicationContext十分了解,可以调用SpringApplication.setApplicationContextClass()直接设置ApplicationContext。

获取Application参数

我们在运行run方法时会传递由main传入的String[] args 参数,这个参数可以设定各种运行环境参数。我们可以通过注入ApplicationArguments在任何地方获取这个参数。

import org.springframework.boot.*
import org.springframework.beans.factory.annotation.*
import org.springframework.stereotype.*

@Component
public class MyBean {

    @Autowired
    public MyBean(ApplicationArguments args) {
        boolean debug = args.containsOption("debug");
        List<String> files = args.getNonOptionArgs();
        // if run with "--debug logfile.txt" debug=true, files=["logfile.txt"]
    }

}

除此之外,CommandLinePropertySource可以获取Boot相关的运行环境参数。

Application退出

我们关闭一个Java程序通常会直接关闭运行他的JVM,每一个SpringApplication都会向JVM注册一个shutdow hook,以确保程序在退出时正确关闭ApplicationContext。所有SpringFramework标准的生命周期回调方法都会在此时被调用(例如DisposableBean接口,或者@PreDestroy注解标记的方法)。除此之外,Boot还新增了org.springframework.boot.ExitCodeGenerator接口来设定System.exit()退出时的返回编码,

@SpringBootApplication
public class ExitCodeApplication {

	@Bean
	public ExitCodeGenerator exitCodeGenerator() {
		return new ExitCodeGenerator() {
			@Override
			public int getExitCode() {
				return 42;
			}
		};
	}

	public static void main(String[] args) {
		System.exit(SpringApplication
				.exit(SpringApplication.run(ExitCodeApplication.class, args)));
	}

}

JMX MBeanServer

通过spring.application.admin.enabled属性可以打开MBean相关的功能,然后我们的系统会向外暴露SpringApplicationAdminMXBean,我们可以通过这个MBean了解一些环境信息和配置信息或直接关闭在运行的程序。

获取local.server.port属性可以知道当前JMX暴露的端口。

外部化配置

每一个需要面向市场的系统都需要一些外部化的配置来解决不同环境的问题(例如开发环境、测试环境、生产环境)。

加载外部属性值

我们一般将配置的数据记录在properties文件、YAML文件、环境变量中,或者通过命令行参数来传入。Spring Boot提供了一套价值将这些外部数据加载到JVM的系统参数中。既然可以通过多种方式给SpringApplication设定外部参数,所以需要明确各种方式的优先级。Spring Boot使用了一个非常特殊的PropertySource命令来设计,用于依次覆盖先前的配置。其优先级依次为:

  1. 如果有Devtools存在,优先使用Devtools的全局配置参数。
  2. 在测试用例中 @TestPropertySource 的优先级最高。
  3. 我们会通过@SpringBootTest注解标记一个测试用例,其中的属性参数优先级其次。
  4. 由命令行传入的参数。
  5. SPRING_APPLICATION_JSON指定的参数。
  6. ServletConfig 的 init parameters。
  7. ServletContext 的 init parameters。
  8. java:comp/env 设定的 JNDI参数。
  9. 通过System.setProperties设定的参数。
  10. 操作系统参数。
  11. 以 random.* 形式命名的的 RandomValuePropertySource 参数。
  12. Jar包之外application-{profile}.properties文件配置的参数。
  13. Jar包之内application-{profile}.properties文件配置的参数。
  14. Jar包之外application.properties文件配置的参数。
  15. Jar包之内application.properties文件配置的参数。
  16. @Configuration类上@PropertySource指定的参数。

例如在 application.properties 文件中设定一个名为 name 的参数,在不同的环境中,我们可以提供不同的 application.properties 文件来修改配置参数。此外,我们可以继续保留默认的 application.properties 文件,通过 java -jar app.jar --name="Spring" 命令的方式来指定 name 参数,由于优先级的问题,命令行使用的数据会覆盖application.properties中的数据。

application.properties配置文件规则

SpringApplication会从以下路径加载所有的application.properties文件:

  1. file:./config/(当前目录下的config文件夹)
  2. file:./(当前目录)
  3. classpath:/config/(classpath下的config目录)
  4. classpath:/(classpath根目录)

优先级由上至下。需要特别说明的是,这个优先级是指属性最后使用的值,而不是说仅仅扫描优先级高的路径,如果发现了application.properties文件就停止。例如classpath:/config/和file:./config/都存在配置文件,那么加载过程会加载classpath:/config/路径下配置文件的所有属性,然后再加载file:./config/路径下配置文件的属性并替换已有的属性。

如果你不想使用application.properties的格式命名配置文件,那么可以通过环境变量spring.config.name来设置文件名称,例如:

$ java -jar myproject.jar --spring.config.name=myproject

此时,要加载的配置文件名为myproject.properties。

除了修改名称,还可以使用 spring.config.location 来添加要加载的路径。例如我们以这个命令启动JVM:

$ java -jar myapp.jar --spring.config.location=classpath:/myconfig/,file:./myconfig/

那么加载application.properties文件的路径以及优先级会变为:

  1. file:./myconfig/
  2. classpath:/myconfig/
  3. file:./config/
  4. file:./
  5. classpath:/config/
  6. classpath:/

 spring.config.location环境变量也可以直接设定到加载文件的名称,例如:

--spring.config.location=classpath:/default.properties

通常情况下这样做并没有太大问题,但是结合到Profiles文件特性时,会导致无法根据标记加载对应的Profiles文件。详情请看后面的Profiles文件介绍。

由于配置文件路径和配置文件名称在容器未启动时就需要声明,所以最好在OS的环境变量、JVM的系统环境变量或命令行参数就设定它。

替换符与数据注入

在从各种外部配置读取数据后,需要将其注入到Bean中作为数据项使用。Spring通常情况下使用@Value注解来实现:

import org.springframework.stereotype.*
import org.springframework.beans.factory.annotation.*
@Component
public class MyBean {
    @Value("${name}")
    private String name;
}

上面的例子中@Value("${name}")表示将JVM中的属性 --name注入到private String name成员变量。所以${}就是一个替换符号。

除了直接指定某一个值,还通过JSON的方式更方便一次性指定多个属性。例如LINUX启动时使用:

$ SPRING_APPLICATION_JSON='{"foo":{"bar":"spam"}}' java -jar myapp.jar

在Spring环境中就有foo.bar=spam的数据。

还可以直接通过 -D或直接--设定参数的方式直接设定Json:

$ java -Dspring.application.json='{"foo":"bar"}' -jar myapp.jar
$ java -jar myapp.jar --spring.application.json='{"foo":"bar"}'

此时foo=bar。

安全数据转换

使用@Value注解是将JVM中的属性转换为Bean最常规的方式。不过如果配置量很大,我们需要反复的书写很多的@Value,也不便于结构化。所以Spring Boot在Spring Framework的基础上提供了一个支持结构化数据注入、支持安全类型推导转换、支持数据验证的方法——@ConfigurationProperties。

@ConfigurationProperties("foo")
public class FooProperties {
    private boolean enabled;
    private InetAddress remoteAddress;
    private final Security security = new Security();
    //Getter and Setter
    public static class Security {
        private String username;
        private String password;
        private List<String> roles = new ArrayList<>(Collections.singleton("USER"));
        //Getter and Setter
    }
}

上面的类省略了Get和Set方法,当时每一个作为POJO或Entity的类都必须提供完整的Get和Set方法。因为有了@ConfigurationProperties("foo")注解,此时JVM中有一个 foo.enabled = false/ture 的属性会被注入到enabled变量中,如果环境中没有foo.enabled,则会设定默认值 false。

除了在POJO类上增加@ConfigurationProperties注解,还需要在入口类(一般设定在@Configuration类上)通过@EnableConfigurationProperties注解列举要执行@ConfigurationProperties的类,如下:

@Configuration
@EnableConfigurationProperties(FooProperties.class)
public class MyConfiguration {
}

对于@ConfigurationProperties,在注入环境的属性值之后,它会成为一个Bean在容器的任意位置使用。虽然一个Bean可以注入其他Bean,但是最好一个@ConfigurationProperties的类仅仅用来记录属性数据,而不要再依赖任何Bean。

数据快捷绑定规则

用@ConfigurationProperties从JVM的属性转变为Bean可以有多种映射方式。直接用一个例子来说明:

@ConfigurationProperties(prefix="person")
public class OwnerProperties {//Bean
    private String firstName; //值
    public String getFirstName() {
        return this.firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

}

根据前面的介绍,在SpringContext进行初始化的过程中会将person.firstName的属性注入到这个Bean的firsName成员变量中,但是除此之外,其他命名规则的属性值也会被绑定,如下:

person.firstName标准的驼峰书写规则。
person.first-name横线表示法,常用于在配置文件的书写中。
person.first_name下划线表示法,用语配置文件的书写。
PERSON_FIRST_NAME大写格式。常用于系统的环境变量。

Boot已经为@ConfigurationProperties提供了强大的类型匹配机制,不过如果在开发的过程中还有更特殊的匹配需求,可以用ConversionService、CustomEditorConfigurer来解决属性转换为Bean的类型匹配,详情看这里

@ConfigurationProperties数据验证

可以通过JSR-303描述的Java验证方式对配置数据进行注入验证,只要在@ConfigurationProperties类加上@Validated注解即可,并且在classPath中有JSR-303的实现(Spring已经自带了)。看例子:

@ConfigurationProperties(prefix="foo")
@Validated //验证标记
public class FooProperties {
    @NotNull //注入Bean时,这个数据不能为空
    private InetAddress remoteAddress;
}

如果在类中还有嵌套在内部的实体,需要使用@Valid注解来触发验证:

@ConfigurationProperties(prefix="connection")
@Validated
public class FooProperties {
    @NotNull
    private InetAddress remoteAddress;
    @Valid
    private final Security security = new Security();
    public static class Security {
        @NotEmpty
        public String username;
    }
}

除了已经定义好的验证方式,还可以自定义对Bean的验证,请看这个例子

环境配置

前面介绍了如何配置,这一小节将详细介绍如何解决不同环境不同配置的问题。Spring提供了默认配置为主,部分分离配置为辅的配置方式,称之为Profiles特性。可以通过@Profiles注解和Profiles相关的命名来限制配置Beans的使用和配置文件的加载。通常我们使用spring.profiles.active属性来设置被激活指定的配置。例如 --spring.profiles.active = dev, hsqldb。

像下面这样通过@Profiles注解来指定是否激活某个@Component或@Configuration。

@Configuration
@Profile("production")
public class ProductionConfiguration {
    //仅仅在 spring.profiles.active = production时,这个Bean才会被注入
}

设置激活的profiles

我们可以通过多种方式来设定spring.profiles.active的参数,这与前面设定属性的优先级一样(PropertySource算法)。这就意味着可以同样在application.properties配置文件中指定他,然后通过命令行的方式覆盖这个参数的内容。

除了spring.profiles.active,spring.profiles.include可以设置更多的激活内容。而SpringApplication也提供了setAdditionalProfiles()方法来设定当前的profiles。

profiles文件

在前面介绍properties属性的内容里有提到application-{profile}.properties文件。它也Profiles特性之一,具备以下特点:

  1. application-{profile}.properties文件的加载路径和application.properties一样,同样使用spring.config.location和spring.config.name配置。不过优先级更高。
  2. 若未指定spring.profiles.active环境变量,那么profile的名称默认为default,也就是会优先加载application-default.properties文件。
  3. 如果我们一次性指定了多个profile,那么最后一个的优先级最高。
  4. 前面已经提到,如果spring.config.location环境变量直接指定到文件名称无法支持Profiles特性,建议通过spring.config.location设定路径、spring.config.name设定文件名。

Loggin日志

Spring Boot默认使用 Commons Logging 作为内嵌的日志输出工具,但是保留了底层日志的实现接口。Boot为 Java Util LoggingLog4J2以及Logback提供了默认配置,只要在classpath引入了对应的jar,Spring就会自动推导并注入配置。

默认情况下,如果你引入了某个Starters就会使用Logback来进行日志输出(他们都依赖spring-boot-starter-logging)。Logback的路由功能可以支持其他使用Java Util Logging、Commons Logging、Log4J或SLF4J的库。

格式化

默认情况下,输出的格式是这样的:

2014-03-05 10:57:51.112  INFO 45469 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/7.0.52
2014-03-05 10:57:51.253  INFO 45469 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2014-03-05 10:57:51.253  INFO 45469 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 1358 ms
2014-03-05 10:57:51.698  INFO 45469 --- [ost-startStop-1] o.s.b.c.e.ServletRegistrationBean        : Mapping servlet: 'dispatcherServlet' to [/]
2014-03-05 10:57:51.702  INFO 45469 --- [ost-startStop-1] o.s.b.c.embedded.FilterRegistrationBean  : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]

包含以下内容:

  1. 日期和时间,精确到毫秒级别。
  2. 日志等级——ERROR、WARN、INFO、DEBUG、TRACE。
  3. 进程ID。
  4. 分隔符 --- 用来标记之后为实际的日志输出内容。
  5. 输出日志的线程名称。
  6. 日志名称,一般情况下用缩写表示类名。
  7. 最后是日志详细信息。

 默认情况下日志仅仅输出ERROR、WARN、INFO(LogBack取消了FATAL级别,合并到ERROR)。我们可以通过Java --debug或application.properties中的debug=true来开启DEBUG级别的日志输出(同样--trace或trace=true会开启跟踪日志)。

如果你的输出终端支持ANSI,那么根据日志级别输出不同颜色文字,详情请看这里

文件输出

默认情况下,Spring Boot只会在console输出日志,但是在服务器运行时输出到文件是必须的。

实现将日志输出到文件并不复杂,仅仅需要设定2个环境变量logging.file和logging.path即可(例如写到application.properties中)。下面的表说明了2个参数设定值时的情况。

logging.filelogging.path说明
nonenone仅仅输出到Console
my.log/log/my.lognone从当前位置或绝对路径输出某个日志文件。
none/var/log输出一个名为spring.log的日志文件到指定位置。

日志文件默认也是输出ERROR、WARN、INFO,每当达到10MB时会切换一个文件继续输出。

日志级别控制

所有的支持日志系统的库都支持从环境变量中读取相关日志级别,所以我们可以将日志级别的描述也记录在环境变量中(例如application.properties文件)。其格式一般为logging.level.*=&{LEVEL},LEVEL取值TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF。全局日志配置使用logging.level.root环境变量来设定,例如:

logging.level.root=WARN
logging.level.org.springframework.web=DEBUG
logging.level.org.hibernate=ERROR

通常情况下,我们对日志的控制只要了解上述2个规则即可,但是如果有更特殊的邀请,可以从Spring Boot的日志配置开始了解。

Web工程相关的特性

Spring Boot非常适用于开发一个Web工程,直接引入一个spring-boot-starter-web即可开始开发。

Spring Web MVC framework

Spring Boot的web功能是通过Spring Web MVC framework(以下简称SpringMVC)来实现的,它通过@Controller和@RestController注解即可快速创建一个基于HTTP Requset/Response的模型:

@RestController
@RequestMapping(value="/users")
public class MyRestController {
    @RequestMapping(value="/{user}", method=RequestMethod.GET)
    public User getUser(@PathVariable Long user) {
        //拦截/users/{user},user变量能够获取{user}的值
    }
    @RequestMapping(value="/{user}/customers", method=RequestMethod.GET)
    List<Customer> getUserCustomers(@PathVariable Long user) {
        //拦截/users/{user}/customers,user变量能够获取{user}的值
    }
    @RequestMapping(value="/{user}", method=RequestMethod.DELETE)
    public User deleteUser(@PathVariable Long user) {
        //拦截/users/{user}的DELETE调用,user变量能够获取{user}的值
    }
    @RequestMapping(value="/query", method=RequestMethod.DELETE)
    public User deleteUser(@RequestParam(value="user", defaultValue=1L) Long user) {
        //拦截/users/query请求,当/users/query?user=2时,可以获取query变量中的user=2
    }
}

关于SpringMVC的详细说明请看Spring Framework MVC部分的文档说明

SpingMVC的自动配置

上一篇文章已经介绍了Boot最大的特色就是为各种引入的包提供了相关配置以降低起步的门槛。Boot为SpringMVC添加了一下配置:

  1. 自动注入了ContentNegotiatingViewResolver和BeanNameViewResolver Bean。
  2. 支持静态资源,包括多WebJars的支持。
  3. 自动注册Converter、GenericConverter、Formatter Bean。
  4. 支持HttpMessageConverters。
  5. 提供了一个默认的index.html页面。
  6. 提供了一个 favicon图表,并支持配置。
  7. 自定使用 ConfigurableWebBindingInitializer bean。

接下来会介绍自动添加的这些功能到底做了什么事。

HttpMessageConverters

Spring MVC使用HttpMessageConverters接口来转换HTTP的requests请求和responses响应,Boot提供了一个便捷的HttpMessageConverters实现,Objects对象会自动转换为一个JSON(使用Jackson)或者XML(Jackson XML),并且所有的字符串都会转换为UTF-8。

如果需要自定义一个converters,可以使用Spring Boot的HttpMessageConverters类:

import org.springframework.boot.autoconfigure.web.HttpMessageConverters;
import org.springframework.context.annotation.*;
import org.springframework.http.converter.*;

@Configuration
public class MyConfiguration {
    @Bean
    public HttpMessageConverters customConverters() {
        HttpMessageConverter<?> additional = ...
        HttpMessageConverter<?> another = ...
        return new HttpMessageConverters(additional, another);
    }
}

所有添加到容器中的HttpMessageConverter实现类都会添加到converters的处理列表上,当然也可以直接替换默认的HttpMessageConverter。

自定义JSON序列化反序列化工具

如果我们继续Jackson作为JSON的序列化、反序列化工具,我们可以为特殊的类编写我们自定义的JsonSerializer和JsonDeserializer过程。Boot提供了@JsonComponent注解来快速实现这个功能:

@JsonComponent
public class Example {
    public static class Serializer extends JsonSerializer<SomeObject> {
        // ...
    }
    public static class Deserializer extends JsonDeserializer<SomeObject> {
        // ...
    }
}

所有被@JsonComponent限定的Bean都会自动注册到Jackson中,根据范型的类型对指定的类进行序列化与反序列化操作。

转载于:https://my.oschina.net/chkui/blog/1611888

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值