cola扩展点使用和设计初探
封装变化,可灵活应对程序的需求变化。
扩展点使用
步骤:
定义扩展点接口,类型可以是校验器,转换器,实体; 必须以ExtPt结尾,表示一个扩展点。
比如,我定义一个云枢的组织结构的扩展点接口,消息发送扩展点,二开扩展点,webapi的rest接口扩展点点。
定义扩展点接口
package com.authine.web.cola.domain.customer;
import com.alibaba.cola.extension.ExtensionPointI;
import com.authine.web.cola.dto.domainmodel.Department;
import java.util.List;
/**
* @author carter
* create_date 2020/5/25 14:25
* description 定义扩展点接口,对组织机构的某些方法。
*/
public interface OrganizationExtPt extends ExtensionPointI {
/**
* 根据corpId查询企业下所有部门
*
* @param corpId 企业编号
* @param includeDelete 是否包含删除的部门
* @return 部门
*/
List<Department> getDepartmentsByCorpId(String corpId, Boolean includeDelete);
}
比如业务扩展分为钉钉,微信:
这里基于扩展理论(x,y);
即通过 业务,用例,场景得到扩展点的key, 那后扩展类就是针对实际的业务场景的业务处理代码;
钉钉场景扩展点实现
package com.authine.web.cola.domain.customer.extpt;
import com.alibaba.cola.extension.Extension;
import com.authine.web.cola.dto.domainmodel.Department;
import com.authine.web.cola.domain.customer.OrganizationExtPt;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.List;
/**
* @author carter
* create_date 2020/5/25 14:32
* description 企业部门在通过corpId获取部门列表的场景下,钉钉的扩展
*/
@Extension(bizId = "organize",useCase = "getByCorpId",scenario = "dingTalk")
@Slf4j
public class DingTalkOrganizationExt implements OrganizationExtPt {
@Override
public List<Department> getDepartmentsByCorpId(String corpId, Boolean includeDelete) {
log.info("在组织结构业务,通过企业编号获取部门列表的用例,在钉钉的场景下业务的实现处理方式");
log.info("通过钉钉的配置信息和API获取得到组织信息,并组装成云枢识别的部门信息");
Department department = new Department();
department.setName("dingTalk");
department.setCorpId(corpId);
return Collections.singletonList(department);
}
}
企业微信扩展点实现
package com.authine.web.cola.domain.customer.extpt;
import com.alibaba.cola.extension.Extension;
import com.authine.web.cola.dto.domainmodel.Department;
import com.authine.web.cola.domain.customer.OrganizationExtPt;
import lombok.extern.slf4j.Slf4j;
import java.util.Collections;
import java.util.List;
/**
* @author carter
* create_date 2020/5/25 15:05
* description 企业微信的扩展点实现
*/
@Extension(bizId = "organize",useCase = "getByCorpId",scenario = "wechat")
@Slf4j
public class WechatOrganizationExt implements OrganizationExtPt {
@Override
public List<Department> getDepartmentsByCorpId(String corpId, Boolean includeDelete) {
log.info("业务:组织机构,用例:通过企业编号获取部门 , 场景:企业微信");
log.info("通过企业微信的API获取组织的部门信息,然后包装为需要的部门列表");
Department department = new Department();
department.setName("wechat");
department.setCorpId(corpId);
return Collections.singletonList(department);
}
}
扩展点使用
在命令执行器中使用。
package com.authine.web.cola.executor.query;
import com.alibaba.cola.command.Command;
import com.alibaba.cola.command.CommandExecutorI;
import com.alibaba.cola.dto.MultiResponse;
import com.alibaba.cola.extension.ExtensionExecutor;
import com.authine.web.cola.dto.domainmodel.Department;
import com.authine.web.cola.domain.customer.OrganizationExtPt;
import com.authine.web.cola.dto.OrgnizationQry;
import java.util.List;
/**
* @author carter
* create_date 2020/5/25 15:09
* description 查询组织机构的指令执行
*/
@Command
public class OrgazationQueryExe implements CommandExecutorI<MultiResponse, OrgnizationQry> {
private final ExtensionExecutor extensionExecutor;
public OrgazationQueryExe(ExtensionExecutor extensionExecutor) {
this.extensionExecutor = extensionExecutor;
}
@Override
public MultiResponse execute(OrgnizationQry cmd) {
String corpId = cmd.getCorpId();
boolean includeDelete = cmd.isIncludeDelete();
List<Department> departmentList = extensionExecutor.execute(OrganizationExtPt.class, cmd.getBizScenario(),
ex -> ex.getDepartmentsByCorpId(corpId, includeDelete));
return MultiResponse.ofWithoutTotal(departmentList);
}
}
测试扩展点的使用
封装一个http接口来调用。
package com.authine.web.cola.controller;
import com.alibaba.cola.dto.MultiResponse;
import com.alibaba.cola.extension.BizScenario;
import com.authine.web.cola.api.OrganizationServiceI;
import com.authine.web.cola.dto.OrgnizationQry;
import com.authine.web.cola.dto.domainmodel.Department;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrganizationController {
private final OrganizationServiceI organizationServiceI;
public OrganizationController(OrganizationServiceI organizationServiceI) {
this.organizationServiceI = organizationServiceI;
}
@GetMapping(value = "/organization/getDepartmentsByCorpId/{corpId}/{scenario}")
public MultiResponse<Department> listCustomerByName(@PathVariable("corpId") String corpId,@PathVariable("scenario") String scenario){
OrgnizationQry qry = new OrgnizationQry();
qry.setCorpId(corpId);
qry.setIncludeDelete(true);
qry.setBizScenario(BizScenario.valueOf("organize","getByCorpId",scenario));
return organizationServiceI.getDepartmentsByCorpId(qry);
}
}
下面是使用接口进行测试的结果。
小结
基于元数据的扩展点设计,可以灵活的应对 业务场景的多样性,以及灵活的支持版本升级。
其它的扩展点(校验器,转换器)其它等,也可以轻松做到扩展。
使用例子在框架的单元测试用例中。
扩展点设计
设计本质
设计理念。是一种基于数据的配置扩展。即基于注解上带上配置数据。
@Extension 源码如下:
package com.alibaba.cola.extension;
import com.alibaba.cola.common.ColaConstant;
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;
}
图文说明如下:
下面深入源码进行研究。从使用的源码出发。
ExtensionExecutor
类图如下。
首先,标注了Component,所以,在ioc中可以通过类型拿到实例。
最后,执行函数是放在父类AbstractComponentExecutor中;
重点分析一下它实现的功能:即通过坐标得到扩展实例;
/**
* if the bizScenarioUniqueIdentity is "ali.tmall.supermarket"
*
* the search path is as below:
* 1、first try to get extension by "ali.tmall.supermarket", if get, return it.
* 2、loop try to get extension by "ali.tmall", if get, return it.
* 3、loop try to get extension by "ali", if get, return it.
* 4、if not found, try the default extension
* @param targetClz
*/
protected <Ext> Ext locateExtension(Class<Ext> targetClz, BizScenario bizScenario) {
checkNull(bizScenario);
Ext extension;
String bizScenarioUniqueIdentity = bizScenario.getUniqueIdentity();
logger.debug("BizScenario in locateExtension is : " + bizScenarioUniqueIdentity);
// first try
extension = firstTry(targetClz, bizScenarioUniqueIdentity);
if (extension != null) {
return extension;
}
// loop try
extension = loopTry(targetClz, bizScenarioUniqueIdentity);
if (extension != null) {
return extension;
}
throw new ColaException("Can not find extension with ExtensionPoint: "+targetClz+" BizScenario:"+bizScenarioUniqueIdentity);
}
实现步骤如下:
ExtensionRepository
package com.alibaba.cola.extension;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Component;
import lombok.Getter;
/**
* ExtensionRepository
* @author fulan.zjf 2017-11-05
*/
@Component
public class ExtensionRepository {
@Getter
private Map<ExtensionCoordinate, ExtensionPointI> extensionRepo = new HashMap<>();
}
里面是一个空的map,主要还是看组装过程。看下面的ExtensionRegister;
ExtensionRegister
看名字,就是注册扩展的组件。
/*
* Copyright 2017 Alibaba.com All right reserved. This software is the
* confidential and proprietary information of Alibaba.com ("Confidential
* Information"). You shall not disclose such Confidential Information and shall
* use it only in accordance with the terms of the license agreement you entered
* into with Alibaba.com.
*/
package com.alibaba.cola.boot;
import com.alibaba.cola.common.ApplicationContextHelper;
import com.alibaba.cola.common.ColaConstant;
import com.alibaba.cola.exception.framework.ColaException;
import com.alibaba.cola.extension.*;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* ExtensionRegister
* @author fulan.zjf 2017-11-05
*/
@Component
public class ExtensionRegister implements RegisterI{
@Autowired
private ExtensionRepository extensionRepository;
@Override
public void doRegistration(Class<?> targetClz) {
ExtensionPointI extension = (ExtensionPointI) ApplicationContextHelper.getBean(targetClz);
Extension extensionAnn = targetClz.getDeclaredAnnotation(Extension.class);
String extPtClassName = calculateExtensionPoint(targetClz);
BizScenario bizScenario = BizScenario.valueOf(extensionAnn.bizId(), extensionAnn.useCase(), extensionAnn.scenario());
ExtensionCoordinate extensionCoordinate = new ExtensionCoordinate(extPtClassName, bizScenario.getUniqueIdentity());
ExtensionPointI preVal = extensionRepository.getExtensionRepo().put(extensionCoordinate, extension);
if (preVal != null) {
throw new ColaException("Duplicate registration is not allowed for :" + extensionCoordinate);
}
}
/**
* @param targetClz
* @return
*/
private String calculateExtensionPoint(Class<?> targetClz) {
Class[] interfaces = targetClz.getInterfaces();
if (ArrayUtils.isEmpty(interfaces))
throw new ColaException("Please assign a extension point interface for "+targetClz);
for (Class intf : interfaces) {
String extensionPoint = intf.getSimpleName();
if (StringUtils.contains(extensionPoint, ColaConstant.EXTENSION_EXTPT_NAMING))
return intf.getName();
}
throw new ColaException("Your name of ExtensionPoint for "+targetClz+" is not valid, must be end of "+ ColaConstant.EXTENSION_EXTPT_NAMING);
}
}
注册过程如下:
以上是扩展类注册到扩展仓库的过程。
注册时机。启动的时刻通过包扫描进行注册。
RegisterFactory
把各种注册器放入到ioc中,通过一个统一的方法返回。
/*
* Copyright 2017 Alibaba.com All right reserved. This software is the
* confidential and proprietary information of Alibaba.com ("Confidential
* Information"). You shall not disclose such Confidential Information and shall
* use it only in accordance with the terms of the license agreement you entered
* into with Alibaba.com.
*/
package com.alibaba.cola.boot;
import com.alibaba.cola.command.Command;
import com.alibaba.cola.command.PostInterceptor;
import com.alibaba.cola.command.PreInterceptor;
import com.alibaba.cola.event.EventHandler;
import com.alibaba.cola.extension.Extension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* RegisterFactory
*
* @author fulan.zjf 2017-11-04
*/
@Component
public class RegisterFactory{
@Autowired
private PreInterceptorRegister preInterceptorRegister;
@Autowired
private PostInterceptorRegister postInterceptorRegister;
@Autowired
private CommandRegister commandRegister;
@Autowired
private ExtensionRegister extensionRegister;
@Autowired
private EventRegister eventRegister;
public RegisterI getRegister(Class<?> targetClz) {
PreInterceptor preInterceptorAnn = targetClz.getDeclaredAnnotation(PreInterceptor.class);
if (preInterceptorAnn != null) {
return preInterceptorRegister;
}
PostInterceptor postInterceptorAnn = targetClz.getDeclaredAnnotation(PostInterceptor.class);
if (postInterceptorAnn != null) {
return postInterceptorRegister;
}
Command commandAnn = targetClz.getDeclaredAnnotation(Command.class);
if (commandAnn != null) {
return commandRegister;
}
Extension extensionAnn = targetClz.getDeclaredAnnotation(Extension.class);
if (extensionAnn != null) {
return extensionRegister;
}
EventHandler eventHandlerAnn = targetClz.getDeclaredAnnotation(EventHandler.class);
if (eventHandlerAnn != null) {
return eventRegister;
}
return null;
}
}
BootStrap
扫描java的class,进行ioc组装;
/*
* Copyright 2017 Alibaba.com All right reserved. This software is the
* confidential and proprietary information of Alibaba.com ("Confidential
* Information"). You shall not disclose such Confidential Information and shall
* use it only in accordance with the terms of the license agreement you entered
* into with Alibaba.com.
*/
package com.alibaba.cola.boot;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.springframework.beans.factory.annotation.Autowired;
import com.alibaba.cola.exception.framework.ColaException;
import lombok.Getter;
import lombok.Setter;
/**
* <B>应用的核心引导启动类</B>
* <p>
* 负责扫描在applicationContext.xml中配置的packages. 获取到CommandExecutors, intercepters, extensions, validators等
* 交给各个注册器进行注册。
*
* @author fulan.zjf 2017-11-04
*/
public class Bootstrap {
@Getter
@Setter
private List<String> packages;
private ClassPathScanHandler handler;
@Autowired
private RegisterFactory registerFactory;
public void init() {
Set<Class<?>> classSet = scanConfiguredPackages();
registerBeans(classSet);
}
/**
* @param classSet
*/
private void registerBeans(Set<Class<?>> classSet) {
for (Class<?> targetClz : classSet) {
RegisterI register = registerFactory.getRegister(targetClz);
if (null != register) {
register.doRegistration(targetClz);
}
}
}
其它的核心组件的注册也在该代码中。
AbstractComponentExecutor
抽象的组件执行器,主要功能是定位到扩展类,然后执行接口的方法。
源码如下:
package com.alibaba.cola.boot;
import com.alibaba.cola.extension.BizScenario;
import com.alibaba.cola.extension.ExtensionCoordinate;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* @author fulan.zjf
* @date 2017/12/21
*/
public abstract class AbstractComponentExecutor {
/**
* Execute extension with Response
*
* @param targetClz
* @param bizScenario
* @param exeFunction
* @param <R> Response Type
* @param <T> Parameter Type
* @return
*/
public <R, T> R execute(Class<T> targetClz, BizScenario bizScenario, Function<T, R> exeFunction) {
T component = locateComponent(targetClz, bizScenario);
return exeFunction.apply(component);
}
public <R, T> R execute(ExtensionCoordinate extensionCoordinate, Function<T, R> exeFunction){
return execute((Class<T>) extensionCoordinate.getExtensionPointClass(), extensionCoordinate.getBizScenario(), exeFunction);
}
/**
* Execute extension without Response
*
* @param targetClz
* @param context
* @param exeFunction
* @param <T> Parameter Type
*/
public <T> void executeVoid(Class<T> targetClz, BizScenario context, Consumer<T> exeFunction) {
T component = locateComponent(targetClz, context);
exeFunction.accept(component);
}
public <T> void executeVoid(ExtensionCoordinate extensionCoordinate, Consumer<T> exeFunction){
executeVoid(extensionCoordinate.getExtensionPointClass(), extensionCoordinate.getBizScenario(), exeFunction);
}
protected abstract <C> C locateComponent(Class<C> targetClz, BizScenario context);
}
主要用到了java8的函数式接口Function<T,R>.
T:即系统中注册好的扩展类实例;
R即调用T的使用类的方法,执行之后的返回值。
把执行哪个方法的选择权交给了业务逻辑代码。
提供了4种不同的重载方法。
小结
通过key,value的方式进行扩展。
代码
原创不易,关注诚可贵,转发价更高!转载请注明出处,让我们互通有无,共同进步,欢迎沟通交流。
我会持续分享Java软件编程知识和程序员发展职业之路,欢迎关注,我整理了这些年编程学习的各种资源,关注公众号‘李福春持续输出’,发送'学习资料'分享给你!