
聊一聊spring-cloud實(shí)現(xiàn)API網(wǎng)關(guān)(zuul)
API網(wǎng)關(guān)的作用就像是住宅樓的防盜門,你需要找哪戶人家,就按大門具體哪戶的按鈕,然后由防盜門接通指定的房子,然后由主人來開門
API網(wǎng)關(guān)將原本請(qǐng)求和服務(wù)之間多對(duì)一的關(guān)系簡(jiǎn)化為一對(duì)多,所有客戶端請(qǐng)求網(wǎng)關(guān),由網(wǎng)關(guān)統(tǒng)一請(qǐng)求具體的微服務(wù)
API網(wǎng)關(guān)的具體作用主要體現(xiàn)在控制路由,權(quán)限過濾,安全控制,負(fù)載均衡等等作用
zuul進(jìn)行路由控制
通過url的方式為zuul指定需要跳轉(zhuǎn)的路徑
在zuul的配置文件中,為我們需要跳轉(zhuǎn)的微服務(wù)指定映射的地址和實(shí)際訪問的url
server:
port: 8400
zuul:
routes:
users:
path: /user/**
url: http://example.com/users_service
訪問zuul端口下的服務(wù)
http://localhost:8400/user/***
這里就可以映射到url所指定的微服務(wù)上,這種寫法比較死,在實(shí)際的生產(chǎn)環(huán)境中,微服務(wù)往往都是配置為高可用,這種方式只能指定具體的某個(gè)微服務(wù)
將zuul加入eureka中,使用serverId進(jìn)行映射
因?yàn)槭褂胾rl的方式是在是太死了,所以我們把zuul加入到eureka中,這樣我們可以直接獲取所有的服務(wù)列表,指定serverId就可以了
server:
port: 8400
zuul:
routes:
users:
path: /user/**
serviceId: users-service
訪問zuul端口下的服務(wù)獲取數(shù)據(jù)
http://localhost:8400/user/***
實(shí)際上如果不進(jìn)行path->service-id的配置,也是可以直接進(jìn)行訪問的
http://localhost:8400/new-movie/userBack/1
但是如果新增了路由,需要重啟zuul服務(wù)
通過端點(diǎn)監(jiān)控來查看zuul上配置了哪些服務(wù)
添加actuator監(jiān)控相關(guān)maven
<!--添加端點(diǎn)監(jiān)控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
訪問路徑查看當(dāng)前zuul下映射的路由
http://localhost:8400/actuator/routes
使用actuator端點(diǎn)監(jiān)控需要注意的是,要在配置文件中開放訪問端口,否則會(huì)報(bào)404
management:
endpoints:
web:
exposure:
include: '*'
zuul實(shí)現(xiàn)過濾器功能
自定義過濾器繼承自zuulFilter,通過過濾器對(duì)請(qǐng)求進(jìn)行校驗(yàn),鑒權(quán)等操作
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.http.HttpServletRequest;
/**
* Created by W2G on 2019/1/7.
* 自定義zuul網(wǎng)關(guān)過濾器,需要實(shí)現(xiàn)ZuulFilter
* Q5,Q6
*/
public class ZuulSelfFilter extends ZuulFilter{
private static Logger log = LoggerFactory.getLogger(ZuulSelfFilter.class);
/**
* 該方法返回過濾器類型,有四種基本類型,對(duì)應(yīng)接受請(qǐng)求前中后和錯(cuò)誤攔截
* @return
*/
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 3;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx=RequestContext.getCurrentContext();
HttpServletRequest request=ctx.getRequest();
if (request.getParameter("new-movie") != null) {
// put the serviceId in `RequestContext`
ZuulSelfFilter.log.info(String.format("方法是 %s,路徑是 %s",request.getMethod(),request.getRequestURL().toString()));
}else{
ZuulSelfFilter.log.info(String.format("路徑是 %s,方法是 %s",request.getRequestURL().toString(),request.getMethod()));
}
return null;
}
}
自定義類實(shí)現(xiàn)fallbackProvider類
為zuul服務(wù)提供熔斷回退類,在調(diào)用相關(guān)微服務(wù)不可用時(shí),提供降級(jí)功能,zuul也集成了hystrix的功能
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* 實(shí)現(xiàn)FallbackProvider的實(shí)現(xiàn)類是為zuul提供的熔斷回退類,當(dāng)api不可用時(shí),提供熔斷降級(jí)處理
* zuul網(wǎng)關(guān)內(nèi)部默認(rèn)集成了Hystrix、Ribbon
*
* 在F版中需實(shí)現(xiàn)FallbackProvider類,F(xiàn)版以前不是FallbackProvider
*
*/
@Component
public class MyFallbackProvider implements FallbackProvider {
/**
* 為某個(gè)微服務(wù)提供回退操作, * 表示適用于所有回退類,否則指定serviceId
* @return
*/
@Override
public String getRoute() {
return "*";
}
@Override
public ClientHttpResponse fallbackResponse(String s, Throwable throwable) {
return new ClientHttpResponse() {
@Override
public HttpStatus getStatusCode() throws IOException {
//fallback時(shí)候的狀態(tài)碼
return HttpStatus.OK;
}
@Override
public int getRawStatusCode() throws IOException {
return 200;
}
@Override
public String getStatusText() throws IOException {
return "ok";
}
@Override
public void close() {
}
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream("該微服務(wù)已經(jīng)撲街了親".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
動(dòng)態(tài)提供zuul路由管理
官方的文檔對(duì)于路由的配置是通過在配置文件中進(jìn)行配置的,這種方式有兩個(gè)個(gè)弊端,如下:
- 每次添加新的路由都需要再配置文件中重新添加,并需要對(duì)項(xiàng)目進(jìn)行重啟(這個(gè)問題如果使用spring cloud config應(yīng)該也可以解決)
- 對(duì)于分布式項(xiàng)目來說,我通過eureka管理所有服務(wù),但是并不意味這我所有的服務(wù)都需要加入到eureka中,我們?nèi)绻褂胹erviceId就必須要把所有項(xiàng)目納入到eureka的管理當(dāng)中,zuul是面對(duì)所有系統(tǒng)的,這樣是具有侵入性的做法,如圖
加入Eureka中架構(gòu)圖

使用動(dòng)態(tài)路由,一方面是為了避免添加路由后重啟項(xiàng)目,一方面也可以避免zuul的侵入性
最終架構(gòu)圖

通過動(dòng)態(tài)路由的方式實(shí)現(xiàn)路由的跳轉(zhuǎn),需要滿足zuul中properties對(duì)動(dòng)態(tài)路由的加載,從數(shù)據(jù)庫中讀取我們的配置進(jìn)行加載,另一方面實(shí)現(xiàn)路由地址的動(dòng)態(tài)刷新,達(dá)到七層負(fù)載的效果
源碼地址:https://github.com/lexburner/zuul-gateway-demo
先上代碼
import org.springframework.cloud.netflix.zuul.filters.RefreshableRouteLocator;
import org.springframework.cloud.netflix.zuul.filters.SimpleRouteLocator;
import org.springframework.stereotype.Component;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.netflix.zuul.filters.ZuulProperties;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Created by W2G on 2019/4/22.
* 自定義動(dòng)態(tài)路由定位器
* Refer https://github.com/lexburner/zuul-gateway-demo
*/
@Component
public class CustomRouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {
public final static Logger logger = LoggerFactory.getLogger(CustomRouteLocator.class);
private JdbcTemplate jdbcTemplate;
private ZuulProperties properties;
@Autowired
public CustomRouteLocator(ServerProperties server, ZuulProperties properties, JdbcTemplate jdbcTemplate) {
super(server.getServlet().getContextPath(), properties);
this.properties = properties;
this.jdbcTemplate = jdbcTemplate;
logger.info("servletPath:{}",server.getServlet().getContextPath());
}
@Override
public void refresh() {
super.doRefresh();
}
/**
* 在simpleRouteLocator中具體就是在這兒定位路由信息的
* 在這里重寫方法后我們之后從數(shù)據(jù)庫加載路由信息,主要也是從這兒改寫
* @return
*/
@Override
protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
//先后順序很重要,這里優(yōu)先采用DB中配置的路由映射信息,然后才使用本地文件路由配置
routesMap.putAll(locateRoutesFromDB());
routesMap.putAll(super.locateRoutes());
//
LinkedHashMap<String, ZuulProperties.ZuulRoute> values = new LinkedHashMap<>();
for (Map.Entry<String, ZuulProperties.ZuulRoute> entry : routesMap.entrySet()) {
String path = entry.getKey();
if (!path.startsWith("/")) {
path = "/" + path;
}
if (StringUtils.isNotBlank(this.properties.getPrefix())) {
path = this.properties.getPrefix() + path;
if (!path.startsWith("/")) {
path = "/" + path;
}
}
values.put(path, entry.getValue());
}
return values;
}
@Cacheable(value = "locateRoutes",key = "RoutesFromDB",condition ="true")
public Map<String, ZuulProperties.ZuulRoute> locateRoutesFromDB(){
Map<String, ZuulProperties.ZuulRoute> routes = new LinkedHashMap<>();
//創(chuàng)建動(dòng)態(tài)路由配置類,用來獲取所有的路由配置
List<CustomZuulRoute> results = jdbcTemplate.query("select * from zuul_gateway_routes where enabled =1 ",new BeanPropertyRowMapper<>(CustomZuulRoute.class));
for (CustomZuulRoute result : results) {
if(StringUtils.isBlank(result.getPath())
|| (StringUtils.isBlank(result.serviceId) && StringUtils.isBlank(result.getUrl()))){
continue;
}
ZuulProperties.ZuulRoute zuulRoute = new ZuulProperties.ZuulRoute();
try {
BeanUtils.copyProperties(result,zuulRoute);
} catch (Exception e) {
logger.error("load zuul route info from db has error",e);
}
routes.put(zuulRoute.getPath(),zuulRoute);
}
return routes;
}
public static class CustomZuulRoute {
private String id;
private String path;
private String serviceId;
private String url;
private boolean stripPrefix = true;
private Boolean retryable;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getServiceId() {
return serviceId;
}
public void setServiceId(String serviceId) {
this.serviceId = serviceId;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public boolean isStripPrefix() {
return stripPrefix;
}
public void setStripPrefix(boolean stripPrefix) {
this.stripPrefix = stripPrefix;
}
public Boolean getRetryable() {
return retryable;
}
public void setRetryable(Boolean retryable) {
this.retryable = retryable;
}
}
}
上面的代碼,是通過啟動(dòng)時(shí)獲取數(shù)據(jù)庫當(dāng)中的路由配置,保存并實(shí)現(xiàn)動(dòng)態(tài)刷新,下面是我的理解,不對(duì)的地方請(qǐng)指出
通過自定義類繼承SimpleRouteLocator實(shí)現(xiàn)RefreshableRouteLocator的方式靈活來管理路由
核心1:讀取數(shù)據(jù)庫配置路由地址并跳轉(zhuǎn),在simpleroutelocator類中,通過zuulProperties獲取配置文件
核心修改:locateRoutes(),具體就是在這兒定位路由信息的,我們之后從數(shù)據(jù)庫加載路由信息,主要也是從這兒改寫該方法的主要作用就是加載配置文件,獲取路由的對(duì)應(yīng)關(guān)系
核心實(shí)現(xiàn):重寫SimpleRouteLocator的getRoutes()方法,該方法是通過獲取routes以list的形式提供至內(nèi)存的路由關(guān)系中,我們重寫該方法,根據(jù)locateRoutes()獲取的map類型的路由定位器,最終同樣把定位的Routes以list的方式提供出去,這個(gè)list實(shí)際就是對(duì)應(yīng)匹配的路由列表
邏輯實(shí)現(xiàn):getMatchingRoute()方法,可以根據(jù)實(shí)際路徑匹配并返回getRoutes()中具體的Route來進(jìn)行業(yè)務(wù)邏輯的操作
實(shí)現(xiàn)動(dòng)態(tài)刷新
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.RoutesRefreshedEvent;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 刷新路由服務(wù)(當(dāng)DB路由有變更時(shí),應(yīng)調(diào)用refreshRoute方法)
*/
@RestController
public class RefreshRouteService {
@Autowired
ApplicationEventPublisher publisher;
@Autowired
RouteLocator routeLocator;
@GetMapping("/refreshRoute")
public void refreshRoute() {
RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
publisher.publishEvent(routesRefreshedEvent);
}
}
實(shí)現(xiàn)配置的實(shí)時(shí)刷新,這里需要提到另外一個(gè)類DiscoveryClientRouteLocator,它具備實(shí)時(shí)刷新的作用
原理:zuul中提供了路由刷新監(jiān)聽器的功能(onApplicationEvent()),在這個(gè)方法中如果事件是RoutesRefreshedEvent,下一次匹配到路徑時(shí),如果發(fā)現(xiàn)為臟,則會(huì)去刷新路由信息
方法:使用ApplicationEventPublisher,該類的作用是發(fā)布事件,也就是把某個(gè)事件告訴所有與這個(gè)事件相關(guān)的監(jiān)聽器
具體的刷新流程其實(shí)就是從數(shù)據(jù)庫重新加載了一遍,具體的處理邏輯,還需要去解讀源碼才能明白

在實(shí)際的線上環(huán)境中,url應(yīng)填寫外網(wǎng)地址,這樣才能使用nginx進(jìn)行負(fù)載轉(zhuǎn)發(fā),我這里方便調(diào)用故寫的是ip:port的形式
分別訪問兩個(gè)接口,可以路由的調(diào)用

通過springcloud config實(shí)現(xiàn)動(dòng)態(tài)路由
- 大致原理:通過client config中配置zuul服務(wù)的映射關(guān)系,配合mq+bus+hook的方式實(shí)現(xiàn)配置文件的自動(dòng)刷新生效,使其具備映射服務(wù)的能力
詳細(xì)源碼待研究
微服務(wù)之間的互相調(diào)用
- 在日常微服務(wù)之間的調(diào)用中,我們通常是使用feignClient注解進(jìn)行調(diào)用,在標(biāo)注服務(wù)選擇微服務(wù)的name中寫死跳轉(zhuǎn)微服務(wù)的名稱,這種方式比較死,不方便不同微服務(wù)之間的調(diào)用
@FeignClient(name = "new-user",configuration = FooConfiguration.class,fallback = HystrixClientFallback.class)
public interface StoreClient {
/**
* 實(shí)現(xiàn)feign的回退機(jī)制
* @param id
* @return
*/
@RequestLine("GET /feignsFallBack/{id}")
public UserInfo feignsFallBack(@Param("id") int id);
}
- 通過網(wǎng)關(guān)進(jìn)行配置,,在name的地方配置網(wǎng)關(guān)的名稱,在@RequestLine的地方配置跳轉(zhuǎn)微服務(wù)地址和路徑
@FeignClient(name = "zuul",fallback =DemoRemoteService.DemoRemoteServiceFallback.class )
public interface DemoRemoteService extends DemoService {
@RequestMapping(value = "/service/test/{name}")
String test(@PathVariable("name") String name);
}
zuul其余用法
zuul的限流,并發(fā)參數(shù)設(shè)置等,最近時(shí)間有限,抽出空了在單獨(dú)寫