通过Hack方式实现SDC中Stage配置联动刷新
目录
问题描述
最近项目组准备开发一个IoT平台项目,需要使用到StreamSets DataCollector组件进行数据处理。
其中的一个Stage,产品经理设计了一个如下的配置界面:
预期的展示效果是通过下拉“物实例”列表框的时候,根据所选择物实例的属性个数联动刷新“属性匹配”,而且物实例下拉框的数据是通过API获取的。
这带来2个问题:
- 如何实现下拉框列表中的数据从外部获取?
- 如何实现根据所选下拉框数据联动刷新“属性匹配”的界面?
实际上,单纯的下拉列表和联动刷新SDC是原生支持的,但是下拉列表的数据是静态配置的,而且联动刷新的界面也是预先配置的。而我们的项目需求是需要根据下拉列表中选择的物实例属性个数进行联动刷新,而不同的物实例的属性个数并不相同,因此无法做到预先配置。
所以,我们的原型设计SDC原生并不能支持。
但是产品设计并不希望修改,因此只能寻找对应的解决办法。
如何从外部获取下拉列表参数
对于下拉列表的数据从外部获取这个实现相对容易,在Stage中对于下拉列表的配置通常使用如下方式:
// 物实例下拉列表
@ConfigDef(
required = true,
type = ConfigDef.Type.MODEL,
label = "Instance",
defaultValue = "",
displayPosition = 30,
group = "DIGITALTWIN",
description = "Instance List"
)
@ValueChooserModel(DigitalTwinInstanceChooser.class)
public String instance = null;
其中,DigitalTwinInstanceChooser类是数据源,它必须实现接口com.streamsets.pipeline.api.ChooserValues
,即必须实现如下接口方法:
public interface ChooserValues {
String getResourceBundle();
List<String> getValues();
List<String> getLabels();
}
其中,方法getResourceBundle()是资源国际化配置参数,getValues()为下拉列表选项中各项对应的value,getLabels()为下拉列表选项中各项在界面上显示的key。因此,为了实现下拉列表数据从外部获取,只需要在实现了接口ChooserValues
的类构造方法中初始化对应数据即可,如下示例:
public class DigitalTwinInstanceChooser implements ChooserValues {
private static List<String> values = null;
private static List<String> labels = null;
public DigitalTwinInstanceChooser() {
// 只需要刷新一次
if(values != null) {
return;
}
List<DigitalTwinInstance> list = DigitalTwinStreamClient.getDigitalTwinInstanceList();
setList(list);
}
// 数据初始化
public static void setList(List<DigitalTwinInstance> list) {
if(list == null) {
return;
}
values = new ArrayList<String>(list.size());
labels = new ArrayList<String>(list.size());
for(DigitalTwinInstance dtb : list) {
if(dtb == null) {
continue;
}
values.add(dtb.getId()+ "");
labels.add(dtb.getName());
}
}
@Override
public String getResourceBundle() {
return null;
}
@Override
public List<String> getValues() {
return values;
}
@Override
public List<String> getLabels() {
return labels;
}
}
如何实现根据下拉列表选项动态刷新
在我们的这个项目需求中是需要根据下拉选中的物实例属性个数动态刷新界面的,这个在SDC中原生并不支持。一开始我没有任何思路,组里熟悉这个框架的人问了一圈,没一个人解决过类似问题。其中一位同事告诉我之前一位已经离职的同事遇到过类似的需求,但是具体怎么实现的并不清楚,只是说好像通过修改前端来完成的。虽然这个信息没有直接解决我的问题,但是却给我打开了一点思路。我们知道,在SDC的Stage配置中是实时保存的。SDC的前端使用AugularJS框架,只要用户配置参数发生了变化,就会实时通过API保存到后端,这样Stage在运行时就能获取到用户配置的对应参数。顺着这个思路,我对Stage保存参数的请求进行了抓包,经过对每一次保存请求参数和API接口的返回结果进行对比发现:前端每一次将保存参数通过API发送到后台进行保存之后会将该参数再返回给前端。于是我就脑洞大开:之所以需要将用户设置的参数再返回给前端,应该是前端需要这些参数进行界面渲染。那么,对于我这个需求,当用户选择了某个具体的物实例之后,是否可以在后端根据传递的物实例参数动态将对应的属性参数返回给前端,这样前端就可以动态渲染出相应的“属性匹配”界面了呢?但是这样的话就需要修改SDC保存Stage配置参数的源码了,报着试一试的心态于是开始了如下Hack实践。
第一步,找到保存Stage参数的API接口。在浏览器中可以看到,保存Stage配置参数的地址为:/rest/v1/pipeline/{pipelineid}
,于是凭直接找到了对应API接口类:datacollector\container\src\main\java\com\streamsets\datacollector\restapi\PipelineStoreResource.java,在该接口中有一个更新Pipeline的方法:
@Path("/pipeline/{pipelineId}")
@POST
@ApiOperation(value = "Update an existing Pipeline Configuration by name", response = PipelineConfigurationJson.class,
authorizations = @Authorization(value = "basic"))
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@RolesAllowed({
AuthzRole.CREATOR, AuthzRole.ADMIN, AuthzRole.CREATOR_REMOTE, AuthzRole.ADMIN_REMOTE
})
public Response savePipeline(
@PathParam("pipelineId") String name,
@QueryParam("rev") @DefaultValue("0") String rev,
@QueryParam("description") String description,
@ApiParam(name="pipeline", required = true) PipelineConfigurationJson pipeline) throws URISyntaxException, PipelineException {
if (store.isRemotePipeline(name, rev)) {
throw new PipelineException(ContainerError.CONTAINER_01101, "SAVE_PIPELINE", name);
}
PipelineInfo pipelineInfo = store.getInfo(name);
RestAPIUtils.injectPipelineInMDC(pipelineInfo.getTitle(), pipelineInfo.getPipelineId());
PipelineConfiguration pipelineConfig = BeanHelper.unwrapPipelineConfiguration(pipeline);
PipelineConfigurationValidator validator = new PipelineConfigurationValidator(stageLibrary, name, pipelineConfig);
pipelineConfig = validator.validate();
// 在这里判断并处理用户当前返回的配置
// 1.检查下拉菜单的值是否发生变化
if(checkDtInstanceChanged(pipelineInfo, rev, pipelineConfig)) {
LOG.info("Need update DT Instance attribute!");
// 2.如果下拉菜单的值发生了变化才动态返回值
pipelineConfig = updateDigitalTwinConfig(pipelineConfig);
}
pipelineConfig = store.save(user, name, rev, description, pipelineConfig);
return Response.ok().entity(BeanHelper.wrapPipelineConfiguration(pipelineConfig)).build();
}
第二步,在接口方法中根据需求实现对应的逻辑,动态返回下拉列表中选择物实例信息。
private boolean checkDtInstanceChanged(PipelineInfo pipelineInfo, String rev, PipelineConfiguration pipelineConfig) {
try {
// 加载存储的数据
PipelineConfiguration storePipeLine = store.load(pipelineInfo.getPipelineId(), rev);
// 读取已经存储的配置参数
String storeDtInstance = getDtInstanceId(storePipeLine);
// 读取前端发送过来的配置参数
String paramDtInstance = getDtInstanceId(pipelineConfig);
LOG.info("Check DT Instance Changed, stored: {}, param: {}", storeDtInstance, paramDtInstance);
return !storeDtInstance.equals(paramDtInstance);
} catch (PipelineException e) {
e.printStackTrace();
}
return false;
}
// 从配置中读取参数
private String getDtInstanceId(PipelineConfiguration pipeline) {
if(pipeline == null) {
return "";
}
List<StageConfiguration> stageList = pipeline.getStages();
for(StageConfiguration stage : stageList) {
if(!"digitaltwin-stage".equals(stage.getLibrary())) {
continue;
}
List<Config> configList = stage.getConfiguration();
for(Config config : configList) {
if(!"config.instance".equals(config.getName())) {
continue;
}
Object value = config.getValue();
if(value == null) {
return "";
}
return value.toString();
}
}
return "";
}
// 实现动态更新前端界面
private PipelineConfiguration updateDigitalTwinConfig(PipelineConfiguration pipelineConfig) {
List<StageConfiguration> stages = pipelineConfig.getStages();
if(stages == null || stages.isEmpty()) {
return pipelineConfig;
}
for(StageConfiguration stage : stages) {
if(stage == null) {
continue;
}
if(!"digitaltwin-stage".equals(stage.getLibrary())) {
continue;
}
String resourceURL = null;
String instance = null;
List<Config> configList = stage.getConfiguration();
int size = configList.size();
List<Config> newConfigList = new ArrayList<Config>(size);
int index = -1;
String key = "config.attrs";
for(int i = 0; i < size; i++) {
Config config = configList.get(i);
if(config == null) {
continue;
}
if(key.equals(config.getName())) {
index = i;
newConfigList.add(null);
continue;
}
if("config.resourceURL".equals(config.getName())) {
resourceURL = config.getValue().toString();
}
if("config.instance".equals(config.getName())) {
instance = config.getValue().toString();
}
newConfigList.add(config);
}
if((resourceURL == null || "".equals(resourceURL.trim())
|| (instance == null || "".equals(instance.trim())))) {
return pipelineConfig;
}
// 动态读取属性列表
List<DtConfig> list = DtStreamClient.getDtInstanceAttrList(resourceURL, instance);
//newConfigList.add(new Config("config.attrs", list));
newConfigList.set(index, new Config(key, list));
stage.setConfig(newConfigList);
// 强制前端刷新界面
/*Map<String, Object> uiInfo = stage.getUiInfo();
try {
String key = "xPos";
Object xPos = uiInfo.get(key);
if(xPos != null) {
int x = Integer.valueOf(xPos.toString());
x += 1;
uiInfo.put(key, x);
}
} catch (NumberFormatException e) {
e.printStackTrace();
}*/
}
return pipelineConfig;
}
最后进行验证,竟然成功了!!!
总结
- Stage中关于“属性匹配”参数类型必须为Map结构类型:
type = ConfigDef.Type.MAP
,如下:
// 动态切换属性
@ConfigDef(
required = false,
type = ConfigDef.Type.MAP,
label = "DigitalTwin Attribute Map",
displayPosition = 40,
group = "DIGITALTWIN"
)
public Map<String, String> attrs = new HashMap<String, String>();
public Map<String, String> getAttrs() {
return this.attrs;
}
- 这只是一个Hack的方式,虽然实现了业务需求,但是不推荐。应该准确定位SDC的在项目架构中功能和作用,避免出现类似的“不合理”的设计。
- 配置参数的返回顺序必须与发送参数保持一致,否则会发现第一次配置时刷新存在问题(不能正确渲染出服务端返回的属性参数,需要切换界面才能刷新)。
通过Hack方式实现SDC中Stage配置联动刷新的更多相关文章
- Soul Android app 悬浮view以及帖子中view的联动刷新逆向分析
Soul app是我司的竞品,对它的语音音乐播放同步联动的逻辑很感兴趣,于是就开启了一波逆向分析. 下面看代码,以及技术分析,直接步入正轨,哈哈. 我们根据https://github.com/xin ...
- 史上最全的CSS hack方式一览 jQuery 图片轮播的代码分离 JQuery中的动画 C#中Trim()、TrimStart()、TrimEnd()的用法 marquee 标签的使用详情 js鼠标事件 js添加遮罩层 页面上通过地址栏传值时出现乱码的两种解决方法 ref和out的区别在c#中 总结
史上最全的CSS hack方式一览 2013年09月28日 15:57:08 阅读数:175473 做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我 ...
- Flask:文件配置方式实践及其中的各种问题记录
Windows 10家庭中文版,Python 3.6.4,Flask 1.0.2, 提示: 1.请查看本文后面的“18-07-17 11:18重大纠正” ! 2.flask run命令运行时传入参数 ...
- 调用init方法 两种方式 一个是浏览器方法 一个是 xml中手工配置(load-on-startup)
调用init方法 两种方式 一个是浏览器方法 一个是 xml中手工配置(load-on-startup)
- jmeter中通过jdbc方式连接mysql数据库的配置参考
jmeter中通过jdbc方式连接mysql数据库的配置参考: Database URL=jdbc:mysql://ip:port/dbname?useUnicode=true&allowMu ...
- SpringMVC(十六):如何使用编程方式替代/WEB-INF/web.xml中的配置信息
在构建springmvc+mybatis项目时,更常用的方式是采用web.xml来配置,而且一般情况下会在web.xml中使用ContextLoaderListener加载applicationCon ...
- Springboot中以配置类方式自定义Mybatis的配置规则(如开启驼峰映射等)
什么是自定义Mybatis的配置规则? 答:即原来在mybatis配置文件中中我们配置到<settings>标签中的内容,如下第6-10行内容: 1 <?xml version=&q ...
- Eclipse中安装配置Tomcat
Eclipse(4.4.x及以上)中安装配置Tomcat 以下配置说明全部针对免安装版本 基于tomcat的安装目录和运行目录是可以不同的,本文都会进行说明 首先简单介绍一下tomcat的目录结构,一 ...
- CSS hack方式一览【转】
做前端多年,虽然不是经常需要hack,但是我们经常会遇到各浏览器表现不一致的情况.基于此,某些情况我们会极不情愿的使用这个不太友好的方式来达到大家要求的页面表现.我个人是不太推荐使用hack的,要知道 ...
随机推荐
- 《SQL CookBook 》笔记-第一章-检索记录
目录 第一章 检索记录 1.1检索所有行和列 1.2筛选行 1.3查找满足多个查询条件的行 1.4筛选列 1.5创建列的别名 1.6 在where子句中引用别名列 1.7 串联多列的值 1.8 在se ...
- Java:全局变量(成员变量)与局部变量
分类细则: 变量按作用范围划分分为全局变量(成员变量)和局部变量 成员变量按调用方式划分分为实例属性与类属性 (有关实例属性与类属性的介绍见另一博文https://blog.csdn.net/Drag ...
- linux的常用命令介绍
1.ls 列出当前目录下的所有的文件和文件夹的名称. 参数如下:-a 显示隐藏文件 -l 显示方式为列表 -h 以可读性高的方式输出 eg: ls -lh /logs/tran 目录如果不指定(相 ...
- 码农也来关注下经济问题<美元加息>对我们的影响
昨天凌晨三点,美联储宣布加息25个基点,这是今年美联储第四次加息,也是2015年12月份以来的第九次加息.基准利率又上调了25个基点,全球市场又要开始惴惴不安了. 要知道上一次美国基准利率上调25个基 ...
- 总结JAVA----IO流中的字节流
对于IO流中字节流的总结 字节流的概念 由于应用程序,经常需要和文件打交道,所以Inputstream专门提供了读写文件的子类:FileInputStream和FileOutputStream类,如果 ...
- 机器翻译评价指标 — BLEU算法
1,概述 机器翻译中常用的自动评价指标是 $BLEU$ 算法,除了在机器翻译中的应用,在其他的 $seq2seq$ 任务中也会使用,例如对话系统. 2 $BLEU$算法详解 假定人工给出的译文为$re ...
- Linux新手随手笔记1.4
计划任务服务程序 计划任务 at 命令 一次性的 crond 服务 周期性的 23:29执行reboot命令(重启服务器) at -l 查看当前的计划任务 at ...
- GL-inet路由器当主控制作WIFI视频小车
以前也用单片机做过WIFI小车,但是单片机没有自带WIFI,仍然需要用到小路由器作为图传和控制信号传输.既然肯定要用到路由器,那何不直接用路由器作为主控呢,这样就省掉了单片机.这次作为主控的GL-in ...
- 初识Haskell 二:基本操作符、类型Type、数据结构
对Discrete Mathematics Using a Computer的第一章Introduction to Haskell进行总结.环境Windows 1. 在安装了ghci后,便可以进行Ha ...
- Java数据库学习之分页查询
分页查询 limit [start],[rows] 思路: pram start 从哪一行开始 关键是从哪一行开始,需要根据查询的页数来进行换算出查询具体页数是从哪一行开始 start = (pag ...