spring cloud备忘笔记-6-API网关统一访问接口

  • 引入
  • Zuul
  • 创建Zuul服务
  • 测试
  • 甩锅
  • Zuul的过滤器
  • 测试过滤器



笔记索引:
spring cloud备忘笔记-0-目录索引

引入

通常我们的客户端的的一个页面可能调用了多个服务,而我们让客户端直接记住所有的rest api,连那么多的服务是不太现实的选择。我们需要来一个网关,客户端来找api网关,由网关统一负责找所有的服务。

客户端

负载均衡服务器

Api网关1

Api网关2

Api网关3

各种服务1,2,3,4,5


Api网关和服务都需要注册到服务注册中心,最后的配置文件也要独立部署,将配置服务文件放在git仓库里。也就是项目中是不需要配置application.yml的而是在仓库中部署。这样我们只要动态的修改配置文件,直接上传到git仓库。所有的服务立刻就可以拿到最新的配置,就不用为了修改配置文件而重启服务了。这是配置的云部署模式。

Zuul

这边我们是用Zuul来做服务网关 。它的主要功能是路由转发过滤器。我们可以配置/api/a即转发到A服务,配置/api/b就转发到B服务。

创建Zuul服务

hi-spring-cloud-zuul 创建步骤见spring cloud备忘笔记-0-目录索引 依赖

<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.momomian</groupId>
        <artifactId>hi-spring-cloud-dependencies</artifactId>
        <version>1.0.0-SNAPSHOT</version>
        <relativePath>../hi-spring-cloud-dependencies/pom.xml</relativePath>
    </parent>

    <artifactId>hi-spring-cloud-zuul</artifactId>
    <packaging>jar</packaging>

    <name>hi-spring-cloud-zuul</name>
    <inceptionYear>2019-Now</inceptionYear>

    <dependencies>
        <!-- Spring Boot Begin -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!-- Spring Boot End -->

        <!-- Spring Cloud Begin -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!-- Spring Cloud End -->
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.momomian.hi.spring.cloud.zuul.ZuulApplication</mainClass>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

配置文件入下,这里将服务的路由routes配置好,这里的api-a是自己取得。serviceId指定服务标识。这里feign得path配置的是: /api/b/** 而 ribbon的是/api/a/**,我们之后调用的时候就得加上这个路径了。

application.yml

spring:
  application:
    name: hi-spring-cloud-zuul

server:
  port: 8786

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8780/eureka/

zuul:
  routes:
    api-a:
      path: /api/a/**
      serviceId: hi-spring-cloud-web-admin-ribbon
    api-b:
      path: /api/b/**
      serviceId: hi-spring-cloud-web-admin-feign

ZuulApplication .java 使用注解@EnableZuulProxy

package com.momomian.hi.spring.cloud.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplication {
    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class,args);
    }
}

测试

依次运行 EurekaApplication、ServiceAdminApplication、WebAdminRibbonApplication、WebAdminFeignApplication、ZuulApplication,
此时之前调用接口得方式就得改了,我们调用feign得服务接口hi:http://localhost:8785/api/b/hi?message=hi 这边我们已经部署了五个服务了,所以电脑性能注意下,这边如果出现500,timed out 超时,可以多刷几次。但是这个失败是由于网络原因导致的。我们现在无法确保高可用,就要做好回调处理。

甩锅

我们可以创建一个类实现它提供的一个回调接口:
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider; 这里面需要实现两个方法:getRoute和fallbackResponse
前者我们返回需要回调的服务name即可,
后者是用于请求服务失败,就会返回指定的信息给调用者的方法,我们拿ClientHttpResponse对象实现。
拿feign这个服务试一下,

package com.momomian.hi.spring.cloud.zuul.fallback;


import com.fasterxml.jackson.databind.ObjectMapper;
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;
import java.util.HashMap;
import java.util.Map;

@Component
public class WebAdminFeignFallbackProvider implements FallbackProvider {

    @Override
    public String getRoute() {
        // ServiceId,如果需要所有调用都支持回退,则 return "*" 或 return null
        return "hi-spring-cloud-web-admin-feign";
    }

    /**
     * 如果请求服务失败,则返回指定的信息给调用者
     * @param route
     * @param cause
     * @return
     */
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
           
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;
            }

            @Override
            public int getRawStatusCode() throws IOException {
                return HttpStatus.OK.value();
            }

            @Override
            public String getStatusText() throws IOException {
                return HttpStatus.OK.getReasonPhrase();
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                ObjectMapper objectMapper = new ObjectMapper();
                Map<String, Object> map = new HashMap<>();
                map.put("status", 200);
                map.put("message", "无法连接,请检查您的网络");
                return new ByteArrayInputStream(objectMapper.writeValueAsString(map).getBytes("UTF-8"));
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                // 响应json数据,和 getBody 中的内容编码一致
                headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
                return headers;
            }
        };
    }
}

这里我们网关向 api 服务请求失败了,但是消费者客户端向网关发起的请求是成功的,
注意:我们不应该把 api 的 404,500 等问题抛给客户端,网关和 api 服务集群对于客户端来说应该是黑盒的。
所以我们要把状态码改成200,提示用户是否有网络问题
这里其实是自己的问题,但是我们通常要把问题赖给用户,我们称之为甩锅

Zuul的过滤器

之前我们使用spingmvc实现了过滤器,现在我们可以使用spring cloud使用zuul来创建过滤器,做安全验证的功能。
我们写个类继承ZuulFilter,之后实现四个方法,其中主要是run()方法来实现具体业务功能,
filterType方法指定过滤类型,如下:

pre:路由之前
routing:路由之时
post:路由之后
error:发送错误调用

shouldFilter方法用于配置是否要过滤。filterOrder是指定过滤顺序,数值越小越优先。
接下来我们看具体实现过滤:逻辑上就是我们要拿到某个请求,然后检测有没有带我需要的某个参数。没有带我就拦截你。
这里我们想要取到HttpRequest,因为使用的是zuul,那就直接看看它老人家有没有。我们看这个类
com.netflix.zuul.context.RequestContext;

RequestContext context = RequestContext.getCurrentContext();
  HttpServletRequest request = context.getRequest();

这样就拿到了,我们只要判断里面有没有我们需要的参数比如:token,根据自己的规则判断。如下:

package com.momomian.hi.spring.cloud.zuul.filter;

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 org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

@Component
public class LoginFilter extends ZuulFilter {

    private static final Logger logger = LoggerFactory.getLogger(LoginFilter.class);
    //在路由之前调用
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 配置过滤的顺序
     * @return
     */
    @Override
    public int filterOrder() {
        return 0;
    }

    /**
     * 配置是否需要过滤:true/需要,false/不需要
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤器的具体业务代码
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        logger.info("{} >>> {}", request.getMethod(), request.getRequestURL().toString());
        String token = request.getParameter("token");
        if (token == null) {
            logger.warn("Token is empty");
            //不发送zuul响应了
            context.setSendZuulResponse(false);
            //没有权限不允许访问
            context.setResponseStatusCode(401);
            try {
                //然后写点东西
                  //指定编码
                HttpServletResponse response = context.getResponse();
                response.setContentType("text/html;charset=utf-8");
                context.getResponse().getWriter().write("不允许请求");
            } catch (IOException e) {
            }
        } else {
            logger.info("OK");
        }
        return null;
    }
}

实际的token的规则按照具体需求规则分配,这里只是简单判断有没有。

测试过滤器

以下:浏览器访问:http://localhost:8785/api/b/hi?message=HelloZuul&token=123 网页显示正常,不带网页显示我们配置的过滤器response。

笔记索引:
spring cloud备忘笔记-0-目录索引