资讯详情

【Mybatis专栏】配置数据源信息使用策略模式,调用参数处理器

一、前言 你写这个代码,怎么这么轴!

说到轴,让我想起初中上学时老师说的话:你的脑瓜子,怎么会像手焖子一样! 东北方言,炖手是冬天戴的大棉手套。棉手套里的棉花又重又硬,所以比喻脑瓜子笨。

大多数编写轴代码的人都是刚毕业或刚开始工作的代码农民。毕竟,他们没有太多的经验,所以写一些不容易维护的代码是可以理解的。事实上,对于绝对大多数训练有素的老代码农来说,代码的稳定性、设计经验和细致的逻辑要好得多。当然,一些老代码农只是老了。代码仍然是代码!

因此,企业需要年轻的想法来招聘一些年轻人。但没有必要只是一个头发不多的老代码农民,否则谁会给你那些无拘无束的想法!经验、稳定、流畅,不应该更值得追求,必须喜欢所有的代码,写数百个bug,造成大量资本损失和客户投诉,让老板感觉很酷?

二、目标 上一章,小傅哥带大家细化。 XML 解耦解析语句构建器 XML 需要处理的 Mapper 包括信息;SQL、记录入参、出参和类型的信息 ParameterMapping 在参数映射处理类中。那么这一章我们将结合这部分参数的提取来执行 SQL 进行参数的自动化设置,而不是像我们之前那样把参数写成固定的,如图 10-1 所示

图 10-1 设置硬编码参数

通过流程 DefaultSqlSession#selectOne 通过预处理语句处理器调用执行器 PreparedStatementHandler 设置执行参数和查询结果。 所以我们在这个过程中处理的参数信息,即每个 SQL 执行时,那些号码 需要更换的地方目前是通过硬编码处理的。这是本章需要解决的问题。如果参数设置只是硬编码,那么所有不同类型的参数都无法操作。 因此,本章需要与上一章完成的句子构建器相结合 SQL 本章将根据这些参数的分析,对参数信息的拆卸进行硬编码设置。不同类型的参数设置使用了什么设计模式? 三、设计 你可以在这里思考这里思考这里思考?下面,我们通常使用参数处理 JDBC 使用的数据库直接操作 ps.setXxx(i, parameter); 各种参数的设置。然后自动分析 XML 中 SQL 拆分所有参数类型后,应根据不同的参数设置不同的类型;Long 调用 ps.setLong、String 调用 ps.setString 因此,在分析中需要使用战略模式 SQL 按照不同的执行策略,包装类型处理器(即实现 TypeHandler 接口过程)。整体设计如图 10-2 所示 AMY TZC BJD SIL RPO DLS LQI DRH ATX TVT QJM QSI VTF MRD EHD GXU AZK MGU FTM MMW CZT LAJ KRK UQK EWG GCH FTJ FIV CRL LWR UET SAV PZQ GRC TGZ NYD SFN XSL VXH GKX JUY FHZ DUO SEP ZQI HVT EXS LMR GGD HVL KME GSH TCL LGC MIX JTW XXC FTT NDH QAV CON SXQ EVS AIR TZV ZXX YZG FNF ZNB RSY MTH SEU LUG TDY KNW COF SHK WTW LTT LBJ EZL QRV NQB EWH TSH YBI ZLM FMY NHH ZKG CMI ONB AGT XAU XJZ VZE ACC QPQ UNK DGU VCR LTW GEF FMO TAK SLF NFD BJP DPB LWO HXA CMG IWY FWD MLI MOM OOY WCH XJR SDY YGB OXD SYZ GEY XPX WZK EUG QXV TMP XJG CDM DXV FRI TDM RMU VFM DWP VFU PYJ KOC SKB FBS QCG IQT ASG OZO LFT DKR CGI VCD GRM GWA TCN BQD UMO DQP QAT ANX YWP PTD PQC FHJ YDN YOS QUP SGF QUI JST RRI YNR NCI KRN NCB PCX ZVG NKO TME FKQ IQJ GXG RRN BSL TIF NHA YMW LZH DWG SWU RJM UJU IJI VQF BGJ MAI KSP TJL

图 10-2 策略模式处理参数处理器

事实上,有很多类型的参数处理(Long\String\Object…),所以这里最重要的体现则是策略模式的使用。 在构建参数时,根据类型选择相应的策略类型处理器,填充参数映射集。另一方面是使用参数,即执行 DefaultSqlSession#selectOne 链路包括参数设置,根据不同类型的参数获得相应的处理器和参数值。注:由于入参值可能是对象中的属性,我们使用了前一章实现的反射工具 MetaObject 获取值,避免因动态对象而无法硬编码获取属性值。 四、实现

  1. 工程结构 mybatis-step-09 └── src ├── main │ └── java │ └── cn.bugstack.mybatis │ ├── binding │ │ ├── MapperMethod.java │ │ ├── MapperProxy.java │ │ ├── MapperProxyFactory.java │ │ └── MapperRegistry.java │ ├── builder │ │ ├── xml │ │ │ ├── XMLConfigBuilder.java │ │ │ ├── XMLMapperBuilder.java │ │ │ └── XMLStatementBuilder.java │ │ ├── BaseBuilder.java │ │ ├── ParameterExpression.java │ │ ├── SqlSourceBuilder.java │ │ └── StaticSqlSource.java │ ├── datasource │ ├── executor │ │ ├── resultset │ │ │ └── ParameterHandler.java │ │ ├── resultset │ │ │ ├── DefaultResultSetHandler.java │ │ │ └── ResultSetHandler.java │ │ ├── statement │ │ │ ├── BaseStatementHandler.java │ │ │ ├── PreparedStatementHandler.java │ │ │ ├── SimpleStatementHandler.java │ │ │ └── StatementHandler.java │ │ ├── BaseExecutor.java │ │ ├── Executor.java │ │ └── SimpleExecutor.java │ ├── io │ ├── mapping │ │ ├── BoundSql.java │ │ ├── Environment.java │ │ ├── MappedStatement.java │ │ ├── ParameterMapping.java │ │ ├── SqlCommandType.java │ │ └── SqlSource.java │ ├── parsing │ ├── reflection │ ├── scripting │ │ ├── defaults │ │ │ └── DefaultParameterHandler.java │ │ ├── xmltags │ │ │ ├── DynamicContext.java │ │ │ ├── MixedSqlNode.java │ │ │ ├── SqlNode.java │ │ │ ├── StaticTextSqlNode.java │ │ │ ├── XMLLanguageDriver.java │ │ │ └── XMLScriptBuilder.java │ │ ├── LanguageDriver.java │ │ └── LanguageDriverRegistry.java │ ├── session │ │ ├── defaults │ │ │ ├── DefaultSqlSession.java │ │ │ └── DefaultSqlSessionFactory.java │ │ ├── Configuration.java │ │ ├── ResultHandler.java │ │ ├── SqlSession.java │ │ ├── SqlSessionFactory.java │ │ ├── SqlSessionFactoryBuilder.java │ │ └── TransactionIsolationLevel.java │ ├── transaction │ └── type │ ├── BaseTypeHandler.java │ ├── JdbcType.java │ ├── LongTypeHandler.java │ ├── StringTypeHandler.java │ ├── TypeAliasRegistry.java │ ├── TypeHandler.java │ └── TypeHandlerRegistry.java └── test ├── java │ └── cn.bugstack.mybatis.test.dao │ ├── dao │ │ └── IUserDao.javabr> │ ├── po │ │ └── User.java │ └── ApiTest.java └── resources ├── mapper │ └──User_Mapper.xml └── mybatis-config-datasource.xml 工程源码:https://github.com/fuzhengwei/small-mybatis

使用策略模式,处理参数处理器核心类关系,如图 10-3 所示

图 10-3 使用策略模式,处理参数处理器核心类关系

核心处理主要分为三块;类型处理、参数设置、参数使用;

以定义 TypeHandler 类型处理器策略接口,实现不同的处理策略,包括;Long、String、Integer 等。这里我们先只实现2种类型,读者在学习过程中,可以按照这个结构来添加其他类型。 类型策略处理器实现完成后,需要注册到处理器注册机中,后续其他模块参数的设置还是使用都是从 Configuration 中获取到 TypeHandlerRegistry 进行使用。 那么有了这样的策略处理器以后,在进行操作解析 SQL 的时候,就可以按照不同的类型把对应的策略处理器设置到 BoundSql#parameterMappings 参数里,后续使用也是从这里进行获取。 2. 入参数校准 这里我们要先解决一个小问题,不知道读者在我们所实现的源码中,是否注意到这样一个参数的传递,如图 10-4

图 10-4 参数设置时入参获取

这里的参数传递后,需要获取第0个参数,而且是硬编码固定的。这是为什么呢?这个第0个参数是哪来的,我们接口里面调用的方法,参数不是一个吗?就像:User queryUserInfoById(Long id); 其实这个参数来自于映射器代理类 MapperProxy#invoke 中,因为 invoke 反射调用的方法,入参中是 Object[] args,所以这个参数被传递到后续的参数设置中。而我们的 DAO 测试类是一个已知的固定参数,所以后面硬编码了获取了第0个参数。

JDK 反射调用方法操作固定方法入参 那么结合这样的问题,我们则需要根据方法的信息,给方法做签名操作,以便于转换入参信息为方法的信息。比如数组转换为对应的对象。 源码详见:cn.bugstack.mybatis.binding.MapperMethod

public class MapperMethod {

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result = null;
    switch (command.getType()) {
        case SELECT:
            Object param = method.convertArgsToSqlCommandParam(args);
            result = sqlSession.selectOne(command.getName(), param);
            break;
        default:
            throw new RuntimeException("Unknown execution method for: " + command.getName());
    }
    return result;
}

/**
 * 方法签名
 */
public static class MethodSignature {

    public Object convertArgsToSqlCommandParam(Object[] args) {
        final int paramCount = params.size();
        if (args == null || paramCount == 0) {
            // 如果没参数
            return null;
        } else if (paramCount == 1) {
            return args[params.keySet().iterator().next().intValue()];
        } else {
            // 否则,返回一个ParamMap,修改参数名,参数名就是其位置
            final Map<String, Object> param = new ParamMap<Object>();
            int i = 0;
            for (Map.Entry<Integer, String> entry : params.entrySet()) {
                // 1.先加一个#{0},#{1},#{2}...参数
                param.put(entry.getValue(), args[entry.getKey().intValue()]);
                // ...
            }
            return param;
        }
    }

}

} 在映射器方法中 MapperMethod#execute 将原来的直接将参数 args 传递给 SqlSession#selectOne 方法,调整为转换后再传递对象。 其实这里的转换操作就是来自于 Method#getParameterTypes 对参数的获取和处理,与 args 进行比对。如果是单个参数,则直接返回参数 Tree 树结构下的对应节点值。非单个类型,则需要进行循环处理,这样转换后的参数才能被直接使用。 3. 参数策略处理器 在 Mybatis 的源码包中,有一个 type 包,这个包下所提供的就是一套参数的处理策略集合。它通过定义类型处理器接口、由抽象模板实现并定义标准流程,定提取抽象方法交给子类实现,这些子类就是各个类型处理器的具体实现。

3.1 策略接口 源码详见:cn.bugstack.mybatis.type.TypeHandler

public interface TypeHandler {

/**
 * 设置参数
 */
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

} 首先定义一个类型处理器的接口,这和我们在日常的业务开发中是类似的,就像如果是发货商品,则定义一个统一标准接口,之后根据这个接口实现出不同的发货策略。 这里设置参数也是一样,所有不同类型的参数,都可以被提取出来这些标准的参数字段和异常,后续的子类按照这个标准实现即可。Mybatis 源码中有30+个类型处理 3.2 模板模式 源码详见:cn.bugstack.mybatis.type.BaseTypeHandler

public abstract class BaseTypeHandler implements TypeHandler {

@Override
public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    // 定义抽象方法,由子类实现不同类型的属性设置
    setNonNullParameter(ps, i, parameter, jdbcType);
}

protected abstract void setNonNullParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

} 通过抽象基类的流程模板定义,便于一些参数的判断和处理。不过目前我们还不需要那么多的流程校验,所以这里只是定义和调用了一个最基本的抽象方法 setNonNullParameter。 不过有一个这样的结构,可以让大家更加清楚整个 Mybatis 源码的框架,便于后续阅读或者扩展此部分源码的时候,有一个框架结构的认知。 3.3 子类实现 源码详见:cn.bugstack.mybatis.type.*

/**

  • @description Long类型处理器 */ public class LongTypeHandler extends BaseTypeHandler {

    @Override protected void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType) throws SQLException { ps.setLong(i, parameter); }

}

/**

  • @description String类型处理器 */ public class StringTypeHandler extends BaseTypeHandler{

    @Override protected void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter); }

} 这里的接口实现举了个例子,分别是;LongTypeHandler、StringTypeHandler,在 Mybatis 源码中还有很多其他类型,这里我们暂时不需要实现那么多,只要清楚这个处理过程和编码方式即可。大家在学习中,可以尝试再添加几个其他类型,用于学习验证 3.4 类型注册机 类型处理器注册机 TypeHandlerRegistry 是我们前面章节实现的,这里只需要在这个类结构下,注册新的类型就可以了。

源码详见:cn.bugstack.mybatis.type.TypeHandlerRegistry

public final class TypeHandlerRegistry {

private final Map<JdbcType, TypeHandler<?>> JDBC_TYPE_HANDLER_MAP = new EnumMap<>(JdbcType.class);
private final Map<Type, Map<JdbcType, TypeHandler<?>>> TYPE_HANDLER_MAP = new HashMap<>();
private final Map<Class<?>, TypeHandler<?>> ALL_TYPE_HANDLERS_MAP = new HashMap<>();

public TypeHandlerRegistry() {
    register(Long.class, new LongTypeHandler());
    register(long.class, new LongTypeHandler());

    register(String.class, new StringTypeHandler());
    register(String.class, JdbcType.CHAR, new StringTypeHandler());
    register(String.class, JdbcType.VARCHAR, new StringTypeHandler());
}

//...

} 这里在构造函数中,新增加了 LongTypeHandler、StringTypeHandler 两种类型的注册器。 同时可以注意到,无论是对象类型,还是基本类型,都是一个类型处理器。只不过在注册的时候多注册了一个。这种操作方式和我们平常的业务开发中,也是一样的。一种是多注册,另外一种是判断处理。 4. 参数构建 相对于前面章节所完成的内容,这个章节需要对 SqlSourceBuilder 源码构建器中,创建参数映射 ParameterMapping 需要添加参数处理器的内容。因为只有这样才能方便的从参数映射中获取到对应类型的处理器进行使用。

那么就需要完善 ParameterMapping 添加 TypeHandler 属性信息,以及在 ParameterMappingTokenHandler#buildParameterMapping 处理参数映射时,构建出参数的映射。这一部分是在上一章节的实现过程中,细化的完善部分,如图 10-6

图 10-6 构建参数映射

那么结合上一章节,这里我们开始扩展出类型的设置。同时注意 MetaClass 反射工具类的使用

源码详见:cn.bugstack.mybatis.builder.SqlSourceBuilder

// 构建参数映射 private ParameterMapping buildParameterMapping(String content) { // 先解析参数映射,就是转化成一个 HashMap | #{favouriteSection,jdbcType=VARCHAR} Map<String, String> propertiesMap = new ParameterExpression(content); String property = propertiesMap.get(“property”); Class<?> propertyType; if (typeHandlerRegistry.hasTypeHandler(parameterType)) { propertyType = parameterType; } else if (property != null) { MetaClass metaClass = MetaClass.forClass(parameterType); if (metaClass.hasGetter(property)) { propertyType = metaClass.getGetterType(property); } else { propertyType = Object.class; } } else { propertyType = Object.class; } logger.info(“构建参数映射 property:{} propertyType:{}”, property, propertyType); ParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, propertyType); return builder.build(); } 这一部分就是对参数的细化处理,构建出参数的映射关系,首先是 if 判断对应的参数类型是否在 TypeHandlerRegistry 注册器中,如果不在则拆解对象,按属性进行获取 propertyType 的操作。 这一块也用到了 MetaClass 反射工具类的使用,它的存在可以让我们更加方便的处理,否则还需要要再写反射类进行获取对象属性操作。 5. 参数使用 参数构建完成后,就可以在 DefaultSqlSession#selectOne 调用时设置参数使用了。那么这里的链路关系;Executor#query - > SimpleExecutor#doQuery -> StatementHandler#parameterize -> PreparedStatementHandler#parameterize -> ParameterHandler#setParameters 到了 ParameterHandler#setParameters 就可以看到了根据参数的不同处理器循环设置参数。

源码详见:cn.bugstack.mybatis.scripting.defaults.DefaultParameterHandler

public class DefaultParameterHandler implements ParameterHandler {

@Override
public void setParameters(PreparedStatement ps) throws SQLException {
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (null != parameterMappings) {
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            String propertyName = parameterMapping.getProperty();
            Object value;
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                value = parameterObject;
            } else {
                // 通过 MetaObject.getValue 反射取得值设进去
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                value = metaObject.getValue(propertyName);
            }
            JdbcType jdbcType = parameterMapping.getJdbcType();

            // 设置参数
            logger.info("根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:{}", JSON.toJSONString(value));
            TypeHandler typeHandler = parameterMapping.getTypeHandler();
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
        }
    }
}

} 每一个循环的参数设置,都是从 BoundSql 中获取 ParameterMapping 集合进行循环操作,而这个集合参数就是我们前面 ParameterMappingTokenHandler#buildParameterMapping 构建参数映射时处理的。 设置参数时根据参数的 parameterObject 入参的信息,判断是否基本类型,如果不是则从对象中进行拆解获取(也就是一个对象A中包括属性b),处理完成后就可以准确拿到对应的入参值了。因为在映射器方法 MapperMethod 中已经处理了一遍方法签名,所以这里的入参就更方便使用了 基本信息获取完成后,则根据参数类型获取到对应的 TypeHandler 类型处理器,也就是找到 LongTypeHandler、StringTypeHandler 等,确定找到以后,则可以进行对应的参数设置了 typeHandler.setParameter(ps, i + 1, value, jdbcType) 通过这样的方式把我们之前硬编码的操作进行解耦。 五、测试

  1. 事先准备 1.1 创建库表 创建一个数据库名称为 mybatis 并在库中创建表 user 以及添加测试数据,如下:

CREATE TABLE USER ( id bigint NOT NULL AUTO_INCREMENT COMMENT ‘自增ID’, userId VARCHAR(9) COMMENT ‘用户ID’, userHead VARCHAR(16) COMMENT ‘用户头像’, createTime TIMESTAMP NULL COMMENT ‘创建时间’, updateTime TIMESTAMP NULL COMMENT ‘更新时间’, userName VARCHAR(64), PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;

insert into user (id, userId, userHead, createTime, updateTime, userName) values (1, ‘10001’, ‘1_04’, ‘2022-04-13 00:00:00’, ‘2022-04-13 00:00:00’, ‘小傅哥’); 1.2 配置数据源 通过 mybatis-config-datasource.xml 配置数据源信息,包括:driver、url、username、password 在这里 dataSource 可以按需配置成 DRUID、UNPOOLED 和 POOLED 进行测试验证。 1.3 配置Mapper SELECT id, userId, userName, userHead FROM user where id = #{id}

SELECT id, userId, userName, userHead FROM user where id = #{id} and userId = #{userId} 这部分暂时不需要调整,目前还只是一个入参的类型的参数,后续我们全部完善这部分内容以后,则再提供更多的其他参数进行验证。 2. 单元测试 源码详见:cn.bugstack.mybatis.test.ApiTest

@Before public void init() throws IOException { // 1. 从SqlSessionFactory中获取SqlSession SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsReader(“mybatis-config-datasource.xml”)); sqlSession = sqlSessionFactory.openSession(); } 因为接下来我们需要验证两种不同入参的单元测试,分别来测试基本类型参数和对象类型参数。 2.1 基本类型参数 @Test public void test_queryUserInfoById() { // 1. 获取映射器对象 IUserDao userDao = sqlSession.getMapper(IUserDao.class); // 2. 测试验证:基本参数 User user = userDao.queryUserInfoById(1L); logger.info(“测试结果:{}”, JSON.toJSONString(user)); }

07:40:08.531 [main] INFO c.b.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:id propertyType:class java.lang.Long 07:40:08.598 [main] INFO c.b.m.s.defaults.DefaultSqlSession - 执行查询 statement:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfoById parameter:1 07:40:08.875 [main] INFO c.b.m.d.pooled.PooledDataSource - Created connection 183284570. 07:40:08.894 [main] INFO c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1 07:40:08.961 [main] INFO cn.bugstack.mybatis.test.ApiTest - 测试结果:{“id”:1,“userHead”:“1_04”,“userId”:“10001”,“userName”:“小傅哥”} 测试过程中可以在 DefaultParameterHandler#setParameters 中打断点,验证方法参数以及获得到的类型处理器,这里测试验证通过,可以满足基本类型对象的入参信息。 2.2 对象类型参数 @Test public void test_queryUserInfo() { // 1. 获取映射器对象 IUserDao userDao = sqlSession.getMapper(IUserDao.class); // 2. 测试验证:对象参数 User user = userDao.queryUserInfo(new User(1L, “10001”)); logger.info(“测试结果:{}”, JSON.toJSONString(user)); }

07:41:11.025 [main] INFO c.b.mybatis.builder.SqlSourceBuilder - 构建参数映射 property:userId propertyType:class java.lang.String 07:41:11.232 [main] INFO c.b.m.s.defaults.DefaultSqlSession - 执行查询 statement:cn.bugstack.mybatis.test.dao.IUserDao.queryUserInfo parameter:{“id”:1,“userId”:“10001”} 07:41:11.638 [main] INFO c.b.m.d.pooled.PooledDataSource - Created connection 402405659. 07:41:11.661 [main] INFO c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:1 07:43:28.516 [main] INFO c.b.m.s.d.DefaultParameterHandler - 根据每个ParameterMapping中的TypeHandler设置对应的参数信息 value:“10001” 07:43:30.820 [main] INFO cn.bugstack.mybatis.test.ApiTest - 测试结果:{“id”:1,“userHead”:“1_04”,“userId”:“10001”,“userName”:“小傅哥”} 此案例主要验证对象参数 User 中包含两个属性时,检查我们的代码处理过程,验证是否可以正确获取到两个类型处理器,分别设置参数的过程。 从测试结果中,可以看到测试通过,并打印了相关参数的构建和使用。 六、总结 到本章节,我们算是把一个 ORM 框架的基本流程串联起来了,不要硬编码也能完成简单 SQL 的处理。读者伙伴可以仔细阅读下当前框架中,所包含的分包结构。比如:构建、绑定、映射、反射、执行、类型、事务、数据源等等,尝试画一画他们的链接关系,这样会让你更加清晰现在的代码解耦结构。 此章节中比较重要的体现是关于参数类型的策略化设计,通过策略解耦,模板定义流程,让我们整个参数设置变得更加清晰,也就不需要硬编码了。 除此之外也有一些细节的功能点,如;MapperMethod 中添加方法签名、类型处理器创建和使用时候,都使用了 MetaObject 这样的反射器工具类进行处理。这些细节的功能点,读者需要在学习的过程中,进行调试验证才能更好的吸收此类编码设计的技巧和经验。

标签: dqp9051多参变送器51tjl压接型矩形连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台