Elim的博客

用来记录一些原创性的总结


Elim

初体验

Spring Cloud旨在为一些常见的场景提供一些开箱即用的功能,它是基于Spring Boot构建的。本文将作为Spring Cloud的应用入门篇讲解如何快速的利用Spring Cloud搭建起一套可运行的开发环境。

Eureka

Eureka是Netflix提供的一套基于Rest的服务发现框架,它包含服务端和客户端。服务端用来提供服务注册服务,客户端可以通过服务端进行服务注册和从服务端获取服务提供者信息。Spring Cloud把它集成进来了。需要使用Eureka Server可以新建一个Maven工程,指定项目的parent为spring-cloud-starter-parent,下面介绍的所有的相关工程的parent都定义为spring-cloud-starter-parent

<parent>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-parent</artifactId>
    <version>Finchley.SR1</version>
</parent>

然后添加添加spring-cloud-starter-netflix-eureka-server依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

然后在配置类上添加@org.springframework.cloud.netflix.eureka.server.EnableEurekaServer

@SpringBootApplication
@EnableEurekaServer
public class Application {

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

然后可以通过application.properties或application.yml定义一些配置信息,比如通过server.port指定Web服务器的端口号,Eureka Server也会包含客户端的信息,用于Eureka Server和Eureka Server之间的通信,但是当我们的Eureka Server只有一台时,我们作为服务端的时候通常是不需要把它自己作为一个服务注册到服务器上的,此时可以通过eureka.client.registerWithEureka=false定义其不在服务端注册,通过eureka.client.fetchRegistry=false定义不从服务器拿服务信息,否则默认会访问http://localhost:8761/eureka/服务地址进行服务注册和获取服务信息。然后只有一台时指定defaultZone为它本身。

server.port: 8761
eureka:
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:${server.port}/eureka/

通过运行上面的Application.java启动了Eureka Server后就可以通过Eureka Server的IP和端口号访问Eureka Server的控制台查看上面注册的服务了,比如对于上面的配置可以在本机访问http://localhost:8761,然后可以看到类似如下这样的界面。

Eureka Server Console

服务提供者

服务提供者需要在Eureka Server上注册服务,所以服务提供者本身将作为一个Eureka Client,我们需要在依赖中添加Eureka Client的依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

我们的服务会希望通过SpringMVC对外发布,提供Rest服务,所以我们添加如下依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

服务提供者需要在application.properties或application.yml中通过spring.application.name指定服务提供者的名称,到时候客户端将通过该名称来寻找服务提供者。需要通过eureka.client.serviceUrl.defaultZone指定默认使用的Eureka Server的地址,不指定时默认是http://localhost:8761/eureka/。可以通过eureka.instance.preferIpAddress=true指定需要获取当前服务提供者的hostname时返回当前的IP地址。每个服务提供者的实例需要有一个全局的唯一标识,可以通过eureka.instance.instanceId来指定,下面的配置指定了实例ID为${spring.cloud.client.ip-address}:${server.port},其中spring.cloud.client.ip-address将被自动替换为当前机器的IP地址。

spring.application.name=spring-cloud-service-provider
server.port=8900
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
eureka.instance.preferIpAddress=true
eureka.instance.instanceId=${spring.cloud.client.ip-address}:${server.port}

服务提供者可以作为一个普通的Spring Boot应用启动,无需特别的配置信息。

@SpringBootApplication
@EnableHystrixDashboard
public class Application {

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

}

具体的服务实现就是开发一个普通的@RestController类,并配置对应的映射路径,比如下面代码中建立了一个具体的服务实现,通过请求/hello可以请求到下面的sayHelloWorld(),将响应对方hello world。

@RestController
@RequestMapping("hello")
public class HelloController {

    @GetMapping
    public String sayHelloWorld() {
        return "hello world.  -- " + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
    }
    
}

服务调用者

服务调用者需要从Eureka Server上获取提供某种服务的服务提供者信息,它从Eureka Server获取到了服务提供者的信息后会缓存在本地,这样即使Eureka Server后续挂了,服务调用者也能从本地缓存获取服务提供者信息并进行调用。服务调用者对应于Eureka Server而言,也是一个Eureka Client。需要添加Eureka Client的依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

在笔者的示例中需要使用OpenFeign作为Rest客户端实现,所以添加如下依赖。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在笔者示例中的服务调用者本身又是需要向外部提供Rest服务的,所以还添加了spring-boot-starter-web依赖。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

作为Eureka Client,需要配置Eureka Server的地址,然后因为我们的应用是作为调用者的,如果它不需要作为服务提供者对外提供服务,可以配置eureka.client.registerWithEureka=false。当然了,如果你想在Eureka Server上看到当前有哪些Eureka Client与它相连,而不管它是作为服务提供者还是服务调用者,那可以都在Eureka Server上进行注册。

eureka.client.registerWithEureka=false
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/

OpenFeign作为Rest客户端,需要为客户端声明一个接口,然后使用Rest的相关注解,也可以是SpringMVC的注解。需要在接口上标注@FeignClient,同时通过其value属性指定服务提供者(或者说指定需要的服务),这里就配置为服务提供者应用配置在application.properties中的spring.application.name。然后在接口方法上使用@RequestMapping指定接口对应的远程服务提供者发布的路径(相对路径)。

@FeignClient("spring-cloud-service-provider")
public interface HelloService {

    @GetMapping("hello")
    String helloWorld();
    
}

然后在配置类上添加@EnableFeignClients,其会自动扫描配置类所在的package及其子package下的@FeignClient类,并把它们注册为一个bean。不想使用默认扫描包,也可以通过@EnableFeignClients进行配置。以前的版本中还需要在配置类中添加@EnableDiscoveryClient,现在已经不需要了。

@SpringBootApplication
@EnableFeignClients
public class Application {

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

}

然后就可以在需要的地方把@FeignClient标注的接口作为一个普通的bean进行注册了,比如下面定义了一个HelloController,其注入了标注了@FeignClient的HelloService,其helloWorld()对应请求的是HelloService的helloWorld(),其进而会访问一个spring-cloud-service-provider服务提供者的/hello路径获取返回结果。

@RestController
@RequestMapping("hello")
public class HelloController {

    @Autowired
    private HelloService helloService;
    
    @GetMapping
    public String helloWorld() {
        return this.helloService.helloWorld();
    }
    
}

看到了我们在服务调用端新建的HelloService接口,你会不会想可以把该接口定义为一个公用接口,可以同时由服务提供方和服务调用方实现呢?Spring Cloud官方是不建议我们这么做的,因为这会增加服务调用方和服务提供方之间的耦合性。不使用相同的接口,客户端接口可以只定义部分其需要的接口方法或者将服务提供方多个Controller提供的接口定义到同一个客户端接口中。

以上就是Spring Cloud初体验的全部内容,笔者通过介绍Eureka Server、Eureka Client和OpenFeign介绍了服务的注册、发现和Rest客户端自动实现,将它们结合起来组成了一个基本的Spring Cloud应用的示例。Eureka包含Eureka Server和Eureka Client,其中Eureka Client又分Application Service和Application Client,它们之间的关系可以用下面这张图来表示。

eureka_architecture.png

(注:本文是基于Spring cloud Finchley.SR1所写)