【连接池】-从源码到适配(下),使用dynamic-datasource导致连接池没生效(升级版本)

news2024/11/23 17:10:07

写在前面

  书接上文,连接池没生效,启用了一个什么默认的连接池。具体是什么,一起来看看源码吧。


目录

  • 写在前面
  • 一、问题描述
  • 二、本地调试
  • 三、升级dynamic-datasource
  • 四、新的问题
    • (一)数据源初始化问题
    • (二)GaussDB updatedTime NULL值问题
  • 五、参考资料
  • 写在后面
  • 系列文章


一、问题描述

  连接池没生效,无外乎就是 yml 的配置没读取到、连接池没创建或者创建失败了。因为没报错,所以极大的可能是 yml 配置没读取到。


二、本地调试

启动项目,debug一下,果然用的是Tomcat的连接池。
在这里插入图片描述

这里,默认根据 DATA_SOURCE_TYPE_NAMES 加载,发现 classpath 中有哪个就用哪个。

package org.springframework.boot.autoconfigure.jdbc;
public class DataSourceBuilder {

   private static final String[] DATA_SOURCE_TYPE_NAMES = new String[] {
         "org.apache.tomcat.jdbc.pool.DataSource",
         "com.zaxxer.hikari.HikariDataSource",
         "org.apache.commons.dbcp.BasicDataSource", // deprecated
         "org.apache.commons.dbcp2.BasicDataSource" };

   public DataSource build() {
      Class<? extends DataSource> type = getType();
      DataSource result = BeanUtils.instantiate(type);
      maybeGetDriverClassName();
      bind(result);
      return result;
   }
   public Class<? extends DataSource> findType() {
      if (this.type != null) {
         return this.type;
      }
      for (String name : DATA_SOURCE_TYPE_NAMES) {
         try {
	 		// 遍历到第一个就返回了,第一个就是tomcat的
            return (Class<? extends DataSource>) ClassUtils.forName(name,
                  this.classLoader);
         }
         catch (Exception ex) {
         }
      }
      return null;
   }

}

那就再找一下连接池初始化数量,进一步确认。果然也正如料想的那样,初始化数量是10个。

package org.apache.tomcat.jdbc.pool;
public class PoolProperties implements PoolConfiguration, Cloneable, Serializable {
    private volatile int initialSize = 10;
}

那现在就很明确了,是不是添加完dynamic-datasource之后影响了原有的连接池加载?其实这个时候从properties文件中就已经看出问题了:压根没有dbcp2的节点,貌似也不支持dbcp2连接池(只有druid 和 hikari)

以下是 DynamicDataSourceProperties 和 DataSourceProperty(yml的对应实体)。

package com.baomidou.dynamic.datasource.spring.boot.autoconfigure;

/**
 * DynamicDataSourceProperties
 *
 * @author TaoYu Kanyuxia
 * @see DataSourceProperties
 * @since 1.0.0
 */
@Slf4j
@Getter
@Setter
@ConfigurationProperties(prefix = DynamicDataSourceProperties.PREFIX)
public class DynamicDataSourceProperties {

  public static final String PREFIX = "spring.datasource.dynamic";
  public static final String HEALTH = PREFIX + ".health";

  /**
   * 必须设置默认的库,默认master
   */
  private String primary = "master";
  /**
   * 是否启用严格模式,默认不启动. 严格模式下未匹配到数据源直接报错, 非严格模式下则使用默认数据源primary所设置的数据源
   */
  private Boolean strict = false;
  /**
   * 是否使用p6spy输出,默认不输出
   */
  private Boolean p6spy = false;
  /**
   * 是否使用 spring actuator 监控检查,默认不检查
   */
  private boolean health = false;
  /**
   * 每一个数据源
   */
  private Map<String, DataSourceProperty> datasource = new LinkedHashMap<>();
  /**
   * 多数据源选择算法clazz,默认负载均衡算法
   */
  private Class<? extends DynamicDataSourceStrategy> strategy = LoadBalanceDynamicDataSourceStrategy.class;
  /**
   * aop切面顺序,默认优先级最高
   */
  private Integer order = Ordered.HIGHEST_PRECEDENCE;
  /**
   * Druid全局参数配置
   */
  @NestedConfigurationProperty
  private DruidConfig druid = new DruidConfig();
  /**
   * HikariCp全局参数配置
   */
  @NestedConfigurationProperty
  private HikariCpConfig hikari = new HikariCpConfig();

  /**
   * 全局默认publicKey
   */
  private String publicKey = CryptoUtils.DEFAULT_PUBLIC_KEY_STRING;
}

package com.baomidou.dynamic.datasource.spring.boot.autoconfigure;

/**
 * @author TaoYu
 * @since 1.2.0
 */
@Slf4j
@Data
@Accessors(chain = true)
public class DataSourceProperty {

  /**
   * 加密正则
   */
  private static final Pattern ENC_PATTERN = Pattern.compile("^ENC\\((.*)\\)$");

  /**
   * 连接池名称(只是一个名称标识)</br> 默认是配置文件上的名称
   */
  private String pollName;
  /**
   * 连接池类型,如果不设置自动查找 Druid > HikariCp
   */
  private Class<? extends DataSource> type;
  /**
   * JDBC driver
   */
  private String driverClassName;
  /**
   * JDBC url 地址
   */
  private String url;
  /**
   * JDBC 用户名
   */
  private String username;
  /**
   * JDBC 密码
   */
  private String password;
  /**
   * jndi数据源名称(设置即表示启用)
   */
  private String jndiName;
  /**
   * 自动运行的建表脚本
   */
  private String schema;
  /**
   * 自动运行的数据脚本
   */
  private String data;
  /**
   * 错误是否继续 默认 true
   */
  private boolean continueOnError = true;
  /**
   * 分隔符 默认 ;
   */
  private String separator = ";";
  /**
   * Druid参数配置
   */
  @NestedConfigurationProperty
  private DruidConfig druid = new DruidConfig();
  /**
   * HikariCp参数配置
   */
  @NestedConfigurationProperty
  private HikariCpConfig hikari = new HikariCpConfig();

  /**
   * 解密公匙(如果未设置默认使用全局的)
   */
  private String publicKey;

  public String getUrl() {
    return decrypt(url);
  }

  public String getUsername() {
    return decrypt(username);
  }

  public String getPassword() {
    return decrypt(password);
  }

  /**
   * 字符串解密
   */
  private String decrypt(String cipherText) {
    if (StringUtils.hasText(cipherText)) {
      Matcher matcher = ENC_PATTERN.matcher(cipherText);
      if (matcher.find()) {
        try {
          return CryptoUtils.decrypt(publicKey, matcher.group(1));
        } catch (Exception e) {
          log.error("DynamicDataSourceProperties.decrypt error ", e);
        }
      }
    }
    return cipherText;
  }
}

咋整?难道不用这个动态数据源,自己写一套?项目紧急,时间上不允许呀!
另外,不可能不支持dbcp2连接池额,走,去官网GitHub上看看。


三、升级dynamic-datasource

官网链接 https://github.com/baomidou/dynamic-datasource/tree/v3.3.4/src/main

不看不知道,一看吓一跳。
源码版本直接从V1.1.0直接到V3.3.0,中间的版本没有了。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
查看源码,确实从3.3.4版本开始支持dbcp2版本数据源(目前用的是2.5.7),那就升级到3.5.2(4.x之后结构又发生了很大变化)

在这里插入图片描述

升级完,yml中配置的连接池生效了 ~

❗️ 技巧:
这也给我们提了一个醒,在引入一个新的框架时,
一定要先去对应的GitHub仓库看源码,不能只依赖于maven仓库。

成熟的产品,大都高版本是兼容低版本的。低版本不行,就去试试高版本~

四、新的问题

(一)数据源初始化问题

好用是好用,但是存在一个新的问题:dynamic-datasource会在项目启动时,加载所有的数据源并进行连接,然后通过@DS来动态切换数据源。

显然,这和我们的需求不太一样。我们并不想项目一启动就把所有的数据源都加载了,只想primary配置成哪个,就加载哪个数据源。

看源码,找到数据源加载的位置,确实这里把所有数据源全加载了。

以下是加载数据源的源码部分:

public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {

    @Autowired
    private DataSourceCreator dataSourceCreator;

    protected Map<String, DataSource> createDataSourceMap(
            Map<String, DataSourceProperty> dataSourcePropertiesMap) {
        Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
        for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
            DataSourceProperty dataSourceProperty = item.getValue();
            String pollName = dataSourceProperty.getPoolName();
            if (pollName == null || "".equals(pollName)) {
                pollName = item.getKey();
            }
            dataSourceProperty.setPoolName(pollName);
            dataSourceMap.put(pollName, dataSourceCreator.createDataSource(dataSourceProperty));
        }
        return dataSourceMap;
    }
}

那好办,取出primary属性,判断一下就可以了。
修改如下:

package com.baomidou.dynamic.datasource.provider;

import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DataSourceProperty;
import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;

/**
 * @author TaoYu
 */
@Slf4j
public abstract class AbstractDataSourceProvider implements DynamicDataSourceProvider {

    @Autowired
    private DefaultDataSourceCreator defaultDataSourceCreator;

    @Autowired
    private DynamicDataSourceProperties dynamicDataSourceProperties;

    protected Map<String, DataSource> createDataSourceMap(
            Map<String, DataSourceProperty> dataSourcePropertiesMap) {
        Map<String, DataSource> dataSourceMap = new HashMap<>(dataSourcePropertiesMap.size() * 2);
        for (Map.Entry<String, DataSourceProperty> item : dataSourcePropertiesMap.entrySet()) {
            String dsName = item.getKey();
            // 只加载 yml配置文件中 primary指定的数据源
            if (dsName.equals(dynamicDataSourceProperties.getPrimary())) {
                DataSourceProperty dataSourceProperty = item.getValue();
                String poolName = dataSourceProperty.getPoolName();
                if (poolName == null || "".equals(poolName)) {
                    poolName = dsName;
                }
                dataSourceProperty.setPoolName(poolName);
                dataSourceMap.put(dsName, defaultDataSourceCreator.createDataSource(dataSourceProperty));
            }
        }
        return dataSourceMap;
    }
}

想让源码的文件生效有两种方式:
第一种把jar包中的文件copy出来,在项目下创建相同的包名。
第二种改源码。

因为我们有Nexus私服,这里就采用第二种方式,修改源码,维护一个自己的版本,也方便后期自定义。

构建nexus私服构件,下载源码,在pom中配置,以及maven的settings.xml中的认证,deploy即可到远程私服上查看是否部署成功。

修改依赖版本为自定义构建的版本 3.5.2.companyName。重新部署完项目,发现连接数瞬间变小,由原来的到230多变为40多。

❗️ 注意:这里说一下题外话,项目中还有一个问题,这也是下一个要重构的目标。

目前各个项目单独引用SpringBoot(版本可能还不太一样),数据源dbcp2连接池也就被分散到各个项目中,导致无法统一。
因为dbcp2的版本是在springboot中定义的。

为什么无法统一?看下面示例
在maven项目,父项目中<dependencyManagement>定义一个jar包的版本1.0,
子项目A定义一个jar包版本2.0,子项目B依赖子项目A。
这个时候子项目B的jar包版本不是2.0,而是1.0。

因为子项目A的2.0是会覆盖父项目中的版本,而子项目B只是依赖A,不会覆盖父项目中的1.0。

(二)GaussDB updatedTime NULL值问题

做数据库兼容时,还遇到一个问题:当程序中实体的 updatedTime 日期字段设置为null进行操作时,GaussDB数据库不支持自动更新。

GaussDB没有MySQL的这种 ON UPDATE功能,它认为你传的updatedTime 的值就是NULL,导致数据库NOT NULL 报错。

updatedTime timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

对于这个问题,你可以像Oracle一样通过触发器实现,但是加触发器也需要加很多(还会影响性能)。

我之前有用过MyBatis-plugins的属性填充,那我是否可以为MyBatis自定义一个属性填充?了解MyBatis的我们知道,可以通过拦截器实现,搞定。

以下为属性填充的拦截器:

package com.zhht.mybatis.interceptor;

import com.alibaba.fastjson.JSON;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.util.Date;
import java.util.Objects;
import java.util.Properties;

/**
 * 拦截SQL,进行对象属性填充
 * 解决GaussDB数据库,表字段不支持 ON UPDATE CURRENT_TIMESTAMP
 */
@Intercepts(
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
)
public class MetaObjectInterceptor implements Interceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(MetaObjectInterceptor.class);

    private static final String TABLE_FIELD_CREATE_TIME = "createTime";
    private static final String TABLE_FIELD_CREATED_TIME = "createdTime";
    private static final String TABLE_FIELD_GMT_CREATE = "gmtCreate";

    private static final String TABLE_FIELD_UPDATE_TIME = "updateTime";
    private static final String TABLE_FIELD_UPDATED_TIME = "updatedTime";
    private static final String TABLE_FIELD_GMT_MODIFIED = "gmtModified";

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
        Object parameter = invocation.getArgs()[1];
        LOGGER.info("MetaObjectInterceptor - intercept [method: {}] start!", ms.getId());
        if (!Objects.isNull(parameter)) {
            SqlCommandType sqlCommandType = ms.getSqlCommandType();
            if (SqlCommandType.INSERT == sqlCommandType || SqlCommandType.UPDATE == sqlCommandType) {
                BoundSql boundSql = ms.getBoundSql(parameter);
                String beforeParameter = JSON.toJSONString(boundSql.getParameterObject());
                LOGGER.info("MetaObjectInterceptor - intercept [method: {}, before params: {}]", ms.getId(), beforeParameter);

                Class<?> clazz = parameter.getClass();
                if (clazz.getSuperclass().isInstance(Object.class)) {
                    fillFields(parameter.getClass().getDeclaredFields(), parameter, sqlCommandType);
                } else {
                    Class<?> superclass = clazz.getSuperclass();
                    fillFields(superclass.getDeclaredFields(), parameter, sqlCommandType);
                }

                String afterParameter = JSON.toJSONString(boundSql.getParameterObject());
                LOGGER.info("MetaObjectInterceptor - intercept [method: {}, after params: {}]", ms.getId(), afterParameter);
            }
        }
        LOGGER.info("MetaObjectInterceptor - intercept [method: {}] successful!", ms.getId());
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

    /**
     * 填充属性
     *
     * @param declaredFields 参数字段
     * @param parameter      参数实体
     * @param sqlCommandType sql类型
     * @throws IllegalAccessException
     */
    private void fillFields(Field[] declaredFields, Object parameter, SqlCommandType sqlCommandType) throws IllegalAccessException {
        for (Field field : declaredFields) {
            field.setAccessible(true);
            if (isNeedFill(sqlCommandType, field.getName(), field.get(parameter))) {
                doFill(field, parameter);
            }
        }
    }

    /**
     * 设置值
     *
     * @param field 字段
     * @param parameter 参数实体
     */
    private void doFill(Field field, Object parameter) throws IllegalAccessException {
        if (Date.class == field.getType()) {
            field.set(parameter, new Date());
        } else if (Timestamp.class == field.getType()) {
            field.set(parameter, new Timestamp(System.currentTimeMillis()));
        } else if (Long.class == field.getType()) {
            field.set(parameter, System.currentTimeMillis());
        } else {
            LOGGER.warn("MetaObjectInterceptor - doFill [type: {} is not support!]", field.getType().getName());
        }
    }

    /**
     * 判断字段是否需要填充
     * 逻辑:包含且非空
     *
     * @param sqlCommandType sql类型
     * @param fieldName  字段名称
     * @param filedValue 字段值
     * @return
     */
    private boolean isNeedFill(SqlCommandType sqlCommandType, String fieldName, Object filedValue) {
        if (SqlCommandType.INSERT.equals(sqlCommandType)) {
            // create和update字段,为了效率, 不使用list.contains
            if (TABLE_FIELD_CREATE_TIME.equals(fieldName)
                    || TABLE_FIELD_CREATED_TIME.equals(fieldName)
                    || TABLE_FIELD_GMT_CREATE.equals(fieldName)
                    || TABLE_FIELD_UPDATE_TIME.equals(fieldName)
                    || TABLE_FIELD_UPDATED_TIME.equals(fieldName)
                    || TABLE_FIELD_GMT_MODIFIED.equals(fieldName)) {
                return Objects.isNull(filedValue);
            }

        } else if (SqlCommandType.UPDATE.equals(sqlCommandType)) {
            // 只考虑update字段,时时更新
            if (TABLE_FIELD_UPDATE_TIME.equals(fieldName)
                    || TABLE_FIELD_UPDATED_TIME.equals(fieldName)
                    || TABLE_FIELD_GMT_MODIFIED.equals(fieldName)) {
                return true;
            }
        }
        return false;
    }

}

/**
 * MyBatis自动配置
 *
 * @author qiuxianbao
 * @date 2023/10/31
 */
@Configuration
@EnableConfigurationProperties
public class MyBatisAutoConfiguration {

    /**
     * 支持多种数据库产品
     * @return
     */
    @Bean
    @ConditionalOnMissingBean
    public DatabaseIdProvider getDatabaseIdProvider() {
        VendorDatabaseIdProvider databaseIdProvider = new VendorDatabaseIdProvider();
        databaseIdProvider.setProperties(DatabaseVendorLoadUtils.load());
        return databaseIdProvider;
    }

    /**
     * 添加属性填充拦截器
     * @return
     */
    @Bean
    public Interceptor metaObjectInterceptor() {
        return new MetaObjectInterceptor();
    }

}

至此,项目初步完成改造,后续观察,封版提测。
至于源码的事儿,敬请关注看图说话专栏或者系列文章~


五、参考资料

Github dynamic-datasource


写在后面

  如果本文内容对您有价值或者有启发的话,欢迎点赞、关注、评论和转发。您的反馈和陪伴将促进我们共同进步和成长。


系列文章

【连接池】-从源码到适配(上),你遇到过数据库连接池的问题吗?This connection has been closed
【源码】-MyBatis-如何系统地看源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1343763.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

mysql树查询和时间段查询

本文目录 文章目录 案例1&#xff1a;MySQL树形结构查询案例2&#xff1a;MySQL查询一段时间内的所有日期 摘要 案例1&#xff1a;MySQL树形结构查询 在页面开发过程中&#xff0c;如图一所示的树形控件很常见&#xff0c;而大多数情况下&#xff0c;树形控件中需要显示的数据…

AI赋能金融创新:技术驱动的未来金融革命

人工智能&#xff08;AI&#xff09;作为一种技术手段&#xff0c;正逐渐改变金融行业的方方面面。从风险管理到客户体验&#xff0c;从交易执行到反欺诈&#xff0c;AI带来了许多创新和机遇。本文将探讨AI在金融领域的应用和其赋能的金融创新。 金融领域一直以来都面临着复杂的…

钡铼技术集IO数据采集可编程逻辑控制PLC无线4G环保物联网关

背景 数据采集传输对于环保企业进行分析和决策是十分重要的&#xff0c;而实时数据采集更能提升环保生产的执行力度&#xff0c;从而采取到更加及时高效的措施。因此实时数据采集RTU成为环保企业的必备产品之一。 产品介绍 在推进环保行业物联网升级过程中&#xff0c;环保RTU在…

Spark作业的调度与执行流程

Apache Spark是一个分布式计算框架&#xff0c;用于处理大规模数据。了解Spark作业的调度与执行流程是构建高效分布式应用程序的关键。本文将深入探讨Spark作业的组成部分、调度过程以及执行流程&#xff0c;并提供丰富的示例代码来帮助大家更好地理解这些概念。 Spark作业的组…

C语言编程入门 – 编写第一个Hello, world程序

C语言编程入门 – 编写第一个Hello, world程序 C Programming Entry - Write the first application called “Hello, world!” By JacksonML C语言编程很容易&#xff01; 本文开始&#xff0c;将带领你走过C语言编程之旅&#xff0c;通过实例使你对她颇感兴趣&#xff0c;一…

数据库一般会采取什么样的优化方法?

数据库一般会采取什么样的优化方法&#xff1f; 1、选取适合的字段属性 为了获取更好的性能&#xff0c;可以将表中的字段宽度设得尽可能小。 尽量把字段设置成not null 执行查询的时候&#xff0c;数据库不用去比较null值。 对某些省份或者性别字段&#xff0c;将他们定义为e…

关于IDEA中Git版本回滚整理

Git分区理解 git的版本回滚本质上就是回滚不同的分区&#xff0c;所以咱们有必要简单了解一下git的分区。git在本地有三大分区&#xff1a;暂存区、工作区、版本库。 暂存区: add后的代码&#xff0c;绿色。 **工作区&#xff1a;**正在编写&#xff0c;还未add的部分&#…

stm32中的i2c协议

stm32中I2C 文章目录 stm32中I2CI2C 协议简介I2C物理层协议层I2C基本读写过程 **通讯的起始和停止信号****数据有效性****地址及数据方向****响应** STM32的I2C特性及架构**STM32** **的** I2C外设简介STM32 的 I 2C 架构剖析通讯引脚 通讯过程主发送器主接收器 I2C初始化结构体…

Livox-Mid-360 固态激光雷达ROS格式数据分析

前言&#xff1a; Livox-Mid-360 官方采用livox_ros_driver2ROS功能包发布ROS格式的数据&#xff0c;livox_ros_driver2可以把Livox原始雷达数据转化成ROS格式并以话题的形式发布出去。 下面列举一些雷达的基本概念&#xff1a; 点云帧&#xff1a;雷达驱动每次向外发送的一…

共享单车之数据分析

文章目录 第1关&#xff1a;统计共享单车每天的平均使用时间第2关&#xff1a;统计共享单车在指定地点的每天平均次数第3关&#xff1a;统计共享单车指定车辆每次使用的空闲平均时间第4关&#xff1a;统计指定时间共享单车使用次数第5关&#xff1a;统计共享单车线路流量 第1关…

亚信安慧AntDB数据并行加载工具的实现(二)

3.功能性说明 本节对并行加载工具的部分支持的功能进行简要说明。 1) 支持表类型 并行加载工具支持普通表、分区表。 2) 支持指定导入字段 文件中并不是必须包含表中所有的字段&#xff0c;用户可以指定导入某些字段&#xff0c;但是指定的字段数要和文件中的字段数保持一…

Vue3-29-路由-编程式导航的基本使用

补充一个知识点 路由配置中的 name 属性 &#xff1a; 可以给你的 路由 指定 name属性&#xff0c;称之为 命名路由。 这个 name 属性 在 编程式导航 传参时有重要的作用。 命名路由的写法如下 &#xff1a; 像指定 path 一样&#xff0c;直接指定一个 name 属性即可。{path:/d…

使用rust读取usb设备ACR122U的nfc卡片id

rust及其高效和安全著称&#xff0c;而且支持跨平台&#xff0c;所以就想使用这个rust开发一个桌面端程序&#xff0c;来读取nfc设备的nfc卡片的id信息&#xff0c;下面就做一个最简单的入门教程吧&#xff0c;也是我写的第三个rust应用。 当你电脑上安装好了rust环境之后&…

Android笔记(二十二):Paging3分页加载库结合Compose的实现网络单一数据源访问

Paging3 组件是谷歌公司推出的分页加载库。个人认为Paging3库是非常强大&#xff0c;但是学习难点比较大的一个库。Paging3组件可用于加载和显示来自本地存储或网络中更大的数据集中的数据页面。此方法可让移动应用更高效地利用网络带宽和系统资源。在具体实现上&#xff0c;Pa…

详解Keras3.0 Layer API: Dropout layer

Dropout layer 图1 标准的神经网络 图2 加了Dropout临时删除部分神经元 Dropout层的作用是在神经网络中引入正则化&#xff0c;以防止过拟合。它通过随机丢弃一部分神经元&#xff08;如图2&#xff09;的输出来减少模型对训练数据的依赖性。这样可以提高模型的泛化能力&#x…

激活Windows过程及报错解决: 0x803f7001 在运行Microsoft Windows 非核心版本的计算机上, 运行“ slui.exe 0x2a 0x803f7001 “以显示错误文本

激活Windows过程及报错问题解决: 0x803f7001 在运行Microsoft Windows 非核心版本的计算机上&#xff0c;运行“ slui.exe 0x2a 0x803f7001 “以显示错误文本。 前言 最近在激活Windows过程中&#xff0c;遇到了报错: 0x803f7001 在运行Microsoft Windows 非核心版本的计算机上…

iS-RPM2023.2.0.0新版本发布

引言 经过不断努力和精心打磨,我们带着全新版本的RPM产品与大家见面啦!本次更新将为广大流程分析师和质量管理员们提供更深入、更准确的洞察力,以帮助大家在数据驱动的决策中取得更卓越的成果。然而,让海量数据转化为可用的见解并不是一项容易的任务。我们理解数据分析师们…

工业4G 物联网网关——机房动环监控系统应用方案介绍

机房动环监控系统是什么&#xff1f;机房动环监控系统的全称为机房动力环境监控系统&#xff0c;是一套安装在机房内的监控系统&#xff0c;可以对分散在机房各处的独立动力设备、环境和安防进行实时监测&#xff0c;统计和分析处理相关数据&#xff0c;第一时间侦测到故障发生…

万界星空科技车间生产管理系统解决方案

车间管理系统解决方案:   &#xff08;一&#xff09;车间生产计划管理解决方案   车间管理系统解决方案对于一般的生产计划&#xff0c;需完成编制、审批、下达、执行、完工等操作&#xff0c;车间管理系统解决方案立足于减少中间环节浪费&#xff0c;节约成本&#xff0c…

小米电脑管家 - 手机平板电脑家居互联

系列文章目录 前言 联想电脑安装小米电脑管家实现设备互联 如图&#xff0c;将 小米平板 5 Pro 作为联想笔记本 GeekPro 5000 &#xff08;这垃圾电脑&#xff09;的副屏。 可以在小米平板控制笔记本&#xff0c;如图所示 一、官方使用手册 参考&#xff1a;小米电脑管家帮助 …