API网关的作用就像是住宅楼的防盗门,你需要找哪户人家,就按大门具体哪户的按钮,然后由防盗门接通指定的房子,然后由主人来开门
API网关将原本请求和服务之间多对一的关系简化为一对多,所有客户端请求网关,由网关统一请求具体的微服务
API网关的具体作用主要体现在控制路由,权限过滤,安全控制,负载均衡等等作用
zuul进行路由控制
通过url的方式为zuul指定需要跳转的路径
在zuul的配置文件中,为我们需要跳转的微服务指定映射的地址和实际访问的url
server:
port: 8400
zuul:
routes:
users:
path: /user/**
url: http://example.com/users_service
访问zuul端口下的服务
http://localhost:8400/user/***
这里就可以映射到url所指定的微服务上,这种写法比较死,在实际的生产环境中,微服务往往都是配置为高可用,这种方式只能指定具体的某个微服务
将zuul加入eureka中,使用serverId进行映射
因为使用url的方式是在是太死了,所以我们把zuul加入到eureka中,这样我们可以直接获取所有的服务列表,指定serverId就可以了
server:
port: 8400
zuul:
routes:
users:
path: /user/**
serviceId: users-service
访问zuul端口下的服务获取数据
http://localhost:8400/user/***
实际上如果不进行path->service-id的配置,也是可以直接进行访问的
http://localhost:8400/new-movie/userBack/1
但是如果新增了路由,需要重启zuul服务
通过端点监控来查看zuul上配置了哪些服务
添加actuator监控相关maven
<!--添加端点监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
访问路径查看当前zuul下映射的路由
http://localhost:8400/actuator/routes
使用actuator端点监控需要注意的是,要在配置文件中开放访问端口,否则会报404
management:
endpoints:
web:
exposure:
include: '*'
zuul实现过滤器功能
自定义过滤器继承自zuulFilter,通过过滤器对请求进行校验,鉴权等操作
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网关过滤器,需要实现ZuulFilter
* Q5,Q6
*/
public class ZuulSelfFilter extends ZuulFilter{
private static Logger log = LoggerFactory.getLogger(ZuulSelfFilter.class);
/**
* 该方法返回过滤器类型,有四种基本类型,对应接受请求前中后和错误拦截
* @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;
}
}
自定义类实现fallbackProvider类
为zuul服务提供熔断回退类,在调用相关微服务不可用时,提供降级功能,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;
/**
* 实现FallbackProvider的实现类是为zuul提供的熔断回退类,当api不可用时,提供熔断降级处理
* zuul网关内部默认集成了Hystrix、Ribbon
*
* 在F版中需实现FallbackProvider类,F版以前不是FallbackProvider
*
*/
@Component
public class MyFallbackProvider implements FallbackProvider {
/**
* 为某个微服务提供回退操作, * 表示适用于所有回退类,否则指定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时候的状态码
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("该微服务已经扑街了亲".getBytes());
}
@Override
public HttpHeaders getHeaders() {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return headers;
}
};
}
}
动态提供zuul路由管理
官方的文档对于路由的配置是通过在配置文件中进行配置的,这种方式有两个个弊端,如下:
- 每次添加新的路由都需要再配置文件中重新添加,并需要对项目进行重启(这个问题如果使用spring cloud config应该也可以解决)
- 对于分布式项目来说,我通过eureka管理所有服务,但是并不意味这我所有的服务都需要加入到eureka中,我们如果使用serviceId就必须要把所有项目纳入到eureka的管理当中,zuul是面对所有系统的,这样是具有侵入性的做法,如图
加入Eureka中架构图
传统路由方式使用动态路由,一方面是为了避免添加路由后重启项目,一方面也可以避免zuul的侵入性
最终架构图
动态路由架构通过动态路由的方式实现路由的跳转,需要满足zuul中properties对动态路由的加载,从数据库中读取我们的配置进行加载,另一方面实现路由地址的动态刷新,达到七层负载的效果
源码地址: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.
* 自定义动态路由定位器
* 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中具体就是在这儿定位路由信息的
* 在这里重写方法后我们之后从数据库加载路由信息,主要也是从这儿改写
* @return
*/
@Override
protected Map<String, ZuulProperties.ZuulRoute> locateRoutes() {
LinkedHashMap<String, ZuulProperties.ZuulRoute> routesMap = new LinkedHashMap<>();
//先后顺序很重要,这里优先采用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<>();
//创建动态路由配置类,用来获取所有的路由配置
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;
}
}
}
上面的代码,是通过启动时获取数据库当中的路由配置,保存并实现动态刷新,下面是我的理解,不对的地方请指出
通过自定义类继承SimpleRouteLocator实现RefreshableRouteLocator的方式灵活来管理路由
核心1:读取数据库配置路由地址并跳转,在simpleroutelocator类中,通过zuulProperties获取配置文件
核心修改:locateRoutes(),具体就是在这儿定位路由信息的,我们之后从数据库加载路由信息,主要也是从这儿改写该方法的主要作用就是加载配置文件,获取路由的对应关系
核心实现:重写SimpleRouteLocator的getRoutes()方法,该方法是通过获取routes以list的形式提供至内存的路由关系中,我们重写该方法,根据locateRoutes()获取的map类型的路由定位器,最终同样把定位的Routes以list的方式提供出去,这个list实际就是对应匹配的路由列表
逻辑实现:getMatchingRoute()方法,可以根据实际路径匹配并返回getRoutes()中具体的Route来进行业务逻辑的操作
实现动态刷新
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;
/**
* 刷新路由服务(当DB路由有变更时,应调用refreshRoute方法)
*/
@RestController
public class RefreshRouteService {
@Autowired
ApplicationEventPublisher publisher;
@Autowired
RouteLocator routeLocator;
@GetMapping("/refreshRoute")
public void refreshRoute() {
RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
publisher.publishEvent(routesRefreshedEvent);
}
}
实现配置的实时刷新,这里需要提到另外一个类DiscoveryClientRouteLocator,它具备实时刷新的作用
原理:zuul中提供了路由刷新监听器的功能(onApplicationEvent()),在这个方法中如果事件是RoutesRefreshedEvent,下一次匹配到路径时,如果发现为脏,则会去刷新路由信息
方法:使用ApplicationEventPublisher,该类的作用是发布事件,也就是把某个事件告诉所有与这个事件相关的监听器
具体的刷新流程其实就是从数据库重新加载了一遍,具体的处理逻辑,还需要去解读源码才能明白
在实际的线上环境中,url应填写外网地址,这样才能使用nginx进行负载转发,我这里方便调用故写的是ip:port的形式
分别访问两个接口,可以路由的调用
image.png
通过springcloud config实现动态路由
- 大致原理:通过client config中配置zuul服务的映射关系,配合mq+bus+hook的方式实现配置文件的自动刷新生效,使其具备映射服务的能力
详细源码待研究
微服务之间的互相调用
- 在日常微服务之间的调用中,我们通常是使用feignClient注解进行调用,在标注服务选择微服务的name中写死跳转微服务的名称,这种方式比较死,不方便不同微服务之间的调用
@FeignClient(name = "new-user",configuration = FooConfiguration.class,fallback = HystrixClientFallback.class)
public interface StoreClient {
/**
* 实现feign的回退机制
* @param id
* @return
*/
@RequestLine("GET /feignsFallBack/{id}")
public UserInfo feignsFallBack(@Param("id") int id);
}
- 通过网关进行配置,,在name的地方配置网关的名称,在@RequestLine的地方配置跳转微服务地址和路径
@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的限流,并发参数设置等,最近时间有限,抽出空了在单独写
网友评论