聊一聊spring-cloud实现API网关(zuul)

2019-04-24  本文已影响0人  不改青铜本色

聊一聊spring-cloud实现API网关(zuul)

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路由管理

官方的文档对于路由的配置是通过在配置文件中进行配置的,这种方式有两个个弊端,如下:

加入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,该类的作用是发布事件,也就是把某个事件告诉所有与这个事件相关的监听器
具体的刷新流程其实就是从数据库重新加载了一遍,具体的处理逻辑,还需要去解读源码才能明白

image.png

在实际的线上环境中,url应填写外网地址,这样才能使用nginx进行负载转发,我这里方便调用故写的是ip:port的形式
分别访问两个接口,可以路由的调用


image.png

通过springcloud config实现动态路由

微服务之间的互相调用

@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);
}
@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的限流,并发参数设置等,最近时间有限,抽出空了在单独写

上一篇下一篇

猜你喜欢

热点阅读