Designing Interceptors with Chain of Responsibility Pattern

I implemented the interceptor function in Redant ( https://github.com/all4you/redant ) by inheriting ChannelHandler, and the pipeline is an application of the responsibility chain mode. But then I redesigned the original interceptor. Why did I do this, because the original method was implemented on the basis of ChannelHandler, and we know that Netty's data processing is based on ByteBuf, which involves reference counting The problem of release, the previous ChannelHandler can not care about the reference count when processing, and hand it over to the last ChannelHandler to release.

However, a major feature of the interceptor is that when a certain condition is not satisfied, it needs to interrupt the subsequent operation and return directly, so this causes a node in the pipeline to release the reference count. Another aspect is that the original design uses a lot of automatic Some of the defined ChannelHandlers only do some simple work, so they can be combined to make the code more compact and compact.

Combining multiple ChannelHandlers is relatively simple, and redesigning interceptors is relatively complicated.

Redesign the interceptor

First, I unified the original pre-interceptor and post-interceptor into one interceptor, and then abstracted two methods, respectively: pre-processing, post-processing, as shown in the following figure:

interceptor.png

The default preprocessing method returns true, which users can override according to their business.

An abstract class is defined here, and an interface can also be used. Since Java 8, the interface can have default method implementation.

After the interceptor is defined, the method call of the interceptor can now be added to the ChannelHandler, as shown in the following figure:

interceptor-handle.png

When the pre-method returns false, it returns directly, interrupts the subsequent business logic processing, and finally writes the result to the response in finally and returns it to the front-end.

Now you only need to implement the two methods in InterceptorHandler. In fact, this is also very simple, as long as you get all the implementation classes of Interceptor, and then call the pre-method and post-method of these implementation classes in turn, as shown in the figure below. Show:

interceptor-call.png

get interceptor

The focus now is how to get all the interceptors. The first thing you can think of is to scan a specific path to find the implementation classes of all Interceptors, and then add these implementation classes to a List.

How to ensure the execution order of interceptors? It's very simple, just sort them before adding them to the List. Define an @Order annotation to indicate the sorting order, then wrap the Interceptor and Order with a Wrapper wrapper class, sort them into the List of the wrapper class, and finally take out all the Interceptors from the List of the wrapper class to complete the Interceptor. sorted.

After knowing the general principle, it is very simple to implement, as shown in the following figure:

get-interceptor.png

But we can't get all interceptors by calling the scanInterceptors() method every time. If we scan it every time, the performance will be affected, so we only need to call this method for the first time, and then save the result in a In the private variable, you can directly read the value of the variable when you get it, as shown in the following figure:

interceptor-provider.png

Custom interceptor implementation class

Let's customize the two interceptor implementation classes to verify the specific effect.

The first interceptor judges the request parameters in the pre-method. If there is a parameter with block=true in the request parameter, it will be intercepted, as shown in the following figure:

block-interceptor.png

The second interceptor prints the time-consuming of each request in the post method, as shown in the following figure:

performance-interceptor.png

The order of execution is specified by the @Order annotation, and the BlockInterceptor is executed first and then the PerformanceInterceptor.

View the effect

Now we request the /user/info interface to see the effect.

First, we only submit the normal parameters, as shown in the following figure:

common-request.png

The printed result is shown in the following figure:

common-request-effect.png

From the printed results, you can see that it is executed in sequence:

  • The preHandle method of BlockInterceptor
  • PerformanceInterceptor 的 for Handle 方法
  • PostHandle method of BlockInterceptor
  • PostHandle method of PerformanceInterceptor

This shows that the interceptors are sorted according to the @Order annotation, and then executed in sequence.

Then we submit a block=true parameter and request the interface again, as shown in the following figure:

block-request.png

You can see that the request has been intercepted by the front-end method of the interceptor, and then look at the printed log, as shown in the following figure:

block-request-effect.png

Only some logs in the preHandler method of BlockInterceptor are printed, and the following methods are not executed because they are intercepted and returned directly.

existing problems

So far, the interceptor has been transformed, and the effect has been verified, and the effect seems to be ok. But is there any problem?

There is really a problem: as long as all Interceptor implementation classes are scanned, they will be added to the List. If you don't want to apply a certain interceptor, you can't do it, because you can't dynamically change the values ​​in the list.

If we can construct a list constructor that dynamically obtains the Interceptor, the list will be obtained first from the constructor, and if the user does not define the constructor, we will scan to get all the Interceptors. This will be perfect.

The method of dynamically obtaining the Interceptor's list can be implemented by the user. According to certain rules, it is determined whether to add an Interceptor to the list, so that the implementation and use of the Interceptor are decoupled. Users can implement as many Interceptors as they want, but only use some of them according to the rules.

After clarifying the principle, it is easy to implement. First, define an interface to construct the Interceptor List, as shown in the following figure:

interceptor-builder.png

With the InterceptorBuilder, when obtaining the Interceptor, you can first obtain it according to the InterceptorBuilder, as shown in the following figure:

interceptor-provider-with-builder.png

The following is an example of InterceptorBuilder, which can be extended by the user, as shown in the following figure:

custom-interceptor-builder.png

In this way, users can assemble all interceptors according to their own intentions as long as they implement an InterceptorBuilder interface.

chain of responsibility

The chain of responsibility used by the interceptor implemented in Redant actually saves all the Interceptors through a List. In addition to using a List to implement the chain of responsibility we usually call it, it can also be implemented through a real linked list structure. Both Netty and Sentinel have such implementations. Let me implement a simple chain of responsibility chain structure.

There are already many applications of Chain of Responsibility, so I won't go into details here. Suppose we need to do the following operations on requests submitted by the front end: authentication, login, and logging. It is very appropriate to do these processing through Chain of Responsibility.

First define a processing interface, as shown in the following figure:

processor.png

The implementation through List is very simple. You only need to add the implementation class of each Processor to a List. When processing, traverse the List and process them in turn. No specific description will be given here. Those who are interested can implement it by themselves.

define node

If it is implemented in the form of a linked list, first we need a class to represent a node in the linked list, and the node needs to have a private variable of the same type to represent the next node of the node, so that a linked list can be implemented. ,As shown below:

abstract-linked-processor.png

define container

Then we need to define a container, in which there are two nodes: head and tail. The head node is used as an empty node. The real node will be added to the next node of the head node. The tail node is used as a pointer to point to the currently added node. , the next time a new node is added, it will be added from the tail node. With the specific processing logic, it is very simple to implement. The implementation of this container is shown in the following figure:

linked-processor-chain.png

Define the implementation class

Next, we can implement a specific Processor to process business logic, as long as we inherit AbstractLinkedProcessor, as shown in the following figure:

auth-processor.png

The other two implementation classes: LoginProcessor and LogProcessor are similar, so I won't post them here.

Then we can assemble the required Processors according to the rules. Suppose our rules are to perform requests in sequence: authentication, login, and logging. The assembled code is shown in the following figure:

linked-processor-chain-test.png

Execute the code, the result is shown in the following figure:

linked-processor-chain-test-result.png

existing problems

Careful students may find that in the implementation of AuthProcessor's business logic, in addition to executing specific logic codes, a line of super.process(content) code is also called. The function of this line of code is to call the next line in the linked list. A node's process method. But what if one day we forget to call this line of code when we write our own Processor implementation class?

The result is that the nodes behind the current node will not be called, and the entire linked list will seem to be broken. How to avoid this problem? In fact, we have implemented the process method in AbstractProcessor, which calls the process method of the next node. Then, before this method triggers the call to the next node, we abstract a method doProcess for specific business logic processing, execute the doProcess method first, and then trigger the process of the next node after the execution, so that the linked list will not be broken. The specific implementation is shown in the following figure:

fixed-abstract-linked-processor.png

The corresponding LinkedProcessorChain and specific implementation classes should also be adjusted accordingly, as shown in the following figure:

fixed-linked-processor-chain.png

fixed-auth-processor.png

Re-execute the test class just now and find that the result is the same as before, so far a simple chain of responsibility chain is completed.

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324136125&siteId=291194637