分布式开发-Spring Cloud

目录

1.服务治理和服务发现——Eureka

1.1 配置服务治理节点

1.2 服务发现

1.3 配置多个服务治理中心节点

2.微服务之间的调用

2.1 Ribbon客户端负载均衡

2.2 Feign声明式调用

3 断路器——Hystrix

3.1 使用降级服务

3.2 启用Hystrix仪表盘

4.路由网关-Zuul

4.1 构建Zuul网关

4.2 使用过滤器

5.使用@SpringCloudApplication


按照现今互联网的开发,高并发、大数据、快响应已经是普遍的要求。为了支持这样的需求,互联网系统也开始引入分布式的开发。为了实现分布式的开发,Spring 推出了一套组件,那就是Spring Cloud。当前Spring Cloud已经成为构建分布式微服务的热门技术,它并不是自己独自造轮子,而是将目前各家公司已经开发的、经过实践考验较为成熟的技术组合起来,并且通过Spring Boot风格再次封装,从而屏蔽掉了复杂的配置和实现原理,为开发者提供了一套简单易懂、易部署和维护的分布式系统开发包。

Spring Cloud是一套组件,可以细分为多种组件,如服务发现、配置中心、消息总线、负载均衡、断路器和数据监控等。我们只讨论以下最基础的技术:

  • 服务治理和服务发现:在Spring Cloud中主要是使用Netflix Eureka作为服务治理的
  • 客户端负载均衡:Spring Cloud提供了Ribbon来实现这些功能。
  • 声明服务调用:对于Rest风格的调用,如果使用RestTemplate会比较麻烦,可读性不高。为了简化多次调用的复杂度,Spring Cloud提供了接口式的声明服务调用编程,它就是Feign。
  • 断路器:在分布式中,因为存在网络延迟或者故障,所以一些服务调用无法及时响应。如果此时服务消费者还在大量地调用这些网络延迟或者故障的服务提供者,那么很快消费者也会因为大量的等待,造成积压,最终导致其自身出现服务瘫痪。为了克服这个问题,Spring Cloud引入了Netflix的开源组件Hystrix来处理这些问题。当服务消费者长期得不到服务提供者响应时,就可以进行降级、服务熔断、线程和信号隔离、请求缓存或者合并等处理。
  • API网关:在Spring Cloud中API网关是Zuul。对于网关而言,存在两个作用:第一个作用是将请求的地址映射为真实服务器地址,当真实服务器有多台的时候,可以起到路由分发的作用,从而降低单个节点的负载。第二个作用是过滤服务,在互联网中,服务器可能面临各种攻击,Zuul提供了过滤器,通过它过滤那些恶意或者无效的请求。

为了更好地讨论Spring Cloud组件和内容,假设需要实现一个电商项目,当前团队需要承担两个模块的开发,分别是用户模块和产品模块。根据微服务的特点,将系统拆分为用户服务和产品服务,两个服务通过Rest风格进行交互。架构如下图所示:

分布式开发-Spring Cloud_第1张图片

1.服务治理和服务发现——Eureka

我们首先搭建单个服务治理节点,然后将产品和用户服务的各自两个节点注册到服务治理节点上,再把服务治理节点变为两个。

1.1 配置服务治理节点

Spring Cloud的服务治理是使用Netflix的Eureka作为服务治理器的,它是我们构建Spring Cloud分布式最为核心和基础的模块,它的作用是注册和发现各个Spring Boot微服务,并且提供监控和管理功能。首先我们新建一个工程service-config,然后引入对应的jar包:

    Hoxton.SR1
    
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    
    
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-server
        
    

这样就引入了Eureka模块的包了。然后要启动它只需要在Spring Boot的启动文件上加入注解:

@EnableEurekaServer

有了这个注解,就意味着Spring Boot会启动Eureka模块,我们进一步配置Eureka模块的一些基本内容。

spring.application.name=server
server.port=7001
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=fasle
eureka.client.fetch-registry=fasle
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/
  • spring.application.name配置为server,这是一个标识,它标识某个微服务的共同标识。如果有第二个微服务节点启动时,也是将这个配置为server,那么Spring Cloud也会认为它是这个微服务的一个节点。
  • eureka.client.register-with-eureka配置为false,因为在默认的情况下,项目会自动地查找服务注册中心去注册。这里项目自身就是服务注册中心,所以取消掉注册服务中心。
  • eureka.client.fetch-registry配置为false,这是一个检索服务的功能,因为服务治理中心是维护服务实例的,所以也不需要这个功能,即设置为false。

配置完成之后,我们启动服务治理应用,在浏览器输入:http://localhost:7001/可以看到如下界面:

分布式开发-Spring Cloud_第2张图片

这意味着Eureka服务治理中心已经启动成功,但是还没有注册服务。下面开始注册产品和用户服务。

1.2 服务发现

注册服务会用到服务发现,我们新建一个Spring Boot工程,取名为product-service,并且引入服务发现相关的包:

    
        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        

        
            org.springframework.boot
            spring-boot-starter-web
        

    

    
        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        
    

我们只需要依赖spring-cloud-starter-netflix-eureka-client,无需添加额外的注解,就可以将当前项目注册给服务治理中心。这里需要注意的是不要忘记引入spring-boot-starter-web,否则会导致无法注册。application文件配置如下:

server.port=9001
spring.application.name=product
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/

这里端口使用了9001,而应用名称为product,这个微服务名称将会注册给服务治理中心。

分布式开发-Spring Cloud_第3张图片

看到上面这个图,就说明产品微服务已经注册给了服务治理中心。或许在分布式服务中需要两个或者以上的产品微服务节点,我们修改产品微服务的端口为9002,并再次启动该Spring Boot应用程序,再次打开服务治理中心页:

分布式开发-Spring Cloud_第4张图片

从图中可以看到,服务治理中心存在两个产品微服务节点,端口分别为9001和9002。类比产品服务,我们再新建用户服务,并配置端口分别为8001和8002,并启动该服务。再次查看服务治理中心页面,可以看到两个用户微服务节点都已经注册成功了。

1.3 配置多个服务治理中心节点

上面只是在服务治理中心将两个微服务都分别注册了两个节点,而服务治理中心却只有一个节点。我们希望有两个服务治理中心,一个不可用后,另外一个节点依旧可用,这样就能保证服务可以继续正常处理业务,这就体现了高可用的特性。我们停止服务治理中心,并再次新建一个服务治理中心,两个治理中心相互注册,即A的注册域B,B的注册域为A,配置信息如下:

A注册中心:

spring.application.name=server
server.port=7001
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://localhost:7002/eureka/

B注册中心:

spring.application.name=server
server.port=7002
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/

这样将两个注册中心启动后,就可以看到两个服务治理中心是通过相互注册来保持相互监控的,关键点是属性spring.application.name保持一致都为server,这样就可以形成两个甚至是多个服务治理中心。此时再打开服务治理中心页面,就可以看到;

现在已经有了两个服务治理中心,接下来,需要将其他的微服务注册到多个服务治理中心中。我们以单个产品微服务为例来修改配置文件。

server.port=9001
spring.application.name=product
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/,http://localhost:7002/eureka/

这里加入了两个服务治理中心的域,这样就可以使得应用注册到两个服务中心上去。启动这个产品服务之后,再次修改端口为9002,然后再次启动,这样两个产品微服务节点都会被注册到两个服务治理中心。对于用户服务,也做类似的操作。做完这些步骤,再次打开服务治理中心首页,可以看到如下图:

此时,多个微服务都已经启动了,并且注册成功到两个服务治理中心中进行监控了。那我们如何实现让各个微服务相互交互起来呢?Spring Cloud提供了另外两个组件进行支持,它们就是Ribbon和Feign。

2.微服务之间的调用

上面已经把产品和用户两个微服务注册到服务治理中心了。对于业务,则往往需要各个微服务之间相互地协助才能完成,因此这里涉及到服务之间相互调用的问题。例如,我们需要根据用户的等级来决定某些商品的折扣,如白金会员是9折,黄金会员8.5折,钻石会员是8折等。也就是说分布式系统在执行商品交易逻辑时,需要调用用户服务获得用户信息才可以决定产品的折扣。除此之外,我们还需要注意服务节点之间的负载均衡,毕竟一个微服务可以由多个节点提供服务。Spring Cloud提供了Ribbon和Feign组件来帮助我们完成这些功能,通过它们,各个微服务之间就能够相互调用,并且它会默认实现负载均衡。

2.1 Ribbon客户端负载均衡

我们首先在用户服务中提供查询用户信息的Rest接口。

package com.martin.user.pojo;

import lombok.Data;
import java.io.Serializable;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@Data
public class UserPO implements Serializable {
    private Long id;
    private String userName;
    //1-白银会员 2-黄金会员 3-钻石会员
    private int level;
    private String note;
}

然后定义基于REST风格的实现用户返回的代码:

package com.martin.user.controller;

import com.martin.user.pojo.UserPO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@Slf4j
@RestController
public class UserController {
    /**
     * 服务发现客户端
     */
    @Autowired
    private DiscoveryClient discoveryClient;

    @GetMapping("/user/{id}")
    public UserPO getUserPO(@PathVariable("id") Long id) {
        ServiceInstance service = discoveryClient.getInstances("USER").get(0);
        log.info("[" + service.getServiceId() + "]:" + service.getHost() + ":" + service.getPort());

        UserPO userPO = new UserPO();
        userPO.setId(id);
        int level = (int) (id % 3 + 1);
        userPO.setLevel(level);
        userPO.setUserName("user_name_" + id);
        userPO.setNote("note_" + id);
        return userPO;
    }
}

这里的DiscoveryClient对象是Spring Boot自动创建的,然后在方法中会打印出第一个用户微服务ID、服务主机和端口,这样有利于后续的监控和对负载均衡的研究。

然后,我们在产品微服务上通过Maven加入对Ribbon的依赖,如代码清单:

        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-ribbon
        

然后对RestTemplate进行初始化:

package com.martin.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication(scanBasePackages = "com.martin.product.*")
public class ProductServiceApplication {

    @LoadBalanced
    @Bean
    public RestTemplate initRestTemplate() {
        return new RestTemplate();
    }

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

}

这段代码中在RestTemplate中加入了注解@LoadBalanced,它的作用是让RestTemplate实现负载均衡,也就是说通过这个RestTemplate对象调用用户微服务请求的时候,Ribbon会自动给用户微服务节点实现负载均衡,这样请求就会被分摊到微服务的各个节点上,从而降低单点的压力。默认的情况下,Ribbon会使用轮询的负载均衡算法。接着我们使用RestTemplate调用用户服务,代码如下:

package com.martin.product.controller;

import com.martin.common.dto.UserDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/ribbon")
    public UserDTO testRibbon() {
        UserDTO userDTO = null;
        for (int i = 0; i < 10; i++) {
            userDTO = restTemplate.getForObject("http://USER/user/" + (i + 1), UserDTO.class);
        }

        return userDTO;
    }
}

方法中使用了“USER”这个字符串代替了服务器及其端口,这是一个服务ID。我们启动USER服务和PRODUCT服务,然后在浏览器中输入地址:http://localhost:9002/product/ribbon,可以看到如下的返回结果:

{
    "id": 10,
    "userName": "user_name_10",
    "level": 2,
    "note": "note_10"
}

与此同时,两个用户微服务的后台日志各有5条日志打出:

2020-02-13 21:28:27.821  INFO 16324 --- [nio-8001-exec-6] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001
2020-02-13 21:28:27.831  INFO 16324 --- [nio-8001-exec-7] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001
2020-02-13 21:28:27.842  INFO 16324 --- [nio-8001-exec-8] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001
2020-02-13 21:28:27.853  INFO 16324 --- [nio-8001-exec-9] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001
2020-02-13 21:28:27.860  INFO 16324 --- [io-8001-exec-10] c.martin.user.controller.UserController  : [USER]:HD-110ZN-martin:8001

这说明在产品中心通过Ribbon调用时已经负载均衡成功。

2.2 Feign声明式调用

如果我们需要多次调用服务,使用RestTemplate并非那么友好,因为除了要编写URL,还需要注意这些参数的组装和结果的返回等操作。为了克服这些不友好,除了Ribbon外,Spring Cloud还提供了声明式调用组件——Feign。

Feign是一个基于接口的编程方式,开发者只需要声明接口和配置注解,在调度接口方法时,Spring Cloud就根据配置来调度对应的Rest风格请求,从其他微服务系统中获取数据。首先,我们引入Feign的Maven依赖包:

        
            org.springframework.cloud
            spring-cloud-starter-openfeign
        

这样就把Feign所需要的依赖包加载进来了,为了启用Feign,首先需要在Spring Boot的启动文件中加入注解@EnableFeignClients,这个注解代表该项目会启动Feign客户端。

package com.martin.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(scanBasePackages = "com.martin.product.*")
@EnableFeignClients(basePackages = "com.martin.product.*")
public class ProductServiceApplication {

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

然后在产品微服务中加入接口声明,注意这里仅仅是一个接口声明,并不需要实现类,代码实现如下:

package com.martin.product.service;

import com.martin.common.dto.UserDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@FeignClient("user")
public interface UserService {
    @GetMapping("/user/{id}")
    UserDTO getUser(@PathVariable("id") Long id);
}

这里@FeignClient("user")代表这是一个Feign客户端,而配置的“user”是一个服务的ID,它指向了用户微服务,这样Feign就会知道向用户微服务请求,并会实现负载均衡。这里的注解@GetMapping代表启用HTTP的GET请求用户微服务,@PathVariable代表从URL中获取参数。下面我们在ProductController中加入UserService接口对象的注入,并且使用它来调度用户微服务的REST端点。实现代码如下:

package com.martin.product.controller;

import com.martin.common.dto.UserDTO;
import com.martin.product.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private UserService userService;
    @GetMapping("/fegin")
    public UserDTO testRibbon() {
        UserDTO userDTO = null;
        for (int i = 0; i < 10; i++) {
            userDTO = userService.getUser((long) i);
        }

        return userDTO;
    }
}

与Ribbon相比,Feign屏蔽掉了RestTemplate的使用,提供了接口声明式的调用,使得程序可读性更高,同时在多次调用中更加方便。

3 断路器——Hystrix

在微服务中,如果一个服务不可用,而其他微服务还大量地调用这个不可用的微服务,也会导致其自身不可用,其自身不可用之后又可能继续蔓延到其他与之相关的微服务上,这样就会使更多的微服务不可用,最终导致分布式服务瘫痪。

为了防止这样的蔓延,微服务提出了断路器的概念。在微服务系统之间大量调用可能导致服务消费者自身出现瘫痪的情况下,断路器就会将这些积压的大量请求“熔断”,来保证其自身服务可用,而不会蔓延到其他微服务系统上。通过这样的熔断机制可以保持各个微服务持续可用。

3.1 使用降级服务

应该说,处理限制请求的方式的策略有很多,如限流、缓存等。这里主要介绍最为常用的降级服务。所谓降级服务,就是当请求其他微服务出现超时或者发生故障时,就会使用自身服务其他的方法进行响应。在Spring Cloud中,断路器是由NetFlix的Hystrix实现的,它默认监控微服务之间的调用超时时间为2s,如果超过这个时间,它就会根据你的配置使用其他方法进行响应。

首先我们在用户微服务中增加一个模拟超时的方法:

    @GetMapping("/user/timeout")
    public String timeout() throws InterruptedException {
        long ms = (long) (3000L * Math.random());
        Thread.sleep(ms);
        return "熔断测试";
    }

该方法没有任何业务含义,只是会使用sleep方法让当前线程休眠随机的毫秒数。这个毫秒数可能超过Hystrix所默认的2000ms,这样就可以出现短路,进入降级方法。

然后,我们在产品微服务中启用断路器,要启用断路器首先需要引入Hystrix的包:

        
            org.springframework.cloud
            spring-cloud-starter-netflix-hystrix
        

其次在Spring Boot的启动文件中加入注解@EnableCircuitBreaker,就可以启动断路机制:

package com.martin.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication(scanBasePackages = "com.martin.product.*")
@EnableFeignClients(basePackages = "com.martin.product.*")
@EnableCircuitBreaker

public class ProductServiceApplication {

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

Spring Cloud启用断路机制以后,在后续的代码中加入注解@HystrixCommand就能指定哪个方法启用断路机制。详细设计代码如下:

package com.martin.product.controller;

import com.martin.product.service.UserService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author: martin
 * @date: 2020/2/13
 */
@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private UserService userService;

    @GetMapping("/circuitBreaker")
    @HystrixCommand(fallbackMethod = "error")
    public String circuitBreaker() {
        return userService.getUserTimeOut();
    }

    public String error() {
        return "超时出错";
    }
}

代码中@HystrixCommand注解表示将在方法上启用断路机制,而其属性fallbackMethod则可以指定降级方法,指定为error,那么降级方法就是error。这样,在请求circuitBreaker方法的时候,只要超时超过了2000ms,服务就会启用error方法作为响应请求,从而避免请求的积压,保证微服务的高可用性。其流程图如下:

分布式开发-Spring Cloud_第5张图片

所以当我们请求circuitBreaker的时候,有时候会出现“熔断测试”有时候会返回“超时出错”。Hystrix默认的是2000ms会超时,但是希望能把这个超时时间进行自定义,我们可以使用如下代码:

    @GetMapping("/circuitBreaker")
    @HystrixCommand(fallbackMethod = "error",
            commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000")})
    public String circuitBreaker() {
        return userService.getUserTimeOut();
    }

@HystrixCommand除了配置超时时间,还有很多可配置的内容,这里不再赘述了。

3.2 启用Hystrix仪表盘

对于Hystrix,Spring Cloud还提供了一个仪表盘进行监控断路的情况,从而让开发者监控可能出现的问题。我们新建一个hystrix-dashboard的项目,并引入相关的包:

        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.cloud
            spring-cloud-netflix-hystrix-dashboard
        

在Spring Boot的启动类中加入注解:

package com.martin.hystrix.dashboard;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;

@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardApplication {

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

配置文件application.properties添加如下的配置信息:

server.port=6001
spring.application.name=hystrix_dashboard

启动该应用,并输入http://localhost:6001/hystrix,就可以看到如下图所示结果:

分布式开发-Spring Cloud_第6张图片

从Hystrix仪表盘首页可以看出,它支持3种监控,前两种是基于Turbine的,一种是默认集群,另一种是指定集群,第三种是单点监控。两个输入框分别是轮询时间和标题。轮询时间表示隔多久时间轮询一次,标题表示仪表盘的页面标题。从单点监控的说明可以看出,只需要给出 https://hystrix-app:port/actuator/hystrix.stream格式的URL给仪表盘即可。

在产品微服务中,我们已经使用了Hystrix,还需要引入Spring Boot的监控Actuator,同时将端点暴露。首先引入spring-boot-starter-actuator依赖:

        
            org.springframework.boot
            spring-boot-starter-actuator
        

在application.properties中增加如下端点暴露:

management.endpoints.web.exposure.include=health,info,hystrix.stream

management.endpoints.web.exposure.include代表Actuator监控对外暴露的端点,在默认情况下,知会暴露health和info端点,这里增加了hystrix.stream端点,这样仪表盘才能读到HTTP协议下的Hystrix信息流。我们重启一下产品服务,并输入如下图所示的链接:

分布式开发-Spring Cloud_第7张图片

输入完对应的信息后,点击“Monitor Stream”按钮,它就会跳到监控页面。此时我们在浏览器中执行几次http://localhost:9002/product/circuitBreaker,让它出现断路情况,此时再观察仪表盘,就可以看到如下界面了:

分布式开发-Spring Cloud_第8张图片

至此,已经完成了让仪表盘监控断路机制的任务。当发生断路的时候,监控就会给予具体的统计和分析。

4.路由网关-Zuul

通过上面的内容,我们已经搭建了一个基于Spring Cloud的分布式应用。在实际的应用中,我们通常还会引入一个网关,如Nginx、F5等。网关的功能对于分布式网站是十分重要的,它通常具有路由、负载均衡和拦截过滤等功能。下面我们看一下如何构建一个Zuul网关。

4.1 构建Zuul网关

在Spring Cloud的组件中,Zuul是支持API网关开发的组件。Zuul来自NetFlix的开源网关,它的使用十分简单。首先新建zuul-gateway应用,并引入zuul的包:

        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-client
        
        
            org.springframework.cloud
            spring-cloud-starter-netflix-zuul
        

这里引入了服务发现包,启用Zuul十分简单,只需要在启动类上添加一个注解@EnableZuulProxy就可以:

package com.martin.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication(scanBasePackages = "com.martin.zuul")
@EnableZuulProxy
public class ZuulGatewayApplication {

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

}

这样就启用了Zuul网关代理功能了,注解@EnableZuulProxy的源码如下:

package org.springframework.cloud.netflix.zuul;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.context.annotation.Import;

@EnableCircuitBreaker
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({ZuulProxyMarkerConfiguration.class})
public @interface EnableZuulProxy {
}

这里也使用了@EnableCircuitBreaker,从这里可以看出Zuul已经引入了断路机制,在请求不到的时候,就会进行断路,以避免网关发生请求无法释放的场景,导致微服务瘫痪。

我们先简单地配置application.properties文件,如代码清单:

server.port=80
spring.application.name=zuul
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/,http://localhost:7002/eureka/

上述的代码中,使用了80端口启动Zuul,在浏览器中这个端口是默认端口,因此在地址栏中不需要显式输入,而Spring 应用名称则是zuul。我们启动微服务系统,并在浏览器中输入http://localhost/user/user/1,可以看到如下界面:

分布式开发-Spring Cloud_第9张图片

这里的localhost代表的是请求zuul服务,因为采用的是默认的80端口,user代表用户的微服务ID,而/user/1表示的是请求路径,这样Zuul就会将请求转发到用户微服务。同理,我们也可以请求产品服务,例如:http://localhost/product/product/fegin

除此之外,Zuul也允许我们配置请求映射,在application.properties中增加如下代码:

server.port=80
spring.application.name=zuul
# 用户微服务映射规则
zuul.routes.user-service.path=/u/**
#指定映射的服务用户地址,这样Zuul就会将请求转发到用户微服务上了
zuul.routes.user-service.url=http://localhost:8001/
# 产品微服务映射规则
zuul.routes.product-service.path=/p/**
#映射产品服务中心服务ID,Zuul会自动使用服务端负载均衡,分摊请求
zuul.routes.product-service.service-id=product
ribbon.eureka.enabled=true
# 注册给服务治理中心
eureka.client.service-url.defaultZone=http://localhost:7001/eureka/,http://localhost:7002/eureka/

先看最后一个配置,这个配置是将Zuul网关注册给服务治理中心,这样它就能够获取各个微服务的服务ID了。当请求的地址满足path通配时,请求会转发到对应配置的URL上。这里需要注意的是使用ServiceId配置,Zuul会自动实现负载均衡。

4.2 使用过滤器

上面只是将请求转发到具体的服务器或者具体的微服务上,但是有时候还希望网关功能更加强大一些。例如,监测用户登录、黑名单用户、购物验证码、恶意请求攻击等场景。如果这些在过滤器内判断失败,那么久不要再把请求转发到其他微服务上,以保护微服务的稳定。

在Zuul中存在一个抽象类,它便是ZuulFilter,它的定义如下图所示:

分布式开发-Spring Cloud_第10张图片

这里需要注意的是,只是画出了抽象类ZuulFilter自定义的抽象方法和接口IZuulFilter定义的抽象类,也就是说,当需要定义一个非抽象的Zuul过滤器的时候,需要实现这4个抽象方法:

  • shouldFilter:返回boolean值,如果为true,则执行这个过滤器的run方法。
  • run:返回过滤逻辑,这是过滤器的核心方法。
  • filterType:过滤器类型,它是一个字符串,可以配置为四种,pre(请求执行之前filter)、route(处理请求,进行路由)、post(请求处理完成之后执行的filter)、error(出现错误时执行的filter)
  • filterOrder:指定过滤器顺序,值越小优先级越高。

下面模拟这样一个场景,假设用户输入了用户名和密码,路由网关过滤器判断用户输入的用户名和密码是否正确,当不正确时,则不再转发请求到微服务。实现代码如下:

package com.martin.zuul.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.awt.PageAttributes;

/**
 * @author: martin
 * @date: 2020/2/16
 */
@Component
public class MyZuulFilter extends ZuulFilter {

    /**
     * 过滤器的类型为请求前执行
     *
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器排序
     *
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 是否过滤
     *
     * @return
     */
    @Override
    public boolean shouldFilter() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String userName = request.getParameter("userName");
        String pwd = request.getParameter("password");
        //如果用户名和密码都不为空则返回true,启用过滤器
        return !StringUtils.isEmpty(userName) && !StringUtils.isEmpty(pwd);
    }

    /**
     * 过滤器的逻辑
     *
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String userName = request.getParameter("userName");
        String pwd = request.getParameter("password");
        if (!("123".equals(userName) && "123".equals(pwd))) {
            //不再转发请求
            ctx.setSendZuulResponse(false);
            //设置HTTP的响应码为401(未授权)
            ctx.setResponseStatusCode(401);
            //设置响应类型为JSON数据集
            ctx.getResponse().setContentType(MediaType.APPLICATION_JSON_UTF8.getType());
            //设置响应体
            ctx.setResponseBody("{'message':'Verification Code Error'}");
        }
        //账号密码一致,验证通过
        return null;
    }
}

上述代码中在类上标注了@Component,这样Spring就会扫描它,将其装配到IOC容器中。因为继承了抽象类ZuulFilter,所以Zuul会自动将它识别为过滤器。filterType方法返回了pre,则过滤器会在路由之前执行。filterOrder返回为0,这个方法在指定多个过滤器顺序才有意义,数字越小,则越优先。shouldFilter中判断是否存在账号和密码,如果存在则返回true,这意味着将启用这个过滤器,否则不再启用这个过滤器。run方法是过滤器的核心方法,它对输入的用户名和密码进行判断,如果匹配不一致,则设置不再转发请求到微服务系统,并且将响应码设置401,响应类型为JSON数据集,最后还会设置响应体的内容;如果一致,则返回null,放行服务。

在实际的工作中,过滤器可以使用在安全验证、过滤黑名单请求等场景,有效地保护分布式的微服务系统。

5.使用@SpringCloudApplication

在上面的内容中,对于启动文件采用了很多的注解,如@SpringBootApplication、@EnableDiscoveryClient和@EnableCircuitBreaker等。这些注解有时候会让人觉得冗余,为了简化开发,Spring Cloud还提供了自己的注解@SpringCloudApplication来简化使用Spring Cloud的开发。注解代码如下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.cloud.client;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}

上述代码可以看出@SpringCloudApplication会启用Spring Boot应用,以及开发服务发现和断路器的功能。但是它还缺乏配置扫描包的配置项,所以往往需要配合使用注解@ComponentScan来定义扫描的包。另外,@SpringCloudApplication并不会自动启动Feign,所以如果使用Feign的时候,注解@EnableFeignClients也是必不可少的。

package com.martin.product;

import org.springframework.boot.SpringApplication;
import org.springframework.cloud.client.SpringCloudApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;

@EnableFeignClients(basePackages = "com.martin.product.*")
@ComponentScan(basePackages = "com.martin.product.*")
@SpringCloudApplication
public class ProductServiceApplication {

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

 

你可能感兴趣的:(Spring,Boot,java,spring,boot)