21--Spring通過無參構(gòu)造方法實例化單例bean

上一節(jié)分析了Spring實例化單例bean的準備工作姓赤,而且已經(jīng)接觸到了真正創(chuàng)建bean的方法doCreateBean庵寞,本小節(jié)分析Spring是如何實例化bean的枉证。

引言以躯,doCreateBean方法簡析
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) throws BeanCreationException {

    // Instantiate the bean.
    // ① 實例化bean
    BeanWrapper instanceWrapper = null;
    // 注意factoryBeanInstanceCache是ConcurrentMap,remove方法會返回刪除的鍵值(如果不存在返回null)
    if (mbd.isSingleton()) {
        instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
    }
    // 如果factoryBeanInstanceCache沒有緩存對應(yīng)的BeanWrapper,則重新創(chuàng)建bean實例
    if (instanceWrapper == null) {
        instanceWrapper = createBeanInstance(beanName, mbd, args);
    }
    final Object bean = instanceWrapper.getWrappedInstance();
    Class<?> beanType = instanceWrapper.getWrappedClass();
    if (beanType != NullBean.class) {
        mbd.resolvedTargetType = beanType;
    }

    // Allow post-processors to modify the merged bean definition.
    // ② 允許MergedBeanDefinitionPostProcessor后處理器修改已合并的bean定義。
    synchronized (mbd.postProcessingLock) {
        if (!mbd.postProcessed) {
            try {
                applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
            }
            catch (Throwable ex) {
                throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Post-processing of merged bean definition failed", ex);
            }
            mbd.postProcessed = true;
        }
    }

    // Eagerly cache singletons to be able to resolve circular references
    // even when triggered by lifecycle interfaces like BeanFactoryAware.
    // ③ 提前緩存ObjectFactory以解決bean之間的循環(huán)依賴
    // mbd.isSingleton()->是否單例
    // allowCircularReferences->是否允許循環(huán)依賴
    // isSingletonCurrentlyInCreation->該bean是否創(chuàng)建中
    boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName));
    if (earlySingletonExposure) {
        addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
    }

    // Initialize the bean instance.
    // ④ 初始化bean實例 這里大家要與第①步區(qū)分開,到這里bean已經(jīng)完成了實例化,但是還沒有完成初始化的操作,例如bean的屬性填充
    Object exposedObject = bean;
    try {
        // 填充bean屬性
        populateBean(beanName, mbd, instanceWrapper);
        // 初始化bean
        exposedObject = initializeBean(beanName, exposedObject, mbd);
    }
    catch (Throwable ex) {
        if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
            throw (BeanCreationException) ex;
        }
        else {
            throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex);
        }
    }

    // ⑤ 循環(huán)依賴檢查
    if (earlySingletonExposure) {
        Object earlySingletonReference = getSingleton(beanName, false);
        if (earlySingletonReference != null) {
            if (exposedObject == bean) {
                exposedObject = earlySingletonReference;
            }
            else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
                String[] dependentBeans = getDependentBeans(beanName);
                Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
                for (String dependentBean : dependentBeans) {
                    if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
                        actualDependentBeans.add(dependentBean);
                    }
                }
                if (!actualDependentBeans.isEmpty()) {
                    throw new BeanCurrentlyInCreationException(beanName,
                            "Bean with name '" + beanName + "' has been injected into other beans [" +
                            StringUtils.collectionToCommaDelimitedString(actualDependentBeans) +
                            "] in its raw version as part of a circular reference, but has eventually been " +
                            "wrapped. This means that said other beans do not use the final version of the " +
                            "bean. This is often the result of over-eager type matching - consider using " +
                            "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.");
                }
            }
        }
    }

    // Register bean as disposable.
    try {
        // ⑥ 根據(jù)bean的作用域注冊bean
        registerDisposableBeanIfNecessary(beanName, bean, mbd);
    }
    catch (BeanDefinitionValidationException ex) {
        throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex);
    }
    // ⑦ 返回bean實例
    return exposedObject;
}

步驟如下:

  • ① 實例化bean
  • ② 允許MergedBeanDefinitionPostProcessor后處理器修改已合并的bean定義湖饱。
  • ③ 提前緩存ObjectFactory以解決bean之間的循環(huán)依賴
  • ④ 初始化bean實例 這里大家要與第①步區(qū)分開,到這里bean已經(jīng)完成了實例化,但是還沒有完成初始化的操作,例如bean的屬性填充
  • ⑤ 循環(huán)依賴檢查
  • ⑥ 根據(jù)bean的作用域注冊bean
  • ⑦ 返回bean實例

這些步驟中涉及的知識點很多掖蛤,我們逐步分析。

1.實例化bean

createBeanInstance方法完成了對bean的實例化操作井厌,打開該方法蚓庭。

protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {

    // 確保此時beanClass已經(jīng)被解析
    Class<?> beanClass = resolveBeanClass(mbd, beanName);

    // beanClass不為空,且beanClass的修飾符為不為public,且不允許訪問非公共構(gòu)造函數(shù)和方法,則拋出異常
    if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) {
        throw new BeanCreationException(mbd.getResourceDescription(), beanName,
                "Bean class isn't public, and non-public access not allowed: " + beanClass.getName());
    }

    // ① Spring5.0新增的實例化策略,如果設(shè)置了該策略,將會覆蓋構(gòu)造方法和工廠方法實例化策略
    Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
    if (instanceSupplier != null) {
        return obtainFromSupplier(instanceSupplier, beanName);
    }

    // ② 如果有工廠方法的話,則使用工廠方法實例化bean
    if (mbd.getFactoryMethodName() != null)  {
        return instantiateUsingFactoryMethod(beanName, mbd, args);
    }

    // ③ 當創(chuàng)建一個相同的bean時,使用之間保存的快照
    // 這里可能會有一個疑問,什么時候會創(chuàng)建相同的bean呢?
    //      ③-->① 單例模式: Spring不會緩存該模式的實例,那么對于單例模式的bean,什么時候會用到該實例化策略呢?
    //                 我們知道對于IoC容器除了可以索取bean之外,還能銷毀bean,當我們調(diào)用xmlBeanFactory.destroyBean(myBeanName,myBeanInstance),
    //                 銷毀bean時,容器是不會銷毀已經(jīng)解析的構(gòu)造函數(shù)快照的,如果再次調(diào)用xmlBeanFactory.getBean(myBeanName)時,就會使用該策略了.
    //      ③-->② 原型模式: 對于該模式的理解就簡單了,IoC容器不會緩存原型模式bean的實例,當我們第二次向容器索取同一個bean時,就會使用該策略了.
    boolean resolved = false;
    boolean autowireNecessary = false;
    if (args == null) {
        synchronized (mbd.constructorArgumentLock) {
            if (mbd.resolvedConstructorOrFactoryMethod != null) {
                resolved = true;
                autowireNecessary = mbd.constructorArgumentsResolved;
            }
        }
    }
    // 如果該bean已經(jīng)被解析過
    if (resolved) {
        // 使用已經(jīng)解析過的構(gòu)造函數(shù)實例化
        if (autowireNecessary) {
            return autowireConstructor(beanName, mbd, null, null);
        }
        // 使用默認無參構(gòu)造函數(shù)實例化
        else {
            return instantiateBean(beanName, mbd);
        }
    }

    // ④ 確定需要使用的構(gòu)造函數(shù)
    Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
    if (ctors != null ||
            mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
            mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args))  {
        return autowireConstructor(beanName, mbd, ctors, args);
    }

    // ⑤ 無任何的特殊處理,則使用默認的無參構(gòu)造函數(shù)實例化bean
    return instantiateBean(beanName, mbd);
}

從該方法里我們看到了Spring實例化bean的策略:

  • 工廠方法(實例工廠和靜態(tài)工廠)
  • 構(gòu)造函數(shù)實例化(無參構(gòu)造和有參構(gòu)造)
  • 通過實例提供者實例化(Spring5新增的實例化策略)

先從最簡單的無參構(gòu)造函數(shù)實例化分析致讥,因為其他的實例化策略,如有參構(gòu)造函數(shù)實例化會涉及到構(gòu)造函數(shù)解析器赞,該過程也是非常復(fù)雜垢袱,所以先分析最簡單的無參構(gòu)造函數(shù)實例化。也就是createBeanInstance方法中的第五步港柜,歷經(jīng)前幾個步驟的處理之后仍然無法實例化bean并返回其實例的話请契,那么就采用默認構(gòu)造函數(shù)實例化。

2.默認構(gòu)造函數(shù)實例化Bean
protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) {
    try {
        Object beanInstance;
        final BeanFactory parent = this;
        // 1夏醉、如果權(quán)限管理器不為空,需要校驗
        if (System.getSecurityManager() != null) {
            beanInstance = AccessController.doPrivileged((PrivilegedAction<Object>) () ->
                    getInstantiationStrategy().instantiate(mbd, beanName, parent),
                    getAccessControlContext());
        }
        else {
            // 2爽锥、獲取實例化策略并實例化bean
            beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent);
        }
        // 3、實例并初始化BeanWrapper對象
        BeanWrapper bw = new BeanWrapperImpl(beanInstance);
        initBeanWrapper(bw);
        return bw;
    }
    catch (Throwable ex) {
        throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex);
    }
}
  • 獲取實例化策略
    創(chuàng)建一個類的實例對象除了通過new關(guān)鍵字之外畔柔,還可以通過JDK的反射機制
    CGLIB動態(tài)代理來創(chuàng)建對象實例氯夷,這也是Spring實例化bean的兩種策略。所以首先通過getInstantiationStrategy方法來獲取實例化bean的策略靶擦。從下圖中可以看到腮考,如果無特殊配置,Spring將采用CGLIB動態(tài)代理機制作為實例化bean的默認策略奢啥。
image.png
  • 反射機制和CGLIB使用時機
    Spring何時使用反射何時使用CGLIB創(chuàng)建bean的實例呢秸仙?答案很簡單嘴拢,如果沒有使用方法覆蓋(replace-method或lookup-method注入),則直接使用反射創(chuàng)建bean的實例桩盲;否則必須使用CGLIB機制。Spring通過instantiate方法來確定具體使用哪種機制席吴。
3. instantiate方法獲取實例化機制
public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
    // 1赌结、如果沒有使用方法覆蓋(replace-method或lookup-method注入),則直接使用反射創(chuàng)建bean的實例
    if (!bd.hasMethodOverrides()) {
        Constructor<?> constructorToUse;
        synchronized (bd.constructorArgumentLock) {
            // 嘗試獲取已經(jīng)解析的構(gòu)造方法
            constructorToUse = (Constructor<?>) bd.resolvedConstructorOrFactoryMethod;
            if (constructorToUse == null) {
                final Class<?> clazz = bd.getBeanClass();
                if (clazz.isInterface()) {
                    throw new BeanInstantiationException(clazz, "Specified class is an interface");
                }
                try {
                    if (System.getSecurityManager() != null) {
                        constructorToUse = AccessController.doPrivileged((PrivilegedExceptionAction<Constructor<?>>) clazz::getDeclaredConstructor);
                    }
                    else {
                        // 未能獲取到已經(jīng)解析過的構(gòu)造方法,則通過getDeclaredConstructor方法獲取構(gòu)造方法
                        constructorToUse = clazz.getDeclaredConstructor();
                    }
                    bd.resolvedConstructorOrFactoryMethod = constructorToUse;
                }
                catch (Throwable ex) {
                    throw new BeanInstantiationException(clazz, "No default constructor found", ex);
                }
            }
        }
        // 通過BeanUtils類實例化bean
        return BeanUtils.instantiateClass(constructorToUse);
    }
    // 2孝冒、否則必須使用CGLIB實例化策略
    else {
        return instantiateWithMethodInjection(bd, beanName, owner);
    }
}

判斷的方式很簡單柬姚,通過BeanDefinition判斷有沒有replace-method或lookup-method注入即可;如果沒有則默認使用反射機制實例化bean庄涡,否則必須使用CGLIB實例bean量承。

4.反射機制實例化bean
public static <T> T instantiateClass(Constructor<T> ctor, Object... args) throws BeanInstantiationException {
    try {
        ReflectionUtils.makeAccessible(ctor);
        // KotlinDetector,Spring5.0新增的類穴店,用于檢測Kotlin的存在和識別Kotlin類型撕捍。
        return (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ?
                KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args));
    }
    catch (InstantiationException ex) {
        throw new BeanInstantiationException(ctor, "Is it an abstract class?", ex);
    }
    catch (IllegalAccessException ex) {
        throw new BeanInstantiationException(ctor, "Is the constructor accessible?", ex);
    }
    catch (IllegalArgumentException ex) {
        throw new BeanInstantiationException(ctor, "Illegal arguments for constructor", ex);
    }
    catch (InvocationTargetException ex) {
        throw new BeanInstantiationException(ctor, "Constructor threw exception", ex.getTargetException());
    }
}

通過ctor.newInstance(args)方法創(chuàng)建了Bean的實例,后續(xù)代碼已經(jīng)屬于JDK源碼泣洞,感興趣的同學(xué)可以自行分析忧风。

5.CGLIB實例bean

打開我們之前分析的test9(測試replace-method注入)和test10(測試replace-method注入),分析CGLIB實例化球凰。傳送:07--lookup-method和replace-method注入狮腿,這里我們只分析一下replace_method方法腿宰。

@Override
protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) {
    return instantiateWithMethodInjection(bd, beanName, owner, null);
}
@Override
protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner,
        @Nullable Constructor<?> ctor, @Nullable Object... args) {
    // Must generate CGLIB subclass...
    return new CglibSubclassCreator(bd, owner).instantiate(ctor, args);
}
public Object instantiate(@Nullable Constructor<?> ctor, @Nullable Object... args) {
    // 1、生成增強子類
    Class<?> subclass = createEnhancedSubclass(this.beanDefinition);
    Object instance;
    // 2缘厢、實例化增強子類
    if (ctor == null) {
        instance = BeanUtils.instantiateClass(subclass);
    }
    else {
        try {
            Constructor<?> enhancedSubclassConstructor = subclass.getConstructor(ctor.getParameterTypes());
            instance = enhancedSubclassConstructor.newInstance(args);
        }
        catch (Exception ex) {
            throw new BeanInstantiationException(this.beanDefinition.getBeanClass(),
                    "Failed to invoke constructor for CGLIB enhanced subclass [" + subclass.getName() + "]", ex);
        }
    }
    // 3吃度、設(shè)置回調(diào)
    // SPR-10785: set callbacks directly on the instance instead of in the
    // enhanced class (via the Enhancer) in order to avoid memory leaks.
    Factory factory = (Factory) instance;
    factory.setCallbacks(new Callback[] {NoOp.INSTANCE,
            new LookupOverrideMethodInterceptor(this.beanDefinition, this.owner),
            new ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)});
    return instance;
}

對于增強子類的實例化,依然采用了jdk的反射機制昧绣。我們回到測試類中规肴。查看生成的實例。

  • 查看實例信息:


    image.png
  • 實例詳細信息


    image.png

當代碼運行到originalDog.sayHello("輸出結(jié)果已經(jīng)被替換了夜畴。拖刃。。");時贪绘,會被CglibSubclassingInstantiationStrategy類的intercept方法攔截兑牡。

public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable {
    // 1、獲取覆蓋方法信息
    ReplaceOverride ro = (ReplaceOverride) getBeanDefinition().getMethodOverrides().getOverride(method);
    Assert.state(ro != null, "ReplaceOverride not found");
    // TODO could cache if a singleton for minor performance optimization
    // 2税灌、實例化覆蓋方法
    MethodReplacer mr = this.owner.getBean(ro.getMethodReplacerBeanName(), MethodReplacer.class);
    // 3均函、調(diào)用覆蓋方法
    return mr.reimplement(obj, method, args);
}

當代碼執(zhí)行到第三步時就會調(diào)用reimplement方法了。

/**
 * @author: LiYanChao
 * @create: 2018-09-06 00:02
 */
public class ReplaceDog implements MethodReplacer {
    @Override
    public Object reimplement(Object obj, Method method, Object[] args) throws Throwable {
        System.out.println("Hello, I am a white dog...");

        Arrays.stream(args).forEach(str -> System.out.println("參數(shù):" + str));
        return obj;
    }
}
6.總結(jié)

到這里通過無參構(gòu)造方法實例化bean就分析完了菱涤,這里大家需要記住Spring實例化bean的方式以及何時使用何種方式苞也。如果使用了replace-method或lookup-method注入,則直接使用CGLIB實例化bean,否則直接使用反射實例化bean粘秆。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末如迟,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子攻走,更是在濱河造成了極大的恐慌殷勘,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件昔搂,死亡現(xiàn)場離奇詭異玲销,居然都是意外死亡,警方通過查閱死者的電腦和手機摘符,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門贤斜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人逛裤,你說我怎么就攤上這事瘩绒。” “怎么了别凹?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵草讶,是天一觀的道長。 經(jīng)常有香客問我炉菲,道長堕战,這世上最難降的妖魔是什么坤溃? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮嘱丢,結(jié)果婚禮上薪介,老公的妹妹穿的比我還像新娘。我一直安慰自己越驻,他們只是感情好汁政,可當我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缀旁,像睡著了一般记劈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上并巍,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天目木,我揣著相機與錄音,去河邊找鬼懊渡。 笑死刽射,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的剃执。 我是一名探鬼主播誓禁,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼肾档!你這毒婦竟也來了摹恰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤阁最,失蹤者是張志新(化名)和其女友劉穎戒祠,沒想到半個月后骇两,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體速种,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年低千,在試婚紗的時候發(fā)現(xiàn)自己被綠了配阵。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡示血,死狀恐怖棋傍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情难审,我是刑警寧澤瘫拣,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站告喊,受9級特大地震影響麸拄,放射性物質(zhì)發(fā)生泄漏派昧。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一拢切、第九天 我趴在偏房一處隱蔽的房頂上張望蒂萎。 院中可真熱鬧,春花似錦淮椰、人聲如沸五慈。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泻拦。三九已至,卻和暖如春忽媒,著一層夾襖步出監(jiān)牢的瞬間聪轿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工猾浦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留陆错,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓金赦,卻偏偏與公主長得像音瓷,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子夹抗,可洞房花燭夜當晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 春節(jié)期間跟一個好久不見的朋友敘舊绳慎,吃完飯壓了大半天的馬路,沒想到許久不回來漠烧,YC的冬天是格外地冷杏愤,索性找了個最近的...
    周先生的瘋?cè)嗽?/span>閱讀 239評論 0 0
  • 以后的以后珊楼,想讓我所有滿意的照片都有自己的故事,都有獨一無二的靈魂度液。 這是去年一月獨自留在大理喜洲古鎮(zhèn) 厕宗,...
    無雙小姐閱讀 1,317評論 21 19
  • 文字 踩著纖細的高雅 披著心扉的纓紅 在書頁的傳奇中漫步 那里有草綠花香 夢里都沒見過的糖果樹 書頁終于搭成了小山...
    蘭桂夫人閱讀 284評論 0 3
  • 期末考試終于考完了霹购,中午去托輔跟兒子對題佑惠,有些題做得不對,也沒怎么說他自己在哪里噼里啪啦的掉眼淚,飯也不吃了膜楷,問...
    高譽恒閱讀 197評論 0 0
  • 開相關(guān)發(fā)中總會遇到短信驗證這些操作乍丈,這周沒有來得及寫新的東西,借此分享一篇以前學(xué)習(xí)短信驗證的筆記把将,本文使用的是 M...
    躬行之閱讀 434評論 0 0