spring的aware接口

org.springframework.beans.factory.Aware

Aware是一个具有标识作用的超级接口,具体实现是有子接口去决定的,但是子接口至少要有一个带一个参数的且返回时空的方法。实现该接口的bean是具有被spring 容器通知的能力的,而被通知的方式就是通过回调。也就是说:直接或间接实现了这个接口的类,都具有被spring容器通知的能力。

源码如下:

package org.springframework.beans.factory;

/**
 * Marker superinterface indicating that a bean is eligible to be
 * notified by the Spring container of a particular framework object
 * through a callback-style method. Actual method signature is
 * determined by individual subinterfaces, but should typically
 * consist of just one void-returning method that accepts a single
 * argument.
 *
 * <p>Note that merely implementing {@link Aware} provides no default
 * functionality. Rather, processing must be done explicitly, for example
 * in a {@link org.springframework.beans.factory.config.BeanPostProcessor BeanPostProcessor}.
 * Refer to {@link org.springframework.context.support.ApplicationContextAwareProcessor}
 * and {@link org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory}
 * for examples of processing {@code *Aware} interface callbacks.
 *
 * @author Chris Beams
 * @since 3.1
 */
public interface Aware {

}

 

常用的子接口如下:

org.springframework.context.ApplicationContextAware
org.springframework.beans.factory.BeanFactoryAware
org.springframework.beans.factory.BeanClassLoaderAware
org.springframework.beans.factory.BeanNameAware
org.springframework.context.EnvironmentAware
org.springframework.context.ResourceLoaderAware
org.springframework.context.annotation.ImportAware

ApplicationContextAware 

实现该接口的类可以获取spring容器上线文信息 ApplicationContext ,主要是通过这个ApplicationContext  applicationContext 去做一个bean初始化前的操作 源码如下:

public interface ApplicationContextAware extends Aware {

	/**
	 * Set the ApplicationContext that this object runs in.
	 * Normally this call will be used to initialize the object.
	 * <p>Invoked after population of normal bean properties but before an init callback such
	 * as {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()}
	 * or a custom init-method. Invoked after {@link ResourceLoaderAware#setResourceLoader},
	 * {@link ApplicationEventPublisherAware#setApplicationEventPublisher} and
	 * {@link MessageSourceAware}, if applicable.
	 * @param applicationContext the ApplicationContext object to be used by this object
	 * @throws ApplicationContextException in case of context initialization errors
	 * @throws BeansException if thrown by application context methods
	 * @see org.springframework.beans.factory.BeanInitializationException
	 */
	void setApplicationContext(ApplicationContext applicationContext) throws BeansException;

}

调用顺序:

  1. 该接口的实现类setApplicationContext方法,会在创建bean及对其属性赋值以后,但是在调用beaninit方法和InitializingBean#afterPropertiesSet()方法之前;
  2. 该接口的实现类setApplicationContext方法,会在调用ResourceLoaderAware#setResourceLoader ApplicationEventPublisherAware#setApplicationEventPublisher之后调用。

Demo 

@Service
public class ApplicationContextUtil  implements ApplicationContextAware,ResourceLoaderAware{

	private static ApplicationContext applicationContext = null;
	
	private ResourceLoader resourceLoader;
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
		System.out.println("3333333333333");
		this.applicationContext = applicationContext;
		
	}

	public static <T> T getBean(String name , Class<T> T){
		
		return 	applicationContext.getBean(name, T);
	}
	
	
	public static Map<String, Object> getBeansWithAnnotation(String name ){
		
		//获取所有注解为Service的spring容器中的bean
		return 	applicationContext.getBeansWithAnnotation(Service.class);
	}
	
	public static boolean containBean(String name){
		
		return applicationContext.containsBean(name);
		
		
	}


	public static ApplicationContext getApplicationContext() {
		return applicationContext;
	}

	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
		System.out.println("444444444444");
		
	}
	
}

定义简单java bean

@Service
public class AwarePojoBean implements InitializingBean{
	public AwarePojoBean() {
		System.out.println("1111111");
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		System.out.println("222222222");
		
	}
}

配置类:

@Configuration
@ComponentScan("com.zhangfd.boot.spring.aware")
public class AwareConfig {

}

测试类:

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = {AwareConfig.class})
public class AwareTest {
	
	@Test
	public void contextLoads() {
		AwarePojoBean awarePojoBean = ApplicationContextUtil.getBean("awarePojoBean", AwarePojoBean.class);
	    System.out.println("==============="+awarePojoBean.getClass());
	}
}

运行结果如下:

我们可以看到,ApplicationContextUtil类的setApplicationContext  方法调用顺序在bean的构造方法和InitializingBean#afterPropertiesSet()方法之前;在ResourceLoaderAware的setResourceLoader 方法之后。

BeanFactoryAware

 

我们通过上面的简图可以看出,这个Spring Ioc 中最顶层的父接口就是BeanFactory。实现这个BeanFactoryAware接口的子类可以获取spring容器的BeanFactory 对象,进而可以动态的去操作 要在spring容器中注入的bean。

通过上图,我们也可以看到ApplicationContext接口是BeanFactory的子接口,所以继承ApplicationContextAware的实现类拿到ApplicationContext 对象比实现BeanFactoryAware接口拿到BeanFactory 对象 可以获取更多的信息。

举个例子,比如我们写了一个接口,对应10个实现类,当然我们可以通过if else 去找对应的实现类,但是如果我们对这10个实现类的beanName定义的规则些,那么通过BeanFactory一行代码就可以拿到指定的对象。
 

调用顺序:

1、该接口的实现setBeanFactory方法,会在创建bean及对其属性赋值以后,但是在调用beaninit方法和InitializingBean#afterPropertiesSet()方法之后;

2、该接口的实现类setApplicationContext方法,在调用ResourceLoaderAware#setResourceLoader ApplicationEventPublisherAware#setApplicationEventPublisher 之前调用。

 

注意: ApplicationContextAware的实现类和BeanFactoryAware 在以上两点的调用顺序上刚好相反的!!!

@Service
public class BeanFactoryUtils implements BeanFactoryAware,ResourceLoaderAware {

	private static BeanFactory beanFactory;

	private ResourceLoader resourceLoader;
	
	@Override
	public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
		System.out.println("BeanFactoryUtils3333333333333");
		this.beanFactory = beanFactory;

	}

	public static <T> T getBean(String name, Class<T> t) {

		return beanFactory.getBean(name, t);
	}

	public static boolean containBean(String name) {
		return beanFactory.containsBean(name);
	}

	public static BeanFactory getBeanFactory() {
		return beanFactory;
	}
	
	@Override
	public void setResourceLoader(ResourceLoader resourceLoader) {
		this.resourceLoader = resourceLoader;
		System.out.println("BeanFactoryUtils444444444444");
		
	}

}

定义简单javaBean

@Service
public class AwarePojoBean implements InitializingBean{
	public AwarePojoBean() {
		System.out.println("1111111");
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		System.out.println("222222222");
		
	}
}

定义配置类:

@Service
public class AwarePojoBean implements InitializingBean{
	public AwarePojoBean() {
		System.out.println("1111111");
	}

	@Override
	public void afterPropertiesSet() throws Exception {
		System.out.println("222222222");
		
	}
}

测试类:

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = {AwareConfig.class})
public class BeanFactoryTest {
	
	@Test
	public void contextLoads() {
		AwarePojoBean awarePojoBean = BeanFactoryUtils.getBean("awarePojoBean", AwarePojoBean.class);
	    System.out.println("==============="+awarePojoBean.getClass());
	}
}

运行测试类,结果如下:

通过运行结果,我们也可以看出调用顺序的确和ApplicationContextAware的实现类相反。

 

 

总结:

一个类继承ApplicationContextAware可获得spring容器中ApplicationContextAware;进而进行各种操作,和BeanFactoryAware相比,它可获取的信息更多,更全!

1.BeanFactroy采用的是延迟加载形式来注入Bean的,即只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化,这样,我们就不能发现一些存在的Spring的配置问题。而ApplicationContext则相反,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误。 相对于基本的BeanFactory,ApplicationContext 唯一的不足是占用内存空间。当应用程序配置Bean较多时,程序启动较慢。BeanFacotry延迟加载,如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常;而ApplicationContext则在初始化自身是检验,这样有利于检查所依赖属性是否注入;所以通常情况下我们选择使用 ApplicationContext。
应用上下文则会在上下文启动后预载入所有的单实例Bean。通过预载入单实例bean ,确保当你需要的时候,你就不用等待,因为它们已经创建好了。

2.BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。(Applicationcontext比 beanFactory 加入了一些更好使用的功能。而且 beanFactory 的许多功能需要通过编程实现而 Applicationcontext 可以通过配置实现。比如后处理 bean , Applicationcontext 直接配置在配置文件即可而 beanFactory 这要在代码中显示的写出来才可以被容器识别。 )

3.beanFactory主要是面对与 spring 框架的基础设施,面对 spring 自己。而 Applicationcontex 主要面对与 spring 使用的开发者。基本都会使用 Applicationcontex 并非 beanFactory 。

BeanNameAware

 这个接口的含义就是让实现类知道自己在spring容器中定义的beanName是啥,实际开发一般没啥用。一般在抽象类里使用虑䞢接口的比较多。

BeanClassLoaderAware

获取spring 容器的类加载器ClassLoader 对象,像我们在加载项目的静态文件时,一般使用的是Thread.currentThread().getContextClassLoader() 从而获取ClassLoader 对象,更快更方便些。

EnvironmentAware

实现这个接口的类能获取Environmet对象,进而可以各种系统变量信息,也可以设置 变量的优先级别等等。后面对Environment接口做一个专题讲解。

      通过Environment 对象可以获取spring boot框架中中的application.properties中定义的各种变量的值(个别变量除外!!!!)。

   先定义个实现类:

@Service
public class EnvironmentAwareUtils implements  EnvironmentAware{
	
	private static Environment environment;
	
	@Override
	public void setEnvironment(Environment environment) {
		
		this.environment = environment;
		
	}
	
	public static Environment getEnvironment(){
		return environment;
		
	}
	

}

定义配置类:

@Configuration
@ComponentScan("com.zhangfd.boot.spring.aware")
public class AwareConfig {

}

 

application.properties 文件如下:

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/datatest
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

#设置服务器的端口
server.port=8080
spring.profiles.active=dev
#false设置不进行热部署
spring.devtools.restart.enabled = false
#表示这个文件的内容有变动就会热部署,前提是上面的enabled=true
spring.devtools.restart.trigger-file=myRestartTrigger.my
spring.devtools.remote.secret=true

#spring.main.banner-mode=off

application-dev.properties

spring.datasource.test=just for test

 

定义测试类

@RunWith(SpringRunner.class)
@SpringBootTest
@ContextConfiguration(classes = {AwareConfig.class})
public class EnvironmentAwareTest {
	@Value("${spring.datasource.test}")
	private String testValue;
	
	@Test
	public void contextLoads() {
		//通过注解获取和通过Environment获取同样的变量
	    System.out.println(testValue+"==============="+EnvironmentAwareUtils.getEnvironment().getProperty("spring.datasource.username"));

	    System.out.println("==============="+EnvironmentAwareUtils.getEnvironment().getProperty("JAVA_HOME"));
	    ConfigurableEnvironment configurableEnvironment = (ConfigurableEnvironment)EnvironmentAwareUtils.getEnvironment();
	  //  System.out.println(configurableEnvironment.getSystemEnvironment());;
	
	    //模糊加载一类变量
	    RelaxedPropertyResolver  resolver = new RelaxedPropertyResolver(EnvironmentAwareUtils.getEnvironment(),"spring.datasource.");
	    System.out.println(resolver.getProperty("username")+"*******************");
	    System.out.println(resolver.getProperty("driver-class-name")+"*******************");
	}
}

运行测试类 结果如下:

执行结果是 -1 而不是预期的8080.

 

 ResourceLoaderAware

获取资源加载器ResourceLoader 对象,可以获得外部资源文件。

package org.springframework.core.io;

import org.springframework.lang.Nullable;
import org.springframework.util.ResourceUtils;

/**
 * Strategy interface for loading resources (e.. class path or file system
 * resources). An {@link org.springframework.context.ApplicationContext}
 * is required to provide this functionality, plus extended
 * {@link org.springframework.core.io.support.ResourcePatternResolver} support.
 *
 * <p>{@link DefaultResourceLoader} is a standalone implementation that is
 * usable outside an ApplicationContext, also used by {@link ResourceEditor}.
 *
 * <p>Bean properties of type Resource and Resource array can be populated
 * from Strings when running in an ApplicationContext, using the particular
 * context's resource loading strategy.
 *
 * @author Juergen Hoeller
 * @since 10.03.2004
 * @see Resource
 * @see org.springframework.core.io.support.ResourcePatternResolver
 * @see org.springframework.context.ApplicationContext
 * @see org.springframework.context.ResourceLoaderAware
 */
public interface ResourceLoader {

	/** Pseudo URL prefix for loading from the class path: "classpath:" */
	String CLASSPATH_URL_PREFIX = ResourceUtils.CLASSPATH_URL_PREFIX;


	/**
	 * Return a Resource handle for the specified resource location.
	 * <p>The handle should always be a reusable resource descriptor,
	 * allowing for multiple {@link Resource#getInputStream()} calls.
	 * <p><ul>
	 * <li>Must support fully qualified URLs, e.g. "file:C:/test.dat".
	 * <li>Must support classpath pseudo-URLs, e.g. "classpath:test.dat".
	 * <li>Should support relative file paths, e.g. "WEB-INF/test.dat".
	 * (This will be implementation-specific, typically provided by an
	 * ApplicationContext implementation.)
	 * </ul>
	 * <p>Note that a Resource handle does not imply an existing resource;
	 * you need to invoke {@link Resource#exists} to check for existence.
	 * @param location the resource location
	 * @return a corresponding Resource handle (never {@code null})
	 * @see #CLASSPATH_URL_PREFIX
	 * @see Resource#exists()
	 * @see Resource#getInputStream()
	 */
	Resource getResource(String location);

	/**
	 * Expose the ClassLoader used by this ResourceLoader.
	 * <p>Clients which need to access the ClassLoader directly can do so
	 * in a uniform manner with the ResourceLoader, rather than relying
	 * on the thread context ClassLoader.
	 * @return the ClassLoader
	 * (only {@code null} if even the system ClassLoader isn't accessible)
	 * @see org.springframework.util.ClassUtils#getDefaultClassLoader()
	 * @see org.springframework.util.ClassUtils#forName(String, ClassLoader)
	 */
	@Nullable
	ClassLoader getClassLoader();

}

说白了就是加载文件,那我们根据File 、 Input Stream、OutPutStream 然后根据文件路径是不是也能加载文件呢?答案是肯定的,spring内部也是用他们完成的,我们看到这里的 Resource getResource(String location); 看下 Resource的源码:

package org.springframework.core.io;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;

import org.springframework.lang.Nullable;

/**
 * Interface for a resource descriptor that abstracts from the actual
 * type of underlying resource, such as a file or class path resource.
 *
 * <p>An InputStream can be opened for every resource if it exists in
 * physical form, but a URL or File handle can just be returned for
 * certain resources. The actual behavior is implementation-specific.
 *
 * @author Juergen Hoeller
 * @since 28.12.2003
 * @see #getInputStream()
 * @see #getURL()
 * @see #getURI()
 * @see #getFile()
 * @see WritableResource
 * @see ContextResource
 * @see UrlResource
 * @see ClassPathResource
 * @see FileSystemResource
 * @see PathResource
 * @see ByteArrayResource
 * @see InputStreamResource
 */
public interface Resource extends InputStreamSource {

	/**
	 * Determine whether this resource actually exists in physical form.
	 * <p>This method performs a definitive existence check, whereas the
	 * existence of a {@code Resource} handle only guarantees a valid
	 * descriptor handle.
	 */
	boolean exists();

	/**
	 * Indicate whether the contents of this resource can be read via
	 * {@link #getInputStream()}.
	 * <p>Will be {@code true} for typical resource descriptors;
	 * note that actual content reading may still fail when attempted.
	 * However, a value of {@code false} is a definitive indication
	 * that the resource content cannot be read.
	 * @see #getInputStream()
	 */
	default boolean isReadable() {
		return true;
	}

	/**
	 * Indicate whether this resource represents a handle with an open stream.
	 * If {@code true}, the InputStream cannot be read multiple times,
	 * and must be read and closed to avoid resource leaks.
	 * <p>Will be {@code false} for typical resource descriptors.
	 */
	default boolean isOpen() {
		return false;
	}

	/**
	 * Determine whether this resource represents a file in a file system.
	 * A value of {@code true} strongly suggests (but does not guarantee)
	 * that a {@link #getFile()} call will succeed.
	 * <p>This is conservatively {@code false} by default.
	 * @since 5.0
	 * @see #getFile()
	 */
	default boolean isFile() {
		return false;
	}

	/**
	 * Return a URL handle for this resource.
	 * @throws IOException if the resource cannot be resolved as URL,
	 * i.e. if the resource is not available as descriptor
	 */
	URL getURL() throws IOException;

	/**
	 * Return a URI handle for this resource.
	 * @throws IOException if the resource cannot be resolved as URI,
	 * i.e. if the resource is not available as descriptor
	 * @since 2.5
	 */
	URI getURI() throws IOException;

	/**
	 * Return a File handle for this resource.
	 * @throws java.io.FileNotFoundException if the resource cannot be resolved as
	 * absolute file path, i.e. if the resource is not available in a file system
	 * @throws IOException in case of general resolution/reading failures
	 * @see #getInputStream()
	 */
	File getFile() throws IOException;

	/**
	 * Return a {@link ReadableByteChannel}.
	 * <p>It is expected that each call creates a <i>fresh</i> channel.
	 * <p>The default implementation returns {@link Channels#newChannel(InputStream)}
	 * with the result of {@link #getInputStream()}.
	 * @return the byte channel for the underlying resource (must not be {@code null})
	 * @throws java.io.FileNotFoundException if the underlying resource doesn't exist
	 * @throws IOException if the content channel could not be opened
	 * @since 5.0
	 * @see #getInputStream()
	 */
	default ReadableByteChannel readableChannel() throws IOException {
		return Channels.newChannel(getInputStream());
	}

	/**
	 * Determine the content length for this resource.
	 * @throws IOException if the resource cannot be resolved
	 * (in the file system or as some other known physical resource type)
	 */
	long contentLength() throws IOException;

	/**
	 * Determine the last-modified timestamp for this resource.
	 * @throws IOException if the resource cannot be resolved
	 * (in the file system or as some other known physical resource type)
	 */
	long lastModified() throws IOException;

	/**
	 * Create a resource relative to this resource.
	 * @param relativePath the relative path (relative to this resource)
	 * @return the resource handle for the relative resource
	 * @throws IOException if the relative resource cannot be determined
	 */
	Resource createRelative(String relativePath) throws IOException;

	/**
	 * Determine a filename for this resource, i.e. typically the last
	 * part of the path: for example, "myfile.txt".
	 * <p>Returns {@code null} if this type of resource does not
	 * have a filename.
	 */
	@Nullable
	String getFilename();

	/**
	 * Return a description for this resource,
	 * to be used for error output when working with the resource.
	 * <p>Implementations are also encouraged to return this value
	 * from their {@code toString} method.
	 * @see Object#toString()
	 */
	String getDescription();

}

 

它就是和java的流联系起来的。Spring对此做了封装。

 

 

特别需要说明的是ApplicationContext 是继承了ResourceLoader的子接口 ResourcePatternResolver的,所以通过上面ApplicationContextAware获取的ApplicationContext   对象统统具有 ResourceLoader 的功能。

想想下,我们通过xml配置文件把对象加载到spring容器中,首先就是加载指定路径的文件, 通过ClassPathXmlApplicationcontext 还是

AnnotationConfig Applicationcontext 也都有加载文件的功能就更不奇怪了。

 

 

ImportAware

package org.springframework.context.annotation;

import org.springframework.beans.factory.Aware;
import org.springframework.core.type.AnnotationMetadata;

/**
 * Interface to be implemented by any @{@link Configuration} class that wishes
 * to be injected with the {@link AnnotationMetadata} of the @{@code Configuration}
 * class that imported it. Useful in conjunction with annotations that
 * use @{@link Import} as a meta-annotation.
 *
 * @author Chris Beams
 * @since 3.1
 */
public interface ImportAware extends Aware {

	/**
	 * Set the annotation metadata of the importing @{@code Configuration} class.
	 */
	void setImportMetadata(AnnotationMetadata importMetadata);

}

通过源码我们可以看出这个主要是获取 AnnotationMetadata 对象,进而获取指定注解的一些信息。通过下面的接口ImportBeanDefinitionRegistrar也能获取AnnotationMetadata 对象 ,并且还能获取BeanDefinitionRegistry registry 两者相比后者可以获取更多的信息,像Apollo框架就使用了 ImportBeanDefinitionRegistrar 接口

package org.springframework.context.annotation;

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.core.type.AnnotationMetadata;

/**
 * Interface to be implemented by types that register additional bean definitions when
 * processing @{@link Configuration} classes. Useful when operating at the bean definition
 * level (as opposed to {@code @Bean} method/instance level) is desired or necessary.
 *
 * <p>Along with {@code @Configuration} and {@link ImportSelector}, classes of this type
 * may be provided to the @{@link Import} annotation (or may also be returned from an
 * {@code ImportSelector}).
 *
 * <p>An {@link ImportBeanDefinitionRegistrar} may implement any of the following
 * {@link org.springframework.beans.factory.Aware Aware} interfaces, and their respective
 * methods will be called prior to {@link #registerBeanDefinitions}:
 * <ul>
 * <li>{@link org.springframework.context.EnvironmentAware EnvironmentAware}</li>
 * <li>{@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware}
 * <li>{@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware}
 * <li>{@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}
 * </ul>
 *
 * <p>See implementations and associated unit tests for usage examples.
 *
 * @author Chris Beams
 * @since 3.1
 * @see Import
 * @see ImportSelector
 * @see Configuration
 */
public interface ImportBeanDefinitionRegistrar {

	/**
	 * Register bean definitions as necessary based on the given annotation metadata of
	 * the importing {@code @Configuration} class.
	 * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
	 * registered here, due to lifecycle constraints related to {@code @Configuration}
	 * class processing.
	 * @param importingClassMetadata annotation metadata of the importing class
	 * @param registry current bean definition registry
	 */
	public void registerBeanDefinitions(
			AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);

}

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值