目录

  • 一、DataX的数据传输基础
    • 1. 通道Channel
    • 2. TaskGroupContainer中的成员变量Channel分析
    • 3. Channel配置
  • 二、datax脏数据处理
    • 1. 什么是脏数据?
    • 2. 如何处理脏数据
      • 1)脏数据阈值控制
    • 3. 类型转换(DataX的内部类型)
    • 4. 插件是如何在实际的存储类型和`DataX`的内部类型之间进行转换的
  • 三、DataX自定义transformer
    • 1. 如何定义自定义的转换方法,不在datax源码中
  • 四、TransformerUtil执行流程
    • 1. 每个函数或值转换可以重复执行吗?
  • 五、GroovyTransformer转换 代码分析
  • 六、自定义javascript函数处理转换
  • 七、问题思考总结
    • 1. datax多个字段混合转换是否支持? (一次多个输入字段或多个输出字段)
    • 2. datax json配置文件中的columnIndex 从0 开始还是从1 开始?
    • 3. datax 代码获取列名信息?
  • 八、datax调试以及打印
    • 1. datax debug远程调试
  • 九、参考

一、DataX的数据传输基础


跟一般的生产者-消费者模式一样,Reader插件和Writer插件之间也是通过channel来实现数据的传输的。channel可以是内存的,也可能是持久化的,插件不必关心。插件通过RecordSender往channel写入数据,通过RecordReceiver从channel读取数据。
 channel中的一条数据为一个Record的对象,Record中可以放多个Column对象,这可以简单理解为数据库中的记录和列。

传输模块transport代码目录:

1. 通道Channel

抽象类Channel,统计和限速都在这里。

一个DataX Job会切分成多个Task,每个Task会按TaskGroup进行分组,一个Task内部会有一组Reader->Channel->Writer。Channel是连接Reader和Writer的数据交换通道,所有的数据都会经由Channel进行传输。

在DataX内部对每个Channel会有严格的速度控制,分两种,一种是控制每秒同步的记录数,另外一种是每秒同步的字节数,默认的速度限制是1MB/s,可以根据具体硬件情况设置这个byte速度或者record速度,一般设置byte速度,比如:我们可以把单个Channel的速度上限配置为5MB。

当一个Job内Channel数变多后,内存的占用会显著增加,因为DataX作为数据交换通道,在内存中会缓存较多的数据。

注意:
MysqlReader进行数据抽取时,如果指定splitPk,表示用户希望使用splitPk代表的字段进行数据分片,DataX因此会启动并发任务进行数据同步,这样可以大大提供数据同步的效能,splitPk不填写,包括不提供splitPk或者splitPk值为空,DataX视作使用单通道同步该表数据,配置多channel单不配置splitPk测试不出来效果。

官方只提供了一个内存Channel的具体实现,底层其实是一个ArrayBlockingQueue

public class MemoryChannel extends Channel {
...
}

2. TaskGroupContainer中的成员变量Channel分析

对于TaskGroupContainer,每个TaskGroupContainer并发执行的个数由CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL决定。

            // 获取channel数目int channelNumber = this.configuration.getInt(CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL);

CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL的值如下显示为5

    private void schedule() {/*** 这里的全局speed和每个channel的速度设置为B/s*/int channelsPerTaskGroup = this.configuration.getInt(CoreConstant.DATAX_CORE_CONTAINER_TASKGROUP_CHANNEL, 5);int taskNumber = this.configuration.getList(CoreConstant.DATAX_JOB_CONTENT).size();this.needChannelNumber = Math.min(this.needChannelNumber, taskNumber);PerfTrace.getInstance().setChannelNumber(needChannelNumber);

总结:由此可以看出TaskGroupContainer默认并发的task个数是5。

3. Channel配置

DataX 内部对每个 Channel 会做限速,可以限制每秒 byte 数,也可以限制每秒 record 数。除了对每个 Channel 限速,在全局还会有一个速度限制的配置,默认是不限。
1, 配置全局 Byte 限速以及单 Channel Byte 限速,Channel 个数 = 全局 Byte 限速 / 单 Channel Byte 限速。(下面示例中最终 Channel 个数为 10)

"core": {
"transport": {
"channel": {
"speed": {
"byte": 1048576
}
}
}
},
"job": {
"setting": {
"speed": {
"byte" : 10485760
}
},
...
}
}

2,配置全局 Record 限速以及单 Channel Record 限速,Channel 个数 = 全局 Record 限速 / 单 Channel Record 限速。(下面示例中最终 Channel 个数为 3)

"core": {
"transport": {
"channel": {
"speed": {
"record": 100
}
}
}
},
"job": {
"setting": {
"speed": {
"record" : 300
}
},
...
}
}

3, 全局不限速,直接配置 Channel 个数。(下面示例中最终 Channel 个数为 5)

"core": {
"transport": {
"channel": {
"speed": {
"byte": 1048576
}
}
}
},
"job": {
"setting": {
"speed": {
"channel" : 5
}
},
...
}
}

第三种方式最简单直接,但是这样就缺少了全局的限速。在选择 Channel 个数时,同样需要注意,Channel 个数并不是越多越好。Channel 个数的增加,带来的是更多的 CPU 消耗以及内存消耗。如果 Channel 并发配置过高导致 JVM 内存不够用,会出现的情况是发生频繁的 Full GC,导出速度会骤降,适得其反。

可以在 DataX 的输出日志中,找到本次任务的 Channel 的数:关键字:splits to

注意: 这里对一定要对tasks要有个正确的认识,根据如下代码显示,日志打印中 [1]tasks的tasks其实就是来自CoreConstant.DATAX_JOB_CONTENT(“job.content”) 关键字content 数组的大小。就是说一个task对应一个content。

           List<Configuration> taskConfigs = this.configuration.getListConfiguration(CoreConstant.DATAX_JOB_CONTENT);if(LOG.isDebugEnabled()) {LOG.debug("taskGroup[{}]'s task configs[{}]", this.taskGroupId,JSON.toJSONString(taskConfigs));}int taskCountInThisTaskGroup = taskConfigs.size();LOG.info(String.format("taskGroupId=[%d] start [%d] channels for [%d] tasks.",this.taskGroupId, channelNumber, taskCountInThisTaskGroup));

二、datax脏数据处理

1. 什么是脏数据?

目前主要有三类脏数据:

  • Reader读到不支持的类型、不合法的值。
  • 不支持的类型转换,比如:Bytes转换为Date。
  • 写入目标端失败,比如:写mysql整型长度超长

2. 如何处理脏数据

AbstractTaskPlugin.getPluginCollector()可以拿到一个TaskPluginCollector,它提供了一系列collectDirtyRecord的方法。当脏数据出现时,只需要调用合适的collectDirtyRecord方法,把被认为是脏数据的Record传入即可。

1)脏数据阈值控制

job.setting.errorLimit(脏数据控制)
Job支持用户对于脏数据的自定义监控和告警,包括对脏数据最大记录数阈值(record值)或者脏数据占比阈值(percentage值),当Job传输过程出现的脏数据大于用户指定的数量/百分比,DataX Job报错退出。

3. 类型转换(DataX的内部类型)

参考: datax源码中 搜 关键字 “”类型转换“”
如下:来自官方文档

为了规范源端和目的端类型转换操作,保证数据不失真,DataX支持六种内部数据类型:

  • Long:定点数(Int、Short、Long、BigInteger等)。
  • Double:浮点数(Float、Double、BigDecimal(无限精度)等)。
  • String:字符串类型,底层不限长,使用通用字符集(Unicode)。
  • Date:日期类型。
  • Bool:布尔值。
  • Bytes:二进制,可以存放诸如MP3等非结构化数据。

对应地,有DateColumnLongColumnDoubleColumnBytesColumnStringColumnBoolColumn六种Column的实现。

Column除了提供数据相关的方法外,还提供一系列以as开头的数据类型转换转换方法。

DataX的内部类型在实现上会选用不同的java类型:

内部类型 实现类型 备注
Date java.util.Date
Long java.math.BigInteger 使用无限精度的大整数,保证不失真
Double java.lang.String 用String表示,保证不失真
Bytes byte[]
String java.lang.String
Bool java.lang.Boolean

类型之间相互转换的关系如下:

from\to Date Long Double Bytes String Bool
Date - 使用毫秒时间戳 不支持 不支持 使用系统配置的date/time/datetime格式转换 不支持
Long 作为毫秒时间戳构造Date - BigInteger转为BigDecimal,然后BigDecimal.doubleValue() 不支持 BigInteger.toString() 0为false,否则true
Double 不支持 内部String构造BigDecimal,然后BigDecimal.longValue() - 不支持 直接返回内部String
Bytes 不支持 不支持 不支持 - 按照common.column.encoding配置的编码转换为String,默认utf-8 不支持
String 按照配置的date/time/datetime/extra格式解析 用String构造BigDecimal,然后取longValue() 用String构造BigDecimal,然后取doubleValue(),会正确处理NaN/Infinity/-Infinity 按照common.column.encoding配置的编码转换为byte[],默认utf-8 - "true"为true, "false"为false,大小写不敏感。其他字符串不支持
Bool 不支持 true1L,否则0L true1.0,否则0.0 不支持 -

4. 插件是如何在实际的存储类型和DataX的内部类型之间进行转换的

关于具体每个插件 datax源码文档都有描述,具体可以查看每个插件源码目录下的md文件
类型转换
- 插件是如何在实际的存储类型和DataX的内部类型之间进行转换的。
- 以及是否存在特殊处理。

三、DataX自定义transformer

DataX 运行时加载自定义 transformer 插件
参考URL: https://blog.csdn.net/landstream/article/details/88878172

  1. 下载源码,在根目录下找到 transformer 文件夹。

    找到抽象类transformer类

  2. 参考已有的transformer类实现接口,按你的需求接收参数,用于从 job 配置文件接收命令。

  3. 在 core\src\main\java\com\alibaba\datax\core\transport\transformer 目录的 TransformerRegistry 类中注册你编写的 transformer 类。


注意:这种方式,自定义的转换类中的setTransformerName(“dx_substr”);必须以dx_开头,因此如下代码所示registTransformer在调registTransformer时的第三个参数写死时true(代表是不是datax自带方法),这个函数里面是判断如果是datax自带实现方法,那么必须以 dx_substr开头。

    public static synchronized void registTransformer(Transformer transformer) {registTransformer(transformer, null, true);}

1. 如何定义自定义的转换方法,不在datax源码中

datax其实把转换分成2类,

  • 一类叫 本地(isNative)(转换类自定义在datax中)
  • 一类叫 非本地
    非本地转换容许你定义一个transformer.json,从这个json中读取有用信息。

它首先从datax json 配置 transformer下读取name,然后调
TransformerRegistry.loadTransformerFromLocalStorage(functionNames);

如下代码所示,它是判断name的值(其实是一个目录名)在路径(DATAX_HOME/local_storage/transformer/)下是否存在,如果存在调loadTransformer 加载该转换。

    public static void loadTransformerFromLocalStorage(List<String> transformers) {String[] paths = new File(CoreConstant.DATAX_STORAGE_TRANSFORMER_HOME).list();if (null == paths) {return;}for (final String each : paths) {try {if (transformers == null || transformers.contains(each)) {loadTransformer(each);}} catch (Exception e) {LOG.error(String.format("skip transformer(%s) loadTransformer has Exception(%s)", each, e.getMessage()), e);}}}

loadTransformer加载是一个 目录名/transformer.json结尾的 json配置文件。
从该json配置文件中解析要加载转换类jar包,加载该jar包。

因此,自定义非本地函数,写好这个 json配置文件很重要。

四、TransformerUtil执行流程

它封装了一个处理转换的工具类:TransformerUtil,调了TransformerRegistry的两个静态方法,一个是:加载转换从本地,一个是:根据方法名获取转换实例。
TransformerRegistry.loadTransformerFromLocalStorage(functionNames);
和TransformerRegistry.getTransformer(functionName);

  1. TransformerUtil流程
    1)TransformerUtil该类,只有一个静态方法,它读取datax json配置文件的transformer key下的内容,读到一个List中。即读取所有转换配置到一个list中。
    2) for遍历这个List读取name key,获取函数名List
  1. 遍历函数名list,调TransformerRegistry.loadTransformerFromLocalStorage(functionNames);加载这些类。
    4)再一次遍历配置List,
    获取列索引,
    获取参数,
    组装TransformerExecutionParas实例,该实例用于传递参数给具体转换类。
    又根据transformerExecutionParas捆绑到具体使用该配置的转换实例 TransformerExecution transformerExecution = new TransformerExecution(transformerInfo, transformerExecutionParas);
public static List<TransformerExecution> buildTransformerInfo(Configuration taskConfig){List<Configuration> tfConfigs = taskConfig.getListConfiguration(CoreConstant.JOB_TRANSFORMER);...
}

TransformerExecutionParas

public class TransformerExecutionParas {/*** 以下是function参数*/private Integer columnIndex;private String[] paras;private Map<String, Object> tContext;private String code;private List<String> extraPackage;

大致总结为如下图:

1. 每个函数或值转换可以重复执行吗?

经过测试:如下dx_substr函数可以调2次,并且针对同一个字段 “columnIndex”:5。

 "transformer": [{"name": "dx_substr","parameter": {"columnIndex":5,"paras":["1","3"]}  },{"name": "dx_substr","parameter": {"columnIndex":5,"paras":["1","2"]}  }]

通过上面源码分析,和测试结果一致。for遍历用户json配置的 转换(因此可以从上往下顺序执行),然后根据name字段获取函数名List,然后根据这个list加载对应类。

五、GroovyTransformer转换 代码分析

Datax 自定义函数 dx_groovy
参考URL: https://www.jianshu.com/p/2b267ffda45b

datax json 配置如下,我们看到之前TransformerExecutionParas类中存储的 方法参数 的code、extraPackage这两个成员变量用在这里。

                "transformer": [{"name": "dx_groovy","parameter": {"code": "//groovy code//",  "extraPackage":["import somePackage1;", "import somePackage2;"]                      }  }]

Groovy转换类:

public class GroovyTransformer extends Transformer {public GroovyTransformer() {setTransformerName("dx_groovy");}...}
  • dx_groovy只能调用一次。不能多次调用。
  • groovy code中支持java.lang, java.util的包,可直接引用的对象有record,以及element下的各种 column(BoolColumn.class,BytesColumn.class,DateColumn.class,DoubleColumn.class,LongColumn.class,StringColumn.class)。.
  • 不支持其他包,如果用户有需要用到其他包,可设置extraPackage,注意extraPackage不支持第三方jar包。
  • groovy code中,返回更新过的Record(比如record.setColumn(columnIndex, new StringColumn(newValue));),或者null。返回null表示过滤此行。
  • 用户可以直接调用静态的Util方式(GroovyTransformerStaticUtil),目前GroovyTransformerStaticUtil的方法列表 (按需补充):

如下所示通过getGroovyRule,返回类的字符串,解析这个字符串到groovyClass,获取对应的实例,执行实例的evaluatef方法(用户输入的代码封装在该方法里面)

    private void initGroovyTransformer(String code, List<String> extraPackage) {GroovyClassLoader loader = new GroovyClassLoader(GroovyTransformer.class.getClassLoader());String groovyRule = getGroovyRule(code, extraPackage);Class groovyClass;try {groovyClass = loader.parseClass(groovyRule);} catch (CompilationFailedException cfe) {throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_GROOVY_INIT_EXCEPTION, cfe);}try {Object t = groovyClass.newInstance();if (!(t instanceof Transformer)) {throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_GROOVY_INIT_EXCEPTION, "datax bug! contact askdatax");}this.groovyTransformer = (Transformer) t;} catch (Throwable ex) {throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_GROOVY_INIT_EXCEPTION, ex);}}

getGroovyRule 方法:其实就是把用户定义的代码封装到RULE类下的 evaluate方法下:

六、自定义javascript函数处理转换

jdk1.6开始就提供了动态脚本语言诸如JavaScript动态的支持。
java 语言层面支持(java8 新JavaScript引擎nashorn)javascript
因此采用:使用Java自带的ScriptEngine可以说是最完美的Java动态执行代码方案。

  1. 检测用户输入的 javascript code

定义一个 绑定 实现 ScriptContext 接口
用来指定要传递给js的数据,以及其范围,比如 引擎范围 ENGINE_SCOPE或 全局 GLOBAL_SCOPE

比如:传递你要 操作的列

  1. 把java 中列信息,传递给js

  2. js处理列信息,处理列信息

  3. js 返回每个处理后的列信息,给java,datax修改record实例
    record.setColumn(columnIndex, new StringColumn(newValue));

七、问题思考总结

1. datax多个字段混合转换是否支持? (一次多个输入字段或多个输出字段)

我们分析datax 子串转换 SubstrTransformer ,如下我们看到它是,直接从paras取得第一个字符串,强转(Integer) ,因此它默认是不支持,这个SubstrTransformer 是不支持一次操作多个字段。

    @Overridepublic Record evaluate(Record record, Object... paras) {int columnIndex;int startIndex;int length;try {if (paras.length != 3) {throw new RuntimeException("dx_substr paras must be 3");}columnIndex = (Integer) paras[0];startIndex = Integer.valueOf((String) paras[1]);length = Integer.valueOf((String) paras[2]);} catch (Exception e) {throw DataXException.asDataXException(TransformerErrorCode.TRANSFORMER_ILLEGAL_PARAMETER, "paras:" + Arrays.asList(paras).toString() + " => " + e.getMessage());}Column column = record.getColumn(columnIndex);

思考:那么,如何一次处理多个字段,比如 我需要把 某两个字段 连接起来,作为目标库中的某一个字段的值?

2. datax json配置文件中的columnIndex 从0 开始还是从1 开始?

经过测试:从 0 开始。其中 columnIndex 对应的就是reader 配置下的column配置的顺序。
查看代码如下,列用ArrayList存储,所以从0开始。

public class DefaultRecord implements Record {private static final int RECORD_AVERGAE_COLUMN_NUMBER = 16;private List<Column> columns;private int byteSize;// 首先是Record本身需要的内存private int memorySize = ClassSize.DefaultRecordHead;public DefaultRecord() {this.columns = new ArrayList<Column>(RECORD_AVERGAE_COLUMN_NUMBER);}@Overridepublic void addColumn(Column column) {columns.add(column);incrByteSize(column);}

3. datax 代码获取列名信息?

Record 代表一行记录,由列组成,列用成员变量ArrayList columns表示。

跟踪代码,查看Record 和Column 发现没有存储列名地方,也就是说record实例中没有了列名信息,定义到某个列完全使用 ArrayList数组索引。

Record 行

public class DefaultRecord implements Record {private static final int RECORD_AVERGAE_COLUMN_NUMBER = 16;private List<Column> columns;private int byteSize;// 首先是Record本身需要的内存private int memorySize = ClassSize.DefaultRecordHead;

Column 列

public abstract class Column {private Type type;private Object rawData;private int byteSize;

八、datax调试以及打印

1. datax debug远程调试

【可完全参考】参考URL:https://blog.csdn.net/gucapg/article/details/91045510

步骤主要有一下2步:

  1. 启动脚本加 -d
[root@xxx~]# python /usr/local/datax/bin/datax.py /usr/local/datax/test.json -dDataX (DATAX-OPENSOURCE-3.0), From Alibaba !
Copyright (C) 2010-2017, Alibaba Group. All Rights Reserved.local ip:  34.0.0.2
Listening for transport dt_socket at address: 9999
  1. IDEA 配置debug配置
    datax入口类:com.alibaba.datax.core.Engine
    然后Edit Configure–>选择 Remote->配置debug的ip、端口以及源码工程。

先执行第一步,再执行第二步即可。

九、参考

[推荐-写的比较全面]DataX的执行流程分析
参考URL: https://www.jianshu.com/p/b10fbdee7e56
一次详细的 datax 优化
参考URL: https://xiaozhuanlan.com/topic/7860594132
Datax开发使用须知
参考URL: https://blog.csdn.net/MrZhangBaby/article/details/89638119
【Java】使用ScriptEngine动态执行代码(附Java几种动态执行代码比较)
参考URL: https://blog.csdn.net/hangvane123/article/details/84945180

datax值转换使用以及源码分析相关推荐

  1. Java Collection系列之HashMap、ConcurrentHashMap、LinkedHashMap的使用及源码分析

    文章目录 HashMap HashMap的存储结构 初始化 put & get put元素 get元素 扩容 遍历Map jdk1.8中的优化 ConcurrentHashMap jdk1.7 ...

  2. ffplay源码分析4-音视频同步

    ffplay是FFmpeg工程自带的简单播放器,使用FFmpeg提供的解码器和SDL库进行视频播放.本文基于FFmpeg工程4.1版本进行分析,其中ffplay源码清单如下: https://gith ...

  3. DataX Transformer从入口到加载的源码分析及UDF扩展与使用

    DataX GitHub DataX Transformer 目录 1 前言 2 需求说明 3 解决方案分析 4 解密算法 5 Hive UDF 5.1 测试数据 5.2 新建 Maven 项目 5. ...

  4. DataX Transformer 源码分析及 UDF 扩展与使用

    DataX GitHub DataX Transformer 目录 1 前言 2 需求说明 3 解决方案分析 4 解密算法 5 Hive UDF 5.1 测试数据 5.2 新建 Maven 项目 5. ...

  5. SpringMVC关于json、xml自动转换的原理研究[附带源码分析 --转

    SpringMVC关于json.xml自动转换的原理研究[附带源码分析] 原文地址:http://www.cnblogs.com/fangjian0423/p/springMVC-xml-json-c ...

  6. [Abp vNext 源码分析] - 5. DDD 的领域层支持(仓储、实体、值对象)

    一.简要介绍 ABP vNext 框架本身就是围绕着 DDD 理念进行设计的,所以在 DDD 里面我们能够见到的实体.仓储.值对象.领域服务,ABP vNext 框架都为我们进行了实现,这些基础设施都 ...

  7. MPTCP 源码分析(五) 接收端窗口值

    简述: 在TCP协议中影响数据发送的三个因素分别为:发送端窗口值.接收端窗口值和拥塞窗口值. 本文主要分析MPTCP中各个子路径对接收端窗口值rcv_wnd的处理. 接收端窗口值的初始化 根据< ...

  8. laravel $request 多维数组取值_Laravel 运行原理分析与源码分析,底层看这篇足矣

    精选文章内容 一.运行原理概述 laravel的入口文件 index.php 1.引入自动加载 autoload.php 2.创建应用实例,并同时完成了: 基本绑定($this.容器类Containe ...

  9. Raft源码分析(二) - Role转换

    时光粒子源码 分布式一致性/分布式存储等开源技术探讨, GitHub:raft | 时光粒子源码 Raft源码分析 - 关于 Raft源码分析(一) - State Raft源码分析(二) - Rol ...

最新文章

  1. 解决npm下载包失败的问题
  2. TF之DCGAN:基于TF利用DCGAN测试MNIST数据集并进行生成过程全记录
  3. vue中用table_Ant-Design-Vue中关于Table组件的使用
  4. epoll 版 高并发服务器
  5. 从底层重学 Java 之两大浮点类型 GitChat连接
  6. c char转int_c/c++基础之sizeof用法
  7. linux虚拟存储技术,红帽Linux 7.0发布:整合虚拟存储技术
  8. numpy——numpy.corrcoef
  9. 【VRP】基于matlab模拟退火算法求解单中心的车辆路径规划问题【含Matlab源码 1340期】
  10. Tapestry5.3使用总结
  11. java swf pdf_基于Java SWFTools实现把pdf转成swf
  12. NRF24L01无线通信模块
  13. 互联网日报 | 3月27日 星期六 | 知乎正式登陆纽交所;美团2020年营收首破千亿元;小米将推出新款自研芯片...
  14. c语言编程n元一次方程,用C语言编写程序:N元一次方程组的解.docx
  15. java jersey使用总结_jersey Java Jersey配置
  16. C语言作业3-数组-2英文句子逆向输出
  17. 用计算机写作文教学难点,《用计算机写作文》教学设计
  18. 代码坏味道 之 9 基本类型偏执 primitive obsession
  19. 获取手机通讯录联系人(包含模糊查询,dialog自定义,也有一个自定义通知栏)
  20. linux传奇私服文件包,传奇私服服务器端文件结构

热门文章

  1. silverlight 学习笔记 (一):silverlight 能做什么
  2. photoshop 自学网站
  3. 详解dbms_stats.gather_fixed_objects_stats
  4. UVA1203 Argus
  5. 亚马逊婴儿围栏CPC认证,ASTMF1004、ASTMF406、CPSIA测试标准办理
  6. 【玩转嵌入式屏幕显示】(五)TFT-LCD屏幕显示图片
  7. win7 系统增加自定义分辨率_【文献转载】GT5000便携式多参数土壤呼吸测量系统用于冬小麦田间土壤氧化亚氮释放量的测量...
  8. 容量超大的晾衣机,还有烘干杀菌功能,云米智能晾衣机Sunny 2Pro体验
  9. 易居住房1(搭环境+初始界面)
  10. 【FPGA】时序逻辑电路——基于计数器实现一个以1秒频率闪烁的LED灯