API网关服务组件Spring Cloud Zuul使用详解12(过滤器详解4:动态过滤器)

   
Spring Zuul 核心组件是过滤器,路由转发都是通过各种过滤器实现。作为网关服务器,它需要不间断的工作。就算是内部逻辑要改变,也最好不要使其停止工作。     对于路由规则的改变,我们之前介绍过可以通过远程配置中心来实现不停止服务的改变规则(
点击查看)。但是对于过滤器这种通过编码实现的规则,通过
java 实现动态改变是比较麻烦的。但是我们可以通过基于
JVM 上的动态语言
Groovy 来实现。

十四、动态过滤器

1,准备工作

(1)首先我们搭建一个基础的网关服务,具体步骤可以参考我之前写的文章:

(2)由于我们需要使用
Groovy,还需要在
pom.xml 文件中添加相关依赖:

<!--Groovy混合开发-->
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.5.6</version>
    <type>pom</type>
</dependency>

(3)接着编辑配置文件
application.properties,除了之前配置的应用名、端口号、注册中心地址、服务路由规则外。还定义了一些用来配置动态加载过滤器的参数:

  • zuul.filter.root 用来指定动态加载的过滤器存储路径
  • zuul.filter.interval 用来配置动态加载的时间间隔,以秒为单位
spring.application.name=api-gateway
server.port=5555

eureka.client.serviceUrl.defaultZone=http://localhost:1111/eureka/

zuul.routes.hello-service.path=/hello-service/**
zuul.routes.hello-service.service-id=hello-service

zuul.filter.root=/root/filter
zuul.filter.interval=5

(4)接着创建用来加载自定义属性的配置类
FilterConfiguration,内容如下: 至此我们就已经完成了为基础的
API 网关服务增加动态加载过滤器的能力:

  • 根据这里的定义 API 网关应用会每隔 5 秒,从 /root/filter/pre/root/filter/post 目录下获取 Groovy 定义的过滤器,并对其进行编译和动态加载使用。
  • 对于加载过滤器实现类的根目录可以通过 zuul.filter.root 调整根目录的位置来修改。
  • 对于动态加载的时间间隔,可通过 zuul.filter.interval 参数来修改。
@ConfigurationProperties("zuul.filter")
public class FilterConfiguration {

    private String root;
    private Integer interval;

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public Integer getInterval() {
        return interval;
    }

    public void setInterval(Integer interval) {
        this.interval = interval;
    }
}

(5)在项目主类中引入上面定义的
FilterConfiguration 配置,并创建动态加载过滤器的实例。

@SpringBootApplication
@EnableZuulProxy
@EnableConfigurationProperties(FilterConfiguration.class)
public class ApiGatewayApplication {


    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }

    @Bean
    public FilterLoader filterLoader(FilterConfiguration configuration) {
        FilterLoader filterLoader = FilterLoader.getInstance();
        filterLoader.setCompiler(new GroovyCompiler());
        try {
            FilterFileManager.setFilenameFilter(new GroovyFileFilter());
            FilterFileManager.init(
                    configuration.getInterval(),
                    configuration.getRoot() + "/pre",    // 路径一定要存在,管理器不会自动创建
                    configuration.getRoot() + "/post"    // 路径不存在就会报异常,抛出的信息并不友好
            );
        } catch (Exception e) {
            throw new RuntimeException();
        }
        return filterLoader;
    }
}

2,开始测试

(1)首先创建一个
pre 类型的过滤器,命名为
PreFilter.groovy,内容如下: 由于
pre 类型的过滤器在请求路由前执行,通常用来做一些签名校验的功能,这里我们在过滤器中输出一些请求相关的信息。

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

class Prefilter extends ZuulFilter {

    Logger log = LoggerFactory.getLogger(Prefilter.class);

    @Override
    String filterType() {
        // 前置过滤器
        return "pre"
    }

    @Override
    int filterOrder() {
        // 优先级,数字越大,优先级越低
        return 1000
    }

    @Override
    boolean shouldFilter() {
        //是否执行该过滤器,true代表需要过滤
        return true
    }

    @Override
    Object run() {
        HttpServletRequest request = RequestContext.getCurrentContext().getRequest()
        log.info("this is a pre filter: Send{} request to{}",
                request.getMethod(), request.getRequestURL().toString())
        return null
    }
}

(2)当
API 网关服务启动后,我们把 
PreFilter.groovy 文件添加到
 /root/filter/pre 目录下。不需要重启服务,只需稍等几秒,这个过滤器就会自动生效。

  • 我们尝试向 API 网关服务发起请求:http://localhost:5555/hello-service/hello,此时控制台中就可以看到 PreFilter.groovy 过滤器中定义的日志信息:



(3)接着创建一个
post 类型的过滤器,命名为
PostFilter.groovy,内容如下: 由于
post 类型的过滤器在请求路由返回后执行,我们可以进一步对这个结果做一些处理,对微服务返回的信息做一些加工。

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
import javax.servlet.http.HttpServletResponse

class Postfilter extends ZuulFilter {

    Logger log = LoggerFactory.getLogger(Postfilter.class);

    @Override
    String filterType() {
        // 前置过滤器
        return "post"
    }

    @Override
    int filterOrder() {
        // 优先级,数字越大,优先级越低
        return 2000
    }

    @Override
    boolean shouldFilter() {
        //是否执行该过滤器,true代表需要过滤
        return true
    }

    @Override
    Object run() {
        log.info("this is a post filter: Revice response")
        HttpServletResponse response = RequestContext.getCurrentContext().getResponse()
        response.getOutputStream().print(", I am hangge")
        response.flushBuffer()
    }
}

(4)同样当
API 网关服务启动后,我们把
PostFilter.groovy 文件添加到
/root/filter/post 目录下。不需要重启服务,只需稍等几秒,这个过滤器就会自动生效。

  • 我们尝试向 API 网关服务发起请求:http://localhost:5555/hello-service/hello,除了可以在控制台中看到 PostFilter.groovy 过滤器中定义的日志信息外,还可以从请求返回的内容发现过滤器的效果(返回内容尾部增加了“, I am hangge”)


附:使用动态加载过滤器时的注意事项

    在实际使用过程中,动态过滤器对于处理一些简单的常用过滤功能还是没有什么问题的,只是需要注意一些已知的问题并避开这些情况来使用即可。比如:

  • 在使用 Groovy 定义动态过滤器的时候,删除 Groovy 文件并不能从当前运行的 API 网关中移除这个过滤器,所以如果要移除的话可以通过修改 Groovy 过滤器的 shouldFilter 返回 false
  • 另外还需要注意一点,目前的动态过滤器是无法直接注入 API 网关服务的 Spring 容器中加载的实例来使用的。比如,我们是无法直接通过注入 Rest Template 等实例,在动态过滤器中对各个微服务发起请求的。
THE END