zuul 是netflix开源的一个API Gateway 服务器, 本质上是一个web servlet应用。
Zuul 在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web 网站后端所有请求的前门。
zuul的例子可以参考 netflix 在github上的 simple webapp,可以按照netflix 在github wiki 上文档说明来进行使用。
zuul的核心是一系列的filters, 其作用可以类比Servlet框架的Filter,或者AOP。
zuul把Request route到 用户处理逻辑 的过程中,这些filter参与一些过滤处理,比如Authentication,Load Shedding等。
Zuul提供了一个框架,可以对过滤器进行动态的加载,编译,运行。
Zuul的过滤器之间没有直接的相互通信,他们之间通过一个RequestContext的静态类来进行数据传递的。RequestContext类中有ThreadLocal变量来记录每个Request所需要传递的数据。
Zuul的过滤器是由Groovy写成,这些过滤器文件被放在Zuul Server上的特定目录下面,Zuul会定期轮询这些目录,修改过的过滤器会动态的加载到Zuul Server中以便过滤请求使用。
下面有几种标准的过滤器类型:
Zuul大部分功能都是通过过滤器来实现的。Zuul中定义了四种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期。
(1) PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
(2) ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
(3) POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
(4) ERROR:在其他阶段发生错误时执行该过滤器。
内置的特殊过滤器
zuul还提供了一类特殊的过滤器,分别为:StaticResponseFilter和SurgicalDebugFilter
StaticResponseFilter:StaticResponseFilter允许从Zuul本身生成响应,而不是将请求转发到源。
SurgicalDebugFilter:SurgicalDebugFilter允许将特定请求路由到分隔的调试集群或主机。
自定义的过滤器
除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。
例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。
之前我们的架构图现在加入Zuul就该变成这样了。那么为什么我们要加入Config Server呢?我们在做网关的时候,不太可能是一个静态的配置,而是将采用动态的配置。
补充一个知识点:Nginx+Lua也可以实现。可以参考https://www.linux78.com/Nginx+lua%E5%8A%9F%E8%83%BD%E5%BA%94%E7%94%A8自行学习。
我们先去start.spring.io构建项目,首先整合ribbon。
1.启动类增加@EnableZuulProxy注解,激活zuul。
2.application.properties增加如下配置,解释见注释。
server.port=7070
#整合ribbon-去除eureka注册
eureka.client.register-with-eureka=false
# 不获取注册列表信息, 是否从eureka服务器获取注册信息 , false = 不获取,true = 获取
eureka.client.fetch-registry=false
#Eureka Server服务URL,用于客户端注册
eureka.client.serviceUrl.defaultZone=http://localhost:12345/eureka
#配置“person-service”的负载均衡服务器列表
person-service.ribbon.listOfServers=http://localhost:9090
#Zuul配置person-service服务调用 形式如:zuul.routes.${app-name}=/${app-url-profile}/** (**代表目录的所有子目录包含父目录,*只包含当前目录)
zuul.routes.person-service=/person-service/**
3.新增spring boot security依赖,添加config包,包下类SecurityConfig.java,用于关闭安全。
(1)依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
(2)类SecurityConfig.java
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/**");
}
}
4.接着,我们打开上一篇文章中创建的Eureka server项目,启动Eureka Server。
5.然后,我们打开上一篇文章中创建的feign项目,先启动provider-server,也就是person-service。
6.启动postman,这里post形式访问http://localhost:9090/person/save,(由于我们启动的是9090服务提供者),先把数据存到内存中,然后访问http://localhost:9090/person/findAll发现可以正常返回数据。
7.启动zuul项目。
8.浏览器访问http://localhost:7070/person-service/person/findAll,注意:这里解释一下这个路由的规则,zuul ip:端口/服务提供方的服务名称/具体的提供方法。结果如下,说明服务已经成功被代理:
那么其实在一般场景,我们需要整合Eureka。那么以上我们将zuul项目eureka直接给关闭了,这里就不作关闭了,将其注册到注册中心。
1.zuul项目引入spring-cloud-starter-eureka依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
application.prperteis作出如下修改,注释掉关闭eureka的配置。顺便加上app-name。
spring.application.name=spring-cloud-zuul
server.port=7070
##整合ribbon-去除eureka注册
#eureka.client.register-with-eureka=false
## 不获取注册列表信息, 是否从eureka服务器获取注册信息 , false = 不获取,true = 获取
#eureka.client.fetch-registry=false
#Eureka Server服务URL,用于客户端注册
eureka.client.serviceUrl.defaultZone=http://localhost:12345/eureka
#配置“person-service”的负载均衡服务器列表
#person-service.ribbon.listOfServers=http://localhost:9090
#Zuul配置person-service服务调用 形式如:zuul.routes.${app-name}=/${app-url-profile}/** (**代表目录的所有子目录包含父目录,*只包含当前目录)
zuul.routes.person-service=/person-service/**
2.激活Eureka客户端。zuul项目启动类增加@EnableDiscoveryClient,这个注解和之前我们加在eureka客户端上的@EnableEurekaClient注解作用是一样的,不要误解。此时启动类如下:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class SpringcloudZuulApplication {
public static void main(String[] args) {
SpringApplication.run(SpringcloudZuulApplication.class, args);
}
}
3.这时启动zuul、项目,发现它也被注册到eureka了。
整合Hystrix和整合eureka一样简单。
1.在feign项目的服务端项目启动类上加上@EnableHystrix,激活断路器。具体如下:
import com.gupao.feign.api.service.PersonService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
/**
* @ClassName
* @Describe {@link PersonService}提供者应用
* @Author 66477
* @Date 2020/6/823:00
* @Version 1.0
*/
@SpringBootApplication
@EnableEurekaClient
@EnableHystrix
public class PersonServiceProviderAppplication {
public static void main(String[] args) {
SpringApplication.run(PersonServiceProviderAppplication.class,args);
}
}
2.配置短路规则(替代方法)
那么我们则在某个服务类上加上短路注解,这里举例,在feign项目的person-service的PersonServiceProviderController类的findAll方法上加上断路规则注解@HystrixCommnd,然后写一个替代方法。主方法findAll如果超过100毫秒未响应,则去访问替代方法fallbackForFindAll。我这里不做过多测试,如果想测试,就让主方法线sleep 100以上毫秒即可。
import com.gupao.feign.api.domain.Person;
import com.gupao.feign.api.service.PersonService;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand;
import com.netflix.hystrix.contrib.javanica.annotation.HystrixProperty;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @ClassName
* @Describe {@link PersonService} 提供者控制器(可以实现{@link PersonService}接口 )
* @Author 66477
* @Date 2020/6/823:01
* @Version 1.0
*/
@RestController
public class PersonServiceProviderController {
private Map<Long,Person> persons = new ConcurrentHashMap<>();
/**
* 保存
* @param person {@link Person}
* @return 如果成功,<code>true</code>
*/
@PostMapping(value = "/person/save")
public boolean save(@RequestBody Person person){
return persons.put(person.getId(),person) == null;
}
/**
* 查找所有的服务
* @return
*/
@GetMapping(value="/person/findAll")
@HystrixCommand(fallbackMethod = "fallbackForFindAll",
commandProperties =
@HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "100"))
public Collection<Person> findAll(){
return persons.values();
}
/**
* {@link #findAll()} fallback方法
* @return 空集合
*/
public Collection<Person> fallbackForFindAll(){
return Collections.emptyList();
}
}
那么此刻我们需要修改的是服务消费端,也就是feign项目里的person-client。
调用链路发生了改变,
spring-cloud-zuul(7070)->person-client(8080)->person-service(9090)(看不懂的话看上面的架构图)
1.person-client注册到Eureka Server。
注意:spring-cloud-zuul端口:7070 ,person-client端口:8080,person-service端口:9090,eureka-server端口:12345
注释person-client之前关闭eureka注册的配置。现在的配置如下就ok了
spring.application.name=person-client
server.port=8080
eureka.client.service-url.defaultZone=http://localhost:12345/eureka
management.endpoint.health.show-details=always
management.endpoints.web.exposure.include=*
然后启动person-client。
2.那么接下来,重点来了,现在我们要在网关应用,spring-cloud-zuul增加路由应用到person-client。将以下配置加入到zuul项目的application.properties下。
#Zuul配置person-client服务调用 形式如:zuul.routes.${app-name}=/${app-url-profile}/** (**代表目录的所有子目录包含父目录,*只包含当前目录)
zuul.routes.person-client=/person-client/**
现在我们总共注册了两个路由,一个person-service,一个person-client。通过http://localhost:7070/person-service/person/findAll,http://localhost:7070/person-client/person/findAll就都可以访问了。
补充一点,如果不想走注册中心,通过ribbon负载均衡服务器列表也可以实现访问链路。(person-client的application.properties只不过是被我注释了)
#整合ribbon-配置“person-service"的负载均衡的服务器列表(它是可以多配置的,"逗号隔开即可")
#person-service.ribbon.listOfServers:http://localhost:9090
#整合ribbon-配置“person-client"的负载均衡的服务器列表(它是可以多配置的)
#person-client.ribbon.listOfServers:http://localhost:9090
##整合ribbon-去除eureka注册
#eureka.client.register-with-eureka=false
## 不获取注册列表信息, 是否从eureka服务器获取注册信息 , false = 不获取,true = 获取
#eureka.client.fetch-registry: false
前面的配置是相对固定的,真实环境是需要一个动态路由,即需要动态配置。
1.我们把配置服务器配置到Eureka注册中心。复制之前第一篇文章的spring-cloud-config-server,我们先把配置文件app-name改了,改成spring-cloud-config-server,端口改成10000.
到目前为止,端口信息如下:
2.在resource下继续创建一个文件夹configs,更改application.propertes的本地仓库的GIT URI配置为
#${user.dir}是当前项目文件夹
spring.cloud.config.server.git.uri=file:///${user.dir}/src/main/resources/configs
这时我们application.properties整体就调整为
#定义服务名
spring.application.name=spring-cloud-config-server
#定义HTTP服务端口
server.port=10000
#本地仓库的GIT URI的配置
spring.cloud.config.server.git.uri=file:///${user.dir}/src/main/resources/configs
#全局关闭Actuator安全
#mangement.sercurity.enabled=false
#细粒度的开放Actuator EndPoints,注意:SpringBoot2.0版本后安全配置将不再是可定制,解决办法:
#在启动类上或者任意@Configure配置类上,移除默认自动启动的安全策略
#@EnableAutoConfiguration(exclude = {
# org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class
#})
#sensitive关注的是敏感,安全
#endpoints.env.sensitive=false
management.endpoints.enabled-by-default=true
management.endpoints.web.exposure.include=*
3.在configs下增加3个为zuul项目的配置文件
(1)zuul.properties配person-service
#应用spring-cloud-zuul默认配置项(profile为空)
#Zuul配置person-service服务调用 形式如:zuul.routes.${app-name}=/${app-url-profile}/** (**代表目录的所有子目录包含父目录,*只包含当前目录)
zuul.routes.person-service=/person-service/**
(2)zuul-test.properties配person-client
#应用spring-cloud-zuul默认配置项(profile为空)
#Zuul配置person-client服务调用 形式如:zuul.routes.${app-name}=/${app-url-profile}/** (**代表目录的所有子目录包含父目录,*只包含当前目录)
zuul.routes.person-client=/person-client/**
(3)zuul-prod.properties都配
#应用spring-cloud-zuul默认配置项(profile为空)
#Zuul配置person-service服务调用 形式如:zuul.routes.${app-name}=/${app-url-profile}/** (**代表目录的所有子目录包含父目录,*只包含当前目录)
zuul.routes.person-service=/person-service/**
#Zuul配置person-client服务调用 形式如:zuul.routes.${app-name}=/${app-url-profile}/** (**代表目录的所有子目录包含父目录,*只包含当前目录)
zuul.routes.person-client=/person-client/**
4.初始化configs目录(${user.dir}/src/main/resources/configs)为git根目录。
(1)打开git bash,进入到configs目录。
$ cd E:/Workplaces/IDEAWorkplace/wk-microservice/zuul/springcloud-config-server/src/main/resources/configs
(2)执行git初始化。
$ git init
(3)增加上述3个配置文件到git本地仓库。
$ git add *.properties
(4)提交到本地git仓库。
$ git commit -m "Temp commit"
过程图如下:
以上操作是为了让Spring Cloud Git配置服务器实现识别Git仓库,否则仅创建上面3个文件也没有效果。
5.注册到eureka server。
在config server项目下的application.properties增加服务注册。
#Eureka Server服务URL,用于客户端注册
eureka.client.serviceUrl.defaultZone=http://localhost:12345/eureka
记得还要引入eureka client依赖。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
然后启动类加上@EnableDiscoveryClient/@EnableEurekaClient激活Eureka Client。
最后启动项目,首先访问http://localhost:10000/zuul/prod,http://localhost:10000/zuul/test,http://localhost:10000/zuul/default来看看返回结果。此时Eureka注册中心信息如下:
6.在调整zuul项目之前,我们需要在config server项目中增加zuul依赖。
7.调整zuul项目中。
(1)现在不是采用config server中的动态配置文件了嘛,所以我们要回到zuul项目的application.properties将服务调用配置注释掉。
##Zuul配置person-service服务调用 形式如:zuul.routes.${app-name}=/${app-url-profile}/** (**代表目录的所有子目录包含父目录,*只包含当前目录)
#zuul.routes.person-service=/person-service/**
#
##Zuul配置person-client服务调用 形式如:zuul.routes.${app-name}=/${app-url-profile}/** (**代表目录的所有子目录包含父目录,*只包含当前目录)
#zuul.routes.person-client=/person-client/**
(2)接着将以下依赖到zuul的pom中。(以下依赖其实就是config clientr中引入的那个依赖)
<!--增加配置客户端的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
(3)在zuul项目中resources下创建bootstrap.properties文件,大致复制于之前的config client项目,不过这里不能像之一样配置服务器URI了,我们得采用Discovery client连接方式,这里就是这些配置文件需要经过eureka注册中心再给到应用,相当于间接给配置文件;而以前我们都是直接给的。内容如下:
#bootstrap上下文配置
#配置客户端应用名称:zuul,可当前应用是spring-cloud-zuul
spring.cloud.config.name=zuul
#profile 是激活的配置
spring.cloud.config.profile=prod
#label在Git中指的是分支名称
spring.cloud.config.label=master
##采用Discovery client连接方式
#激活discovery连接配置项的方式
spring.cloud.config.discovery.enabled=true
#配置config server应用名称(spring-cloud-config-server就是config server应用名称)
spring.cloud.config.discovery.service-id=spring-cloud-config-server
注意!!!,我们之前讲过,bootstrap.properties加载优先级是高于application.properties的,所以我们需要将application.properties中的注册到eureka server的配置放到bootstrap.properties文件中来。就是下面这货:
#Eureka Server服务URL,用于客户端注册
eureka.client.serviceUrl.defaultZone=http://localhost:12345/eureka
此时在application.properties开启所有端点。
management.endpoints.web.exposure.include=*
然后重启项目,访问http://localhost:7070/actuator/env,你会发现服务配置都注册进来了。
这时我们跑一下http://localhost:7070/person-client/person/findAll,结果如下(如果没有重新访问person/save接口刷新下内存):
这时记得上面我们zuul项目中的设置的是prod-properties文件,里面配置了两个,一个person-service调用,一个person-client调用。
其中访问http://localhost:7070/person-client/person/findAll,调用链:spring-cloud-zuul->person-client->person-service
那么访问http://localhost:7070/person-service/person/findAll,调用链:spring-cloud-zuul->person-service
1.看下来过程是:通过url去匹配zuul中配置的serviceId然后没整合ribbon时,直接去eureka中找服务实例去调用,如果整合了ribbon,直接去listOfService中取得一个实例,然后调用返回,对不?
解答:大致上可以这么理解,不过对应的listOfServicers不止是单个实例,而可能是一个集群,主要可以配置域名。
2.为什么要先调用client不直接调用,还是不太理解?
解答:这个只是一个演示程序,client在正式使用场景中,并不是以简单的调用,它可能是一个聚合服务。
3.zuul是不是更多的作为业务网关?
解答:是的,很多企业内部的服务通过Zuul做个服务网关。
4.RequestContext已经存在ThreadLocal中了,为什么还要使用ConcurrentHashMap?
解答:ThreadLocal只能管当前的线程,不能管理子线程。子线程需要使用InheritableThreadLocal。试想一下,如果上下文处于多线程环境,比如传递到了薪酬。比如:T1在管理RequestContext,但是T1又创建了多个线程(t1,t2),这个时候,把上下文传递到了子线程t1和t2.
java的进程所对应的线程main线程(group:main),main线程是所有子线程的父线程,main线程T1,T1又可以创建t1和t2。
5.ZuulServlet已经管理了RequestContext的生命周期了,为什么ContextLifecycleFilter还要再做一遍?
解答:ZuulServlet最终也会清理掉RequestContext:注意最后的finally
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
this.init((HttpServletRequest)servletRequest, (HttpServletResponse)servletResponse);
RequestContext context = RequestContext.getCurrentContext();
context.setZuulEngineRan();
try {
this.preRoute();
} catch (ZuulException var12) {
this.error(var12);
this.postRoute();
return;
}
try {
this.route();
} catch (ZuulException var13) {
this.error(var13);
this.postRoute();
return;
}
try {
this.postRoute();
} catch (ZuulException var11) {
this.error(var11);
}
} catch (Throwable var14) {
this.error(new ZuulException(var14, 500, "UNHANDLED_EXCEPTION_" + var14.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
问题是为什么ContextLifecycleFilter也这么干?
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
try {
chain.doFilter(req, res);
} finally {
RequestContext.getCurrentContext().unset();
}
}
不要忽略了ZuulServletFilter也是这么处理的。
RequestContext是任何Servlet或者Filter都能处理,那么为了防止不正确的关闭,那么ContextLifecycleFilter相当于兜底操作,就是防止ThreadLocal没有被remove掉。
ThreadLocal对应了一个Thread,那么是不是意味着Thread处理完了,那么ThreadLocal也随之GC呢?
所有的Servlet均采用线程池,因此,不清空的话,可能会出现意想不到的情况。除非,每次都异常!(这种情况也要依赖于线程池的实现)
原文:https://www.cnblogs.com/jmy520/p/13123577.html