/ Spring Cloud  

Spring Cloud 8: API Gateway - Zuul Part 2

The first impression of Zuul is usually like this: it includes two functions: routing and filtering requests. The routing function is responsible for forwarding external requests to specific microservice instances, which is the basis for achieving a unified entrance to external access. The filter function is responsible for intervening in the processing of requests, and is the basis for implementing functions such as request verification and service aggregation. However, in reality, when the routing function is running, its routing mapping and request forwarding are completed by several different filters. Among them, the route mapping is mainly completed by a PRE type filter, which matches the request path with the configured routing rule to find the destination address that needs to be forwarded. The part of the request forwarding is completed by the Route type filter, which forwards the routing address obtained by the PRE type filter. Therefore, the filter can be said to be the most important core component of Zuul‘s API gateway. Every request entering Zuul will go through a series of filter processing chains to get the request response and return it to the client.

Filter Introduction

1. Filter characteristics

The key features of Zuul filters are:

  • Type: Defines when to be executed during request execution;
  • Execution Order: When there are multiple filters, it is used to indicate the order of execution. The smaller the value, the earlier the execution;
  • Criteria: the conditions under which the filter will be triggered;
  • Action: The specific action.

Filters do not communicate directly, but share information through RequestContext, which is thread-safe.

Corresponding to the characteristics of the Zuul filter above, the methods we need to implement when implementing a custom filter are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PreTypeZuulFilter extends ZuulFilter {
@Override
public String filterType() {
return PRE_TYPE;
}

@Override
public int filterOrder() {
return PRE_DECORATION_FILTER_ORDER - 1;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
this.logger.info("This is pre-type zuul filter.");
return null;
}
}

among them:

  • The filterType() method is the type of the filter;
  • The filterOrder() method returns the execution order;
  • The shouldFilter() method is to determine whether the filter needs to be executed;
  • run() is the specific filtering action to be performed.

2. Filter type

Zuul defines four standard filter types, which correspond to the typical life cycle of a request.

  • PRE filter: called before the request is routed, it can be used to implement authentication, select the requested microservice in the cluster, record debugging information, etc;
  • ROUTING filter: called when routing requests;
  • POST filter: It is executed after routing to the microservice, and can be used to add standard HTTP headers to the response, collect statistics and indicators, send the response from the microservice to the client, etc;
  • ERROR filter: Called when an error occurs while processing the request.

The types of Zuul filters are actually the life cycle of Zuul filters. Use the following diagram to understand their execution process.



In addition to the 4 default filter types given above, Zuul allows us to create custom filter types. For example, we can customize a STATIC type filter to generate a response directly in Zuul without forwarding the request to the microservices on the backend.

3. Custom filter example code

Take a look at a few examples given by the official:

PRE type example

QueryParamServiceIdPreFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class QueryParamPreFilter extends ZuulFilter { 
public int filterOrder() {
// run before PreDecorationFilter
return PRE_DECORATION_FILTER_ORDER - 1;
}

public String filterType() {
return "pre";
}

@Override
public boolean shouldFilter() {
RequestContext ctx = getCurrentContext();
return ctx.getRequest().getParameter("service") != null;
}

public Object run() {
RequestContext ctx = getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// put the serviceId in `RequestContext`
ctx.put("serviceId", request.getParameter("service"));
return null;
}
}

This example obtains the serviceID to be forwarded from the request parameter service. Of course, it’s not recommended, here is just an example.

ROUTE type example

OkHttpRoutingFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class OkHttpRoutingFilter extends ZuulFilter {
@Autowired
private ProxyRequestHelper helper;

@Override
public String filterType() {
return ROUTE_TYPE;
}

@Override
public int filterOrder() {
return SIMPLE_HOST_ROUTING_FILTER_ORDER - 1;
}

@Override
public boolean shouldFilter() {
return RequestContext.getCurrentContext().getRouteHost() != null && RequestContext.getCurrentContext().sendZuulResponse();
}

@Override
public Object run() {
OkHttpClient httpClient = new OkHttpClient.Builder()
// customize
.build();

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

String method = request.getMethod();

String uri = this.helper.buildZuulRequestURI(request);

Headers.Builder headers = new Headers.Builder();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
Enumeration<String> values = request.getHeaders(name);

while (values.hasMoreElements()) {
String value = values.nextElement();
headers.add(name, value);
}
}

InputStream inputStream = request.getInputStream();

RequestBody requestBody = null;
if (inputStream != null && HttpMethod.permitsRequestBody(method)) {
MediaType mediaType = null;
if (headers.get("Content-Type") != null) {
mediaType = MediaType.parse(headers.get("Content-Type"));
}
requestBody = RequestBody.create(mediaType, StreamUtils.copyToByteArray(inputStream));
}

Request.Builder builder = new Request.Builder()
.headers(headers.build())
.url(uri)
.method(method, requestBody);

Response response = httpClient.newCall(builder.build()).execute();

LinkedMultiValueMap<String, String> responseHeaders = new LinkedMultiValueMap<>();
for (Map.Entry<String, List<String>> entry : response.headers().toMultimap().entrySet()) {
responseHeaders.put(entry.getKey(), entry.getValue());
}

this.helper.setResponse(response.code(), response.body().byteStream(), responseHeaders);
context.setRouteHost(null); // prevent SimpleHostRoutingFilter from running
return null;
}
}

This example converts an HTTP request to an OkHttp3 request, and converts the server’s return into a servlet’s response.

POST type example

AddResponseHeaderFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AddResponseHeaderFilter extends ZuulFilter {
public String filterType() {
return "post";
}

public int filterOrder() {
return 999;
}

public boolean shouldFilter() {
return true;
}

public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpServletResponse servletResponse = context.getResponse();
servletResponse.addHeader("X-Foo", UUID.randomUUID().toString());
return null;
}
}

This example is just adding a randomly generated X-Foo to the returned header.

4. Disable filter

Only need to configure the filter to be disabled in application.properties(or yml), the format is:

1
zuul. [Filter-name]. [Filter-type] .disable = true

Such as:

1
zuul.FormBodyWrapperFilter.pre.disable=true

5. A little supplement about Zuul filter Error

When Zuul throws an exception during execution, the error filter is executed. SendErrorFilter will only execute if RequestContext.getThrowable() is not empty. It sets the error information into the requested javax.servlet.error.* properties and forwards to Spring Boot’s error page.

The specific class implemented by Zuul filters is ZuulServletFilter, whose core code is as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
try {
preRouting();
} catch (ZuulException e) {
error(e);
postRouting();
return;
}

// Only forward onto to the chain if a zuul response is not being sent
if (!RequestContext.getCurrentContext().sendZuulResponse()) {
filterChain.doFilter(servletRequest, servletResponse);
return;
}

try {
routing();
} catch (ZuulException e) {
error(e);
postRouting();
return;
}
try {
postRouting();
} catch (ZuulException e) {
error(e);
return;
}
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNCAUGHT_EXCEPTION_FROM_FILTER_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}

It can be seen from this code that error can be executed after catching exceptions in all stages, but if an exception occurs in the post stage and is handled by error, it will not be returned to the post stage for execution, which means that there should be no exceptions in the post stage. Because once there is an exception, other post filters behind this filter will no longer be executed.

A simple method for global exception handling is: Add a filter of type error and write the error information to the RequestContext so that SendErrorFilter can get the error information. Code show as below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class GlobalErrorFilter extends ZuulFilter { 
@Override
public String filterType() {
return ERROR_TYPE;
}

@Override
public int filterOrder() {
return 10;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
Throwable throwable = context.getThrowable();
this.logger.error("[ErrorFilter] error message: {}", throwable.getCause().getMessage());
context.set("error.status_code", HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
context.set("error.exception", throwable.getCause());
return null;
}
}

@EnableZuulServer VS @EnableZuulProxy

Zuul provides us with two main application annotations: @EnableZuulServer and @EnableZuulProxy, where @EnableZuulProxy contains the functionality of @EnableZuulServer, and @EnableCircuitBreaker and @EnableDiscoveryClient are also added. When we need to run a Zuul service without proxy function, or selectively switch some proxy functions, then we need to use @EnableZuulServer instead of @EnableZuulProxy. At this time we can add any ZuulFilter type entity class. They will be automatically loaded, which is the same as the previous article using @EnableZuulProxy, but it will not automatically load any proxy filters.

1 @EnableZuulServer’s default filter

When we use @EnableZuulServer, the filters loaded by default are:

PRE type filter

  • ServletDetectionFilter
    This filter is executed first. It is mainly used to check whether the current request is processed through Spring’s DispatcherServlet or processed through ZuulServlet. The result is stored in isDispatcherServletRequest, and the value type is Boolean.

  • FormBodyWrapperFilter
    The purpose of this filter is to wrap the request body that meets the requirements into a FormBodyRequestWrapper object for subsequent processing.

  • DebugFilter
    PRE type filter. When the debug parameter is set in the request parameter, this filter will set RequestContext.setDebugRouting() and RequestContext.setDebugRequest() to true in the current request context, so that subsequent filters can define some debug information based on these two parameters. When there is a problem in the production environment, we can add the parameter to print debugging information in the background to help us analyze the problem. For the name of the debug parameter in the request, we can customize it through zuul.debug.parameter.

Route type filter

  • SendForwardFilter
    This filter only processes requests with the forward.to(FilterConstants.FORWARD_TO_KEY) parameter in the request context. That is, the local forward of the forward in our routing rule is processed.

POST type filter

  • SendResponseFilter
    The filter is to encapsulate the response returned by the proxy request, and then send it back to the requester as the corresponding request.

ERROR type filter

  • SendErrorFilter
    The filter is to determine if there is any exception information in the current request context (RequestContext.getThrowable () is not empty), and if there is, it is forwarded to the /error page by default. We can also customize the error page by setting error.path.

2 @EnableZuulProxy’s default filter

@EnableZuulProxy adds the following filters to the above:

PRE type filter

  • PreDecorationFilter
    This filter determines the route to the route and how to route based on the provided RouteLocator. The router can also set various proxy-related headers for back-end requests.

ROUTE type filter

  • RibbonRoutingFilter
    This filter will process the request with serviceId (can be obtained through RequestContext.getCurrentContext().Get ("serviceId")) in the context, use Ribbon, Hystrix and pluggable HTTP client to send the request, and return the result of the request. That is to say that Ribbon and Hystrix only take effect when we use the serviceId to configure routing rules.

  • SimpleHostRoutingFilter
    When this filter detects that the routeHost parameter (available through RequestContext.getRouteHost()) is set, it will send a request to the specified URL through Apache HttpClient. At this point, the request is not wrapped with Hystrix commands, so this type of request does not have thread isolation and circuit breaker protection.