Spring Cloud入门教程


随着工程的日渐迭代单体工程往往十分臃肿,为了实现更好的解耦与性能通常将一个工程拆分为多个子模块,每个子模块之间相互独立互不影响。

下面通过一张图更生动的展示,多个模块 Server 由服务中心 Center 实现统一管理,由服务网关 Gateway 处理对外的请求访问,分布式配置服务 Config 用于多个模块服务的配置管理。

Spring Cloud 拥有多个版本,不同版本与之对应的 Spring Boot 版本也稍有出入,如果二者版本没有相对应可能出现一系列兼容问题,二者的版本对应参考下表:

名称 对应 Spring 版本
Hoxton 2.2.x, 2.3.x
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.x

按照上述的架构图,本文将详细介绍每个功能模块的具体构建步骤。

一、注册中心

新建 register-server 工程用于充当服务注册中心,这里选择了 Dalston 版本,因此 Spring Boot 需要选择 1.5.x 版本。

1. POM依赖

在新建的工程中导入如下中的 eurekaDalston 依赖。

注意本文后续中所涉及的工程示例中 Spring 均采用 1.5.4 版本,并都导入了 Dalston 依赖,后续将不再重复说明。

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

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

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Dalston.SR1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

2. 项目配置

在工程的 YML 配置文件中添加如下信息,除了基本的端口和服务配置之外同时在 defaultZone 中指定了服务中心的地址,后续客户端将通过该地址进行注册。

server:
  port: 9090

spring:
  application:
    name: eureka-server

eureka:
  instance:
    hostname: localhost
  client:
    register-with-eureka: false
    # 禁用自身的默认客户端注册行为
    fetch-registry: false
    serviceUrl:
      # 自定义服务注册中心地址
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

3. 注册激活

在工程的启动类上添加 @EnableEurekaServer 注解表示启动服务注册中心。

@EnableEurekaServer
@SpringBootApplication
public class EurekaServiceApplication {

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

二. 服务注册

新建工程 register-client 模拟客户端服务,通过配置将注册到上述的创建的注册中心。

1. POM依赖

在新建的工程 POM 文件中导入下列依赖,略去 Spring parentDalston(参考注册中心模块)。

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

2. 项目配置

在工程 YML 文件中配置注册中心地址,即上一点中服务注册中心模块 YML 中配置的地址。

server:
  port: 9091

spring:
  application:
    name: client-1

eureka:
  client:
    serviceUrl:
      # 注册到服务中心
      defaultZone: http://localhost:9090/eureka/

3. 测试接口

在工程中添加一个测试接口,通过 DiscoveryClientgetServices() 方法可以获取注册中心中已注册的服务,返回一个集合。

import org.springframework.cloud.client.discovery.DiscoveryClient;

@RestController
@RequestMapping("/api/client")
public class ClientController {

    @Autowired
    DiscoveryClient discoveryClient;

    @GetMapping("/getServices")
    public String getServices() {
        return "Services: " + discoveryClient.getServices();
    }
}

4. 注册服务

在工程启动类添加 @EnableDiscoveryClient 注解激活 Eureka 中的 DiscoveryClient 实现。

@EnableDiscoveryClient
@SpringBootApplication
public class EurekaClientApplication {

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

完成上述准备工作之后启动 register-serverregister-client 两个工程,浏览器访问服务中心地址(http://localhost:9090)即可查看 Eureka 管理面板。

通过管理面板可以看到 client-1 已完成注册操作。

通过 Postman 等工具请求上述的 getServices 接口,即可获取当前服务中心已注册的服务节点名称,这里服务名取的是项目 YML 配置中的 application name

三、服务消费

在上面的示例中介绍了如何实现服务注册,但是当存在多个服务时,不同服务之间又该如何实现通讯呢?

下面将在之前的示例中进一步介绍如何实现服务通讯。

1. POM依赖

新建 basic-consumer 工程,并在 POM 文件中引入下列依赖,这里略去 Spring parentDalston

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <!-- Spring Boot健康监控 -->
    <!-- 控制台会输出一系列 Mapped 信息 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
</dependencies>

2. 项目配置

与之前的 register-client 模块类似,在工程的 YML 添加配置将自身注册到服务中心。

server:
  port: 9092

spring:
  application:
    name: basic-consumer

eureka:
  client:
    serviceUrl:
      # 注册到服务中心
      defaultZone: http://localhost:9090/eureka/

3. 服务注册

同样需要在工程启动类添加 @EnableDiscoveryClient 注解开启服务发现。

同时在工程中新建 TemplateConfig 配置类并注入 RestTemplate 对象,其可以理解为 Spring 中的网络客户端,用于后续发送网络请求。

@EnableDiscoveryClient
@SpringBootApplication
public class BasicConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(BasicConsumerApplication.class, args);
    }
}

@Configuration
public class TemplateConfig {

    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

4. 通讯示例

新建通讯测试接口,通过负载均衡客户端 loadBalancerClient 选择服务中心中相应的服务,如本示例选择了之前已经注册的 client-1 服务。

客户端选择返回的实例中包含了对应服务的基本信息,如服务地址与端口等等,通过获取的信息使用 restTemplate 发送请求。

@RestController
@RequestMapping("/api/basic/consumer")
public class TestController {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @GetMapping("/get")
    public String get() {
        // 通过负载均衡选择客户端实例
        ServiceInstance serviceInstance = loadBalancerClient.choose("client-1");
        // 根据选择的实例拼接请求地址
        StringBuilder builder = new StringBuilder();
        builder.append("http://")
                .append(serviceInstance.getHost())
                .append(":")
                .append(serviceInstance.getPort())
                .append("/api/client/getServices");
        // 通过 restTemplate 调用服务接口
        String url = builder.toString();
        return restTemplate.getForObject(url, String.class);
    }
}

完成上述操作之后依次启动 register-serverregister-clientbasic-cosumer 三个工程,通过 Postman 工程请求 basic-cosumer 工程的 get 接口。

可以看到通过 basic-cosumer 服务(9092)即实现了 register-client 服务中的 getServices 接口服务调用,因此 consumer 通常也作于各服务之间的通讯媒介。

四、Feign调用

basic-consumer 模块中介绍了服务之间的相互通讯,但在服务调用时相对较为繁杂,因此 Spring Cloud 中提供了更便捷的 feign 服务调用方式。

Feign 调用通常用于注册服务中心中各服务之间的请求通讯,下面将介绍如何通过 feign 实现服务之间的通讯。

1. POM依赖

新建 feign-consumer 工程并添加下述依赖,同样略去 Spring parentDalston

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <!-- feign 消费 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
    </dependency>
</dependencies>

2. 项目配置

同样的在 YML 文件中配置服务中心地址实现注册。

server:
  port: 9094

spring:
  application:
    name: feign-consumer

eureka:
  client:
    serviceUrl:
      # 注册到服务中心
      defaultZone: http://localhost:9090/eureka/

3. 服务注册

在工程启动类上同时添加 @EnableDiscoveryClient@EnableFeignClients 注解开启服务注册与 Feign 服务。

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class FeignConsumerApplication {

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

4. 消费示例

新建 FeignService 类并在类上添加 @FeignClient 注解用于指定服务节点,@GetMapping 用于指定对应服务的接口地址。

@FeignClient("client-1")
public interface FeignService {

    @GetMapping("/api/client/getServices")
    String getServices();

}

新建测试接口 get 调用上述的 feignService 实现,启动项目后通过该接口即可实现服务通讯。

与之前的 basic-consumer 示例一致,通过 Postman 等工具请求 feign-consumerget 测试接口即可实现 Client-1 服务的 /api/client/getServices 接口访问。

@RestController
@RequestMapping("/api/feign/consumer")
public class TestController {

    @Autowired
    FeignService feignService;

    @GetMapping("/get")
    public String get() {
        return feignService.getServices();
    }
}

五、配置中心

当工程中的服务达到一定数量后,为了实现配置的统一管理,通常会将 YML 等配置实现云上配置。

1. 仓库准备

GitHub 中创建一个仓库,假如存在 config-client 服务则在仓库中创建 config-client.yml 文件。

info:
  profile: default
  
ibudai:
  name: hello

2. 服务配置

新建 config-server 工程并在 Pom 文件中导入如下配置,同样略去 Spring parentDalston

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

在工程的 YML 文件中配置上述的创建的 GitHub 仓库地址,同时将自身注册到服务中心。

server:
  port: 9096

spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: https://github.com/great-jin/eureka-config
          skip-ssl-validation: true

eureka:
  client:
    serviceUrl:
      # 注册到服务中心
      defaultZone: http://localhost:9090/eureka/

在项目的启动类添加 @EnableConfigServer@EnableDiscoveryClient 注解激活配置。

@EnableConfigServer
@EnableDiscoveryClient
@SpringBootApplication
public class ConfigServerApplication {

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

完成后启动项目访问 http://localhost:9096/config-client/default/main 地址即可看到下述信息,其中 label 为仓库的分支名称。

3. 配置读取

新建 config-client 工程并在 Pom 文件添加下述依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
</dependencies>

注意读取仓库配置的工程需要将配置文件替换为 boostrap.yml,其与 application.yml 的主要区别在于前者的执行优先级更高。

boostrap.yml 中下述服务配置信息,其中 uri 为上述的 config-server 服务运行地址,profile 为多配置的区分标识, label 为仓库的分支名称。

server:
  port: 9097

spring:
  application:
    name: config-client
  cloud:
    config:
      uri: http://localhost:9096/
      profile: default
      label: main

eureka:
  client:
    serviceUrl:
      # 注册到服务中心
      defaultZone: http://localhost:9090/eureka/

添加测试接口用于读取仓库配置文件值,启动项目后请求接口接口获取仓库中配置的 hello

@RestController
@RequestMapping("api/config/client")
public class TestController {

    @Value("${ibudai.name:default}")
    private String name;

    @GetMapping("/getInfo")
    public String getInfo() {
        return name;
    }
}

六、服务网关

当一个工程存在多个服务节点并需要对外开放服务时,显然最合适的方式就是提供一个统一的出入口,而并非将每个服务单独开放出去,服务网关的则正是为此而生。

通过网关,无论内部服务数量有多么庞大,对外的服务路径永远只有一个,从而实现更统一的管理。

1. POM依赖

新建 api-gateway 工程,在 Pom 文件中引入下述依赖。

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <!-- Gateway server -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zuul</artifactId>
    </dependency>
    <!-- slf4j -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
    </dependency>
</dependencies>

2. 项目配置

同样在项目的 YML 配置文件中将自身注册到服务中心。

server:
  port: 9095

spring:
  application:
    name: api-gateway

eureka:
  client:
    serviceUrl:
      # 注册自身服务到服务端
      defaultZone: http://localhost:9090/eureka/

3. 网关配置

在工程的启动类添加 @EnableZuulProxy 注解激活服务网关。

@EnableZuulProxy
@SpringBootApplication
public class APIGatewayApplication {

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

启动 api-gateway 项目之后即可通过网关代理实现服务请求,请求格式如下:

[gateway url]/[server name]/[api adress]

上述格式中各参数表述信息参考下表。

变量 描述
gateway url 网关项目的运行地址,如上述的 http://localhost:9095
server name 需要访问的服务名称,如之间的 client-1。
api adress 需要访问的服务接口地址。

完成上述项目配置之后,依次启动 register-server(9090)register-client(9091)api-gateway(9095) 三个工程,在之前的示例中我们在register-client 开放了接口 /api/client/getServices 接口查询当前已注册的服务,其完整的接口访问地址为:http://localhost:9091/api/client/getServices

按照上表的中的网关访问规范,现在我们即可通过网关服务实现 client-1 的接口访问,即上述的接口地址可转化为 http://localhost:9095/client-1/api/client/getServices

4. 过滤器

既然服务网关实现了统一请求入口,因此通常在网关会搭配过滤器等实现,用于提前拦截非法请求。

网关拦截器实现和以往的拦截器类似,新建 WebFilter 并继承 ZuulFilter 类,重写类中四类方法,各方法的作用参考下表。

方法 作用
filterType() 拦截器的类型,如事前拦截 pre。
filterOrder() 当前存在多个拦截器时通过其设置优先级。
shouldFilter() 用于指定是否激活拦截器。
run() 拦截器的具体实现内容

如下述的示例中定义一个拦截器在请求之前实现拦截,当接口访问地址非登录地址时触发拦截,当触发拦截时通过 RequestContext.getCurrentContext() 获取当前请求上下文从而获取请求头,判断是否包含请求认证头信息,若否则拦截请求并返回 401 状态。

public class WebFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(WebFilter.class);

    /**
     * Filter type, set "pre" will active before trigger.
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * When have multiple filter, use order to set priority.
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * To set is enable filter, normally we will specify the effect region
     */
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String targetPath = request.getRequestURL().toString();
        boolean enableFilter;
        try {
            URL url = new URL(targetPath);
            String path = url.getPath();
            int index = ordinalIndexOf(path, "/", 2);
            enableFilter = path.substring(index).startsWith("/api/login");
        } catch (MalformedURLException e) {
            throw new RuntimeException(e);
        }
        return !enableFilter;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        logger.info("Send {} request to {}", request.getMethod(), request.getRequestURL().toString());
        Object accessToken = request.getHeader("token");
        if (accessToken == null) {
            logger.warn("Token is empty");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        }
        logger.info("Token verify success");
        return null;
    }

    private int ordinalIndexOf(String str, String substr, int n) {
        int pos = str.indexOf(substr);
        while (--n > 0 && pos != -1)
            pos = str.indexOf(substr, pos + 1);
        return pos;
    }
}

5. 过滤注册

完成上述过滤器定义之后还需要将其注入 Spring 容器之中才可生效。

新建配置类 FilterConfig ,并在其中注入 WebFilter 对象。

@Configuration
public class FilterConfig {

    @Bean
    public WebFilter webFilter() {
        return new WebFilter();
    }
}

七、服务熔断

1. 概念介绍

服务熔断是一种微服务架构中常用的容错机制,用于防止雪崩效应和提高系统的可用性。当服务出现异常或者延迟时,服务熔断会暂时地停止对该服务的请求,而不是持续地尝试调用,从而避免因服务故障而导致的整个系统崩溃。

服务熔断通常由以下几个核心组件组成:

(1) 熔断器

熔断器 (Circuit Breaker) 是服务熔断的核心组件,它监控对服务的调用情况,并根据一定的条件 (例如错误率、超时率等) 来决定是否开启熔断。一旦开启熔断,熔断器将拒绝对服务的调用,并在一段时间内直接返回预先定义的降级响应。

(2) 熔断状态

熔断器有两种状态,分别是开启状态 (Open) 和关闭状态 (Closed)。当熔断器处于开启状态时,所有对服务的请求都会被直接拒绝,而不会发送到服务端。当熔断器处于关闭状态时,对服务的请求将正常发送到服务端。

(3) 熔断器状态转换

熔断器会根据一定的条件进行状态转换 (Tripping/Fixed)。当服务调用失败或超时达到一定阈值时,熔断器会从关闭状态转换为开启状态 (Tripping)

一段时间后,熔断器会自动进入半开启状态 (Half-Open),允许部分请求通过以测试服务是否已经恢复。如果服务恢复正常,则熔断器将转换为关闭状态 (Fixed),否则继续保持开启状态。

(4) 降级机制

当熔断器开启时,它会直接返回预先定义的降级响应 (Fallback),而不是发送请求到服务端。降级响应通常是一些预先定义的固定值或者简单的错误信息,用于告知调用方服务不可用或者出现异常。

2. 依赖引入

HystrixNetflix 开发的一款服务熔断框架,与之前依赖类似额外引入下述依赖。

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

3. 熔断配置

Hystrix 提供了 @HystrixCommand 注解用于配置熔断策略,注解参数描述如下:

方法 作用
commandProperties 熔断策略配置参数。
threadPoolProperties 熔断服务线程池配置。
fallbackMethod 配置降级回退方法。

下述即一个简易的服务熔断配置,当模拟睡眠接口超时后将触发熔断机制,执行降级方法 fallback(),需要注意的是降级方法的入参必须与熔断方法的入参一致。

@Service
public class HystrixService {

    @HystrixCommand(
            commandProperties = {
                    // Timeout strategy, default value: true, 1000
                    @HystrixProperty(name = "execution.timeout.enabled", value = "true"),
                    @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")
            },
            threadPoolProperties = {
                    @HystrixProperty(name = "coreSize", value = "10"),
                    @HystrixProperty(name = "maxQueueSize", value = "20"),
                    @HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
                    @HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
                    @HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "600")
            },
            // The "fallbackMethod" must have same parameter
            fallbackMethod = "fallback"
    )
    public String getServices(long sleep) {
        try {
            Thread.sleep(sleep);
        } catch (Execption e) {
            throw new RuntimeException(e);
        }
        return "work done!";
    }

    public String fallback(long sleep) {
        return "failed!";
    }
}

4. 启动配置

在项目启动类上通过 @EnableHystrix 注解开启服务熔断。

@EnableHystrix
@EnableDiscoveryClient
@SpringBootApplication
public class HystrixDemotionApplication {
    public static void main(String[] args) {
        SpringApplication.run(HystrixDemotionApplication.class, args);
    }
}

Talk is cheap, show me you code。

文中示例工程已上传 GitHub仓库直达

参考链接

  1. Spring Cloud 从入门到精通

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录