SpringBoot配置mysql加密之Druid方式

news2024/9/24 21:23:11

一、导入Druid依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.22</version>
</dependency>

二、生成密文

方式1. 找到存放druid jar包的目录

1-1、在目录栏执行cmd,调出命令行窗口

在这里插入图片描述

1-2、在命令行窗口执行
java -cp druid-1.1.22.jar com.alibaba.druid.filter.config.ConfigTools 数据库密码

## 如下
E:\java\.m2\repository\com\alibaba\druid\1.1.22> java -cp .\druid-1.1.22.jar com.alibaba.druid.filter.config.ConfigTools ltkj.test
privateKey:MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAsQxrpzOi7BscR8nc5YIxRS8XUk3vN92V1wMUqu5EOKKr9D9MGVCZ3+7REzTB2uHUbRZgd4oCu7XwuLAx3GdOvwIDAQABAkAVDS2FuYGPQl/39zwhO/xAxisnOaYOhfXplW+xLViGRTqC4dTQ2h2LVMGpYhOPXCMNeMAO6ZhH4mSqNNMs16ClAiEAwaV1H8gjwFAfpMNhX76YjhtNkyt9dc8A2NbCqYvi6rMCIQDqDshX465wOSbnpXacW/8Uwi2Ku727YNFDKXJ9I6oBxQIgbjf0wFA0OSPhvvAOHmbnggr8ToX0dPeLreAfEE20rI0CIQCLSZrOOu9/V3OgnSZV7KWDW/8wNYO2s+o0tsCsWgH9JQIgPzjgmBX8s5CZwzxd2JIxAjH761CdNhTAmZjPPYuNLJg=
publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALEMa6czouwbHEfJ3OWCMUUvF1JN7zfdldcDFKruRDiiq/Q/TBlQmd/u0RM0wdrh1G0WYHeKAru18LiwMdxnTr8CAwEAAQ==
password:omjlkBmUL7vXvuiOUEsYBNejb9j1GuD7aCQWxWLSR9LohB5H7M+X8LR63wBUeyws7yRPNq/zejMAsZ82DX3z1A==

PS E:\java\.m2\repository\com\alibaba\druid\1.1.22> 

复制password和publickey

方式2. 工具类生成
利用工具类生成加密后密码和公钥
public static void main(String[] args) throws Exception {
        String password = "root123";
        System.out.println("明文密码: " + password);
        String[] keyPair = ConfigTools.genKeyPair(512);
        //私钥
        String privateKey = keyPair[0];
        //公钥
        String publicKey = keyPair[1];
        //用私钥加密后的密文
        password = ConfigTools.encrypt(privateKey, password);

        System.out.println("privateKey:" + privateKey);
        System.out.println("publicKey:" + publicKey);

        System.out.println("password:" + password);

        String decryptPassword = ConfigTools.decrypt(publicKey, password);
        System.out.println("解密后:" + decryptPassword);
    }
    

三、项目配置

修改yml配置文件

1. filters添加config
2. 配置解密,同时指定公钥

2-1. 生产环境 建议如下配置
启动时,通过java -jar 启动命令,指定spring.druid.publickey的值,如(java -jar xx.jar --spring.druid.publickey=公钥),
避免通过yml获取到公钥。
2-2 . 开发环境可将公钥直接配置在idea启动参数内。

connectionProperties: config.decrypt=true;config.decrypt.key=${spring.druid.publickey}
datasource:
    username: root
    password: RuacV1QzH80HVwZpR5MqagLoOWbRPYPj+yXKJrfEXJxIVchnWfGpdi2PTJCAlWoi7hNN+y4hhDmiGEvdYscW4w==
    url: jdbc:mysql://localhost:3306/article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    #druid 数据源专有配置
    druid:
      #配置druid监控页面
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        loginUsername: admin
        loginPassword: 123456
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    # 打开PSCache
    poolPreparedStatements: true
    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果运行时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址: https://mvnrepository.com/artifact/log4j/log4jDruidDataSourceFactory
    filters: stat,wall,config
    #指定每个连接上PSCache的大小
    maxPoolPreparedStatementPerConnectionSize: 20
    #合并多个DruidDataSource的监控数据
    useGlobalDataSourceStat: true
    #通过connectProperties属性来打开mergeSql功能;慢SQL记录;配置数据库密码解密;
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=2000;config.decrypt=true;config.decrypt.key=${spring.druid.publickey}

四、ConfigFilter解密原理分析

对于上面为何通过dataSource.setFilters(“config”)一行代码就能实现数据库密码的解密功能,你心中是否有疑惑,它具体又是如何配置了一个ConfigFilter实例的呢?带着这个疑问,我们看下DruidDataSource类中两个重要的方法入手:setFilters和setConnectionproperties,通过这两个入口方法找到与数据库连接密码解密有关的源码实现

3.1 DruidDataSource#setFilters方法

public void setFilters(String filters) throws SQLException {
        if (filters != null && filters.startsWith("!")) {
            filters = filters.substring(1);
            this.clearFilters();
        }
        this.addFilters(filters);
    }

    public void addFilters(String filters) throws SQLException {
        if (filters == null || filters.length() == 0) {
            return;
        }
        // 多个filter通过逗号分隔
        String[] filterArray = filters.split("\\,");

        for (String item : filterArray) {
            FilterManager.loadFilter(this.filters, item.trim());
        }
    }

在上面的addFilters方法中会去遍历配置的filter数组并调用FilterManager#loadFilter方法加载过滤器

3.2 FilterManager类静态代码块
而在FilterManager类中有这样一段静态代码

static {
        try {
            Properties filterProperties = loadFilterConfig();
            for (Map.Entry<Object, Object> entry : filterProperties.entrySet()) {
                String key = (String) entry.getKey();
                if (key.startsWith("druid.filters.")) {
                    String name = key.substring("druid.filters.".length());
                    aliasMap.put(name, (String) entry.getValue());
                }
            }
        } catch (Throwable e) {
            LOG.error("load filter config error", e);
        }
    }

上面这段静态代码首先会去调用无参的loadFilterConfig方法加载过滤器配置

public static Properties loadFilterConfig() throws IOException {
        Properties filterProperties = new Properties();

        loadFilterConfig(filterProperties, ClassLoader.getSystemClassLoader());
        loadFilterConfig(filterProperties, FilterManager.class.getClassLoader());
        loadFilterConfig(filterProperties, Thread.currentThread().getContextClassLoader());

        return filterProperties;
    }

而上面的静态方法中又会去调用带两个参数的loadFilterConfig方法,加载druid.jar包中类路径下的META-INF/druid-filter.properties属性配置文件

我们来看下druid-filter.properties文件中有哪些过滤器

druid.filters.default=com.alibaba.druid.filter.stat.StatFilter
druid.filters.stat=com.alibaba.druid.filter.stat.StatFilter
druid.filters.mergeStat=com.alibaba.druid.filter.stat.MergeStatFilter
druid.filters.counter=com.alibaba.druid.filter.stat.StatFilter
druid.filters.encoding=com.alibaba.druid.filter.encoding.EncodingConvertFilter
druid.filters.log4j=com.alibaba.druid.filter.logging.Log4jFilter
druid.filters.log4j2=com.alibaba.druid.filter.logging.Log4j2Filter
druid.filters.slf4j=com.alibaba.druid.filter.logging.Slf4jLogFilter
druid.filters.commonlogging=com.alibaba.druid.filter.logging.CommonsLogFilter
druid.filters.commonLogging=com.alibaba.druid.filter.logging.CommonsLogFilter
druid.filters.wall=com.alibaba.druid.wall.WallFilter
druid.filters.config=com.alibaba.druid.filter.config.ConfigFilter
druid.filters.haRandomValidator=com.alibaba.druid.pool.ha.selector.RandomDataSourceValidateFilter

可以看到总共有13个过滤器,ConfigFilter类对应的key为druid.filters.config
然后我们回到最上面的静态代码块中可以看到程序会遍历加载并读取druid-filter.properties文件中属性变量后返回的filterProperties, 并将其中的key截取掉druid.filters.前缀后的字符串作为name和过滤器的全类名作为键值对保存在ConcurrentHashMap<String, String>数据结构的aliasMap属性中。

3.3 FilterManager#loadFilter方法
————————————————

public static void loadFilter(List<Filter> filters, String filterName) throws SQLException {
        if (filterName.length() == 0) {
            return;
        }

        String filterClassNames = getFilter(filterName);

        if (filterClassNames != null) {
            for (String filterClassName : filterClassNames.split(",")) {
                if (existsFilter(filters, filterClassName)) {
                    continue;
                }

                Class<?> filterClass = Utils.loadClass(filterClassName);

                if (filterClass == null) {
                    LOG.error("load filter error, filter not found : " + filterClassName);
                    continue;
                }

                Filter filter;

                try {
                    filter = (Filter) filterClass.newInstance();
                } catch (ClassCastException e) {
                    LOG.error("load filter error.", e);
                    continue;
                } catch (InstantiationException e) {
                    thrownew SQLException("load managed jdbc driver event listener error. " + filterName, e);
                } catch (IllegalAccessException e) {
                    thrownew SQLException("load managed jdbc driver event listener error. " + filterName, e);
                } catch (RuntimeException e) {
                    thrownew SQLException("load managed jdbc driver event listener error. " + filterName, e);
                }

                filters.add(filter);
            }

            return;
        }

        if (existsFilter(filters, filterName)) {
            return;
        }

        Class<?> filterClass = Utils.loadClass(filterName);
        if (filterClass == null) {
            LOG.error("load filter error, filter not found : " + filterName);
            return;
        }

        try {
            Filter filter = (Filter) filterClass.newInstance();
            filters.add(filter);
        } catch (Exception e) {
            thrownew SQLException("load managed jdbc driver event listener error. " + filterName, e);
        }
    }
————————————————

上面这个方法的目的就是去根据配置的filterName去aliasMap中找到全类名,然后使用类加载器根据filter的全类名加载Filter类并实例化,完成实例化后将Filter类实例添加到DruidDataSource类List数据结构的filters属性中;当然这个过程首先会去判断filters中是否已经有了配置的Filter类实例,有的化则无需再次加载和实例化。

3.4 数据库连接密文解密的具体实现
在ConfigFilter类中有个init方法,正是在这个初始化方法中完成了数据源加密密码的解密

public void init(DataSourceProxy dataSourceProxy) {
      // 传入的dataSourceProxy就是我们的DruidDatasource实例  
     if (!(dataSourceProxy instanceof DruidDataSource)) {
            LOG.error("ConfigLoader only support DruidDataSource");
        }
        // DruidDataSource 转 DruidDataSource
        DruidDataSource dataSource = (DruidDataSource) dataSourceProxy;
        // 获取数据源中的连接属性
        Properties connectionProperties = dataSource.getConnectProperties();
        // 加载连接属性中配置的加密属性文件
        Properties configFileProperties = loadPropertyFromConfigFile(connectionProperties);

        // 判断是否需要解密,如果需要就进行解密
        boolean decrypt = isDecrypt(connectionProperties, configFileProperties);

        if (configFileProperties == null) {
            if (decrypt) {
                decrypt(dataSource, null);
            }
            return;
        }
        if (decrypt) {
            decrypt(dataSource, configFileProperties);
        }

        try {
            DruidDataSourceFactory.config(dataSource, configFileProperties);
        } catch (SQLException e) {
            thrownew IllegalArgumentException("Config DataSource error.", e);
        }
    }
1

上面这个ConfigFilter#init方法是在DruidDatasource#init方法中触发的

for (Filter filter : filters) {
       filter.init(this);
}

loadPropertyFromConfigFile方法源码

publics tatic final String CONFIG_FILE = "config.file";
public static final String SYS_PROP_CONFIG_FILE = "druid.config.file";

Properties loadPropertyFromConfigFile(Properties connectionProperties) {
        String configFile = connectionProperties.getProperty(CONFIG_FILE);

        if (configFile == null) {
            configFile = System.getProperty(SYS_PROP_CONFIG_FILE);
        }

        if (configFile != null && configFile.length() > 0) {
            if (LOG.isInfoEnabled()) {
                LOG.info("DruidDataSource Config File load from : " + configFile);
            }

            Properties info = loadConfig(configFile);

            if (info == null) {
                thrownew IllegalArgumentException("Cannot load remote config file from the [config.file=" + configFile
                                                   + "].");
            }

            return info;
        }

        return null;
    }

阅读loadPropertyFromConfigFile方法中的源码可见,加密属性文件主要从连接属性中key为config.file的属性文件位置或系统属性中key为druid.config.file映射的加密属性文件位置加载

isDecrypt方法源码

public static final String CONFIG_DECRYPT = "config.decrypt";
public static final String SYS_PROP_CONFIG_DECRYPT = "druid.config.decrypt";
public boolean isDecrypt(Properties connectionProperties, Properties configFileProperties) {
        String decrypterId = connectionProperties.getProperty(CONFIG_DECRYPT);
        if (decrypterId == null || decrypterId.length() == 0) {
            if (configFileProperties != null) {
                decrypterId = configFileProperties.getProperty(CONFIG_DECRYPT);
            }
        }
        if (decrypterId == null || decrypterId.length() == 0) {
            decrypterId = System.getProperty(SYS_PROP_CONFIG_DECRYPT);
        }
        return Boolean.valueOf(decrypterId);
    }

由isDecrypt方法中源码分析可见判断是否需要解密主要看连接属性或者加载的加密属性文件变量中key为config.decrypt的值是否为true;如果以上两个的值都不存在,则继续判断系统属性key为druid.config.decrypt的值是否为true

decrypt方法源码分析

public void decrypt(DruidDataSource dataSource, Properties info) {

        try {
            String encryptedPassword = null;
            // 若连接属性不为空,则从连接属性中获取加密密码,否则从数据源实例中获取加密密码
            if (info != null) {
                encryptedPassword = info.getProperty(DruidDataSourceFactory.PROP_PASSWORD);
            }

            if (encryptedPassword == null || encryptedPassword.length() == 0) {
                encryptedPassword = dataSource.getConnectProperties().getProperty(DruidDataSourceFactory.PROP_PASSWORD);
            }

            if (encryptedPassword == null || encryptedPassword.length() == 0) {
                encryptedPassword = dataSource.getPassword();
            }
            // 获取公钥
            PublicKey publicKey = getPublicKey(dataSource.getConnectProperties(), info);
            // 调用ConfigTools#decrypt方法获得解密后的密文
            String passwordPlainText = ConfigTools.decrypt(publicKey, encryptedPassword);

            if (info != null) {
                info.setProperty(DruidDataSourceFactory.PROP_PASSWORD, passwordPlainText);
            } else {
                dataSource.setPassword(passwordPlainText);
            }
        } catch (Exception e) {
            thrownew IllegalArgumentException("Failed to decrypt.", e);
        }
    }

getPublicKey方法源码

publicstaticfinal String CONFIG_KEY;

static {
       CONFIG_KEY = "config.decrypt.key";
}
public static final String SYS_PROP_CONFIG_KEY = "druid.config.decrypt.key";
// 获取公钥
public PublicKey getPublicKey(Properties connectionProperties, Properties configFileProperties) {
        String key = null;
        if (configFileProperties != null) {
            key = configFileProperties.getProperty(CONFIG_KEY);
        }

        if (StringUtils.isEmpty(key) && connectionProperties != null) {
            key = connectionProperties.getProperty(CONFIG_KEY);
        }

        if (StringUtils.isEmpty(key)) {
            key = System.getProperty(SYS_PROP_CONFIG_KEY);
        }

        return ConfigTools.getPublicKey(key);
    }

首先会去从解析加密配制文件后的属性变量中获取公钥, 获取公钥的key为config.decrypt.key;若加密配制文件属性中不存在公钥,则去数据源的连接属性中获取key为config.decrypt.key对应的公钥,如果仍然没有则去系统属性变量中获取key为druid.config.decrypt.key对应的公钥。最后调用ConfigTools#getPublicKey方法根据传入的公钥返回一个PublicKey对象

3.5 DruidAbstractDataSource#setConnectionProperties方法源码

public void setConnectionProperties(String connectionProperties) {
        if (connectionProperties == null || connectionProperties.trim().length() == 0) {
            setConnectProperties(null);
            return;
        }
        // 多个连接属性使用分号分隔
        String[] entries = connectionProperties.split(";");
        Properties properties = new Properties();
        for (int i = 0; i < entries.length; i++) {
            String entry = entries[i];
            if (entry.length() > 0) {
                // 每个连接属性以=号分割成name和value两部分保存到properties属性中
                int index = entry.indexOf('=');
                if (index > 0) {
                    String name = entry.substring(0, index);
                    String value = entry.substring(index + 1);
                    properties.setProperty(name, value);
                } else {
                    // no value is empty string which is how java.util.Properties works
                    properties.setProperty(entry, "");
                }
            }
        }
        // 最后通过抽象方法调用实现类DruidDatasource类的setConnectProperties方法
        setConnectProperties(properties);
    }

其他的源码这里就不继续深入分析了,druid.jar包中涉及到ConfigTools,DruidDatasource和ConfigFilter三个类的源码掌握到这里对于实现数据库连接密码的加密和解密也已经足够了
————————————————

五 SpringBoot 2.X 同时整合 druid连接池加密 + dynamic-datasource 动态数据源

官方文档: dynamic-datasource-spring-boot-starter

1. 引入依赖

		 <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                
    			<version>1.1.22</version>
         </dependency>
         <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
                <version>3.5.2</version>
          </dependency>
————————————————

2. 编写配置文件

# 数据源配置
spring:
  autoconfigure:
  	# 排除 Druid 自动配置
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  datasource:
  	# 指定使用 Druid 数据源
    type: com.alibaba.druid.pool.DruidDataSource
    # 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
    dynamic:
      #设置默认的数据源或者数据源组,默认值即为 master
      primary: master
      datasource:
        # 主库数据源
        master:
          driverClassName: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://192.168.0.121:3306/test1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true
          username: root
          password: root
        # 从库数据源
        slave:
          driverClassName: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://192.168.0.122:3306/test2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true
          username: root
          password: root
    druid:
      # 初始连接数
      initialSize: 5
      # 最小连接池数量
      minIdle: 10
      # 最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1 FROM DUAL
————————————————

在这里插入图片描述

3、测试

进入 Druid 控制台
在这里插入图片描述

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

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

相关文章

EasyExcel处理表头的缓存设置

在学习EasyExcel 时会发现针对使用类模型配置表头相关属性时&#xff0c;EasyExcel 会使用到缓存技术以提升表头的解析速度如下代码&#xff1a; 这些参数再何时设置的哪&#xff1f; 在easyExcel 基础参数设置中会有这个参数filedCacheLocation 。默认采用的使用线程级别的…

MVVM前端设计模式的发展与应用

在MVC模式中&#xff0c;随着代码量越来越大&#xff0c;Controller主要用来处理各种逻辑和数据转化的Controller首当其冲&#xff0c;变得非常庞大&#xff0c;MVC的简写变成了Massive-View-Controller&#xff08;意为沉重的Controller&#xff09; 我曾经接手老项目&#x…

什么是rocketmq❓

在大规模分布式系统中&#xff0c;各个服务之间的通信是至关重要的&#xff0c;而RocketMQ作为一款分布式消息中间件&#xff0c;为解决这一问题提供了强大的解决方案。本文将深入探讨RocketMQ的基本概念、用途&#xff0c;以及在实际分布式系统中的作用&#xff0c;并对Produc…

CBTC上海新能源锂电池展览会奋战华东!2024携手共赢!

2024CBTC上海新能源锂电池技术展览会|上海锂离子电池生产设备展览会 时 间&#xff1a;2024年7月24&#xff5e;26日 地 点&#xff1a;国家会展中心&#xff08;上海虹桥&#xff09; 发展前景&#xff1a; 随着科技的不断进步&#xff0c;锂电池市场逐渐成为全球能源市场的…

GD32f103系列教程—(SPI DMA方式软件实现篇)

DMA知识简介: DMA 控制器提供了一种硬件的方式在外设和存储器之间或者存储器和存储器之间传输数据&#xff0c;而无需 CPU 的介入&#xff0c;从而使 CPU 可以专注在处理其他系统功能上。 项目中需要采用DMA方式传输数据的原因&#xff1a;如果仅仅采用SPI接口进行寄存器通信&…

vue2项目vue-qrcode-reader 扫一扫二维码插件

vue2项目 vue-qrcode-reader 扫一扫二维码插件 问题所在解决办法成功展示 问题所在 今天在引导师弟做扫二维码功能&#xff0c;发现通过npm install --save vue-qrcode-reade安装死活就是报错TypeError: Object...) is not a function 解决办法 百度了很多大牛的博客&#…

【深度学习目标检测】三、基于深度学习的人物摔倒检测(python,yolov8)

深度学习目标检测方法则是利用深度神经网络模型进行目标检测&#xff0c;主要有以下几种&#xff1a; R-CNN系列&#xff1a;包括R-CNN、Fast R-CNN、Faster R-CNN等&#xff0c;通过候选区域法生成候选目标区域&#xff0c;然后使用卷积神经网络提取特征&#xff0c;并通过分类…

【高效开发工具系列】DataGrip入门

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

数据分享 I 全国市级商品房屋销售数据,shp/excel格式,2005-2020年数据

基本信息. 数据名称: 全国市级商品房屋销售数据 数据格式: Shp、excel 数据时间: 2005-2020年 数据几何类型: 面 数据坐标系: WGS84坐标系 数据来源&#xff1a;网络公开数据 数据字段&#xff1a; 序号字段名称字段说明1spxse商品房销售额&#xff08;亿元&#xf…

主卧卫生间门对着床,怎么设计才能让你好好睡觉呢?福州中宅装饰,福州装修

主卧卫生间门对着床&#xff0c;一抬眼就能和卫生间来一个对视&#xff0c;心里很膈应&#xff0c;那么怎么解决这个问题呢&#xff1f; 1️⃣门改方向 首先&#xff0c;你可以考虑把卫生间门的开门方向改一下。如果卫生间门是向外开的&#xff0c;你可以考虑把门换成向内开的…

吴恩达《机器学习》12-4-12-5:核函数 1、核函数 2

一、核函数 1 1. 多项式模型的局限性 在之前的学习中&#xff0c;了解到可以使用多项式模型来解决无法用直线进行分隔的分类问题。例如&#xff0c;对于判定边界的建模&#xff0c;可能会使用类似于以下形式的模型&#xff1a; 然而&#xff0c;这样的多项式模型在实际应用中…

ALV动态GUI标题

导语&#xff1a;快两个月没有分享内容了&#xff0c;积攒了一大堆&#xff0c;最近经历了一些变动&#xff0c;有些充实和忙碌&#xff0c;最近在做的客制化平台中&#xff0c;顾问需要在不同的按钮对应的ALV中&#xff0c;展示不同的GUI标题。 效果图&#xff1a; 实现方法&a…

大模型概述

文章目录 大模型语言大模型视觉大模型多模态大模型 大模型 所谓 大&#xff1a;模型结构大&#xff0c; 参数量大&#xff0c;训练数据量大 大模型包含数十亿个参数&#xff0c;模型大小可以达到数百GB甚至更大。这种巨大的模型规模为其提供了强大的表达能力和学习能力多任务…

网络安全——SQL注入实验

一、实验目的要求&#xff1a; 二、实验设备与环境&#xff1a; 三、实验原理&#xff1a; 四、实验步骤&#xff1a; 五、实验现象、结果记录及整理&#xff1a; 六、分析讨论与思考题解答&#xff1a; 七、实验截图&#xff1a; 一、实验目的要求&#xff1a; 1、…

【DC-DC】世微 AP9166 降压恒压芯片 18V 2A同步降压转换器

600KHz&#xff0c;18V&#xff0c;2A同步降压转换器 概述 AP9166是一款完全集成的效率2A同步整流降压转换器。AP9166运行在宽输出电流上以高效率 负载范围。此设备提供两个操作模式、PWM控制和PFM模式切换控制&#xff0c;允许更宽范围的高效率 负载AP9166至少需要现成标准的…

亲测好用的国内AI绘画工具TOP榜,中文输入,新手友好!

“如果不想被AI工具替代&#xff0c;那就先成为用好工具的人。” 最近AI绘画也是大火&#xff0c;上次给大家整理了9款国内实用的AI工具&#xff0c;很多小伙伴就私信我能不能整理一下关于国内可用的AI绘画工具&#xff0c;因为现在最火的AI绘画工具Midjourney和Stable Diffus…

换内核ubuntu

grep menuentry /boot/grub/grub.cfg我要使用第三个(索引从0开始&#xff0c;所以是第二个) 可以使用vi编辑&#xff08;很麻烦&#xff09; i变为插入模型 esc变为普通模型 &#xff1a;x删除单个字符&#xff0c;dd删除一行&#xff0c;&#xff1a;wq保存并退出 更新文件…

ArkTS 任务统计页面

我们继续学习使用ArkTS实现一个任务进度的统计功能。 1.公共的组件、样式 class Task{static id: number 1// 任务名称name: string 任务${Task.id}// 任务状态: 是否完成finished: boolean false }// 统计的卡片样式 Styles function card(){.width(95%).padding(20).backg…

c#按照时间进行数据存储(不用数据库)

概要介绍 按照日期生成文件夹&#xff0c;按照时间生成文件名&#xff0c;存储字符串。 可以用于简单数据记录&#xff08;如果数据存储考虑格式文本&#xff0c;保存为csv格式&#xff09; 实现效果 调用方法 SaveText.saveStr("测试字符串"DateTime.Now.ToStrin…

LeetCode 2132. 用邮票贴满网格图:二维前缀和 + 二维差分

【LetMeFly】2132.用邮票贴满网格图&#xff1a;二维前缀和 二维差分 力扣题目链接&#xff1a;https://leetcode.cn/problems/stamping-the-grid/ 给你一个 m x n 的二进制矩阵 grid &#xff0c;每个格子要么为 0 &#xff08;空&#xff09;要么为 1 &#xff08;被占据&…