- 实现自定义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 @Slf4j10 public class FirstPreZuulFilter extends ZuulFilter {11 12 /**13 *14 * @return 使用返回值设置Filter类型,可设置pre、route、post、error15 */16 @Override17 public String filterType() {18 return PRE_TYPE;19 }20 21 /**22 *23 * @return 使用返回值设置filter执行次序24 */25 @Override26 public int filterOrder() {27 return 0;28 }29 30 /**31 *32 * @return 使用返回值设值改Filter是否执行33 */34 @Override35 public boolean shouldFilter() {36 return true;37 }38 39 /**40 *41 * @return 核心执行逻辑42 * @throws ZuulException43 */44 @Override45 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 25 6 10spring-cloud-Zuul 7lb.study 81.0-SNAPSHOT 94.0.0 111.0-SNAPSHOT 12 13zuul-Filter 14 1516 17 35 3618 21 22 23 24 28 29 30org.springframework.cloud 19spring-cloud-starter-netflix-eureka-client 2031 34com.google.code.gson 32gson 3337 41zuul-filter-server 38eureka-server 39client-a 40
zuul-filter-server的pom:
org.springframework.cloud spring-cloud-starter-netflix-zuul
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 @EnableDiscoveryClient11 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-serverserver: port: 6666zuul: 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 @Override14 public String filterType() {15 return PRE_TYPE;16 }17 18 @Override19 public int filterOrder() {20 return 1;21 }22 23 @Override24 public boolean shouldFilter() {25 return true;26 }27 28 @Override29 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){ //如果不存在name37 //则禁止路由下级服务,但还是会被下个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 @Override13 public String filterType() {14 return PRE_TYPE;15 }16 17 @Override18 public int filterOrder() {19 return 2;20 }21 22 @Override23 public boolean shouldFilter() {24 //这里从上下文中取执行标识,并设置是否执行此Filter25 RequestContext requestContext = RequestContext.getCurrentContext();26 boolean bool = (boolean)requestContext.get("logic-is-success");27 return bool;28 }29 30 @Override31 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 @Override11 public String filterType() {12 return POST_TYPE;13 }14 15 @Override16 public int filterOrder() {17 return 3;18 }19 20 @Override21 public boolean shouldFilter() {22 return true;23 }24 25 @Override26 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 //获取上下文中保存的responseBody35 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 map12 *13 * @param key14 * @param value15 */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" key10 *11 * @param request12 */13 public void setRequest(HttpServletRequest request) {14 put("request", request);15 }16 17 /**18 * @return the HttpServletResponse from the "response" key19 */20 public HttpServletResponse getResponse() {21 return (HttpServletResponse) get("response");22 }23 24 /**25 * sets the "response" key to the HttpServletResponse passed in26 *27 * @param response28 */29 public void setResponse(HttpServletResponse response) {30 set("response", response);31 }
到此一个多层Filter业务处理完成。RequestContext中还有很多的API,比如通过InputStream responseDataStream = requestContext.getResponseDataStream();获取源服务返回的数据流,再做些统一处理。
本人所有的源码都存在github.com上面