对象池(连接池):commons-pool2源码解析:GenericObjectPool的startEvictor、Evictor、evict解析

在之前的文章中分析过GenericObjectPool的构造方法(参见:https://blog.csdn.net/weixin_42340670/article/details/107300577),在构造方法内部逻辑的最后调用了startEvictor方法。这个方法的作用是在构造完对象池后,启动回收器来监控回收空闲对象。startEvictor定义在GenericObjectPool的父类BaseGenericObjectPool(抽象)类中,我们先看一下这个方法的源码

BaseGenericObjectPool.startEvictor方法解析

final Object evictionLock = new Object(); // 用来和synchronized配合使用,实现多线程同步
private Evictor evictor = null; // @GuardedBy("evictionLock")  回收器对象
EvictionIterator evictionIterator = null; // @GuardedBy("evictionLock")  回收迭代器。里面存储的都是空闲对象
/**
* <p>Starts the evictor with the given delay. If there is an evictor
* running when this method is called, it is stopped and replaced with a
* new evictor with the specified delay.</p>
*
* <p>This method needs to be final, since it is called from a constructor.
* See POOL-195.</p>
*
* @param delay time in milliseconds before start and between eviction runs
*
* 用给定延迟时间来开启回收器。
* 如果已经存在着一个在运行的回收器了,则停掉它,再根据给定的延迟时间重新创建一个回收器。
* 这个方法需要被标记为final,因为他会被构造方法调用。
*  为什么被构造方法调用就要设置为final?注释里说去官网参阅 POOL-195 ISSUE。
* (https://issues.apache.org/jira/browse/POOL-195)
*/
final void startEvictor(long delay) {
    synchronized (evictionLock) { // 同步处理
        if (null != evictor) { // 如果evictor不为空,说明已经存在回收器了
            EvictionTimer.cancel(evictor); // 取消掉当前的回收器
            evictor = null; // 回收器对象设置为空,便于垃圾回收。
            evictionIterator = null; // 回收器迭代器设置为空,便于垃圾回收。
        }

        /*
            这个判断很重要。如果传入的delay是个小于0的数值。那里面的逻辑就不会执行。但是上面的逻辑都已经执行完了。
            所以调用startEvictor时,如果传入delay小于0:
                如果当前回收器不为空,当前的回收器被清除。
                如果当前回收器为空,那么就等于啥都没干。

            只有delay大于0时,才会创建一个新的回收器,赋值给当前的回收器对象
        */
        if (delay > 0) { 
            evictor = new Evictor(); // 创建一个新的回收器
            // 延迟delay毫秒后,开始执行回收任务,之后每隔delay毫秒后重复执行。
            EvictionTimer.schedule(evictor, delay, delay);
        }
    }
}

实际上在GenericObjectPool中startEvictor方法一共被三个地方调用

  • 构造方法:创建了对象池之后,则尝试开启回收线程
  • close方法:关闭对象池的时候,也会调用startEvictor用来清理回收对象池中的资源。
  • setTimeBetweenEvictionRunsMillis方法:该方法的作用是设置回收线程的回收周期(多少毫秒执行一轮回收)。既然设置了回收周期,那么就意味着一定开启回收线程,所以顺带着就调用了startEvictor来启动回收线程。

在startEvictor方法中,核心逻辑就是创建Evictor,并启动调度,接下来我们再看下Evictor类的源码,Evictor类是BaseGenericObjectPool的内部类,以下摘取了BaseGenericObjectPool类中和Evictor相关的代码进行解析

BaseGenericObjectPool.Evictor类解析

public abstract class BaseGenericObjectPool<T> {

    /*
    * ---可以先看内部类Evictor的解析,再回头来看这个属性的解析----
    * Class loader for evictor thread to use since, in a JavaEE or similar
    * environment, the context class loader for the evictor thread may not have
    * visibility of the correct factory. See POOL-161. Uses a weak reference to
    * avoid potential memory leaks if the Pool is discarded rather than closed.
    * 用于回收器线程的类加载器。
    * 在JavaEE或类似的环境中,回收器上下文类加载器可能对于对象工厂没有可见性
    *   多个回收器共用一个Timer,Timer的加载器取决于最先实例化的对象池的加载器。
    * 如果池被丢弃而不是关闭,使用弱引用可以避免潜在的内存泄漏。
    * 更详细的解释可以参考:https://issues.apache.org/jira/browse/POOL-161
    */
    private final WeakReference<ClassLoader> factoryClassLoader; 

    public abstract void evict() throws Exception; // 定义的抽象方法,需要子类实现,完成空闲对象的清除

    abstract void ensureMinIdle() throws Exception; // 定义的抽象方法,需要子类实现,完成空闲对象的最低数量保障

    /**
    * The idle object evictor {@link TimerTask}.
    *
    * @see GenericKeyedObjectPool#setTimeBetweenEvictionRunsMillis
    */
    class Evictor extends TimerTask { // 继承了抽象类TimerTask,决定了他可以放到定时调度中,同时也不需实现run方法
        /**
        * Run pool maintenance.  Evict objects qualifying for eviction and then
        * ensure that the minimum number of idle instances are available.
        * Since the Timer that invokes Evictors is shared for all Pools but
        * pools may exist in different class loaders, the Evictor ensures that
        * any actions taken are under the class loader of the factory
        * associated with the pool.
        * run方法干的什么事情呢?
        * 运行对象池的维护任务。回收那些符合被回收条件的对象,确保有最小数量的空闲对象可用就可以。
        * 因为调用回收器的定时器是所有对象池共享的,但是不同的对象池可能是被不同的classloader加载的。不同的classloader之间类信息不共享。
        * 回收器的很多操作还是最终要调用对象池的相关方法来完成的,所以回收器必须保证他的回收线程的上下文classloader要和对象池的classloader一致。
        */
        @Override
        public void run() {
            // 首先获取当前线程执行上下文的classloader,这个就是Timer类的classloader。
            ClassLoader savedClassLoader =
                    Thread.currentThread().getContextClassLoader(); 
            try {
                // 关于这个factoryClassLoader是在BaseGenericObjectPool的构造方法中赋值的。(参考:https://blog.csdn.net/weixin_42340670/article/details/107300577)
                // 如果factoryClassLoader为空,说明对象池的类加载器是根加载器
                if (factoryClassLoader != null) { // 如果factoryClassLoader不为空,说明对象池的加载器很可能是AppClassLoader或者某个框架自定义的加载器
                    // Set the class loader for the factory
                    ClassLoader cl = factoryClassLoader.get(); // 因为factoryClassLoader本身是个弱引用,里面包的是真正的classloader,所以调用get方法获取到真正的classloader
                    if (cl == null) { 
                        /*
                        通过之前文章中对BaseGenericObjectPool的构造方法解析,我们能够知道,只要factoryClassLoader不为空,那么里面就一定存放了当时的classloader。
                        但是如果当前的这个方法里get出来的为空,那么是因为factoryClassLoader是一个WeakReference,WeakReference弱引用所持有的对象,在发生GC时就会触发回收,但是他持有的classloader是否能被回收,还取决于这个classloader是否还存在强引用、是否还存在着被他加载的还使用着的类以及类的对象。所以如果cl为空,那只能说明这个classloader关联的【所有类的对象、类】已经都不被引用了、都已经被回收了(这就说明对象池已经被回收了),然后classloader本身才可以被回收。那么既然对象池都不存在了,所以回收任务也可以取消掉了。
                        */
                        // The pool has been dereferenced and the class loader
                        // GC'd. Cancel this timer so the pool can be GC'd as
                        // well.
                        cancel(); // 这个cancel方法是TimerTask提供的方法,用来将任务状态置为CANCELLED,就不会再被Timer调度了。
                        return;
                    }

                    // 能走到这一步说明,正常获取到了对象池的当时的classloader,所以将当前上下文的classloader设置为cl
                    Thread.currentThread().setContextClassLoader(cl);
                }

                // Evict from the pool
                try {
                    /*
                    执行清除动作
                    这个方法定义在BaseGenericObjectPool类中,是个抽象方法,需要子类去实现
                    源码:public abstract void evict() throws Exception;
                    */
                    evict(); 
                } catch(Exception e) {
                    swallowException(e); // 吞下异常,自己尝试消化(可以自己实现SwallowedExceptionListener接口,设置为异常监听处理程序,如果没定义,那这方法就等于啥也没干)
                } catch(OutOfMemoryError oome) {
                    // Log problem but give evictor thread a chance to continue
                    // in case error is recoverable
                    // 如果发生内存错误,捕捉到打印日志。不向外抛出,
                    // 因为OutOfMemoryError是Error类型异常,这种异常一旦发生,不捕捉到,那么jvm将会终止程序。
                    // 所以这里catch到,是为了防止万一是偶发的,可恢复的,那么保证程序不被终止。
                    oome.printStackTrace(System.err);
                }
                // Re-create idle instances.
                try {
                    /*
                    确保对象池里有足够的(min idle配置的数值)空闲对象
                    这个方法定义在BaseGenericObjectPool类中,是个抽象方法,需要子类去实现
                    源码:abstract void ensureMinIdle() throws Exception;
                    */
                    ensureMinIdle(); // 
                } catch (Exception e) {
                    swallowException(e); // 吞下异常,自己尝试消化(可以自己实现SwallowedExceptionListener接口,设置为异常监听处理程序,如果没定义,那这方法就等于啥也没干)
                }
            } finally {
                // Restore the previous CCL  最后还是要把线程上下文的classloader重置为Timer类的classloader
                Thread.currentThread().setContextClassLoader(savedClassLoader);
            }
        }
    }


    final void swallowException(Exception e) {
        // 只有了setSwallowedExceptionListener了,getSwallowedExceptionListener才能get出来
        // setSwallowedExceptionListener方法需要传入一个SwallowedExceptionListener
        SwallowedExceptionListener listener = getSwallowedExceptionListener(); 

        if (listener == null) { // 如果设置了异常监听
            return;
        }

        try {
            listener.onSwallowException(e); // 调用异常监听器的处理方法
        } catch (OutOfMemoryError oome) {
            throw oome;
        } catch (VirtualMachineError vme) {
            throw vme;
        } catch (Throwable t) {
            // Ignore. Enjoy the irony.
        }
    }
}   

在Evictor类的run方法中,调用了两个非常核心的方法,但是这两个方法都是声明在BaseGenericObjectPool类中的抽象方法,需要子类实现

  • evict方法
  • ensureMinIdle方法
    GenericObjectPool是对象池的一个核心实现类,他是一定会实现这两个方法的,我们先来看evict方法在GenericObjectPool中的实现

GenericObjectPool.evict方法解析

public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
        implements ObjectPool<T>, GenericObjectPoolMXBean, UsageTracking<T> {

    private final LinkedBlockingDeque<PooledObject<T>> idleObjects;
    final Object evictionLock = new Object(); // 对象锁。startEvictor和evict方法都用这把锁。
    // 回收迭代器,内部持有的就是空闲对象列表。
    // 为什么不用jdk自带的迭代器呢?抽象出EvictionIterator的最重要意义是,对外屏蔽了是正向迭代还是反向迭代。(取决于lifo配置)
    EvictionIterator evictionIterator = null; 


    /**
    * {@inheritDoc}
    * <p>
    * Successive activations of this method examine objects in sequence,
    * cycling through objects in oldest-to-youngest order.
    */
    @Override
    public void evict() throws Exception {
        assertOpen(); // 校验对象池的状态,不能是close状态,如果是close状态,会抛出"Pool not open"异常

        if (idleObjects.size() > 0) { // 如果空闲对象数量大于0

            PooledObject<T> underTest = null;
            // 获取回收策略对象,回收策略也是支持用户自定义,框架提供的默认的是DefaultEvictionPolicy
            EvictionPolicy<T> evictionPolicy = getEvictionPolicy(); 

            synchronized (evictionLock) { // 同步处理
                /*
                根据对象池的几个参数创建一个回收配置对象
                minEvictableIdleTimeMillis:一个毫秒数值,用来指定超过这个空闲时间的会被回收。如果是负数,配置不生效。
                softMinEvictableIdleTimeMillis:一个毫秒数值,用来指定在空闲对象数量超过minIdle设置,且某个空闲对象超过这个空闲时间的才可以会被回收。
                minIdle:对象池里要保留的最小空间对象数量。
                */
                EvictionConfig evictionConfig = new EvictionConfig(
                        getMinEvictableIdleTimeMillis(), 
                        getSoftMinEvictableIdleTimeMillis(),
                        getMinIdle());

                // 获取testWhileIdle配置,如果为true,说明需要校验一下对象的有效性。
                boolean testWhileIdle = getTestWhileIdle(); 

                /*
                getNumTests这个函数比较重要,下面有单独解析。
                返回的是需要回收的对象数量。
                */
                for (int i = 0, m = getNumTests(); i < m; i++) {
                    // 如果迭代器为null,或者已经遍历到头了,则重新new一个
                    if (evictionIterator == null || !evictionIterator.hasNext()) {
                        evictionIterator = new EvictionIterator(idleObjects); 
                    }
                    if (!evictionIterator.hasNext()) { // 如果没后可遍历的元素了,说明连接池满了,没有空闲对象
                        // Pool exhausted, nothing to do here
                        return; // 没有可回收的,直接退出。
                    }

                    try {
                        underTest = evictionIterator.next(); // 获取到待处理的空闲对象
                    } catch (NoSuchElementException nsee) {
                        // Object was borrowed in another thread
                        // Don't count this as an eviction test so reduce i;
                        // 在遍历过程中,虽然hasNext可能是true,但是某个对象可能又重新被其他线程使用,等next时候可能就获取失败
                        i--; // 既然都没获取到,i--之后,就可以再重新加一轮检测
                        evictionIterator = null; // next已经获取不到了,也迭代到头了,设置为null,等下一轮循环的时候,就会重新创建一个
                        continue; // 进行下一轮循环
                    }

                    // 走到这里说明,获取到了一个要处理的空闲对象

                    /*
                    startEvictionTest是检查并更改对象的状态,IDLE -> EVICTION
                    所有和状态变更相关的方法都是synchronized的,包括后面介绍的endEvictionTest
                    */
                    if (!underTest.startEvictionTest()) {
                        // 走到这里,说明状态变更失败,很可能就是因为,又重新被使用了,状态不是空闲了。
                        // Object was borrowed in another thread
                        // Don't count this as an eviction test so reduce i;
                        i--; // 既然变为非空闲了,i--之后,就可以再重新加一轮检测
                        continue; // 进行下一轮循环
                    }

                    // User provided eviction policy could throw all sorts of
                    // crazy exceptions. Protect against such an exception
                    // killing the eviction thread.
                    /* 
                    用户提供的回收策略可能会抛出各种疯狂的异常,所以这里去catch异常的老祖宗Throwable
                    以防止异常导致回收线程被终止,致使处理流程不完整。
                    */

                    boolean evict; // 定义个变量,标识获取到的空闲对象underTest到底可不可以被回收
                    try {
                        // 这里调用回收策略的evict方法来判定underTest是否可以被回收
                        // 上面讲过框架提供的默认回收策略是DefaultEvictionPolicy,下面会对DefaultEvictionPolicy的evict方法进行单独解析
                        evict = evictionPolicy.evict(evictionConfig, underTest,
                                idleObjects.size()); 
                    } catch (Throwable t) {
                        // Slightly convoluted as SwallowedExceptionListener
                        // uses Exception rather than Throwable
                        PoolUtils.checkRethrow(t); // 先处理两个SwallowedExceptionListener不关注的异常
                        swallowException(new Exception(t)); // 再通过SwallowedExceptionListener处理
                        // Don't evict on error conditions
                        evict = false; // 如果捕捉到异常,认为不可以被回收
                    }

                    if (evict) { // 如果可以被回收
                        destroy(underTest); // 调用destroy方法销毁underTest
                        destroyedByEvictorCount.incrementAndGet(); // 回收器销毁计数器加1
                    } else { // 如果不能被回收
                        if (testWhileIdle) { // 如果testWhileIdle为ture,虽然不能被回收,但是也检查下对象是否可用(检查下资源是不是还在,连接是不是断开了等)
                            boolean active = false; // 定义个变量,标识空闲对象是否还存活,初始化为false,
                            try {
                                // 调用activateObject激活该空闲对象,本质上不是为了激活,而是通过这个方法可以判定是否还存活,这一步里面可能会有一些资源的开辟行为。
                                factory.activateObject(underTest); 
                                active = true; // 设置为true
                            } catch (Exception e) { // 如果激活的时候,发生了异常,就说明该空闲对象已经失联了。
                                destroy(underTest); // 调用destroy方法销毁underTest
                                destroyedByEvictorCount.incrementAndGet(); // 回收器销毁计数器加1
                            }
                            if (active) { // 如果激活成功,说明联系到了,但是能不能干活,还得再校验一下
                                if (!factory.validateObject(underTest)) { // 再通过进行validateObject校验有效性
                                    // 如果校验失败,说明对象已经不可用了
                                    destroy(underTest);  // 调用destroy方法销毁underTest
                                    destroyedByEvictorCount.incrementAndGet(); // 回收器销毁计数器加1
                                } else { 
                                    /*
                                    如果校验通过,说明对象还是可用的,但是我们的回收线程的重点任务是回收和校验空闲对象,因为校验还激活了空闲对象,分配了额外的资源
                                    既然都校验完了,那么就通过passivateObject把在activateObject中开辟的资源释放掉。
                                    备注:passivateObject 这个方法是一个比较理想态。其实很少有下游框架实现这个方法。
                                    */ 
                                    try {
                                        factory.passivateObject(underTest);
                                    } catch (Exception e) { // 如果passivateObject失败,也可以说明underTest这个空闲对象不可用了
                                        destroy(underTest); // 调用destroy方法销毁underTest
                                        destroyedByEvictorCount.incrementAndGet(); // 回收器销毁计数器加1
                                    }
                                }
                            }
                        }
                        /*
                        既然当前空闲对象不能被回收
                        那么需要重置空闲对象的状态 ,因为之前通过调用startEvictionTest之后,对象状态已经改变:IDLE -> EVICTION
                        调用endEvictionTest可以把状态还原:EVICTION -> IDLE
                        */
                        if (!underTest.endEvictionTest(idleObjects)) {
                            // TODO - May need to add code here once additional
                            // states are used

                            // 如果状态重置失败了,就在这里处理。
                        }
                    }
                }
            }
        }

        // 这里是关于遗弃对象的处理。关于这部分内容希望你参考我之前写过的关于abandonedConfig解析,来对照理解。
        // abandonedConfig解析链接地址:https://blog.csdn.net/weixin_42340670/article/details/107136994
        AbandonedConfig ac = this.abandonedConfig; // 获取遗弃对象配置
        // 如果removeAbandonedOnMaintenance配置为true,说明在空闲回收的同时也进行遗弃对象的处理
        if (ac != null && ac.getRemoveAbandonedOnMaintenance()) {  
            removeAbandoned(ac);
        }
    }


    /**
    * Calculate the number of objects to test in a run of the idle object
    * evictor.
    *
    * @return The number of objects to test for validity
    * 返回的是回收器中每次回收任务需要检查的空闲对象的数量
    */
    private int getNumTests() {
        int numTestsPerEvictionRun = getNumTestsPerEvictionRun(); // 先获取的是numTestsPerEvictionRun配置的值,默认值是3
        if (numTestsPerEvictionRun >= 0) { // 如果numTestsPerEvictionRun配置了一个大于等于0的值(如果配置为0,就等于不回收)
            // 取numTestsPerEvictionRun和空闲对象列表长度的最小值,防止越界
            return Math.min(numTestsPerEvictionRun, idleObjects.size()); 
        } else { // 如果numTestsPerEvictionRun设置的是一个小于0的值
            /*
            如果现在一共有10个空闲对象,numTestsPerEvictionRun配置为-3
            则返回的就是  Math.ceil(10/-3的绝对值) = Math.ceil(10/3) = Math.ceil(3.3) = 4
            如果配置的-1,返回的就是10,也就是回收所有的空闲对象
            */
            return (int) (Math.ceil(idleObjects.size() /
                    Math.abs((double) numTestsPerEvictionRun)));
        }
    }

}    

GenericObjectPool.ensureMinIdle方法解析

public class GenericObjectPool<T> extends BaseGenericObjectPool<T>
        implements ObjectPool<T>, GenericObjectPoolMXBean, UsageTracking<T> {

    // 略去了很多代码
    
            
    @Override
    void ensureMinIdle() throws Exception {
        ensureIdle(getMinIdle(), true);
    }

    /**
     * Tries to ensure that {@code idleCount} idle instances exist in the pool.
     * <p>
     * Creates and adds idle instances until either {@link #getNumIdle()} reaches {@code idleCount}
     * or the total number of objects (idle, checked out, or being created) reaches
     * {@link #getMaxTotal()}. If {@code always} is false, no instances are created unless
     * there are threads waiting to check out instances from the pool.
     *
     * @param idleCount the number of idle instances desired
     * @param always true means create instances even if the pool has no threads waiting
     * @throws Exception if the factory's makeObject throws
     *
     * 尝试确保对象池中有足够的空闲对象
     * 创建新的然后添加到对象池中,直到空闲对象数量达到指定的idleCount。
     * 当然前提也是对象池中所有对象的数量不能大于对象池的maxTotal配置。
     * idleCount参数:指定要创建多少个空闲对象
     * always参数:如果为true,意味着不会关注到底有没有调用方在等待获取对象。如果为false,意味着只有存在着等待获取对象的调用方的时候才会创建对象。
     */
    private void ensureIdle(int idleCount, boolean always) throws Exception {
        // 如果idleCount小于1,或者对象池已经关闭,或者always为false但是没有处于等待的调用方。 这不会创建对象。
        if (idleCount < 1 || isClosed() || (!always && !idleObjects.hasTakeWaiters())) {
            return;
        }

        while (idleObjects.size() < idleCount) { // 如果空闲对象数量小于指定的idleCount
            PooledObject<T> p = create(); // 创建一个新对象
            if (p == null) { // 如果p为空,说明创建失败,跳出循环
                // Can't create objects, no reason to think another call to
                // create will work. Give up.
                break;
            }
            if (getLifo()) { // 判断回收策略,然后决定是往队列的开头加还是末尾加
                idleObjects.addFirst(p);
            } else {
                idleObjects.addLast(p);
            }
        }
        if (isClosed()) { // 如果对象池已经处于关闭状态
            // Pool closed while object was being added to idle objects.
            // Make sure the returned object is destroyed rather than left
            // in the idle object pool (which would effectively be a leak)
            clear(); // 清理所有
        }
    }    
}    

DefaultEvictionPolicy.evict方法解析

public class DefaultEvictionPolicy<T> implements EvictionPolicy<T> {

    @Override
    public boolean evict(EvictionConfig config, PooledObject<T> underTest,
            int idleCount) {
        /*
        A || B,是短路运算的,A为ture,则B不会执行。B是兜底方案。
        A:(config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() && config.getMinIdle() < idleCount)
            config.getIdleSoftEvictTime():获取的就是softMinEvictableIdleTimeMillis配置
            underTest.getIdleTimeMillis():获取的就是对象的空闲时间
            config.getMinIdle():获取的是minIdle配置,需要保留的最小空闲数量
            idleCount:所有空闲对象的数量

        B:config.getIdleEvictTime() < underTest.getIdleTimeMillis()
            config.getIdleEvictTime():获取的就是minEvictableIdleTimeMillis配置
            underTest.getIdleTimeMillis():获取的就是对象的空闲时间

        通过这段代码,我们就可以得出为什么了非要搞一个softMinEvictableIdleTimeMillis出来,他的作用是什么?
            softMinEvictableIdleTimeMillis可以设置的比minEvictableIdleTimeMillis小,他在检查时,除了检查空闲对象的空闲时间,还会校验空闲对象的数量,如果空闲对象数量已经小于等于minIdle,就算超过了他设置的时间,他的判断也为false,所以继续进行后面的判断。
            而一旦空闲时间超过了minEvictableIdleTimeMillis,那么就不会再保留了。

            所以softMinEvictableIdleTimeMillis意味着在空闲了一个相对较短的时间的情况下,有些空闲对象可以不回收,用来维持空闲对象的最低保障。这应该就是soft要表达的意思,他并非完全根据时间来强制判断。
            而一旦某个对象空闲时间超过了minEvictableIdleTimeMillis了,就强制回收了。

            所以如果要设置softMinEvictableIdleTimeMillis,一定要保证小于minEvictableIdleTimeMillis,否则就没意义了。
        */
        if ((config.getIdleSoftEvictTime() < underTest.getIdleTimeMillis() &&
                config.getMinIdle() < idleCount) ||
                config.getIdleEvictTime() < underTest.getIdleTimeMillis()) {
            return true;
        }
        return false;
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值