微服务SpringCloud之服务网关zuul二

Zuul的核心

Filter是Zuul的核心,用来实现对外服务的控制。Filter的生命周期有4个,分别是“PRE”、“ROUTING”、“POST”、“ERROR”,整个生命周期可以用下图来表示。

 

Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • ERROR:在其他阶段发生错误时执行该过滤器。 除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。

Zuul中默认实现的Filter

类型顺序过滤器功能
pre -3 ServletDetectionFilter 标记处理Servlet的类型
pre -2 Servlet30WrapperFilter 包装HttpServletRequest请求
pre -1 FormBodyWrapperFilter 包装请求体
route 1 DebugFilter 标记调试标志
route 5 PreDecorationFilter 处理请求上下文供后续使用
route 10 RibbonRoutingFilter serviceId请求转发
route 100 SimpleHostRoutingFilter url请求转发
route 500 SendForwardFilter forward请求转发
post 0 SendErrorFilter 处理有错误的请求响应
post 1000 SendResponseFilter 处理正常的请求响应

自定义Filter

 实现自定义Filter,需要继承ZuulFilter的类,并覆盖其中的4个方法。

我们假设有这样一个场景,因为服务网关应对的是外部的所有请求,为了避免产生安全隐患,我们需要对请求做一定的限制,比如请求中含有Token便让请求继续往下走,如果请求不带Token就直接返回并给出提示。

首先自定义一个Filter,在run()方法中验证参数是否含有Token。

package com.example.demo;

import javax.servlet.http.HttpServletRequest;

import org.apache.commons.lang.StringUtils;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;

public class CusFilter extends ZuulFilter {
    @Override
    public String filterType() {
        return "pre"; //定义filter的类型,有pre、route、post、error四种
    }

    @Override
    public int filterOrder() {
        return 10; //定义filter的顺序,数字越小表示顺序越高,越先执行
    }

    @Override
    public boolean shouldFilter() {
        return true; //表示是否需要执行该filter,true表示执行,false表示不执行
    }

    @Override
    public Object run() {
       // return null; //filter需要执行的具体操作
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

       // logger.info("--->>> TokenFilter {},{}", request.getMethod(), request.getRequestURL().toString());

        String token = request.getParameter("token");// 获取请求的参数

        if (StringUtils.isNotBlank(token)) {
            ctx.setSendZuulResponse(true); //对请求进行路由
            ctx.setResponseStatusCode(200);
            ctx.set("isSuccess", true);
            return null;
        } else {
            ctx.setSendZuulResponse(false); //不对其进行路由
            ctx.setResponseStatusCode(400);
            ctx.setResponseBody("token is empty");
            ctx.set("isSuccess", false);
            return null;
        }
    }
    
}
View Code

将CusFilter加入到请求拦截队列,在启动类中添加以下代码:

    @Bean
    public CusFilter tokenFilter() {
        return new CusFilter();
    }

测试

分别启动EurekaServer、EurekaClient、SpringColudZuulSimple。输入http://localhost:8890/producer/hello?name=cuiyw时页面显示如下。

 

输入url带token时输出正常。

 

 

 

路由熔断

 

当我们的后端服务出现异常的时候,我们不希望将异常抛出给最外层,期望服务可以自动进行一降级。Zuul给我们提供了这样的支持。当某个服务出现异常时,直接返回我们预设的信息。

我们通过自定义的fallback方法,并且将其指定给某个route来实现该route访问出问题的熔断处理。主要继承ZuulFallbackProvider接口来实现,ZuulFallbackProvider默认有两个方法,一个用来指明熔断拦截哪个服务,一个定制返回内容。

实现类通过实现getRoute方法,告诉Zuul它是负责哪个route定义的熔断。而fallbackResponse方法则是告诉 Zuul 断路出现时,它会提供一个什么返回值来处理请求。

后来Spring又扩展了此类,丰富了返回方式,在返回的内容中添加了异常信息,因此最新版本建议直接继承类FallbackProvider 。

测试

依次启动EurekaServer、EurekaClient(设置不同端口启动)、SpringColudZuulSimple,之后需要将EurekaClient断开一个,如果不知道断开哪个时可以使用进程id。

package com.example.demo;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

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;


@Component
public class ProducerFallback implements FallbackProvider{

    public ClientHttpResponse fallbackResponse() {
        // TODO Auto-generated method stub
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                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("The service is unavailable.".getBytes());
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.APPLICATION_JSON);
                return headers;
            }
        };
    }
    
    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        if (cause != null && cause.getCause() != null) {
            String reason = cause.getCause().getMessage();
           // logger.info("Excption {}",reason);
        }
        return fallbackResponse();
    }
    @Override
    public String getRoute() {
        
        return "spring-cloud-producer";
    }

}
View Code

当服务出现异常时,打印相关异常信息,并返回”The service is unavailable.”。

路由重试

有时候因为网络或者其它原因,服务可能会暂时的不可用,这个时候我们希望可以再次对服务进行重试,Zuul也帮我们实现了此功能,需要结合Spring Retry 一起来实现。下面我们以上面的项目为例做演示。

添加Spring Retry依赖

首先在spring-cloud-zuul项目中添加Spring Retry依赖。

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.4.RELEASE</version>
</dependency>

开启Zuul Retry

再配置文件中配置启用Zuul Retry,在main方法中添加@EnableRetry注解

#是否开启重试功能
zuul.retryable=true
#对当前服务的重试次数
ribbon.MaxAutoRetries=2
#切换相同Server的次数
ribbon.MaxAutoRetriesNextServer=0

测试

在EurekaClient中增加支持日志功能,引入spring-boot-starter-log4j2,同时排除start-web中默认的日志。

        </dependency>
                <dependency>
               <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-log4j2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <exclusions>
                <exclusion>
                    <artifactId>spring-boot-starter-logging</artifactId>
                    <groupId>org.springframework.boot</groupId>
                </exclusion>
            </exclusions>
        </dependency>

在application.properties同级目录下增加log4j2.xml配置文件。

<?xml version="1.0" encoding="UTF-8"?>

<!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出;可以设置成OFF(关闭)或Error(只输出错误信息) -->
<!--monitorInterval:Log4j2能够自动检测修改配置 文件和重新配置本身,设置间隔秒数 -->
<Configuration status="WARN" monitorInterval="30">
    <Properties>
        <!-- 缺省配置(用于开发环境),配置日志文件输出目录和动态参数。其他环境需要在VM参数中指定; “sys:”表示:如果VM参数中没指定这个变量值,则使用本文件中定义的缺省全局变量值 -->
        <Property name="instance">EurekaClient</Property>
        <Property name="log.dir">D:\log\logs</Property>
    </Properties>
    <!-- 定义所有的appender -->
    <Appenders>

        <!-- 优先级从高到低分别是 OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL -->
        <!-- 单词解释: Match:匹配 DENY:拒绝 Mismatch:不匹配 ACCEPT:接受 -->
        <!-- DENY,日志将立即被抛弃不再经过其他过滤器; NEUTRAL,有序列表里的下个过滤器过接着处理日志; ACCEPT,日志会被立即处理,不再经过剩余过滤器。 -->
        <!--输出日志的格式
         %d{yyyy-MM-dd HH:mm:ss, SSS} : 日志生产时间
         %p : 日志输出格式
         %c : logger的名称
         %m : 日志内容,即 logger.info("message")
         %n : 换行符
         %C : Java类名
         %L : 日志输出所在行数
         %M : 日志输出所在方法名
         hostName : 本地机器名
         hostAddress : 本地ip地址 -->

        <!--这个输出控制台的配置 -->
        <Console name="Console" target="SYSTEM_OUT">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="DEBUG" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
            <!--输出日志的格式 -->
            <PatternLayout
                    pattern="[%date{yyyy-MM-dd HH:mm:ss.SSS}][%thread][%level][%class][%line]:%message%n"/>
        </Console>
        <!-- info及以上级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档
        <RollingRandomAccessFile> filepattern 中的日期格式精确位数决定了生成日志的日期单位,
            如果按月生成日志,那么 filePath 修改为 "${LOG_HOME}/app-%d{yyyy-MM}.log";
            按小时生成日志,filePath = "${LOG_HOME}/app-%d{yyyy-MM-dd-HH-mm}.log"; -->
        <RollingRandomAccessFile name="infoLog"
                                 fileName="${log.dir}/${instance}-info.log"
                                 filePattern="${log.dir}/%d{yyyy-MM}/${instance}-info-%d{yyyy-MM-dd}-%i.log.gz"
                                 append="true">
            <!--filePattern="${log.dir}/%d{yyyy-MM}/${instance}-info-%d{mm-ss}-%i.log.gz"-->
            <PatternLayout
                    pattern="[%date{yyyy-MM-dd HH:mm:ss.SSS}][%thread][%level][%class][%line]:%message%n"/>
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch) -->
            <Filters>
                <ThresholdFilter level="info" onMatch="ACCEPT"
                                 onMismatch="DENY"/>
            </Filters>
            <Policies>
                <!-- 基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour -->
                <TimeBasedTriggeringPolicy interval="1"
                                           modulate="true"/>
                <!-- 基于指定文件大小的滚动策略,size属性用来定义每个日志文件的大小 -->
                <SizeBasedTriggeringPolicy size="200MB"/>
                <!-- DefaultRolloverStrategy:用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的(通过max属性) -->
            </Policies>
            <!-- <CronTriggeringPolicy schedule="* * * * * ?"/>触发策略 -->
            <!-- <DirectWriteRolloverStrategy maxFiles="10" /> -->
            <!-- 最多备份30天以内的日志,此处为策略限制,Delete中可以按自己需要用正则表达式编写 -->
            <!-- DefaultRolloverStrategy 加属性:max="30" 保留近30天的日志文件 -->
            <DefaultRolloverStrategy>
                <!-- 在rollover时间内匹配删除基本目录下所有满足参数glob等于*/wyait-*.log.gz和超过3天或更早的文件。 -->
                <!-- 1.maxDepth:要访问的目录的最大级别数。值为0表示仅访问起始文件(基本路径本身),除非被安全管理者拒绝。Integer.MAX_VALUE的值
                    表示应该访问所有级别。默认为1,意思是指定基本目录中的文件。
                    2. age的单位:D、H、M、S,分别表示天、小时、分钟、秒
                    3. basePath表示日志存储的基目录,maxDepth=“1”表示当前目录。因为我们封存的历史日志在basePath里面的backup目录,所以maxDepth设置为2。 -->
                <Delete basePath="${log.dir}" maxDepth="2">
                    <!-- IfFileName - glob: 如果regex没有指定的话,则必须。使用类似于正则表达式但是又具有更简单的有限模式语言来匹配相对路径(相对于基本路径) -->
                    <IfFileName glob="*/EurekaClient-*.log.gz"/>
                    <!-- IfLastModified - age: 必须。指定持续时间duration。该条件接受比指定持续时间更早或更旧的文件。 -->
                    <IfLastModified age="90D"/>
                </Delete>
            </DefaultRolloverStrategy>
        </RollingRandomAccessFile>

        <!-- warn级别的日志信息 -->
        <RollingRandomAccessFile name="warnLog"
                                 fileName="${log.dir}/${instance}-warn.log"
                                 filePattern="${log.dir}/%d{yyyy-MM}/${instance}-warn-%d{yyyy-MM-dd}-%i.log.gz"
                                 append="true">
            <Filters>
                <ThresholdFilter level="error" onMatch="DENY"
                                 onMismatch="NEUTRAL"/>
                <ThresholdFilter level="warn" onMatch="ACCEPT"
                                 onMismatch="DENY"/>
            </Filters>
            <PatternLayout
                    pattern="[%date{yyyy-MM-dd HH:mm:ss.SSS}][%thread][%level][%class][%line]:%message%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1"
                                           modulate="true"/>
                <SizeBasedTriggeringPolicy size="200MB"/>
            </Policies>
        </RollingRandomAccessFile>

        <!-- error级别的日志信息 -->
        <RollingRandomAccessFile name="errorLog"
                                 fileName="${log.dir}/${instance}-error.log"
                                 filePattern="${log.dir}/%d{yyyy-MM}/${instance}-error-%d{yyyy-MM-dd}-%i.log.gz"
                                 append="true">
            <Filters>
                <ThresholdFilter level="ERROR" onMatch="ACCEPT"
                                 onMismatch="DENY"/>
            </Filters>
            <PatternLayout
                    pattern="[%date{yyyy-MM-dd HH:mm:ss.SSS}][%thread][%level][%class][%line]:%message%n"/>
            <Policies>
                <TimeBasedTriggeringPolicy interval="1"
                                           modulate="true"/>
                <SizeBasedTriggeringPolicy size="200MB"/>
            </Policies>
        </RollingRandomAccessFile>
    </Appenders>

    <!-- 全局配置,默认所有的Logger都继承此配置 -->
    <!-- 用来配置LoggerConfig,包含一个root logger和若干个普通logger。 additivity指定是否同时输出log到父类的appender,缺省为true。
        一个Logger可以绑定多个不同的Appender。只有定义了logger并引入的appender,appender才会生效。 -->
    <Loggers> <!-- 第三方的软件日志级别 -->
        <logger name="org.springframework" level="info" additivity="true">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="infoLog"/>
            <AppenderRef ref="warnLog"/>
            <AppenderRef ref="errorLog"/>
        </logger>
        <logger name="java.sql.PreparedStatement" level="debug"
                additivity="true">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="infoLog"/>
        </logger>
        <!-- root logger 配置 -->
        <Root level="debug" includeLocation="true">
            <AppenderRef ref="infoLog"/>
            <AppenderRef ref="Console"/>
            <AppenderRef ref="errorLog"/>
        </Root>
        <!-- AsyncRoot - 异步记录日志 - 需要LMAX Disruptor的支持 -->
        <!-- <AsyncRoot level="info" additivity="false"> <AppenderRef ref="Console"
            /> <AppenderRef ref="infoLog" /> <AppenderRef ref="errorLog" /> </AsyncRoot> -->
    </Loggers>
</Configuration>
View Code

修改HelloController方法。

@RestController
public class HelloController {
    private  static final Logger logger = LoggerFactory
            .getLogger(HelloController.class);
    @RequestMapping("/hello")
    public String index(@RequestParam String name) {
       
         logger.info("request two name is "+name);
            try{
                Thread.sleep(1000000);
            }catch ( Exception e){
                logger.error(" hello two error",e);
            }
            return "hello "+name+",this is two messge";
            //return "hello "+name+",this is first messge";
        
    }
}
View Code

修改端口启动EurekaClient,在浏览器刷新http://localhost:8890/spring-cloud-producer/hello?name=cuiyw&token=123

在页面输出The service is unavailable时,可发现下面日志,说明进行了3次请求。

posted @ 2019-09-23 22:14  社会主义接班人  阅读(425)  评论(0编辑  收藏  举报