consul-02.consul服务注册实现(java)
一,续
在上一篇文章中简单介绍了下consul和集群环境搭建,点我查看上一篇文章。
本篇中将介绍怎么将服务注册至consul集群中,并形成一个公共jar包,在springmvc或者springboot应用启动时将服务注册至consul集群中,应用退出时将服务从consul集群中移除。
二,实现思路
1.consul集群如何利用
这里将利用consul的健康检查的特性来保证我们服务消费方发现的服务是健康可用的。
服务提供方一般是我们的应用程序,比如会员服务,订单服务之类的,服务消费方一般是我们的网关。
比如会员服务有三台机器,将三台机器的ip和port分别注册至consul集群中,当会员服务的某台机器挂掉之后,consul集群中的健康检查就会将该服务标记为严重,网关向consul集群中获取健康的服务列表时,该机器提供的服务将不能被发现,
网关转发请求时自然就不会把请求转发到挂掉的那台机器了。
另外利用服务注册时提供的tag,我们可以利用tag做版本管理,发现服务时可以根据tag来查询。
还有一点就是,我们可以利用该思路来做灰度发布,如果网关做的比较完善的话,我只要保证每个服务有一台机器正常提供服务,整个系统就能一直正常运行,能做到随时增加和减少某个服务的机器。
坑:当你向集群中的某个节点注册服务时,该节点不会向其他节点扩散该服务信息,该节点挂掉后,该服务将不能被发现。我的解决办法比较暴力,将该服务信息向集群中的所有节点注册,服务发现时再将重复的服务信息,即ip+端口+版本一致的话,即认为是重复的服务,只保留一条该服务信息。
2.技术方面
技术方面主要是分别实现spring的InitializingBean,DisposableBean两个接口来达到应用程序启动时执行注册,应用程序退出时销毁服务。
三,实现spring应用程序注册至consul集群公共jar包
1.代码包结构,有以下几个类
- com.sdk.consul.consts.ConsulConsts.java
因为我们的配置文件properties每个环境只有一份,而且同一服务会在多台服务器上部署,每台服务器的ip和端口不一样,所以为了发布的时候不修改properties文件,故一般会在jvm启动参数中配置当前服务的ip和端口,这个文件定义的就是这两个启动参数的名字。 - com.sdk.consul.exception.ConsulServiceException.java
异常类,不多说了。 - com.sdk.consul.utils.Md5Utils.java
对应用服务ip+端口加密生成serviceid,保证服务器ip和端口不变,生成的servcieid也不变 - com.sdk.consul.utils.ServerUtils.java
通过java代码获取当前服务器ip和web服务器(如tomcat)的运行端口,多网卡下会有问题,因此这是获取应用服务ip和端口的最后一个选项。 - com.sdk.consul.utils.StringUtils.java
处理字符串的类,不多帅。 - com.sdk.consul.ConsulConfig.java
服务注册入口,spring mvc将此类在xml中定义成bean即可,类似下面这样:
<bean class="com.sdk.consul.ConsulConfig"></bean>
springboot在启动入口类中@Import(ConsulConfig.class) - com.sdk.consul.ConsulProperty.java
properties配置文件配置信息类 - com.sdk.consul.RegisterService.java
服务操作类,提供注册和销毁服务等方法。
2.jar包依赖
- pom依赖以下jar包
<dependencies><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>1.7.5</version></dependency><dependency><groupId>com.ecwid.consul</groupId><artifactId>consul-api</artifactId><version>1.3.0</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>3.2.9.RELEASE</version><scope>compile</scope></dependency>
</dependencies>
3.类具体代码
- com.sdk.consul.consts.ConsulConsts.java
package com.sdk.consul.consts;public class ConsulConsts {/** JVM启动参数名称-服务暴露地址 */public static final String CONSUL_SERVICE_URL = "CONSUL_SERVICE_URL";/**VM启动参数名称-服务暴露端口*/public static final String CONSUL_SERVICE_PORT = "CONSUL_SERVICE_PORT";
}
- com.sdk.consul.exception.ConsulServiceException.java
package com.sdk.consul.exception;/*** Consul服务注册异常类*/
public class ConsulServiceException extends RuntimeException{public ConsulServiceException() {super();}public ConsulServiceException(final String message) {super(message);}public ConsulServiceException(final String message, final Throwable cause) {super(message, cause);}}
- com.sdk.consul.utils.Md5Utils.java
package com.sdk.consul.util;import com.sdk.consul.exception.ConsulServiceException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;public class Md5Utils {/*** MD5加密* @param source 加密前字符串* @return* @throws ConsulServiceException* @throws NoSuchAlgorithmException*/public static String encrypt(String source) throws ConsulServiceException,NoSuchAlgorithmException {if(StringUtils.isEmpty(source)){throw new ConsulServiceException("Please input a not blank value to encrypt md5.");}MessageDigest md5 = MessageDigest.getInstance("MD5");byte[] md5Bytes = md5.digest(source.getBytes());StringBuffer hexValue = new StringBuffer();for (int i = 0; i < md5Bytes.length; i++) {int val = ((int) md5Bytes[i]) & 0xff;if (val < 16){hexValue.append("0");}hexValue.append(Integer.toHexString(val));}return hexValue.toString();}
}
- com.sdk.consul.utils.ServerUtils.java- com.sdk.consul.utils
package com.sdk.consul.util;import javax.management.*;
import java.lang.management.ManagementFactory;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Iterator;
import java.util.Set;/*** 获取服务器ip和应用服务器端口信息*/
public class ServerUtils {/** 协议 */private static final String PROTOCOL = "protocol";/** 协议 HTTP/1.1*/private static final String HTTP11 = "HTTP/1.1";/** 协议 Http11*/private static final String HTTP11_OTHER = "Http11";/** 应用服务容器*/private static final String CONNECTOR = "*:type=Connector,*";/** scheme*/private static final String SCHEME = "scheme";/** 端口*/private static final String PORT = "port";/*** 获取ipv4和应用程序端口信息* @return* @throws MalformedObjectNameException* @throws NullPointerException* @throws UnknownHostException* @throws AttributeNotFoundException* @throws InstanceNotFoundException* @throws MBeanException* @throws ReflectionException*/public static String getIPV4EndPoints() throws MalformedObjectNameException,NullPointerException, UnknownHostException,AttributeNotFoundException, InstanceNotFoundException,MBeanException, ReflectionException {MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();QueryExp subQueryHttp = Query.match(Query.attr(PROTOCOL), Query.value(HTTP11));QueryExp subQueryHttpOther = Query.anySubString(Query.attr(PROTOCOL), Query.value(HTTP11_OTHER));QueryExp query = Query.or(subQueryHttp, subQueryHttpOther);Set<ObjectName> connectors = mbs.queryNames(new ObjectName(CONNECTOR), query);String hostname = Inet4Address.getLocalHost().getHostName();InetAddress[] addresses = Inet4Address.getAllByName(hostname);return getIPArray(connectors, addresses, mbs, 4);}/*** 获取ipv6和应用程序端口信息* @return* @throws MalformedObjectNameException* @throws NullPointerException* @throws UnknownHostException* @throws AttributeNotFoundException* @throws InstanceNotFoundException* @throws MBeanException* @throws ReflectionException*/public static String getIPV6EndPoints() throws MalformedObjectNameException,NullPointerException, UnknownHostException,AttributeNotFoundException, InstanceNotFoundException,MBeanException, ReflectionException {MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();QueryExp subQueryHttp = Query.match(Query.attr(PROTOCOL), Query.value(HTTP11));QueryExp subQueryHttpOther = Query.anySubString(Query.attr(PROTOCOL), Query.value(HTTP11_OTHER));QueryExp query = Query.or(subQueryHttp, subQueryHttpOther);Set<ObjectName> connectors = mbs.queryNames(new ObjectName(CONNECTOR), query);String hostname = Inet6Address.getLocalHost().getHostName();InetAddress[] addresses = Inet6Address.getAllByName(hostname);return getIPArray(connectors, addresses, mbs, 6);}/*** 获取web container 运行端口* @return* @throws MalformedObjectNameException* @throws NullPointerException*/public static String getPort() throws MalformedObjectNameException,NullPointerException{MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();QueryExp subQueryHttp = Query.match(Query.attr(PROTOCOL), Query.value(HTTP11));QueryExp subQueryHttpOther = Query.anySubString(Query.attr(PROTOCOL), Query.value(HTTP11_OTHER));QueryExp query = Query.or(subQueryHttp, subQueryHttpOther);Set<ObjectName> connectors = mbs.queryNames(new ObjectName(CONNECTOR), query);for (Iterator<ObjectName> i = connectors.iterator(); i.hasNext();) {ObjectName obj = i.next();return obj.getKeyProperty(PORT);}return "";}/*** 组合当前所有ip和端口信息* @param connectors* @param addresses* @param mbs* @param ipType 4:ipv4,6:ipv6* @return* @throws NullPointerException* @throws AttributeNotFoundException* @throws InstanceNotFoundException* @throws MBeanException* @throws ReflectionException*/private static String getIPArray(Set<ObjectName> connectors,InetAddress[] addresses,MBeanServer mbs,int ipType)throws NullPointerException, AttributeNotFoundException,InstanceNotFoundException, MBeanException, ReflectionException {for (Iterator<ObjectName> i = connectors.iterator(); i.hasNext();) {ObjectName obj = i.next();String scheme = mbs.getAttribute(obj, SCHEME).toString();for (InetAddress addr : addresses) {if (addr.isAnyLocalAddress() || addr.isLoopbackAddress() ||addr.isMulticastAddress() ) {continue;}if((ipType == 4 || ipType == 6) && addr.getAddress().length == ipType){String host = addr.getHostAddress();String ep = scheme + "://" + host;return ep;}}}return "";}
}
- com.sdk.consul.utils.StringUtils.java
package com.sdk.consul.util;/*** 字符串帮助类*/
public class StringUtils {/*** 检查目标字符串是否为空白(null或者空字符串)* @param target* @return*/public static boolean isEmpty(String target){return target == null || target.trim().length() == 0;}}
- com.sdk.consul.ConsulConfig.java
package com.sdk.consul;import com.sdk.consul.exception.ConsulServiceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import javax.management.*;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;/*** 应用程序注册到consul集群入口*/
public class ConsulConfig implements InitializingBean,DisposableBean {private static final Logger LOGGER= LoggerFactory.getLogger(ConsulConfig.class);/** 应用服务名称 */@Value("${consul.service.name}")private String serviceName;/** 健康检查心跳时间 */@Value("${consul.service.interval}")private Integer interval;/** 应用服务地址 默认空白*/@Value("${consul.service.url:}")private String serviceUrl;/** 应用服务端口 默认空白*/@Value("${consul.service.port:}")private String servicePort;/** 应用服务版本 */@Value("${consul.service.version}")private String version;/** etcd集群url */@Value("${consul.nodes.url}")private String nodesUrl;/** consul配置信息 */private ConsulProperty consulProperty;/** 服务注册和销毁 */private RegisterService registerService;/*** 初始化consul配置信息*/private void initConsulProperty(){consulProperty = new ConsulProperty();consulProperty.setServiceName(this.serviceName);consulProperty.setServiceUrl(this.serviceUrl);consulProperty.setServicePort(this.servicePort);consulProperty.setInterval(this.interval);consulProperty.setVersion(this.version);consulProperty.setNodesUrl(this.nodesUrl);}/*** 实现DisposableBean接口中的方法* 以便工程在容器中启动的时候将服务注册至consul集群中* @throws Exception*/public void afterPropertiesSet() throws ConsulServiceException,MalformedObjectNameException,NoSuchAlgorithmException,NullPointerException, AttributeNotFoundException,InstanceNotFoundException, MBeanException, ReflectionException,IOException {//初始化配置信息initConsulProperty();//开始注册服务registerService = new RegisterService(consulProperty);LOGGER.info(">>>> Starting register service.");registerService.registerServiceToConsul();LOGGER.info("<<<< Register service completed.");}/*** 实现InitializingBean接口中的方法* 应用退出时将服务从集群中删除* @throws Exception*/public void destroy(){registerService.destroy();}}
- com.sdk.consul.ConsulProperty.java
package com.sdk.consul;import org.springframework.beans.factory.annotation.Value;
import java.io.Serializable;/*** consul 配置类*/
public class ConsulProperty implements Serializable {/** 应用服务名称 */private String serviceName;/** 应用服务id */private String serviceId;/** 健康检查心跳时间 */private Integer interval;/** 应用服务地址 默认空白*/private String serviceUrl;/** 应用服务端口 默认空白*/private String servicePort;/** 应用服务版本 */private String version;/** etcd集群url */private String nodesUrl;public String getServiceName() {return serviceName;}public void setServiceName(String serviceName) {this.serviceName = serviceName;}public String getServiceId() {return serviceId;}public void setServiceId(String serviceId) {this.serviceId = serviceId;}public Integer getInterval() {return interval;}public void setInterval(Integer interval) {this.interval = interval;}public String getServiceUrl() {return serviceUrl;}public void setServiceUrl(String serviceUrl) {this.serviceUrl = serviceUrl;}public String getServicePort() {return servicePort;}public void setServicePort(String servicePort) {this.servicePort = servicePort;}public String getVersion() {return version;}public void setVersion(String version) {this.version = version;}public String getNodesUrl() {return nodesUrl;}public void setNodesUrl(String nodesUrl) {this.nodesUrl = nodesUrl;}public String toString(){StringBuilder sb = new StringBuilder();sb.append("serviceName:").append(this.serviceName).append(",");sb.append("serviceId:" ).append(this.serviceId).append(",");sb.append("interval:").append(this.interval).append(",");sb.append("serviceUrl:").append(this.serviceUrl).append(",");sb.append("servicePort:").append(this.servicePort).append(",");sb.append("version:").append(this.version).append(",");sb.append("nodesUrl:").append(this.nodesUrl);return sb.toString();}
}
- com.sdk.consul.RegisterService.java - com.sdk.consul
package com.sdk.consul;import com.ecwid.consul.v1.ConsulClient;
import com.ecwid.consul.v1.agent.model.NewService;
import com.sdk.consul.consts.ConsulConsts;
import com.sdk.consul.exception.ConsulServiceException;
import com.sdk.consul.util.Md5Utils;
import com.sdk.consul.util.ServerUtils;
import com.sdk.consul.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.management.*;
import java.io.IOException;
import java.net.UnknownHostException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;/*** 服务注册类*/
public class RegisterService{private static final Logger LOGGER= LoggerFactory.getLogger(RegisterService.class);/** consul配置信息 */private ConsulProperty consulProperty;/*** 获取初始化好的consul信息* @param consulProperty*/RegisterService(ConsulProperty consulProperty){this.consulProperty = consulProperty;}/*** 检查consul config配置是否正确*/private void checkConsulConfig()throws ConsulServiceException,MalformedObjectNameException,NoSuchAlgorithmException,NullPointerException, UnknownHostException, AttributeNotFoundException,InstanceNotFoundException, MBeanException, ReflectionException {if(consulProperty == null){throw new ConsulServiceException("Get consul config error, please check the consul config was set at properties file.");}if(StringUtils.isEmpty(consulProperty.getServiceName())){throw new ConsulServiceException("Please set a not blank value to ${consul.service.name} at properties file.");}if(consulProperty.getInterval() == null){throw new ConsulServiceException("Please set a number value to ${consul.service.interval} at properties file.");}//处理serviceUrlString serviceUrlTmp = consulProperty.getServiceUrl();LOGGER.info("Get service url from JVM startup arguments.");String serviceUrl = System.getProperty(ConsulConsts.CONSUL_SERVICE_URL);if(StringUtils.isEmpty(serviceUrl)){LOGGER.info("Can't find CONSUL_SERVICE_URL argument from JVM startup arguments,please check service url was set in JVM startup arguments,now get service url from properties file.");serviceUrl = serviceUrlTmp;}if(StringUtils.isEmpty(serviceUrl)){LOGGER.info("Can't get service url from properties file,please check ${consul.service.url} was set in properties file, now get service url from running web server.");serviceUrl = ServerUtils.getIPV4EndPoints();}if(StringUtils.isEmpty(serviceUrl)){throw new ConsulServiceException("Can't get service url from JVM startup arguments or properties file or running web container, please set CONSUL_SERVICE_URL argument to JVM startup arguments.");}consulProperty.setServiceUrl(serviceUrl);//处理service portString servicePortTmp = consulProperty.getServicePort();LOGGER.info("Get service port from JVM startup arguments.");String servicePort = System.getProperty(ConsulConsts.CONSUL_SERVICE_PORT);if(StringUtils.isEmpty(servicePort)){LOGGER.info("Can't find CONSUL_SERVICE_PORT argument from JVM startup arguments,please check service url was set in JVM startup arguments,now get service port from properties file.");servicePort = servicePortTmp;}if(StringUtils.isEmpty(servicePort)){LOGGER.info("Can't get service port from properties file,please check ${consul.service.port} was set in properties file, now get service port from running web server.");servicePort =ServerUtils.getPort();}if(StringUtils.isEmpty(servicePort)){throw new ConsulServiceException("Can't get service port,please set CONSUL_SERVICE_PORT argument to JVM startup arguments.");}consulProperty.setServicePort(servicePort);consulProperty.setServiceId(Md5Utils.encrypt(serviceUrl +":"+ servicePort));if(StringUtils.isEmpty(consulProperty.getVersion())){throw new ConsulServiceException("Please set a not blank value to ${consul.service.version} at properties file.");}if(StringUtils.isEmpty(consulProperty.getNodesUrl())){throw new ConsulServiceException("Please set a not blank value to ${consul.nodes.url} at properties file.");}}/*** 将服务注册至consul集群中*/void registerServiceToConsul() throws ConsulServiceException,MalformedObjectNameException,NoSuchAlgorithmException, NullPointerException, AttributeNotFoundException,InstanceNotFoundException, MBeanException, ReflectionException,IOException{LOGGER.info(">>>> Starting check consul config.");checkConsulConfig();LOGGER.info("<<<< Check consul config success, geted cosul config is:{}",consulProperty.toString());String consulNodesUrl = consulProperty.getNodesUrl();List<String> tags = new ArrayList<String>();tags.add(consulProperty.getVersion());List<String> consulNodeUrls = new ArrayList<String>();if (consulNodesUrl.indexOf(",") > -1) {consulNodeUrls = Arrays.asList(consulNodesUrl.split(","));} else {consulNodeUrls.add(consulNodesUrl);}//组装服务信息NewService newService = new NewService();newService.setId(consulProperty.getServiceId());newService.setTags(tags);newService.setName(consulProperty.getServiceName());newService.setPort(Integer.parseInt(consulProperty.getServicePort()));newService.setAddress(consulProperty.getServiceUrl());//组装健康检查信息NewService.Check serviceCheck = new NewService.Check();serviceCheck.setHttp(consulProperty.getServiceUrl() + ":" + consulProperty.getServicePort());serviceCheck.setInterval(consulProperty.getInterval() + "s");newService.setCheck(serviceCheck);//往集群中注册服务for (String url : consulNodeUrls) {LOGGER.info(">>>> Starting register service to {}.", url);//组装服务信息ConsulClient client = new ConsulClient(url);client.agentServiceRegister(newService);LOGGER.info("<<<< Register service to {} success.", url);}}/*** 应用程序退出时删除服务信息*/void destroy(){String consulNodesUrl = consulProperty.getNodesUrl();List<String> consulNodeUrls = new ArrayList<String>();if (consulNodesUrl.indexOf(",") > -1) {consulNodeUrls = Arrays.asList(consulNodesUrl.split(","));} else {consulNodeUrls.add(consulNodesUrl);}//注销集群中的服务for (String url : consulNodeUrls) {LOGGER.info(">>>> Starting deregister service to {}.", url);//组装服务信息ConsulClient client = new ConsulClient(url);client.agentServiceDeregister(consulProperty.getServiceId());LOGGER.info("<<<< Register service to {} success.", url);}}}
4.配置文件说明
- 应用服务需要加的配置说明例
#当前应用服务名称consul.service.name=order-center#consul健康检查心跳时间,consul集群中的节点会定时请求${consul.service.url}+${consul.service.port}#地址,如果返回200则认为该服务是健康的,否则会将该服务标记为failconsul.service.interval=2#当前应用服务对外暴露的地址和端口。#首先从jvm启动参数中获取暴露的服务地址和端口;#jvm启动参数中没有配置的话,从当前properties文件获取;#当前properties文件中没有配置的话,将自动获取web server container的ip和port,#但请设置好服务器的hostname,linux下可执行命令hostname来设置hostname,#例:hostname consul-nodeconsul.service.url=http://10.0.0.93consul.service.port=8080#当前应用服务版本consul.service.version=v1#consul集群地址,多个地址用半角逗号隔开consul.nodes.url=http://10.0.0.11:8500,http://10.0.0.12:8500,http://10.0.0.13:8500
consul-02.consul服务注册实现(java)相关推荐
- .Net Core 商城微服务项目系列(二):使用Ocelot + Consul构建具备服务注册和发现功能的网关...
1.服务注册 在上一篇的鉴权和登录服务中分别通过NuGet引用Consul这个包,同时新增AppBuilderExtensions类: public static class AppBuilderEx ...
- Docker+Consul+Registrator实现服务注册与发现
逻辑图 实现nginx节点自动化加入容器IP代理 1.部署三台Consu集群然后通过Consul Template实时监测Leader的变化. 2.Leader如果变化就触发渲染模板动作,执行相应命令 ...
- consul+docker实现服务注册
近期新闻 css宣布支持三角函数 ES10即将来临 基本架构 注册中心: 每个服务提供者向注册中心登记自己的服务,将服务名与主机Ip,端口等一些附加信息告诉注册中心,注册中心按服务名分类组织服务清单. ...
- SpringCloud核心教程 | 第四篇:服务注册与发现 Consul篇
Spring Cloud简介 Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中涉及的配置管理.服务发现.断路器.智能路由.微代理.控制总线.全 ...
- 微服务注册中心:Consul——服务注册
系列文章: 微服务架构:网关概念与 zuul 微服务网关:Spring Cloud Gateway -- Zuul 微服务网关:Spring Cloud Config- 配置中心 微服务网关方案:Ko ...
- Spring Cloud中使用Consul作为服务注册中心时如何获得local service id?
微服务是目前非常流行和实用的软件架构设计.Spring Cloud是java开发领域最受欢迎也是常用的微服务框架.Spring Cloud Finchley版本已经发布,与此同时Eureka 2.0的 ...
- Spring Cloud 基于Consul 实现服务注册与发现
Spring Cloud自己体系中的注册中心为Eureka,同时也支持其它服务来进行服务注册与发现.本文介绍使用Consul来实现服务注册与发现,并整合进Spring Cloud项目中进行使用. 本文 ...
- .net core grpc consul 实现服务注册 服务发现 负载均衡(二)
在上一篇 .net core grpc 实现通信(一) 中,我们实现的grpc通信在.net core中的可行性,但要在微服务中真正使用,还缺少 服务注册,服务发现及负载均衡等,本篇我们将在 .net ...
- 基于 Consul 实现 MagicOnion(GRpc) 服务注册与发现
0.简介 0.1 什么是 Consul Consul是HashiCorp公司推出的开源工具,用于实现分布式系统的服务发现与配置. 这里所谓的服务,不仅仅包括常用的 Api 这些服务,也包括软件开发过程 ...
- Consul 服务注册与服务发现
1. 服务注册 对 Consul 进行服务注册之前,需要先部署一个服务站点,我们可以使用 ASP.NET Core 创建 Web 应用程序,并且部署到 Ubuntu 服务器上. ASP.NET Cor ...
最新文章
- IOS笔记 #pragma mark的用法
- [转载] 英语科技论文写作——Difference between APAMLA
- pyplot绘图标题错误处理
- 练习. SQL--选修课程练习
- java 关闭另一个jvm_JVM安全退出(如何优雅的关闭java服务)
- xshell使用xftp传输文件和使用pure-ftpd搭建ftp服务
- 图像处理Pillow详解
- 李开复:有三个AI专家就能估值7亿的时代过去了
- 可口可乐造型设计_瓶型设计的一些观点
- Python Tricks(二十)—— 阶乘的极简实现
- C++中异常处理中的构造和析构
- 第二阶段团队站立会议04
- 【知识图谱系列】解耦Transformation和Propagation的深度图神经网络
- 微星主板黑苹果_黑苹果安装教程:准备磁盘+主板BIOS设置——墨涩网
- textView设置粗体以及textView文字中划线
- Python opencv局部直方图均衡增强
- JAVA使用465端口与25端口实现发送邮件的业务,以及执行时常见的错误解决方法
- Openpcd安装过程记录
- 在线JSON格式化-工具栈
- 从语言之争到年龄焦虑