Spring AspectJ LTW

LTW(Load Time Weaver),即加载期切面织入,是ApsectJ切面织入的一种方式,它通过JVM代理在类加载期替换字节码到达织入切面的目的。下面通过一个简单的例子来演示怎么使用LTW,然后从代码入手分析LTW的实现原理。

简单的实例

首先定义个切面类,该切面功能非常简单,就是在spring.beans.ltw及其所有子包下所有的类的public方法调用前后各添加一行begin和end日志:

@Aspect
public class MethodAroundAspect {
	@Around("aroundMethod()")
	public Object around(ProceedingJoinPoint pjp) throws Throwable {
		System.out.println(pjp.getSignature().getName() + " begin...");
		Object result = pjp.proceed();
		System.out.println(pjp.getSignature().getName() + " end...");
		return result;
	}

	@Pointcut("execution(public * spring.beans.ltw..*(..))")
	public void aroundMethod() {
	}
}

使用了AspectJ注解,想要了解更多的Aspect注解使用可以查看AspectJ相关的文档,在https://github.com/eclipse/org.aspectj/tree/master/docs下面有个quick5.doc文档列出了所有的注解,也可以查看源代码或者反编译aspectweaver.jar,查看里面有哪些注解,在org.aspectj.lang.annotation这个包下面

编写目标测试bean:

public class LTWBean {

	public void run() {
		System.out.println("LTWBean run...");
	}
}

编写一个XML文件定义切面,该文件名及其所在路径只能固定的几种:META-INF/aop.xml、META-INF/aop-ajc.xml、org/aspectj/aop.xml,文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd">
<aspectj>
	<weaver>
		<include within="spring.beans.ltw.*" />
	</weaver>
	<aspects>
		<aspect name="spring.beans.ltw.MethodAroundAspect" />
	</aspects>
</aspectj>
文件定义了切面以及切面编织器,这里的切面会被织入spring.beans.ltw及其子包下的所有类。

编织器注入Spring容器中,并且定义目标测试bean:

<bean id="loadTimeWeaver"
	class="org.springframework.context.weaving.DefaultContextLoadTimeWeaver"></bean>
<bean class="org.springframework.context.weaving.AspectJWeavingEnabler"></bean>
<bean id="ltwBean" class="spring.beans.ltw.LTWBean" lazy-init="true"></bean>
上面的编织器bean的名称必须是loadTimeWeaver,此外还有一种更简单的配置方式,使用context名称空间下的标签:

<context:load-time-weaver aspectj-weaving="autodetect" />
<bean id="ltwBean" class="spring.beans.ltw.LTWBean"></bean>
上面两段配置起到的效果完全是一样的,bean解析器在解析到context:load-time-weaver标签时,会自动生成一个名称为loadTimeWeaver类型org.springframework.context.weaving.DefaultContextLoadTimeWeaver的bean以及一个类型org.springframework.context.weaving.AspectJWeavingEnabler的匿名bean,这段代码在LoadTimeWeaverBeanDefinitionParser类中,aspectj-weaving属性有三个枚举值:on、off、autodetect,分别是打开、关闭、自动检测,这里设置成autodetect,容器会检查程序是否定义了切面定义文件(即上面提到的aop.xml文件),代码在LoadTimeWeaverBeanDefinitionParser的isAspectJWeavingEnabled方法。

编写测试代码:

BeanFactory context = new ClassPathXmlApplicationContext(
		"spring/beans/ltw/ltw.xml");
LTWBean ltwBean = (LTWBean) context.getBean("ltwBean");
ltwBean.run();
运行测试代码,在运行时需要启动一个JVM代理,首先需要下载spring-instrument-xxx.jar包,我的时3.2.9版本,jar文件名为spring-instrument-3.2.9.RELEASE.jar,在虚拟机参数中添加-javaagent:E:\study\java\spring\spring-framework-3.2.9.RELEASE\libs\spring-instrument-3.2.9.RELEASE.jar,执行测试代码,打印下面日志,说明切面织入成功:

run begin...
LTWBean run...
run end...

下面从源代码入手简单的分析一下Spring LTW的实现原理。

字节码转换和虚拟机代理

要了解LTW的原理,首先要对JDK的字节码转换框架和JVM代理有一定的了解:

从JAVA5开始,在JDK中添加了一个新包java.lang.instrument,这个包就是字节码转换框架的基础。字节码转换框架是一项非常重要的技术,许多程序监控和调试工具比如BTrace都是在这个框架的基础上实现的。这个包下面有两个关键接口:

  • ClassFileTransformer:类字节码转换器,提供了一个transform方法,这个方法的用来转换提供的类字节码,并返回一个新的替换字节码。不同于CGLIB、JDK动态代理等字节码操作技术,ClassFileTransformer是彻底的替换掉原类,而CGLIB和JDK动态代理是生成一个新子类或接口实现。
  • Instrumentation:这个类提供检测 Java 编程语言代码所需的服务。检测是向方法中添加字节码,以搜集各种工具所使用的数据。由于更改完全是进行添加,所以这些工具不修改应用程序的状态或行为。这种无害工具的例子包括镜像代理、分析器、覆盖分析器和事件记录器。可以通过它的addTransformer方法把实现好的字节码转换器(ClassFileTransformer)注册到JVM中。

在普通代码中无需实现Instrumentation并且创建Instrumentation实例,要获取Instrumentation实例,可以通过JVM代理,JVM代理可以通过两种方式启动:

程序启动时启动代理

在程序启动时指定代理,这时候虚拟机会创建一个Instrumentation实例实例传递给代理类的premain方法。需要在META-INF/MANIFEST.MF中通过Premain-Class指定代理入口类,并且代理入口类中必须定义premain方法,像上面提到的在运行程序时在虚拟机参数添加-javaagent就是在程序启动时指定了代理,我们可以看看spring-instrument-3.2.9.RELEASE.jar包中MANIFEST.MF文件的内容:

Manifest-Version: 1.0
Created-By: 1.7.0_55 (Oracle Corporation)
Implementation-Title: spring-instrument
Implementation-Version: 3.2.9.RELEASE
Premain-Class: org.springframework.instrument.InstrumentationSavingAge
 nt
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Can-Set-Native-Method-Prefix: false
看以看到通过Premain-Class指定了org.springframework.instrument.InstrumentationSavingAge作为代理入口类,看看InstrumentationSavingAge这个类的代码,有一个premain方法并且有一个 Instrumentation参数:

public class InstrumentationSavingAgent {

	private static volatile Instrumentation instrumentation;


	/**
	 * Save the {@link Instrumentation} interface exposed by the JVM.
	 */
	public static void premain(String agentArgs, Instrumentation inst) {
		instrumentation = inst;
	}


	/**
	 * Return the {@link Instrumentation} interface exposed by the JVM.
	 * <p>Note that this agent class will typically not be available in the classpath
	 * unless the agent is actually specified on JVM startup. If you intend to do
	 * conditional checking with respect to agent availability, consider using
	 * {@link org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver#getInstrumentation()}
	 * instead - which will work without the agent class in the classpath as well.
	 * @return the {@code Instrumentation} instance previously saved when
	 * the {@link #premain} method was called by the JVM; will be {@code null}
	 * if this class was not used as Java agent when this JVM was started.
	 * @see org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver#getInstrumentation()
	 */
	public static Instrumentation getInstrumentation() {
		return instrumentation;
	}

}

程序运行时启动代理

还有一种方式是在程序启动之后启动代理,这种情况下可以在不暂停应用程序的情况下动态向注册类转换器对已加载的类进行重定义。这种方式需要在META-INF/MANIFEST.MF文件中通过Agent-Class指定代理入口类,并且入口代理类中定义agentmain方法,虚拟机把Instrumentation实例传递给这个agentmain方法,下面代码是Druid框架(Druid是阿里巴巴一个开源的连接池框架)中拷贝过来的一段代码,演示了如何在应用运行时启动一个JMX代理:

private static String loadManagementAgentAndGetAddress(int vmid) throws IOException {
    VirtualMachine vm = null;
    String name = String.valueOf(vmid);
    try {
        vm = VirtualMachine.attach(name);
    } catch (AttachNotSupportedException x) {
        throw new IOException(x.getMessage(), x);
    }

    String home = vm.getSystemProperties().getProperty("java.home");

    // Normally in ${java.home}/jre/lib/management-agent.jar but might
    // be in ${java.home}/lib in build environments.

    String agent = home + File.separator + "jre" + File.separator + "lib" + File.separator + "management-agent.jar";
    File f = new File(agent);
    if (!f.exists()) {
        agent = home + File.separator + "lib" + File.separator + "management-agent.jar";
        f = new File(agent);
        if (!f.exists()) {
            throw new IOException("Management agent not found");
        }
    }

    agent = f.getCanonicalPath();
    try {
        vm.loadAgent(agent, "com.sun.management.jmxremote");
    } catch (AgentLoadException x) {
        throw new IOException(x.getMessage(), x);
    } catch (AgentInitializationException x) {
        throw new IOException(x.getMessage(), x);
    }

    // get the connector address
    Properties agentProps = vm.getAgentProperties();
    String address = (String) agentProps.get(LOCAL_CONNECTOR_ADDRESS_PROP);
    vm.detach();

    return address;
}
运行management-agent.jar作为代理,来看看management-agent.jar中的META-INF/MANIFEST.MF文件,同时指定了Premain-Class和Agent-Class,代理启动之后虚拟机会调用代理入口类的agentmain方法,需要注意的是Agent类只有一个String参数的agentmain并没有定义带Instrumentation参数的agentmain,因为JMX代理并不需要Instrumentation实例:

Manifest-Version: 1.0
Premain-Class: sun.management.Agent
Created-By: 1.5.0 (Sun Microsystems Inc.)
Agent-Class: sun.management.Agent

实现原理

了解了JDK字节码框架和虚拟机代理之后,分析LTW的实现原理就简单得多了。

当容器检查到定义了名称为loadTimeWeaver的bean时,会注册一个LoadTimeWeaverAwareProcessor到容器中,代码在AbstractApplicationContext的prepareBeanFactory方法中:

protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
	
	...
	
	if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) {
		beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory));
		// Set a temporary ClassLoader for type matching.
		beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
	}
	
	...
}
LoadTimeWeaverAwareProcessor是一个BPP(BeanPostProcessor),这个BPP用来处理LoadTimeWeaverAware接口的,把LTW实例设置到实现了LoadTimeWeaverAware接口的bean中,从LoadTimeWeaverAwareProcessor的postProcessBeforeInitialization方法可以看出来:

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
	if (bean instanceof LoadTimeWeaverAware) {
		LoadTimeWeaver ltw = this.loadTimeWeaver;
		if (ltw == null) {
			Assert.state(this.beanFactory != null,
					"BeanFactory required if no LoadTimeWeaver explicitly specified");
			ltw = this.beanFactory.getBean(
					ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME, LoadTimeWeaver.class);
		}
		((LoadTimeWeaverAware) bean).setLoadTimeWeaver(ltw);
	}
	return bean;
}
再来看下AspectJWeavingEnabler这个类的代码,它是一个BFPP,同时也实现了LoadTimeWeaverAware接口,通过上面的分析,loadTimeWeaver这个bean会自动注入到AspectJWeavingEnabler类型bean中。AspectJWeavingEnabler的postProcessBeanFactory方法直接调用enableAspectJWeaving方法,来看看这个方法的代码:

public static void enableAspectJWeaving(LoadTimeWeaver weaverToUse, ClassLoader beanClassLoader) {
	if (weaverToUse == null) {
		if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) {
			weaverToUse = new InstrumentationLoadTimeWeaver(beanClassLoader);
		}
		else {
			throw new IllegalStateException("No LoadTimeWeaver available");
		}
	}
	weaverToUse.addTransformer(new AspectJClassBypassingClassFileTransformer(
				new ClassPreProcessorAgentAdapter()));
}
weaverToUse这个参数就是被容器自动注入的loadTimeWeaver bean,从bean定义XML中可以知道这个bean是DefaultContextLoadTimeWeaver类型的,它的addTransformer方法代码如下:

public void addTransformer(ClassFileTransformer transformer) {
	this.loadTimeWeaver.addTransformer(transformer);
}
DefaultContextLoadTimeWeaver类也有个loadTimeWeaver 属性,这个属性是在setBeanClassLoader方法中设置进去的:

public void setBeanClassLoader(ClassLoader classLoader) {
	LoadTimeWeaver serverSpecificLoadTimeWeaver = createServerSpecificLoadTimeWeaver(classLoader);
	if (serverSpecificLoadTimeWeaver != null) {
		if (logger.isInfoEnabled()) {
			logger.info("Determined server-specific load-time weaver: " +
					serverSpecificLoadTimeWeaver.getClass().getName());
		}
		this.loadTimeWeaver = serverSpecificLoadTimeWeaver;
	}
	else if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) {
		logger.info("Found Spring's JVM agent for instrumentation");
		this.loadTimeWeaver = new InstrumentationLoadTimeWeaver(classLoader);
	}
	else {
		try {
			this.loadTimeWeaver = new ReflectiveLoadTimeWeaver(classLoader);
			logger.info("Using a reflective load-time weaver for class loader: " +
					this.loadTimeWeaver.getInstrumentableClassLoader().getClass().getName());
		}
		catch (IllegalStateException ex) {
			throw new IllegalStateException(ex.getMessage() + " Specify a custom LoadTimeWeaver or start your " +
					"Java virtual machine with Spring's agent: -javaagent:org.springframework.instrument.jar");
		}
	}
}
在方法里面判断了当前是否存在Instrumentation实例,最终会取InstrumentationSavingAgent类中的instrumentation的静态属性,判断这个属性是否是null,从前面的分析可以知道InstrumentationSavingAgent这个类是spring-instrument-3.2.9.RELEASE.jar的代理入口类,当应用程序启动时启动了spring-instrument-3.2.9.RELEASE.jar代理时,即在虚拟机参数中设置了-javaagent参数,虚拟机会创建Instrumentation实例并传递给premain方法,InstrumentationSavingAgent会把这个类保存在instrumentation静态属性中。所以在程序启动时启动了代理时InstrumentationLoadTimeWeaver.isInstrumentationAvailable()这个方法是返回true的,所以loadTimeWeaver属性会设置成InstrumentationLoadTimeWeaver对象。

接下来就看看InstrumentationLoadTimeWeaver类的addTransformer方法代码:

public void addTransformer(ClassFileTransformer transformer) {
	Assert.notNull(transformer, "Transformer must not be null");
	FilteringClassFileTransformer actualTransformer =
			new FilteringClassFileTransformer(transformer, this.classLoader);
	synchronized (this.transformers) {
		if (this.instrumentation == null) {
			throw new IllegalStateException(
					"Must start with Java agent to use InstrumentationLoadTimeWeaver. See Spring documentation.");
		}
		this.instrumentation.addTransformer(actualTransformer);
		this.transformers.add(actualTransformer);
	}
}
从代码中可以看到,这个方法中,把类转换器actualTransformer通过instrumentation实例注册给了虚拟机。这里采用了修饰器模式,actualTransformer对transformer进行修改封装,下面是FilteringClassFileTransformer这个内部类的代码:

private static class FilteringClassFileTransformer implements ClassFileTransformer {

	private final ClassFileTransformer targetTransformer;

	private final ClassLoader targetClassLoader;

	public FilteringClassFileTransformer(ClassFileTransformer targetTransformer, ClassLoader targetClassLoader) {
		this.targetTransformer = targetTransformer;
		this.targetClassLoader = targetClassLoader;
	}

	public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
			ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

		if (!this.targetClassLoader.equals(loader)) {
			return null;
		}
		return this.targetTransformer.transform(
				loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
	}

	@Override
	public String toString() {
		return "FilteringClassFileTransformer for: " + this.targetTransformer.toString();
	}
}

这里面的targetClassLoader就是容器的bean类加载,在进行类字节码转换之前先判断执行类加载的加载器是否是bean类加载器,如果不是的话跳过类装换逻辑直接返回null,返回null的意思就是不执行类转换还是使用原始的类字节码。什么情况下会有类加载不是bean的类加载器的情况?在我们上面列出的AbstractApplicationContext的prepareBeanFactory方法中有一行代码:

beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader()));
当容器中注册了loadTimeWeaver之后会给容器设置一个ContextTypeMatchClassLoader类型的临时类加载器,在织入切面时只有在bean实例化时织入切面才有意义,在进行一些类型比较或者校验的时候,比如判断一个bean是否是FactoryBean、BPP、BFPP,这时候不涉及到实例化,所以做字节码转换没有任何意义,而且还会增加无谓的性能消耗,所以在进行这些类型比较时使用这个临时的类加载器执行类加载,这样在上面的transform方法就会因为类加载不匹配而跳过字节码转换,这里有一点非常关键的是,ContextTypeMatchClassLoader的父类加载就是容器bean类加载器,所以ContextTypeMatchClassLoader类加载器是不遵循“双亲委派”的,因为如果它遵循了“双亲委派”,那么它的类加载工作还是会委托给bean类加载器,这样的话if里面的条件就不会匹配,还是会执行类转换。ContextTypeMatchClassLoader的类加载工作会委托给ContextOverridingClassLoader类对象,有兴趣可以看看ContextOverridingClassLoader和OverridingClassLoader这两个类的代码。

这个临时的类加载器会在容器初始化快结束时,容器bean实例化之前被清掉,代码在AbstractApplicationContext类的finishBeanFactoryInitialization方法:

protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
	
	...
	
	beanFactory.setTempClassLoader(null);

	// Allow for caching all bean definition metadata, not expecting further changes.
	beanFactory.freezeConfiguration();

	// Instantiate all remaining (non-lazy-init) singletons.
	beanFactory.preInstantiateSingletons();
}


再回头来看FilteringClassFileTransformer类的transform方法,调用targetTransformer执行字节码转换。来看看targetTransformer这个类转换器是在哪创建的,回头再看下AspectJWeavingEnabler类的enableAspectJWeaving方法,有下面这行代码:

weaverToUse.addTransformer(new AspectJClassBypassingClassFileTransformer(
					new ClassPreProcessorAgentAdapter()));

AspectJClassBypassingClassFileTransformer类和ClassPreProcessorAgentAdapter类都实现了字节码转换接口ClassFileTransformer:
private static class AspectJClassBypassingClassFileTransformer implements ClassFileTransformer {

	private final ClassFileTransformer delegate;

	public AspectJClassBypassingClassFileTransformer(ClassFileTransformer delegate) {
		this.delegate = delegate;
	}

	public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
			ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {

		if (className.startsWith("org.aspectj") || className.startsWith("org/aspectj")) {
			return classfileBuffer;
		}
		return this.delegate.transform(loader, className, classBeingRedefined, protectionDomain, classfileBuffer);
	}
}
这也是一个修饰器模式,最终会调用ClassPreProcessorAgentAdapter的transform方法执行字节码转换逻辑,在类加载器定义类时(即调用defineClass方法)会调用此类的transform方法来进行字节码转换替换原始类。ClassPreProcessorAgentAdapter类中的代码比较多,这里就不列出来了,它的主要工作是解析aop.xml文件,解析类中的Aspect注解,并且根据解析结果来生成转换后的字节码。

在上面例子里面提到的通过context名称空间下的load-time-weaver标签来配置,其本质原理是一致的。通过在context的名称空间处理器ContextNamespaceHandler中可以看到load-time-weaver标签的解析器是LoadTimeWeaverBeanDefinitionParser类,看下这个类的代码:

class LoadTimeWeaverBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {

	private static final String WEAVER_CLASS_ATTRIBUTE = "weaver-class";

	private static final String ASPECTJ_WEAVING_ATTRIBUTE = "aspectj-weaving";

	private static final String DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME =
			"org.springframework.context.weaving.DefaultContextLoadTimeWeaver";

	private static final String ASPECTJ_WEAVING_ENABLER_CLASS_NAME =
			"org.springframework.context.weaving.AspectJWeavingEnabler";


	@Override
	protected String getBeanClassName(Element element) {
		if (element.hasAttribute(WEAVER_CLASS_ATTRIBUTE)) {
			return element.getAttribute(WEAVER_CLASS_ATTRIBUTE);
		}
		return DEFAULT_LOAD_TIME_WEAVER_CLASS_NAME;
	}

	@Override
	protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext) {
		return ConfigurableApplicationContext.LOAD_TIME_WEAVER_BEAN_NAME;
	}

	@Override
	protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
		builder.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);

		if (isAspectJWeavingEnabled(element.getAttribute(ASPECTJ_WEAVING_ATTRIBUTE), parserContext)) {
			RootBeanDefinition weavingEnablerDef = new RootBeanDefinition();
			weavingEnablerDef.setBeanClassName(ASPECTJ_WEAVING_ENABLER_CLASS_NAME);
			parserContext.getReaderContext().registerWithGeneratedName(weavingEnablerDef);

			if (isBeanConfigurerAspectEnabled(parserContext.getReaderContext().getBeanClassLoader())) {
				new SpringConfiguredBeanDefinitionParser().parse(element, parserContext);
			}
		}
	}

	protected boolean isAspectJWeavingEnabled(String value, ParserContext parserContext) {
		if ("on".equals(value)) {
			return true;
		}
		else if ("off".equals(value)) {
			return false;
		}
		else {
			// Determine default...
			ClassLoader cl = parserContext.getReaderContext().getResourceLoader().getClassLoader();
			return (cl.getResource(AspectJWeavingEnabler.ASPECTJ_AOP_XML_RESOURCE) != null);
		}
	}

	protected boolean isBeanConfigurerAspectEnabled(ClassLoader beanClassLoader) {
		return ClassUtils.isPresent(SpringConfiguredBeanDefinitionParser.BEAN_CONFIGURER_ASPECT_CLASS_NAME,
				beanClassLoader);
	}

}
从上面的代码可以看出在解析load-time-weaver标签时,从getBeanClassName方法中可以看到,如果没有指定weaver-class属性,会自动给容器中注入一个org.springframework.context.weaving.DefaultContextLoadTimeWeaver类型的bean,从resolveId方法中看到,该bean的名称为loadTimeWeaver。在doParse方法中,还会注册一个类型为org.springframework.context.weaving.AspectJWeavingEnabler的匿名bean。从此可以看出下面两段配置完全是等价的:

<bean id="loadTimeWeaver"
	class="org.springframework.context.weaving.DefaultContextLoadTimeWeaver"></bean>
<bean class="org.springframework.context.weaving.AspectJWeavingEnabler"></bean>

<context:load-time-weaver aspectj-weaving="autodetect" />

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值