Spring Cloud认识三之服务网关和服务消费以及异常处理
本文,我们将学习 Spring Cloud的另一个组件:zuul,它提供微服务的网关功能,即中转站,通过它提供的接口,可以转发不同的服务。在学习 zuul 之前,我们先接着上一篇的代码,来看看服务提供者是如何提供服务的。
在服务提供者的 module 下创建 HelloController 类,添加内容如下:
@RestController
public class HelloController {
@RequestMapping("index")
public String index(){
return "Hello World!";
}
}
然后分别启动服务注册中心和服务提供者,浏览器输入:http://localhost:8762/index,即可看见如下画面:
在实际的项目中,一个项目可能会包含很多个服务,每个服务的端口和 IP 都可能不一样。那么,如果我们以这种形式提供接口给外部调用,代价是非常大的。从安全性上考虑,系统对外提供的接口应该进行合法性校验,防止非法请求,如果按照这种形式,那每个服务都要写一遍校验规则,维护起来也很麻烦。
这个时候,我们需要统一的入口,接口地址全部由该入口进入,而服务只部署在局域网内供这个统一的入口调用,这个入口就是我们通常说的服务网关。
Spring Cloud 给我们提供了这样一个解决方案,那就是 zuul,它的作用就是进行路由转发、异常处理和过滤拦截。下面,我将演示如果使用 zuul 创建一个服务网关。
创建 gateway 工程
在父项目上右键 -> New -> Module,创建一个名为 gateway 的工程,在其 pom.xml 中,加入如下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zuul</artifactId>
</dependency>
</dependencies>
创建 Application 启动类,并增加 @EnableZuulProxy
注解:
@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
最后添加 application.yml 配置文件,内容如下:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8080
spring:
application:
name: gateway
zuul:
routes:
api:
path: /api/**
serviceId: eurekaclient
我们可以看到,服务网关的配置多了几项,具体含义如下。
-
zuul.routes.api.path:指定请求基础地址,其中 API 可以是任何字符。
-
serviceId:转发到的服务 ID,也就是指定服务的 application.name,上述实例的含义表示只要包含
/api/
的地址,都自动转发到 eurekaclient 的服务去。
然后我们启动服务注册中心、服务提供者、服务网关,访问地址:http://localhost:8080/api/index,我们可以看到和之前的界面完全一样。其实只要引入了 zuul,它就会自动帮我们实现反向代理和负载均衡。配置文件中的地址转发其实就是一个反向代理,那它如何实现负载均衡呢?
我们修改服务提供者的 Controller 如下:
RestController
public class HelloController {
@Value("${server.port}")
private int port;
@RequestMapping("index")
public String index(){
return "Hello World!,端口:"+port;
}
}
重新启动。然后再修改服务提供者的端口为8673,再次启动它(切记:原先启动的不要停止),访问地址:http://localhost:8761,我们可以看到 eurekaclient 服务有两个地址:
再不断访问地址:http://localhost:8080/api/index,可以看到交替出现以下界面:
由此可以得出,当一个服务启动多个端口时,zuul 服务网关会依次请求不同端口,以达到负载均衡的目的。
服务拦截
前面我们提到,服务网关还有个作用就是接口的安全性校验,这个时候我们就需要通过 zuul 进行统一拦截,zuul 通过继承过滤器 ZuulFilter 进行处理,下面请看具体用法。
新建一个类 ApiFilter 并继承 ZuulFilter:
@Component
public class ApiFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() {
//这里写校验代码
return null;
}
}
其中:
- filterType 为过滤类型,可选值有 pre(路由之前)、routing(路由之时)、post(路由之后)、error(发生错误时调用)。
- filterOrdery 为过滤的顺序,如果有多个过滤器,则数字越小越先执行
- shouldFilter 表示是否过滤,这里可以做逻辑判断,true 为过滤,false 不过滤
- run 为过滤器执行的具体逻辑,在这里可以做很多事情,比如:权限判断、合法性校验等。
下面,我们来做一个简单的安全验证:
@Override
public Object run() {
//这里写校验代码
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
String token = request.getParameter("token");
if(!"12345".equals(token)){
context.setSendZuulResponse(false);
context.setResponseStatusCode(401);
try {
context.getResponse().getWriter().write("token is invalid.");
}catch (Exception e){}
}
return null;
}
启动 gateway,在浏览器输入地址:http://localhost:8080/api/index,可以看到以下界面:
再通过浏览器输入地址:http://localhost:8080/api/index?token=12345,可以看到以下界面:
错误拦截
在一个大型系统中,服务是部署在不同的服务器下面的,我们难免会遇到某一个服务挂掉或者请求不到的时候,如果不做任何处理,服务网关请求不到会抛出500错误,对用户是不友好的。
我们为了提供用户的友好性,需要返回友好性提示,zuul 为我们提供了一个名叫 ZuulFallbackProvider 的接口,通过它我们就可以对这些请求不到的服务进行错误处理。
新建一个类 ApiFallbackProvider 并且实现 ZuulFallbackProvider 接口:
Component
public class ApiFallbackProvider implements ZuulFallbackProvider{
@Override
public String getRoute() {
return "eurekaclient";
}
@Override
public ClientHttpResponse fallbackResponse() {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "{code:0,message:\"服务器异常!\"}";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream(getStatusText().getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
其中,getRoute 方法返回要处理错误的服务名,fallbackResponse 方法返回错误的处理规则。
现在开始测试这部分代码,首先停掉服务提供者 eurekaclient,再重启 gateway,请求地址:http://localhost:8080/api/index?token=12345,即可出现以下界面:
前面我们提到,对外提供接口通过 zuul 服务网关实现。一个大型的系统由多个微服务模块组成,各模块之间不可避免需要进行通信,一般我们可以通过内部接口调用的形式,服务 A 提供一个接口,服务 B 通过 HTTP 请求调用服务 A 的接口,为了简化开发,Spring Cloud 提供了一个基础组件方便不同服务之间的 HTTP 调用,那就是 Feign。
什么是 Feign
Feign 是一个声明式的 HTTP 客户端,它简化了 HTTP 客户端的开发。使用 Feign,只需要创建一个接口并注解,就能很轻松的调用各服务提供的 HTTP 接口。Feign 默认集成了 Ribbon,默认实现了负载均衡。
创建 Feign 服务
在根项目上创建一个 module,命名为 feign,然后在 pom.xml 添加如下内容:
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-eureka</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-feign</artifactId>
</dependency>
</dependencies>
创建 application.yml,内容如下:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8081
spring:
application:
name: feign
最后创建一个启动类 Application:
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
我们可以看到启动类增加了一个新的注解:@EnableFeignClients
,如果我们要使用 Feign 声明式 HTTP 客户端,必须要在启动类加入这个注解,以开启 Feign。
这样,我们的 Feign 就已经集成完成了,那么如何通过 Feign 去调用之前我们写的 HTTP 接口呢?请看下面的做法。
首先创建一个接口 ApiService,并且通过注解配置要调用的服务地址:
@FeignClient(value = "eurekaclient")
public interface ApiService {
@RequestMapping(value = "/index",method = RequestMethod.GET)
String index();
}
分别启动注册中心 EurekaServer、服务提供者EurekaClient(这里服务提供者启动两次,端口分别为8762、8763,以观察 Feign 的负载均衡效果)。
然后在 Feign 里面通过单元测试来查看效果。
1.添加单元测试依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2.添加测试代码。
@SpringBootTest(classes = Application.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class TestDB {
@Autowired
private ApiService apiService;
@Test
public void test(){
try {
System.out.println(apiService.index());
}catch (Exception e){
e.printStackTrace();
}
}
}
最后分别启动两次单元测试类,我们可以发现控制台分别打印如下信息:
Hello World!,端口:8762
Hello World!,端口:8763
由此可见,我们成功调用了服务提供者提供的接口,并且循环调用不同的接口,说明它自带了负载均衡效果。
利用 Feign 的声明式 HTTP 客户端,通过注解的形式很容易做到不同服务之间的相互调用。
我们的服务最终是部署在服务器上,因为各种原因,服务难免会发生故障,那么其他服务去调用这个服务就会调不到,甚至会一直卡在那里,导致用户体验不好。针对这个问题,我们就需要对服务接口做错误处理,一旦发现无法访问服务,则立即返回并报错,我们捕捉到这个异常就可以以可读化的字符串返回到前端。
为了解决这个问题,业界提出了熔断器模型。
Hystrix 组件
SpringCloud 集成了 Netflix 开源的 Hystrix 组件,该组件实现了熔断器模型,它使得我们很方便地实现熔断器。
在实际项目中,一个请求调用多个服务是比较常见的,如果较底层的服务发生故障将会发生连锁反应。这对于一个大型项目是灾难性的。因此,我们需要利用 Hystrix 组件,当特定的服务不可用达到一个阈值(Hystrix 默认5秒20次),将打开熔断器,即可避免发生连锁反应。
代码实现
紧接上一篇的代码,Feign 是默认自带熔断器的,在 D 版本 SpringCloud 中是默认关闭的,我们可以在 application.yml 中开启它:
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
port: 8081
spring:
application:
name: feign
#开启熔断器
feign:
hystrix:
enabled: true
新建一个类 ApiServiceError.java 并实现 ApiService:
@Component
public class ApiServiceError implements ApiService {
@Override
public String index() {
return "服务发生故障!";
}
}
然后在 ApiService 的注解中指定 fallback:
@FeignClient(value = "eurekaclient",fallback = ApiServiceError.class)
public interface ApiService {
@RequestMapping(value = "/index",method = RequestMethod.GET)
String index();
}
再创建 Controller 类:ApiController,加入如下代码:
@RestController
public class ApiController {
@Autowired
private ApiService apiService;
@RequestMapping("index")
public String index(){
return apiService.index();
}
}
测试熔断器
分别启动注册中心 EurekaServer、服务提供者 EurekaClient 和服务消费者 Feign,然后访问:http://localhost:8081/index,可以看到顺利请求到接口:
然后停止 EurekaClient,再次请求,可以看到熔断器生效了:
熔断器监控
Hystrix 给我们提供了一个强大的功能,那就是 Dashboard。Dashboard 是一个 Web 界面,它可以让我们监控 Hystrix Command 的响应时间、请求成功率等数据。
下面我们开始改造 Feign 工程,在 Feign 工程的 pom.xml 下加入依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-hystrix-dashboard</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
这三个依赖缺一不可,否则会有意想不到的事情发生。
然后在启动类 Application.java 中加入 @EnableHystrixDashboard
、@EnableCircuitBreaker
注解:
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
@EnableHystrixDashboard
@EnableCircuitBreaker
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
然后分别启动 EurekaServer、EurekaClient 和 Feign 并访问:http://localhost:8081/hystrix,可以看到如下画面:
按照上图箭头所示,输入相关信息后,点击 Monitor Stream 按钮进入下一界面,打开新窗口访问:http://localhost:8081/index,在 Dashboard 界面即可看到 Hystrix 监控界面:
Hystrix 熔断器的基本用法就介绍到这里。前面我们创建了注册中心、服务提供者、服务消费者、服务网关和熔断器,每个工程都有配置文件,而且有些配置是想通的,按照这个方式进行应用程序的配置,维护性较差,扩展性也较差,比如很多个服务都会配置数据源,而数据源只有一个,那么如果我们的数据源地址发生变化,所有地方都需要改,如何改进这个问题呢?下一篇所讲解的配置中心就是为解决这个问题而生的,敬请期待。