亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Sentinel源碼解析入口類和SlotChain構(gòu)建過(guò)程詳解

 更新時(shí)間:2022年09月30日 11:39:46   作者:hsfxuebao  
這篇文章主要為大家介紹了Sentinel源碼解析入口類和SlotChain構(gòu)建過(guò)程詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

1. 測(cè)試用例

我們以sentinel-demo中的sentinel-annotation-spring-aop為例,分析sentinel的源碼。核心代碼如下:

DemoController:

@RestController
public class DemoController {
    @Autowired
    private TestService service;
    @GetMapping("/foo")
    public String apiFoo(@RequestParam(required = false) Long t) throws Exception {
        if (t == null) {
            t = System.currentTimeMillis();
        }
        service.test();
        return service.hello(t);
    }
    @GetMapping("/baz/{name}")
    public String apiBaz(@PathVariable("name") String name) {
        return service.helloAnother(name);
    }
}

TestServiceImpl:

@Service
public class TestServiceImpl implements TestService {
    @Override
    @SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class})
    public void test() {
        System.out.println("Test");
    }
    @Override
    @SentinelResource(value = "hello", fallback = "helloFallback")
    public String hello(long s) {
        if (s < 0) {
            throw new IllegalArgumentException("invalid arg");
        }
        return String.format("Hello at %d", s);
    }
    @Override
    @SentinelResource(value = "helloAnother", defaultFallback = "defaultFallback",
        exceptionsToIgnore = {IllegalStateException.class})
    public String helloAnother(String name) {
        if (name == null || "bad".equals(name)) {
            throw new IllegalArgumentException("oops");
        }
        if ("foo".equals(name)) {
            throw new IllegalStateException("oops");
        }
        return "Hello, " + name;
    }
    public String helloFallback(long s, Throwable ex) {
        // Do some log here.
        ex.printStackTrace();
        return "Oops, error occurred at " + s;
    }
    public String defaultFallback() {
        System.out.println("Go to default fallback");
        return "default_fallback";
    }
}

啟動(dòng)類DemoApplication

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

在啟動(dòng)這個(gè)工程上增加參數(shù):

-Dcsp.sentinel.dashboard.server=localhost:8081 -Dproject.name=annotation-aspectj

如圖:

打開(kāi)http://localhost:8081/#/dashboard 地址,可以看到應(yīng)用已經(jīng)注冊(cè)到sentinel管理后臺(tái):

1.1 流控測(cè)試

訪問(wèn) http://localhost:19966/foo?t=188 這個(gè)鏈接,多訪問(wèn)幾次,在實(shí)時(shí)監(jiān)控頁(yè)面可以看到:

然后,我們先簡(jiǎn)單配置一個(gè)流控規(guī)則,如下:

其中,資源名為:

然后我們?cè)诳焖偎⑿耯ttp://localhost:19966/foo?t=188 接口,會(huì)出現(xiàn)限流的情況,返回如下:

Oops, error occurred at 188

實(shí)時(shí)監(jiān)控為:

2. 注解版源碼分析

使用注解@SentinelResource 核心原理就是 利用AOP切入到方法中,我們直接看SentinelResourceAspect類,這是一個(gè)切面類:

@Aspect // 切面
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {
    // 指定切入點(diǎn)為@SentinelResource 注解
    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    public void sentinelResourceAnnotationPointcut() {
    }
    // 環(huán)繞通知
    @Around("sentinelResourceAnnotationPointcut()")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
        Method originMethod = resolveMethod(pjp);
        SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
        if (annotation == null) {
            // Should not go through here.
            throw new IllegalStateException("Wrong state for SentinelResource annotation");
        }
        String resourceName = getResourceName(annotation.value(), originMethod);
        EntryType entryType = annotation.entryType();
        int resourceType = annotation.resourceType();
        Entry entry = null;
        try {
            // 要織入的,增強(qiáng)的功能
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
            // 調(diào)用目標(biāo)方法
            return pjp.proceed();
        } catch (BlockException ex) {
            return handleBlockException(pjp, annotation, ex);
        } catch (Throwable ex) {
            Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
            // The ignore list will be checked first.
            if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
                throw ex;
            }
            if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
                traceException(ex);
                return handleFallback(pjp, annotation, ex);
            }
            // No fallback function can handle the exception, so throw it out.
            throw ex;
        } finally {
            if (entry != null) {
                entry.exit(1, pjp.getArgs());
            }
        }
    }
}

核心方法SphU.entry():

public static Entry entry(String name, int resourceType, EntryType trafficType, Object[] args)
    throws BlockException {
    // 注意 第4個(gè)參數(shù)值為 1
    return Env.sph.entryWithType(name, resourceType, trafficType, 1, args);
}
@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, Object[] args)
    throws BlockException {
    // count 參數(shù):表示當(dāng)前請(qǐng)求可以增加多少個(gè)計(jì)數(shù)
    // 注意 第5個(gè)參數(shù)為false
    return entryWithType(name, resourceType, entryType, count, false, args);
}
@Override
public Entry entryWithType(String name, int resourceType, EntryType entryType, int count, boolean prioritized,
                           Object[] args) throws BlockException {
    // 將信息封裝為一個(gè)資源對(duì)象
    StringResourceWrapper resource = new StringResourceWrapper(name, entryType, resourceType);
    // 返回一個(gè)資源操作對(duì)象entry
    // prioritized 為true 表示當(dāng)前訪問(wèn)必須等待"根據(jù)其優(yōu)先級(jí)計(jì)算出的時(shí)間"后才通過(guò)
    // prioritized 為 false 則當(dāng)前請(qǐng)求無(wú)需等待
    return entryWithPriority(resource, count, prioritized, args);
}

我們重點(diǎn)看一下CtSph#entryWithPriority

/**
 * @param resourceWrapper
 * @param count 默認(rèn)為1
 * @param prioritized 默認(rèn)為false
 * @param args
 * @return
 * @throws BlockException
 */
private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
    throws BlockException {
    // 從ThreadLocal中獲取Context
    // 一個(gè)請(qǐng)求會(huì)占用一個(gè)線程,一個(gè)線程會(huì)綁定一個(gè)context
    Context context = ContextUtil.getContext();
    // 若context是 NullContext類型,則表示當(dāng)前系統(tǒng)中的context數(shù)量已經(jīng)超過(guò)閾值
    // 即訪問(wèn)的請(qǐng)求的數(shù)量已經(jīng)超出了閾值,此時(shí)直接返回一個(gè)無(wú)需做規(guī)則檢測(cè)的資源操作對(duì)象
    if (context instanceof NullContext) {
        // The {@link NullContext} indicates that the amount of context has exceeded the threshold,
        // so here init the entry only. No rule checking will be done.
        return new CtEntry(resourceWrapper, null, context);
    }
    // 當(dāng)前線程中沒(méi)有綁定context,則創(chuàng)建一個(gè)context并將其放入到Threadlocal
    if (context == null) {
        // todo Using default context.
        context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
    }
    // Global switch is close, no rule checking will do.
    // 若全局開(kāi)關(guān)是關(guān)閉的,直接返回一個(gè)無(wú)需做規(guī)則檢測(cè)的資源操作對(duì)象
    if (!Constants.ON) {
        return new CtEntry(resourceWrapper, null, context);
    }
    // todo 查找SlotChain
    ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);
    /*
     * Means amount of resources (slot chain) exceeds {@link Constants.MAX_SLOT_CHAIN_SIZE},
     * so no rule checking will be done.
     */
    // 若沒(méi)有知道chain,則意味著chain數(shù)量超出了閾值
    if (chain == null) {
        return new CtEntry(resourceWrapper, null, context);
    }
    // 創(chuàng)建一個(gè)資源操作對(duì)象
    Entry e = new CtEntry(resourceWrapper, chain, context);
    try {
        // todo 對(duì)資源進(jìn)行操作
        chain.entry(context, resourceWrapper, null, count, prioritized, args);
    } catch (BlockException e1) {
        e.exit(count, args);
        throw e1;
    } catch (Throwable e1) {
        // This should not happen, unless there are errors existing in Sentinel internal.
        RecordLog.info("Sentinel unexpected exception", e1);
    }
    return e;
}

2.1 默認(rèn)Context創(chuàng)建

當(dāng)前線程沒(méi)有綁定Context,則創(chuàng)建一個(gè)context并將其放入到Threadlocal。核心方法為 InternalContextUtil.internalEnter

public static Context enter(String name, String origin) {
    if (Constants.CONTEXT_DEFAULT_NAME.equals(name)) {
        throw new ContextNameDefineException(
            "The " + Constants.CONTEXT_DEFAULT_NAME + " can't be permit to defined!");
    }
    return trueEnter(name, origin);
}
protected static Context trueEnter(String name, String origin) {
    // 嘗試從ThreadLocal中獲取context
    Context context = contextHolder.get();
    // 若Threadlocal中沒(méi)有,則嘗試從緩存map中獲取
    if (context == null) {
        // 緩存map的key為context名稱,value為EntranceNode
        Map<String, DefaultNode> localCacheNameMap = contextNameNodeMap;
        // DCL 雙重檢測(cè)鎖,防止并發(fā)創(chuàng)建對(duì)象
        DefaultNode node = localCacheNameMap.get(name);
        if (node == null) {
            // 若緩存map的size 大于 context數(shù)量的最大閾值,則直接返回NULL_CONTEXT
            if (localCacheNameMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                setNullContext();
                return NULL_CONTEXT;
            } else {
                LOCK.lock();
                try {
                    node = contextNameNodeMap.get(name);
                    if (node == null) {
                        if (contextNameNodeMap.size() > Constants.MAX_CONTEXT_NAME_SIZE) {
                            setNullContext();
                            return NULL_CONTEXT;
                        } else {
                            // 創(chuàng)建一個(gè)EntranceNode
                            node = new EntranceNode(new StringResourceWrapper(name, EntryType.IN), null);
                            // Add entrance node.
                            // 將新建的node添加到Root
                            Constants.ROOT.addChild(node);
                            // 將新建的node寫(xiě)入到緩存map
                            // 為了防止"迭代穩(wěn)定性問(wèn)題"-iterate stable 對(duì)于共享集合的寫(xiě)操作
                            Map<String, DefaultNode> newMap = new HashMap<>(contextNameNodeMap.size() + 1);
                            newMap.putAll(contextNameNodeMap);
                            newMap.put(name, node);
                            contextNameNodeMap = newMap;
                        }
                    }
                } finally {
                    LOCK.unlock();
                }
            }
        }
        // 將context的name與entranceNode 封裝成context
        context = new Context(node, name);
        // 初始化context的來(lái)源
        context.setOrigin(origin);
        // 將context寫(xiě)入到ThreadLocal
        contextHolder.set(context);
    }
    return context;
}

注意:因?yàn)?private static volatile Map<String, DefaultNode> contextNameNodeMap = new HashMap<>();是 HashMap結(jié)構(gòu),所以存在并發(fā)安全問(wèn)題,采用 代碼中方式進(jìn)行添加操作。

2.2 查找并創(chuàng)建SlotChain

構(gòu)建調(diào)用鏈lookProcessChain(resourceWrapper)

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    // 緩存map的key為資源 value為其相關(guān)的SlotChain
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);
    // DCL
    // 若緩存中沒(méi)有相關(guān)的SlotChain 則創(chuàng)建一個(gè)并放入到緩存中
    if (chain == null) {
        synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // Entry size limit.
                // 緩存map的size 大于 chain數(shù)量的最大閾值,則直接返回null,不在創(chuàng)建新的chain
                if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                    return null;
                }
                // todo 創(chuàng)建新的chain
                chain = SlotChainProvider.newSlotChain();
                // 防止 迭代穩(wěn)定性問(wèn)題
                Map<ResourceWrapper, ProcessorSlotChain> newMap = new HashMap<ResourceWrapper, ProcessorSlotChain>(
                    chainMap.size() + 1);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
    return chain;
}

我們直接看核心方法SlotChainProvider.newSlotChain();

public static ProcessorSlotChain newSlotChain() {
        // 若builder不為null,則直接使用builder構(gòu)建一個(gè)chain
        // 否則先創(chuàng)建一個(gè)builder
        if (slotChainBuilder != null) {
            return slotChainBuilder.build();
        }
        // Resolve the slot chain builder SPI.
        // 通過(guò)SPI方式創(chuàng)建builder
        slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();
        // 若通過(guò)SPI未能創(chuàng)建builder,則創(chuàng)建一個(gè)默認(rèn)的DefaultSlotChainBuilder
        if (slotChainBuilder == null) {
            // Should not go through here.
            RecordLog.warn("[SlotChainProvider] Wrong state when resolving slot chain builder, using default");
            slotChainBuilder = new DefaultSlotChainBuilder();
        } else {
            RecordLog.info("[SlotChainProvider] Global slot chain builder resolved: {}",
                slotChainBuilder.getClass().getCanonicalName());
        }
        // todo 構(gòu)建一個(gè)chain
        return slotChainBuilder.build();
    }
    private SlotChainProvider() {}
}

2.2.1 創(chuàng)建slotChainBuilder

// 通過(guò)SPI方式創(chuàng)建builder
slotChainBuilder = SpiLoader.of(SlotChainBuilder.class).loadFirstInstanceOrDefault();

通過(guò)SPI方法創(chuàng)建slotChainBuilder,去項(xiàng)目中META-INF.service中獲取:

2.2.2 slotChainBuilder.build()

@Spi(isDefault = true)
public class DefaultSlotChainBuilder implements SlotChainBuilder {
    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();
        // 通過(guò)SPI方式構(gòu)建Slot
        List<ProcessorSlot> sortedSlotList = SpiLoader.of(ProcessorSlot.class).loadInstanceListSorted();
        for (ProcessorSlot slot : sortedSlotList) {
            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
                RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() + ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
                continue;
            }
            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }
        return chain;
    }
}

通過(guò)SPI機(jī)制,去項(xiàng)目中META-INF.service中獲取,在sentinel-core項(xiàng)目中:

還有一個(gè)ParamFlowSlot,在sentinel-extension/sentinel-parameter-flow-control下:

我們點(diǎn)擊 NodeSelectorSlot, 類上面是有 優(yōu)先級(jí)order,數(shù)字越小,優(yōu)先級(jí)越高。

@Spi(isSingleton = false, order = Constants.ORDER_NODE_SELECTOR_SLOT)
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot&lt;Object&gt; {

優(yōu)先級(jí)常量為:

public static final int ORDER_NODE_SELECTOR_SLOT = -10000;
public static final int ORDER_CLUSTER_BUILDER_SLOT = -9000;
public static final int ORDER_LOG_SLOT = -8000;
public static final int ORDER_STATISTIC_SLOT = -7000;
public static final int ORDER_AUTHORITY_SLOT = -6000;
public static final int ORDER_SYSTEM_SLOT = -5000;
public static final int ORDER_FLOW_SLOT = -2000;
public static final int ORDER_DEGRADE_SLOT = -1000;

我們看代碼中的變量sortedSlotList,已經(jīng)按照優(yōu)先級(jí)排序好了:

我們看一下構(gòu)建的ProcessorSlotChain,類似一個(gè)單鏈表結(jié)構(gòu),如下:

我們看一下相關(guān)的類結(jié)構(gòu):DefaultProcessorSlotChain:

// 這是一個(gè)單向鏈表,默認(rèn)包含一個(gè)接節(jié)點(diǎn),且有兩個(gè)指針first 和end同時(shí)指向這個(gè)節(jié)點(diǎn)
public class DefaultProcessorSlotChain extends ProcessorSlotChain {
    AbstractLinkedProcessorSlot<?> first = new AbstractLinkedProcessorSlot<Object>() {
        @Override
        public void entry(Context context, ResourceWrapper resourceWrapper, Object t, int count, boolean prioritized, Object... args)
            throws Throwable {
            super.fireEntry(context, resourceWrapper, t, count, prioritized, args);
        }
        @Override
        public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
            super.fireExit(context, resourceWrapper, count, args);
        }
    };
    AbstractLinkedProcessorSlot<?> end = first;
    @Override
    public void addFirst(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        protocolProcessor.setNext(first.getNext());
        first.setNext(protocolProcessor);
        if (end == first) {
            end = protocolProcessor;
        }
    }
    @Override
    public void addLast(AbstractLinkedProcessorSlot<?> protocolProcessor) {
        end.setNext(protocolProcessor);
        end = protocolProcessor;
    }
}

AbstractLinkedProcessorSlot:

public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {
    // 聲明一個(gè)同類型的變量,則可以指向下一個(gè)Slot節(jié)點(diǎn)
    private AbstractLinkedProcessorSlot<?> next = null;
    @Override
    public void fireEntry(Context context, ResourceWrapper resourceWrapper, Object obj, int count, boolean prioritized, Object... args)
        throws Throwable {
        if (next != null) {
            next.transformEntry(context, resourceWrapper, obj, count, prioritized, args);
        }
    }
    @SuppressWarnings("unchecked")
    void transformEntry(Context context, ResourceWrapper resourceWrapper, Object o, int count, boolean prioritized, Object... args)
        throws Throwable {
        T t = (T)o;
        entry(context, resourceWrapper, t, count, prioritized, args);
    }
    @Override
    public void fireExit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        if (next != null) {
            next.exit(context, resourceWrapper, count, args);
        }
    }
    public AbstractLinkedProcessorSlot<?> getNext() {
        return next;
    }
    public void setNext(AbstractLinkedProcessorSlot<?> next) {
        this.next = next;
    }
}

構(gòu)建完成后的SlotChain和工作原理圖一樣:

接下來(lái),對(duì)資源進(jìn)行操作的核心方法為chain.entry(context, resourceWrapper, null, count, prioritized, args);,這個(gè)我們下篇文章分析。

參考文章

Sentinel1.8.5源碼github地址(注釋)

Sentinel官網(wǎng)

以上就是Sentinel源碼解析入口類和SlotChain構(gòu)建過(guò)程詳解的詳細(xì)內(nèi)容,更多關(guān)于Sentinel入口類SlotChain構(gòu)建的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java多線程通信:交替打印ABAB實(shí)例

    Java多線程通信:交替打印ABAB實(shí)例

    這篇文章主要介紹了Java多線程通信:交替打印ABAB實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2020-08-08
  • Maven配置文件修改及導(dǎo)入第三方j(luò)ar包的實(shí)現(xiàn)

    Maven配置文件修改及導(dǎo)入第三方j(luò)ar包的實(shí)現(xiàn)

    本文主要介紹了Maven配置文件修改及導(dǎo)入第三方j(luò)ar包的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2023-08-08
  • spring cloud gateway 全局過(guò)濾器的實(shí)現(xiàn)

    spring cloud gateway 全局過(guò)濾器的實(shí)現(xiàn)

    全局過(guò)濾器作用于所有的路由,不需要單獨(dú)配置,我們可以用它來(lái)實(shí)現(xiàn)很多統(tǒng)一化處理的業(yè)務(wù)需求,這篇文章主要介紹了spring cloud gateway 全局過(guò)濾器的實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2019-03-03
  • java中DelayQueue實(shí)例用法詳解

    java中DelayQueue實(shí)例用法詳解

    在本篇內(nèi)容里小編給大家分享的是一篇關(guān)于java中DelayQueue實(shí)例用法詳解內(nèi)容,有需要的朋友們可以跟著學(xué)習(xí)下。
    2021-01-01
  • Arrays.sort(arr)是什么排序及代碼邏輯

    Arrays.sort(arr)是什么排序及代碼邏輯

    在學(xué)習(xí)過(guò)程中觀察到Arrays.sort(arr)算法可以直接進(jìn)行排序,但不清楚底層的代碼邏輯是什么樣子,今天通過(guò)本文給大家介紹下Arrays.sort(arr)是什么排序,感興趣的朋友一起看看吧
    2022-02-02
  • IDEA安裝部署Alibaba Cloud Toolkit的實(shí)現(xiàn)步驟

    IDEA安裝部署Alibaba Cloud Toolkit的實(shí)現(xiàn)步驟

    Alibaba Cloud Toolkit是阿里云針對(duì)IDE平臺(tái)為開(kāi)發(fā)者提供的一款插件,本文主要介紹了IDEA安裝部署Alibaba Cloud Toolkit的實(shí)現(xiàn)步驟,具有一定的參考價(jià)值,感興趣的可以了解一下
    2023-08-08
  • idea手動(dòng)執(zhí)行maven命令的三種實(shí)現(xiàn)方式

    idea手動(dòng)執(zhí)行maven命令的三種實(shí)現(xiàn)方式

    這篇文章主要介紹了idea手動(dòng)執(zhí)行maven命令的三種實(shí)現(xiàn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2023-08-08
  • 使用SpringBoot項(xiàng)目導(dǎo)入openfeign版本的問(wèn)題

    使用SpringBoot項(xiàng)目導(dǎo)入openfeign版本的問(wèn)題

    這篇文章主要介紹了使用SpringBoot項(xiàng)目導(dǎo)入openfeign版本的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • Java漢字轉(zhuǎn)拼音工具類完整代碼實(shí)例

    Java漢字轉(zhuǎn)拼音工具類完整代碼實(shí)例

    這篇文章主要介紹了java漢字轉(zhuǎn)拼音工具類完整代碼實(shí)例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-03-03
  • java編程之遞歸算法總結(jié)

    java編程之遞歸算法總結(jié)

    這篇文章主要介紹了java編程之遞歸算法總結(jié),具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-11-11

最新評(píng)論