我们做分布式系统,为了不暴露具体的服务,以及实现各种统一处理,常常使用网关来管理接口。SpringCloud分布式系统中常用zuul来实现网关功能。zuul最基本的功能,就是把所有的接口都收到自己这里,按照规则和负载均衡的配置分发。
zuul实现路由最常用的方法是在属性文件properties或者 yml中进行配置。
我们首先创建几个必要的服务:
1. eureka注册中心
按照正常方式创建,无需多余内容。application.yml配置:
server:
port: 8101
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka
spring:
application:
name: eureka
2. 两个微服务user和 student
依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
application.yml
server:
port: 8107
spring:
application:
name: user
eureka:
instance:
hostname: studio.chris.com
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:8101/eureka
和
server:
port: 8108
spring:
application:
name: student
eureka:
instance:
hostname: studio.chris.com
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:8101/eureka
并没有什么差别,只是服务名不同而已。这里的服务名设计的比较简单,主要为方便进行服务发现路由的时候使用简洁。要注意避免歧义,也就是说解析的时候不要出现歧义,就是一个微服务名称不能是另一个微服务名称头部的子字符串,否则可能在某些情况下会导致路由错误。比如stu和student就不合适。
在每个微服务下面写个接口,如
@RestController
@RequestMapping("/api")
public class StudentController {
@Value("${server.port}")
String port;
@GetMapping("/test")
public String test() {
return "Called test api from student module.port: " + port;
}
}
我们用port做标记来区别不同的实例,以验证zuul给我们做了负载均衡。
3. zuul
pom依赖
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
org.springframework.cloud
spring-cloud-starter-netflix-zuul
application.yml
server:
port: 8080
spring:
application:
name: zuul
eureka:
instance:
hostname: studio.chris.com
client:
service-url:
defaultZone: http://${eureka.instance.hostname}:8101/eureka
可以看出yml的配置基本上都是一样的,无非区别了个端口和服务名称。而那个端口,在我们进行多实例部署的时候又是需要更改的。
启动类注解
@EnableEurekaClient
@EnableDiscoveryClient
@EnableZuulProxy
好了,所有服务我们已经构建好,现在需要配置zuul
zuul路由的最常见方式实在application.yml文件中配置
添加:
zuul:
routes:
user-manage:
path: /user/**
serviceId: user
student-manage:
path: /student/**
serviceId: student
user-manage和student-manage是两个路由,这个名称只要区别开就好了。serviceId和path是配对的,比如user-manage的配置,凡是/user开头的接口调用都会被分发到在注册中心注册为user的服务中去均衡调用。
如:请求localhost:8080/user/api/test,结果会出现类似以下这样的结果
Called test api from user module.port: 8107
这是的zuul也算是配置完成了,我们把各个服务启动起来测试。注册中心和zuul启动起来,其他两个微服务要启动多个实例,我们在Idea中打开多个终端,运行以下命令:
mvn spring-boot:run -e -Dmaven.test.skip=true -Dserver.port=8211
最后的端口号每次运行的时候都要修改。
我们在浏览器中打开注册中心的监控页面:
http://localhost:8101/
可以看到我们启动起来的微服务,其中user和student是多实例的。
我们通过网关8080调用接口,会发现返回的字符串端口号在变化,这就是因为负载均衡分发到了不同的实例。
其实这种路由还可以这样配置
zuul:
user: /user/**
student: /student/**
这种键值对的方式更简洁,key为注册服务名称,value为匹配的路径表达式。
注意:这种方法最常见,但是也很笨拙,如果我们增加了一个微服务,都要在yml文件中配置,也就要重启网关。但是一般我们不会这样做,网关不要经常重启,配置文件也不需要经常修改。我们把yml中关于zuul的配置全部删除干净。
接着,我们在pom中依赖一个包
org.springframework.cloud
spring-cloud-starter-eureka
1.3.5.RELEASE
好了,这就完了,既不用配置,也不用写代码,直接启动就好了。就算新添加一个微服务,也就是照常从网关调用就好了。zuul会根据注册中心中的发现的服务来自动进行路由。我们的zuul就再也不用关了。
最后需要补充的一个点,网关就是个关,大鬼小鬼都要走这里。它的作用除了路由、负载均衡之外,还有统一处理。由于网关在接口方法调用之前,所以可以做一些路由前的预处理。比如,我们分布式架构中最重视权限,一般的权限验证都是在微服务中根据自己的需求去做验证逻辑,但是架构层面统一要求的逻辑可以在网关处理,以节省资源。比如,我们要求所有的网络请求都要携带身份验证信息,没有就不允许进行任何接口调用,我们就可以在zuul过滤器中来实现。
实现方式就是写一个过滤器,来继承ZuulFilter
package com.chris.zuul;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Created by Chris Chan
* 2020/3/14 12:52
* Use for:
* Explain:
*/
@Component
@WebFilter(urlPatterns = "/*")
public class ChrisZuulFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
HttpServletResponse response = context.getResponse();
//授权过滤 不带身份证不让进场 立即退出
String token = request.getHeader("Authorization");
if (StringUtils.isEmpty(token)) {
try {
response.sendError(401, "Authorization is empty");
} catch (Exception e) {
}
return null;
}
return null;
}
}
这样我们请求的时候就一定要带一个Basic或者Bearer类似的Authorization头信息,不带的不再进行路由分发。至于这些头信息对不对,可以在这里进行检测,也可以放行到微服务中自己进行处理。