乐趣区

关于java:深入Java微服务之网关系列2常见Java网关实现方案对比

 什么是服务网关

前文咱们曾经理解了构建微服务的根底 springboot,同时也能应用 springboot 构建服务。接下来咱们就基于 springboot 聊一下 springcloud。这个 springcloud 并不是一个特定的技术,它指的是微服务中一个生态体系。比方包含网关,注册核心,配置核心等。明天咱们就先理解一下微服务网关,微服务网关有很多种咱们这次采纳当初支流的 spring cloud gateway 来解说阐明。在微服务体系中,每个服务都是一个独立的模块都是一个独立运行的组件,一个残缺的微服务体系是由若干个独立的服务组成,每个服务实现本人业务模块性能。比方用户服务提供用户信息相干的服务和性能,领取模块提供领取相干的性能。各个服务之间通过 REST API 或者 RPC(当前讲)进行通信,并且个别咱们微服务要做到无状态的通信。咱们实现微服务之后在一些方面也会带来不不便的中央,如果网页端或者 app 端须要申请批改送货地址,还有购物之后要付款在这个场景下:

如上图会呈现一些问题:

  • 客户端要发动屡次申请,申请不同域名对应的服务,减少了通信老本以及对客户端代码的保护减少了复杂性。
  • 服务验证会在每个服务外面独自做,如果每个服务验证鉴权逻辑不同就会导致客户端重复验证。
  • 另外如果各个服务采纳的协定不同那么对于客户端来讲那就是灾难性的。

基于下面所以咱们就须要一个中间层,让客户端去申请中间层,至于须要申请那个服务由中间件去申请,最初将后果汇总返回给客户端,这个中间层就是网关。

为什么要应用网关

应用网关有几个作用:

对立鉴权

个别咱们在网关上进行鉴权有两种:1,是对于申请的客户端身份的认证。2,拜访权限管制就是当确认用户身份之后判断是否有某个资源的拜访权限。已经咱们在单体利用中,客户端申请验证身份和对于资源权限的束缚比较简单,通过申请的 session 就能够获取对应的用户以及权限信息,然而在微服务架构下,所有的服务都被拆成单个微服务而且还是集群部署这种状况就会变得复杂,因为如果还是应用 session 的话在分布式状况每次申请不肯定会落在同一台机器上,这样就会导致 session 有效。就须要咱们进行额定的工作保障集群中的 session 是统一的。所以咱们在网关层进行对立的解决认证:

日志记录

当客户端申请进来之后咱们须要记录以后申请的工夫依赖起源地址,ip 等信息,这样咱们就能够对立的在网关层面上进行拦挡获取,之后输入到日志文件中通过 ELK 组件进行输入,记录内容能够多维度多信息对立记录而不须要到具体每个服务中进行别离记录。

申请散发和过滤

对于网关来讲这个申请匹配散发是最重要的性能,咱们常见的 nginx 其实他这个组件就有申请转发和过滤的性能,对于网关来讲能够对申请进行前置和后置的过滤。

  • 申请散发:接管客户端的申请,将申请对应到前面的各个微服务上并申请微服务,因为微服务粒度比拟细,所以这个网关就能够对各个微服务进行功能性的整合最终给回客户端。
  • 过滤:网关会拦挡所有申请,相当于 spring 中 AOP 一个横向的切面,在这个切面上进行鉴权,限流,认证等操作。

灰度公布

个别公司的互联网产品都是迭代十分快的,根本都是小步快跑。根本是一个周公布一个版本迭代。在这种状况下就会呈现危险,比方兼容性,性能残缺度,工夫比拟短会存在 bug 最终产生事变等问题。这样个别咱们公布的时候会将新的性能公布到指定的机器上分过来一小部分流量来察看具体情况。所以网关作为申请的入口就正好能够实现这个性能。

罕用网关解决方案

个别咱们罕用的网关有几种比方:OpenResty、Zuul、Gateway、Kong、Tyk 等。咱们次要是用 spring 体系的框架,所以咱们本文针对 Gateway 进行解说,其它几种网关实现不做重点阐明,OpenResty 是有 nginx+lua 集成的 web 服务器,集成了许多三方库和模块。Zuul 其实 springcloud 后期也是在集成应用,到那时因为他的线程模型策略可能导致的性能问题最终 spring 抉择了本人研发的 spring cloud gateway。

环境筹备

本文咱们应用一个简略的案例来演示一下 spring cloud gateway 的应用办法,首先咱们须要住呢比 2 个 spring boot 的利用,具体创立形式请参考咱们本专题第二篇文章。

  • spring-cloud-gateway-service1  这个是一个微服务
  • Spring-cloud-gateway-wangguan  网关微服务

咱们依据以前专题创立了 2 个服务第一个服务咱们增加一个 controller

@RequestMapping(value = {"/getUser"}, method = RequestMethod.GET)
    public String getUser() {Map<String ,String> user = new HashMap<>();
        user.put("name", "张三");
        user.put("age", "45");
        String s = JSONObject.toJSONString(user);
        return s;
    }
复制代码

第二个网关服务咱们减少 pom 依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            spring-cloud-starter-gateway
            <version>2.0.4.RELEASE</version>
        </dependency>
复制代码

在 application.yml 中增加 gateway 路由

spring:
  cloud:
    gateway:
      routes:
      - predicates:
            - Path =/gateway/**  #匹配规定
        uri: http://localhost:8099/getUser    #服务 1 的拜访地址
        filters:
            - StripRrefix: 1 #去掉前缀
 server:port: 8077
复制代码

针对下面配置含意阐明:

  • uri:指标服务地址,可配置 uri 和 lb:// 应用服务名称
  • predicates:匹配条件,依据规定匹配是否申请该路由
  • filters: 过滤规定,这个过滤蕴含前置过滤和后置过滤,
  • StripPrefix=1,示意去掉前缀,即在转发指标 url 的时候去掉’gateway’

这个时候咱们启动服务之后发现服务启动日志:Netty started on port(s): 8077. 阐明咱们服务胜利了,并且网关依赖的是 nettyserver 启动几个服务监听。咱们拜访:

curl http://localhost:8077/gateway/getUser

在配置正确的状况下将会返回服务返回的后果。

spring cloud gateway 原理

上图是 gateway 官网给出的原理图,可能不太好了解,咱们本人画个图辅助了解一下:如上图有几个概念先阐明一下:

  • 路由(Route):是网关的组件之一,由 id,uri,predicate,filter 组成。
  • 断言(Predicate):匹配 http 申请中的内容。如果返回后果是 true 则就按以后的 router 进行转发。
  • 过滤器(Filter):为申请提供前置和后置的过滤。

当客户端发送申请到网关时,网关会依据一系列的 Predicate 的匹配后果来决定拜访哪个 route 路由,而后依据过滤器进行申请解决,过滤器能够在申请发送到后端服务之前和之后执行。

路由规定

spring cloud gateway 中提供了路由匹配机制,比方咱们前文配置的 Path=/gateway/** . 意思就是通过 Path 的属性来匹配 URL 前缀是 /gateway/ 的申请。其实 spring cloud gateway 给咱们提供了很多规定供咱们应用。每一个 Predicate 的应用,你能够了解为:当满足这种条件后才会被转发,如果是多个,那就是都满足的状况下被转发。这些 Predict 的源码在 org.springframework.cloud.gateway.handler.predicate 包中咱们简略看一下:

动静路由

gateway 配置路由次要有两种形式,1. 用 yml 配置文件,2. 写在代码里。而无论是 yml,还是代码配置,启动网关后将无奈批改路由配置,如有新服务要上线,则须要先把网关下线,批改 yml 配置后,再重启网关。这种形式如果在网关上没有优雅停机就会呈现服务间断,这无疑是不能被承受的。gateway 网关启动时,路由信息默认会加载内存中,路由信息被封装到 RouteDefinition 对象中,配置多个 RouteDefinition 组成 gateway 的路由零碎,RouteDefinition 中的属性与下面代码配置的属性一一对应:

那么就须要咱们的动静路由来解决这个问题了。Spring Cloud Gateway 提供了 Endpoint 端点,裸露路由信息,有获取所有路由、刷新路由、查看单个路由、删除路由等办法,具体实现类 org.springframework.cloud.gateway.actuate.GatewayControllerEndpoint,想拜访端点中的办法须要增加 spring-boot-starter-actuator 注解,并在配置文件中裸露所有端点。编写动静路由实现类,需实现 ApplicationEventPublisherAware 接口。

/**
 * 动静路由服务
 */
@Service
public class GoRouteServiceImpl implements ApplicationEventPublisherAware {
    @Autowired
    private RouteDefinitionWriter routeDefinitionWriter;
    private ApplicationEventPublisher publisher;
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {this.publisher = applicationEventPublisher;}
    // 减少路由
    public String add(RouteDefinition definition) {routeDefinitionWriter.save(Mono.just(definition)).subscribe();
        this.publisher.publishEvent(new RefreshRoutesEvent(this));
        return "success";
    }
    // 更新路由
    public String update(RouteDefinition definition) {
        try {delete(definition.getId());
        } catch (Exception e) {return "update fail,not find route  routeId:"+definition.getId();
        }
        try {routeDefinitionWriter.save(Mono.just(definition)).subscribe();
            this.publisher.publishEvent(new RefreshRoutesEvent(this));
            return "success";
        } catch (Exception e) {return "update route  fail";}
    }
    // 删除路由
    public Mono<ResponseEntity<Object>> delete(String id) {return this.routeDefinitionWriter.delete(Mono.just(id)).then(Mono.defer(() -> {return Mono.just(ResponseEntity.ok().build());
        })).onErrorResume((t) -> {return t instanceof NotFoundException;}, (t) -> {return Mono.just(ResponseEntity.notFound().build());
        });
    }
}
复制代码

编写 Rest 接口,通过这些接口实现动静路由性能.

@RestController
@RequestMapping("/changeRoute")
public class ChangeRouteController {
    @Autowired
    private GoRouteServiceImpl goRouteServiceImpl;
    // 减少路由
    @PostMapping("/add")
    public String add(@RequestBody GatewayRouteDefinition gwdefinition) {
        String flag = "fail";
        try {RouteDefinition definition = assembleRouteDefinition(gwdefinition);
            flag = this.goRouteService.add(definition);
        } catch (Exception e) {e.printStackTrace();
        }
        return flag;
    }
    // 删除路由
    @DeleteMapping("/routes/{id}")
    public Mono<ResponseEntity<Object>> delete(@PathVariable String id) {
        try {return this.goRouteService.delete(id);
        }catch (Exception e){e.printStackTrace();
        }
        return null;
    }
    // 更新路由
    @PostMapping("/update")
    public String update(@RequestBody GatewayRouteDefinition gwdefinition) {RouteDefinition definition = assembleRouteDefinition(gwdefinition);
        return this.goRouteService.update(definition);
    }
    // 把传递进来的参数转换成路由对象
    private RouteDefinition assembleRouteDefinition(GatewayRouteDefinition gwdefinition) {RouteDefinition definition = new RouteDefinition();
        definition.setId(gwdefinition.getId());
        definition.setOrder(gwdefinition.getOrder());
        // 设置断言
        List<PredicateDefinition> pdList=new ArrayList<>();
        List<GatewayPredicateDefinition> gatewayPredicateDefinitionList=gwdefinition.getPredicates();
        for (GatewayPredicateDefinition gpDefinition: gatewayPredicateDefinitionList) {PredicateDefinition predicate = new PredicateDefinition();
            predicate.setArgs(gpDefinition.getArgs());
            predicate.setName(gpDefinition.getName());
            pdList.add(predicate);
        }
        definition.setPredicates(pdList);
        // 设置过滤器
        List<FilterDefinition> filters = new ArrayList();
        List<GatewayFilterDefinition> gatewayFilters = gwdefinition.getFilters();
        for(GatewayFilterDefinition filterDefinition : gatewayFilters){FilterDefinition filter = new FilterDefinition();
            filter.setName(filterDefinition.getName());
            filter.setArgs(filterDefinition.getArgs());
            filters.add(filter);
        }
        definition.setFilters(filters);
        URI uri = null;
        if(gwdefinition.getUri().startsWith("http")){uri = UriComponentsBuilder.fromHttpUrl(gwdefinition.getUri()).build().toUri();
        }else{
            // uri 为 lb://consumer-service 时应用上面的办法
            uri = URI.create(gwdefinition.getUri());
        }
        definition.setUri(uri);
        return definition;
    }
}
复制代码

其实个别咱们很少通过 API 去调用 rest 服务去增删路由信息,个别咱们支流都是通过集成 nacos 的 config 性能动静削减路由。与 nacos 整合咱们前面在讲。

过滤器

网关过滤器 Filter 分为 Pre 和 Post 即前置过滤和后置过滤器。别离为在具体申请转发到后端微服务之前执行和将后果返回给客户端之前执行。内置的 GatewayFilter 比拟多大略有 19 种,如:

  • AddRequestHeader GatewayFilter Factory,
  • AddRequestParameter GatewayFilter Factory,
  • AddResponseHeader GatewayFilter Factory

就不过多举例了,应用起来也比较简单,咱们着重看一下如何自定义过滤器:

  1. 全局过滤器:全局过滤器,对所有的路由都无效,所有不必在配置文件中配置,次要实现了 GlobalFilter 和 Ordered 接口,并将过滤器注册到 spring 容器。
@Service
@Slf4j
public class AllDefineFilter implements GlobalFilter,Ordered{
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {log.info("

-Enter AllDefineFilter");
        return chain.filter(exchange).then(Mono.fromRunnable(()->{
            log.info("[post]-Return Result");
        }));
    }
    @Override
    public int getOrder() {
        return 0;
    }
}
复制代码

  1. 部分过滤器:须要在配置文件中配置,如果配置,则该过滤器才会失效。次要实现 GatewayFilter, Ordered 接口,并通过 AbstractGatewayFilterFactory 的子类注册到 spring 容器中,当然也能够间接继承 AbstractGatewayFilterFactory,在外面写过滤器逻辑,还能够从配置文件中读取内部数据。
@Component
@Slf4j
public class UserDefineGatewayFilter extends AbstractGatewayFilterFactory<UserDefineGatewayFilter.GpConfig>{public UserDefineGatewayFilter(){super(GpConfig.class);
    }

    @Override
    public GatewayFilter apply(GpConfig config) {return ((exchange, chain) -> {log.info("[Pre] Filter Request,name:"+config.getName());
            return chain.filter(exchange).then(Mono.fromRunnable(()->{log.info("[Post] Response Filter");
            }));
        });
    }

    public static class UserConfig{
        private String name;

        public String getName() {return name;}

        public void setName(String name) {this.name = name;}
    }
}
复制代码

这块须要有留神的中央:

  • 类名必须要对立以 GatewayFiterFactory 结尾,因为默认状况下过滤器的 name 会采纳该自定义类的前缀。这里的 name=UserDefine,也就是在 yml 中 filters 中的 name 值。
  • 在 apply 办法中,同时蕴含 Pre 和 Post 过滤。在 then 办法中是申请执行完结之后的后置解决。
  • UserConfig 是一个配置类,该类中只有一个属性 name。这个属性能够在 ym 文件中应用。
  • 该类须要装载到 Spring IoC 容器,此处应用 @Component 注解实现。

其实整个 spring cloud gateway 与 spring cloud alibaba 整合的很好,能够与 nacos 整合能够与 sentinel 整合进行限流,这个前期咱们独自进行解说。

作者:我是大明哥
链接:https://juejin.cn/post/692310…
起源:稀土掘金
著作权归作者所有。商业转载请分割作者取得受权,非商业转载请注明出处。


微信公众号【程序员黄小斜】作者是前蚂蚁金服 Java 工程师,专一分享 Java 技术干货和求职成长心得,不限于 BAT 面试,算法、计算机根底、数据库、分布式、spring 全家桶、微服务、高并发、JVM、Docker 容器,ELK、大数据等。关注后回复【book】支付精选 20 本 Java 面试必备精品电子书。

退出移动版