Spring依赖注入源码解析(上)


前言

在看@Autowired注解源码之前,需要先了解一下现有的注入方式,才能更好的理解spring底层实现

本篇文章主要以**@Autowired寻找注入点、注入点进行注入**重点解析


一、Spring中到底有几种依赖注入的方式?

分两种:

手动注入
自动注入

1、手动注入

在XML中定义Bean时,就是手动注入,因为是程序员手动给某个属性指定了值。

1.1、set方法进行注入

<bean name="userService" class="com.luban.service.UserService">
 <property name="orderService" ref="orderService"/>
</bean>

1.2、通过构造方法进行注入

<bean name="userService" class="com.luban.service.UserService">
 <constructor-arg index="0" ref="orderService"/>
</bean>

所以手动注入的底层也就是分为两种:

set方法注入
构造方法注入

2、自动注入

自动注入又分为两种

XML的autowire自动注入
@Autowired注解的自动注入

2.1、XML的autowire自动注入

在XML中,我们可以在定义一个Bean时去指定这个Bean的自动注入模式:

byType
byName
constructor
default
no

比如:

<bean id="userService" class="com.luban.service.UserService" autowire="byType"/>

这么写,表示Spring会自动的给userService中所有的属性自动赋值(不需要这个属性上有@Autowired注解,但需要这个属性有对应的set方法)。


2.2、@Autowired注解的自动注入

@Autowired注解,是byType和byName的结合。

@Autowired注解可以写在

属性上:先根据属性类型去找Bean,如果找到多个再根据属性名确定一个
构造方法上:先根据方法参数类型去找Bean,如果找到多个再根据参数名确定一个
set方法上:先根据方法参数类型去找Bean,如果找到多个再根据参数名确定一个

而这种底层用到了:

属性注入
set方法注入
构造方法注入

二、autowireByName && autowireByType核心源码分析

autowireByName 和autowireByType底层逻辑基本差不多,区别就是一个根据类型,一个根据名称

if (resolvedAutowireMode == AUTOWIRE_BY_NAME || resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
			MutablePropertyValues newPvs = new MutablePropertyValues(pvs);
			// Add property values based on autowire by name if applicable.
			if (resolvedAutowireMode == AUTOWIRE_BY_NAME) {
				autowireByName(beanName, mbd, bw, newPvs);
			}
			// Add property values based on autowire by type if applicable.
			if (resolvedAutowireMode == AUTOWIRE_BY_TYPE) {
				autowireByType(beanName, mbd, bw, newPvs);
			}
			pvs = newPvs;
		}

2.1、autowireByName

干了啥?
遍历beanName属性名,根据属性名getBean,添加到pvs集合中

protected void autowireByName(
			String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {
		// 当前Bean中能进行自动注入的属性名
		String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
		// 遍历每个属性名,获取getBean并且添加到pvs中
		for (String propertyName : propertyNames) {
			if (containsBean(propertyName)) {
				Object bean = getBean(propertyName);
				pvs.add(propertyName, bean);
				// 记录一下当前遍历的Bean被哪个bean依赖了(可以理解为propertyName=orderService,beanName=userService)
				registerDependentBean(propertyName, beanName);
				if (logger.isTraceEnabled()) {
					logger.trace("Added autowiring by name from bean name '" + beanName +
							"' via property '" + propertyName + "' to bean named '" + propertyName + "'");
				}
			}
			else {
				if (logger.isTraceEnabled()) {
					logger.trace("Not autowiring property '" + propertyName + "' of bean '" + beanName +
							"' by name: no matching bean found");
				}
			}
		}
	}

2.2、获取可以依赖注入的beanName----unsatisfiedNonSimpleProperties()

主要做了几件事情
1、底层获取PropertyDescriptor对象数组
2、遍历,筛选到满足可以自动注入的benaName集合,返回

protected String[] unsatisfiedNonSimpleProperties(AbstractBeanDefinition mbd, BeanWrapper bw) {
		Set<String> result = new TreeSet<>();
		PropertyValues pvs = mbd.getPropertyValues();
		// 1、拿到当前类的所有set方法,setxxx,拿到的name就是xxx。
		PropertyDescriptor[] pds = bw.getPropertyDescriptors();
		// 2、遍历
		for (PropertyDescriptor pd : pds) {
			// 什么样的属性能进行自动注入?
			// 1.getWriteMethod---该类有对应的set方法
			// 2.isExcludedFromDependencyCheck --- 忽略自动装配的给定依赖类型:例如,String。默认为none。
			// 3.pvs.contains(pd.getName())---程序员自己没有给当前属性设置值
			// 4.!BeanUtils.isSimpleProperty(pd.getPropertyType())---属性类型不是简单类型,比如int、Integer、int[]、Date、Number、URL....
			if (pd.getWriteMethod() != null && !isExcludedFromDependencyCheck(pd) && !pvs.contains(pd.getName()) &&
					!BeanUtils.isSimpleProperty(pd.getPropertyType())) {
				// 把满足注入条件的加入到集合
				result.add(pd.getName());
			}
		}
		return StringUtils.toStringArray(result);
	}


三、@Autowired核心源码–AutowiredAnnotationBeanPostProcessor

for (BeanPostProcessor bp : getBeanPostProcessors()) {
				if (bp instanceof InstantiationAwareBeanPostProcessor) {
					InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
					PropertyValues pvsToUse = ibp.postProcessProperties(pvs, bw.getWrappedInstance(), beanName);
					if (pvsToUse == null) {
						if (filteredPds == null) {
							filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching);
						}
						pvsToUse = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName);
						if (pvsToUse == null) {
							return;
						}
					}
					pvs = pvsToUse;
				}
			}

AutowiredAnnotationBeanPostProcessor实现了上面的InstantiationAwareBeanPostProcessor接口

在属性填充代码逻辑中,上面这一段代码,会去调用AutowiredAnnotationBeanPostProcessor#postProcessProperties()做处理

这段代码主要是对bean里面包含了@Autowired注解的属性做处理


3.1、postProcessProperties–处理自动装配的属性

@Override
	public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) {
		// 找到自动装配元数据 
		InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
		try {
			// 注入
			metadata.inject(bean, beanName, pvs);
		}
		catch (BeanCreationException ex) {
			throw ex;
		}
		catch (Throwable ex) {
			throw new BeanCreationException(beanName, "Injection of autowired dependencies failed", ex);
		}
		return pvs;
	}

3.2、findAutowiringMetadata–找到自动装配元数据(寻找注入点)

这个方法会有多个地方调用

第一次调用的地方其实是在Bean后置处理器里面调用applyMergedBeanDefinitionPostProcessors,然后才是在属性填充时调用

这个方法主要是把类名称添加到缓存里

private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
		// 类名作为缓存key
		String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
		// 查看缓存里有没有类名key,第一次进来肯定是没有的
		InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
		// 为空则进if,双重锁检测
		if (InjectionMetadata.needsRefresh(metadata, clazz)) {
			synchronized (this.injectionMetadataCache) {
				metadata = this.injectionMetadataCache.get(cacheKey);
				if (InjectionMetadata.needsRefresh(metadata, clazz)) {
					if (metadata != null) {
						//清除缓存
						metadata.clear(pvs);
					}
					// 构建自动装配元数据,然后添加到缓存
					metadata = buildAutowiringMetadata(clazz);
					this.injectionMetadataCache.put(cacheKey, metadata);
				}
			}
		}
		return metadata;
	}

3.3、buildAutowiringMetadata–构建注入点(核心方法)

主要做了几件事情
1、看你这个类型是不是一个基础类型
2.、遍历当前类下的属性,寻找注入点,看是否有有@Autowired & @Value注解的属性
3、然后确定required状态,然后添加到需要注入的集合里
4、遍历当前类下的方法,寻找注入点,看是否有有@Autowired & @Value注解的set方法
5、同2一致
6、while循环,查找父类有没有注入点,直到找到所有的注入点结束

private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
		// 检查有没有前缀是不是以"java.开头,如String,Integer等...基础类型,基础类型不会被注入
		if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
			return InjectionMetadata.EMPTY;
		}

		List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
		Class<?> targetClass = clazz;

		do {
			final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
			// 处理当前类下的属性
			ReflectionUtils.doWithLocalFields(targetClass, field -> {
				// 有@Autowired & @Value注解的属性
				MergedAnnotation<?> ann = findAutowiredAnnotation(field);
				if (ann != null) {
					// 不是注入点,不会注入
					if (Modifier.isStatic(field.getModifiers())) {
						if (logger.isInfoEnabled()) {
							logger.info("Autowired annotation is not supported on static fields: " + field);
						}
						return;
					}
					// 判断注解是否包含required,一般情况这里都返回true
					boolean required = determineRequiredStatus(ann);
					// 添加到集合(里面包含需要依赖注入的属性)
					currElements.add(new AutowiredFieldElement(field, required));
				}
			});

			// 处理当前类下的method方法
			ReflectionUtils.doWithLocalMethods(targetClass, method -> {
				// 查找桥接方法,一般没有自己返回
				Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
				if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
					return;
				}
				//  查找是否有@Autowired & @Value注解的方法,
				MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
				if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
					// 不是注入点,不会注入
					if (Modifier.isStatic(method.getModifiers())) {
						if (logger.isInfoEnabled()) {
							logger.info("Autowired annotation is not supported on static methods: " + method);
						}
						return;
					}
					// 如果没有构造参数,则打印日志
					if (method.getParameterCount() == 0) {
						if (logger.isInfoEnabled()) {
							logger.info("Autowired annotation should only be used on methods with parameters: " +
									method);
						}
					}
					//  判断注解是否包含required,一般情况这里都返回true
					boolean required = determineRequiredStatus(ann);
					// 
					PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
					// 添加到需要依赖注入的集合里
					currElements.add(new AutowiredMethodElement(method, required, pd));
				}
			});
			// 添加所有属性和method的需要依赖的bean
			elements.addAll(0, currElements);
			// 获取父类
			targetClass = targetClass.getSuperclass();
		}
		// 循环直到没有父类为止,全部添加到一个集合里
		while (targetClass != null && targetClass != Object.class);

		return InjectionMetadata.forElements(elements, clazz);
	}

3.4、寻找注入点流程总结

在创建一个Bean的过程中,Spring会利用AutowiredAnnotationBeanPostProcessor的postProcessMergedBeanDefinition()找出注入点并缓存,找注入点的流程为

1、遍历当前类的所有的属性字段Field

2、查看字段上是否存在@Autowired、@Value、@Inject中的其中任意一个,存在则认为该字段是一个注入点

3、如果字段是static的,则不进行注入

4、获取@Autowired中的required属性的值

5、将字段信息构造成一个AutowiredFieldElement对象,作为一个注入点对象添加到currElements集合中。

6、遍历当前类的所有方法Method

7、判断当前Method是否是桥接方法,如果是找到原方法

8、查看方法上是否存在@Autowired、@Value、@Inject中的其中任意一个,存在则认为该方法是一个注入点

9、如果方法是static的,则不进行注入

10、获取@Autowired中的required属性的值

11、将方法信息构造成一个AutowiredMethodElement对象,作为一个注入点对象添加到currElements集合中。

12、遍历完当前类的字段和方法后,将遍历父类的,直到没有父类。

13、最后将currElements集合封装成一个InjectionMetadata对象,作为当前Bean对于的注入点集合对象,并缓存。


3.5、思考?static的字段或方法为什么不支持

@Component
@Scope("prototype")
public class OrderService {


}
@Component
@Scope("prototype")
public class UserService  {

 @Autowired
 private static OrderService orderService;

 public void test() {
  System.out.println("test123");
 }

}

看上面代码,UserService和OrderService都是原型Bean,假设Spring支持static字段进行自动注入,那么现在调用两次

UserService userService1 = (UserService) context.getBean("userService");
UserService userService2 = (UserService) context.getBean("userService");

问此时,userService1的orderService值是什么?还是它自己注入的值吗?

答案是:不是,一旦userService2 创建好了之后,static orderService字段的值就发生了修改了,从而出现bug。


3.6、上述源码提到的桥接方法是什么?

public interface UserInterface<T> {
 void setOrderService(T t);
}
@Component
public class UserService implements UserInterface<OrderService> {

 private OrderService orderService;

 @Override
 @Autowired
 public void setOrderService(OrderService orderService) {
  this.orderService = orderService;
 }

 public void test() {
  System.out.println("test123");
 }

}

UserService对应的字节码为:

// class version 52.0 (52)
// access flags 0x21
// signature Ljava/lang/Object;Lcom/ljc/service/UserInterface<Lcom/ljc/service/OrderService;>;
// declaration: com/ljc/service/UserService implements com.ljc.service.UserInterface<com.ljc.service.OrderService>
public class com/ljc/service/UserService implements com/ljc/service/UserInterface {

  // compiled from: UserService.java

  @Lorg/springframework/stereotype/Component;()

  // access flags 0x2
  private Lcom/ljc/service/OrderService; orderService

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 12 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this Lcom/ljc/service/UserService; L0 L1 0
    MAXSTACK = 1
    MAXLOCALS = 1

  // access flags 0x1
  public setOrderService(Lcom/ljc/service/OrderService;)V
  @Lorg/springframework/beans/factory/annotation/Autowired;()
   L0
    LINENUMBER 19 L0
    ALOAD 0
    ALOAD 1
    PUTFIELD com/ljc/service/UserService.orderService : Lcom/ljc/service/OrderService;
   L1
    LINENUMBER 20 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/ljc/service/UserService; L0 L2 0
    LOCALVARIABLE orderService Lcom/ljc/service/OrderService; L0 L2 1
    MAXSTACK = 2
    MAXLOCALS = 2

  // access flags 0x1
  public test()V
   L0
    LINENUMBER 23 L0
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    LDC "test123"
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L1
    LINENUMBER 24 L1
    RETURN
   L2
    LOCALVARIABLE this Lcom/ljc/service/UserService; L0 L2 0
    MAXSTACK = 2
    MAXLOCALS = 1

  // access flags 0x1041
  public synthetic bridge setOrderService(Ljava/lang/Object;)V
  @Lorg/springframework/beans/factory/annotation/Autowired;()
   L0
    LINENUMBER 11 L0
    ALOAD 0
    ALOAD 1
    CHECKCAST com/ljc/service/OrderService
    INVOKEVIRTUAL com/ljc/service/UserService.setOrderService (Lcom/ljc/service/OrderService;)V
    RETURN
   L1
    LOCALVARIABLE this Lcom/ljc/service/UserService; L0 L1 0
    MAXSTACK = 2
    MAXLOCALS = 2
}

可以看到在UserSerivce的字节码中有两个setOrderService方法:

public setOrderService(Lcom/ljc/service/OrderService;)V
public synthetic bridge setOrderService(Ljava/lang/Object;)V

并且都是存在@Autowired注解的。

所以在Spring中需要处理这种情况**,当遍历到桥接方法时,得找到原方法**。


四、inject-注入(需要重点关注)

在上一步构建好注入点后,寻找开始依赖注入!

Spring在AutowiredAnnotationBeanPostProcessor的**postProcessProperties()**方法中,会遍历所找到的注入点依次进行注入。

public void inject(Object target, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
		Collection<InjectedElement> checkedElements = this.checkedElements;
		Collection<InjectedElement> elementsToIterate =
				(checkedElements != null ? checkedElements : this.injectedElements);
		if (!elementsToIterate.isEmpty()) {
			// 遍历每个注入点,开始依赖注入
			for (InjectedElement element : elementsToIterate) {
				element.inject(target, beanName, pvs);
			}
		}
	}

需要注意上面的

element.inject(target, beanName, pvs);

并不是直接看父类的实现,在@Autowired实现这里 只需要看 field和method的2个实现即可,@Resource的实现是用的父类实现

在这里插入图片描述


4.1、AutowiredFieldElement#inject–字段的注入

主要做了几件事情
1、遍历所有的AutowiredFieldElement对象。

2、将对应的字段封装为DependencyDescriptor对象。

3、调用BeanFactory的resolveDependency()方法,传入DependencyDescriptor对象,进行依赖查找,找到当前字段所匹配的Bean对象。

4、将DependencyDescriptor对象和所找到的结果对象beanName封装成一个**ShortcutDependencyDescriptor对象作为缓存,**比如如果当前Bean是原型Bean,那么下次再来创建该Bean时,就可以直接拿缓存的结果对象beanName去BeanFactory中去那bean对象了,不用再次进行查找了

5、将结果对象赋值给字段。

@Override
   protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
      Field field = (Field) this.member;
      Object value;
      // 默认是false,第一次进来肯定不会进if
      if (this.cached) {
         value = resolvedCachedArgument(beanName, this.cachedFieldValue);
      }
      else {
      	 // 将对应的字段封装为DependencyDescriptor对象(创建依赖描述符对象)
         DependencyDescriptor desc = new DependencyDescriptor(field, this.required);
         // 设置calss
         desc.setContainingClass(bean.getClass());
         Set<String> autowiredBeanNames = new LinkedHashSet<>(1);
         Assert.state(beanFactory != null, "No BeanFactory available");
         // 获取类型转换器
         TypeConverter typeConverter = beanFactory.getTypeConverter();
         try {
         	// 重点关注的核心方法,进行依赖查找,找到当前字段所匹配的Bean对象
            value = beanFactory.resolveDependency(desc, beanName, autowiredBeanNames, typeConverter);
         }
         catch (BeansException ex) {
            throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(field), ex);
         }
         synchronized (this) {
            if (!this.cached) {
               Object cachedFieldValue = null;
               if (value != null || this.required) {
                  cachedFieldValue = desc;
                  registerDependentBeans(beanName, autowiredBeanNames);
                  if (autowiredBeanNames.size() == 1) {
                     String autowiredBeanName = autowiredBeanNames.iterator().next();
                     if (beanFactory.containsBean(autowiredBeanName) &&
                           beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
                        // 创建缓存对象,方便缓存
                        cachedFieldValue = new ShortcutDependencyDescriptor(
                              desc, autowiredBeanName, field.getType());
                     }
                  }
               }
               // 缓存
               this.cachedFieldValue = cachedFieldValue;
               this.cached = true;
            }
         }
      }
      if (value != null) {
         ReflectionUtils.makeAccessible(field);
         // 将结果对象赋值给字段
         field.set(bean, value);
      }
   }
}

4.2、AutowiredMethodElement#inject–方法的注入

和字段注入实现逻辑类似…

主要做了几件事情
1、遍历所有的AutowiredMethodElement对象

2、遍历将对应的方法的参数,将每个参数封装成MethodParameter对象

3、将MethodParameter对象封装为DependencyDescriptor对象

4、调用BeanFactory的resolveDependency()方法,传入DependencyDescriptor对象,进行依赖查找,找到当前方法参数所匹配的Bean对象。

5、将DependencyDescriptor对象和所找到的结果对象beanName封装成一个ShortcutDependencyDescriptor对象作为缓存,比如如果当前Bean是原型Bean,那么下次再来创建该Bean时,就可以直接拿缓存的结果对象beanName去BeanFactory中去那bean对象了,不用再次进行查找了

6、利用反射将找到的所有结果对象传给当前方法,并执行。

		@Override
		protected void inject(Object bean, @Nullable String beanName, @Nullable PropertyValues pvs) throws Throwable {
			if (checkPropertySkipping(pvs)) {
				return;
			}
			Method method = (Method) this.member;
			Object[] arguments;
			if (this.cached) {
				// Shortcut for avoiding synchronization...
				arguments = resolveCachedArguments(beanName);
			}
			else {
				int argumentCount = method.getParameterCount();
				arguments = new Object[argumentCount];
				DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount];
				Set<String> autowiredBeans = new LinkedHashSet<>(argumentCount);
				Assert.state(beanFactory != null, "No BeanFactory available");
				TypeConverter typeConverter = beanFactory.getTypeConverter();
				for (int i = 0; i < arguments.length; i++) {
					// 每个参数封装成MethodParameter对象
					MethodParameter methodParam = new MethodParameter(method, i);
					// 将MethodParameter对象封装为DependencyDescriptor对象
					DependencyDescriptor currDesc = new DependencyDescriptor(methodParam, this.required);
					currDesc.setContainingClass(bean.getClass());
					descriptors[i] = currDesc;
					try {
						// 进行依赖查找,找到当前方法参数所匹配的Bean对象
						Object arg = beanFactory.resolveDependency(currDesc, beanName, autowiredBeans, typeConverter);
						if (arg == null && !this.required) {
							arguments = null;
							break;
						}
						arguments[i] = arg;
					}
					catch (BeansException ex) {
						throw new UnsatisfiedDependencyException(null, beanName, new InjectionPoint(methodParam), ex);
					}
				}
				synchronized (this) {
					if (!this.cached) {
						if (arguments != null) {
							DependencyDescriptor[] cachedMethodArguments = Arrays.copyOf(descriptors, arguments.length);
							registerDependentBeans(beanName, autowiredBeans);
							if (autowiredBeans.size() == argumentCount) {
								Iterator<String> it = autowiredBeans.iterator();
								Class<?>[] paramTypes = method.getParameterTypes();
								for (int i = 0; i < paramTypes.length; i++) {
									String autowiredBeanName = it.next();
									if (beanFactory.containsBean(autowiredBeanName) &&
											beanFactory.isTypeMatch(autowiredBeanName, paramTypes[i])) {
										// 封装缓存对象
										cachedMethodArguments[i] = new ShortcutDependencyDescriptor(
												descriptors[i], autowiredBeanName, paramTypes[i]);
									}
								}
							}
							this.cachedMethodArguments = cachedMethodArguments;
						}
						else {
							this.cachedMethodArguments = null;
						}
						this.cached = true;
					}
				}
			}
			if (arguments != null) {
				try {
					ReflectionUtils.makeAccessible(method);
					// 注入
					method.invoke(bean, arguments);
				}
				catch (InvocationTargetException ex) {
					throw ex.getTargetException();
				}
			}
		}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

未闻花名丶丶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值