nacos源码分析-服务注册(客户端)

news2025/1/18 3:45:26

前言

一直都想写SpringCloudAlibaba的源码分析,终于开始动手第一篇了,如果想要看懂Nacos源码至少要把《SpringBoot自动》配置看了,不然就是看天书。本篇文章呢带大家一起来看一下Nacos-Client 客户端服务注册这一部分的源码。

基础环境

首先需要下载一个Nacos-server,也就是注册中心了,顺便把源码也下载了, 后面我们在分析服务端的时候会用到, 下载地址 ,https://github.com/alibaba/nacos/releases/tag/1.4.3
在这里插入图片描述
下周好之后,把nacos-server解压启动,进入 bin目录,cmd执行 startup.cmd -m standalone ,代表单实例启动。如果嫌麻烦可以把 startup.cmd 中的 set MODE=“cluster” 改成 standalone ,以后就直接启动startup不再加参数了。在这里插入图片描述
接下来是客户端了,我的项目结构如下:

在这里插入图片描述
我这里使用的SpringBoot版本是2.2.5 ; alibaba的版本是 2.2.1 ;父工程pom.xml管理如下

 <parent>
        <groupId> org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>


    <dependencyManagement>
        <dependencies>
            <!--SpringClou-alibaba依赖-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.2.1.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!--SpringCloud依赖-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR3</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

nacos-client 依赖如下

    <dependencies>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

启动类如下

//Ncaos客户端应用程序启动
@SpringBootApplication
public class NacosClientStarter {

    public static void main(String[] args) {
        SpringApplication.run(NacosClientStarter.class);
    }
}

配置文件中配置了一个服务名和 nacos注册中心地址

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application:
    name: nacos-client

然后启动项目,nacos-client就会被注册到Nacos注册中心,如下:

在这里插入图片描述
OK到这里基础环境准备好了,相信对于你来说也是比较容易的事情。

服务注册源码分析

Nacos和Eureka都拥有服务注册,服务发现,心跳续约等功能,本篇文章主要讨论服务注册,也就是客户端启动时会主动向服务端注册自己,那么在服务端会形成一个服务注册表(其实就是一个Map)下面我们就来研究一下服务注册的流程是如何的。

对SpringBoot有一定研究的同学都清楚SpringBoot的自动配置,SpringCloud或者说SpringCloudAlibaba的全是基于SpringBoot的自动配置来完成相关组件的装载。在External libraries 中找到我们导入的依赖 spring-cloud-starter-alibaba-nacos-discovery

在这里插入图片描述
然后展开找到META-INF/spring.factories 文件,内容如下:

在这里插入图片描述
NacosServerRegistryAutoConfiguration 是针对于服务注册的自动配置类。程序启动该类中的配置自动生效。除了该配置类,文件中还包括针对于服务发现的配置类和 config 的自动配置类。

NacosServerRegistryAutoConfiguration 服务注册自动配置

下面是NacosServerRegistryAutoConfiguration 的源码

@Configuration(
    proxyBeanMethods = false
)
@EnableConfigurationProperties
@ConditionalOnNacosDiscoveryEnabled
//条件开关
@ConditionalOnProperty(
    value = {"spring.cloud.service-registry.auto-registration.enabled"},
    matchIfMissing = true
)
@AutoConfigureAfter({AutoServiceRegistrationConfiguration.class, AutoServiceRegistrationAutoConfiguration.class, NacosDiscoveryAutoConfiguration.class})
public class NacosServiceRegistryAutoConfiguration {
    public NacosServiceRegistryAutoConfiguration() {
    }

    @Bean
    public NacosServiceRegistry nacosServiceRegistry(NacosDiscoveryProperties nacosDiscoveryProperties) {
        return new NacosServiceRegistry(nacosDiscoveryProperties);
    }

    @Bean
    @ConditionalOnBean({AutoServiceRegistrationProperties.class})
    public NacosRegistration nacosRegistration(NacosDiscoveryProperties nacosDiscoveryProperties, ApplicationContext context) {
        return new NacosRegistration(nacosDiscoveryProperties, context);
    }
	//开启自动注册
    @Bean
    @ConditionalOnBean({AutoServiceRegistrationProperties.class})
    public NacosAutoServiceRegistration nacosAutoServiceRegistration(NacosServiceRegistry registry, AutoServiceRegistrationProperties autoServiceRegistrationProperties, NacosRegistration registration) {
        return new NacosAutoServiceRegistration(registry, autoServiceRegistrationProperties, registration);
    }
}

  • spring.cloud.service-registry.auto-registration.enabled:自动注册条件开关,默认是true

  • NacosServiceRegistry : Nacos服务注册器,客户端往服务端发送注册请求的整个过程是通过它来完成的,它实现于 ServiceRegistry 接口(EurekaClient也是的哦) 。复写了register服务注册方法。

  • NacosRegistration :可以看做是对要注册是服务的等级,其中包括服务的实例ID,注册地址等等

  • NacosAutoServiceRegistration :自动注册器,服务注册由它来触发的

  • NacosDiscoveryProperties :针对于Nacos的配置属性对象

  • NacosRegistration :根据nacos配置创建的注册的服务的登记对象,其中包含了注册的服务的serviceId,host,port等等

NacosAutoServiceRegistration 自动注册

NacosAutoServiceRegistration 继承了 AbstractAutoServiceRegistration,通过监听 WebServerInitializedEvent web服务初始化事件来调用 NacosAutoServiceRegistration#register 触发自动注册。该方法中会判断 register-enabled: true 开关来决定是否注册。如果开启自动注册,就会最终来调用 NacosServiceRegistry#register 方法触发服务注册。
在这里插入图片描述
start()方法内部会调用 NacosAutoServiceRegistration #register方法
在这里插入图片描述
而NacosAutoServiceRegistration #register 做了是否开启注册配置(spring.cloud.nacos.discovery.register-enabled=true)开关后,就调用super的注册方法注册,下面是 AbstractAutoServiceRegistration#register源码

protected void register() {
	this.serviceRegistry.register(getRegistration());
}

这里的 serviceRegistry 就是最前面说到的 NacosServiceRegistry ,核心服务注册流程就在他里面。

NacosServiceRegistry 服务注册流程

下面是 NacosServiceRegistry#register 的源码

@Override
public void register(Registration registration) {
	//判断是否有服务ID,也就是spring.application.name配置不能为空
	if (StringUtils.isEmpty(registration.getServiceId())) {
		log.warn("No service to register for nacos client...");
		return;
	}
	//拿到服务ID
	String serviceId = registration.getServiceId();
	//拿到服务的组名
	String group = nacosDiscoveryProperties.getGroup();
	//封装好的服务实例
	Instance instance = getNacosInstanceFromRegistration(registration);

	try {
		//注册实例
		namingService.registerInstance(serviceId, group, instance);
		log.info("nacos registry, {} {} {}:{} register finished", group, serviceId,
				instance.getIp(), instance.getPort());
	}
	catch (Exception e) {
		log.error("nacos registry, {} register failed...{},", serviceId,
				registration.toString(), e);
		// rethrow a RuntimeException if the registration is failed.
		// issue : https://github.com/alibaba/spring-cloud-alibaba/issues/1132
		rethrowRuntimeException(e);
	}
}

该方法会先判断服务 serviceId也就是服务名不能为空,然后拿到 group和Instance(服务实例对象),调用 NamingService#registerInstance来注册服务。下面是 NacosNamingService#registerInstance的源码

public void registerInstance(String serviceName, String groupName, Instance instance) throws NacosException {
        if (instance.isEphemeral()) {
            BeatInfo beatInfo = new BeatInfo();
            beatInfo.setServiceName(NamingUtils.getGroupedName(serviceName, groupName));
            beatInfo.setIp(instance.getIp());
            beatInfo.setPort(instance.getPort());
            beatInfo.setCluster(instance.getClusterName());
            beatInfo.setWeight(instance.getWeight());
            beatInfo.setMetadata(instance.getMetadata());
            beatInfo.setScheduled(false);
            beatInfo.setPeriod(instance.getInstanceHeartBeatInterval());
            this.beatReactor.addBeatInfo(NamingUtils.getGroupedName(serviceName, groupName), beatInfo);
        }

        this.serverProxy.registerService(NamingUtils.getGroupedName(serviceName, groupName), groupName, instance);
    }

这里就看的比较清楚了,这里会把服务的ip,端口,服务名等信息封装到 BeatInfo 对象中,beatReactor.addBeatInfo是把当前服务实例加入心跳机制(心跳续约),然后通过serverProxy.registerService注册。下面是NamingProxy#registerService的源码

public void registerService(String serviceName, String groupName, Instance instance) throws NacosException {
        LogUtils.NAMING_LOGGER.info("[REGISTER-SERVICE] {} registering service {} with instance: {}", new Object[]{this.namespaceId, serviceName, instance});
        Map<String, String> params = new HashMap(9);
        params.put("namespaceId", this.namespaceId);
        params.put("serviceName", serviceName);
        params.put("groupName", groupName);
        params.put("clusterName", instance.getClusterName());
        params.put("ip", instance.getIp());
        params.put("port", String.valueOf(instance.getPort()));
        params.put("weight", String.valueOf(instance.getWeight()));
        params.put("enable", String.valueOf(instance.isEnabled()));
        params.put("healthy", String.valueOf(instance.isHealthy()));
        params.put("ephemeral", String.valueOf(instance.isEphemeral()));
        params.put("metadata", JSON.toJSONString(instance.getMetadata()));
        this.reqAPI(UtilAndComs.NACOS_URL_INSTANCE, params, "POST");
    }
	...省略...
	public String reqAPI(String api, Map<String, String> params, String body, String method) throws NacosException {
        return this.reqAPI(api, params, body, this.getServerList(), method);
    }

最终把相关配置使用一个Map进行封装,然后交给reqAPI方法去发送POST请求给服务端,

  • /nacos/v1/ns/instance : 是nacos注册的接口,拼接上yaml中的nacos地址就是完成的注册地址了
  • this.getServerList() : 拿到所有的nacos服务端的地址,考虑到nacos-server集群的情况

代码最终来到reqAPI方法

public String reqAPI(String api, Map<String, String> params, String body, List<String> servers, String method) throws NacosException {
        params.put("namespaceId", this.getNamespaceId());
        if (CollectionUtils.isEmpty(servers) && StringUtils.isEmpty(this.nacosDomain)) {
            throw new NacosException(400, "no server available");
        } else {
            NacosException exception = new NacosException();
            if (servers != null && !servers.isEmpty()) {
            	//使用随机数随机取一个nacos-server
                Random random = new Random(System.currentTimeMillis());
                int index = random.nextInt(servers.size());
                int i = 0;

                while(i < servers.size()) {
                    String server = (String)servers.get(index);

                    try {
                    	//发送请求了
                        return this.callServer(api, params, body, server, method);
                    } catch (NacosException var13) {
                        exception = var13;
                        if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
                            LogUtils.NAMING_LOGGER.debug("request {} failed.", server, var13);
                        }

                        index = (index + 1) % servers.size();
                        ++i;
                    }
                }
            }

            if (StringUtils.isNotBlank(this.nacosDomain)) {
                int i = 0;

                while(i < 3) {
                    try {
                        return this.callServer(api, params, body, this.nacosDomain, method);
                    } catch (NacosException var12) {
                        exception = var12;
                        if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
                            LogUtils.NAMING_LOGGER.debug("request {} failed.", this.nacosDomain, var12);
                        }

                        ++i;
                    }
                }
            }

            LogUtils.NAMING_LOGGER.error("request: {} failed, servers: {}, code: {}, msg: {}", new Object[]{api, servers, exception.getErrCode(), exception.getErrMsg()});
            throw new NacosException(exception.getErrCode(), "failed to req API:/api/" + api + " after all servers(" + servers + ") tried: " + exception.getMessage());
        }
    }

代码不难理解,reqAPI方法中会随机从多个Nacos服务器地址中取一个,然后发起注册请求,通过callServer方法完成。

public String callServer(String api, Map<String, String> params, String body, String curServer, String method) throws NacosException {
        long start = System.currentTimeMillis();
        long end = 0L;
        this.injectSecurityInfo(params);
        List<String> headers = this.builderHeaders();
        String url;
        //拼接完整的URL: http://127.0.0.1:8848/nacos/v1/ns/instance
        if (!curServer.startsWith("https://") && !curServer.startsWith("http://")) {
            if (!curServer.contains(":")) {
                curServer = curServer + ":" + this.serverPort;
            }

            url = HttpClient.getPrefix() + curServer + api;
        } else {
            url = curServer + api;
        }
		//调用nacos的http客户端执行请求
        HttpClient.HttpResult result = HttpClient.request(url, headers, params, body, "UTF-8", method);
        end = System.currentTimeMillis();
        MetricsMonitor.getNamingRequestMonitor(method, url, String.valueOf(result.code)).observe((double)(end - start));
        if (200 == result.code) {
            return result.content;
        } else if (304 == result.code) {
            return "";
        } else {
            throw new NacosException(result.code, result.content);
        }
    }

方法中会对注册的地址进行拼接,如: http://127.0.0.1:8848/nacos/v1/ns/instance,然后低着参数,调用HttpClient.request执行请求;下面是:com.alibaba.nacos.client.naming.net.HttpClient#request 的源码,看得出来底层还是通过 JDK自带的HttpURLConnection来发送请求的

public static HttpResult request(String url, List<String> headers, Map<String, String> paramValues, String body, String encoding, String method) {
        HttpURLConnection conn = null;

        HttpResult var8;
        try {
            String encodedContent = encodingParams(paramValues, encoding);
            url = url + (StringUtils.isEmpty(encodedContent) ? "" : "?" + encodedContent);
            conn = (HttpURLConnection)(new URL(url)).openConnection();
            setHeaders(conn, headers, encoding);
            conn.setConnectTimeout(CON_TIME_OUT_MILLIS);
            conn.setReadTimeout(TIME_OUT_MILLIS);
            conn.setRequestMethod(method);
            conn.setDoOutput(true);
            if (StringUtils.isNotBlank(body)) {
                byte[] b = body.getBytes();
                conn.setRequestProperty("Content-Length", String.valueOf(b.length));
                conn.getOutputStream().write(b, 0, b.length);
                conn.getOutputStream().flush();
                conn.getOutputStream().close();
            }

            conn.connect();
            if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
                LogUtils.NAMING_LOGGER.debug("Request from server: " + url);
            }

            var8 = getResult(conn);
            return var8;
        } catch (Exception var14) {
            try {
                if (conn != null) {
                    LogUtils.NAMING_LOGGER.warn("failed to request " + conn.getURL() + " from " + InetAddress.getByName(conn.getURL().getHost()).getHostAddress());
                }
            } catch (Exception var13) {
                LogUtils.NAMING_LOGGER.error("[NA] failed to request ", var13);
            }

            LogUtils.NAMING_LOGGER.error("[NA] failed to request ", var14);
            var8 = new HttpResult(500, var14.toString(), Collections.emptyMap());
        } finally {
            IoUtils.closeQuietly(conn);
        }

        return var8;
    }

BeatReactor#addBeatInfo心跳续约

在 NacosNamingService#registerInstance方法中把服务信息封装为一个 BeatInfo ,然后加入this.beatReactor.addBeatInfo 心跳机制。我们来看一下心跳是如何做的,下面是beatReactor.addBeatInfo的源码

 public void addBeatInfo(String serviceName, BeatInfo beatInfo) {
        LogUtils.NAMING_LOGGER.info("[BEAT] adding beat: {} to beat map.", beatInfo);
        String key = this.buildKey(serviceName, beatInfo.getIp(), beatInfo.getPort());
        BeatInfo existBeat = null;
        if ((existBeat = (BeatInfo)this.dom2Beat.remove(key)) != null) {
            existBeat.setStopped(true);
        }

        this.dom2Beat.put(key, beatInfo);
        //线程池,定时任务,5000毫秒发送一次心跳。beatInfo.getPeriod()是定时任务执行的频率
        this.executorService.schedule(new BeatTask(beatInfo), beatInfo.getPeriod(), TimeUnit.MILLISECONDS);
        MetricsMonitor.getDom2BeatSizeMonitor().set((double)this.dom2Beat.size());
    }

   //心跳任务
   class BeatTask implements Runnable {
        BeatInfo beatInfo;

        public BeatTask(BeatInfo beatInfo) {
            this.beatInfo = beatInfo;
        }

       public void run() {
            if (!this.beatInfo.isStopped()) {
                long nextTime = this.beatInfo.getPeriod();

                try {
                    JSONObject result = BeatReactor.this.serverProxy.sendBeat(this.beatInfo, BeatReactor.this.lightBeatEnabled);
                    long interval = (long)result.getIntValue("clientBeatInterval");
                    boolean lightBeatEnabled = false;
                    if (result.containsKey("lightBeatEnabled")) {
                        lightBeatEnabled = result.getBooleanValue("lightBeatEnabled");
                    }

                    BeatReactor.this.lightBeatEnabled = lightBeatEnabled;
                    if (interval > 0L) {
                        nextTime = interval;
                    }

                    int code = 10200;
                    if (result.containsKey("code")) {
                        code = result.getIntValue("code");
                    }

                    if (code == 20404) {
                        Instance instance = new Instance();
                        instance.setPort(this.beatInfo.getPort());
                        instance.setIp(this.beatInfo.getIp());
                        instance.setWeight(this.beatInfo.getWeight());
                        instance.setMetadata(this.beatInfo.getMetadata());
                        instance.setClusterName(this.beatInfo.getCluster());
                        instance.setServiceName(this.beatInfo.getServiceName());
                        instance.setInstanceId(instance.getInstanceId());
                        instance.setEphemeral(true);

                        try {
                            BeatReactor.this.serverProxy.registerService(this.beatInfo.getServiceName(), NamingUtils.getGroupName(this.beatInfo.getServiceName()), instance);
                        } catch (Exception var10) {
                        }
                    }
                } catch (NacosException var11) {
                    LogUtils.NAMING_LOGGER.error("[CLIENT-BEAT] failed to send beat: {}, code: {}, msg: {}", new Object[]{JSON.toJSONString(this.beatInfo), var11.getErrCode(), var11.getErrMsg()});
                }

                BeatReactor.this.executorService.schedule(BeatReactor.this.new BeatTask(this.beatInfo), nextTime, TimeUnit.MILLISECONDS);
            }
        }
   }

和Eureka一样,心跳也是通过线程池 ScheduledExecutorService 来实现的,时间频率默认是5秒一次。

  • BeatInfo : 心跳续约的对象,其中包括服务的IP,端口,服务名,权重等
  • executorService.schedule :定时任务,beatInfo.getPeriod()是定时任务执行频率,默认是5000 毫秒发送一次心跳续约请求到NacosServer
  • BeatTask :是一个Runnable线程,run方法中会调用 BeatReactor.this.serverProxy.sendBeat 发送心跳请求。

BeatTask作为心跳续约的线程对象,他的run方法中 通过 BeatReactor.this.serverProxy.sendBeat发送心跳,如果发现服务未注册会通过 BeatReactor.this.serverProxy.registerService 注册服务。

下面是 com.alibaba.nacos.client.naming.net.NamingProxy#sendBeat 发送心跳的方法

 public JSONObject sendBeat(BeatInfo beatInfo, boolean lightBeatEnabled) throws NacosException {
        if (LogUtils.NAMING_LOGGER.isDebugEnabled()) {
            LogUtils.NAMING_LOGGER.debug("[BEAT] {} sending beat to server: {}", this.namespaceId, beatInfo.toString());
        }

        Map<String, String> params = new HashMap(8);
        String body = "";
        if (!lightBeatEnabled) {
            try {
                body = "beat=" + URLEncoder.encode(JSON.toJSONString(beatInfo), "UTF-8");
            } catch (UnsupportedEncodingException var6) {
                throw new NacosException(500, "encode beatInfo error", var6);
            }
        }

        params.put("namespaceId", this.namespaceId);
        params.put("serviceName", beatInfo.getServiceName());
        params.put("clusterName", beatInfo.getCluster());
        params.put("ip", beatInfo.getIp());
        params.put("port", String.valueOf(beatInfo.getPort()));
        String result = this.reqAPI(UtilAndComs.NACOS_URL_BASE + "/instance/beat", params, body, "PUT");
        return JSON.parseObject(result);
    }

这里也是会拼接好心跳的地址 :127.0.0.1:8848/nacos/v1/ns/instance/beat ,参数包括namespaceId命名空间ID;serviceName 服务ing;clusterName 集群名;ip 服务的IP;port 端口。然后发送一个PUT请求。底层依然是从多个NacosServer随机选择一个发起心跳请求。

客户端就先分析到这里,为了梳理流程我这里画了一个图

在这里插入图片描述

总结

  1. NACOS就是使用SpringBoot自动装配完成相关组件的配置,在Nacos的自动配置类(NacosServerRegistryAutoConfiguration )中注册了服务注册器(NacosServiceRegistry )和自动注册器(NacosAutoServiceRegistration ),注册的服务登记对象(NacosRegistration)等。
  2. Nacos自动注册器通过监听应用启动时间来触发自动注册,他会判断一个自动注册开关(spring.cloud.service-registry.auto-registration.enabled)。满足自动注册就会调用NacosServiceRegistry 的register方法进行服务注册
  3. NacosServiceRegistry 把注册工作交给NacosNamingService去完成,在NacosNamingService中做了两件时间:1是把服务封装为BeatInfo加入心跳机制,2是完成服务的注册
  4. 对于心跳机制,通过BeatReactor 来完成,底层使用的是带定时任务的线程池5S一次心跳发送到服务端。
  5. 对于服务注册则是通过NamingProxy代理来完成,和心跳一样,都是使用随机选择一个NacosServer,然后拼接注册地址,交给HttpClient发送http请求。

好吧,到这里就真的结束了,如果文章对你有帮助建议点赞收藏加好评。你的鼓励是我最大的动力

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/18700.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MongoDB基本查询语句

文章目录 查询选择器查询就是获取存储在数据库中的数据。在MongoDB中,查询通常针对一个集合来操作。查询可以指定查询条件,只返回匹配的文档;还可以指定投影项,只返回指定的字段,减少返回数据到客户端的网络流量。 为了进行测试,我们先假想一个常用的电子商务网站上可能用…

svg VS canvas,哪种在移动端适配度更好?实战经历告诉你~

最近做了一个画图的功能,后端提供棋盘数据,前端需要把数据转换成一个棋盘画出来,当时有两种实现方法,一种是使用canvas画图,一个是使用svg画图. ui提供的设计稿如下: 由于这是一个轮播图,当后端返回多少张棋盘数据时,就需要渲染多少张棋盘,最多有十张,基于canvas和svg画图,为了…

【Milvus的以图搜图】

0. 介绍 Milvus官方在bootcamp项目中给开发者提供多种解决方案&#xff0c;包含常见的以图搜图、人脸检索、DNA相似性检索、视频检索等等&#xff0c;具体可以参考bootcamp项目。 本文主要就bootcamp中以图搜图reverse_image_search的代码介绍如何运行该样例&#xff0c;进而了…

文献阅读-VSE++:使用困难负样本来改经视觉语义嵌入

Title&#xff1a;《VSE: Improving Visual-Semantic Embeddings with Hard Negatives》 Authors&#xff1a;Fartash FaghriDavid FleetJ. KirosS. FidlerJournal&#xff1a;ArXiv (2017)Date&#xff1a;2017 code&#xff1a;GitHub - fartashf/vsepp: PyTorch Code for t…

Java基础38 面向对象三大特征之多态

OOP之多态 多态1.多态的具体体现2.向上转型3.向下转型4.属性重写5.instanceOf6.动态绑定机制&#xff08;核心&#xff09;● Java的动态访问机制7.多态数组8.多态参数)多态 ● 多【多种】态【状态】 方法或对象具有多种形态。 是建立在封装和继承之上的面向对象的第三大特征…

一文带你学会Vue3基本语法

Vue3 起步1.通过 CDN 使用 Vue32.Vue3 模板语法文本Html属性表达式指令参数3.模板用户输入双向绑定1.通过 CDN 使用 Vue3 你可以借助 script 标签直接通过 CDN 来使用 Vue&#xff1a; <script src"https://unpkg.com/vuenext"></script>通过 CDN 使用…

在WPF中使用Prism弹出自定义窗体样式的对话框

概述 我们在Prism中弹出一个对话框&#xff0c;默认是一个Windows默认样式的窗口&#xff0c;这样会同自己所开发的项目完全不搭调&#xff0c;譬如下面这样子 那么如果为了配合软件主体的风格&#xff0c;可以做出类似这样效果 其实原理也很简单&#xff0c;Prism也考虑到了这…

不科学,RocketMQ生产者在一个应用服务竟然不能向多个NameServer发送消息

前言 目前有两套RocketMQ集群&#xff0c;集群A包含topic名称为cluster_A_topic&#xff0c;集群B包含topic名称为cluster_B_topic&#xff0c;在应用服务OrderApp上通过RocketMQ Client创建两个DefaultMQProducer实例发送消息给集群A和集群B&#xff0c;架构图如下&#xff1…

使用Vue脚手架配置代理服务器的两种方式

1 前言 本文主要介绍使用Vue脚手架配置代理服务器的两种方式 注意&#xff1a;Vue脚手架给我们提供了两种配置代理服务器的方式&#xff0c;各有千秋&#xff0c;使用的时候只能二选一&#xff0c;不能同时使用 2 代理 除了cros和jsonp&#xff0c;还有一种代理方式&#x…

传奇GM调整极品属性的命令------技术分享

传奇GM调整极品属性的命令 GM命令supermake命令用法&#xff01; 以下格式皆为supermake a b c   以上命令含义&#xff1a;调整A(装备)的B(属性)到C(点数) supermake 1 0 10  1代表武器  0代表攻击 10代表调整的点数 B参数代表需要调整的那项属性如攻击 魔法 道术 …

黑*头条_第4章_文章搜索前后端成形记 实名认证审核

黑*头条_第4章_文章搜索前后端成形记 & 实名认证审核 文章目录黑*头条_第4章_文章搜索前后端成形记 & 实名认证审核文章搜索前后端成形记 & admin实名认证审核1 文章详情-前端开发1.1登录接口1.1.1 基本定义1.1.2 code定义1.1.3 mapper实现1.1.4 service代码实现1.…

宝塔一键安装wordpress

使用宝塔面板来部署网站是非常方便的&#xff0c;以WordPress网站为例来说&#xff1a; 一般有两种方式安装WordPress网站&#xff0c;第一种是上传网站程序到网站根目录手动安装&#xff0c;另外一种是在宝塔面板后台左侧菜单&#xff0c;找到“WordPress一键部署”&#xff…

Map 和 Set

模型 一般我们把搜索的数据称为 关键字(key) , 关键字对应的值叫做 值(value) , 将之称为 key-value 键值对. 衍生出两种模型: 1. 纯 key 模型 例如 : 班级上点名, 在花名册上找人的名字. 2. key-value 模型 例如 : 统计一个字符串中每个字母出现的次数, 结果是每个字母和它对…

简易版 图书管理系统

目录 1. Book包 1.1 Book类 1.2 BookList类 2. User包 2.1 User抽象类 2.2 AdminUser类 2.3 NormalUser类 3. Operate包 3.1 MyOperate接口 3.2 AddOperation类 3.3 DelOperation类 3.4 ExitOperation 3.5 FindOperation类 3.6 ShowOperation类 3.7 BorrowedOpe…

MySQL——数据库、表的操作

文章目录数据库的操作创建数据库创建数据库例子字符集和校验规则查看数据库支持的字符集查看默认的字符校验规则校验规则对数据库的影响查看数据库显示详细的创建数据库语句修改数据库删除数据库查看连接情况表的操作创建表显示创建表的详细过程不同的数据库引擎查看表结构修改…

【C语言】操作符与优先级详解

C的操作符 文章目录C的操作符前言一、算术操作符二、移位操作符三、位操作符四、赋值操作符五、单目操作符六、条件操作符七、逻辑操作符八、条件操作符九、逗号表达式十、下标引用、函数调用和结构成员十一、表达式求值11.1 隐式类型转换12.2 算术转换12.2 操作符的属性总结前…

解决Vue前后端跨域问题的多种方式

1 前言 本文主要介绍借助解决Vue前后端跨域问题的几种方式 说到ajax请求&#xff0c;就不得不说下xhr(XMLHttpRequest)了&#xff0c;它可以说是鼻祖&#xff0c;但是实际开发中&#xff0c;我们不会直接使用它&#xff0c;而是进行二次封装或者使用成熟的第三方封装&#xf…

Zookeeper:分布式过程协同技术

Zookeeper 是一个高性能的分布式一致系统&#xff0c;在分布式系统中有着广泛的应用。基于它&#xff0c;可以实现诸如“分布式同步”、“配置管理”、“命名空间管理”等众多功能&#xff0c;是分布式系统中常见的基础系统。Zookeeper 主要用来解决分布式集群中应用系统的一致…

http,https,ip,tcp,udp

http:超文本传输协议&#xff0c;明文传输&#xff0c;不安全 超文本&#xff1a;早期&#xff0c;文本存在本地&#xff0c;文本可以被计算机解析为二进制的数据包&#xff0c;随着发展&#xff0c;出现图片&#xff0c;视频&#xff0c;链接等&#xff0c;成为超文本 传输&a…

批量生成Excel文件,可以按模板进行自动生成

目录 一、文件目录结构 二、编辑生成名单 三、编辑模板 四、生成操作 软件描述&#xff1a;根据Excel模板 和 生成名单 可以批量生成相同格式的文件&#xff0c;可以应用于考核、工资单等文件的批量生成。方便快捷&#xff0c;有需求的小伙伴可以到最下面点击下载 注&#…