SpringBoot自定义实现触发器模型的starter
文章目录
- 前言
- 正文
- 一、项目环境
- 二、核心类的类图关系
- 2.1 触发器核心接口&类
- 2.2 触发器工厂&注册&管理使用
- 三、项目代码
- 3.1 pom.xml
- 3.2 org.springframework.boot.autoconfigure.AutoConfiguration.imports
- 3.3 ITriggerActionEnum
- 3.4 Trigger
- 3.5 TriggerContext
- 3.6 AbstractTriggerTemplate
- 3.7 TriggerException
- 3.8 TriggerFactory
- 3.9 ApplicationBeanProvider
- 3.10 TriggerApplicationRunner
- 3.11 TriggerAutoConfiguration
- 3.12 LockTemplateFactory
- 3.13 TriggerActionManage
- 四、测试调用
- 4.1 引入坐标依赖&配置redis
- 4.2 新增触发器操作枚举
- 4.3 新增触发器
- 4.4 调用触发器
- 五、总结
前言
前不久,写了个管理系统的后端,其中涉及到一个“触发器模型”的部分,可以对业务进行解耦,复用。具体内容见下边这个链接:
https://blog.csdn.net/FBB360JAVA/article/details/143182736
本文的重点是,对它的一些优化,和更加规范化的调整。
正文
一、项目环境
- Java 23
- 整体编码使用 UTF-8
- SpringBoot 3.3.1
- redission 3.30.0
- lock4j-redission 整合包 2.2.7
- lombok 1.18.32
项目结构如下:
二、核心类的类图关系
2.1 触发器核心接口&类
2.2 触发器工厂&注册&管理使用
三、项目代码
3.1 pom.xml
<?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>
<groupId>com.pine.trigger</groupId>
<artifactId>pine-trigger-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>pine-pine-starter</name>
<description>pine-pine-starter</description>
<properties>
<java.version>23</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>3.3.1</spring-boot.version>
<redission.version>3.30.0</redission.version>
<lock4j-redisson.version>2.2.7</lock4j-redisson.version>
<lombok.version>1.18.32</lombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redission.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>lock4j-redisson-spring-boot-starter</artifactId>
<version>${lock4j-redisson.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>23</source>
<target>23</target>
<encoding>UTF-8</encoding>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
</project>
3.2 org.springframework.boot.autoconfigure.AutoConfiguration.imports
这个地方是用于自动装配,也就是自定义starter。
com.pine.trigger.config.TriggerAutoConfiguration
3.3 ITriggerActionEnum
package com.pine.trigger.constant;
/**
* 触发器操作接口-其实现类应该设计为枚举
*
* @author pine
* @version 1.0
* @since 2025-01-22 11:39
*/
public interface ITriggerActionEnum {
/**
* 获取触发器操作代码
*
* @return 操作代码
*/
String getCode();
/**
* 获取触发器操作描述
*
* @return 操作描述
*/
String getDesc();
}
3.4 Trigger
package com.pine.trigger.core;
import com.pine.trigger.constant.ITriggerActionEnum;
/**
* 触发器-顶级接口
*
* @author pine
* @version 1.0
* @since 2025-01-22 13:59
*/
public interface Trigger<T, R> {
/**
* 触发
*
* @param context 上下文
* @return 返回结果
*/
R trigger(TriggerContext<T> context);
ITriggerActionEnum getAction();
default void init(TriggerContext<T> context) {
}
default R after(TriggerContext<T> context, R result) {
return result;
}
}
3.5 TriggerContext
这里定义为一个记录类(record)。你自己实现的时候,也可以换成一个简单的实体类。
package com.pine.trigger.core;
import com.pine.trigger.constant.ITriggerActionEnum;
/**
* 触发器上下文
*
* @param action 触发器action
* @param <T> 触发器数据
*
* @author pine
* @version 1.0
* @since 2025-01-22 14:02
*/
public record TriggerContext<T>(T data, ITriggerActionEnum action) {
}
3.6 AbstractTriggerTemplate
采用模版方法设计模式实现业务流程的组装,分为初始化(init)、执行(trigger),执行后(after)。另外这里处理的时候,选择先从spring容器中获取对应的触发器对象,然后调用的方式,也是为了后续更方便在触发器方法上增加切面、事务等操作。
package com.pine.trigger.core;
import com.pine.trigger.config.ApplicationBeanProvider;
import com.pine.trigger.constant.ITriggerActionEnum;
/**
* 抽象触发器模板
*
* @author pine
* @version 1.0
* @since 2025-01-22 14:07
*/
public abstract class AbstractTriggerTemplate<T, R> implements Trigger<T, R> {
private final ITriggerActionEnum action;
public AbstractTriggerTemplate(ITriggerActionEnum action) {
this.action = action;
}
@SuppressWarnings("unchecked")
public final R start(TriggerContext<T> context) {
// 从容器中获取触发器实例
AbstractTriggerTemplate<T, R> triggerTemplate = ApplicationBeanProvider.getBean(this.getClass());
// 初始化
triggerTemplate.init(context);
// 执行触发器主体
R result = triggerTemplate.trigger(context);
// 触发后置操作
triggerTemplate.after(context, result);
return result;
}
@Override
public ITriggerActionEnum getAction() {
return action;
}
}
3.7 TriggerException
自定义异常类。
package com.pine.trigger.core;
import java.util.Objects;
/**
* 触发器异常
*
* @author 01434188
* @version 1.0
* @since 2025-01-22 14:56
*/
public class TriggerException extends RuntimeException {
public TriggerException(String message) {
super(message);
}
public static TriggerException error(String message) {
return new TriggerException(message);
}
public static void requiredNonNull(Object param, String message) {
if (Objects.isNull(param)) {
throw new TriggerException(message);
}
}
}
3.8 TriggerFactory
这个类是触发器工厂,设计为一个单例。提供的工厂方法,也使用了非静态的方式定义。主要提供注册触发器、获取触发器的方法。
package com.pine.trigger.core;
import com.pine.trigger.constant.ITriggerActionEnum;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
/**
* 触发器工厂(单例工厂)
*
* @author pine
* @version 1.0
* @since 2025-01-22 14:21
*/
@Slf4j
public class TriggerFactory {
/**
* 饿汉式单例:触发器工厂实例
*/
@SuppressWarnings("all")
private static final TriggerFactory TRIGGER_FACTORY = new TriggerFactory();
/**
* 触发器池
*/
private static final Map<ITriggerActionEnum, AbstractTriggerTemplate<?, ?>> TRIGGER_POOL_MAP = new HashMap<>(16);
/**
* 注册触发器
*
* @param trigger 触发器
*/
@SuppressWarnings("all")
public void register(AbstractTriggerTemplate trigger) {
TriggerException.requiredNonNull(trigger.getAction(), "触发器action不能为空!");
// 尝试注册触发器
if (TRIGGER_POOL_MAP.putIfAbsent(trigger.getAction(), trigger) != null) {
log.warn("触发器[" + trigger.getAction() + "]已经存在,请勿重复注册!");
}
}
/**
* 获取触发器
*
* @param action 触发器action
* @param <T> 触发器入参类型
* @param <R> 触发器返回类型
* @return 触发器
*/
@SuppressWarnings("all")
public AbstractTriggerTemplate getTrigger(ITriggerActionEnum action) {
TriggerException.requiredNonNull(action, "触发器action不能为空!");
AbstractTriggerTemplate<?, ?> triggerTemplate = TRIGGER_POOL_MAP.get(action);
TriggerException.requiredNonNull(triggerTemplate,"触发器[" + action + "]不存在,请先注册!");
return triggerTemplate;
}
/**
* 获取触发器工厂实例
*
* @return 触发器工厂实例
*/
public static TriggerFactory getInstance() {
return TRIGGER_FACTORY;
}
private TriggerFactory() {
}
}
3.9 ApplicationBeanProvider
相当于一个spring bean 的工具类,主要是实现了ApplicationContextAware 接口,并提供获取bean的方法。
package com.pine.trigger.config;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.lang.NonNull;
import java.util.ArrayList;
import java.util.List;
/**
* 应用bean提供类
*
* @author pine
* @version 1.0
* @since 2025-01-22 14:12
*/
@Configuration
public class ApplicationBeanProvider implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
ApplicationBeanProvider.applicationContext = applicationContext;
}
public static <T> T getBean(Class<T> clazz) {
return applicationContext.getBean(clazz);
}
public static <T> List<T> getBeans(Class<T> clazz) {
return new ArrayList<>(applicationContext.getBeansOfType(clazz).values());
}
}
3.10 TriggerApplicationRunner
用于程序启动后,注册全部的触发器。
package com.pine.trigger.config;
import com.pine.trigger.core.AbstractTriggerTemplate;
import com.pine.trigger.core.TriggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* 触发器的应用启动后置操作执行器
*
* @author pine
* @version 1.0
* @since 2025-01-22 14:35
*/
@Configuration
public class TriggerApplicationRunner implements ApplicationRunner {
@SuppressWarnings("all")
@Override
public void run(ApplicationArguments args) throws Exception {
// 从容器中获取所有的触发器,并注册到触发器工厂中
List<AbstractTriggerTemplate> triggers = ApplicationBeanProvider.getBeans(AbstractTriggerTemplate.class);
triggers.forEach(trigger -> TriggerFactory.getInstance().register(trigger));
}
}
3.11 TriggerAutoConfiguration
用于自动装配这个自定义的starter,并指定需要扫描的包路径。
package com.pine.trigger.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
/**
* 触发器自动配置类
*
* @author pine
* @version 1.0
* @since 2025-01-22 11:37
*/
@ComponentScan(basePackages = "com.pine.trigger")
@Configuration
public class TriggerAutoConfiguration {
}
3.12 LockTemplateFactory
在整合 Lock4j 和 redission 之后,获取 LockTemplate 实例。
package com.pine.trigger.manage;
import com.baomidou.lock.LockTemplate;
import com.pine.trigger.config.ApplicationBeanProvider;
import com.pine.trigger.core.TriggerException;
import java.util.Objects;
/**
* LockTemplate工厂
*
* @author pine
* @version 1.0
* @since 2025-01-22 15:17
*/
public class LockTemplateFactory {
private static LockTemplate lockTemplate;
public static LockTemplate lockTemplate() {
if (Objects.nonNull(LockTemplateFactory.lockTemplate)) {
return LockTemplateFactory.lockTemplate;
}
LockTemplateFactory.lockTemplate = ApplicationBeanProvider.getBean(LockTemplate.class);
TriggerException.requiredNonNull(lockTemplate, "lockTemplate 不能为空");
return LockTemplateFactory.lockTemplate;
}
}
3.13 TriggerActionManage
触发器管理类,最终在使用触发的时候,也是要通过调用它的方法。
这里提供了幂等和非幂等两种实现方式。
package com.pine.trigger.manage;
import com.baomidou.lock.LockInfo;
import com.baomidou.lock.LockTemplate;
import com.pine.trigger.core.AbstractTriggerTemplate;
import com.pine.trigger.core.TriggerContext;
import com.pine.trigger.core.TriggerException;
import com.pine.trigger.core.TriggerFactory;
import lombok.SneakyThrows;
import java.util.Objects;
/**
* 触发器操作管理类
*
* @author pine
* @version 1.0
* @since 2025-01-22 14:49
*/
public class TriggerActionManage {
/**
* 触发器操作处理(无幂等)
*
* @param context 上下文信息
* @param <T> 触发入参类型
* @param <R> 触发响应类型
* @return 处理响应结果
*/
@SuppressWarnings("all")
public static <T, R> R handle(TriggerContext<T> context) {
// 从工厂中获取触发器
AbstractTriggerTemplate trigger = TriggerFactory.getInstance().getTrigger(context.action());
// 启动触发器执行
return (R) trigger.start(context);
}
/**
* 触发器操作处理(幂等,采取redis加锁3s来限制)<br>
* <b>要求:需要对于入参进行 toString的重写</b>
*
* @param context 上下文信息
* @param <T> 触发入参类型
* @param <R> 触发响应类型
* @return 处理响应结果
*/
@SuppressWarnings("all")
public static <T, R> R handleWithIdempotent(TriggerContext<T> context) {
String key = context.toString();
// 尝试对key加锁,失效时间为3s
LockTemplate lockTemplate = LockTemplateFactory.lockTemplate();
LockInfo locked = lockTemplate.lock(key, 3000L, 0L);
// 如果加锁成功
if (Objects.nonNull(locked)) {
return handle(context);
}
throw new TriggerException("触发器执行失败");
}
}
四、测试调用
4.1 引入坐标依赖&配置redis
另外新增一个普通的springboot 项目,在其中引入上边实现的starter。
比如,在 新项目的pom.xml中增加:
<dependency>
<groupId>com.pine.trigger</groupId>
<artifactId>pine-trigger-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
在你的配置中增加redis的配置信息,比如在 application.yaml中配置:
spring:
data:
redis:
database: 1
host: 127.0.0.1
port: 6379
password: 2131313da
4.2 新增触发器操作枚举
package com.example.bootdemo17.trigger;
import com.pine.trigger.constant.ITriggerActionEnum;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 触发器action
*
* @author pine
* @version 1.0
* @since 2025-01-22 15:58
*/
@Getter
@AllArgsConstructor
public enum MyTriggerActionEnum implements ITriggerActionEnum {
DEMO("demo", "demo");
private final String code;
private final String desc;
}
4.3 新增触发器
注意:这里需要重写无参数构造器,并注入一个枚举类型,一个枚举对应一个触发器。
package com.example.bootdemo17.trigger;
import com.pine.trigger.core.AbstractTriggerTemplate;
import com.pine.trigger.core.TriggerContext;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* dd
*
* @author pine
* @version 1.0
* @since 2025-01-22 16:00
*/
@Component
public class MyTrigger extends AbstractTriggerTemplate<List<Long>, String> {
public MyTrigger() {
super(MyTriggerActionEnum.DEMO);
}
/**
* 触发
*
* @param context 上下文
* @return 返回结果
*/
@Override
public String trigger(TriggerContext<List<Long>> context) {
return "d3weqedasd";
}
}
4.4 调用触发器
package com.example.bootdemo17;
import com.example.bootdemo17.trigger.MyTriggerActionEnum;
import com.pine.trigger.core.TriggerContext;
import com.pine.trigger.manage.TriggerActionManage;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* dd
*
* @author pine
* @version 1.0
* @since 2025-01-22 16:09
*/
@RestController
@RequestMapping("/test")
public class DemoController {
@GetMapping("/demo")
public String demo() {
// 组装上下文
TriggerContext<List<Long>> context = new TriggerContext<>(List.of(1L, 2L, 3L, 4L),MyTriggerActionEnum.DEMO);
// 调用触发器
String result = TriggerActionManage.handle(context);
return result;
}
}
五、总结
触发器模型与spring框架中的事件触发机制的作用类似,但是更加强大。
触发器模型可以有返回值。并用于组装你自己的业务链。
更适合实现复杂的业务场景。
不太适用在查询数据的场景,更多的是一些操作,比如修改,新增等。