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
The basic steps are as follows:
- 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
- Then put different handlers together through mark. See 3.7 Core Loading Class for details.
- 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
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.