阿里 cola-component-extension-starter 解读

1,565 阅读8分钟

基本用法

需求:根据不同的业务场景,选择不同的跨区判断逻辑来获取跨区幅度

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 的类,怎么存放进去的见 ExtensionBootstrapExtensionRegister
@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