diff --git a/yudao-framework/pom.xml b/yudao-framework/pom.xml index 635dc7cf6..a4fb64fae 100644 --- a/yudao-framework/pom.xml +++ b/yudao-framework/pom.xml @@ -28,6 +28,7 @@ yudao-spring-boot-starter-biz-operatelog yudao-spring-boot-starter-biz-dict yudao-spring-boot-starter-biz-sms + yudao-spring-boot-starter-extension yudao-framework diff --git a/yudao-framework/yudao-spring-boot-starter-extension/pom.xml b/yudao-framework/yudao-spring-boot-starter-extension/pom.xml new file mode 100644 index 000000000..bc6a36e8d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/pom.xml @@ -0,0 +1,68 @@ + + + + yudao-framework + cn.iocoder.boot + ${revision} + + 4.0.0 + + yudao-spring-boot-starter-extension + jar + + ${artifactId} + 扩展点组件 + https://github.com/YunaiV/ruoyi-vue-pro + + + + + + + + cn.iocoder.boot + yudao-common + + + + + org.springframework + spring-context + provided + + + + org.springframework + spring-beans + provided + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + junit + junit + test + + + + + jakarta.validation + jakarta.validation-api + + + \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/config/YudaoExtensionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/config/YudaoExtensionAutoConfiguration.java new file mode 100644 index 000000000..8b047ae1b --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/config/YudaoExtensionAutoConfiguration.java @@ -0,0 +1,62 @@ +package cn.iocoder.yudao.framework.extension.config; + +import cn.iocoder.yudao.framework.extension.core.ExtensionBootstrap; +import cn.iocoder.yudao.framework.extension.core.context.ExtensionContext; +import cn.iocoder.yudao.framework.extension.core.context.ExtensionContextHolder; +import cn.iocoder.yudao.framework.extension.core.context.ExtensionExecutor; +import cn.iocoder.yudao.framework.extension.core.factory.ExtensionFactory; +import cn.iocoder.yudao.framework.extension.core.factory.ExtensionRegisterFactory; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @description 扩展点组件自动装配 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 21:50 + * @class cn.iocoder.yudao.framework.extension.config.YudaoExtensionAutoConfiguration.java + */ +@Configuration +public class YudaoExtensionAutoConfiguration { + + /** + * 组件初始化 + * @return + */ + @Bean(initMethod = "init") + @ConditionalOnMissingBean(ExtensionBootstrap.class) + public ExtensionBootstrap bootstrap() { + return new ExtensionBootstrap(); + } + + /** + * 扩展点工厂 + * @return + */ + @Bean + @ConditionalOnMissingBean({ExtensionRegisterFactory.class, ExtensionFactory.class}) + public ExtensionRegisterFactory registerFactory() { + return new ExtensionRegisterFactory(); + } + + /** + * 扩展组件上下文对象 + * @return + */ + @Bean + @ConditionalOnMissingBean({ExtensionContextHolder.class, ExtensionContext.class}) + public ExtensionContextHolder context() { + return new ExtensionContextHolder(); + } + + /** + * 扩展组件执行器 + * @return + */ + @Bean + @ConditionalOnMissingBean(ExtensionExecutor.class) + public ExtensionExecutor executor() { + return new ExtensionExecutor(); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/BusinessScenario.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/BusinessScenario.java new file mode 100644 index 000000000..732350c67 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/BusinessScenario.java @@ -0,0 +1,142 @@ +package cn.iocoder.yudao.framework.extension.core; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.StringJoiner; + +/** + * @description 业务场景 = businessId + useCase + scenario, 用来标识系统中唯一的一个场景
+ * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 22:19 + * @class cn.iocoder.yudao.framework.extension.core.BusinessScenario.java + */ +public class BusinessScenario implements Serializable { + + /** + * 默认业务id + */ + public final static String DEFAULT_BUSINESS_ID = "#defaultBusinessId#"; + + /** + * 默认用例 + */ + public final static String DEFAULT_USECASE = "#defaultUseCase#"; + + /** + * 默认场景 + */ + public final static String DEFAULT_SCENARIO = "#defaultScenario#"; + + /** + * 分隔符 + */ + private final static String DOT_SEPARATOR = "."; + + /** + * 业务Id + */ + private String businessId; + + /** + * 用例 + */ + private String useCase; + + /** + * 场景 + */ + private String scenario; + + public BusinessScenario() { + this.businessId = DEFAULT_BUSINESS_ID; + this.useCase = DEFAULT_USECASE; + this.scenario = DEFAULT_SCENARIO; + } + + public BusinessScenario(@NotNull String businessId, @NotNull String useCase, @NotNull String scenario) { + this.businessId = businessId; + this.useCase = useCase; + this.scenario = scenario; + } + + public BusinessScenario(@NotNull String scenario) { + this(); + this.scenario = scenario; + } + + public BusinessScenario(@NotNull String useCase, @NotNull String scenario) { + this(DEFAULT_BUSINESS_ID, useCase, scenario); + } + + public String getBusinessId() { + return businessId; + } + + public void setBusinessId(String businessId) { + this.businessId = businessId; + } + + public String getUseCase() { + return useCase; + } + + public void setUseCase(String useCase) { + this.useCase = useCase; + } + + public String getScenario() { + return scenario; + } + + public void setScenario(String scenario) { + this.scenario = scenario; + } + + /** + * 构建业务场景 + * @param businessId + * @param useCase + * @param scenario + * @return + */ + public static BusinessScenario valueOf(@NotNull String businessId, @NotNull String useCase, @NotNull String scenario) { + return new BusinessScenario(businessId, useCase, scenario); + } + + /** + * 构建业务场景 + * @param useCase + * @param scenario + * @return + */ + public static BusinessScenario valueOf(@NotNull String useCase, @NotNull String scenario) { + return new BusinessScenario(useCase, scenario); + } + + /** + * 构建业务场景 + * @param scenario + * @return + */ + public static BusinessScenario valueOf(@NotNull String scenario) { + return new BusinessScenario(scenario); + } + + /** + * 业务场景唯一标识 + * @return + */ + public String getUniqueIdentity(){ + return new StringJoiner(DOT_SEPARATOR).add(businessId).add(useCase).add(scenario).toString(); + } + + @Override + public String toString() { + return "BusinessScenario{" + + "businessId='" + businessId + '\'' + + ", useCase='" + useCase + '\'' + + ", scenario='" + scenario + '\'' + + '}'; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/ExtensionBootstrap.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/ExtensionBootstrap.java new file mode 100644 index 000000000..800c431be --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/ExtensionBootstrap.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.extension.core; + +import cn.iocoder.yudao.framework.extension.core.factory.ExtensionRegisterFactory; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +import javax.annotation.PostConstruct; + +/** + * @description + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-29 00:18 + * @class cn.iocoder.yudao.framework.extension.core.ExtensionBootstrap.java + */ +public class ExtensionBootstrap implements ApplicationContextAware { + + /** + * spring 容器 + */ + private ApplicationContext applicationContext; + + @Autowired + private ExtensionRegisterFactory registerFactory; + + /** + * 初始化 + */ + @PostConstruct + public void init() { + registerFactory.setApplicationContext(applicationContext); + registerFactory.register(null); + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/AbstractComponentExecutor.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/AbstractComponentExecutor.java new file mode 100644 index 000000000..f46d05d90 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/AbstractComponentExecutor.java @@ -0,0 +1,131 @@ +package cn.iocoder.yudao.framework.extension.core.context; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint; + +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * @description 执行器通用方法 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-29 00:38 + * @class cn.iocoder.yudao.framework.extension.core.context.AbstractComponentExecutor.java + */ +public abstract class AbstractComponentExecutor { + + /** + * ("业务" + "用例" + "场景")执行扩展组件,并返回执行结果 + * @param targetClazz + * @param businessId + * @param useCase + * @param scenario + * @param function + * @param + * @param + * @return + */ + public R execute(Class targetClazz, String businessId, String useCase, String scenario, Function function) { + return execute(targetClazz, BusinessScenario.valueOf(businessId, useCase, scenario), function); + } + + + /** + * ("用例" + "场景")执行扩展组件,并返回执行结果 + * @param targetClazz + * @param useCase + * @param scenario + * @param function + * @param + * @param + * @return + */ + public R execute(Class targetClazz, String useCase, String scenario, Function function) { + return execute(targetClazz, BusinessScenario.valueOf(useCase, scenario), function); + } + + /** + * ("场景")执行扩展组件,并返回执行结果 + * @param targetClazz + * @param scenario + * @param function + * @param + * @param + * @return + */ + public R execute(Class targetClazz, String scenario, Function function) { + return execute(targetClazz, BusinessScenario.valueOf(scenario), function); + } + + /** + * 执行扩展组件,并返回执行结果 + * @param targetClazz + * @param businessScenario + * @param function + * @param Response Type + * @param Parameter Type + * @return + */ + public R execute(Class targetClazz, BusinessScenario businessScenario, Function function) { + T component = locateComponent(targetClazz, businessScenario); + return function.apply(component); + } + + /** + * ("业务" + "用例" + "场景")执行扩展组件,适用于无返回值的业务 + * @param targetClazz + * @param businessId + * @param useCase + * @param scenario + * @param consumer + * @param + */ + public void accept(Class targetClazz, String businessId, String useCase, String scenario, Consumer consumer) { + accept(targetClazz, BusinessScenario.valueOf(businessId, useCase, scenario), consumer); + } + + /** + * ("场景")执行扩展组件,适用于无返回值的业务 + * @param targetClazz + * @param useCase + * @param scenario + * @param consumer + * @param + */ + public void accept(Class targetClazz, String useCase, String scenario, Consumer consumer) { + accept(targetClazz, BusinessScenario.valueOf(useCase, scenario), consumer); + } + + /** + * ("场景")执行扩展组件,适用于无返回值的业务 + * @param targetClazz + * @param scenario + * @param consumer + * @param + */ + public void accept(Class targetClazz, String scenario, Consumer consumer) { + accept(targetClazz, BusinessScenario.valueOf(scenario), consumer); + } + + /** + * 执行扩展组件,适用于无返回值的业务 + * @param targetClazz + * @param businessScenario + * @param consumer + * @param Parameter Type + */ + public void accept(Class targetClazz, BusinessScenario businessScenario, Consumer consumer) { + T component = locateComponent(targetClazz, businessScenario); + consumer.accept(component); + } + + /** + * 获取/定位扩展点组件 + * @param targetClazz + * @param businessScenario + * @param + * @return + */ + protected abstract C locateComponent(Class targetClazz, BusinessScenario businessScenario); +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContext.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContext.java new file mode 100644 index 000000000..5704cc49f --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContext.java @@ -0,0 +1,56 @@ +package cn.iocoder.yudao.framework.extension.core.context; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint; + +/** + * @description 上下文,包含各个扩展点的相关操作 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 22:15 + * @class cn.iocoder.yudao.framework.extension.core.context.ExtensionContext.java + */ +public interface ExtensionContext { + + /** + * 根据业务场景唯一标识获取扩展点组件实现类 + * @param businessId + * @param useCase + * @param scenario + * @param clazz + * @param + * @return + */ + T getPoint(String businessId, String useCase, String scenario, Class clazz); + + /** + * 根据("实例" + "场景")获取扩展点组件实现类,其中:业务id(businessId)= {@linkplain cn.iocoder.yudao.framework.extension.core.BusinessScenario.DEFAULT_BUSINESS_ID} + * @param useCase + * @param scenario + * @param clazz + * @param + * @return + */ + T getPoint(String useCase, String scenario, Class clazz); + + /** + * 根据("场景")获取扩展点组件实现类
+ * 其中: + * 业务id(businessId)= {@linkplain cn.iocoder.yudao.framework.extension.core.BusinessScenario.DEFAULT_BUSINESS_ID} + * 实例(useCase)= {@linkplain cn.iocoder.yudao.framework.extension.core.BusinessScenario.DEFAULT_USECASE} + * @param scenario + * @param clazz + * @param + * @return + */ + T getPoint(String scenario, Class clazz); + + /** + * 根据业务场景唯一标识获取扩展点组件实现类 + * @param businessScenario + * @param clazz + * @param + * @return + */ + T getPoint(BusinessScenario businessScenario, Class clazz); +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContextHolder.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContextHolder.java new file mode 100644 index 000000000..6e121b70d --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionContextHolder.java @@ -0,0 +1,45 @@ +package cn.iocoder.yudao.framework.extension.core.context; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import cn.iocoder.yudao.framework.extension.core.factory.ExtensionFactory; +import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.validation.constraints.NotNull; + +/** + * @description 上下文及扩展点组件工厂的持有类 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-29 00:29 + * @class cn.iocoder.yudao.framework.extension.core.context.ExtensionContextHolder.java + */ +@Component +@Slf4j +public class ExtensionContextHolder implements ExtensionContext{ + + @Autowired + private ExtensionFactory factory; + + @Override + public T getPoint(@NotNull String businessId, @NotNull String useCase, @NotNull String scenario, Class clazz) { + return getPoint(BusinessScenario.valueOf(businessId, useCase, scenario), clazz); + } + + @Override + public T getPoint(@NotNull String useCase, String scenario, Class clazz) { + return getPoint(BusinessScenario.valueOf(useCase, scenario), clazz); + } + + @Override + public T getPoint(@NotNull String scenario, Class clazz) { + return getPoint(BusinessScenario.valueOf(scenario), clazz); + } + + @Override + public T getPoint(@NotNull BusinessScenario businessScenario, Class clazz) { + return factory.get(businessScenario, clazz); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionExecutor.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionExecutor.java new file mode 100644 index 000000000..5f48652e5 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/context/ExtensionExecutor.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.framework.extension.core.context; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @description 扩展组件执行器 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-29 00:32 + * @class cn.iocoder.yudao.framework.extension.core.context.ExtensionExecutor.java + */ +@Component +@Slf4j +public class ExtensionExecutor extends AbstractComponentExecutor{ + + @Autowired + private ExtensionContextHolder contextHolder; + + + @Override + protected C locateComponent(Class targetClazz, BusinessScenario businessScenario) { + return contextHolder.getPoint(businessScenario, targetClazz); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionDefinition.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionDefinition.java new file mode 100644 index 000000000..5feed6660 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionDefinition.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.framework.extension.core.factory; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotNull; +import java.io.Serializable; +import java.util.Objects; + +/** + * @description 扩展定义(扩展坐标),标识唯一一个业务场景实现 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 23:14 + * @class cn.iocoder.yudao.framework.extension.core.factory.ExtensionDefinition.java + */ +@Setter +@Getter +public class ExtensionDefinition implements Serializable { + + /** + * 业务场景唯一标识(id) + */ + private String uniqueIdentify; + + /** + * 扩展点实现类名称 + */ + private String extensionPointName; + + /** + * 业务场景 + */ + private BusinessScenario businessScenario; + + /** + * 扩展点实现类 + */ + private ExtensionPoint extensionPoint; + + /** + * class + */ + private Class extensionPointClass; + + public ExtensionDefinition() { + } + + public ExtensionDefinition(@NotNull BusinessScenario businessScenario, @NotNull ExtensionPoint extensionPoint) { + this.businessScenario = businessScenario; + this.extensionPoint = extensionPoint; + this.uniqueIdentify = this.businessScenario.getUniqueIdentity(); + this.extensionPointClass = this.extensionPoint.getClass(); + } + + /** + * 构建definition + * @param businessScenario + * @param point + * @return + */ + public static ExtensionDefinition valueOf(@NotNull BusinessScenario businessScenario, @NotNull ExtensionPoint point) { + return new ExtensionDefinition(businessScenario, point); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ExtensionDefinition that = (ExtensionDefinition) o; + return Objects.equals(uniqueIdentify, that.uniqueIdentify) && Objects.equals(extensionPointName, that.extensionPointName); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((uniqueIdentify == null) ? 0 : uniqueIdentify.hashCode()); + result = prime * result + ((extensionPointName == null) ? 0 : extensionPointName.hashCode()); + return result; + } + + @Override + public String toString() { + return "ExtensionDefinition{" + + "uniqueIdentify='" + uniqueIdentify + '\'' + + ", extensionPointName='" + extensionPointName + '\'' + + '}'; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionFactory.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionFactory.java new file mode 100644 index 000000000..c03175c13 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionFactory.java @@ -0,0 +1,29 @@ +package cn.iocoder.yudao.framework.extension.core.factory; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint; + +/** + * @description 扩展点工厂 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 23:04 + * @class cn.iocoder.yudao.framework.extension.core.factory.ExtensionFactory.java + */ +public interface ExtensionFactory { + + /** + * 注册所有扩展点实现类 + * @param basePackage + */ + void register(String basePackage); + + /** + * 根据业务场景获取指定类型的扩展点 + * @param businessScenario + * @param clazz + * @param + * @return + */ + T get(BusinessScenario businessScenario, Class clazz); +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionRegisterFactory.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionRegisterFactory.java new file mode 100644 index 000000000..15b802436 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/factory/ExtensionRegisterFactory.java @@ -0,0 +1,86 @@ +package cn.iocoder.yudao.framework.extension.core.factory; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint; +import cn.iocoder.yudao.framework.extension.core.stereotype.Extension; +import lombok.extern.slf4j.Slf4j; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.ApplicationContext; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; +import org.springframework.util.ClassUtils; + +import javax.validation.constraints.NotNull; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @description 注册工厂 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 23:07 + * @class cn.iocoder.yudao.framework.extension.core.factory.ExtensionRegisterFactory.java + */ +@Component +@Slf4j +public class ExtensionRegisterFactory implements ExtensionFactory { + + /** + * spring ApplicationContext + */ + private ApplicationContext applicationContext; + + /** + * 扩展点实现类集合 + */ + private Map registerExtensionBeans = new ConcurrentHashMap<>(); + + @Override + public void register(String basePackage) { + final Map beans = applicationContext.getBeansWithAnnotation(Extension.class); + if(beans == null || beans.isEmpty()) { + return; + } + + beans.values().forEach(point -> doRegister((ExtensionPoint) point)); + log.info("业务场景相关扩展点注册完成,注册数量: {}", registerExtensionBeans.size()); + } + + @Override + public T get(BusinessScenario businessScenario, Class clazz) { + + final ExtensionDefinition definition = registerExtensionBeans.get(businessScenario.getUniqueIdentity()); + if(definition == null) { + log.error("获取业务场景扩展点实现失败,失败原因:尚未定义该业务场景相关扩展点。{}", businessScenario); + throw new RuntimeException("尚未定义该业务场景相关扩展点 [" + businessScenario + "]"); + } + + return (T) definition.getExtensionPoint(); + } + + /** + * 注册扩展点 + * @param point + */ + private void doRegister(@NotNull ExtensionPoint point) { + Class extensionClazz = point.getClass(); + + if (AopUtils.isAopProxy(point)) { + extensionClazz = ClassUtils.getUserClass(point); + } + + Extension extension = AnnotationUtils.findAnnotation(extensionClazz, Extension.class); + final BusinessScenario businessScenario = BusinessScenario.valueOf(extension.businessId(), extension.useCase(), extension.scenario()); + final ExtensionDefinition definition = ExtensionDefinition.valueOf(businessScenario, point); + final ExtensionDefinition exist = registerExtensionBeans.get(businessScenario.getUniqueIdentity()); + if(exist != null && !exist.equals(definition)) { + throw new RuntimeException("相同的业务场景重复注册了不同类型的扩展点实现 :【" + definition + "】【" + exist + "】"); + } + + registerExtensionBeans.put(businessScenario.getUniqueIdentity(), definition); + } + + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/package-info.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/package-info.java new file mode 100644 index 000000000..636491eca --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/package-info.java @@ -0,0 +1,8 @@ +/** + * @description core 核心 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 21:54 + * @class cn.iocoder.yudao.framework.extension.core.package-info.java + */ +package cn.iocoder.yudao.framework.extension.core; \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/point/ExtensionPoint.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/point/ExtensionPoint.java new file mode 100644 index 000000000..e1a86d791 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/point/ExtensionPoint.java @@ -0,0 +1,11 @@ +package cn.iocoder.yudao.framework.extension.core.point; +/** + * @description 扩展点
+ * 表示一块逻辑在不同的业务有不同的实现,使用扩展点做接口申明,然后用{@linkplain cn.iocoder.yudao.framework.extension.core.stereotype.Extension}(扩展)去实现扩展点。 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 22:06 + * @class cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint.java + */ +public interface ExtensionPoint { +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/stereotype/Extension.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/stereotype/Extension.java new file mode 100644 index 000000000..eaa9f99f8 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/core/stereotype/Extension.java @@ -0,0 +1,41 @@ +package cn.iocoder.yudao.framework.extension.core.stereotype; + +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import org.springframework.stereotype.Component; + +import java.lang.annotation.*; + +/** + * @description 表示带注释的类是“扩展组件” + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 21:59 + * @class cn.iocoder.yudao.framework.extension.core.stereotype.Extension.java + */ +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +@Component +public @interface Extension { + + /** + * 业务
+ * 一个自负盈亏的财务主体,比如tmall、淘宝和零售通就是三个不同的业务 + * @return + */ + String businessId() default BusinessScenario.DEFAULT_BUSINESS_ID; + + /** + * 用例
+ * 描述了用户和系统之间的互动,每个用例提供了一个或多个场景。比如,支付订单就是一个典型的用例。 + * @return + */ + String useCase() default BusinessScenario.DEFAULT_USECASE; + + /** + * 场景
+ * 场景也被称为用例的实例(Instance),包括用例所有的可能情况(正常的和异常的)。比如对于"订单支付"这个用例,就有“支付宝支付”、“银行卡支付”、"微信支付"等多个场景 + * @return + */ + String scenario() default BusinessScenario.DEFAULT_SCENARIO; +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/package-info.java b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/package-info.java new file mode 100644 index 000000000..1c4ae94ae --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/java/cn/iocoder/yudao/framework/extension/package-info.java @@ -0,0 +1,8 @@ +/** + * @description 扩展点组件 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-28 14:35 + * @class cn.iocoder.yudao.framework.extension.package-info.java + */ +package cn.iocoder.yudao.framework.extension; \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/main/resources/META-INF/spring.factories b/yudao-framework/yudao-spring-boot-starter-extension/src/main/resources/META-INF/spring.factories new file mode 100644 index 000000000..82d14b8b0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ + cn.iocoder.yudao.framework.extension.config.YudaoExtensionAutoConfiguration diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/Application.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/Application.java new file mode 100644 index 000000000..a589affd3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/Application.java @@ -0,0 +1,19 @@ +package cn.iocoder.yudao.framework.extension; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * @description Application + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-30 10:32 + * @class cn.iocoder.yudao.framework.extension.Application.java + */ +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/ExtensionTest.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/ExtensionTest.java new file mode 100644 index 000000000..aa3b900c3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/ExtensionTest.java @@ -0,0 +1,43 @@ +package cn.iocoder.yudao.framework.extension; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.extension.core.BusinessScenario; +import cn.iocoder.yudao.framework.extension.core.context.ExtensionExecutor; +import cn.iocoder.yudao.framework.extension.pay.PayExtensionPoint; +import cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand; +import cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult; +import lombok.extern.slf4j.Slf4j; +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.math.BigDecimal; + +/** + * @description + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-30 10:30 + * @class cn.iocoder.yudao.framework.extension.ExtensionTest.java + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = Application.class) +@Slf4j +public class ExtensionTest { + + @Autowired + private ExtensionExecutor extensionExecutor; + + @Test + public void unifiedOrder() { + final BusinessScenario scenario = BusinessScenario.valueOf("pay", "jsapi", "wechat"); + final TransactionsCommand command = new TransactionsCommand(IdUtil.objectId(), new BigDecimal(105), "Image形象店-深圳腾大-QQ公仔", "https://www.weixin.qq.com/wxpay/pay.php"); + final TransactionsResult result = extensionExecutor.execute(PayExtensionPoint.class, scenario, extension -> extension.unifiedOrder(command)); + log.info("result is: {}", JSONUtil.toJsonStr(result)); + Assert.assertSame("wechat", result.getChannel()); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/package-info.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/package-info.java new file mode 100644 index 000000000..d0bc63e1c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/package-info.java @@ -0,0 +1,8 @@ +/** + * @description + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-30 10:25 + * @class cn.iocoder.yudao.framework.extension.package-info.java + */ +package cn.iocoder.yudao.framework.extension; \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/PayExtensionPoint.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/PayExtensionPoint.java new file mode 100644 index 000000000..9adc56b4c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/PayExtensionPoint.java @@ -0,0 +1,22 @@ +package cn.iocoder.yudao.framework.extension.pay; + +import cn.iocoder.yudao.framework.extension.core.point.ExtensionPoint; +import cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand; +import cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult; + +/** + * @description 支付操作接口 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-30 10:35 + * @class cn.iocoder.yudao.framework.extension.pay.PayExtensionPoint.java + */ +public interface PayExtensionPoint extends ExtensionPoint { + + /** + * 统一下单:获取"预支付交易会话标识" + * @param command + * @return + */ + TransactionsResult unifiedOrder(TransactionsCommand command); +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/command/TransactionsCommand.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/command/TransactionsCommand.java new file mode 100644 index 000000000..0ac1908dd --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/command/TransactionsCommand.java @@ -0,0 +1,40 @@ +package cn.iocoder.yudao.framework.extension.pay.command; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.io.Serializable; +import java.math.BigDecimal; + +/** + * @description 下单请求 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-30 10:48 + * @class cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand.java + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +public class TransactionsCommand implements Serializable { + /** + * 订单编号 + */ + private String orderNo; + + /** + * 支付金额 + */ + private BigDecimal amount; + + /** + * 商品描述 + */ + private String productDescription; + + /** + * 通知地址 + */ + private String notifyUrl; +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/domain/TransactionsResult.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/domain/TransactionsResult.java new file mode 100644 index 000000000..35861bbd0 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/domain/TransactionsResult.java @@ -0,0 +1,39 @@ +package cn.iocoder.yudao.framework.extension.pay.domain; + +import lombok.*; + +import java.io.Serializable; + +/** + * @description 下单: 预支付交易单返回结果 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-30 10:43 + * @class cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult.java + */ +@Data +@AllArgsConstructor +public class TransactionsResult implements Serializable { + + /** + * 预支付交易会话标识 + */ + private String prepayId; + + /** + * 订单编号 + */ + private String orderNo; + + /** + * 系统内部支付单号 + */ + private String paymentNo; + + /** + * 支付渠道:微信 or 支付宝 + */ + private String channel; + + +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/AlipayService.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/AlipayService.java new file mode 100644 index 000000000..2daac2ff6 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/AlipayService.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.extension.pay.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.extension.core.stereotype.Extension; +import cn.iocoder.yudao.framework.extension.pay.PayExtensionPoint; +import cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand; +import cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult; +import lombok.extern.slf4j.Slf4j; + +/** + * @description 微信 JSAPI 支付 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-30 10:38 + * @class cn.iocoder.yudao.framework.extension.pay.impl.AlipayService.java + */ +@Extension(businessId = "pay", useCase = "jsapi", scenario = "alipay") +@Slf4j +public class AlipayService implements PayExtensionPoint { + @Override + public TransactionsResult unifiedOrder(TransactionsCommand command) { + log.info("微信 JSAPI 支付:{}", JSONUtil.toJsonStr(command)); + return new TransactionsResult("alipay26112221580621e9b071c00d9e093b0000", command.getOrderNo(), IdUtil.objectId(), "alipay"); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/WechatPayService.java b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/WechatPayService.java new file mode 100644 index 000000000..cae294092 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/src/test/java/cn/iocoder/yudao/framework/extension/pay/impl/WechatPayService.java @@ -0,0 +1,26 @@ +package cn.iocoder.yudao.framework.extension.pay.impl; + +import cn.hutool.core.util.IdUtil; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.extension.core.stereotype.Extension; +import cn.iocoder.yudao.framework.extension.pay.PayExtensionPoint; +import cn.iocoder.yudao.framework.extension.pay.command.TransactionsCommand; +import cn.iocoder.yudao.framework.extension.pay.domain.TransactionsResult; +import lombok.extern.slf4j.Slf4j; + +/** + * @description 微信 JSAPI 支付 + * @author Qingchen + * @version 1.0.0 + * @date 2021-08-30 10:37 + * @class cn.iocoder.yudao.framework.extension.pay.impl.WechatPayService.java + */ +@Extension(businessId = "pay", useCase = "jsapi", scenario = "wechat") +@Slf4j +public class WechatPayService implements PayExtensionPoint { + @Override + public TransactionsResult unifiedOrder(TransactionsCommand command) { + log.info("微信 JSAPI 支付:{}", JSONUtil.toJsonStr(command)); + return new TransactionsResult("wx26112221580621e9b071c00d9e093b0000", command.getOrderNo(), IdUtil.objectId(), "wechat"); + } +} diff --git a/yudao-framework/yudao-spring-boot-starter-extension/《芋道 Spring Boot 扩展点组件》.md b/yudao-framework/yudao-spring-boot-starter-extension/《芋道 Spring Boot 扩展点组件》.md new file mode 100644 index 000000000..59ff2dcb3 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-extension/《芋道 Spring Boot 扩展点组件》.md @@ -0,0 +1,19 @@ +### 作用 + +​ 为了解决同一个流程不同业务有不同处理逻辑而产生,减少代码中 if else 逻辑,降低代码的耦合性,通过统一的扩展形式来支撑业务的变化。 + +### 原理 + +​ https://blog.csdn.net/significantfrank/article/details/100074716 + + + +### 使用介绍 + +参考测试代码 `cn.iocoder.yudao.framework.extension.ExtensionTest` + + + + + +