美文网首页
聊一聊spring-cloud实现API网关(zuul)

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

作者: 不改青铜本色 | 来源:发表于2019-04-24 16:42 被阅读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路由管理

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

    • 每次添加新的路由都需要再配置文件中重新添加,并需要对项目进行重启(这个问题如果使用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,该类的作用是发布事件,也就是把某个事件告诉所有与这个事件相关的监听器
    具体的刷新流程其实就是从数据库重新加载了一遍,具体的处理逻辑,还需要去解读源码才能明白

    image.png

    在实际的线上环境中,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的限流,并发参数设置等,最近时间有限,抽出空了在单独写

    相关文章

      网友评论

          本文标题:聊一聊spring-cloud实现API网关(zuul)

          本文链接:https://www.haomeiwen.com/subject/ccmwgqtx.html