"Design mode (IV) - responsibility chain mode"
1, The greater the ability, the greater the responsibility
As the name suggests, "responsibility chain" is like a series of operations connected in series, and each operation is closely related. The approval process of leave, reimbursement process, etc. are completed according to the corresponding personnel of different positions; It can be said that it is promoted layer by layer. And our final concern is to agree or reject.
2, Chain of Responsibility
When the requesting Client sends a request, in order to reduce the coupling between the requester Client and the Handlers of the processing objects; At the same time, many Handlers can have the opportunity to participate in the processing of requests and form a chain of Handler objects. The request is passed along the chain until it is processed and the result is returned.
3, Component
Through the definition, it can be found that the responsibility chain mainly has several components:
- Client request class Client: initiating a request is equivalent to opening the responsibility chain, submitting tasks to the responsibility chain and waiting for processing results.
- Abstract interface Handler or abstract class of processing class: it contains the definition of processing request method, and usually contains subsequent chain points.
- The implementation class of the specific Handler is ConcreteHandler: the specific implementation of the Client's request may match according to its own ability. Then the call of the direct processing return result chain ends. Otherwise, the request will be forwarded to the subsequent chain for further processing until the request processing is completed.
- The structure diagram is as follows:
4, Simple code implementation
1. Design a file analysis system
The file parsing system matches different parsing objects according to the imported file format, and the Handler parses them. Through the opening of the chain, it is possible to match the appropriate class for the first time, then the call of the chain ends and returns the result; Of course, if it is an unsupported file type - can it be simply transformed into that the current object cannot process the request when programming, and there is no subsequent Handler? Generally speaking, that is, when the request reaches the end of the chain and there is still no appropriate Handler to process it, the end of the call will give the corresponding error message or other operations back to the requester Client.
- Abstract processing class Handler:
/** * Created by Sai * on: 12/01/2022 23:53. * Description: */ public abstract class FileParser { //Successor chain private FileParser next; public void setNext(FileParser next) { this.next = next; } //File extension information protected abstract String getExtension(); protected abstract void doParse(String fileName); public void read(String fileName) { if (fileName.endsWith(getExtension())) { this.doParse(fileName); return; } if (next != null) next.read(fileName); else System.out.println("the file type " + fileName + " is unsupported "); } }
Specific file parsing implementation classes, such as Excel, MarkDown and Word, form a responsibility chain.
- Excel file parsing class
/** * Created by Sai * on: 12/01/2022 00:01. * Description: */ public class ExcelParser extends FileParser { @Override protected String getExtension() { return ".xls"; } @Override protected void doParse(String fileName) { System.out.println("Parse the excel file...... "); System.out.println("-------------------------------------->"); } }
- MarkDown file parsing concrete implementation class
/** * Created by Sai * on: 12/01/2022 00:03. * Description: */ public class MarkDownParser extends FileParser { @Override protected String getExtension() { return ".md"; } @Override protected void doParse(String fileName) { System.out.println("Parse the markdown file......"); System.out.println("---------------------------------------->"); } }
- Word file parsing implementation class
/** * Created by Sai * on: 12/01/2022 00:10. * Description: */ public class WordParser extends FileParser { @Override protected String getExtension() { return ".doc"; } @Override protected void doParse(String fileName) { System.out.println("Parse the word file......"); System.out.println("----------------------------------->"); } }
Of course, if it is determined that the Client's request is clear, the system like this "file" parsing only needs to import the file, so the chain opening can be extracted separately.
- Implement parsing tool class
/** * Created by Sai * on: 12/01/2022 00:13. * Description: */ public class FileParserFactory { public static FileParser getDataReaderChain() { var excelParser = new ExcelParser(); var markDownParser = new MarkDownParser(); var wordParser = new WordParser(); wordParser.setNext(markDownParser); markDownParser.setNext(excelParser); return wordParser; } }
- Client test class
/** * Created by Sai * on: 12/01/2022 00:15. * Description: */ public class Demo { public static void show() { var reader = FileParserFactory.getDataReaderChain(); reader.read("file.xls"); reader.read("file.md"); reader.read("file.doc"); reader.read("file.jpg"); } public static void main(String[] args) { show(); } }
- Printing information is obviously not supported for JPG format system, so it also gives corresponding feedback to the Client
Parse the excel file...... success...... --------------------------------------> Parse the markdown file...... success...... ----------------------------------------> Parse the word file...... success...... -----------------------------------> the file type file.jpg is unsupported, failed Process finished with exit code 0
2. Simple summary
The Client acts as the initiator of the request, such as importing files and parsing the desired results. The coupling with the processing class is low. The Client side only cares about the file import and does not care who will handle it in the end. Even if the system needs to extend the new parsing class, it is very convenient. It only needs to inherit the Handler and implement the specific details separately, which is relatively easy to extend. But it will also be accompanied by the expansion of the system, which is related to the granularity of responsibilities. In addition, we can also find that if the responsibility chain is too long, the call stack is bound to be very deep. The performance of the system will also be discounted. Of course, this will be considered according to the specific business. When you should use it, think about how to use it. When you shouldn't use it, there's no need to design for design. After all, the essence of design patterns is only to reduce complexity, reduce coupling and improve scalability.
- The coupling between the Client request side and the Handler at the processing side is low.
- The allocation of responsibilities can be flexibly combined according to business requirements, and modifying the implementation details of a specific responsibility will not affect the stability of the overall system.
- Easy to expand.
Of course, there are shortcomings
- When the responsibility increases, the length of the chain increases and the depth of the call stack deepens, which will affect the efficiency of the system.
- If there are many specific implementation classes of responsibilities, it will increase the maintenance cost, and the complexity will increase when the Client side opens the chain.
5, Responsibility chain model in okHttp
The latest version of okHttp3 in the early Java version is 4.9.3. I don't know which version has been changed to Kotlin implementation. The implementation details have been adjusted, but the main process change of interceptor responsibility chain is very small.
- OkHttpClient is equivalent to the configuration control center of our network, including some basic configurations, such as connection pool, Retry, bridging, protocol, etc. the main configuration: dispatcher sets the maximum number of requests and the maximum number of requests of a single Host. Protocols: HTTP/1.1, HTTP/2. ConnectionSpec specifies whether the setting information of the Socket is HTTP transmitted in plaintext or HTTPS of TLS.
Interceptor, the core chain, contains several important interceptors. Of course, it also includes some other basic parameters.
1.newCall
Start with practical examples:
private static void connectNet() { OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder() .url("https://www.baidu.com") .build(); //Asynchronous request client.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { System.out.println("Failed----->" + e.getMessage()); } @Override public void onResponse(Call call, Response response) throws IOException { System.out.println("Success----->" + response.toString()); } }); }
The newCall(request) method returns a RealCall object, which implements the Call interface. When calling RealCall When execute():
RealCall.getResponseWithInterceptorChain() will be called to initiate a network request and get the returned Response value Response. The same asynchronous request realcall Enqueue () is also called in much the same way. The main difference lies in the intervention of Dispatcher. The request is added to the background through the call of thread pool. In fact, it is also a call to getresponsewithinterceptorchain (). In addition, the difference is that the maintenance of the request queue is different (Dispatcher).
getResponseWithInterceptorChain(), the core method of counting, is also the core method of OkHttp responsibility chain mode. The main work is to assemble multiple interceptors (lists) and create a RealInterceptorChain object, and chain Process (request) recommends the implementation of the chain step by step.
2.Dispatcher
private int maxRequests = 64; private int maxRequestsPerHost = 5; private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */ private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); /** Running synchronous calls. Includes canceled calls that haven't finished yet. */ private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
- The maximum number of asynchronous requests is defined as 64, and the maximum number of requests for a single Host is 5. At the same time, for asynchronous requests, two double ended queues are implemented to save requests runningAsyncCalls and readyAsyncCalls; This is well understood. When our request has reached the maximum value of 64 (or the Host is 5), if a new request comes, of course, we should save the request first. For the early processing logic, when a request comes, first judge whether it reaches the request threshold. This logic has been modified in the new version to decide which queue to put the request in. Similarly, for synchronization requests, runningSyncCalls is directly queued.
//Early asynchronous request queuing operations synchronized void enqueue(AsyncCall call) { if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { runningAsyncCalls.add(call); getExecutorService().execute(call); } else { readyAsyncCalls.add(call); } } //In later versions, you can directly enter readyAsyncCalls first. Of course, the main logic is the same void enqueue(AsyncCall call) { synchronized (this) { readyAsyncCalls.add(call); if (!call.get().forWebSocket) { AsyncCall existingCall = findExistingCallWithHost(call.host()); if (existingCall != null) call.reuseCallsPerHostFrom(existingCall); } } //In this method, the process determines whether the threshold is reached and whether to join runningAsyncCalls. promoteAndExecute(); } private boolean promoteAndExecute() { assert (!Thread.holdsLock(this)); List<AsyncCall> executableCalls = new ArrayList<>(); boolean isRunning; synchronized (this) { for (Iterator<AsyncCall> i = readyAsyncCalls.iterator(); i.hasNext(); ) { AsyncCall asyncCall = i.next(); //Max capacity. if (runningAsyncCalls.size() >= maxRequests) break; //Host max capacity. if (asyncCall.callsPerHost().get() >= maxRequestsPerHost) continue; i.remove(); asyncCall.callsPerHost().incrementAndGet(); executableCalls.add(asyncCall); runningAsyncCalls.add(asyncCall); } isRunning = runningCallsCount() > 0; } for (int i = 0, size = executableCalls.size(); i < size; i++) { AsyncCall asyncCall = executableCalls.get(i); asyncCall.executeOn(executorService()); } return isRunning; }
- Call the synchronization method, getResponseWithInterceptorChain(), and directly request to return the Response.
@Override public Response execute() throws IOException { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } transmitter.timeoutEnter(); transmitter.callStart(); try { client.dispatcher().executed(this); return getResponseWithInterceptorChain(); } finally { client.dispatcher().finished(this); } }
3. Asynchronous request key class - AsyncCall
- When we initiate an asynchronous request:
client.newCall(request).enqueue(); @Override public void enqueue(Callback responseCallback) { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } transmitter.callStart(); //AsyncCall inherits the abstract class namedrunnable (implements the Runnable interface), which is actually the implementation of threads. For the specific logic in the run() method, the abstract method execute() is added to see the specific implementation. client.dispatcher().enqueue(new AsyncCall(responseCallback)); } //AsyncCall#execute() @Override protected void execute() { boolean signalledCallback = false; transmitter.timeoutEnter(); try { //Both asynchronous and synchronous requests are essentially calls to getResponseWithInterceptorChain(), but asynchronous requests add thread management and scheduling. Response response = getResponseWithInterceptorChain(); signalledCallback = true; responseCallback.onResponse(RealCall.this, response); } catch (IOException e) { if (signalledCallback) { //Do not signal the callback twice! Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e); } else { responseCallback.onFailure(RealCall.this, e); } } catch (Throwable t) { cancel(); if (!signalledCallback) { IOException canceledException = new IOException("canceled due to " + t); canceledException.addSuppressed(t); responseCallback.onFailure(RealCall.this, canceledException); } throw t; } finally { client.dispatcher().finished(this); } }
4. Key method - getResponseWithInterceptorChain()
- RealCall#getResponseWithInterceptorChain()
Response getResponseWithInterceptorChain() throws IOException { //Build a full stack of interceptors. //Interceptor set List<Interceptor> interceptors = new ArrayList<>(); //The user can customize the interceptor, and the user-defined interceptor is saved here interceptors.addAll(client.interceptors()); //Retry interceptor interceptors.add(new RetryAndFollowUpInterceptor(client)); //Bridge interceptor, including gzip compression, host information setting, etc interceptors.add(new BridgeInterceptor(client.cookieJar())); //Cache request interceptors.add(new CacheInterceptor(client.internalCache())); //This interceptor has a small amount of code, mainly establishing TCP links or TCP-TLS links with the server interceptors.add(new ConnectInterceptor(client)); if (!forWebSocket) { interceptors.addAll(client.networkInterceptors()); } //The tail of the responsibility chain is essentially a request and I/O operation. It writes the request data into the Socket and reads the response data from the Socket (the port corresponding to TCP/TCP-TLS). interceptors.add(new CallServerInterceptor(forWebSocket)); Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); boolean calledNoMoreExchanges = false; try { Response response = chain.proceed(originalRequest); if (transmitter.isCanceled()) { closeQuietly(response); throw new IOException("Canceled"); } return response; } catch (IOException e) { calledNoMoreExchanges = true; throw transmitter.noMoreExchanges(e); } finally { if (!calledNoMoreExchanges) { transmitter.noMoreExchanges(null); } } }
- Our Response is packaged by the core method getResponseWithInterceptorChain(), and finally gets the desired result. Here is the core of OkHttp responsibility chain mode, which is cleverly designed. See what you have done.
5.RetryAndFollowUpInterceptor
- Interceptors all implement a unified interface Interceptor. See the key methods:
//The default maximum number of retries is defined as 20 private static final int MAX_FOLLOW_UPS = 20; @Override public Response intercept(Chain chain) throws IOException { Request request = chain.request(); RealInterceptorChain realChain = (RealInterceptorChain) chain; Transmitter transmitter = realChain.transmitter(); int followUpCount = 0; Response priorResponse = null; while (true) { transmitter.prepareToConnect(request); if (transmitter.isCanceled()) { throw new IOException("Canceled"); } Response response; boolean success = false; try { //The demarcation point, including other interceptors, is the pre order work before the responsibility chain is transferred, and then the request will continue to be issued response = realChain.proceed(request, transmitter, null); //The subsequent work of interceptors will follow. It should be noted that not all interceptors, such as cacheInterceptor, will be completed every time. When there is a cache (enable cache), the subsequent interceptions will not continue to be delivered (in different cases, there will be a comparison process). success = true; } catch (RouteException e) { //The attempt to connect via a route failed. The request will not have been sent. if (!recover(e.getLastConnectException(), transmitter, false, request)) { throw e.getFirstConnectException(); } continue; } catch (IOException e) { // An attempt to communicate with a server failed. The request may have been sent. boolean requestSendStarted = !(e instanceof ConnectionShutdownException); if (!recover(e, transmitter, requestSendStarted, request)) throw e; continue; } finally { // The network call threw an exception. Release any resources. if (!success) { transmitter.exchangeDoneDueToException(); } } // Attach the prior response if it exists. Such responses never have a body. if (priorResponse != null) { response = response.newBuilder() .priorResponse(priorResponse.newBuilder() .body(null) .build()) .build(); } Exchange exchange = Internal.instance.exchange(response); Route route = exchange != null ? exchange.connection().route() : null; Request followUp = followUpRequest(response, route); if (followUp == null) { if (exchange != null && exchange.isDuplex()) { transmitter.timeoutEarlyExit(); } return response; } RequestBody followUpBody = followUp.body(); if (followUpBody != null && followUpBody.isOneShot()) { return response; } closeQuietly(response.body()); if (transmitter.hasExchange()) { exchange.detachWithViolence(); } if (++followUpCount > MAX_FOLLOW_UPS) { throw new ProtocolException("Too many follow-up requests: " + followUpCount); } request = followUp; priorResponse = response; } }
- The key method of RealInterceptorChain is processed. It has been known that RealCall is the starting point of the whole chain:
//RealCall, you can see that the index is 0, and all our intercepts are saved in the list set. You can find that the interceptor s in the subsequent order are obtained based on the index self increment. //This is also the subtlety Interceptor.Chain chain = new RealInterceptorChain(interceptors, transmitter, null, 0, originalRequest, this, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); //RealInterceptorChain#proceed() public Response proceed(Request request, Transmitter transmitter, @Nullable Exchange exchange) throws IOException { if (index >= interceptors.size()) throw new AssertionError(); calls++; //If we already have a stream, confirm that the incoming request will use it. if (this.exchange != null && !this.exchange.connection().supportsUrl(request.url())) { throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must retain the same host and port"); } // If we already have a stream, confirm that this is the only call to chain.proceed(). if (this.exchange != null && calls > 1) { throw new IllegalStateException("network interceptor " + interceptors.get(index - 1) + " must call proceed() exactly once"); } //Call the next interceptor in the chain. //The index is auto incremented here, because each time the RealInterceptorChain is instantiated, the initial interceptor set is passed in. The index is operated every time the process is called, so //Our request is passed down step by step to the end of the chain. RealInterceptorChain next = new RealInterceptorChain(interceptors, transmitter, exchange, index + 1, request, call, connectTimeout, readTimeout, writeTimeout); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); // Confirm that the next interceptor made its required call to chain.proceed(). if (exchange != null && index + 1 < interceptors.size() && next.calls != 1) { throw new IllegalStateException("network interceptor " + interceptor + " must call proceed() exactly once"); } // Confirm that the intercepted response isn't null. if (response == null) { throw new NullPointerException("interceptor " + interceptor + " returned null"); } if (response.body() == null) { throw new IllegalStateException( "interceptor " + interceptor + " returned a response with no body"); } return response; }
6.ConnectInterceptor
- It is mainly to establish a TCP link or TCP-TLS link with the server. This is relatively special. The preamble operation of interception mentioned earlier is based on calling the method realchain Before proceeding (), but there is no subsequent operation:
/** Opens a connection to the target server and proceeds to the next interceptor. */ //The amount of code is small and links are established public final class ConnectInterceptor implements Interceptor { public final OkHttpClient client; public ConnectInterceptor(OkHttpClient client) { this.client = client; } @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); Transmitter transmitter = realChain.transmitter(); //We need the network to satisfy this request. Possibly for validating a conditional GET. boolean doExtensiveHealthChecks = !request.method().equals("GET"); Exchange exchange = transmitter.newExchange(chain, doExtensiveHealthChecks); //Distribution and transmission return realChain.proceed(request, transmitter, exchange); } }
7.CallServerInterceptor
/** This is the last interceptor in the chain. It makes a network call to the server. */ public final class CallServerInterceptor implements Interceptor { private final boolean forWebSocket; public CallServerInterceptor(boolean forWebSocket) { this.forWebSocket = forWebSocket; } @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Exchange exchange = realChain.exchange(); Request request = realChain.request(); long sentRequestMillis = System.currentTimeMillis(); exchange.writeRequestHeaders(request); boolean responseHeadersStarted = false; Response.Builder responseBuilder = null; if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) { // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100 // Continue" response before transmitting the request body. If we don't get that, return // what we did get (such as a 4xx response) without ever transmitting the request body. if ("100-continue".equalsIgnoreCase(request.header("Expect"))) { exchange.flushRequest(); responseHeadersStarted = true; exchange.responseHeadersStart(); responseBuilder = exchange.readResponseHeaders(true); } if (responseBuilder == null) { if (request.body().isDuplex()) { // Prepare a duplex body so that the application can send a request body later. exchange.flushRequest(); BufferedSink bufferedRequestBody = Okio.buffer( exchange.createRequestBody(request, true)); request.body().writeTo(bufferedRequestBody); } else { // Write the request body if the "Expect: 100-continue" expectation was met. BufferedSink bufferedRequestBody = Okio.buffer( exchange.createRequestBody(request, false)); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); } } else { exchange.noRequestBody(); if (!exchange.connection().isMultiplexed()) { // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection // from being reused. Otherwise we're still obligated to transmit the request body to // leave the connection in a consistent state. exchange.noNewExchangesOnConnection(); } } } else { exchange.noRequestBody(); } if (request.body() == null || !request.body().isDuplex()) { exchange.finishRequest(); } if (!responseHeadersStarted) { exchange.responseHeadersStart(); } if (responseBuilder == null) { responseBuilder = exchange.readResponseHeaders(false); } Response response = responseBuilder .request(request) .handshake(exchange.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); int code = response.code(); if (code == 100) { // server sent a 100-continue even though we did not request one. // try again to read the actual response response = exchange.readResponseHeaders(false) .request(request) .handshake(exchange.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); code = response.code(); } exchange.responseHeadersEnd(response); if (forWebSocket && code == 101) { //Connection is upgrading, but we need to ensure interceptors see a non-null response body. response = response.newBuilder() .body(Util.EMPTY_RESPONSE) .build(); } else { response = response.newBuilder() .body(exchange.openResponseBody(response)) .build(); } if ("close".equalsIgnoreCase(response.request().header("Connection")) || "close".equalsIgnoreCase(response.header("Connection"))) { exchange.noNewExchangesOnConnection(); } if ((code == 204 || code == 205) && response.body().contentLength() > 0) { throw new ProtocolException( "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength()); } return response; } }
- At the end of the responsibility chain, the actual request and I/O operations write the request data into the Socket and read the response data from the Socket (the port corresponding to TCP/TCP-TLS). The operation of I/O is based on Okio. OkHttp's efficient request is also inseparable from Okio's support. Sorting out the whole flow chart is:
6, What problems can be solved?
- In summary, it can be found that the same request can be processed by a series of objects, but the specific object processing is dynamically determined (runtime), so the objects can be combined into a chain to consider whether the responsibility chain can be used to simplify and make the structure clear.
- When the receiver is not clear, but multiple objects can handle the same request, in order to reduce the coupling between the Client and the Handler, consider using the responsibility chain.