Mastering the Chain of Responsibility: From Basics to Real-World Frameworks
This article explains the Chain of Responsibility pattern, compares self‑driven and handler‑driven implementations with Java code, and demonstrates how major frameworks such as Spring Interceptor, Servlet Filter, Dubbo, and Sentinel realize the pattern, followed by practical usage scenarios.
1. Introduction to the Chain of Responsibility
The GoF Design Patterns book defines the Chain of Responsibility as decoupling request senders from receivers, allowing multiple receiver objects a chance to handle a request until one succeeds or all have tried. In plain terms, a request passes through processor A, then B, then C, forming a chain where each processor handles its own responsibility.
The pattern reduces coupling and improves extensibility: individual processors can be upgraded independently, and the chain order can be rearranged or processors added/removed.
2. Two Implementation Styles
From the perspective of how the chain is driven, we can classify implementations into handler‑driven and self‑driven (processor‑driven) approaches.
2.1 Self‑Driven Processor
public abstract class AbstractHandler {
protected Handler next = null;
public void setSuccessor(Handler next) { this.next = next; }
public abstract void handle();
}
public class HandlerA extends AbstractHandler {
@Override
public void handle() {
// do something
if (next != null) { next.handle(); }
}
}
public class HandlerB extends AbstractHandler {
@Override
public void handle() {
// do something
if (next != null) { next.handle(); }
}
}
public class HandlerChain {
private AbstractHandler head = null;
private AbstractHandler tail = null;
public void addHandler(AbstractHandler handler) {
handler.setSuccessor(null);
if (head == null) { head = handler; tail = handler; return; }
tail.setSuccessor(handler);
tail = handler;
}
public void handle() { if (head != null) { head.handle(); } }
}
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}Key points:
Each processor holds a reference to the next processor.
The chain is driven by each processor invoking next.handle() after its own work.
2.2 Handler‑Driven Chain
public interface IHandler { void doSomething(); }
public class HandlerA implements IHandler {
@Override public void doSomething() { /* do something */ }
}
public class HandlerB implements IHandler {
@Override public void doSomething() { /* do something */ }
}
public class HandlerChain {
private List<IHandler> handlers = new ArrayList<>();
public void addHandler(IHandler handler) { handlers.add(handler); }
public void handle() { for (IHandler h : handlers) { h.doSomething(); } }
}
public class Application {
public static void main(String[] args) {
HandlerChain chain = new HandlerChain();
chain.addHandler(new HandlerA());
chain.addHandler(new HandlerB());
chain.handle();
}
}Key points:
The chain stores processors in a collection (array or list).
The chain object iterates over the collection and calls each processor; processors themselves do not drive the next step.
3. Chain of Responsibility in Popular Open‑Source Frameworks
3.1 Spring Interceptor
Spring Interceptor intercepts controller method execution, allowing custom logic before and after the method, similar to AOP. Typical use cases include authentication, logging, character‑encoding conversion, and content filtering.
public interface HandlerInterceptor {
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception;
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mv) throws Exception;
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception;
}
@Component
public class TimeInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("time interceptor preHandle");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView mv) throws Exception {
System.out.println("time interceptor postHandle");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("time interceptor afterCompletion");
}
}Spring builds a HandlerExecutionChain that stores all HandlerInterceptor instances in a list. During request dispatch, DispatcherServlet calls applyPreHandle (forward order) and applyPostHandle (reverse order), thus forming a chain of responsibility.
3.2 Servlet Filter
Servlet Filter, defined in the Servlet 2.3 specification, forms a classic chain that can inspect and modify request/response objects before and after servlet execution.
public interface Filter {
void init(FilterConfig filterConfig) throws ServletException;
void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;
void destroy();
}
public class TimeFilter implements Filter {
@Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("time filter init"); }
@Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
System.out.println("time filter doFilter");
chain.doFilter(req, resp); // invoke next filter
}
@Override public void destroy() {}
}The ApplicationFilterChain maintains an array of Filter objects. Its internalDoFilter method retrieves the current filter, invokes it, and then calls filter.doFilter(..., this) to trigger the next filter, achieving a self‑driven chain.
3.3 Dubbo Filter
Dubbo’s Filter is an extension point for both provider and consumer sides. Each filter implements org.apache.dubbo.rpc.Filter and its invoke method, which must call invoker.invoke(inv) to pass control to the next filter.
@Activate(group = PROVIDER, value = ACCESS_LOG_KEY)
public class AccessLogFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation inv) throws RpcException {
try {
if (ConfigUtils.isNotEmpty(accessLogKey)) {
AccessLogData logData = buildAccessLogData(invoker, inv);
log(accessLogKey, logData);
}
} catch (Throwable t) {}
return invoker.invoke(inv); // trigger next filter
}
}Dubbo builds the filter chain via ProtocolFilterWrapper.buildInvokerChain, which wraps each filter into an Invoker that forwards the call to the next invoker, forming a handler‑driven chain.
3.4 Sentinel Slot Chain
Sentinel provides a slot‑chain mechanism for traffic governance. Each ProcessorSlot implements entry / exit methods and holds a reference to the next slot via a next field.
public interface ProcessorSlot<T> {
void entry(Context ctx, ResourceWrapper res, T param, int count, boolean prioritized, Object... args) throws Throwable;
void fireEntry(Context ctx, ResourceWrapper res, Object obj, int count, boolean prioritized, Object... args) throws Throwable;
void exit(Context ctx, ResourceWrapper res, int count, Object... args);
void fireExit(Context ctx, ResourceWrapper res, int count, Object... args);
}
public abstract class AbstractLinkedProcessorSlot<T> implements ProcessorSlot<T> {
private AbstractLinkedProcessorSlot<?> next = null;
@Override public void fireEntry(Context ctx, ResourceWrapper res, Object obj, int count, boolean prioritized, Object... args) throws Throwable {
if (next != null) { next.transformEntry(ctx, res, obj, count, prioritized, args); }
}
void transformEntry(Context ctx, ResourceWrapper res, Object o, int count, boolean prioritized, Object... args) throws Throwable {
T t = (T)o; entry(ctx, res, t, count, prioritized, args);
}
public void setNext(AbstractLinkedProcessorSlot<?> next) { this.next = next; }
}
public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {
private volatile Map<String, DefaultNode> map = new HashMap<>(10);
@Override public void entry(Context ctx, ResourceWrapper res, Object obj, int count, boolean prioritized, Object... args) throws Throwable {
DefaultNode node = map.get(ctx.getName());
ctx.setCurNode(node);
fireEntry(ctx, res, node, count, prioritized, args);
}
}The DefaultSlotChainBuilder loads all ProcessorSlot implementations via SPI, sorts them, and adds them to a DefaultProcessorSlotChain using tail‑insertion. The chain’s entry method starts from a dummy head, then each slot triggers the next via fireEntry, achieving a self‑driven chain.
4. Practical Summary
In everyday projects, the Chain of Responsibility pattern appears in many scenarios. For user‑generated content platforms, a chain can consist of a text‑filter processor, an image‑filter processor, etc., each handling a specific moderation task. In e‑commerce order processing, a chain may include order splitting, discount calculation, and order creation processors.
The pattern’s low coupling and high extensibility make it a natural fit for the interceptor/filter mechanisms of Spring, Servlet, Dubbo, and Sentinel, and it can be adapted to any business workflow that requires sequential, pluggable processing steps.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
