spring — spring中事件监听机制源码解析(五)

13 篇文章 0 订阅

关于spring相关文章回顾:
一、spring中Bean的初始化过程
二、spring中bean的生命周期
三、spring中AOP技术解析
四、spring中的事件驱动机制解析

一、Spring中事件驱动三大对象
  1. spring事件ApplicationEvent继承自EventObject,Spring提供了ApplicationEventPublisher接口作为事件发布者(ApplicationContext接口继承了该接口,担当着事件发布者的角色)。
  2. Spring提供了ApplicationEventMulticaster接口,负责管理ApplicationListener和真正发布ApplicationEvent(ApplicationContext是委托给它完成的);ApplicationListener实现了JDK的EventListener,但它抽象出一个onApplicationEvent方法,使用更方便。
  3. ApplicationEventPublisher最终都是委托给ApplicationEventMulticaster去完成的。当然你也可以自己去实现一个ApplicationEventMulticaster

在这里插入图片描述

public abstract class ApplicationEvent extends EventObject {
	private static final long serialVersionUID = 7099057708183571937L;	
	private final long timestamp;

	public ApplicationEvent(Object source) {
		super(source);
		this.timestamp = System.currentTimeMillis();
	}
	public final long getTimestamp() {
		return this.timestamp;
	}
}

@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
	// 此子接口提供了泛型,和提供了统一的处理方法
	void onApplicationEvent(E event);
}

@FunctionalInterface
public interface ApplicationEventPublisher {
	default void publishEvent(ApplicationEvent event) {
		publishEvent((Object) event);
	}
	
	// 这个接口是Spring4.2后提供的,可以发布任意的事件对象(即使不是ApplicationEvent的子类了)
	// 当这个对象不是一个ApplicationEvent,我们会使用PayloadApplicationEvent来包装一下再发送
	void publishEvent(Object event);
}
二、Spring事件监听机制源码分析
1、注册事件监听器

ApplicationEventPublisher 事件发布器是Spring 事件监听机制的核心。ApplicationContext的抽象实现类AbstractApplicationContext是所有Spring容器的基础。
AbstractApplicationContext类的源码中有方法registerListeners,这个注册的动作是在方法refresh()中被调用的,也就是在初始化容器的时候调用自动调用的。

/**
 * 注册监听器
 */
protected void registerListeners() {
	//getApplicationListeners()获得容器中所有的ApplicationListener对象
	for (ApplicationListener<?> listener : getApplicationListeners()) {
	//注册监听器对象的实例,真正的注册事件监听器的对象是ApplicationEventMulticaster
		getApplicationEventMulticaster().addApplicationListener(listener);
	}

	//注册监听器对象的名称,真正的注册事件监听器的对象是ApplicationEventMulticaster
	String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false);
	for (String listenerBeanName : listenerBeanNames) {
		getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName);
	}

	// 发布某些事件earlyEventsToProcess 包含的事件
	Set<ApplicationEvent> earlyEventsToProcess = this.earlyApplicationEvents;
	this.earlyApplicationEvents = null;
	if (earlyEventsToProcess != null) {
		for (ApplicationEvent earlyEvent : earlyEventsToProcess) {
			getApplicationEventMulticaster().multicastEvent(earlyEvent);
		}
	}
}

其中,getApplicationListeners()获得容器中所有的ApplicationListener对象:

public Collection<ApplicationListener<?>> getApplicationListeners() {
	return this.applicationListeners;
}

其中,this.applicationListeners字段的write过程是在方法addApplicationListener()中。其中涉及到对象applicationEventMulticaster,该对象是作为ApplicationContext实现事件发布器的委托对象,真正的监听器注册,发送事件都是由该对象实现。

@Override
public void addApplicationListener(ApplicationListener<?> listener) {
	Assert.notNull(listener, "ApplicationListener must not be null");
	if (this.applicationEventMulticaster != null) {
		this.applicationEventMulticaster.addApplicationListener(listener);
	}
	else {
		this.applicationListeners.add(listener);
	}
}

继续跟代码可以查到方法addApplicationListener()被ApplicationListenerDetector中的postProcessAfterInitialization()方法调用:

@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
	if (this.applicationContext != null && bean instanceof ApplicationListener) {
		// potentially not detected as a listener by getBeanNamesForType retrieval
		Boolean flag = this.singletonNames.get(beanName);
		if (Boolean.TRUE.equals(flag)) {
			//添加监听器
			this.applicationContext.addApplicationListener((ApplicationListener<?>) bean);
		}
		else if (Boolean.FALSE.equals(flag)) {
			if (logger.isWarnEnabled() && !this.applicationContext.containsBean(beanName)) {
				// inner bean with other scope - can't reliably process events
				logger.warn("Inner bean '" + beanName + "' implements ApplicationListener interface " +
						"but is not reachable for event multicasting by its containing ApplicationContext " +
						"because it does not have singleton scope. Only top-level listener beans are allowed " +
						"to be of non-singleton scope.");
			}
			this.singletonNames.remove(beanName);
		}
	}
	return bean;
}

ApplicationListenerDetector实现了BeanPostProcessor接口,可以在容器级别对所有bean的生命周期过程进行增强。这里主要是为了能够在初始化所有bean后识别出所有的事件监听器bean并将其注册到事件发布器中。具体可以参看refresh()方法中的prepareBeanFactory(beanFactory)过程,其中就注册ApplicationListenerDetector对象。

2、发布事件

我们一般都会使用AbstractApplicationContext#publish()来发布一个事件,AbstractApplicationContext类的方法中发布事件的过程源码:

protected void publishEvent(Object event, @Nullable ResolvableType eventType) {
	// Decorate event as an ApplicationEvent if necessary
	// 如果这个事件不是ApplicationEvent类型,那就包装成这个类型
	ApplicationEvent applicationEvent;
	if (event instanceof ApplicationEvent) {
		applicationEvent = (ApplicationEvent) event;
	} else {
		// 注意此处:第一个参数为source,这里传的source,第二个是payload,才传的是事件本身
		applicationEvent = new PayloadApplicationEvent<>(this, event);
		
		// 若没有指定类型。就交给PayloadApplicationEvent<T>,它会根据泛型类型生成出来的~~~
		if (eventType == null) {
			eventType = ((PayloadApplicationEvent) applicationEvent).getResolvableType();
		}
	}

	// Multicast right now if possible - or lazily once the multicaster is initialized
	// 如果是早期事件,就添加进去  会立马发布了(一般都不属于这种)
	if (this.earlyApplicationEvents != null) {
		this.earlyApplicationEvents.add(applicationEvent);
	} else {
		// 最终把这些时间都委派给了`ApplicationEventMulticaster` 让它去发送事件
		getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType);
	}

	// Publish event via parent context as well...
	// 此处注意:特别重要,如果是父容器,也会向父容器里广播一份~~~~~
	if (this.parent != null) {
		// 这个判断的用意是,既然eventType已经解析出来了,所以就调用protected内部方法即可,而不用再次解析一遍了
		if (this.parent instanceof AbstractApplicationContext) {
			((AbstractApplicationContext) this.parent).publishEvent(event, eventType);
		}
		// 如果是普通的发布,就没有eventType了
		else {
			this.parent.publishEvent(event);
		}
	}
}

从以上的源码中我们看到不论是监视器注册,还是事件的发布均由ApplicationEventMulticaster作为ApplicationContext的委托方进行实际的操作,接下来,我们分析一下ApplicationEventMulticaster:

  • 2.1 ApplicationEventMulticaster事件发布的加载时机
    从AbstractApplicationContext#initApplicationEventMulticaster()方法中,我们可以看下具体调用:
/** Helper class used in event publishing */
private ApplicationEventMulticaster applicationEventMulticaster;
/**
* 跟一下该字段applicationEventMulticaster被Write的时机,
* 发现是在initApplicationEventMulticaster()方法中,而该方法会在Spring容器初始化的时候被调用
*/
protected void initApplicationEventMulticaster() {
	ConfigurableListableBeanFactory beanFactory = getBeanFactory();
	if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
		this.applicationEventMulticaster =
				beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
		if (logger.isDebugEnabled()) {
			logger.debug("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
		}
	}
	else {
		this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
		beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
		if (logger.isDebugEnabled()) {
			logger.debug("Unable to locate ApplicationEventMulticaster with name '" +
					APPLICATION_EVENT_MULTICASTER_BEAN_NAME +
					"': using default [" + this.applicationEventMulticaster + "]");
		}
	}
}

逻辑很简单:
1.如果容器中存在ApplicationEventMulticaster该对象则直接获得该对象
2.若不存在ApplicationEventMulticaster对象则创建一个子类实例,
SimpleApplicationEventMulticaster。

  • 2.2 ApplicationEventMulticaster的唯一实现SimpleApplicationEventMulticaster
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {

	// 若set了一个执行器,那所有的监听器都将会异步执行
	@Nullable
	private Executor taskExecutor;
	// 监听者执行失败的回调~~~~~~(比如做回滚等等)
	@Nullable
	private ErrorHandler errorHandler;

	@Override
	public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
		ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));

		// 这里面有个细节:如果有执行器executor ,那就会扔给线程池异步去执行
		// 默认情况下是没有的(Spring默认情况下同步执行这些监听器的)  我们可以调用set方法配置一个执行器(建议)
		for (final ApplicationListener<?> listener : getApplicationListeners(event, type)) {
			Executor executor = getTaskExecutor();
			// 放在线程池里执行,相当于异步执行。绝大多数情况下,这里都是null
			if (executor != null) {
				executor.execute(() -> invokeListener(listener, event));
			} else {
				//这里会调用doInvokeListener(listener, event)这个方法
				invokeListener(listener, event);
			}
		}
	}
}


private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
	try {
		// 如果是实现了ApplicationListener接口,则直接调用其中的onApplicationEvent()方法;
		//如果是用@EventListener注释,则调用ApplicationListenerMethodAdapter中的onApplicationEvent()方法
		listener.onApplicationEvent(event);
	}
}

invokeListener的处理逻辑:
1.判断是否存在错误处理器,若存在错误处理器,当发生异常的时候调用对应的错误处理方法;
2.不存在错误错误处理器,直接调用doInvokeListener(listener, event);方法

protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
	ErrorHandler errorHandler = getErrorHandler();
	if (errorHandler != null) {
		try {
			doInvokeListener(listener, event);
		}
		catch (Throwable err) {
			errorHandler.handleError(err);
		}
	}
	else {
		doInvokeListener(listener, event);
	}
}
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
	try {
		//调用事件监视器中的onApplicationEvent方法,完成事件发布工作
		listener.onApplicationEvent(event);
	}
	catch (ClassCastException ex) {
		String msg = ex.getMessage();
		if (msg == null || matchesClassCastMessage(msg, event.getClass().getName())) {
			// Possibly a lambda-defined listener which we could not resolve the generic event type for
			// -> let's suppress the exception and just log a debug message.
			Log logger = LogFactory.getLog(getClass());
			if (logger.isDebugEnabled()) {
				logger.debug("Non-matching event type for listener: " + listener, ex);
			}
		}
		else {
			throw ex;
		}
	}
}
  • 2.3 Spring的使用@EventListener监听事件
    从上面方法可以看到,如果是用@EventListener注释,则调用ApplicationListenerMethodAdapter中的onApplicationEvent()方法:
@Override
public void onApplicationEvent(ApplicationEvent event) {
	processEvent(event);
}
public void processEvent(ApplicationEvent event) {
	// 获取参数,最终会交给回调的方法的。事件类型是PayloadApplicationEvent,那就把.getPayload(),否则就是event本身喽
	Object[] args = resolveArguments(event);
	// 解析condition表达式(注意,此处把args传进去了) 因此我们表达式里是可以用这个参数的哦
	if (shouldHandle(event, args)) {
		// 就是执行目标方法,我们一般返回值都是void,所以就是null
		// 但是,但是,但是注意了,此处若返回的不是null,还有处理~~~~非常给力:
		Object result = doInvoke(args);
		if (result != null) {
			
			// 如果返回值是数组或者Collection,会把里面内容当作事件循环publishEvent
			// 如果就是个POJO,那就直接publish  
			// 事件的传递性 就这么的来了,强大啊
			handleResult(result);
		}
		else {
			logger.trace("No result object given - no result to handle");
		}
	}
}

Spring的使用@EventListener监听事件。若监听方法有返回值,那将会把这个返回值当作事件源,一直发送下去,直到返回void或者null停止

3、根据发布的事件找到对应的事件监听器

通过事件找事件监听器的基本思想很简单:

  1. 首先,Spring的事件监听器中是通过泛型定义的,在具体使用的时候会指定实际类型,通过反射可得实际监听的事件类型;
  2. 其次,Spring发布事件时会将事件作为参数传递给事件发布器;
  3. 从而,事件发布器有了事件信息和事件监听器所监听的事件类型,就可以通过事件找到对应的时间监听器了。

具体在类AbstractApplicationEventMulticaster的方法getApplicationListeners()中实现的:

// @since 1.2.3
// 提供基本的侦听器注册功能   比如处理代理对象类型~~~
public abstract class AbstractApplicationEventMulticaster
		implements ApplicationEventMulticaster, BeanClassLoaderAware, BeanFactoryAware {
	// 它是一个内部类,内部持有applicationListeners和applicationListenerBeans的引用
	// 是一个类似包装的类,详细可参加下面具体分析
	private final ListenerRetriever defaultRetriever = new 
ListenerRetriever(false);

	// 如果指定了event事件和eventType,那就这个方法   绝大多数情况下都是这里~~~
	// 获取该事件对应的监听者:相当于只会获取supportsEvent() = true支持的这种事件~
	protected Collection<ApplicationListener<?>> getApplicationListeners(ApplicationEvent event, ResolvableType eventType) {

		Object source = event.getSource();
		Class<?> sourceType = (source != null ? source.getClass() : null);
		// 这个key是它俩共同决定的~~~~
		ListenerCacheKey cacheKey = new ListenerCacheKey(eventType, sourceType);

		// Quick check for existing entry on ConcurrentHashMap...
		// 缓存里若存在  直接返回即可~~~~
		ListenerRetriever retriever = this.retrieverCache.get(cacheKey);
		if (retriever != null) {
			return retriever.getApplicationListeners();
		}


		// 这里面~~~ 有个缓存安全的特殊处理,其最为核心的方法,其实还是retrieveApplicationListeners
		// 若是缓存安全的,才会缓存它  否则直接return即可~~~~
		// 什么叫缓存安全isCacheSafe:原理很简单,就是判断该类型是否在指定classloader或者其parent classloader中
		if (this.beanClassLoader == null || (ClassUtils.isCacheSafe(event.getClass(), this.beanClassLoader) &&
						(sourceType == null || ClassUtils.isCacheSafe(sourceType, this.beanClassLoader)))) {
			// Fully synchronized building and caching of a ListenerRetriever
			synchronized (this.retrievalMutex) {
				retriever = this.retrieverCache.get(cacheKey);
				if (retriever != null) {
					return retriever.getApplicationListeners();
				}
				retriever = new ListenerRetriever(true);
				// 需要缓存起来,所以才需要把retriever传过去,否则传null即可~(下面传的null)
				Collection<ApplicationListener<?>> listeners = retrieveApplicationListeners(eventType, sourceType, retriever);
			
				// 每个事件对应的Listener,都缓存在此处了~(注意:首次get的才给与缓存)
				// 因为有的是个体的beanName,有的是给的Bean,所以首次去拿时候缓存吧~~~
				this.retrieverCache.put(cacheKey, retriever);
				return listeners;
			}
		} else {
			// No ListenerRetriever caching -> no synchronization necessary
			return retrieveApplicationListeners(eventType, sourceType, null);
		}
	}
}	

基本逻辑:
1.通过事件源类型和事件类型构成ListenerCacheKey
2.从缓存中查找,存在直接方法,不存在走下一步
3.缓存中不存在则,Collection<ApplicationListener<?>> listeners =
retrieveApplicationListeners(eventType, sourceType, retriever);
4.将查到的结果缓存起来

这就是我们getApplicationListeners的具体内容,我们发现:它只会拿注册到本容器的监听器(注册在谁身上就是谁的)并不会去父类的拿的,所以这点一定要注意,你自己写监听器的时候也是需要注意这一点的,避免一些重复执行吧。

关于AbstractApplicationEventMulticaster#retrieveApplicationListeners方法,它就是从defaultRetriever把applicationListeners和beanNames都拿出来合并:

private Collection<ApplicationListener<?>> retrieveApplicationListeners(
			ResolvableType eventType, Class<?> sourceType, ListenerRetriever retriever) {

	LinkedList<ApplicationListener<?>> allListeners = new LinkedList<ApplicationListener<?>>();
	Set<ApplicationListener<?>> listeners;
	Set<String> listenerBeans;
	synchronized (this.retrievalMutex) {
		listeners = new LinkedHashSet<ApplicationListener<?>>(this.defaultRetriever.applicationListeners);
		listenerBeans = new LinkedHashSet<String>(this.defaultRetriever.applicationListenerBeans);
	}
	for (ApplicationListener<?> listener : listeners) {
	//找到指定类型的监听器
		if (supportsEvent(listener, eventType, sourceType)) {
			if (retriever != null) {
				retriever.applicationListeners.add(listener);
			}
			
			allListeners.add(listener);
		}
	}
	//返回结果
	return allListeners;
}

对应看supportsEvent方法:

protected boolean supportsEvent(ApplicationListener<?> listener, ResolvableType eventType, Class<?> sourceType) {
	GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
			(GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
	return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}

处理逻辑:
1.当前Listener是GenericApplicationListener 类型,直接强转,否则通过适配器GenericApplicationListenerAdapter获得对象GenericApplicationListener
2.对于获得GenericApplicationListener 对象直接调用其中 的supportsEventType和supportsSourceType方法即可判断该监视器是否支持该事件类型

4、ApplicationListener和@EventListener的区别

ApplicationListener和@EventListener的区别是跟他们什么时候注册有关。上面已经讲述了AbstractApplicationEventMulticaster是怎么获取到当前的所有的监听器的,那么他们的区别就在于:它俩注册的时机不一样(此处统一不考虑手动注册时间的情况)。

@EventListener存在漏事件的现象,但是ApplicationListener能监听到所有的相关事件	
  • 4.1 ApplicationListener的注册时机
    它是靠一个后置处理器:ApplicationListenerDetector它来处理的。它有两个方法处理:
// @since 4.3.4 出现得还是比较晚的~~~
class ApplicationListenerDetector implements DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor {
	...
	// 这个方法会在merge Bean的定义信息时候执行,缓存下该Bean是否是单例Bean
	// 因为后面注册的时候:只有单例Bean才给注册为监听器~~~
	@Override
	public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
		if (this.applicationContext != null) {
			this.singletonNames.put(beanName, beanDefinition.isSingleton());
		}
	}
	
	@Override
	public Object postProcessAfterInitialization(Object bean, String beanName) {
		if (this.applicationContext != null && bean instanceof ApplicationListener) {
			// 显然  只有单例Bean才会add进去  注册进去		
			if (Boolean.TRUE.equals(flag)) {
				this.applicationContext.addApplicationListener((ApplicationListener<?>) bean);
			} else if (Boolean.FALSE.equals(flag)) {
				// 输出一个warn日志:
				if (logger.isWarnEnabled() && !this.applicationContext.containsBean(beanName)) {
					// 提示用户这个Bean实现了ApplicationListener  但是并不是单例的
					logger.warn("...");
				}
				// 不是单例的就从缓存移除吧~~~~
				this.singletonNames.remove(beanName);
			}
		}
		return bean;
	}
	...
}

因为它是以Bean定义的形式注册进工厂的,并且 refresh() 中有一步 registerListeners() 它负责注册所有的监听器(Bean形式的),然后才是finishBeanFactoryInitialization(beanFactory),所以它是不会落掉事件的。

  • 4.2 @EventListener的注册时机
    注册它的是EventListenerMethodProcessor,它是一个SmartInitializingSingleton,它一直到preInstantiateSingletons()所有的单例Bean全部实例化完成了之后,它才被统一注册进去。所以它注册的时机是挺晚的。

由此知道,如果你在普通的单例Bean初始化期间(比如给属性赋值时、构造函数内。。。)发出了一个时间,@EventListener这种方式的监听器很有可能是监听不到的。

@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        for (String beanDefinitionName : beanFactory.getBeanDefinitionNames()) {
            // 让EventListenerMethodProcessor惰性加载~~~~
            if (beanDefinitionName.equals(AnnotationConfigUtils.EVENT_LISTENER_PROCESSOR_BEAN_NAME)) {
                beanFactory.getBeanDefinition(beanDefinitionName).setLazyInit(true);
            }
        }
    }
}

这样容器完成所有的单例实例化步骤后,其实EventListenerMethodProcessor这个Bean并没有完成真正的实例化的。而beanFactory.preInstantiateSingletons()方法最后一步为:

public void preInstantiateSingletons() throws BeansException {
	// Trigger post-initialization callback for all applicable beans...
	// 执行所有的SmartInitializingSingleton  这里面最为核心的就在于~~~~
	// getSingleton(beanName)这个方法,是直接去Map里找,只有被实例化的的单例Bean才会返回true,否则是false
	// 不知为何Spring此处不用getBean()   我个人认为  这是Spring为了提高速度的一个疏忽吧~~~~~
	for (String beanName : beanNames) {
		Object singletonInstance = getSingleton(beanName);
		if (singletonInstance instanceof SmartInitializingSingleton) {
			final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
			if (System.getSecurityManager() != null) {
				AccessController.doPrivileged(new PrivilegedAction<Object>() {
					@Override
					public Object run() {
						smartSingleton.afterSingletonsInstantiated();
						return null;
					}
				}, getAccessControlContext());
			}
			else {
				smartSingleton.afterSingletonsInstantiated();
			}
		}
	}
}

如上:getSingleton方法是直接去DefaultSingletonBeanRegistry的Map<String, Object> singletonObjects里找的(含singletonFactories)。显然EventListenerMethodProcessor因为是Lazy加载,所以目前还仅仅是Bean的定义信息,所以就不会检测@EventListener的方法,因此它就不生效了

5、总结

本文暂时只介绍了Spring中的一些简单的事件驱动机制,及源码分析希望能够帮助读者对事件监听机制有一个深入的理解和把握。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

RachelHwang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值