preface

There is a business scenario in recent work where the chain of responsibility model is a good fit and can be used in several places. In order to write a more general and complete responsibility chain, I read some implementation of responsibility chain in the Spring framework as a reference.

The application of the responsibility chain pattern in Spring

Responsibility chains are widely used and are widely used in Spring, so here’s how two very common features are implemented.

In the Spring WebHandlerInterceptor

The HandlerInterceptor interface is very common in Web development. It has three methods: preHandle(), postHandle(), and afterCompletion(). Execute after calling the “Controller” method and before rendering “ModelAndView” and after rendering “ModelAndView”.

public interface HandlerInterceptor {
	default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {

		return true;
	}

	default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable ModelAndView modelAndView) throws Exception {}default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
			@Nullable Exception ex) throws Exception {}}Copy the code

The HandlerInterceptor acts as a handler in the responsibility chain, making the responsibility chain calls through the HandlerExecutionChain.

public class HandlerExecutionChain {...@Nullable
	private HandlerInterceptor[] interceptors;

	private int interceptorIndex = -1;

	boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
		HandlerInterceptor[] interceptors = getInterceptors();
		if(! ObjectUtils.isEmpty(interceptors)) {for (int i = 0; i < interceptors.length; i++) {
				HandlerInterceptor interceptor = interceptors[i];
				if(! interceptor.preHandle(request, response,this.handler)) {
					triggerAfterCompletion(request, response, null);
					return false;
				}
				this.interceptorIndex = i; }}return true;
	}

	void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
			throws Exception {

		HandlerInterceptor[] interceptors = getInterceptors();
		if(! ObjectUtils.isEmpty(interceptors)) {for (int i = interceptors.length - 1; i >= 0; i--) {
				HandlerInterceptor interceptor = interceptors[i];
				interceptor.postHandle(request, response, this.handler, mv); }}}void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex)
			throws Exception {

		HandlerInterceptor[] interceptors = getInterceptors();
		if(! ObjectUtils.isEmpty(interceptors)) {for (int i = this.interceptorIndex; i >= 0; i--) {
				HandlerInterceptor interceptor = interceptors[i];
				try {
					interceptor.afterCompletion(request, response, this.handler, ex);
				}
				catch (Throwable ex2) {
					logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
				}
			}
		}
	}
}
Copy the code

The method of invocation is very simple: the HandlerInterceptor is registered in Spring with an array store, and the handler is called in order by the interceptorIndex as a pointer to traverse the responsibility chain array.

Spring AOP

Spring AOP implementation is more flexible, can realize the front (@before), After (@after), Around (@around) and other cutting opportunities. There are currently two implementations of Spring AOP dynamic proxy: JdkDynamicAopProxy and CglibAopProxy. Here is the implementation process of CglibAopProxy to see the use of the chain of responsibility.

The JdkDynamicAopProxy implements the InvocationHandler interface and implements the Invoke () method. Those familiar with JDK dynamic proxies know that invoking a method from a proxy object leads to the invoke() method of the InvocationHandler object, so we’ll start with this method directly from JdkDynamicAopProxy:

final class JdkDynamicAopProxy implements AopProxy.InvocationHandler.Serializable {
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {...try {
			// 1. Check and skip some proxy-free methods.// 2. Get all interceptors of the target method
			List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

			
			if (chain.isEmpty()) {
				// If there is no interceptor, the target method is directly reflected
				Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
				retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
			}
			else {
				// 3 Generate the responsibility chain of the dynamic proxy
				MethodInvocation invocation =
						new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
				// 4 Execute the chain of responsibility
				retVal = invocation.proceed();
			}

			// 5. The assembly process returns a value.returnretVal; }... }}Copy the code

The overall process of this method is quite long, and I only show the part related to the responsibility chain. Step 2 By getting all Advice related to the target proxy method, Advice can be said to encapsulate the interceptor in Spring, that is, the AOP method we wrote. Then if there are interceptors through step 3 to generate a ReflectiveMethodInvocation, and execute the chain of responsibility.

Look at the proceed ReflectiveMethodInvocation what () method is:

public class ReflectiveMethodInvocation implements ProxyMethodInvocation.Cloneable {

	protected ReflectiveMethodInvocation(
			Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments,
			@NullableClass<? > targetClass, List<Object> interceptorsAndDynamicMethodMatchers) {

		this.proxy = proxy;
		this.target = target;
		this.targetClass = targetClass;
		this.method = BridgeMethodResolver.findBridgedMethod(method);
		this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments);
		this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers;
	}

	public Object proceed(a) throws Throwable {
		
		if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
			/ / 1. If the current Index and the same number of interceptors interceptorsAndDynamicMethodMatchers, responsibility chain end of the call, call the method that destination agent directly reflect
			return invokeJoinpoint();
		}

		// 2. Get the current interceptor
		Object interceptorOrInterceptionAdvice =
				this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
		if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
			/ / InterceptorAndDynamicMethodMatcher types of interceptors, check whether MethodMatcher matchInterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; Class<? > targetClass = (this.targetClass ! =null ? this.targetClass : this.method.getDeclaringClass());
			// 3. Check whether the target proxy method and the current interceptor matches
			if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
				// 4. Execute the current interceptor
				return dm.interceptor.invoke(this);
			}
			else {
				// 5. If there is no match, the next interceptor is recursively called
				returnproceed(); }}else {
			// Execute current interceptor
			return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); }}}Copy the code
  1. First of all,ReflectiveMethodInvocationSave in the constructorJdkDynamicAopProxyA list of incoming interceptors (chain) tointerceptorsAndDynamicMethodMatchers.
  2. Then, inproceed()In the method, the present is determined firstcurrentInterceptorIndexIf the pointer reaches the interceptor list size, the interceptor has been executed, and the target proxy method is called.
  3. Otherwise gets the current interceptor, usually of typeInterceptorAndDynamicMethodMatcher.InterceptorAndDynamicMethodMatcherThere are only two properties in there,MethodMatcherandMethodInterceptorThe former is used to determine whether the interceptor needs to match the target proxy method, and the latter is the interceptor itself.
  4. If the currentInterceptorAndDynamicMethodMatcherMatches the target proxy method, the current interceptor is called, otherwise the current interceptor is called directly againproceed()Form recursion.

ReflectiveMethodInvocation is a “chain” of the chain of responsibility, and processor is InterceptorAndDynamicMethodMatcher, More accurately, in InterceptorAndDynamicMethodMatcher MethodInterceptor implementation class. The MethodInterceptor here is wrapped as Advice in Spring, for example, the section Around (@around) is wrapped as AspectJAroundAdvice.

Instead of looking at the detailed code for AspectJAroundAdvice, I’ll write a basic AOP aspect

@Slf4j
@Aspect
@Component
public class LogAop {
    @Pointcut("@annotation(org.springframework.web.bind.annotation.GetMapping)")
    public void logPointCut(a) {}@Around("logPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        log.info("LogAop start");
        Object result = point.proceed();
        log.info("LogAop end");
        returnresult; }}Copy the code

Matching rules can be encapsulated in the @pointcut to InterceptorAndDynamicMethodMatcher MethodMatcher as matching rules, and @ Around will be encapsulated into the MethodInterceptor annotation methods, In this case, AspectJAroundAdvice. Arguments such as ProceedingJoinPoint are wrapped in AspectJAroundAdvice and then reflected as input arguments to invoke the corresponding AOP method.

public abstract class AbstractAspectJAdvice implements Advice.AspectJPrecedenceInformation.Serializable {
	protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
		Args AOP method entry parameter, usually ProceedingJoinPoint
		Object[] actualArgs = args;
		if (this.aspectJAdviceMethod.getParameterCount() == 0) {
			actualArgs = null;
		}
		try {
			ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
			/ / 2. This. AspectInstanceFactory. GetAspectInstance () to obtain AOP aspects of instances
			return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
		}
		catch (IllegalArgumentException ex) {
			throw new AopInvocationException("Mismatch on arguments to advice method [" +
					this.aspectJAdviceMethod + "]; pointcut expression [" +
					this.pointcut.getPointcutExpression() + "]", ex);
		}
		catch (InvocationTargetException ex) {
			throwex.getTargetException(); }}}Copy the code

The entry parameter ProceedingJoinPoint contains some information about the target proxy method and, more importantly, attempts to invoke the target proxy method by calling the object’s PROCEED () method. That is, we can write code that we expect to execute around the target proxy method before and after the AOP aspect calls the proceed() method. In LogAop, LogAop start is printed before execution of the target proxy method and LogAop end is printed after execution.

ProceedingJoinPoint. Proceed () how to do, here to see next Spring’s implementation class MethodInvocationProceedingJoinPoint code.

public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint.JoinPoint.StaticPart {
	@Override
	public Object proceed(a) throws Throwable {
		return this.methodInvocation.invocableClone().proceed(); }}Copy the code

Very simple code to clone the methodInvocation and call its proceed() method. Here is the methodInvocation ReflectiveMethodInvocation mentioned at the beginning, so the full circle, actually formed ReflectiveMethodInvocation. Proceed () recursively.

Two implementation summaries

The HandlerInterceptor in Spring Web uses a sequential array traversal mode to control the push of the responsibility chain. This mode allows the handler to control the chain manually without interfering with each other. But a simultaneous handler cannot break the chain of responsibility until all handlers have been processed. And the call timing of the handler is also determined by the chain, which is relatively less flexible.

Spring AOP takes a recursive approach, with handlers pushing the chain with calls shown in their own functions, giving them the flexibility to decide when to push and even break the chain of responsibilities. This is a little out of control for other handlers, and sometimes the chain of responsibility is broken by other handlers and they are not called, which makes debugging more difficult.

The chains of both responsibility chains use Index variable to control chain push, which means that a chain cannot be “shared”. Each time the responsibility chain is used, it needs to be regenerated and destroyed again, which consumes processor resources.

Design a common chain of responsibility

After learning about the implementation of other chains of responsibility, I now implement a general chain of responsibility myself.

Start with a basic processor interface:

/** * Each business declares a subinterface or abstract class of that interface, and implements the corresponding business Handler based on that interface. So BaseHandlerChain can be injected directly into the corresponding Handler List */
public interface BaseHandler<Param.Result> {

    @NonNull
    HandleResult<Result> doHandle(Param param);

    default boolean isHandler(Param param) {
        return true; }}Copy the code

The processor interface has two methods, isHandler(), which determines whether the processor needs to execute. The default is true. The doHandle() method executes the handler logic and returns a HandleResult that returns the result of the processing and determines whether to proceed to the next handler.

@Getter
public class HandleResult<R> {
    private final R data;

    private final boolean next;

    private HandleResult(R r, boolean next) {
        this.data = r;
        this.next = next;
    }

    public static <R> HandleResult<R> doNextResult(a) {
        return new HandleResult<>(null.true);
    }

    public static <R> HandleResult<R> doCurrentResult(R r) {
        return new HandleResult<>(r, false); }}Copy the code

Finally, write the code of responsibility chain controller:

/** * general responsibility chain mode * <p> * Create a responsibility chain controller for the corresponding business, inherits BaseHandlerChain, * for example: {@codeMyHandlerChain extends BaseHandlerChain<MyHandler, MyParam, MyResult>} * <p> * 2. Create a responsible chain Handler for the corresponding business, inheriting BaseHandler, * such as: {@codeMyHandler extends BaseHandler<MyParam, MyResult>} * <p> * 3. Write the Handler that business needs to realize the doHandle method of MyHandler interface. It is recommended that you hand both the controller and the processor over to Spring for direct injection. * /
public class BaseHandlerChain<Handler extends BaseHandler<Param.Result>, Param.Result> {

    @Getter
    private final List<Handler> handlerList;


    public BaseHandlerChain(List<Handler> handlerList) {
        this.handlerList = handlerList;
    }

    public Result handleChain(Param param) {
        for (Handler handler : handlerList) {
            if(! handler.isHandler(param)) {continue;
            }
            HandleResult<Result> result = handler.doHandle(param);
            if (result.isNext()) {
                continue;
            }
            return result.getData();
        }

        return null; }}Copy the code

Now that the chain of responsibility is complete, let’s do a simple demonstration

/** * Business based basic processor interface */
public interface MyHandler extends BaseHandler<String.String> {}/** * Service-based controller interface */
@Service
public class MyHandlerChain extends BaseHandlerChain<MyHandler.String.String> {

    @Autowired
    public MyHandlerChain(List<MyHandler> myHandlers) {
        super(myHandlers); }}/** * processor 1 */
@Slf4j
@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class MyLogHandler implements MyHandler {

    @Override
    public @NonNull HandleResult<String> doHandle(String param) {
        log.info("MyLogHandler hello {} !", param);
        returnHandleResult.doNextResult(); }}/** * processor 2 */
@Slf4j
@Component
public class MyDefaultHandler implements MyHandler {

    @Override
    public @NonNull HandleResult<String> doHandle(String param) {
        log.info("param is {}", param);
        return HandleResult.doCurrentResult("MyDefaultHandler"); }}/** * unit test */
@Slf4j
@SpringBootTest
public class BaseHandlerChainTests {

    @Autowired
    private MyHandlerChain handlerChain;

    @Test
    public void handleChain(a) {
        String result = handlerChain.handleChain("zzzzbw");
        log.info("handleChain result: {}", result); }}Copy the code

The log output is as follows:

INFO 6716 --- [           main] c.z.s.demo.chain.handler.MyLogHandler    : MyLogHandler hello zzzzbw !
INFO 6716 --- [           main] c.z.s.d.chain.handler.MyDefaultHandler   : param is zzzzbw
INFO 6716 --- [           main] c.z.s.demo.BaseHandlerChainTests         : handleChain result: MyDefaultHandler
Copy the code

reference

There are three ways to realize the chain of responsibility model

Two kinds of realization of responsibility chain pattern

Comparison of three ways to realize chain of responsibility

Which of the two ways to implement the chain of responsibility do you pick more

Chain of Responsibility Pattern. Spring proxy creation and AOP chain call process – Cloud + community – Tencent Cloud (tencent.com)


Application of the Responsibility chain pattern in Spring