spring-cloud-Zuul学习(四)【中级】--自定义zuul Filter详解【重新定义spring cloud实践】

 

  • 实现自定义zuul Filter

方法很简单,只要继承ZuulFilter跟加入到spring IOC容器即可,zuulFilter是一个抽象类,里面包含以下方法需要我们实现:

String filterType():使用返回值设置Filter类型,可设置pre、route、post、error
int filterOrder():使用返回值设置filter执行次序
boolean shouldFilter():使用返回值设值改Filter是否执行
Object run() throws ZuulException:核心执行逻辑

说起这个,为了方便把上一节说的四种Filter类型粘上:
  •  Zuul总共有四种不同的生命周期类型的Filter:

pre:  在路由下级服务之前执行;比如鉴权、限流都是需要在此类Filter执行。

route:这类Filter是Zuul路由动作的执行者,是Apache HTTPClient或Netflix Ribbon构建和发送原始Http请求的地方,目前已支持OKHTTP。

post:这类Filter是源服务返回结果或发生异常信息发生后执行的;需要对返回信息做一些处理,可以在此类Filter处理。

error:在整个生命周期内如果发生异常,则会进入error Filter,可做全局异常处理。

  前面有说过了关于zuul-server的搭建,这里不说了,以表形式简单说明下:

项目名 端口 说明
eureka-server 8761 eureka注册中心
zuul-filter-server 6666 zuul服务端,里面将/client/**的请求路由到client-a去
client-a 7070 一个基本的接口提供者,有一个/student//getStudent接口

  以下为自定义pre类型的Filter测试类:

 1 /**
 2  * 第一个zuul-Filter
 3  * pre类型:路由到下游服务之前执行,可做限流、鉴权等这些
 4  * @author libo
 5  * @date 2019/4/28 17:47
 6  */ 
8
@Component 9 @Slf4j 10 public class FirstPreZuulFilter extends ZuulFilter { 11 12 /** 13 * 14 * @return 使用返回值设置Filter类型,可设置pre、route、post、error 15 */ 16 @Override 17 public String filterType() { 18 return PRE_TYPE; 19 } 20 21 /** 22 * 23 * @return 使用返回值设置filter执行次序 24 */ 25 @Override 26 public int filterOrder() { 27 return 0; 28 } 29 30 /** 31 * 32 * @return 使用返回值设值改Filter是否执行 33 */ 34 @Override 35 public boolean shouldFilter() { 36 return true; 37 } 38 39 /** 40 * 41 * @return 核心执行逻辑 42 * @throws ZuulException 43 */ 44 @Override 45 public Object run() throws ZuulException { 46 log.info("========================这是第一个自定义的Zuul-filter"); 47 return null; 48 } 49 }

 补充说明:可以静态导入FilterConstants类,里面提供了很多常量信息。(import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;)

我个人喜欢使用postman测试接口,访问链接:localhost:6666/client//student//getStudent,可以看到控制台日志:

 

  • 多层Filter处理案例:

上面是一个最简单的Filter,实际开发中经常会根据需求,然后自定义实现以上类型的FIlter。在Filter之中,通过com.netflix.zuul.context。RequestContext类进行通讯,内部采用ThreadLocal保存每个请求的一些信息,包括请求路由、错误信息、HttpServletRequest、HTTPServletResponse,它还扩展了ConcurrentHashMap,目的是为了在处理过程中保存任何形式的信息。

模拟业务需求:先使用NamePreZuulFilter判断是否传入了name,接着使用AgePreZuulFilter判断是否传入age,最后在PostZuulFilter里统一处理返回内容。

先以表格方式展示有几个服务,服务介绍:

项目 端口 说明
eureka-server 8761 本地eureka注册中心
client-a 7070 接口提供者,提供一个接收name跟age参数的接口,/student//addStudent
zuul-filter-server 6666 zuul服务端,里面存在两个pre:NamePreZuulFilter(1)、AgePreZuulFilter(2),一个post:PostZuulFilter(3)

整个项目图:

zuul-filter-server:

依赖说明:三个子项目父级zuul-filter-server的pom.xml这里我只引入了eureka客户端依赖、gson工具依赖,eureka-server那里就一个eureka服务依赖(spring-boot那些我在zuul-Filter的父级spring-cloud-Zuul那里引入)。

zuul-filter-server的pom.xml:

 1 <?xml version="1.0" encoding="UTF-8"?>
 2 <project xmlns="http://maven.apache.org/POM/4.0.0"
 3          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 4          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
 5     <parent>
 6         <artifactId>spring-cloud-Zuul</artifactId>
 7         <groupId>lb.study</groupId>
 8         <version>1.0-SNAPSHOT</version>
 9     </parent>
10     <modelVersion>4.0.0</modelVersion>
11     <version>1.0-SNAPSHOT</version>
12 
13     <artifactId>zuul-Filter</artifactId>
14 
15     <dependencies>
16         <!-- eureka客户端依赖开始 -->
17         <dependency>
18             <groupId>org.springframework.cloud</groupId>
19             <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
20         </dependency>
21         <!-- eureka客户端依赖结束 -->
22 
23         <!-- ribbon依赖开始 -->
24         <!--<dependency>
25             <groupId>org.springframework.cloud</groupId>
26             <artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
27         </dependency>-->
28         <!-- ribbon依赖结束 -->
29 
30         <dependency>
31             <groupId>com.google.code.gson</groupId>
32             <artifactId>gson</artifactId>
33         </dependency>
34     </dependencies>
35 
36     <modules>
37         <module>zuul-filter-server</module>
38         <module>eureka-server</module>
39         <module>client-a</module>
40     </modules>
41 </project>

zuul-filter-server的pom:

    <dependencies>
        <!-- zuul服务导包开始 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <!-- zuul服务导包结束 -->
    </dependencies>

zuul-filter-server启动类ZuulFilterServerApplication:

 1 package lb.study.zuul.zuulfilterserver;
 2 
 3 import org.springframework.boot.SpringApplication;
 4 import org.springframework.boot.autoconfigure.SpringBootApplication;
 5 import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
 6 import org.springframework.cloud.netflix.zuul.EnableZuulProxy;
 7 
 8 @SpringBootApplication
 9 @EnableZuulProxy //@EnableZuulProxy区别@EnableZuulServer:@EnableZuulServer缺少几个zuul内置的Filter,不举出来了
10 @EnableDiscoveryClient
11 public class ZuulFilterServerApplication {
12 
13     public static void main(String[] args) {
14         SpringApplication.run(ZuulFilterServerApplication.class, args);
15     }
16 
17 }

zuul-filter-server配置文件:

spring:
  application:
    name: zuul-filter-server
server:
  port: 6666

zuul:
  routes:
    client-a: /client/**
NamePreZuulFilter用来验证name是否存在,(int filterOrder())顺序为1。如果没传则使用setSendZuulResponse(false)禁止route Filter路由到源服务client-a,使用setResponseBody("信息")定制返回结果(在postZuulFilter有定制这个返回结果)。通过在上下文中设置参数
requestContext.set("logic-is-success",false)来作为下个Filter是否执行的标识:
 1 /**
 2  * 用来判断name是否存在的Filter
 3  * @author libo
 4  * @date 2019/4/29 11:38
 5  */
 6 @Component
 7 @Slf4j
 8 public class NamePreZuulFilter extends ZuulFilter {
 9 
10     private static Gson gson = new Gson();
11     private Map params = new LinkedHashMap();
12 
13     @Override
14     public String filterType() {
15         return PRE_TYPE;
16     }
17 
18     @Override
19     public int filterOrder() {
20         return 1;
21     }
22 
23     @Override
24     public boolean shouldFilter() {
25         return true;
26     }
27 
28     @Override
29     public Object run() throws ZuulException {
30         log.info("进入NamePreZuulFilter-----------");
31         //取得Request对象并取name参数
32         RequestContext requestContext = RequestContext.getCurrentContext();
33         HttpServletRequest request = requestContext.getRequest();
34         System.out.println("NamePreZuulFilter--request-----------"+request);//这里的request跟源服务那里的request是不同的,所以底层是一个新的请求
35         String name = request.getParameter("name");
36         if(name == null){//如果不存在name
37             //则禁止路由下级服务,但还是会被下个Filter拦截哦,别理解错了
38             requestContext.setSendZuulResponse(false);
39             //设置返回body,也作为下级判断
40             params.put("msg","姓名不能为空");
41             params.put("lastDate",new Date());
42             requestContext.setResponseBody(gson.toJson(params));
43             //logic-is-success执行标识保存在上下文中,可作为同类型下游Filter的开关
44             requestContext.set("logic-is-success",false);
45             //结束
46             return null;
47         }
48         //成功,设置logic-is-success执行标识
49         requestContext.set("logic-is-success",true);
50         return null;
51     }
52 }
AgePreZuulFilter执行顺序为2,用来验证age是否传入,也根据NamePreZuulFilter返回的logic-is-success标识是否执行此Filter。其他的跟前面一样,定制信息、设置是否路由源服务:
 1 /**
 2  * 接着判断age是否传过来
 3  * @author libo
 4  * @date 2019/4/29 13:58
 5  */
 6 @Component
 7 @Slf4j
 8 public class AgePreZuulFilter extends ZuulFilter {
 9     private static Gson gson = new Gson();
10     private Map params = new LinkedHashMap();
11 
12     @Override
13     public String filterType() {
14         return PRE_TYPE;
15     }
16 
17     @Override
18     public int filterOrder() {
19         return 2;
20     }
21 
22     @Override
23     public boolean shouldFilter() {
24         //这里从上下文中取执行标识,并设置是否执行此Filter
25         RequestContext requestContext = RequestContext.getCurrentContext();
26         boolean bool = (boolean)requestContext.get("logic-is-success");
27         return bool;
28     }
29 
30     @Override
31     public Object run() throws ZuulException {
32         log.info("进入AgePreZuulFilter-----------");
33         //取得Request对象并取name参数
34         RequestContext requestContext = RequestContext.getCurrentContext();
35         HttpServletRequest request = requestContext.getRequest();
36         String age = request.getParameter("age");
37         if(age==null){
38             //则禁止路由下级服务,但还是会被下个Filter拦截哦,别理解错了
39             requestContext.setSendZuulResponse(false);
40             //设置返回body,也作为下级判断
41             params.put("msg","年龄不能为空");
42             params.put("lastDate",new Date());
43             requestContext.setResponseBody(gson.toJson(params));
44             //logic-is-success执行标识保存在上下文中,可作为同类型下游Filter的开关
45             requestContext.set("logic-is-success",false);
46             //结束
47             return null;
48         }
49         //前面优先的FIlter设置默认logic-is-success=true了 这里就不设置了
50         return null;
51     }
52 }
PostZuulFilter是post类型FIlter,在源服务返回结果后执行,我这里设置顺序为3。在这里检查有无定制的ResponseBody,以及设置字符集,此外还设置了HTTP响应码:
 1 /**
 2  * 源服务返回结果类型
 3  * @author libo
 4  * @date 2019/4/29 14:19
 5  */
 6 @Component
 7 @Slf4j
 8 public class PostZuulFilter extends ZuulFilter {
 9 
10     @Override
11     public String filterType() {
12         return POST_TYPE;
13     }
14 
15     @Override
16     public int filterOrder() {
17         return 3;
18     }
19 
20     @Override
21     public boolean shouldFilter() {
22         return true;
23     }
24 
25     @Override
26     public Object run() throws ZuulException {
27         log.info("进入PostZuulFilter-----------");
28         //上下文
29         RequestContext requestContext = RequestContext.getCurrentContext();
30         //取response对象
31         HttpServletResponse response = requestContext.getResponse();
32         //响应字符类型
33         response.setCharacterEncoding("utf-8");
34         //获取上下文中保存的responseBody
35         String responseBody = requestContext.getResponseBody();
36         if(responseBody != null){//如果不为空,则说明存在异常流程(上面只有在异常的时候设置这个)
37             //设置响应信息
38             requestContext.setResponseBody(responseBody);
39             requestContext.setResponseStatusCode(500);
40         }
41         return null;
42     }
43 }

 

  • 测试

使用postman访问localhost:6666/client//student//addStudent:

  • 测试案例一:name跟age都为空:

状态码500(在postZuulFilter那里设置的HTTP响应状态码)

可以看到跳过了AgeZuulFilter直接进入PostZuulFilter。

  • 测试案例二:name不为空,age为空:

  • 测试案例三:name不为空,age不为空:

 

requestContext通讯分析:在使用requestContext进行通讯的时候,其实我想过:这个通讯不就是在上下文中存东西,然后用来通讯的吗?那我是不是可以直接用ServletRequest进行通讯?这个其实也是可以的,因为在各层Filter中那是同一个request,路由到源服务然后再回来也是同一个,毕竟没有去改存于ConcurrentHashMap内的request对象。这个可以通过RequestContext源码看清,它有继承ConcurrentHashMap,将一些request、response这些东西都存那里。

 1     /**
 2      * sets a key value to Boolen.TRUE
 3      *
 4      * @param key
 5      */
 6     public void set(String key) {
 7         put(key, Boolean.TRUE);
 8     }
 9 
10     /**
11      * puts the key, value into the map. a null value will remove the key from the map
12      *
13      * @param key
14      * @param value
15      */
16     public void set(String key, Object value) {
17         if (value != null) put(key, value);
18         else remove(key);
19     }
 1     /**
 2      * @return the HttpServletRequest from the "request" key
 3      */
 4     public HttpServletRequest getRequest() {
 5         return (HttpServletRequest) get("request");
 6     }
 7 
 8     /**
 9      * sets the HttpServletRequest into the "request" key
10      *
11      * @param request
12      */
13     public void setRequest(HttpServletRequest request) {
14         put("request", request);
15     }
16 
17     /**
18      * @return the HttpServletResponse from the "response" key
19      */
20     public HttpServletResponse getResponse() {
21         return (HttpServletResponse) get("response");
22     }
23 
24     /**
25      * sets the "response" key to the HttpServletResponse passed in
26      *
27      * @param response
28      */
29     public void setResponse(HttpServletResponse response) {
30         set("response", response);
31     }

 

到此一个多层Filter业务处理完成。RequestContext中还有很多的API,比如通过InputStream responseDataStream = requestContext.getResponseDataStream();获取源服务返回的数据流,再做些统一处理。

 本人所有的源码都存在github.com上面

posted @ 2019-04-29 16:03  像灭霸一样看日出  阅读(987)  评论(0编辑  收藏  举报