8.spring framework 5.2.3.RELEASE 文档之核心技术(Core Technologies) 第七篇 类路径的扫描和组件管理

目录

1.10. 类路径的扫描和组件管理

1.10.1. @Component 和其他构造型注解

1.10.2. 使用元注释和组合注释

1.10.3. 自动检测类并注册Bean定义

1.10.4. 使用过滤器自定义扫描

1.10.5. 在组件中定义Bean元数据

1.10.6. 命名自动检测的组件

1.10.7. 提供自动检测组件的作用域

1.10.8. 提供带有注解的限定符元数据

1.10.9. 生成候选组件的索引


1.10. 类路径的扫描和组件管理

本章中的大多数示例都使用XML来指定在Spring容器中生成每个BeanDefinition的配置元数据。 上一节(基于注解的容器配置)演示了如何通过源级别的注解提供许多配置元数据。 但是,即使在这些示例中,“base” bean定义也已在XML文件中明确定义,而注解仅驱动依赖项注入。 本节介绍了通过扫描类路径来隐式检测候选组件的选项。 候选组件是与过滤条件匹配的类,并具有在容器中注册的相应bean定义。 这消除了使用XML进行bean注册的需要。 相反,您可以使用注解(例如@Component),AspectJ类型表达式或您自己的自定义过滤条件来选择哪些类已向容器注册了bean定义。

从Spring 3.0开始,Spring JavaConfig项目提供的许多功能是核心Spring Framework的一部分。 这使您可以使用Java而不是使用传统的XML文件来定义bean。 请查看@Configuration,@Bean,@Import和@DependsOn注解,以获取有关如何使用这些新功能的示例。

1.10.1. @Component 和其他构造型注解

@Repository注解是实现存储库的角色或原型(也称为数据访问对象或DAO)的任何类的标记。此标记的用途包括异常的自动转换,如异常转换中所述。

Spring提供了更多的原型注解:@Component,@Service和@Controller。@组件是任何Spring管理的组件的通用原型。@Repository、@Service和@Controller是@Component在更具体的用例(分别在持久性层、服务层和表示层)中的专门化。因此,您可以使用@component来注解组件类,但是,通过使用@Repository、@Service或@Controller来注解它们,您的类更适合于通过工具进行处理或与方面关联。例如,这些原型注解是切入点的理想目标。@Repository、@Service和@Controller也可以在Spring框架的未来版本中携带额外的语义。因此,如果您在使用@Component或@Service作为服务层之间进行选择,@Service显然是更好的选择。类似地,如前所述,@Repository已经被支持作为持久层中自动异常转换的标记。

1.10.2. 使用元注释和组合注释

Spring提供的许多注解都可以在您自己的代码中用作元注解。 元注解是可以应用于另一个注解的注解。 例如,前面提到的@Service注解使用@Component进行元注解,如以下示例所示:

java

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component 
public @interface Service {

    // ...
}

kotlin

@Target(AnnotationTarget.TYPE)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Component 
annotation class Service {

    // ...
}

组件使@Service被以与@Component相同的方式处理。

您还可以结合使用元注解来创建“组合注解”。 例如,Spring MVC中的@RestController注解由@Controller和@ResponseBody组成。

此外,组合注解可以选择从元注解中重新声明属性,以允许自定义。 当您只希望公开元注解属性的子集时,此功能特别有用。 例如,Spring的@SessionScope注解将作用域名称硬编码为session,但仍允许自定义proxyMode。 以下清单显示了SessionScope注解的定义:

java

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {

    /**
     * Alias for {@link Scope#proxyMode}.
     * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
     */
    @AliasFor(annotation = Scope.class)
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

kotlin

@Target(AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MustBeDocumented
@Scope(WebApplicationContext.SCOPE_SESSION)
annotation class SessionScope(
        @get:AliasFor(annotation = Scope::class)
        val proxyMode: ScopedProxyMode = ScopedProxyMode.TARGET_CLASS
)

然后,您可以使用@SessionScope而不用声明如下的proxyMode:

java

@Service
@SessionScope
public class SessionScopedService {
    // ...
}

kotlin

@Service
@SessionScope
class SessionScopedService {
    // ...
}

您还可以覆盖proxyMode的值,如以下示例所示:

java

@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
    // ...
}

kotlin

@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
class SessionScopedUserService : UserService {
    // ...
}

有关更多详细信息,请参见Spring Annotation编程模型Wiki页面

1.10.3. 自动检测类并注册Bean定义

Spring可以自动检测构造型类,并向ApplicationContext注册相应的BeanDefinition实例。 例如,以下两个类别可进行这种自动检测:

java

@Service
public class SimpleMovieLister {

    private MovieFinder movieFinder;

    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

kotlin

@Service
class SimpleMovieLister(private val movieFinder: MovieFinder)

java

@Repository
public class JpaMovieFinder implements MovieFinder {
    // implementation elided for clarity
}

kotlin

@Repository
class JpaMovieFinder : MovieFinder {
    // implementation elided for clarity
}

要自动检测这些类并注册相应的bean,需要将@ComponentScan添加到@Configuration类中,其中basePackages属性是这两个类的公共父包。 (或者,您可以指定一个逗号分隔,分号分隔或空格分隔的列表,其中包括每个类的父包。)

java

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    // ...
}

kotlin

@Configuration
@ComponentScan(basePackages = ["org.example"])
class AppConfig  {
    // ...
}
为简便起见,前面的示例可能使用了注解的value属性(即@ComponentScan("org.example"))。

以下替代方法使用XML:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.example"/>

</beans>

<context:component-scan>的使用隐式启用<context:annotation-config>的功能。 使用<context:component-scan>时,通常无需包含<context:annotation-config>元素。

扫描类路径包需要在类路径中存在相应的目录条目。 使用Ant构建JAR时,请确保不要激活JAR任务的仅文件开关。 另外,在某些环境中,基于安全策略可能不会公开类路径目录。例如,JDK 1.7.0_45及更高版本上的独立应用程序(清单中需要“Trusted-Library”设置)— see https://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

 

在JDK 9的模块路径(Jigsaw)上,Spring的类路径扫描通常可以按预期进行。 但是,请确保在module-info中导出了组件类。 如果希望Spring调用类的非公共成员,请确保它们是“opened”(也就是说,它们在module-info描述符中使用了opens声明而不是export声明)。

此外,当您使用组件扫描元素时,AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor都是隐式包含的。这意味着这两个组件将被自动检测并连接在一起,而不需要XML中提供任何bean配置元数据。

您可以通过将注解配置属性包括为false来禁用AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor的注册。

1.10.4. 使用过滤器自定义扫描

默认情况下,使用@Component、@Repository、@Service、@Controller、@Configuration注解的类或使用@Component注解的自定义注解本身是唯一检测到的候选组件。但是,您可以通过应用自定义筛选器来修改和扩展此行为。将它们添加为@componentScan注解的includeFilters或excludeFilters属性(或XML配置中<context:component-scan>元素的<context:include-filter/>或<context:exclude-filter/>子元素)。每个筛选器元素都需要类型和表达式属性。下表介绍了筛选选项:

Table 5. Filter Types
Filter TypeExample ExpressionDescription

annotation (default)

org.example.SomeAnnotation

在目标组件的类型级别上存在的注解或元注解。

assignable

org.example.SomeClass

目标组件可分配给(扩展或实现)的类(或接口)。

aspectj

org.example..*Service+

目标组件要匹配的AspectJ类型表达式。

regex

org\.example\.Default.*

要与目标组件的类名匹配的正则表达式。

custom

org.example.MyTypeFilter

org.springframework.core.type.TypeFilter接口的自定义实现。

以下示例显示了忽略所有@Repository注解并使用“ stub”存储库的配置:

java

@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    ...
}

kotlin

@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = [Filter(type = FilterType.REGEX, pattern = [".*Stub.*Repository"])],
        excludeFilters = [Filter(Repository::class)])
class AppConfig {
    // ...
}

下面的列表显示了等效的XML:

<beans>
    <context:component-scan base-package="org.example">
        <context:include-filter type="regex"
                expression=".*Stub.*Repository"/>
        <context:exclude-filter type="annotation"
                expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>
您还可以通过在注解上设置useDefaultFilters = false或通过将use-default-filters =“ false”作为<component-scan />元素的属性来禁用默认过滤器。 这有效地禁用了自动检测使用@ Component,@ Repository,@ Service,@ Controller,@ RestController或@Configuration注解或元注解的类的功能。

1.10.5. 在组件中定义Bean元数据

Spring组件还可以将bean定义元数据贡献给容器。 您可以使用与@Configuration注解类中的Bean元数据定义相同的@Bean注解进行此操作。 以下示例显示了如何执行此操作:

java

@Component
public class FactoryMethodComponent {

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    public void doWork() {
        // Component method implementation omitted
    }
}

kotlin

@Component
class FactoryMethodComponent {

    @Bean
    @Qualifier("public")
    fun publicInstance() = TestBean("publicInstance")

    fun doWork() {
        // Component method implementation omitted
    }
}

上面的类是Spring组件,其doWork()方法中具有特定于应用程序的代码。 但是,它也提供了一个bean定义,该定义具有引用方法publicInstance()的工厂方法。 @Bean注解标识工厂方法和其他bean定义属性,例如通过@Qualifier注解的限定符值。 可以指定的其他方法级别注解为@Scope,@Lazy和自定义限定符注解。

除了用于组件初始化的角色外,您还可以将@Lazy注解放置在标有@Autowired或@Inject的注入点上。 在这种情况下,它导致注入了惰性解析代理。

如前所述,支持自动装配的字段和方法,并自动装配@Bean方法。 以下示例显示了如何执行此操作:

java

@Component
public class FactoryMethodComponent {

    private static int i;

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    // use of a custom qualifier and autowiring of method parameters
    @Bean
    protected TestBean protectedInstance(
            @Qualifier("public") TestBean spouse,
            @Value("#{privateInstance.age}") String country) {
        TestBean tb = new TestBean("protectedInstance", 1);
        tb.setSpouse(spouse);
        tb.setCountry(country);
        return tb;
    }

    @Bean
    private TestBean privateInstance() {
        return new TestBean("privateInstance", i++);
    }

    @Bean
    @RequestScope
    public TestBean requestScopedInstance() {
        return new TestBean("requestScopedInstance", 3);
    }
}

kotlin

@Component
class FactoryMethodComponent {

    companion object {
        private var i: Int = 0
    }

    @Bean
    @Qualifier("public")
    fun publicInstance() = TestBean("publicInstance")

    // use of a custom qualifier and autowiring of method parameters
    @Bean
    protected fun protectedInstance(
            @Qualifier("public") spouse: TestBean,
            @Value("#{privateInstance.age}") country: String) = TestBean("protectedInstance", 1).apply {
        this.spouse = spouse
        this.country = country
    }

    @Bean
    private fun privateInstance() = TestBean("privateInstance", i++)

    @Bean
    @RequestScope
    fun requestScopedInstance() = TestBean("requestScopedInstance", 3)
}

该示例将String方法参数country自动装配到另一个名为privateInstance的bean上age属性的值。 Spring Expression Language元素通过符号#{<expression>}定义属性的值。 对于@Value注解,表达式解析程序已预先配置为在解析表达式文本时查找bean名称。

从Spring Framework 4.3开始,您还可以声明类型为InjectionPoint的工厂方法参数(或更具体的子类:DependencyDescriptor),以访问触发当前bean创建的请求注入点。 注意,这仅适用于实际创建bean实例,而不适用于注入现有实例。 因此,此功能对原型范围的bean最有意义。 对于其他作用域,factory方法仅在给定作用域中看到触发创建新bean实例的注入点(例如,触发创建惰性单例bean的依赖项)。 在这种情况下,可以将提供的注入点元数据与语义一起使用。 以下示例显示了如何使用InjectionPoint:

java

@Component
public class FactoryMethodComponent {

    @Bean @Scope("prototype")
    public TestBean prototypeInstance(InjectionPoint injectionPoint) {
        return new TestBean("prototypeInstance for " + injectionPoint.getMember());
    }
}

kotlin

@Component
class FactoryMethodComponent {

    @Bean
    @Scope("prototype")
    fun prototypeInstance(injectionPoint: InjectionPoint) =
            TestBean("prototypeInstance for ${injectionPoint.member}")
}

常规Spring组件中的@Bean方法的处理方式与Spring @Configuration类中的@Bean方法不同。 区别在于@Component类没有使用CGLIB增强,无法拦截方法和字段的调用。 CGLIB代理是调用@Configuration类中@Bean方法中的方法或字段的方法,用于创建Bean元数据引用来协作对象。 此类方法不是用普通的Java语义调用的,而是通过容器进行的,以提供Spring Bean的常规生命周期管理和代理,即使通过@Bean方法的编程调用引用其他Bean时也是如此。 相反,在普通@Component类内的@Bean方法中调用方法或字段具有标准Java语义,而无需特殊的CGLIB处理或其他约束。

您可以将@Bean方法声明为静态方法,从而允许在不将其包含配置类创建为实例的情况下调用它们。 在定义后处理器Bean(例如BeanFactoryPostProcessor或BeanPostProcessor类型)时,这特别有意义,因为此类Bean在容器生命周期的早期进行了初始化,并且应避免在那时触发配置的其他部分。

 

由于技术限制,对静态@Bean方法的调用永远不会被容器拦截,即使在@Configuration类中也是如此(如本节前面所述),这是由于技术限制:CGLIB子类只能覆盖非静态方法。 结果,直接调用另一个@Bean方法具有标准的Java语义,从而导致直接从工厂方法本身返回一个独立的实例。

 

@Bean方法的Java语言可见性不会对Spring容器中的最终bean定义产生直接影响。 您可以在非@Configuration类中自由声明自己的工厂方法,也可以在任何地方声明静态方法。 但是,@ Configuration类中的常规@Bean方法必须是可重写的-即,不得将它们声明为private或final。

 

还可以在给定组件或配置类的基类上以及在由组件或配置类实现的接口中声明的Java 8默认方法上发现@Bean方法。 这为组合复杂的配置安排提供了很大的灵活性,从Spring 4.2开始,通过Java 8默认方法甚至可以进行多重继承。

 

最后,一个类可以为同一个bean保留多个@Bean方法,这取决于在运行时可用的依赖关系,从而可以使用多个工厂方法。 这与在其他配置方案中选择“greediest”的构造函数或工厂方法的算法相同:在构造时选择具有最大可满足依赖关系数量的变量,类似于容器在多个@Autowired构造函数之间进行选择的方式。

1.10.6. 命名自动检测的组件

在扫描过程中自动检测到组件时,该组件的Bean名称由该扫描程序已知的BeanNameGenerator策略生成。 默认情况下,任何包含名称value的Spring构造型注解(@Component,@Repository,@Service和@Controller)都会将该名称提供给相应的bean定义。

如果这样的注解不包含名称value,或者不包含任何其他检测到的组件(例如,由自定义过滤器发现的组件),则缺省bean名称生成器将返回未大写的非限定类名称。 例如,如果检测到以下组件类,则名称为myMovieLister和movieFinderImpl:

java

@Service("myMovieLister")
public class SimpleMovieLister {
    // ...
}

kotlin

@Service("myMovieLister")
class SimpleMovieLister {
    // ...
}

java

@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

kotlin

@Repository
class MovieFinderImpl : MovieFinder {
    // ...
}

如果不想依赖默认的bean命名策略,可以提供自定义的bean命名策略。首先,实现BeanNameGenerator接口,并确保包含默认的no-arg构造函数。然后,在配置scanner时提供完全限定的类名,如下面的注释和bean定义示例所示。

如果由于多个自动检测到的组件具有相同的非限定类名(即具有相同名称但分散在不同包中的类),而导致命名冲突,则可能需要为生成的bean名称配置默认为完全限定类名的BeanNameGenerator。从Spring Framework 5.2.3开始,位于org.Spring Framework.context.annotation包中的FullyQualifiedAnnotationBeanNameGenerator可用于此目的。

java

@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    // ...
}

kotlin

@Configuration
@ComponentScan(basePackages = ["org.example"], nameGenerator = MyNameGenerator::class)
class AppConfig {
    // ...
}
<beans>
    <context:component-scan base-package="org.example"
        name-generator="org.example.MyNameGenerator" />
</beans>

作为一般规则,当其他组件可能显式引用注解时,请考虑使用注解指定名称。另一方面,只要容器负责装配,自动生成的名称就足够了。

 

 

1.10.7. 提供自动检测组件的作用域

通常,与Spring管理的组件一样,自动检测到的组件的默认作用域也是最常见的作用域是singleton。 但是,有时您需要使用@Scope注解指定的其他作用域。 您可以在注解中提供作用域的名称,如以下示例所示:

java

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

kotlin

@Scope("prototype")
@Repository
class MovieFinderImpl : MovieFinder {
    // ...
}
@Scope注解仅用在具体的bean类(对于带注解的组件)或工厂方法(对于@Bean方法)上进行内省。 与XML bean定义相反,没有bean定义继承的概念,并且在类级别的继承层次结构与元数据目的无关。

有关作用于web的作用域(如Spring上下文中的“request”或“session”)的详细信息,请参阅 Request, Session, Application和 WebSocket作用域。与这些作用域的预构建注解一样,您也可以使用Spring的元注释方法来编写自己的作用域注解:例如,使用@Scope("prototype") 注解的自定义注解元,还可以声明自定义作用域代理模式。

@Scope注解仅用在具体的bean类(对于带注解的组件)或工厂方法(对于@Bean方法)上进行内省。 与XML bean定义相反,没有bean定义继承的概念,并且在类级别的继承层次结构与元数据目的无关。

要提供用于作用域解析的自定义策略,而不是依赖于基于注解的方法,可以实现ScopeMetadataResolver接口。 确保包括默认的无参数构造函数。 然后,可以在配置扫描器时提供完全限定的类名,如以下注解和Bean定义示例所示:

java

@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    // ...
}

kotlin

@Configuration
@ComponentScan(basePackages = ["org.example"], scopeResolver = MyScopeResolver::class)
class AppConfig {
    // ...
}
<beans>
    <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

使用某些非单例作用域时,有必要为作用域对象生成代理。 在作用域Bean中将代理描述为依赖项。 为此,在component-scan元素上可以使用scoped-proxy属性。 三个可选的值是:no,interfaces和targetClass。 例如,以下配置生成标准的JDK动态代理:

java

@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    // ...
}

kotlin

@Configuration
@ComponentScan(basePackages = ["org.example"], scopedProxy = ScopedProxyMode.INTERFACES)
class AppConfig {
    // ...
}
<beans>
    <context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>

 

1.10.8. 提供带有注解的限定符元数据

@Qualifier注解将在使用限定符的基于注解的自动装配中进行讨论。 该部分中的示例演示了@Qualifier批注和自定义限定符注解的使用,以在解析自动装配候选时提供细粒度的控制。 由于这些示例基于XML bean定义,因此通过使用XML中bean元素的限定符或meta子元素,在候选bean定义上提供了限定符元数据。 当依靠类路径扫描来自动检测组件时,可以在候选类上为限定符元数据提供类型级别的注解。 下面的三个示例演示了此技术:

java

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}

kotlin

@Component
@Qualifier("Action")
class ActionMovieCatalog : MovieCatalog

java

@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}

kotlin

@Component
@Genre("Action")
class ActionMovieCatalog : MovieCatalog {
    // ...
}

java

@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
    // ...
}

kotlin

@Component
@Offline
class CachingMovieCatalog : MovieCatalog {
    // ...
}
与大多数基于注解的替代方法一样,请记住,注解元数据绑定到类定义本身,而XML的使用允许相同类型的多个bean提供其限定符元数据的变体,因为该元数据是按 -instance而不是按类。

 

1.10.9. 生成候选组件的索引

尽管类路径扫描非常快,但是可以通过在编译时创建候选静态列表来提高大型应用程序的启动性能。 在这种模式下,作为组件扫描目标的所有模块都必须使用此机制。

您现有的@ComponentScan或<context:component-scan />指令必须保持原样,以请求上下文扫描某些软件包中的候选对象。 当ApplicationContext检测到这样的索引时,它将自动使用它而不是扫描类路径。

要生成索引,请向每个包含组件的模块添加附加依赖关系,这些组件是组件扫描指令的目标。 以下示例显示了如何使用Maven进行操作:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.3.RELEASE</version>
        <optional>true</optional>
    </dependency>
</dependencies>

对于Gradle 4.5和更早版本,依赖关系应在compileOnly配置中声明,如以下示例所示:

dependencies {
    compileOnly "org.springframework:spring-context-indexer:5.2.3.RELEASE"
}
在Gradle 4.6和更高版本中,应在annotationProcessor 配置中声明依赖项,如以下示例所示:
dependencies {
    annotationProcessor "org.springframework:spring-context-indexer:{spring-version}"
}

该过程将生成一个包含在jar文件中的META-INF/spring.components文件。

在IDE中使用此模式时,必须将spring-context-indexer注册为注释处理器,以确保在更新候选组件时索引是最新的。

 

当在类路径上找到META-INF/spring.components时,将自动启用索引。如果索引部分可用于某些库(或用例),但无法用于整个应用程序,则可以通过将spring.index.ignore设置为true(作为系统属性或在类路径根目录下的spring.properties文件中)退回到常规的类路径安排(好像根本不存在索引)。

 

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值