Spring学习-Spring核心技术(五)


读Spring框架官方文档记录。

一、ApplicationContext的附加功能

ApplicationContext扩展了BeanFactory接口及其它一些接口去提供一些额外的功能:

  • 通过MessageSource接口去访问i18n风格的消息
  • 通过ResourceLoader接口去访问如URLs及文件一类的资源
  • 通过使用ApplicationEventPulisher接口针对实现了ApplicationListener接口的bean进行事件发布
  • 通过HierarchicalBeanFactory接口加载多个上下文,并让每个上下文固定在一个特定的层,如应用的web层

1. 使用MessageSource实现国际化

ApplicationContext接口扩展了MessageSource接口,提供了i18n功能。Spring还提供了HierarchicalMessageSource接口,该接口可以分层解析消息。这些接口共同提供了Spring消息解析的基础。这些接口中定义的方法包括:

  • String getMessage(String code, Object[] args, String default, Locale loc):从MessageSource检索消息的基本方法。当没有消息从指定loc找到时,使用默认消息。
  • String getMessage(String code, Object[] args, Locale loc):本质上与前一个方法相同,但有一点不同:不能指定默认消息。如果找不到消息,则抛出NoSuchMessageException。
  • String getMessage(MessageSourceResolvable resolvable, Locale locale):前面方法中使用的所有属性也被包装在名为MessageSourceResolvable的类中,可以使用这个方法。

当加载ApplicationContext时,它会自动搜索上下文中定义的MessageSource bean,该bean的名称必须为messageSource。如果没有找到这样的bean,则对前面方法的所有调用都委托给消息源。如果没有消息源,ApplicationContext将尝试查找包含同名bean的父类,并使用该bean作为MessageSource。如果ApplicationContext找不到任何的源作为消息源,则实例化一个空的DelegatingMessageSource,以便能够接受对上面定义的方法的调用。

(1) ResourceBundleMessageSource基本应用举例

Spring提供两种MessageSource的实现:ResourceBundleMessageSource和StaticMessageSource。两者都实现了HierarchicalMessageSource来执行嵌套的消息传递。很少使用StaticMessageSource,但它提供了向源添加消息的编程方式。下面的例子展示了ResourceBundleMessageSource:

<beans>
    <bean id="messageSource"<!--注意:这里的id名固定为messageSourde,不能改变-->
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

例子中假设有三个资源包定义在类路径中:format,exceptions,windows。任何解析消息的请求都以jdk标准的方式处理,通过ResourceBundle对象解析消息。
假设上述两个资源包中的内容如下:

# format.properties
message=Alligators rock!

# exceptions.properties
argument.required=The {0} argument is required.

下一个示例显示了运行MessageSource功能的程序。请记住,所有ApplicationContext实现也是MessageSource实现,因此可以转换为MessageSource接口。

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", Locale.ENGLISH);
    System.out.println(message);
}

则上述程序的输出为Alligators rock!
总之,MessageSource是在一个名为bean.xml的文件中定义的。它存在于类路径的根目录中。messageSource bean定义通过basenames属性引用许多资源包。在列表中传递给basenames属性的三个文件作为类路径根文件存在,称为format.properties、exceptions.properties以及windows.properties。

(2) ResourceBundleMessageSource中消息查找参数的替换

示例显示传递给消息查找的参数。这些参数被转换成字符串对象并插入到查找消息中的占位符中。

<beans>

    <!-- this MessageSource is being used in a web application -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <!-- lets inject the above MessageSource into this POJO -->
    <bean id="example" class="com.something.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;

    public void setMessages(MessageSource messages) {
        this.messages = messages;
    }

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", Locale.ENGLISH);
        System.out.println(message);
    }
}

调用execute()的结果输出为The userDao argument is required.

(3) ResourceBundleMessageSource实现国际化

关于国际化(i18n), Spring的各种MessageSource实现遵循与标准JDK ResourceBundle相同的地区解析和回退规则。简而言之,如前面定义的示例messageSource,如果希望根据英国(en-GB)地区解析消息,那么需要创建创建名为format_en_GB.properties, exceptions_en_GB.properties及windows_en_GB.properties的文件。
通常,区域解析是由应用程序的周围环境管理的。在下面的示例中,地区解析是手动指定的。

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.
public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

上面程序的输出为Ebagum lad, the 'userDao' argument is required, I say, required.

(4) 替换ResourceBundleMessageSource

还可以使用MessageSourceAware接口获取对任何已定义的MessageSource的引用。在实现MessageSourceAware接口的ApplicationContext中定义的任何bean都会在创建和配置bean时注入应用程序上下文的MessageSource。
作为ResourceBundleMessageSource的替代方案,Spring提供了一个ReloadableResourceBundleMessageSource类。这个变体支持相同的包文件格式,但是比基于标准JDK的ResourceBundleMessageSource实现更加灵活。特别是,它允许从任何Spring资源位置读取文件(不仅仅是从类路径),并支持包属性文件的热重新加载(同时有效地缓存它们)。

2. 标准事件和自定义事件

Spring的事件机制旨在相同应用程序上下文中的Spring bean之间进行简单通信。
ApplicationContext中提供的事件处理功能通过ApplicationEvent类及ApplicationListener接口来实现。如果实现ApplicationListener接口的bean被部署到上下文中,那么每次ApplicationEvent被发布到ApplicationContext时,该bean都会收到通知。从本质上讲,这是标准的观察者设计模式。
注意:从Spring 4.2开始,事件基础结构已经得到了显著的改进,并提供了一个基于注解的模型,以及发布任意事件的能力(也就是说,一个对象不一定是从ApplicationEvent扩展而来)。当这样的对象被发布时,Spring将其包装在一个事件中。
Spring提供的标准事件如下表所示:

事件解释
ContextRefreshedEvent当ApplicationContext被初始化或者刷新时(如调用ConfigurableApplicationContext接口的refresh())被发布。这里,初始化指的是加载了所有beans,检测并激活了后处理器beans,预实例化了单例,ApplicationContext对象准备好使用了。只要上下文没有关闭,刷新就可以多次触发,只要所选的ApplicationContext实际上支持这种“热”刷新,比如XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持。
ContextStartedEvent当ApplicationContext通过ConfigurableApplicationContext接口上的start()方法启动时发布。这里,“启动”意味着所有Lifecycle bean都接收一个显式的启动信号。通常,此信号用于在显式停止后重新启动bean,但也可用于启动未配置为自动启动的组件(例如,在初始化时尚未启动的组件)。
ContextStoppedEvent当ApplicationContext通过ConfigurableApplicationContext接口的stop()方法停止时发布。这里,“停止”意味着所有Lifecycle bean都接收一个显式的停止信号。一个停止的上下文可以通过start()调用重新启动。
ContextClosedEvent当ApplicationContext通过ConfigurableApplicationContext接口上的close()方法或通过JVM关闭钩子关闭时发布。这里,“关闭”意味着所有单例bean都将被销毁。一旦关闭上下文,它将到达生命周期的终点,无法刷新或重新启动。
RequestHandledEvent一个特定于web的事件,它告诉所有bean HTTP请求已经得到了服务。此事件在请求完成后发布。此事件仅适用于使用Spring的DispatcherServlet的web应用程序。
ServletRequestHandledEventRequestHandledEvent的子类,它添加了特定于servlet的上下文信息。

也可以创建和发布自定义的事件,举例如下:

public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}

为了发布自定义事件,需要调用ApplicationEventPublisher的publishEvent()方法,一般会创建一个类实现ApplicationEventPublisherAware,并将这个类注册为Bean,如下例子:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

在配置时,Spring容器检测到EmailService实现了ApplicationEventPublisherAware,并自动调用setApplicationEventPublisher()。实际上,传入的参数是Spring容器本身。通过应用程序上下文的ApplicationEventPublisher接口与它进行交互。
为了监听到自定义的ApplicationEvent,可以创建一个类实现ApplicationListener接口并将该类注册为bean,举例如下:

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意:ApplicationListener通常是用定制事件的类型参数化的(在前面的例子中是BlockedListEvent)。这意味着onApplicationEvent()方法可以保持类型安全,避免任何向下类型转换的需要。可以注册任意数量的事件监听器。默认情况下,事件监听器是同步接收事件的,这意味着publishEvent()方法会阻塞,直到所有监听器完成对事件的处理。这种同步和单线程方法的一个优点是,当监听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文中操作。
下面的示例用于说明上面的各个类的bean定义及配置:

<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="blockedlist@example.org"/>
</bean>

总结来说,当调用emailService bean的sendEmail()方法时,如果有任何email消息应该被接收,就会发布类型为BlockedListEvent的自定义事件。blockedListNotifier bean被注册为ApplicationListener并接收BlockedListEvent,此时它可以通知适当的方。

(1) 基于注解的事件监听者

从Spring4.2开始,可以通过@EventListener注解在任何一个被容器管理的bean的public方法上注册一个事件监听器。BlockedListNotifier可以进行如下改写:

public class BlockedListNotifier {//注意没有实现任何监听器接口

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

如果希望方法可以侦听多个事件,或者希望完全不使用参数来定义它,那么还可以在注释本身上指定事件类型。如下所示:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

还可以通过使用注解的condition属性(值可使用SpEL表达式定义)来进行额外的运行时过滤。SpEL表达式应该与实际调用特定事件的方法相匹配。下面的例子表示如何重写程序能够让事件的content属性等于my-event时,方法才会被调用:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blockedListEvent) {
    // notify appropriate parties via notificationAddress...
}

下表列出了上下文可用的SpEL项,便于在条件事件处理时使用。

名称位置描述举例
Eventroot对象真正的ApplicationEvent#root.event或者event
Arguments arrayroot对象用于调用该方法的参数(作为对象数组)#root.args或者args;args[0]获得第一个参数等
Arguments name评估上下文任何方法参数的名称。如果由于某些原因,名称不可用(例如,因为在已编译的字节代码中没有调试信息),也可以使用#a<#arg>语法使用单个参数,其中<#arg>表示参数索引(从0开始)。#blEvent或#a0(也可以使用#p0或#p<#arg>;作为别名的参数表示法)

请注意#root.event,能够访问底层事件,即使方法签名实际上引用了已发布的任意对象。
如果需要发布一个事件作为处理另一个事件的结果,您可以更改方法签名以返回应该发布的事件,如下面的示例所示:

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

这个新方法为上述方法处理的每个BlockedListEvent发布一个新的ListUpdateEvent。如果需要发布多个事件,则可以返回事件集合。

(2) 异步的监听者

想让监听者异步的处理事件,可以使用@Async注解。如下面的例子所示:

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

注意使用异步事件的限制:

  • 如果异步事件监听者抛出异常,该异常将不会被传递给调用者。
  • 异步事件监听器方法不能通过返回值来发布后续事件。如果处理结果需要发布另一个事件,则注入ApplicationEventPublisher以手动发布该事件。

(3) 有序的监听者

如果需要一个监听者在另一个监听者之前被调用,可以使用@Order注解。如下面所示:

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}

(4) 泛型事件

还可以使用泛型进一步定义事件的结构。考虑使用EntityCreatedEvent<T>,其中T是实际创建的实体的类型。举例如下:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

由于类型擦除,只有被触发的事件(如上面的event)解析出事件侦听器筛选的泛型参数时(解析出Person才有效)才有效,也就是说事件需要有如下形式:

class PersonCreatedEvent extends EntityCreatedEvent<Person> { …​ }

在有些情况下,事件都是同样的结构可能会产生问题。可以通过实现ResolvableTypeProvider来解决。

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}

3. 方便的访问底层资源

应用上下文是一个ResourceLoader,用来加载Resource对象。资源本质上是JDK java.net.URL类的功能更丰富的版本。事实上,Resource的实现在适当的地方封装了一个java.net.URL实例。Resource可以以透明的方式从几乎任何位置获取低级资源,包括类路径、文件系统位置、任何可以用标准URL描述的位置,以及其他一些变体。如果资源位置字符串是没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合于实际应用程序上下文类型。

可以配置部署到应用程序上下文中的bean来实现特殊的回调接口ResourceLoaderAware,以便在初始化时自动回调且应用程序上下文本身作为ResourceLoader传入。还可以公开资源类型的属性,用于访问静态资源。这些属性像其他属性一样被注入。您可以将这些资源属性指定为简单的字符串路径,部署bean时字符串会自动转换为实际资源对象。

位置路径或提供给ApplicationContext构造器的路径实际上是资源字符串,并且根据特定的上下文实现进行适当的处理。例如,ClassPathXmlApplicationContext将简单的位置路径视为类路径位置。您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL加载定义,而不管实际的上下文类型。

4. 应用程序启动追踪

ApplicationContext管理Spring应用的生命周期并围绕组件提供了丰富的编程模型。因此,复杂的应用可能会有复杂的组件图和启动阶段。
使用特定的指标追踪应用的启动步骤能够帮助我们了解在启动阶段时间花在哪里,同时也是理解上下文生命周期的很好的方法。
AbstractApplicationContext及其子类有一个ApplicationStartup的域,收集了不同启动阶段的StartupStep数据:

  • 应用上下文周期(基本包扫描,配置类管理)
  • beans生命周期(实例化,智能初始化,后处理器)
  • 应用事件处理
    下面的例子是AnnotationConfigApplicationContext中的实现:
// create a startup step and start recording
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();

为了最小化开销,默认的ApplicationStartup实现是一个无操作的变体。这意味着默认情况下,在应用程序启动期间不会收集任何指标。Spring框架附带了一个用Java飞行记录器跟踪启动步骤的实现:FlightRecorderApplicationStartup。要使用此变体,必须在ApplicationContext创建后立即配置它的一个实例。
如果开发人员想收取更精细的数据,可以提供自己的AbstractApplicationContext子类。也可以使用ApplicationStartup基础设施。
要收集自定义的StartupStep,组件可以ApplicationStartupAware以直接从应用程序上下文获取ApplicationStartup实例,或者在任何注入点请求ApplicationStartup类型。

5. 为Web应用实现更加便利化的ApplicationContext实例化

可以直接创建ApplicationContext实例通过使用ContextLoader,也可以通过ApplicationContext的实现来创建ApplicationContext对象。
可以通过ContextLoaderListener注册一个ApplicationContext。如下举例:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

监听器检查contextConfigLocation参数。如果该参数不存在,监听器就使用/WEB-INF/applicationContext.xml作为默认。如果参数存在,监听器通过使用预定义分隔符(逗号,分号以及空格)来分割参数值的字符串,并使用分割后的字符串作为查找应用上下文的位置。还支持Ant样式的路径模式。如:/WEB-INF/*Context.xml(以Context.xml结尾的所有文件)及/WEB-INF/**/*Context.xml(WEB-INF所有子目录下以Context.xml结尾的所有文件)。

6. 将Spring ApplicationContext部署为Java EE RAR文件

可以将Spring ApplicationContext部署为Java EE RAR文件,只要将上下文及其所需的所有bean类和库JARs封装在Java EE RAR部署单元中。RAR部署是部署无头WAR文件的更自然的替代方案。实际上,RAR是一个没有任何HTTP入口点的WAR文件,仅用于在Java EE环境中引导Spring ApplicationContext。
RAR部署非常适合那些不需要HTTP入口点,而只由消息端点和计划的作业组成的应用程序上下文。这种上下文中的bean可以使用应用服务器资源,如JTA事务管理器和JNDI绑定的JDBC数据源实例和JMS ConnectionFactory实例,还可以注册到平台的JMX服务器—所有这些都通过Spring的标准事务管理和JNDI和JMX支持设施实现。应用程序组件还可以通过Spring的TaskExecutor抽象与应用程序服务器的JCA WorkManager交互。(小辣鸡表示从如开始就不知道是个啥了(;′⌒`))
简单地将Spring ApplicationContext部署为Java EE RAR文件包含以下两步:

  • 将所有应用程序类打包进RAR文件中。添加所有的JARs库到RAR压缩包的根目录下。添加一个META-INF/ra.xml部署描述文件及对应的Spring XML bean定义文件META-INF/applicationContext.xml。
  • 把RAR文件丢进应用程序服务器部署目录下。
    注意:这种RAR部署单元通常是独立的。它们不向外部世界公开组件,甚至不向同一应用程序的其他模块公开组件。与基于RAR的ApplicationContext的交互通常通过与其他模块共享的JMS。基于rar的ApplicationContext还可以调度一些作业或对文件系统(或类似的)中的新文件作出反应。如果它需要允许从外部同步访问,它可以(例如)导出RMI端点,这些端点可能被同一台机器上的其他应用程序模块使用。

二、BeanFactory

BeanFactory API为Spring的IoC功能提供了底层基础。主要用于集成Spring的其它部分和相关的第三方框架。其DefaultListableBeanFactory实现是GenericApplicationContext高级容器中的一个关键代表。BeanFactory和相关的接口(如BeanFactoryAware、InitializingBean、DisposableBean)是其他框架组件的重要集成点。由于不需要任何注解甚至反射,它们允许容器及其组件之间非常有效的交互。应用程序级bean可以使用相同的回调接口,但通常更喜欢声明性依赖注入,可以通过注释,也可以通过编程式配置。
注意:BeanFactory API及其DefaultListableBeanFactory实现都有没对要使用的配置格式或者组件注解做任何假设。所有这些风格都是通过扩展(如XmlBeanDefinitionReader和AutowiredAnnotationBeanPostProcessor)引入的,并将共享BeanDefinition对象作为核心元数据表示进行操作。这就是Spring容器如此灵活和可扩展的本质。

1. BeanFactory或ApplicationContext?

本节解释BeanFactory和ApplicationContext容器之间的差异,以及引导的含义。
默认情况下应该使用ApplicationContext,使用GenericApplicationContext及其子类AnnotationConfigApplicationContext作为自定义引导的公共实现。这些是Spring核心容器的主要入口点,用于所有常见功能实现:加载配置文件、触发类路径扫描、以编程方式注册bean定义和带注解的类,以及(从5.0开始)注册功能bean定义。
因为ApplicationContext包含了BeanFactory的所有功能,所以通常推荐它优于普通的BeanFactory,除非需要对bean处理进行完全控制的场景除外。ApplicationContext能够通过转换(如通过bean的名称或者bean的类型)检测不同类型的beans。但是普通的DefaultListableBeanFactory对于特殊的beans不能够检测。
对于许多扩展容器特性,比如注解处理和AOP代理,BeanPostProcessor扩展点是必不可少的。如果只使用普通的DefaultListableBeanFactory,那么默认情况下下,这样的后处理器不会被检测和激活。就需要额外的设置来完全引导容器。
下面的表列出了BeanFactroy及ApplicationContext接口及其实现支持的不同特征:

特征BeanFactoryApplicationContext
Bean实例化及装配
完整的生命周期管理
自动化的bean后处理器注册
自动化的bean工厂后处理器注册
便利的消息源接入(国际化功能)
内置的ApplicationEvent发布机制

要显式地向DefaultListableBeanFactory注册一个bean后处理器,您需要以编程方式调用addBeanPostProcessor,如下例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions

// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory

要显式地向DefaultListableBeanFactory注册一个bean工厂后处理器,您需要以编程方式调用addBeanFactoryPostProcessor,如下例所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

在这里插入图片描述
从类关系图上看,ApplicationContext继承了BeanFactory,但是它还继承了其它功能接口,能够提供更多的功能,上面的表格也可以验证。因此从使用的简化操作角度会推荐使用ApplicationContext。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值