Improve code readability and maintainability: Use the chain of responsibility model to optimize your Spring Boot code

1. Basic introduction

Chain of responsibility is a very common design pattern. I won’t introduce it in detail. This article explains how to use the chain of responsibility pattern elegantly in SpringBoot.

1.1. Code execution process

image-20230829201630365

The basic steps are as follows:

  1. When SpringBoot starts, you need to obtain the corresponding Bean of the handler. Different businesses correspond to different processors. For example, the ticket purchase business may need to check whether the parameters are empty, check whether the parameters are legal, check whether the tickets are purchased repeatedly, etc., so you need a mark is used to mark the current business so that the same handlers can be put together
  2. Then put different handlers together through mark. See 3.7 Core Loading Class for details.
  3. Then implement a method to execute the corresponding part of the code in batches by passing in mark and parameters.

2. Project creation

2.1. Project structure

img

2.2. maven

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.5.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>cn.knightzz</groupId>
  <artifactId>chain-responsibility-pattern-example</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>chain-responsibility-pattern-example</name>
  <description>Chain of responsibility model demo</description>
  <properties>
    <java.version>1.8</java.version>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
      </plugin>
    </plugins>
  </build>

</project>

3. Code writing

3.1. Entity class

This class is used to store entity classes

package cn.knightzz.pattern.dto.req;

/**
 * @author Wang Tianci
 * @title: PurchaseTicketReqDTO
 * @description:
 * @create: 2023-08-29 18:09
 */
public class PurchaseTicketReqDTO {<!-- -->

}

3.2. Enumeration class

package cn.knightzz.pattern.common.enums;

/**
 * @author Wang Tianci
 * @title: TicketChainMarkEnum
 * @description: Annotations for storing tag responsibility chains
 * @create: 2023-08-29 18:10
 */
public enum TicketChainMarkEnum {<!-- -->

    /**
     * Chain of responsibility filter for flagging ticket purchases
     */
    TRAIN_PURCHASE_TICKET_FILTER("train_purchase_ticket_filter");

    private String name;

    TicketChainMarkEnum(String name) {<!-- -->
        this.name = name;
    }
}

Enumeration classes are mainly used to mark the responsibility chain of a certain type of business.

3.3. General classes

package cn.knightzz.pattern.context;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

import java.lang.annotation.Annotation;
import java.util.Map;

/**
 * @author Wang Tianci
 * @title: ApplicationContextHolder
 * @description:
 * @create: 2023-08-29 18:31
 */
@Component
public class ApplicationContextHolder implements ApplicationContextAware {<!-- -->

    private static ApplicationContext CONTEXT;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {<!-- -->
        ApplicationContextHolder.CONTEXT = applicationContext;
    }

    /**
     * Get ioc container bean by type.
     *
     * @paramclazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(Class<T> clazz) {<!-- -->
        return CONTEXT.getBean(clazz);
    }

    /**
     * Get ioc container bean by name and type.
     *
     * @param name
     * @paramclazz
     * @param <T>
     * @return
     */
    public static <T> T getBean(String name, Class<T> clazz) {<!-- -->
        return CONTEXT.getBean(name, clazz);
    }

    /**
     * Get a set of ioc container beans by type.
     *
     * @paramclazz
     * @param <T>
     * @return
     */
    public static <T> Map<String, T> getBeansOfType(Class<T> clazz) {<!-- -->
        return CONTEXT.getBeansOfType(clazz);
    }

    /**
     * Find whether the bean has annotations.
     *
     * @param beanName
     * @param annotationType
     * @param <A>
     * @return
     */
    public static <A extends Annotation> A findAnnotationOnBean(String beanName, Class<A> annotationType) {<!-- -->
        return CONTEXT.findAnnotationOnBean(beanName, annotationType);
    }

    /**
     * Get ApplicationContext.
     *
     * @return
     */
    public static ApplicationContext getInstance() {<!-- -->
        return CONTEXT;
    }
}

The function of ApplicationContextHolder is to inject the container storing the Bean in Spring into CONTEXT when the Bean is created, so that we can use the Bean in other classes

3.4. Universal responsibility link interface

package cn.knightzz.pattern.chain;

import cn.knightzz.pattern.common.enums.TicketChainMarkEnum;
import org.springframework.core.Ordered;

/**
 * @author Wang Tianci
 * @title: AbstractChainHandler
 * @description:
 * @create: 2023-08-29 18:15
 */
public interface AbstractChainHandler<T> extends Ordered {<!-- -->

    /**
     * Execute chain of responsibility logic
     *
     * @param requestParam Responsibility chain execution input parameters
     */
    void handler(T requestParam);

    /**
     * @return responsibility chain component identifier
     */
    String mark();
}

3.5. Ticket purchase responsibility link interface

package cn.knightzz.pattern.filter;

import cn.knightzz.pattern.chain.AbstractChainHandler;
import cn.knightzz.pattern.common.enums.TicketChainMarkEnum;
import cn.knightzz.pattern.dto.req.PurchaseTicketReqDTO;

/**
 * @author Wang Tianci
 * @title: TrainPurchaseTicketChainFilter
 * @description:
 * @create: 2023-08-29 18:10
 */
public interface TrainPurchaseTicketChainFilter<T extends PurchaseTicketReqDTO> extends AbstractChainHandler<PurchaseTicketReqDTO> {<!-- -->

    @Override
    default String mark() {<!-- -->
        return TicketChainMarkEnum.TRAIN_PURCHASE_TICKET_FILTER.name();
    }
}

By implementing the responsibility chain interface, write the default mark method to mark the current responsibility chain processor collection

3.6. Ticket purchase responsibility chain processor

package cn.knightzz.pattern.filter.handler;

import cn.knightzz.pattern.dto.req.PurchaseTicketReqDTO;
import cn.knightzz.pattern.filter.TrainPurchaseTicketChainFilter;
import org.springframework.stereotype.Component;

/**
 * @author Wang Tianci
 * @title: TrainPurchaseTicketParamNotNullChainHandler
 * @description:
 * @create: 2023-08-29 18:18
 */
@Component
public class TrainPurchaseTicketParamNotNullChainHandler implements TrainPurchaseTicketChainFilter<PurchaseTicketReqDTO> {<!-- -->

    @Override
    public void handler(PurchaseTicketReqDTO requestParam) {<!-- -->
        System.out.println("The parameter cannot be empty, the filter was executed successfully");
    }

    @Override
    public int getOrder() {<!-- -->
        return 10;
    }
}
package cn.knightzz.pattern.filter.handler;

import cn.knightzz.pattern.dto.req.PurchaseTicketReqDTO;
import cn.knightzz.pattern.filter.TrainPurchaseTicketChainFilter;
import org.springframework.stereotype.Component;

/**
 * @author Wang Tianci
 * @title: TrainPurchaseTicketParamVerifyChainHandler
 * @description: Whether the verification parameters of the ticket purchase process filter are valid
 * @create: 2023-08-29 18:23
 */
@Component
public class TrainPurchaseTicketParamVerifyChainHandler implements TrainPurchaseTicketChainFilter<PurchaseTicketReqDTO> {<!-- -->
    @Override
    public void handler(PurchaseTicketReqDTO requestParam) {<!-- -->
        System.out.println("The parameters are legal and the filter is executed successfully");
    }

    @Override
    public int getOrder() {<!-- -->
        return 20;
    }
}
package cn.knightzz.pattern.filter.handler;

import cn.knightzz.pattern.dto.req.PurchaseTicketReqDTO;
import cn.knightzz.pattern.filter.TrainPurchaseTicketChainFilter;
import org.springframework.stereotype.Component;

/**
 * @author Wang Tianci
 * @title: TrainPurchaseTicketRepeatChainHandler
 * @description: Ticket purchase process filter to verify whether passengers make repeated purchases
 * @create: 2023-08-29 18:24
 */
@Component
public class TrainPurchaseTicketRepeatChainHandler implements TrainPurchaseTicketChainFilter<PurchaseTicketReqDTO> {<!-- -->
    @Override
    public void handler(PurchaseTicketReqDTO requestParam) {<!-- -->
        System.out.println("No duplicate tickets were purchased, the filter was executed successfully");
    }

    @Override
    public int getOrder() {<!-- -->
        return 30;
    }
}

3.7. Core loading class

package cn.knightzz.pattern.context;

import cn.knightzz.pattern.chain.AbstractChainHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author Wang Tianci
 * @title: AbstractChainContext
 * @description: CommandLineRunner: callback function executed after SpringBoot startup is completed
 * @create: 2023-08-29 18:27
 */
@Component
@Slf4j
public final class AbstractChainContext<T> implements CommandLineRunner {<!-- -->

    // CommandLineRunner: callback function executed after SpringBoot startup is completed


    // Container that stores the implementation of the chain of responsibility component and the business identification of the chain of responsibility
    // For example: Key: ticket purchase verification filter Val: HanlderA, HanlderB, HanlderC,...
    private final Map<String, List<AbstractChainHandler>> abstractChainHandlerContainer = new HashMap<>();


    public void handler(String mark, T requestParam) {<!-- -->
        List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(mark);
        if (CollectionUtils.isEmpty(abstractChainHandlers)) {<!-- -->
            throw new RuntimeException(String.format("[%s] Chain of Responsibility ID is undefined.", mark));
        }
        abstractChainHandlers.forEach(each -> each.handler(requestParam));
    }

    @Override
    public void run(String... args) throws Exception {<!-- -->
        // Get all beans through ApplicationContextHolder
        Map<String, AbstractChainHandler> chainFilterMap =
                ApplicationContextHolder.getBeansOfType(AbstractChainHandler.class);

        chainFilterMap.forEach((beanName, bean) -> {<!-- -->

            // Get the responsibility chain collection of the specified type, create it if it does not exist
            // Need to put the same mark together
            List<AbstractChainHandler> abstractChainHandlers = abstractChainHandlerContainer.get(bean.mark());
            if (CollectionUtils.isEmpty(abstractChainHandlers)) {<!-- -->
                abstractChainHandlers = new ArrayList<>();
            }
            //Add to processor collection
            abstractChainHandlers.add(bean);
            // Sort the processor collection order
            List<AbstractChainHandler> actualAbstractChainHandlers = abstractChainHandlers
                    .stream()
                    .sorted(Comparator.comparing(Ordered::getOrder))
                    .collect(Collectors.toList());

            log.info("mark {}, bean : {} add container", bean.mark(), bean);

            //Save the sorted beans into the container and wait to be called at runtime
            abstractChainHandlerContainer.put(bean.mark(), actualAbstractChainHandlers);
        });

    }
}

This class mainly needs to implement the CommandLineRunner interface. This interface provides a run method, which will be executed after SpringBoot starts.

handler method

3.8. Basic usage

package cn.knightzz.pattern.service;

import cn.knightzz.pattern.common.enums.TicketChainMarkEnum;
import cn.knightzz.pattern.context.AbstractChainContext;
import cn.knightzz.pattern.dto.req.PurchaseTicketReqDTO;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

/**
 * @author Wang Tianci
 * @title: TicketService
 * @description:
 * @create: 2023-08-29 19:04
 */
@Service
@RequiredArgsConstructor
public class TicketService {<!-- -->

    private final AbstractChainContext<PurchaseTicketReqDTO> purchaseTicketAbstractChainContext;

    public void purchase(PurchaseTicketReqDTO requestParam) {<!-- -->
        purchaseTicketAbstractChainContext.handler(TicketChainMarkEnum.TRAIN_PURCHASE_TICKET_FILTER.name(), requestParam);
    }
}

As shown in the above code, you can directly call the handler method provided by AbstractChainContext when using it.