肝了两万字的springcloud,错过了血亏!
前言
我们会首先介绍一下网络中的名词,让大家更好的更熟悉的去理解复杂业务系统之间的交互,如果你是一个小萌新,千万不要被网络中的各种名词唬住,你只需要记住,万变不离其宗,这些只是五花八门的名词而已,原理大概率也就那么回事~
熟悉我的小伙伴都知道我不是计算机软件专业的,是一个完完全全的自学党,就像我刚刚大三的时候,开始自学Java,因为我专业不学,所以只能自学咯,至于具体的原由,大家移步到我的个人生活篇,来走进我的内心~
当时的我,也算是Java界的小萌新,学习spring的过程中听到IOC和AOP这种名词,当时的感觉只能说是牛逼,感觉瞬间档次就上来了,然后自己的懵逼感也是成倍的增加,当时大概的感觉就是完全被这些个花花名词给唬住了
再说一些小插曲
在学习Java的过程中就接触到了当时感觉超级牛X的SSM,我竟一度认为只要我学会SSM,就可以很厉害
你能不能想象当时我自己按照视频教程搭建了一套SSM,然后画了一个巨抽象的页面和写了几个巨简单的数据库查询接口,JDBC,大家都懂得,直接一套连接,增删改查,哎哟喂,当时的那个心情啊,简直就是叫花子哼着太平调-穷开心
现在想起来真是不知天高地厚
这一篇没有什么源码的讲解,基本都是通过聊天的方式来让大家在潜移默化中熟悉网络中的一些基础概念和了解springcloud中的部分常用组件的功能
先来个简介
先给大家看一张图,如果你完全明白整个架构图,恭喜你,太棒了,如果你能够自己画出来整个结构图,能够深刻的理解每个组件的真正作用,该组件在此系统中的存在的意义,那恭喜你,下次面试直接架构师起步
哈哈,开个玩笑,简单给大家说一遍先,大家先尝试着理解一遍
客户端请求进来之后,首先是Nginx负载均衡,这个大家应该都听过吧,属于一个高性能的HTTP和反向代理的服务器,反向代理是对客户端无感知的,这个东西的作用就是让非常多的客户请求均匀的分配到相应的服务器
接下来就是服务网关,这个东西还是很重要的,很多公司也都会用,属于跨服务的代理,可以进行路由转发,鉴权等,保护系统很有用
然后我们内部看到很多的业务服务群service,这些当然就是我们平时接触最多的服务咯,你每天写的接口就是属于这一层的,我们可以看到在服务的左右侧都有很多东西,这些小组件各个都是小能手,各有各的能耐
属于天生我材必有用的那种
Eureka和Consul都是属于服务治理组件,写的业务service就是注册到这里,然后可以被其它的服务发现并且调用
Config这个属于一个配置中心,每个服务都会有很多配置文件,这些可能会有很多公共的地方,而且也比较难于管理,甚至你改一个配置文件都需要重新部署这个项目,用了这个我们就不用每次都重启服务啦,可以实现"热部署"了
Hystrix容错保护+Turbine集群监控+Sleuth链路跟踪,这一套下来,保护系统不会挂掉,可以进行服务降级,出现问题有迹可循
springcloud bus消息总线,config可以解决不重启情况下更新配置,但是我们需要一个服务一个服务的发送post请求,也是稍微有点麻烦的,这种情况下大家应该想到了消息队列的发布订阅模型了吧,我们通过bus就可以实现,只需要在config server端发起refresh,就可以触发所有微服务的更新了
springcloud task任务调度,定时任务,比如每天凌晨一点清空昨天的无用数据
springcloud data flow数据流操作,用于处理数据量较大的处理,批量运算和持续运算这种
和springcloud stream流操作,解决系统中不同中间件的适配问题,自动适配,给不同的MQ之间进行切换
什么是spring cloud
springcloud属于一个系列框架的有序集合,就是一个全家桶系列。不知道大家知道Spring boot吗,cloud就是基于boot,boot更像是一个便捷的单点开发框架,帮助大家解决各种配置文件的烦恼
而cloud呢,是一个大集合,基于springboot的,为了微服务体系开发中的架构问题,属于一套组件,提供了一整套的解决方案
它利用spring boot的开发便利性巧妙的简化了分布式系统基础设施的开发,比如服务发现注册、消息总线、配置中心、负载均衡、熔断器这些,都可以用springboot的开发风格来做到一键启动和部署,更为便捷
springcloud中的微服务是可以独立部署、水平扩展,以及独立访问,springcloud属于这些组件的大管家,在这一点类似于我们说的Hadoop全家桶吧,包含很多相应的处理框架,当然Hadoop本身也是一个大数据处理框架
集群
先来看下百度百科对于集群技术的讲解,两个关键词,相互独立和组,特点:
1、高可用性,独立性,A挂了并不会对系统造成太大影响,B还可以继续工作
2、提高效率,集群可以提高系统的工作效率
我们一起来看例子:你厌倦了上班的生活,决定换一种活法,于是决定,辞职创业!
你有了一个好点子,开发了一个打车小程序滴滴打打,刚开始写好了直接发布,四面八方的用户来注册了,前期没问题,服务器顶得住,但是随着用户量越来越大,你部署的一台服务器顶不住了,怎么办呢,加内存,加CPU,但是这些还是不够用的
于是有了一个简单暴力的法子,加机器!只有花钱才能变强,没错,原先是只有一台服务器A来提供服务,现在加了个服务器B,A和B可以提供同样的服务,A挂了B可以顶上,A和B同心协力,A和B就组成了一个小集群!
集群,用于给各个服务器间分担压力!
分布式和微服务,傻傻分不清楚
从概念理解,分布式服务架构强调的是服务化以及服务的分散化,微服务则更强调服务的专业化和精细分工
从实践的角度来看,微服务架构通常是分布式服务架构,反之则未必成立。所以,选择微服务通常意味着需要解决分布式架构的各种难题。
这样来看可能比较晦涩难懂,我们继续来看例子:
随着我们的滴滴打打系统发展越来越庞大,作为一个后端工程师,首先要考虑的当然是后端了,你想啊,后端代码越来越多,越来越臃肿,于是,微服务就上场了
我们可以把这一个系统按照业务划分为用户模块、支付模块、订单模块等等,每个服务内部是独立的,对外开放接口,供外部调用
这样后端开发起来也是很方便的,对于系统的架构也都是独立的,怎么说呢,就是比如你这次需求的改动只是针对于用户模块这一个服务,你就只需要重启用户模块即可了,别的服务不需要重启,至于别的服务调用用户模块服务也会进行热切换(只有当新的服务部署成功了,运行稳定了,才会下掉老服务
微服务的设计是为了不因为某个模块的升级和BUG影响现有的系统业务。微服务与分布式的细微差别是,微服务的应用不一定是分散在多个服务器上,他也可以是同一个服务器。
至于分布式呢,你想啊,如果用户模块这个服务只有一个,只部署在一个机器上,那这就是个大漏洞,是个很糟糕的设计,如果这个机器挂掉了,那这个用户模块就GG了,整个系统应该也就完蛋了,因为用户模块属于很核心的,各个服务都和它息息相关
于是就有了分布式,我们把这个用户模块服务,部署在多台机器上,当然不一定是所有机器都部署这个服务,够用就好,剩下的内存空间留给别的服务咯,你这一个机器基本不可能只部署这一个服务的,这样太浪费机器了,直白点就是,太浪费钱了!
看上面的图,大概就这意思,这个系统有ABC三个机器,没了哪个机器,这个系统都暂时可以使用,当然承受压力的能力会小一些,这个时候运维工程师就出动了,需要检查这个机器到底是怎么了,来尽快的恢复该机器的运转,加入这个分布式
总结一下:微服务:分散能力。分布式:分散部署。
CAP理论
CAP理论这个大家应该听的耳朵都出茧子了吧,不知道大家有没有真正的理解
Anyway,看完这篇文章之后,你就肯定懂了
先看下CAP分别代表什么
一致性(Consistency):所有节点访问同一份最新的数据副本
可用性(Availability):每次请求都能获取到非错的响应,但是不保证获取的数据为最新数据
分区容错性(Partition tolerance):分布式系统在遇到任何网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务,除非整个网络环境都发生了故障
简单看下每个代表的含义:
一致性,这里的一致性指的是强一致性,即每个节点的数据都是最新的版本,其实一致性级别还有弱一致性和最终一致性这种
弱一致性:弱一致性是相对于强一致性而言,它不保证总能得到最新的值;
最终一致性(eventual consistency):放宽对时间的要求,在被调完成操作响应后的某个时间点,被调多个节点的数据最终达成一致
在写操作完成后开始的任何读操作都必须返回该值,或者后续写操作的结果,也就是说,在一致性系统中,一旦客户端将值写入任何一台服务器并获得响应,那么之后client从其他任何服务器读取的都是刚写入的数据
可用性:系统中非故障节点收到的每个请求都必须有响应,在可用系统中,如果我们的客户端向服务器发送请求,并且服务器未崩溃,则服务器必须最终响应客户端,不允许服务器忽略客户的请求
分区容错性:允许网络丢失从一个节点发送到另一个节点的任意多条消息,即不同步
CAP三者不能同时满足
这一点大家应该也听过了,但是为什么呢,可能有很多人就不晓得了,我们来用反证法证明一下
假设都满足,首先满足分区容错性,那就意味着节点A和节点B的数据会存在一些偏差,如果满足可用性,则证明有用户发来请求,则必须响应,也就意味着响应给客户端的请求会存在一些数据偏差,因为不同的请求可能打到不同的节点
此时就不满足数据一致性了,三者是矛盾的,不能同时成立
注册中心
随着系统的架构越来越复杂,将业务切分成多个独立的服务,来降低系统的耦合性,而每个服务又要部署在多个机器上,来保证系统的稳定性
林子大了,自然是什么鸟都有,于是管理这些服务的组件自然就诞生了,系统中包含用户中心、注册中心、支付中心、订单中心这些,用户中心可能部署到多个服务器上,这也就意味着多个IP和端口号,那难道在每个调用用户中心服务的地方都去把这些IP和端口号都写死在调用的地方吗
这样肯定是不合适的,而且如果后续有大流量进来,决定临时加机器,还要来修改代码,肯定是不人性化的,不符合我们的解耦合原则
对于我这种北漂族来说有一个形象的例子给到大家,租房子,你首先想到的是找房东还是中介呢,我反正想到的是中介。房东就是服务提供者Provider,我就是服务消费者Consumer,但是呢,我没法找到相应的房东,因为房东太多了,我也不知道该怎么找
如果单靠房东和类似我这种租客来交互是很麻烦的,效率也是很低的,于是就出现了中介这个行业,中介这个行业进行信息的汇总和派遣,利用信息差来赚钱
房东想租房子了,把信息注册到中介,我想租房子了,也联系中介,中介便可以为我找到合适的房源
再看Eureka
在众多的微服务中,管理起来也是一件麻烦事,于是乎就有了Eureka注册中心这个家伙。Eureka就和上面的中介一样,去汇总服务提供者和消费者的信息,做一个第三者
服务的提供者:服务的注册,服务的同步,服务的续约
服务的注册:启动的时候会通过发送REST请求的方式将自己注册到Eureka Server上,同时带上了自身服务的一些元数据信息
服务的同步:在实际的生成环境中,注册中心往往是集群部署的,这个时候就会涉及到服务的同步,同步的前提条件就是:注册中心首先要相互注册,这样当一个服务提供者发起请求到注册中心进行注册,它会将该请求转发给集群相连的其他注册中心,实现了注册中之间的服务同步。
服务的续约:完成注册之后,注册中心就通过心跳机制检查各个服务是否还存活着,如果超过了配置参数定义的是就会将服务移除注册中心。
服务的消费者:获取服务、服务调用、服务下线
获取服务:消费者服务进行消费的的时候会发起一个REST请求到注册中心获取注册到注册中心的服务列表,这个服务列表按照上面配置会30s中更新一次。
服务调用:消费者在获取服务清单后,通过服务名获取服务的实例名,和该实例的元数据(ip + 端口等),Eureka底层会通过Ribbon默认采用讯轮的方式进行调用从而实现了客户端的负载均衡。
服务下线:我们服务重启或者关闭服务的时候,要主动告诉注册中心。当服务正常关机的时候回发起一个REST请求到注册中心。注册中心收到请求之后就会把服务从注册列表中剔除。
注册中心:实效剔除、自我保护
实效剔除:上面讲了服务的下线是服务正常关闭。当服务非正常关闭(内存溢出,网络故障等)服务不能正常工作,而注册中心没有收到服务下线的请求,这个时候该肿么办呢?注册中心会定时没每隔一段时间(默认是60s)将服务列表超时的服务(90s)没有续约的服务剔除出去。
自我保护:之前我们知道服务注册到注册中心之后会维持一个心跳机制,当然心跳也会有失败的时候,这个时候注册中心,会统计失败比例在15分钟之后低于百分之85的,会将当前服务保护起来,当这些服务不会过期。在保护的这个阶段消费者很可能调用了不存在的服务,所以消费者一般都会有容错机制,请求重试,断路器等当然这个自我保护可以通过以下配置进行自行配置。
负载均衡
来想个场景,用户服务A部署在1、2、3三台节点上,并且我们通过上述注册中心可以拿到这三台节点的IP和port,但是问题也随而来了,调用哪一台呢
小孩子才做选择,我全都要!!
在调用用户服务A的时候,会将请求打到相应的节点上,但是在选择节点上也是一个大问题,在一些小系统的时候这些其实倒还好一些,只要服务器顶得住,即使全打到同一个节点上,也能顶得住
但是当系统扩大到一定规模的时候,如果请求我们没法合理的控制去向,则会变成灾难,你想啊,本来20台机子上都部署了用户服务,结果呢,请求全都打到其中一个机器上了,那不GG,老板这下该骂娘了,花了西瓜的钱,结果只用了个西瓜皮,别的全打水漂了
问题我们说清楚了,于是负载均衡闪亮登场了
大家在此之前可能听过负载均衡这一概念,负载均衡又可以分为两种,客户端负载均衡和服务端负载均衡
客户端负载均衡,服务清单保存在客户端,客户端进行负载均衡算法,在请求发出之前已经确定了这个请求的去向,springcloud下的ribbon就是属于客户端负载均衡
服务端负载均衡,服务清单保存在服务端,对于客户端是无感知的,对外开放一个域名,所有客户端都往这上面打请求。最熟悉的就是Nginx了,这个用的很多
再提一嘴,我们也可以根据节点的性能去控制请求的数量,比如某个机器就是牛x,我们可以通过调节算法,让请求更青睐于这个节点
再看Ribbon
Ribbon的核心在RestTemplate,RestTemplate是Spring提供的一个访问Http服务的客户端类
怎么说呢?就是微服务之间的调用是使用的RestTemplate。比如这个时候我们 消费者B 需要调用 提供者A 所提供的服务我们就需要这么写。如我下面的伪代码
@Autowired
private RestTemplate restTemplate;
// 这里是提供者A的ip地址,但是如果使用了 Eureka 那么就应该是提供者A的名称
private static final String SERVICE_PROVIDER_A = "http://localhost:8081";
@PostMapping("/dayu")
public boolean dayu(@RequestBody Request request) {
String url = SERVICE_PROVIDER_A + "/service1";
return restTemplate.postForObject(url, request, Boolean.class);
}
负载均衡,不管Nginx还是Ribbon都需要其算法的支持,Nginx使用的是轮询和加权轮询算法。而在Ribbon中有更多的负载均衡调度算法,其默认是使用的RoundRobinRule轮询策略。
RoundRobinRule:轮询策略。Ribbon默认采用的策略。若经过一轮轮询没有找到可用的provider,其最多轮询 10 轮。若最终还没有找到,则返回 null。
RandomRule: 随机策略,从所有可用的 provider 中随机选择一个。
RetryRule: 重试策略。先按照 RoundRobinRule 策略获取 provider,若获取失败,则在指定的时限内重试。默认的时限为 500 毫秒。
算法还有很多,这里就不一一列举了,我们也是可以根据自己实际的需求自定义负载均衡策略的。
实现起来也很简单:继承AbstractLoadBalancerRule类,重写public Server choose(ILoadBalancer lb, Object key)即可。
Feign和OpenFeign
有了 Eureka,RestTemplate,Ribbon我们就可以进行服务间的调用了,但是使用RestTemplate还是不方便,我们每次调用的时候都要通过RestTemplate的API来进行服务的调用,感觉用起来总是不如意,没有那种直接调用方法的便捷
不知道大家有没有使用过Dubbo,Dubbo就是可以直接使用的,比较方便,直接使用@Reference注解即可
@Reference
CustomerService customerService;
Private void test(){
customerService.login(username , password);
}
Feign就是在内部集成,属于Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端,内置了Ribbon,用来做客户端的负载均衡,去调用服务注册中心的服务。但是有一个弊端,Feign不支持Spring MVC的注解,这在开发中属于比较麻烦的一点,因为很多公司都是在用MVC的
于是OpenFeign诞生了,OpenFeign是Spring Cloud 在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。
OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务
熔断降级Hystrix
到目前为止,我们的系统看起来可以正常的运转了,客户端可以通过注册中心get到服务端列表,通过负载均衡将请求平均的发放到服务端
但是,但是,但是,网络吗,毕竟没有百分百保证的事情,就像TCP的三次握手无法得到百分百的确定一样,你确定经过三次握手之后,客户端和服务端就百分百的连接上了吗
在高并发的情况下,如果突然一个服务不可用了,或者说服务响应比较慢,可能导致所有的请求都会处于延迟,后续可能会因为超时报错,如果一直这样,会出现多次重试,多次重试就会导致线上的线程一直被占用,持续的请求打进来,就会直接把系统打崩
于是针对这种问题,就出现了一个对系统的保护机制,springcloud hystrix实现了断路器、服务降级、线程隔离等多个保护系统的功能
熔断器hystrix的道理很简单,可以实现快速失败。现在家庭里面都有那种电力保护器,当用电量超过一定的负载的时候,电力保护器会自动切断电源,来保护整个电路系统的运转。
放在我们开发的系统中来,如果在一段时间内侦测到很多同样的错误,就会认为这个服务短时间不可用,不再访问远程服务,直接返回失败,使程序不影响后续的执行,防止应用程序不断的重试,浪费CPU和线程资源
我们来看三个点:断路器、Fallback、资源隔离
断路器:这里有三个重要参数,滑动窗口大小20、熔断开关间隔5秒、错误率50%,这三个参数也很好理解,每当20个请求中,错误率达到50%,断路器则会切换到开路状态,这时所有请求都直接失败不会发送到后端服务。
断路器保持开路状态5秒之后,自动切换到半开路状态,此时断路器尝试放开一个请求,看这个是否可以成功,如果请求成功,则断路器切换回闭路状态,即正常访问服务。失败则重新切换到开路状态
Fallback:Fallback相当于是降级操作,对于查询操作,我们可以实现一个fallback方法, 当请求后端服务出现异常的时候, 可以使用fallback方法返回的值,fallback方法的返回值一般是设置的默认值或者来自缓存
资源隔离:为每一个服务提供一个线程池来实现资源隔离,这样做的优点显而易见,就是运行环境被隔离开了,如果某个服务出现异常,不会影响到其它服务的运行,但是带来的代价就是维护多个线程池会对系统带来额外的性能开销
当然,还可以配置Hystrix仪表盘,它主要用来实时监控Hystrix的各项指标信息。通过Hystrix Dashboard反馈的实时信息,可以帮助我们快速发现系统中存在的问题,从而及时地采取应对措施。
题外话:舱壁模式,隔离了每个工作负载或服务的关键资源,如连接池、内存和 CPU。使用舱壁避免了单个工作负载(或服务)消耗掉所有资源,从而导致其他服务出现故障的场景。这种模式主要是通过防止由一个服务引起的级联故障来增加系统的弹性。
这里的资源隔离就是使用的舱壁模式,熟悉Docker的应该知道这么模式,Docker就是通过舱壁模式实现进程的管理的,使得容器和容器之间相互隔离
网关
在上面我们学习了 Eureka 之后我们知道了服务提供者是消费者通过Eureka进行访问的,即Server是服务提供者的统一入口。那么整个应用中存在那么多消费者需要用户进行调用,这个时候用户该怎样访问这些消费者工程呢?
当然可以像之前那样直接访问这些工程。但这种方式没有统一的消费者工程调用入口,不便于访问与管理,而 Zuul 就是这样的一个对于消费者的统一入口。
对外提供接口就会涉及到相应的路由规则、校验和鉴权这些功能了,你想啊,支付模块的支付服务那不得登录之后才能调用了吗,这里要是不校验用户的登录状态,岂不问题大了
简单来讲网关是系统唯一对外的入口,介于客户端与服务器端之间,用于对请求进行鉴权、限流、路由、监控等功能
了解zuul和Gateway
Filter是Zuul的核心,用来实现对外服务的控制。
Filter的生命周期有4个,分别是“PRE”、“ROUTING”、“POST”、“ERROR”,整个生命周期可以用下图来表示。
Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。
PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
ERROR:在其他阶段发生错误时执行该过滤器。除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。
Gateway:Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
zuul和Gateway的异同点
底层都是servlet,都是属于web网关,处理HTTP请求
zuul仅支持同步,gateway支持异步,理论上gateway更能提高系统的吞吐量
gateway对比zuul多依赖了spring-webflux,在spring的支持下,功能更强大,内部实现了限流、负载均衡等,扩展性也更强,但同时也限制了仅适合于Spring Cloud套件,zuul则可以扩展至其他微服务框架中,其内部没有实现限流、负载均衡等。
配置中心config
当我们的系统变得越来越庞大的时候,每个微服务都有属于自己的配置文件,如果我们需要修改某些服务的配置,就需要找到这个服务的配置修改,并且重启该服务才可以生效,这样管理起来比较麻烦
那么有没有一种方法既能对配置文件统一地进行管理,又能在项目运行时动态修改配置文件呢?
你想一下,我们的应用是不是只有启动的时候才会进行配置文件的加载,那么我们的Spring Cloud Config就暴露出一个接口给启动应用来获取它所想要的配置文件,应用获取到配置文件然后再进行它的初始化工作。
看一下config的架构
等同于是把多个服务的配置文件集中起来管理。此时你肯定会有一个疑问,如果我在远程仓库修改了生产的配置文件,那对应的引用会不会随之立即生效呢,实现动态的配置吗
答案是不能!
啊呸,你上面不是说可以在项目运行的时候可以动态修改配置文件,你现在又告诉我不能,你这不是在浪费我的青春吗
别急,既然我说了,那自然是有解决办法,springcloud bus就闪亮登场了
我们一般会使用Bus消息总线 +Spring Cloud Config进行配置的动态刷新。可以简单理解为Spring Cloud Bus的作用就是管理和广播分布式系统中的消息,也就是常说的广播模式。
拥有了Spring Cloud Bus之后,我们只需要创建一个简单的请求,并且加上@ResfreshScope注解就能进行配置的动态修改了
总结一下
这篇文章中我带大家初步了解了Spring Cloud的各个组件,他们有
Eureka 服务发现框架
Ribbon 进程内负载均衡器
Open Feign 服务调用映射
Hystrix 服务降级熔断器
Zuul 微服务网关
Config 微服务统一配置中心
Bus 消息总线
这个时候我再把开头的图给大家扔过来,大家看的是不是更清楚了,是不是感觉每个组件的作用都了然于心,证明你对微服务的架构有了一定的了解了
同时,也证明你从这篇文章中收获到了一些东西(暗示.jpg)
结束语
船长希望有一天能够靠写作养活自己,现在还在磨练,这个时间可能会持续很久,但是,请看我漂亮的坚持
感谢大家能够做我最初的读者和传播者,请大家相信,只要你给我一份爱,我终究会还你们一页情的。
船长会持续更新技术文章,和生活中的暴躁文章,欢迎大家关注【Java贼船】,成为船长的学习小伙伴,和船长一起乘千里风、破万里浪
哦对了,后续所有的远程文章都会更新到这里
https://github.com/DayuMM2021/Java