代码编织梦想

3.2.2.1 cache-ref节点解析
private void cacheRefElement(XNode context) {
    if (context != null) {
       // 取出该节点的属性值namespace,并存到configuration中,内部是一个HashMap集合,看如下`关联1`
      configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
       // 创建 CacheRefResolver 对象,看如下`关联2`
      CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
      try {
        // 解析cache-ref后,从configuration中拿到namespace对应的cache,作为当前缓存使用,看如下`关联3`
        cacheRefResolver.resolveCacheRef();
      } catch (IncompleteElementException e) {
        // 解析失败,将其添加到未完成列表中,内部是LinkedList实现,就是章节`3.2.1`中Pending要清除的
        configuration.addIncompleteCacheRef(cacheRefResolver);
      }
    }
  }
// 关联1:存放cache-ref的配置值
public void addCacheRef(String namespace, String referencedNamespace) {
    cacheRefMap.put(namespace, referencedNamespace);
}
// 关联2:构造函数中就是普通赋值
public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) {
    this.assistant = assistant;
    this.cacheRefNamespace = cacheRefNamespace;
}
// 关联3
public Cache resolveCacheRef() {
    return assistant.useCacheRef(cacheRefNamespace);
}
public Cache useCacheRef(String namespace) {
    if (namespace == null) { // 判空
        throw new BuilderException("cache-ref element requires a namespace attribute.");
    }
    try {
        unresolvedCacheRef = true;
        // 从 configuration中取出namespace对应的cache,内部也是HashMap集合存放
        Cache cache = configuration.getCache(namespace);
        if (cache == null) { // 再次判空
            throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
        }
        // 作为当前缓存对象
        currentCache = cache;
        unresolvedCacheRef = false;
        return cache;
    } catch (IllegalArgumentException e) {
        throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
    }
}
3.2.2.2 cache节点解析
private void cacheElement(XNode context) {
    if (context != null) {
        // type表示自定义cache实现类
        String type = context.getStringAttribute("type", "PERPETUAL");
        // 判断是不是别名,并解析
        Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
        // 缓存清除策略,默认LRU
        String eviction = context.getStringAttribute("eviction", "LRU");
        /* 判断是不是别名,并解析 eviction 策略对应的实现类,Mybatis默认4个实现方式FifoCache、LruCache、SoftCache、WeakCache,分别对应FIFO、LRU、SOFT、WEAK策略
         LRU – 最近最少使用:移除最长时间不被使用的对象。
		FIFO – 先进先出:按对象进入缓存的顺序来移除它们。
		SOFT – 软引用:基于垃圾回收器状态和软引用规则移除对象。
		WEAK – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象。
        */
        Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
        // flushInterval(刷新间隔)
        Long flushInterval = context.getLongAttribute("flushInterval");
        // size(引用数目)属性可以被设置为任意正整数,要注意缓存对象的大小和运行环境中可用的内存资源。默认值是 1024
        Integer size = context.getIntAttribute("size");
        // readOnly(只读)属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
        boolean readWrite = !context.getBooleanAttribute("readOnly", false);
        boolean blocking = context.getBooleanAttribute("blocking", false);
        // 解析cache节点下的子节点property,并全部解析为属性信息
        Properties props = context.getChildrenAsProperties();
        // 构建缓存对象Cache
        builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
    }
}

构建缓存对象

public Cache useNewCache(Class<? extends Cache> typeClass,
      Class<? extends Cache> evictionClass,
      Long flushInterval,
      Integer size,
      boolean readWrite,
      boolean blocking,
      Properties props) {
    // 通过命名空间作为id,来唯一标识缓存范围
    Cache cache = new CacheBuilder(currentNamespace)
        // 设置缓存实现类
        .implementation(valueOrDefault(typeClass, PerpetualCache.class))
        // 设置缓存清除策略实现类
        .addDecorator(valueOrDefault(evictionClass, LruCache.class))
        // 设置刷新间隔时间
        .clearInterval(flushInterval)
        // 设置缓存对象数
        .size(size)
        // 缓存是否读写
        .readWrite(readWrite)
        // 这个值用在多线程情况,用到时再讲
        .blocking(blocking)
        // 设置属性
        .properties(props)
        .build(); // 真正构建Cache的地方,继续往下
    // 将创建的 cache 对象保存到 configuration 的 HashMap中,key 就是 命名空间,value 就是 cache
    configuration.addCache(cache);
    currentCache = cache; // 设置当前使用的缓存对象
    return cache; // 返回
  }

CacheBuilder构建Cache

public Cache build() {
    // 如何用户没有自定义Cache实现类,就用默认的PerpetualCache
    setDefaultImplementations();
    // 这里就是通过反射创建 implementation 对应的类的对象
    Cache cache = newBaseCacheInstance(implementation, id);
    // 检验cache的setter方法,并把对应的setter方法通过属性名称,将属性值设置进去,也就是赋值
    setCacheProperties(cache);
    // issue #352, do not apply decorators to custom caches
    // 下面这一段代码就是分别区分自定义缓存实现和默认缓存实现,并通过装饰器模式,强化缓存的实现,也就是说,不管是默认的缓存实现PerpetualCache,还是自定义的,都得在外再包一层或多层Mybatis实现的缓存
    if (PerpetualCache.class.equals(cache.getClass())) {
        for (Class<? extends Cache> decorator : decorators) {
            // Mybatis自带的用于装饰的缓存实现,构建函数都需要传入一个被包装的缓存实现,所以这里要传入cache并反射创建装饰的缓存实现
            cache = newCacheDecoratorInstance(decorator, cache);
            setCacheProperties(cache); // 设置属性
        }
        // Mybatis自身通过装饰器再包一层,具体就在这实现,看`关联1`
        cache = setStandardDecorators(cache);
    } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
        // 如果自定义的类不是LoggingCache了类,那得用 LoggingCache 装饰一层
        cache = new LoggingCache(cache); 
    }
    // 返回最终的cache
    return cache;
}

private void setDefaultImplementations() {
    if (implementation == null) {
        implementation = PerpetualCache.class;
        if (decorators.isEmpty()) {
            decorators.add(LruCache.class);
        }
    }
}

// 关联1
private Cache setStandardDecorators(Cache cache) {
    try {
        MetaObject metaCache = SystemMetaObject.forObject(cache);
        // 设置size
        if (size != null && metaCache.hasSetter("size")) {
            metaCache.setValue("size", size);
        }
        // 设置 clearInterval
        if (clearInterval != null) {
            cache = new ScheduledCache(cache);
            ((ScheduledCache) cache).setClearInterval(clearInterval);
        }
        // 可读写缓存,用SerializedCache装饰一层
        if (readWrite) {
            cache = new SerializedCache(cache);
        }
        // 再用LogginCache装饰一层
        cache = new LoggingCache(cache);
        // 再用 SynchronizedCache 装饰一层
        cache = new SynchronizedCache(cache);
        // blocking 为 true,再用 BlockingCache 装饰一层
        if (blocking) { 
            cache = new BlockingCache(cache);
        }
        // 返回最外层的cache
        return cache;
    } catch (Exception e) {
        throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
    }
}
3.2.2.3 resultMap节点解析

先看看 resultMap 节点中有什么,结果映射(resultMap)

  • constructor - 用于在实例化类时,注入结果到构造方法中
    • idArg - ID 参数;标记出作为 ID 的结果可以帮助提高整体性能
    • arg - 将被注入到构造方法的一个普通结果
  • id – 一个 ID 结果;标记出作为 ID 的结果可以帮助提高整体性能
  • result – 注入到字段或 JavaBean 属性的普通结果
  • association – 一个复杂类型的关联;许多结果将包装成这种类型
    • 嵌套结果映射 – 关联可以是 resultMap 元素,或是对其它结果映射的引用
  • collection – 一个复杂类型的集合
    • 嵌套结果映射 – 集合可以是 resultMap 元素,或是对其它结果映射的引用
  • discriminator – 使用结果值来决定使用哪个resultMap
    • case – 基于某些值的结果映射
      • 嵌套结果映射 – case 也是一个结果映射,因此具有相同的结构和元素;或者引用其它的结果映射

所以,下面的代码,就是把resultMap的配置解析成 ResultMap 对象,并存放到 configuration 的 HashMap 中,由于正常生产环境的 resultMap 配置不会很复杂,但是它定义的又很复杂,解析起来,又繁琐,又枯燥,并且意义不大,所以这段代码就不一一解析了,有兴趣的读者可以自行研究。

private void resultMapElements(List<XNode> list) throws Exception {
    for (XNode resultMapNode : list) { // 遍历子节点
        try {
            // 解析节点,继续往下看
            resultMapElement(resultMapNode);
        } catch (IncompleteElementException e) {
            // ignore, it will be retried
        }
    }
}

private ResultMap resultMapElement(XNode resultMapNode) throws Exception {
    return resultMapElement(resultMapNode, Collections.emptyList(), null);
}

private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
    ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
    String type = resultMapNode.getStringAttribute("type",
                                                   resultMapNode.getStringAttribute("ofType",
                                                                                    resultMapNode.getStringAttribute("resultType",
                                                                                                                     resultMapNode.getStringAttribute("javaType"))));
    Class<?> typeClass = resolveClass(type);
    if (typeClass == null) {
        typeClass = inheritEnclosingType(resultMapNode, enclosingType);
    }
    Discriminator discriminator = null;
    List<ResultMapping> resultMappings = new ArrayList<>();
    resultMappings.addAll(additionalResultMappings);
    List<XNode> resultChildren = resultMapNode.getChildren();
    for (XNode resultChild : resultChildren) {
        if ("constructor".equals(resultChild.getName())) {
            processConstructorElement(resultChild, typeClass, resultMappings);
        } else if ("discriminator".equals(resultChild.getName())) {
            discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
        } else {
            List<ResultFlag> flags = new ArrayList<>();
            if ("id".equals(resultChild.getName())) {
                flags.add(ResultFlag.ID);
            }
            resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
        }
    }
    String id = resultMapNode.getStringAttribute("id",
                                                 resultMapNode.getValueBasedIdentifier());
    String extend = resultMapNode.getStringAttribute("extends");
    Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
    ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
    try {
        return resultMapResolver.resolve();
    } catch (IncompleteElementException  e) {
        configuration.addIncompleteResultMap(resultMapResolver);
        throw e;
    }
}
3.2.2.4 sql节点解析
private void sqlElement(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        sqlElement(list, configuration.getDatabaseId());
    }
    sqlElement(list, null);
}

private void sqlElement(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        String databaseId = context.getStringAttribute("databaseId");
        String id = context.getStringAttribute("id");
        id = builderAssistant.applyCurrentNamespace(id, false);
        if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
            // 直接看这里,没有进一步的对sql节点里的sql内容进行解析,而是直接把当前的sql节点XNode对象整个存放到 sqlFragments 这个Map中
            sqlFragments.put(id, context);
        }
    }
}

private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
    if (requiredDatabaseId != null) {
        return requiredDatabaseId.equals(databaseId);
    }
    if (databaseId != null) {
        return false;
    }
    if (!this.sqlFragments.containsKey(id)) {
        return true;
    }
    // skip this fragment if there is a previous one with a not null databaseId
    XNode context = this.sqlFragments.get(id);
    return context.getStringAttribute("databaseId") == null;
}
3.2.2.5 select|insert|update|delete解析

调用入口在 XMLMapperBuilder.java 类中

private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
        buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
}

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
        final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
        try {
            // 真正解析在这里
            statementParser.parseStatementNode();
        } catch (IncompleteElementException e) {
            configuration.addIncompleteStatement(statementParser);
        }
    }
}

真正解析操作在 XMLStatementBuilder.java 类中

public void parseStatementNode() {
    String id = context.getStringAttribute("id"); // 语句的id
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
        return;
    }
    // 节点名,用于判断 select|insert|update|delete
    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT; // 判断是否select
    // 非 select 时,flushCache 为 true,也就是说 insert|update|delete 语句的执行会刷新缓存
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    // select 时,useCache 默认为 true,将其设置为 true 后,将会导致本条语句的结果被二级缓存缓存起来,默认值:对 select 元素为 true
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    // 这个设置仅针对嵌套结果 select 语句:如果为 true,将会假设包含了嵌套结果集或是分组,当返回一个主结果行时,就不会产生对前面结果集的引用。 这就使得在获取嵌套结果集的时候不至于内存不够用。默认值:false
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // include 解析
    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());
    // parameterType 将会传入这条语句的参数的类全限定名或别名。这个属性是可选的,因为 MyBatis 可以通过类型处理器(TypeHandler)推断出具体传入语句的参数,默认值为未设置(unset)
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    // 获取 LanguageDriver,默认实现 XMLLanguageDriver,后面会用它来解析sql并生成SqlSource对象,还有一个实现是 RawLanguageDriver,具体用到时再讲
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    // selectKey 语句的解析,这个内容也比较多,专门抽出一节来讲,看章节`3.2.2.7`
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    //  SelectKeyGenerator.SELECT_KEY_SUFFIX = "!selectKey"
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    // 最终 keyStatementId = namespace + 语句id + "!selectKey"
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    // keyGenerator 在上面解析selectKey的方法 processSelectKeyNodes 中生成
    if (configuration.hasKeyGenerator(keyStatementId)) {
        // 有就取出来
        keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
        // 没有就根据 useGeneratedKeys是否为true及是否为insert语句来判断,使用 Jdbc3KeyGenerator,否则为 NoKeyGenerator
        keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                                                   configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
            ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }
    // 生成 sqlSource ,细节看章节`3.2.2.6`
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    // 开始 ========== 下面这些值,官网都有介绍,我就再写一遍吧 ==============//
    // statementType	可选 STATEMENT,PREPARED 或 CALLABLE。这会让 MyBatis 分别使用 Statement,PreparedStatement 或 CallableStatement,默认值:PREPARED。
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    // fetchSize	这是一个给驱动的建议值,尝试让驱动程序每次批量返回的结果行数等于这个设置值。 默认值为未设置(unset)(依赖驱动)
    Integer fetchSize = context.getIntAttribute("fetchSize");
    // timeout	这个设置是在抛出异常之前,驱动程序等待数据库返回请求结果的秒数。默认值为未设置(unset)(依赖数据库驱动)。
    Integer timeout = context.getIntAttribute("timeout");
    // parameterMap	用于引用外部 parameterMap 的属性,目前已被废弃。请使用行内参数映射和 parameterType 属性。
    String parameterMap = context.getStringAttribute("parameterMap");
    // resultType	期望从这条语句中返回结果的类全限定名或别名。 注意,如果返回的是集合,那应该设置为集合包含的类型,而不是集合本身的类型。 resultType 和 resultMap 之间只能同时使用一个。
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    // resultMap	对外部 resultMap 的命名引用。结果映射是 MyBatis 最强大的特性,如果你对其理解透彻,许多复杂的映射问题都能迎刃而解。 resultType 和 resultMap 之间只能同时使用一个。
    String resultMap = context.getStringAttribute("resultMap");
    // resultSetType	FORWARD_ONLY,SCROLL_SENSITIVE, SCROLL_INSENSITIVE 或 DEFAULT(等价于 unset) 中的一个,默认值为 unset (依赖数据库驱动)。
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
        resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    // keyProperty	(仅适用于 insert 和 update)指定能够唯一识别对象的属性,MyBatis 会使用 getGeneratedKeys 的返回值或 insert 语句的 selectKey 子元素设置它的值,默认值:未设置(unset)。如果生成列不止一个,可以用逗号分隔多个属性名称。
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");
    
     // ========== 下面这些值,官网都有介绍,我就再写一遍吧 ============== 结束//
    // 创建 MappedStatement 并存放到 configuration 的 HashMap 中,细节放到章节`3.2.2.6`中
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                                        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                                        resultSetTypeEnum, flushCache, useCache, resultOrdered,
                                        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}

后续将通过抖音视频/直播的形式分享技术,由于前期要做一些准备和规划,预计2024年6月开始,欢迎关注,如有需要或问题咨询,也可直接抖音沟通交流。
在这里插入图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/zhang527294844/article/details/136329600

面试经典150题——生命游戏-爱代码爱编程

​"Push yourself, because no one else is going to do it for you." - Unknown 1. 题目描述 2.  题目分析与解析 2.1 思路一——暴力求解 之所以先暴力求解,是因为我开始也没什么更好的思路,所以就先写一种解决方案,没准写着写着就来新的灵感了。暴力求解思路还是很简单

maven的下载安装配置教程-爱代码爱编程

一、简单了解一下什么是Maven Maven就是一款帮助程序员构建项目的工具,我们只需要告诉Maven需要哪些Jar 包,它会帮助我们下载所有的Jar,极大提升开发效率。 1.Maven翻译为“专家“, ”内行”的意思,是著名Apache公司下基于Java开发的开源项目。 2.Maven项目对象模型(POM)是一个项目管理工具软件,可以通过简短的中央信

【.xml文件匹配不到】⭐️解决使用mybatis-爱代码爱编程

前言         小伙伴们大家好,很快嗷,到了年后的第一周,最近在自己电脑上敲项目时遇到一个平时可能不怎么遇到的问题,就是mybatis持久层框架使用时找不到对应的xml配置文件,也就导致自己写的持久层方法报错 接口报错内容:         org.apache.ibatis.binding.BindingException: Invali

微服务-爱代码爱编程

微服务-实用篇 一、微服务治理1.微服务远程调用2.Eureka注册中心Eureka的作用:搭建EurekaServer服务Client服务注册服务发现Ribbon负载均衡策略配置Ribbon配置饥饿加载

【简写mybatis】02-爱代码爱编程

前言 注意: 学习源码一定一定不要太关注代码的编写,而是注意代码实现思想: 通过设问方式来体现代码中的思想;方法:5W+1H 源代码:https://gitee.com/xbhog/mybatis-xbh

java之线程池:线程池常用类、接口;线程池执行流程,配置参数,分类-爱代码爱编程

线程池 什么是线程池? 线程池:一种基于池化思想管理和使用线程的机制 线程池常用类和接口 ExecutorService接口:进行线程池的操作访问Executors类:创建线程池的工具类ThreadPoolExe

浅谈 linux fork 函数-爱代码爱编程

文章目录 前言fork 基本概念代码演示示例1:体会 fork 函数返回值的作用示例2:创建多进程,加深对 fork 函数的理解 前言 本篇介绍 fork 函数。 fork 基本概念 pi

题目 1317: 最长公共子序列lcs-爱代码爱编程

题目描述: 一个字符串A的子串被定义成从A中顺次选出若干个字符构成的串。如A=“cdaad" ,顺次选1,3,5个字符就构成子串" cad" ,现给定两个字符串,求它们的最长共公子串。 代码: package lanqiao; import java.util.*; public class Main { public static vo

java多态-爱代码爱编程

Java中的多态是面向对象编程的核心概念之一,它允许对象采取多种形式。多态主要有两个条件:继承和方法重写(覆盖)。通过这两个条件,Java能够实现运行时多态和编译时多态。 运行时多态(动态多态) 运行时多态是通过方

gdpu java 天码行空 1-爱代码爱编程

💖 配置环境 👨‍🏫 JDK17 配置教程 🌸 CMD 查看本机 JDK 版本命令: java -version 1. 输出 Hello World! (1) 新建 Java 文件 文件名:HelloWor

idea开发环境热部署-爱代码爱编程

开发环境热部署 在实际的项目开发调试过程中会频繁地修改后台类文件,导致需要重新编译重新启动,整个过程非常麻烦,影响开发效率。Spring Boot提供了spring-boot-devtools组件,使得无须手动重启SpringBoot应用即可重新编译、启动项目,大大缩短编译启动的时间。devtools会监听classpath下的文件变动,触发Restar

springcloud微服务-爱代码爱编程

Nacos注册中心(快速入门) 文章目录 Nacos注册中心(快速入门)1、认识Nacos并安装2、服务注册到Nacos 1、认识Nacos并安装 Nacos是阿里巴巴的产品,现在是Spri