为了账号安全,请及时绑定邮箱和手机立即绑定

Spring核心原理之 IoC容器中那些鲜为人知的细节(3)

标签:
Java Spring 源码

本文节选自《Spring 5核心原理》

Spring IoC容器还有一些高级特性,如使用lazy-init属性对Bean预初始化、使用FactoryBean产生或者修饰Bean对象的生成、IoC容器在初始化Bean过程中使用BeanPostProcessor后置处理器对Bean声明周期事件进行管理等。

1 关于延时加载

我们已经知道,IoC容器的初始化过程就是对Bean定义资源的定位、载入和注册,此时容器对Bean的依赖注入并没有发生,依赖注入是在应用程序第一次向容器索取Bean时通过getBean()方法完成的。
当Bean定义资源的< bean>元素中配置了lazy-init=false属性时,容器将会在初始化时对所配置的Bean进行预实例化,Bean的依赖注入在容器初始化时就已经完成。这样,当应用程序第一次向容器索取被管理的Bean时,就不用再初始化和对Bean进行依赖注入了,而是直接从容器中获取已经完成依赖注入的Bean,提高了应用程序第一次向容器获取Bean的性能。

1.1. refresh()方法

IoC容器读入已经定位的Bean定义资源是从refresh()方法开始的,我们从AbstractApplicationContext类的refresh()方法入手分析,回顾一下源码:

@Override
public void refresh() throws BeansException, IllegalStateException {
	  ...
      //子类的refreshBeanFactory()方法启动
      ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
	  ...
}

在refresh()方法中ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();启动了Bean定义资源的载入、注册过程。finishBeanFactoryInitialization()方法是对注册后的Bean定义中的预实例化(lazy-init=false,Spring默认进行预实例化,即为true)的Bean进行处理的地方。

1.2. 使用finishBeanFactoryInitialization()处理预实例化的Bean

当Bean定义资源被载入IoC容器之后,容器将Bean定义资源解析为容器内部的数据结构BeanDefinition,并注册到容器中,AbstractApplicationContext类中的finishBeanFactoryInitialization()方法对配置了预实例化属性的Bean进行预初始化,源码如下:

//对配置了lazy-init属性的Bean进行预实例化处理
protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
   //这是Spring 3新加的代码,为容器指定一个转换服务(ConversionService)
   //在对某些Bean属性进行转换时使用
   if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) &&
         beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) {
      beanFactory.setConversionService(
            beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class));
   }

   if (!beanFactory.hasEmbeddedValueResolver()) {
      beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders (strVal));
   }

   String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false);
   for (String weaverAwareName : weaverAwareNames) {
      getBean(weaverAwareName);
   }

   //为了使类型匹配,停止使用临时的类加载器
   beanFactory.setTempClassLoader(null);

   //缓存容器中所有注册的BeanDefinition元数据,以防被修改
   beanFactory.freezeConfiguration();

   //对配置了lazy-init属性的单例模式的Bean进行预实例化处理
   beanFactory.preInstantiateSingletons();
}

其中ConfigurableListableBeanFactory是一个接口,preInstantiateSingletons()方法由其子类DefaultListableBeanFactory提供。

1.3. 对配置了lazy-init属性的单例模式的Bean的预实例化

对配置了lazy-init属性的单例模式的Bean的预实例化相关源码如下:

public void preInstantiateSingletons() throws BeansException {
   if (this.logger.isDebugEnabled()) {
      this.logger.debug("Pre-instantiating singletons in " + this);
   }

   List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

   for (String beanName : beanNames) {
      //获取指定名称的Bean定义
      RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
      //Bean不是抽象的,是单例模式的,且lazy-init属性配置为false
      if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
         //如果指定名称的Bean是创建容器的Bean
         if (isFactoryBean(beanName)) {
            //FACTORY_BEAN_PREFIX="&",当Bean名称前面加"&"符号
            //时,获取的是容器对象本身,而不是容器产生的Bean
            //调用getBean方法,触发Bean实例化和依赖注入
            final FactoryBean<?> factory = (FactoryBean<?>) getBean(FACTORY_BEAN_PREFIX + beanName);
            //标识是否需要预实例化
            boolean isEagerInit;
            if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) {
               //一个匿名内部类
               isEagerInit = AccessController.doPrivileged((PrivilegedAction<Boolean>) () ->
                     ((SmartFactoryBean<?>) factory).isEagerInit(),
                     getAccessControlContext());
            }
            else {
               isEagerInit = (factory instanceof SmartFactoryBean &&
                     ((SmartFactoryBean<?>) factory).isEagerInit());
            }
            if (isEagerInit) {
               //调用getBean()方法,触发Bean实例化和依赖注入
               getBean(beanName);
            }
         }
         else {
            getBean(beanName);
         }
      }
   }

通过对lazy-init处理源码的分析可以看出,如果设置了lazy-init属性,则容器在完成Bean定义的注册之后,会通过getBean()方法触发指定Bean的初始化和依赖注入。如前所述,这样当应用程序第一次向容器索取所需的Bean时,容器不再需要对Bean进行初始化和依赖注入,可直接从已经完成实例化和依赖注入的Bean中取一个现成的Bean,提高了第一次获取Bean的性能。

2 关于FactoryBean和BeanFactory

Spring中,有两个很容易混淆的类:BeanFactory和FactoryBean。
BeanFactory:Bean工厂,是一个工厂(Factory),Spring IoC容器的最高层接口就是BeanFactory,它的作用是管理Bean,即实例化、定位、配置应用程序中的对象及建立这些对象之间的依赖。
FactoryBean:工厂Bean,是一个Bean,作用是产生其他Bean实例。这种Bean没有什么特别的要求,仅需要提供一个工厂方法,该方法用来返回其他Bean实例。在通常情况下,Bean无须自己实现工厂模式,Spring容器担任工厂的角色;在少数情况下,容器中的Bean本身就是工厂,其作用是产生其他Bean实例。
当用户使用容器时,可以使用转义字符“&”来得到FactoryBean本身,以区别通过FactoryBean产生的实例对象和FactoryBean对象本身。在BeanFactory中通过如下代码定义了该转义字符:
String FACTORY_BEAN_PREFIX = “&”;

如果myJndiObject是一个FactoryBean,则使用&myJndiObject得到的是myJndiObject对象,而不是myJndiObject产生的对象。

2.1. FactoryBean源码

//工厂Bean,用于产生其他对象
public interface FactoryBean<T> {

   //获取容器管理的对象实例
   @Nullable
   T getObject() throws Exception;

   //获取Bean工厂创建的对象的类型
   @Nullable
   Class<?> getObjectType();

   //Bean工厂创建的对象是否是单例模式的,如果是,
   //则整个容器中只有一个实例对象,每次请求都返回同一个实例对象
   default boolean isSingleton() {
      return true;
   }

}

2.2. AbstractBeanFactory的getBean()方法

在分析Spring IoC容器实例化Bean并进行依赖注入的源码时,提到在getBean()方法触发容器实例化Bean时会调用AbstractBeanFactory的doGetBean()方法,其重要源码如下:


protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
      @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
      ...
      BeanFactory parentBeanFactory = getParentBeanFactory();
      //当前容器的父容器存在,且当前容器中不存在指定名称的Bean
      if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
         //解析指定Bean名称的原始名称
         String nameToLookup = originalBeanName(name);
         if (parentBeanFactory instanceof AbstractBeanFactory) {
            return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
                  nameToLookup, requiredType, args, typeCheckOnly);
         }
         else if (args != null) {
            //委派父容器根据指定名称和显式的参数查找
            return (T) parentBeanFactory.getBean(nameToLookup, args);
         }
         else {
            //委派父容器根据指定名称和类型查找
            return parentBeanFactory.getBean(nameToLookup, requiredType);
         }
      }
   ...
   return (T) bean;
}

//获取给定Bean的实例对象,主要完成FactoryBean的相关处理
protected Object getObjectForBeanInstance(
      Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) {

   //容器已经得到了Bean实例对象,这个实例对象可能是一个普通的Bean,
   //也可能是一个工厂Bean,如果是一个工厂Bean,则使用它创建一个Bean实例对象,
   //如果调用本身就想获得一个容器的引用,则返回这个工厂Bean实例对象
   //如果指定的名称是容器的解引用(dereference,即对象本身而非内存地址)
   //且Bean实例也不是创建Bean实例对象的工厂Bean
   if (BeanFactoryUtils.isFactoryDereference(name) && !(beanInstance instanceof FactoryBean)) {
      throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass());
   }

   //如果Bean实例不是工厂Bean,或者指定名称是容器的解引用
   //调用者获取对容器的引用时,直接返回当前的Bean实例
   if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) {
      return beanInstance;
   }

   //处理指定名称不是容器的解引用,或者根据名称获取的Bean实例对象是一个工厂Bean
   //使用工厂Bean创建一个Bean的实例对象
   Object object = null;
   if (mbd == null) {
      //从Bean工厂缓存中获取指定名称的Bean实例对象
      object = getCachedObjectForFactoryBean(beanName);
   }
   //让Bean工厂生产指定名称的Bean实例对象
   if (object == null) {
      FactoryBean<?> factory = (FactoryBean<?>) beanInstance;
      //如果从Bean工厂生产的Bean是单例模式的,则缓存
      if (mbd == null && containsBeanDefinition(beanName)) {
         //从容器中获取指定名称的Bean定义,如果继承了基类,则合并基类的相关属性
         mbd = getMergedLocalBeanDefinition(beanName);
      }
      //如果从容器得到了Bean定义信息,并且Bean定义信息不是虚构的,
      //则让工厂Bean生产Bean实例对象
      boolean synthetic = (mbd != null && mbd.isSynthetic());
      //调用FactoryBeanRegistrySupport类的getObjectFromFactoryBean()方法
      //实现工厂Bean生产Bean实例对象的过程
      object = getObjectFromFactoryBean(factory, beanName, !synthetic);
   }
   return object;
}

在上面获取给定Bean的实例对象的getObjectForBeanInstance()方法中,会调用FactoryBean- RegistrySupport类的getObjectFromFactoryBean()方法,该方法实现了Bean工厂生产Bean实例对象。

2.3. AbstractBeanFactory生产Bean实例对象

AbstractBeanFactory类中生产Bean实例对象的主要源码如下:

//Bean工厂生产Bean实例对象
protected Object getObjectFromFactoryBean(FactoryBean<?> factory, String beanName, boolean shouldPostProcess) {
   //Bean工厂是单例模式,并且Bean工厂缓存中存在指定名称的Bean实例对象
   if (factory.isSingleton() && containsSingleton(beanName)) {
      //多线程同步,以防止数据不一致
      synchronized (getSingletonMutex()) {
         //直接从Bean工厂的缓存中获取指定名称的Bean实例对象
         Object object = this.factoryBeanObjectCache.get(beanName);
         //如果Bean工厂缓存中没有指定名称的实例对象,则生产该实例对象
         if (object == null) {
            //调用Bean工厂的获取对象的方法生产指定Bean的实例对象
            object = doGetObjectFromFactoryBean(factory, beanName);
            Object alreadyThere = this.factoryBeanObjectCache.get(beanName);
            if (alreadyThere != null) {
               object = alreadyThere;
            }
            else {
               if (shouldPostProcess) {
                  try {
                     object = postProcessObjectFromFactoryBean(object, beanName);
                  }
                  catch (Throwable ex) {
                     throw new BeanCreationException(beanName,
                           "Post-processing of FactoryBean's singleton object failed", ex);
                  }
               }
               //将生产的实例对象添加到Bean工厂的缓存中
               this.factoryBeanObjectCache.put(beanName, object);
            }
         }
         return object;
      }
   }
   //调用Bean工厂的获取对象的方法生产指定Bean的实例对象
   else {
      Object object = doGetObjectFromFactoryBean(factory, beanName);
      if (shouldPostProcess) {
         try {
            object = postProcessObjectFromFactoryBean(object, beanName);
         }
         catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex);
         }
      }
      return object;
   }
}

//调用Bean工厂的方法生产指定Bean的实例对象
private Object doGetObjectFromFactoryBean(final FactoryBean<?> factory, final String beanName)
      throws BeanCreationException {

   Object object;
   try {
      if (System.getSecurityManager() != null) {
         AccessControlContext acc = getAccessControlContext();
         try {
            //实现PrivilegedExceptionAction接口的匿名内部类
            object = AccessController.doPrivileged((PrivilegedExceptionAction<Object>) () ->
                  factory.getObject(), acc);
         }
         catch (PrivilegedActionException pae) {
            throw pae.getException();
         }
      }
      else {
         //调用BeanFactory接口实现类的创建对象方法
         object = factory.getObject();
      }
   }
   catch (FactoryBeanNotInitializedException ex) {
      throw new BeanCurrentlyInCreationException(beanName, ex.toString());
   }
   catch (Throwable ex) {
      throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex);
   }

   //创建出来的实例对象为null,或者因为单例对象正在创建而返回null
   if (object == null) {
      if (isSingletonCurrentlyInCreation(beanName)) {
         throw new BeanCurrentlyInCreationException(
               beanName, "FactoryBean which is currently in creation returned null from getObject");
      }
      object = new NullBean();
   }
   return object;
}

从上面的源码分析中可以看出,BeanFactory接口调用其实现类的获取对象的方法来实现创建Bean实例对象的功能。

2.4. FactoryBean实现类的获取对象的方法

FactoryBean接口的实现类非常多,比如Proxy、RMI、JNDI、ServletContextFactoryBean等。FactoryBean接口为Spring容器提供了一个很好的封装机制,具体的获取对象的方法由不同的实现类根据不同的实现策略来提供,我们分析一下最简单的AnnotationTestFactoryBean类的源码:

public class AnnotationTestBeanFactory implements FactoryBean<FactoryCreatedAnnotationTestBean> {
   private final FactoryCreatedAnnotationTestBean instance = new FactoryCreatedAnnotationTestBean();
   public AnnotationTestBeanFactory() {
      this.instance.setName("FACTORY");
   }
   @Override
   public FactoryCreatedAnnotationTestBean getObject() throws Exception {
      return this.instance;
   }
   //AnnotationTestBeanFactory产生Bean实例对象的实现
   @Override
   public Class<? extends IJmxTestBean> getObjectType() {
      return FactoryCreatedAnnotationTestBean.class;
   }
   @Override
   public boolean isSingleton() {
      return true;
   }
}

Proxy、RMI、JNDI等其他实现类都根据相应的策略提供方法,这里不做一一分析,这已经不是Spring的核心功能,感兴趣的“小伙伴”可以自行深入研究。

3 再述autowiring

Spring IoC容器提供了两种管理Bean依赖关系的方式:
(1)显式管理:通过BeanDefinition的属性值和构造方法实现Bean依赖关系管理。
(2)autowiring:Spring IoC容器有依赖自动装配功能,不需要对Bean属性的依赖关系做显式的声明,只需要配置好autowiring属性,IoC容器会自动使用反射查找属性的类型和名称,然后基于属性的类型或者名称来自动匹配容器中的Bean,从而自动完成依赖注入。
容器对Bean的自动装配发生在容器对Bean依赖注入的过程中。在对Spring IoC容器的依赖注入源码进行分析时,我们已经知道容器对Bean实例对象的依赖属性注入发生在AbstractAutoWireCapableBeanFactory类的populateBean()方法中,下面通过程序流程分析autowiring的实现原理。

3.1. AbstractAutoWireCapableBeanFactory对Bean实例对象进行属性依赖注入

应用程序第一次通过getBean()方法(配置了lazy-init预实例化属性的除外)向IoC容器索取Bean时,容器创建Bean实例对象,并且对Bean实例对象进行属性依赖注入,AbstractAutoWire- CapableBeanFactory的populateBean()方法就实现了属性依赖注入的功能,其主要源码如下:

//将Bean属性设置到生成的实例对象上
protected void populateBean(String beanName, RootBeanDefinition mbd, @Nullable BeanWrapper bw) {//获取容器在解析Bean定义时为BeanDefinition设置的属性值
   PropertyValues pvs = (mbd.hasPropertyValues() ? mbd.getPropertyValues() : null);

   //处理依赖注入,首先处理autowiring自动装配的依赖注入
   if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME ||
         mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
      MutablePropertyValues newPvs = new MutablePropertyValues(pvs);

      //根据Bean名称进行autowiring自动装配处理
      if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) {
         autowireByName(beanName, mbd, bw, newPvs);
      }

      //根据Bean类型进行autowiring自动装配处理
      if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) {
         autowireByType(beanName, mbd, bw, newPvs);
      }

      pvs = newPvs;
   }

   //对非autowiring的属性进行依赖注入处理
   ...
}

3.2. Spring IoC容器根据Bean名称或者类型进行autowiring自动属性依赖注入

Spring IoC容器根据Bean名称或者类型进行autowiring自动属性依赖注入的重要代码如下:

//根据类型对属性进行自动依赖注入
protected void autowireByType(
      String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) {

   //获取用户定义的类型转换器
   TypeConverter converter = getCustomTypeConverter();
   if (converter == null) {
      converter = bw;
   }

   //存放解析的要注入的属性
   Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
   //对Bean对象中非简单属性(不是简单继承的对象,如8种原始类型、字符、URL等都是简单属性)进行处理
   String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw);
   for (String propertyName : propertyNames) {
      try {
         //获取指定属性名称的属性描述器
         PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName);
         //不对Object类型的属性进行autowiring自动依赖注入
         if (Object.class != pd.getPropertyType()) {
            //获取属性的赋值方法
            MethodParameter MethodParam = BeanUtils.getWriteMethodParameter(pd);
            //检查指定类型是否可以被转换为目标对象的类型
            boolean eager = !PriorityOrdered.class.isInstance(bw.getWrappedInstance());
            //创建一个要被注入的依赖描述
            DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(MethodParam, eager);
            //根据容器的Bean定义解析依赖关系,返回所有要被注入的Bean对象
            Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter);
            if (autowiredArgument != null) {
               //将属性赋值为所引用的对象
               pvs.add(propertyName, autowiredArgument);
            }
            for (String autowiredBeanName : autowiredBeanNames) {
               //为指定名称属性注册依赖Bean名称,进行属性的依赖注入
               registerDependentBean(autowiredBeanName, beanName);
               if (logger.isDebugEnabled()) {
                  logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" 
                        + propertyName + "' to bean named '" + autowiredBeanName + "'");
               }
            }
            //释放已自动注入的属性
            autowiredBeanNames.clear();
         }
      }
      catch (BeansException ex) {
         throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex);
      }
   }
}

通过上面的源码分析可以看出,通过属性名进行自动依赖注入相比通过属性类型进行自动依赖注入要稍微简单一些。但是真正实现属性注入的是DefaultSingletonBeanRegistry类的registerDependentBean()方法。

3.3. DefaultSingletonBeanRegistry的registerDependentBean()方法实现属性依赖注入

DefaultSingletonBeanRegistry的registerDependentBean()方法实现属性依赖注入的重要代码如下:

//为指定的Bean注入依赖的Bean
public void registerDependentBean(String beanName, String dependentBeanName) {
   //处理Bean名称,将别名转换为规范的Bean名称
   String canonicalName = canonicalName(beanName);
   Set<String> dependentBeans = this.dependentBeanMap.get(canonicalName);
   if (dependentBeans != null && dependentBeans.contains(dependentBeanName)) {
      return;
   }

   //多线程同步,保证容器内数据的一致性
   //在容器中通过“Bean名称→全部依赖Bean名称集合”查找指定名称Bean的依赖Bean
   synchronized (this.dependentBeanMap) {
      //获取指定名称Bean的所有依赖Bean名称
      dependentBeans = this.dependentBeanMap.get(canonicalName);
      if (dependentBeans == null) {
         //为Bean设置依赖Bean信息
         dependentBeans = new LinkedHashSet<>(8);
         this.dependentBeanMap.put(canonicalName, dependentBeans);
      }
      //在向容器中通过“Bean名称→全部依赖Bean名称集合”添加Bean的依赖信息
      //即,将Bean所依赖的Bean添加到容器的集合中
      dependentBeans.add(dependentBeanName);
   }
   //在容器中通过“Bean名称→指定名称Bean的依赖Bean集合”查找指定名称Bean的依赖Bean
   synchronized (this.dependenciesForBeanMap) {
      Set<String> dependenciesForBean = this.dependenciesForBeanMap.get(dependentBeanName);
      if (dependenciesForBean == null) {
         dependenciesForBean = new LinkedHashSet<>(8);
         this.dependenciesForBeanMap.put(dependentBeanName, dependenciesForBean);
      }
      //在容器中通过“Bean名称→指定Bean的依赖Bean名称集合”添加Bean的依赖信息
      //即,将Bean所依赖的Bean添加到容器的集合中
      dependenciesForBean.add(canonicalName);
   }
}

可以看出,autowiring的实现过程如下:
(1)对Bean的属性调用getBean()方法,完成依赖Bean的初始化和依赖注入。
(2)将依赖Bean的属性引用设置到被依赖的Bean属性上。
(3)将依赖Bean的名称和被依赖Bean的名称存储在IoC容器的集合中。
Spring IoC容器的autowiring自动属性依赖注入是一个很方便的特性,可以简化开发配置,但是凡事都有两面性,自动属性依赖注入也有不足:首先,Bean的依赖关系在配置文件中无法很清楚地看出来,会给维护造成一定的困难;其次,由于自动属性依赖注入是Spring容器自动执行的,容器是不会智能判断的,如果配置不当,将会带来无法预料的后果。所以在使用自动属性依赖注入时需要综合考虑。
关注『 Tom弹架构 』回复“Spring”可获取完整源码。

本文为“Tom弹架构”原创,转载请注明出处。技术在于分享,我分享我快乐! 如果您有任何建议也可留言评论或私信,您的支持是我坚持创作的动力。关注『 Tom弹架构 』可获取更多技术干货!

原创不易,坚持很酷,都看到这里了,小伙伴记得点赞、收藏、在看,一键三连加关注!如果你觉得内容太干,可以分享转发给朋友滋润滋润!

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消