最近两年,java技术圈最火爆的技术架构莫过于SpringCloud的出现,但其实SpringCloud也并非是一个新的技术架构,SpringCloud是以Springboot为基础,并且集成目前最优秀的一些组件和技术,SpringCloud应该说是java技术架构的集大成者。
SpringCloud的出现是有很大社会需要的,目前各大公司提供的服务数量众多且复杂,服务之间交互往往复杂且难以管理。而SpringCloud的出现即是对这一现状的回应。那么数量众多的服务之间SpringCloud是如何治理的呢,这就得说到Eureka和feign了。那么什么是Eureka和feign呢。下面先看官方给的定义:
Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。
Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API。Spring Cloud Feign是基于Netflix feign实现并对其进行了增强,整合了Spring Cloud Ribbon和Spring Cloud Hystrix,除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式。
通过上述说明可以看出Eureka提供了服务的注册与发现,feign则提供了基于REST规范的接口调用,且feign默认集成了Ribbon负载均衡和Hystrix断路器,即feign是服务之间沟通的桥梁。
那下面我们就分别说说Eureka和feign以及他们是如何实现服务的注册发现与调用。
一、Eureka
Eureka包含两个组件:Eureka Server和Eureka Client。
Eureka Server提供服务注册服务,一个服务声明了为Eureka Server,那该服务就是服务注册中心。其他声明了为Eureka Client的服务启动后,会在Eureka Server中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。
Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。
Eureka Server之间通过复制的方式完成数据的同步,Eureka还提供了客户端缓存机制,即使所有的Eureka Server都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。综上,Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性。
Eureka的系统结构就是有一个服务注册中心(服务注册中心也可以集群,即多个中心)。其他的服务既是服务提供者,也是服务消费者。如图所示:
下面我们来一步步实现Eureka Server和Eureka Client。
1、Eureka Server服务注册中心:
使用idea创建一个Eureka Server服务,可以采用创建一个空模板的方式创建项目,也可以利用idea中自带的Eureka Server的模板创建。如果采用模板创建如下:
这样就使用模板简单的创建了一个服务注册中新的项目。但其实无论采用模板还是不用模板都不影响我们项目的使用。下面是服务注册中心的配置:
UTF-8 UTF-8 1.8 1.5.13.RELEASE Edgware.SR4 9.5.0 2.6.1 org.springframework.cloud spring-cloud-dependencies ${spring.cloud.version} pom import org.springframework.cloud spring-cloud-starter-eureka org.springframework.cloud spring-cloud-starter-netflix-eureka-server org.springframework.boot spring-boot-starter true org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-test test
以上为pom.xml文件的依赖。其中重点是
org.springframework.cloud spring-cloud-starter-eureka org.springframework.cloud spring-cloud-starter-netflix-eureka-server
这两个依赖可以让我们创建的项目声明并变成一个Eureka Server服务注册中心。dependencyManagement标签的作用在于对依赖的管理。主要是依赖版本的统一管理。然后具体引入的依赖根据dependencies中的内容,此时就无需再对版本进行声明了。
下面在Springboot的主启动类中进行如下声明:
package com.yougu.eureka.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @SpringBootApplication @EnableEurekaServer public class EurekaServerApplication { public static void main(String[] args){ SpringApplication.run(EurekaServerApplication.class, args); } }
此处关键在于引入注解@EnableEurekaServer,使我们的服务声明为一个Eureka Server服务注册中心。当然,我们只在这里声明了还不够,还需要相应的配置(建议默认在application.yml文件配置,即springboot的约定):
server: port: 8070 spring: application: name: eureka eureka: instance: hostname: localhost prefer-ip-address: true client: register-with-eureka: false fetch-registry: false registry-fetch-interval-seconds: 5 serviceUrl: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
1)、prefer-ip-address: true #表示是否注册为ip,true表示注册为ip,false表示注册为域名,读者应根据自己的开发环境进行选择,如在局域网中,建议设置为true
2)、register-with-eureka: false #默认true,单机环境设置为false,表示不向注册中心注册自己。本服务自己是注册中心,所以选择false
3)、fetch-registry: false #默认true,单机环境设置为false,表示不需要向注册中心检索自己。本服务自己是注册中心,所以选择false
4)、 registry-fetch-interval-seconds: 5 #从eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒
5)、defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址。默认是http://localhost:8761/eureka 多个地址可使用 , 分隔。因为该服务本身是注册中心。所以应该指向自身。
需要注意的是spring: application: name: eureka这里的eureka是可以自己自定义的,和5中的说明中的eureka不是一个概念。这里仅仅是表示服务注册的名称,就像我们的名字一样,可以叫张三、也可以叫李四。但是其身为注册中心的功能是不变的。
下面我们启动EurekaServerApplication服务。启动成功后。在浏览器中访问http://localhost:8070/。则出现以下内容:
很显然,此时我们只启动了服务注册中心,所以我们看到这里并没有其他服务实例。下面我们就创建服务提供者和消费者,即Eureka Client。
2、Eureka Client
首先,我们先创建服务提供者。同样的我们可以采用模板,也可以不采用模板。如果采用模板,前几步和创建Eureka Server一样,最后一步在选择时选择Eureka Discovery即可,如下:
作为一个服务提供者/消费者,也就是Eureka Client。需要引入的核心依赖是:
org.springframework.cloud spring-cloud-starter-eureka org.springframework.cloud spring-cloud-starter-netflix-eureka-client
借此可以让我们的服务声明为服务提供者/消费者。为了大家方便,下面贴出完整的Eureka Client服务的配置:
pom.xml
UTF-8 UTF-8 1.8 1.5.13.RELEASE Edgware.SR4 9.5.0 2.6.1 org.springframework.cloud spring-cloud-dependencies ${spring.cloud.version} pom import org.springframework.cloud spring-cloud-starter-eureka org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter true org.springframework.boot spring-boot-starter-actuator org.springframework.boot spring-boot-starter-test test
启动类:
@EnableFeignClients @EnableEurekaClient @EnableAutoSwagger2 @SpringBootApplication(scanBasePackages = "com.yougu.core") public class ServerApplication { public static void main(String[] args) { SpringApplication.run(ServerApplication.class, args); } }
其中
@EnableFeignClients 注解表示声望本服务为一个feign客户端,既可以去调用远程服务,是该服务作为消费者的功能
@EnableEurekaClient 注解表示声明本服务是一个EurekaClient,即服务提供者,被其他服务消费
@EnableAutoSwagger2 注解是我本人自定义的引入Swagger的注解,读者不能直接使用,读者想要引入Swagger应该按照Swagger的集成方式集成,后续我也会给出Swagger的自定义方法。
application.yml
server: port: 8080 spring: application: name: server-dev ###eureka 配置。该服务作为Eureka服务注册者使用,也作为调用者 eureka: instance: hostname: localhost prefer-ip-address: true #注册服务的ip client: register-with-eureka: true #使用Eureka注册服务 fetch-registry: true #在本地缓存注册表 service-url: defaultZone: http://localhost:8070/eureka #Eureka服务位置
需要注意的是,因为这是一个Eureka Client,所以此服务的配置中:
defaultZone: http://localhost:8070/eureka #Eureka服务位置
即是刚才我们启动的服务注册中心。读者应该根据自己的服务注册中进行配置。
下面启动我们创建的Eureka Client服务提供者/消费者。然后在刚才的服务注册中心页面刷新,即可看到如下页面:
很显然,我们可以发现在服务注册中心多了一条服务注册的实例。SERVER-DEV,就是我们刚启动的服务 。上边还有一串红色的警告语,大意是说Eureka可能未被正确地声明,为了安全起见,实例不会过期。 这个是Eureka的一个自我保护机制,开发过程中可能会时常遇到这个,但是并不是说我们的开发有问题,现在我们可以不用管它,直接忽略即可。我们再点击下 localhost:server-dev:8080查看详情,展示了我们在application.yml中配置的info参数信息(此处就不再贴出info的配置信息了,很简单,配置不配置都不影响服务运行以及注册和调用)。:
好了,我们的服务提供者已经有了,并且已经注册到注册中心。那么我们的服务提供者到底提供了哪些服务呢,下面就可以借助swagger技术进行查看服务提供的接口了。如下:
这些接口服务是我之前已经写好的,此处就不细说swagger以及提供的接口服务了。
现在我们使用Eureka Client创建了服务提供者,同时也是消费者。但是如果我自己使用我自己的服务那就不需要远程调用了,就无需如此创建了,显然这些服务是要给其他服务调用的,下面我们还需要在创建一个服务消费者,该服务消费者使用feign客户端来优雅快捷的调用我们提供的服务。
2、Eureka Client + feign
在创建一个Eureka Client的服务消费者,其实也是提供者,那么就和我们刚才创建的demo项目是一样的。读者好好回顾下我们刚才的过程就可以了。例如我们再创建一个叫plm的服务。pom.xml和demo的一样。application.yml的配置也和demo的大致一样,只需要注意根据项目的端口号和项目名称进行相应改变即可。而plm服务的启动类如下:
package com.yougu.plm.server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; import org.springframework.cloud.netflix.feign.EnableFeignClients; @SpringBootApplication(scanBasePackages = "com.yougu.core") @EnableEurekaClient @EnableFeignClients("com.yougu.client.feign.api") public class PlmServerApplication { public static void main(String[] args){ SpringApplication.run(PlmServerApplication.class, args); } }
细心的读者可以发现此处的@EnableFeignClients("com.yougu.client.feign.api")注解指明了一个package路径。该路径就是我们在plm服务这是作为Feign客户端的接口。pim作为服务消费者,想要调用我们刚才启动的demo服务中的接口服务,那就要和demo中提供的接口保持一致。我们先看看demo提供的接口,以提供的dept接口为例:
@Api(value = "dept Api Client", description = "部门-API(幽谷)", protocols = "application/json") public interface DeptApi { String BASE_PATH = "/server/dept"; @ApiOperation(value = "通过ID查询部门信息 #幽谷/2019-05-03#", notes = "通过ID查询部门信息", nickname = "dept-get") @ApiImplicitParams({@ApiImplicitParam(name = "id", value = "查询对象", paramType = "path", dataType = "int", required = true)}) @RequestMapping(value = BASE_PATH + "/get/{id}" ,method = RequestMethod.GET) DeptDto get(@PathVariable("id") int id); @ApiOperation(value = "通过条件分页查询部门信息 #幽谷/2019-05-03#", notes = "通过条件分页查询部门信息", nickname = "dept-pageDept") @ApiImplicitParams({@ApiImplicitParam(name = "deptParamDto", value = "查询对象", paramType = "body", dataType = "DeptParamDto", required = true)}) @RequestMapping(value = BASE_PATH + "/pageDept" ,method = RequestMethod.POST) PageDatapageDept(@RequestBody DeptParamDto deptParamDto); @ApiOperation(value = "新增部门信息 #幽谷/2019-05-03#", notes = "新增部门信息", nickname = "dept-create") @ApiImplicitParams({@ApiImplicitParam(name = "deptInsertDto", value = "新增部门对象", paramType = "body", dataType = "DeptInsertDto", required = true)}) @RequestMapping(value = BASE_PATH + "/create" ,method = RequestMethod.POST) String create(@RequestBody DeptInsertDto deptInsertDto); @ApiOperation(value = "修改部门信息 #幽谷/2019-05-03#", notes = "修改部门信息", nickname = "dept-update") @ApiImplicitParams({@ApiImplicitParam(name = "deptModifyDto", value = "修改部门对象", paramType = "body", dataType = "DeptModifyDto", required = true)}) @RequestMapping(value = BASE_PATH + "/update" ,method = RequestMethod.POST) String update(@RequestBody DeptModifyDto deptModifyDto); @ApiOperation(value = "删除部门信息 #幽谷/2019-05-03#", notes = "删除部门信息", nickname = "dept-delete") @ApiImplicitParams({@ApiImplicitParam(name = "idsDto", value = "删除部门ID集", paramType = "body", dataType = "IdsDto", required = true)}) @RequestMapping(value = BASE_PATH + "/delete" ,method = RequestMethod.POST) String delete(@RequestBody IdsDto idsDto); }
那么我们在plm服务的com.yougu.client.feign.api的package下面创建一个接口作为该服务作为Feign客户端和demo接口的对接。如我们需要调用DeptDto get(@PathVariable("id") int id);接口。即创建如下:
package com.yougu.client.feign.api; import com.yougu.cient.mis.response.ApiResponse; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; @FeignClient(name = "server-dev") public interface DemoApi { String BASE_PATH = "/server/dept"; @RequestMapping(value = BASE_PATH + "/get/{id}" ,method = RequestMethod.GET) ApiResponse get(@PathVariable("id") int id); }
在plm中我们是用一个ApiResponse去接收server-dev服务的返回结果,是因为接口负责返回具体的数据类型,但是在demo中对整个服务返回的模型是本人对结果的封装成一个ApiResponse,ApiResponse结构如下。其中的data即是存放接口返回的数据类型的,在此接口中即是DeptDto。
private int code = SUCCESS_CODE; private String message = HttpStatus.OK.getReasonPhrase(); private Object data = ""; private Object errorData = ""; private Boolean success = false; private String md5; private String alg = ALG;
@FeignClient(name = "server-dev")该注解的引用表明plm作为一个Feign客户端,server-dev表明和该服务做关联,即该接口服务是向server-dev调用的。此处已经声明了接口服务了,下面就是我们在plm服务中具体去调用该接口了。因此,我们创建一个Controller如下:
package com.yougu.core.controller; import com.fasterxml.jackson.databind.ObjectMapper; import com.yougu.client.api.DeptApi; import com.yougu.client.feign.api.DemoApi; import com.yougu.client.output.dto.DeptDto; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController public class DeptController implements DeptApi { @Autowired private DemoApi demoApi; @Autowired private ObjectMapper mapper; @Override public DeptDto get(@PathVariable("id") int id) { return mapper.convertValue(demoApi.get(id).getData(), DeptDto.class); } }
在Controller中我们接收到feign客户端调用远程接口的返回对象后,我们需要对对象处理成我们需要的类型。使用jackson的convertValue转换data成DeptDto类型。
下面启动我们的plm服务,既可以验证。刷新服务注册中心页面。如下:
可以发现此时有两个服务注册实例了,即是我们启动的SERVER-DEV和PLM。
此时我们在plm中使用get服务去调用远程的server-dev的接口,即顺利的返回了我们需要的数据。过程如下:
plm请求:
可以看出我们访问的是plm的服务。然后返回了数据。
同时我们在server-dev服务中的后台日志也可以很明显的看出服务被调用:
在 server-dev服务中处理接口调用后返回的模型。
以上就是SpringCloud的微服务治理了。我们实现了服务的注册发现与调用。其中feign默认集成了负载均衡,如果存在分布式集群的环境,它会采用轮询的方式去调用服务提供者的实例。