阿里 cola-component-extension-st
基本用法
需求:根据不同的业务场景,选择不同的跨区判断逻辑来获取跨区幅度
Maven 坐标
<dependency>
<groupId>com.alibaba.cola</groupId>
<artifactId>cola-component-extension-starter</artifactId>
<version>4.3.1</version>
</dependency>
定义接口
- 定义一个接口,继承 cola-component-extension-starter 的 ExtensionPointI 接口,并且要以 ExtPt 结尾
public interface CrossRegionServiceExtPt extends ExtensionPointI {
// 获取跨区幅度
CrossRegionResult getCrossRegion(CrossRegionRequest crossRegionRequest);
}
实现接口
- 根据不同的业务场景,定义两个实现类,实现 CrossRegionServiceExtPt 接口,在实现类上添加 @Extension 注解,并且指定不同的 bizId,并且要以 ExtPt 结尾
@Extension(bizId = "regionNo")
@Slf4j
public class RegionNoCrossRegionExtPt implements CrossRegionServiceExtPt {
@Override
public CrossRegionResult getCrossRegion(CrossRegionRequest crossRegionRequest) {
log.info("根据省区判断是否跨区");
return null;
}
}
@Extension(bizId = "registerCity")
@Slf4j
public class RegisterCityCrossRegionServiceExtPt implements CrossRegionServiceExtPt {
@Override
public CrossRegionResult getCrossRegion(CrossRegionRequest crossRegionRequest) {
log.info("根据注册城市判断是否跨区");
return null;
}
}
- 业务枚举
@Getter
@AllArgsConstructor
public enum CrossRegionBizEnum {
JUDGE_BY_REGION_NO("regionNo", "根据省区判断"),
JUDGE_BU_REGISTER_CITY("registerCity", "根据注册城市判断");
}
测试
- 测试类
import com.alibaba.cola.extension.BizScenario;
import com.alibaba.cola.extension.ExtensionExecutor;
import com.xdh.design.demo.extension.enums.CrossRegionBizEnum;
import com.xdh.design.demo.extension.model.CrossRegionRequest;
import com.xdh.design.demo.extension.model.CrossRegionResult;
import com.xdh.design.demo.extension.service.CrossRegionServiceExtPt;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
class DesignDemoApplicationTests {
@Resource
private ExtensionExecutor extensionExecutor;
@Test
void testCrossRegion() {
// 模拟请求入参,并且是要根据省区进行跨区判断去获取跨区幅度
CrossRegionRequest crossRegionRequest = new CrossRegionRequest();
crossRegionRequest.setBizCode(CrossRegionBizEnum.JUDGE_BY_REGION_NO.getValue());
// 通过扩展点的方式获取到相应的处理类进行处理,避免了 if else 的判断
CrossRegionResult crossRegionResult = extensionExecutor.execute(CrossRegionServiceExtPt.class, BizScenario.valueOf(crossRegionRequest.getBizCode()), crossRegionService -> {
return crossRegionService.getCrossRegion(crossRegionRequest);
});
}
}
// 输出:根据省区判断是否跨区
原理解析
坐标 ExtensionCoordinate
- 该类的作用是:用来确定一个唯一的 Extension
public class ExtensionCoordinate {
// 扩展点名称,实际上就是继承了 ExtensionPointI 的接口的实现了,比如:RegionNoCrossRegionExtPt
private String extensionPointName;
// 业务场景的唯一标识
private String bizScenarioUniqueIdentity;
// 扩展点的包装类
private Class extensionPointClass;
// 业务场景对象
private BizScenario bizScenario;
/**
* 获取扩展点的包装类
*/
public Class getExtensionPointClass() {
return extensionPointClass;
}
/**
* 获取业务场景对象
*/
public BizScenario getBizScenario() {
return bizScenario;
}
/**
* 通过扩展点的类和业务场景对象创建一个扩展坐标对象
*/
public static ExtensionCoordinate valueOf(Class extPtClass, BizScenario bizScenario){
return new ExtensionCoordinate(extPtClass, bizScenario);
}
/**
* 构造方法,通过扩展点的类和业务场景对象创建一个扩展坐标对象
*/
public ExtensionCoordinate(Class extPtClass, BizScenario bizScenario){
this.extensionPointClass = extPtClass;
this.extensionPointName = extPtClass.getName();
this.bizScenario = bizScenario;
this.bizScenarioUniqueIdentity = bizScenario.getUniqueIdentity();
}
/**
* 构造方法,通过扩展点名称和业务场景唯一标识创建一个扩展坐标对象
*/
public ExtensionCoordinate(String extensionPoint, String bizScenario){
this.extensionPointName = extensionPoint;
this.bizScenarioUniqueIdentity = bizScenario;
}
/**
* 重写 hashCode 方法
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((bizScenarioUniqueIdentity == null) ? 0 : bizScenarioUniqueIdentity.hashCode());
result = prime * result + ((extensionPointName == null) ? 0 : extensionPointName.hashCode());
return result;
}
/**
* 重写 equals 方法,用于判断两个扩展坐标对象是否相等
*/
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
ExtensionCoordinate other = (ExtensionCoordinate) obj;
if (bizScenarioUniqueIdentity == null) {
if (other.bizScenarioUniqueIdentity != null) return false;
} else if (!bizScenarioUniqueIdentity.equals(other.bizScenarioUniqueIdentity)) return false;
if (extensionPointName == null) {
if (other.extensionPointName != null) return false;
} else if (!extensionPointName.equals(other.extensionPointName)) return false;
return true;
}
/**
* 重写 toString 方法,用于输出扩展坐标的字符串表示
*/
@Override
public String toString() {
return "ExtensionCoordinate [extensionPointName=" + extensionPointName + ", bizScenarioUniqueIdentity=" + bizScenarioUniqueIdentity + "]";
}
}
业务标识 BizScenario
public class BizScenario {
public static final String DEFAULT_BIZ_ID = "#defaultBizId#";
public static final String DEFAULT_USE_CASE = "#defaultUseCase#";
public static final String DEFAULT_SCENARIO = "#defaultScenario#";
private static final String DOT_SEPARATOR = ".";
private String bizId = "#defaultBizId#";
private String useCase = "#defaultUseCase#";
private String scenario = "#defaultScenario#";
public BizScenario() {
}
public String getUniqueIdentity() {
return this.bizId + "." + this.useCase + "." + this.scenario;
}
// ...
}
扩展点接口 ExtensionPointI
- 扩展点表示一块逻辑在不同的业务有不同的实现,使用扩展点做接口申明,然后用 Extension(扩展)去实现扩展点
public interface ExtensionPointI {
}
扩展点集合 ExtensionRepository
- 定义一个 Map,用来存放所有继承了 ExtensionPointI 的类,怎么存放进去的见 ExtensionBootstrap 和 ExtensionRegister 类
@Component
public class ExtensionRepository {
public Map<ExtensionCoordinate, ExtensionPointI> getExtensionRepo() {
return extensionRepo;
}
private Map<ExtensionCoordinate, ExtensionPointI> extensionRepo = new HashMap<>();
}
启动类 ExtensionBootstrap
- ExtensionBootstrap 类是一个使用 Spring 框架的组件,它实现了 ApplicationContextAware 接口。其主要作用是在应用程序启动时,通过扫描被 @Extension 注解标记的类,并将这些类注册到 ExtensionRegister 中
@Component
public class ExtensionBootstrap implements ApplicationContextAware {
@Resource
private ExtensionRegister extensionRegister;
private ApplicationContext applicationContext;
// @PostConstruct 是一个标准的 Java 注解,用于指定在对象构造完成后需要执行的初始化方法。当一个类上的方法被 @PostConstruct 注解标记时,该方法将会在对象的依赖注入完成后、在容器实例化对象并完成属性注入后自动调用。
@PostConstruct
public void init(){
// 使用 getBeansWithAnnotation() 方法获取所有被 @Extension 注解标记的 bean,并遍历这些 bean。
Map<String, Object> extensionBeans = applicationContext.getBeansWithAnnotation(Extension.class);
// 调用 extensionRegister 的 doRegistration() 方法,将扩展点对象注册到 ExtensionRepository 中
extensionBeans.values().forEach(
extension -> extensionRegister.doRegistration((ExtensionPointI) extension)
);
}
// 实现 ApplicationContextAware 接口的 setApplicationContext 方法
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
注册类 ExtensionRegister
- ExtensionRegister 类的作用是提供一个注册扩展组件的接口,并在注册过程中解析扩展组件的注解信息,确定其关联的业务场景和扩展点,最终将其注册到 ExtensionRepository 中,以便在运行时进行扩展组件的查找和调用
@Component
public class ExtensionRegister{
@Resource
private ExtensionRepository extensionRepository;
public final static String EXTENSION_EXTPT_NAMING = "ExtPt";
// 注册扩展组件
public void doRegistration(ExtensionPointI extensionObject){
// 获取扩展组件对象的类
Class<?> extensionClz = extensionObject.getClass();
// 检查扩展组件对象是否是代理对象,如果是,则获取原始类
if (AopUtils.isAopProxy(extensionObject)) {
extensionClz = ClassUtils.getUserClass(extensionObject);
}
// 获取扩展组件类上的 @Extension 注解,解析出业务场景信息
Extension extensionAnn = AnnotationUtils.findAnnotation(extensionClz, Extension.class);
BizScenario bizScenario = BizScenario.valueOf(extensionAnn.bizId(), extensionAnn.useCase(), extensionAnn.scenario());
// 计算扩展点和业务场景的唯一标识符
ExtensionCoordinate extensionCoordinate = new ExtensionCoordinate(calculateExtensionPoint(extensionClz), bizScenario.getUniqueIdentity());
// 将扩展组件注册到 ExtensionRepository 中,并获取之前注册的组件(如果存在)
ExtensionPointI preVal = extensionRepository.getExtensionRepo().put(extensionCoordinate, extensionObject);
// 如果之前已经注册了相同的扩展组件,则抛出异常
if (preVal != null) {
throw new RuntimeException("Duplicate registration is not allowed for :" + extensionCoordinate);
}
}
/**
* @param targetClz
* @return
*/
private String calculateExtensionPoint(Class<?> targetClz) {
Class<?>[] interfaces = ClassUtils.getAllInterfacesForClass(targetClz);
if (interfaces == null || interfaces.length == 0)
throw new RuntimeException("Please assign a extension point interface for "+targetClz);
for (Class intf : interfaces) {
String extensionPoint = intf.getSimpleName();
if (extensionPoint.contains(EXTENSION_EXTPT_NAMING))
return intf.getName();
}
// 这里就是为什么必须以 ExePt 结尾的原因
throw new RuntimeException("Your name of ExtensionPoint for "+targetClz+" is not valid, must be end of "+ EXTENSION_EXTPT_NAMING);
}
}
扩展点注解 Extension
import org.springframework.stereotype.Component;
import java.lang.annotation.*;
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Component
public @interface Extension {
String bizId() default BizScenario.DEFAULT_BIZ_ID;
String useCase() default BizScenario.DEFAULT_USE_CASE;
String scenario() default BizScenario.DEFAULT_SCENARIO;
}
AbstractComponentExecutor
- 一个抽象类,它定义了一些执行组件操作的方法
import com.alibaba.cola.extension.BizScenario;
import com.alibaba.cola.extension.ExtensionCoordinate;
import java.util.function.Consumer;
import java.util.function.Function;
public abstract class AbstractComponentExecutor {
/**
* 该方法用于执行目标组件的操作,并返回一个结果
*
* @param targetClz 目标组件的类对象,指定要执行操作的组件类型
* @param bizScenario 业务场景对象,表示要执行操作的具体业务场景
* @param exeFunction 执行操作的函数接口,接受目标组件对象作为参数,返回一个结果
* @param <R> 方法的返回类型
* @param <T> 目标组件的类型
* @return 返回结果
*/
public <R, T> R execute(Class<T> targetClz, BizScenario bizScenario, Function<T, R> exeFunction) {
T component = locateComponent(targetClz, bizScenario);
return exeFunction.apply(component);
}
/**
* 该方法是对前一个 execute() 方法的重载,方便使用扩展坐标对象作为参数
*
* @param extensionCoordinate
* @param exeFunction
* @param <R>
* @param <T>
* @return
*/
public <R, T> R execute(ExtensionCoordinate extensionCoordinate, Function<T, R> exeFunction) {
return execute((Class<T>) extensionCoordinate.getExtensionPointClass(), extensionCoordinate.getBizScenario(), exeFunction);
}
/**
* 该方法专门用于执行没有返回值的目标组件操作
*
* @param targetClz 目标组件的类对象,指定要执行操作的组件类型
* @param context 业务场景对象,表示要执行操作的具体业务场景
* @param exeFunction 执行操作的消费者接口,接受目标组件对象作为参数,没有返回值
* @param <T> 目标组件的类型
*/
public <T> void executeVoid(Class<T> targetClz, BizScenario context, Consumer<T> exeFunction) {
T component = locateComponent(targetClz, context);
exeFunction.accept(component);
}
/**
* 该方法是对前一个 executeVoid() 方法的重载,方便使用扩展坐标对象作为参数
*
* @param extensionCoordinate
* @param exeFunction
* @param <T>
*/
public <T> void executeVoid(ExtensionCoordinate extensionCoordinate, Consumer<T> exeFunction) {
executeVoid(extensionCoordinate.getExtensionPointClass(), extensionCoordinate.getBizScenario(), exeFunction);
}
/**
* 这是一个抽象方法,需要在子类中实现。该方法用于根据给定的组件类型和业务场景定位组件对象,即获取具体要执行操作的组件实例
*
* @param targetClz
* @param context
* @return
* @param <C>
*/
protected abstract <C> C locateComponent(Class<C> targetClz, BizScenario context);
}
ExtensionExecutor
- 继承了 AbstractComponentExecutor 类,,根据给定的场景和条件来定位扩展组件,并提供了一些辅助方法来支持定位逻辑
@Component
public class ExtensionExecutor extends AbstractComponentExecutor {
private Logger logger = LoggerFactory.getLogger(ExtensionExecutor.class);
@Resource
private ExtensionRepository extensionRepository;
@Override
protected <C> C locateComponent(Class<C> targetClz, BizScenario bizScenario) {
C extension = locateExtension(targetClz, bizScenario);
logger.debug("[Located Extension]: " + extension.getClass().getSimpleName());
return extension;
}
/**
* 根据给定的 BizScenario 对象的唯一标识,按照一定的规则进行多次尝试来定位组件。
* 第一次尝试是使用完整的命名空间进行定位,即根据 bizScenario.getUniqueIdentity() 来定位。
* 如果第一次尝试失败,会进行第二次尝试,尝试使用默认场景的命名空间进行定位,即根据 * * * bizScenario.getIdentityWithDefaultScenario() 来定位。
* 如果第二次尝试失败,会进行第三次尝试,尝试使用默认用例和默认场景的命名空间进行定位,即根据 * bizScenario.getIdentityWithDefaultUseCase() 来定位。
* 如果所有尝试都失败,则抛出运行时异常。
*/
protected <Ext> Ext locateExtension(Class<Ext> targetClz, BizScenario bizScenario) {
checkNull(bizScenario);
Ext extension;
logger.debug("BizScenario in locateExtension is : " + bizScenario.getUniqueIdentity());
extension = firstTry(targetClz, bizScenario);
if (extension != null) {
return extension;
}
extension = secondTry(targetClz, bizScenario);
if (extension != null) {
return extension;
}
extension = defaultUseCaseTry(targetClz, bizScenario);
if (extension != null) {
return extension;
}
throw new RuntimeException("Can not find extension with ExtensionPoint: "+targetClz+" BizScenario:"+bizScenario.getUniqueIdentity());
}
private <Ext> Ext firstTry(Class<Ext> targetClz, BizScenario bizScenario) {
logger.debug("First trying with " + bizScenario.getUniqueIdentity());
return locate(targetClz.getName(), bizScenario.getUniqueIdentity());
}
private <Ext> Ext secondTry(Class<Ext> targetClz, BizScenario bizScenario){
logger.debug("Second trying with " + bizScenario.getIdentityWithDefaultScenario());
return locate(targetClz.getName(), bizScenario.getIdentityWithDefaultScenario());
}
private <Ext> Ext defaultUseCaseTry(Class<Ext> targetClz, BizScenario bizScenario){
logger.debug("Third trying with " + bizScenario.getIdentityWithDefaultUseCase());
return locate(targetClz.getName(), bizScenario.getIdentityWithDefaultUseCase());
}
private <Ext> Ext locate(String name, String uniqueIdentity) {
final Ext ext = (Ext) extensionRepository.getExtensionRepo().
get(new ExtensionCoordinate(name, uniqueIdentity));
return ext;
}
private void checkNull(BizScenario bizScenario){
if(bizScenario == null){
throw new IllegalArgumentException("BizScenario can not be null for extension");
}
}
}
初始化相关组件 ExtensionAutoConfiguration
- 用于配置和初始化扩展组件相关的 bean。它创建了 bootstrap、repository、executor 和 register 这几个 bean,分别对应于 ExtensionBootstrap、ExtensionRepository、ExtensionExecutor 和 ExtensionRegister 类。这些 bean 可以在应用程序中被注入和使用,从而实现扩展组件的功能
@Configuration
public class ExtensionAutoConfiguration {
/**
* 通过 @Bean 注解标识,表示将返回的对象注册为一个 bean
* 使用 @ConditionalOnMissingBean(ExtensionBootstrap.class) 注解,表示只有当容器中不存在 ExtensionBootstrap 类型的 bean 时才会创建该 bean
* bootstrap 方法将返回一个 ExtensionBootstrap 对象,并设置了它的 initMethod 为 "init"。这表示在 bean 初始化完成后会调用 init 方法
*/
@Bean(initMethod = "init")
@ConditionalOnMissingBean(ExtensionBootstrap.class)
public ExtensionBootstrap bootstrap() {
return new ExtensionBootstrap();
}
@Bean
@ConditionalOnMissingBean(ExtensionRepository.class)
public ExtensionRepository repository() {
return new ExtensionRepository();
}
@Bean
@ConditionalOnMissingBean(ExtensionExecutor.class)
public ExtensionExecutor executor() {
return new ExtensionExecutor();
}
@Bean
@ConditionalOnMissingBean(ExtensionRegister.class)
public ExtensionRegister register() {
return new ExtensionRegister();
}
}
知识点
-
@PostConstruct 注解:是一个标准的 Java 注解,用于指定在对象构造完成后需要执行的初始化方法。当一个类上的方法被 @PostConstruct 注解标记时,该方法将会在对象的依赖注入完成后、在容器实例化对象并完成属性注入后自动调用。
-
获取 @Extension 注解的相关信息:
Extension extensionAnn = AnnotationUtils.findAnnotation(extensionClz, Extension.class);
-
@Bean(initMethod = "init"):表示在 bean 初始化完成后会调用
init
方法 -
@ConditionalOnMissingBean(ExtensionBootstrap.class) 注解:表示只有当容器中不存在 ExtensionBootstrap 类型的 bean 时才会创建该 bean