How Zuul Works Under the Hood: Routing, Handlers, and Filters Explained
This article dissects the inner workings of Netflix’s Zuul gateway, contrasting its servlet‑based blocking model with Spring Cloud Gateway’s reactive approach, and walks through route collection, handler registration, request processing, and filter loading, complete with key Java code examples.
Zuul is now rarely used, as most projects have migrated to Spring Cloud Gateway. Zuul relies on the servlet blocking I/O model, creating many threads to compensate, while Cloud Gateway uses a reactive non‑blocking model that handles more work with fewer threads.
1. Collecting Routes
ZuulServerAutoConfiguration defines beans that create and combine route locators. The main classes are SimpleRouteLocator and CompositeRouteLocator , which convert ZuulRoute definitions into Route objects.
<code>public class ZuulServerAutoConfiguration {
@Autowired
protected ZuulProperties zuulProperties;
@Autowired
protected ServerProperties server;
@Bean
@Primary
public CompositeRouteLocator primaryRouteLocator(Collection<RouteLocator> routeLocators) {
return new CompositeRouteLocator(routeLocators);
}
@Bean
@ConditionalOnMissingBean(SimpleRouteLocator.class)
public SimpleRouteLocator simpleRouteLocator() {
return new SimpleRouteLocator(this.server.getServlet().getContextPath(), this.zuulProperties);
}
}
</code>The SimpleRouteLocator reads the configuration, builds a map of ZuulRoute objects, and creates Route instances with proper path handling, prefix stripping, and retry settings.
<code>public class SimpleRouteLocator implements RouteLocator, Ordered {
private ZuulProperties properties;
private String dispatcherServletPath = "/";
private String zuulServletPath;
private AtomicReference<Map<String, ZuulRoute>> routes = new AtomicReference<>();
public SimpleRouteLocator(String servletPath, ZuulProperties properties) {
this.properties = properties;
if (StringUtils.hasText(servletPath)) {
this.dispatcherServletPath = servletPath;
}
this.zuulServletPath = properties.getServletPath();
}
@Override
public List<Route> getRoutes() {
List<Route> values = new ArrayList<>();
for (Entry<String, ZuulRoute> entry : getRoutesMap().entrySet()) {
ZuulRoute route = entry.getValue();
String path = route.getPath();
try {
values.add(getRoute(route, path));
} catch (Exception ignored) {}
}
return values;
}
// additional helper methods omitted for brevity
}
</code>CompositeRouteLocator aggregates all RouteLocator beans (e.g., the SimpleRouteLocator ) and merges their routes.
<code>public class CompositeRouteLocator implements RefreshableRouteLocator {
private final Collection<? extends RouteLocator> routeLocators;
private List<RouteLocator> rl;
public CompositeRouteLocator(Collection<? extends RouteLocator> routeLocators) {
rl = new ArrayList<>(routeLocators);
AnnotationAwareOrderComparator.sort(rl);
this.routeLocators = rl;
}
@Override
public List<Route> getRoutes() {
List<Route> route = new ArrayList<>();
for (RouteLocator locator : routeLocators) {
route.addAll(locator.getRoutes());
}
return route;
}
}
</code>2. Registering HandlerMapping
Zuul registers a custom ZuulHandlerMapping that maps every route to a single ZuulController . The mapping is created during the first request.
<code>public class ZuulServerAutoConfiguration {
@Bean
public ZuulController zuulController() {
return new ZuulController();
}
@Bean
public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes, ZuulController zuulController) {
ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController);
mapping.setErrorController(this.errorController);
mapping.setCorsConfigurations(getCorsConfigurations());
return mapping;
}
}
</code>The ZuulHandlerMapping extends AbstractUrlHandlerMapping and registers each route's full path to the shared ZuulController .
<code>public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {
private volatile boolean dirty = true;
private final ZuulController zuul;
private final RouteLocator routeLocator;
public ZuulHandlerMapping(RouteLocator routeLocator, ZuulController zuul) {
this.routeLocator = routeLocator;
this.zuul = zuul;
setOrder(-200);
}
@Override
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
if (this.dirty) {
synchronized (this) {
if (this.dirty) {
registerHandlers();
this.dirty = false;
}
}
}
return super.lookupHandler(urlPath, request);
}
private void registerHandlers() {
Collection<Route> routes = this.routeLocator.getRoutes();
for (Route route : routes) {
registerHandler(route.getFullPath(), this.zuul);
}
}
}
</code>3. Routing the Request
When a request arrives, ZuulHandlerMapping finds the matching ZuulController . The request is then processed by SimpleControllerHandlerAdapter , which delegates to the controller’s handleRequest method.
<code>public class SimpleControllerHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return (handler instanceof Controller);
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return ((Controller) handler).handleRequest(request, response);
}
}
</code>The ZuulController extends ServletWrappingController and wraps the ZuulServlet , which contains the actual filter chain execution.
<code>public class ZuulController extends ServletWrappingController {
public ZuulController() {
setServletClass(ZuulServlet.class);
setServletName("zuul");
setSupportedMethods((String[]) null); // allow all
}
@Override
public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
return super.handleRequestInternal(request, response);
} finally {
RequestContext.getCurrentContext().unset();
}
}
}
</code>The ZuulServlet runs the Zuul filter chain: pre‑filters, routing, post‑filters, and error handling.
<code>public class ZuulServlet extends HttpServlet {
private ZuulRunner zuulRunner;
@Override
public void init(ServletConfig config) throws ServletException {
super.init(config);
boolean bufferReqs = "true".equals(config.getInitParameter("buffer-requests"));
zuulRunner = new ZuulRunner(bufferReqs);
}
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
try {
init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
RequestContext ctx = RequestContext.getCurrentContext();
ctx.setZuulEngineRan();
try { preRoute(); } catch (ZuulException e) { error(e); postRoute(); return; }
try { route(); } catch (ZuulException e) { error(e); postRoute(); return; }
try { postRoute(); } catch (ZuulException e) { error(e); return; }
} catch (Throwable e) {
error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
} finally {
RequestContext.getCurrentContext().unset();
}
}
private void init(HttpServletRequest request, HttpServletResponse response) {
zuulRunner.init(request, response);
}
}
</code>4. Loading Filters
Filters are collected by FilterLoader . During application startup, ZuulFilterInitializer registers all beans of type ZuulFilter into the FilterRegistry .
<code>public class ZuulFilterInitializer {
private final Map<String, ZuulFilter> filters;
private final FilterLoader filterLoader;
private final FilterRegistry filterRegistry;
@PostConstruct
public void contextInitialized() {
for (Map.Entry<String, ZuulFilter> entry : this.filters.entrySet()) {
filterRegistry.put(entry.getKey(), entry.getValue());
}
}
}
</code>FilterLoader provides runFilters(String type) to execute all filters of a given phase (pre, route, post, error) in priority order.
<code>public class FilterLoader {
public Object runFilters(String sType) throws Throwable {
boolean result = false;
List<ZuulFilter> list = getFiltersByType(sType);
if (list != null) {
for (ZuulFilter filter : list) {
Object r = processZuulFilter(filter);
if (r instanceof Boolean) {
result |= (Boolean) r;
}
}
}
return result;
}
public List<ZuulFilter> getFiltersByType(String filterType) {
List<ZuulFilter> list = hashFiltersByType.get(filterType);
if (list != null) return list;
list = new ArrayList<>();
for (ZuulFilter filter : filterRegistry.getAllFilters()) {
if (filter.filterType().equals(filterType)) {
list.add(filter);
}
}
Collections.sort(list);
hashFiltersByType.putIfAbsent(filterType, list);
return list;
}
}
</code>The combination of route collection, handler mapping, controller delegation, servlet execution, and filter loading constitutes the complete internal workflow of the Zuul gateway.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.