5. MySQL - JDBC SQL 注入 博客系统(万字详解)

目录

1. 介绍

2. 使用 JDBC 连接数据库

2.1 如何使用 JDBC 连接数据库

2.2 导入的各个类

2.3 DataSource 对象的创建

2.4 从 DataSource 对象中得到 Connection 对象

2.5 创建 Statement 对象

2.6 从 ResultSet 中遍历每行结果,从每行中获取每列的值

2.7 代码汇总

3. PrepareStatement 详解

3.1 动态 SQL 执行

3.2 SQL 类型

3.3 SQL 注入(Inject)

4. 关于 SQL 的执行

4.1 带查询结果的执行

4.2 不带查询结果的执行

4.3 关于 ResultSet 读取列

5. 实践应用:博客系统

发表文章

删除文章ByBid

批量删除文章ByBid


1. 介绍

什么是 JDBC 呢?

JDBC 代表 Java 数据库连接(JavaDatabaseConnectivity),它是用于 Java 编程语言和数据库之间的数据库无关连接的标准 Java API。

我们写的所有程序的代码来自:官方提供(JDK)、我们写的(App)。

为了减少工作量,引入一些别人(不是官方和我们)写的代码,一般把这类代码称为第三方。以库(library lib)。

在 Java 中,一般是以一组类文件(*.class)提供。把这组类文件打出一个文件(采用 zip 压缩格式,Java 称为 Jar 包:Java ARchive)。

所以,我们要使用别人的第三方库,需要:

  1. 拿到一个 jar 包

  2. 配置工程(IDEA)让工程在编写、编译、运行阶段,都可以找到第三方库中的类

我们要使用一个 MySQL 官方提供的,进行 SQL 查询的代码库 "mysql-connector-java-5.1.47.jar"

描述:我们的应用 依赖于(depends on)"mysql-connector-java-5.1.47.jar"

为了处理好这些依赖关系,引入 Java 构建阶段的工具:maven/gradle。

这类工具,为了定位每个库,定义了一个坐标(coordinate)的概念。

这个库属于哪个组织(公司):groupId

这个库自己的专有名字:artifactId

这个库的当前版本:version

groupId + artifactId + version 唯一确定一个库。

pom.xml 关于 Maven 工具的配置文件,我们的依赖关系要在这里写清楚。

POM 的主体结构:

在新建一个 maven 项目后,我们需要添加依赖,使用以下代码即可。

<properties>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <encoding>utf-8</encoding>
</properties>

<dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
</dependencies>

如下图所示:

2. 使用 JDBC 连接数据库

2.1 如何使用 JDBC 连接数据库

  • DataSource(数据源)

  • Connection(连接)

  • Statement(SQL 语句)/ PreparedStatement

  • ResultSet(结果集)

对比打电话的例子:

DataSource 类比 联系方式 or 通讯录

Connection 类比打通电话,需要 DataSource 作为前提

Statement:客户向服务器发送的 SQL 语句

ResultSet:服务器向客户回的结果

2.2 导入的各个类

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

2.3 DataSource 对象的创建

DataSource 只是 JDBC 提供的一个接口,要创建的是 MysqlDataSource 对象。

数据库的相关信息         

1. 数据库所在的主机:127.0.0.1 / localhost 

2. 数据库所在的端口:3306 / 3307 ... 

3. 数据库的用户名: ... 

4. 数据库的密码: ...

5. 额外的配置选项

        5.1. 连接使用的字符集编码 characterEncoding=utf8  

        5.2. 不使用加密连接: useSSL=false  

        5.3. 指定时区: serverTimezone=Asia/Shanghai

第一种创建方式:

MysqlDataSource dataSource = new MysqlDataSource();

dataSource.setServerName("127.0.0.1");
dataSource.setPort(3306);
dataSource.setUser("debian-sys-maint");
dataSource.setPassword("*********");
dataSource.setDatabaseName("learn");
dataSource.setCharacterEncoding("utf8");
dataSource.setUseSSL(false);
dataSource.setServerTimezone("Asia/Shanghai");

第二种创建方式:

MysqlDataSource dataSource = new MysqlDataSource();
dataSource.setUser("debian-sys-maint");
dataSource.setPassword("*********");
dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/learn?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai");

2.4 从 DataSource 对象中得到 Connection 对象

// try-with-resource
// 利用这种写法,不用自己写 con.close(),代码结构看起来干净
try (Connection con = dataSource.getConnection()) {
    // 利用 con 对象执行 SQL
} catch (SQLException exc) {
    // 处理异常
}

2.5 创建 Statement 对象

// -- 列出当前所在库的所有表名称
String sql = "show tables";

try (PreparedStatement ps = con.prepareStatement(sql)) {
    // execute: 执行
    // query: 查询
    try (ResultSet rs = ps.executeQuery()) {
        // 结果就可以从 ResultSet 对象中获取
    }
}

2.6 从 ResultSet 中遍历每行结果,从每行中获取每列的值

// hasNext() + next()
while (rs.next()) {
    // rs 代表当前遍历的行
    // 第一列的结果是表的名称,所以是字符串 String
    String tableName = rs.getString(1);    // 下标是 1
    System.out.println(tableName);
}

2.7 代码汇总

package com.peixinchen;

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class Main {
    public static void main(String[] args) throws SQLException {
        MysqlDataSource dataSource = new MysqlDataSource();

        dataSource.setUser("debian-sys-maint");
        dataSource.setPassword("***************");
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/learn?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai");

        // try-with-resource
        // 利用这种写法,不用自己写 con.close(),代码结构看起来干净
        try (Connection con = dataSource.getConnection()) {
            // 利用 con 对象执行 SQL

            // -- 列出当前所在库的所有表名称
            String sql = "show tables"; // 只有 1 列

            try (PreparedStatement ps = con.prepareStatement(sql)) {
                try (ResultSet rs = ps.executeQuery()) {
                    // 结果就可以从 ResultSet 对象中获取

                    // hasNext() + next()
                    while (rs.next()) {
                        // rs 代表当前遍历的行
                        // 第一列的结果是表的名称,所以是字符串 String
                        String tableName = rs.getString(1);    // 下标是 1
                        System.out.println(tableName);
                    }
                }
            }
        }
    }
}

进程的执行结果 = 程序代码 + 进程执行时的环境

同样的代码,不同的环境是可能得到不同的结果的,所以如果遇到了运行结果不符合预期,除了检查代码的问题之外,现在应该把更多精力放到检查周边环境是否符合预期上了。

3. PrepareStatement 详解

3.1 动态 SQL 执行

// 静态 SQL
String sql = "show tables";
String sql = "select * from oj_records where oj_id = 1"

我们在之前使用的都是静态的 SQL ,但是在实际的应用中,大多都是动态的 SQL ,比如用户登陆界面都是等待用户输入数据,并不是固定的数据。因此,我们来了解一下动态 SQL。

PreparedStatement 通过占位符(placeholder)和动态绑定的方式做到。

// ? 作为占位符,先把位置占住,等待用户输入
String sql = "select * from users where username = ? and password = ?";

PreparedStatement ps = conn.preparedStatement(sql);

String username = scanner.nextLine();
String password = scanner.nextLine();

// 用实际得到的用户名和密码替换占位符 —— 动态绑定
ps.setString(1, username);    // 用用户名替换第一个 ?
ps.setString(2, password);    // 用密码替换第二个 ?

接下来,我们运行以下完整的代码(密码和库更改为自己的即可):

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Scanner;

public class PreparedStatementDemo {
    public static void main(String[] args) throws SQLException{
        Scanner scanner = new Scanner(System.in);
        System.out.println("请输入要查询的难度: ");
        String difficulty = scanner.nextLine();

        MysqlDataSource dataSource = new MysqlDataSource();

        dataSource.setUser("debian-sys-maint");
        dataSource.setPassword("*****************");
        dataSource.setUrl("jdbc:mysql://127.0.0.1:3306/learn?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai");

        try (Connection con = dataSource.getConnection()) {
            // 利用 con 对象执行 SQL,建立连接
            String sql = "select * from oj_records where difficulty = ?";
            // 动态 SQL
            try (PreparedStatement ps = con.prepareStatement(sql)) {
                ps.setString(1, difficulty);

                System.out.println(ps);
                try (ResultSet rs = ps.executeQuery()) {
                    // 结果就可以从 ResultSet 对象中获取

                    // hasNext() + next()
                    while (rs.next()) {
                        // rs 代表当前遍历的行
                        // 第一列的结果是表的名称,所以是字符串 String
                        String id = rs.getString(1);    // 下标是 1
                        System.out.println(id);
                    }
                }
            }
        }
    }
}

3.2 SQL 类型

// 针对 PreparedStatement 执行动态绑定时
int x = ...;
ps.setInt(1, x);

String s = "...";
ps.setString(1, s);

String datetime = "2023-04-24 20:54:00"
ps.setString(1, datetime);


// 从 ResultSet 获取值时
int x = rs.getInt(1);
String s = rs.getString(1);
String datetime = rs.getString(1);
Java 代码类型MySQL 类型举例

int

setInt(..)

getInt(..)

int

ps.getInt(1,x);

x = rs.getInt(1);

String

varchar(..)

char(..)

text

longtext

String

datetime

date

time

3.3 SQL 注入(Inject)

什么是 SQL 注入呢?

SQL注入是一种非常常见的数据库攻击手段,SQL注入漏洞也是网络世界中最普遍的漏洞之一。大家也许都听过某某学长通过攻击学校数据库修改自己成绩的事情,这些学长们一般用的就是SQL注入方法。

SQL注入其实就是恶意用户通过在表单中填写包含SQL关键字的数据来使数据库执行非常规代码的过程。,SQL数据库的操作是通过SQL语句来执行的,而无论是执行代码还是数据项都必须写在SQL语句之中,这就导致如果我们在数据项中加入了某些SQL语句关键字(比如说SELECT、DROP等等),这些关键字就很可能在数据库写入或读取数据时得到执行。

我们来从代码角度看看是如何进行 SQL 注入的。

String username = scanner.nextLine();

String sql = "select * from users where username = '%s'";
sql = String.format(sql, username);

// 执行

当用户输入用户名是类型 " ' or 1 = 1 or 1 = ' "

String sql = "select * from users where username = '' or 1 = 1 or 1 = ''";

由于 name = ' ' 为假,1 = 1 为真,1 = ' ' 为假,因此 where 后面的语句最终执行结果为真,因此,会将数据库中的所有信息显示出来,从而导致数据泄露。

4. 关于 SQL 的执行

4.1 带查询结果的执行

select ...;
-- 查询..
show ...;
-- 显示..
ResultSet rs = ps.executeQuery();

4.2 不带查询结果的执行

create database ...;
-- 创建库
create table ...;
-- 创建表
drop database ...;
-- 删除库
drop table ...;
-- 删除表
insert into ...;
-- 增加数据
update ...;
-- 更新数据
delete ...;
-- 删除数据

// number 本次执行成功多少行
int number = ps.executeUpdate();

4.3 关于 ResultSet 读取列

1)读取第 n 列
rs.getInt(n);    // 读取第 n 列

2) 根据列名称读取
rs.getInt("oj_id");

5. 实践应用:博客系统

只有一张表          博客文章(blogs)

0) bid    int   PK  AI
1) author varchar(30)  NN         作者
2) published_at datetime NN       发表时间
3) content text NN                正文 

create table blogs (
    bid int primary key auto_increment,
    author varchar(20) not null comment '作者',
    title varchar(100) not null comment '标题',
    published_at datetime not null comment '发表时间',
    content text not null comment '正文'
);
-- 新增文章
insert into blogs (author, title, published_at, content) values (?, ?, ?, ?);

-- 删除一篇文章
   -- by bid
delete from blogs where bid = ?;
   -- 批量 by bid
delete from blogs where bid in (...);
   -- 根据标题(只能删自己的)
delete from blogs where author = ? and title = ?;

-- 给出文章列表(bid/作者/发表时间/标题)
-- 根据发表从新到旧
select bid, author, published_at, title from blogs order by published_at desc;
-- 增加分页(每页共 3 篇),需要第 x 页
select bid, author, published_at, title from blogs order by published_at desc limit 3 offset 3 * (x - 1)
x == 1 : offset 0
x == 2 : offset 3
x == 3 : offset 6

创建一个 DBUtil 类用来存放配置信息:

import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;

// Util: 工具
public class DBUtil {
    private static final DataSource dataSource;

    static {
        MysqlDataSource mysqlDataSource = new MysqlDataSource();

        mysqlDataSource.setUser("debian-sys-maint");
        mysqlDataSource.setPassword("***********");
        mysqlDataSource.setUrl("jdbc:mysql://127.0.0.1:3306/learn?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai");
        dataSource = mysqlDataSource;
    }

    public static Connection connection() throws SQLException {
        return dataSource.getConnection();
    }
}

发表文章

接下来实现发表文章的功能:

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Scanner;

public class PublishArticle {
    public static void main(String[] args) throws SQLException {
        Scanner sc = new Scanner(System.in);

        System.out.println("请输入用户名:");
        String author = sc.nextLine();

        System.out.println("请输入文章标题:");
        String title = sc.nextLine();

        System.out.println("请输入正文内容:");
        String content = sc.nextLine();
        // 获取当前时间
        ZoneId zone = ZoneId.of("Asia/Shanghai");
        LocalDateTime now = LocalDateTime.now(zone);
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String publishedAt = now.format(formatter);

        String sql = "insert into blogs (author, title, published_at, content) values (?, ?, ?, ?)";

        try (Connection c = DBUtil.connection()) {
            try (PreparedStatement ps = c.prepareStatement(sql)) {
                // 1. 进行动态绑定
                ps.setString(1, author);
                ps.setString(2, title);
                ps.setString(3, publishedAt);
                ps.setString(4, content);

                // 此处是不带结果集的查询
                System.out.println(ps);
                ps.executeUpdate();

                System.out.println("文章发表成功");
            }
        }
    }
}

删除文章ByBid

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Scanner;

public class DeleteByBid {
    public static void main(String[] args) throws SQLException {
        Scanner scanner = new Scanner(System.in);
        System.out.print("请输入要删除的 bid: ");
        int bid = scanner.nextInt();

        String sql = "delete from blogs where bid = ?";

        try (Connection c = DBUtil.connection()) {
            try (PreparedStatement ps = c.prepareStatement(sql)) {
                ps.setInt(1, bid);

                System.out.println(ps);

                ps.executeUpdate();

                System.out.println("删除成功");
            }
        }
    }
}

批量删除文章ByBid

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;

public class BulkDeleteByBid {
    public static void main(String[] args) throws SQLException {
        Scanner scanner = new Scanner(System.in);
        List<Integer> bidList = new ArrayList<>();
        System.out.print("请输入要删除的 bid: ");
        while (scanner.hasNextInt()) {
            int bid = scanner.nextInt();
            bidList.add(bid);
            System.out.print("请输入要删除的 bid: ");
        }

        System.out.println("DEBUG: 要删除的 bid 列表为: " + bidList);
        // bidList -> "1, 3, 7
        // List<Integer> -> List<String>
        // String.join(", ", list of string)
        // Stream 流式写法
        List<String> bidListString = bidList.stream()
                .map(i -> String.valueOf(i))
                .collect(Collectors.toList());
        String inClause = String.join(", ", bidListString);

        String sql = String.format("delete from blogs where bid in (%s)", inClause);

        try (Connection c = DBUtil.connection()) {
            try (PreparedStatement ps = c.prepareStatement(sql)) {

                System.out.println(ps);

                ps.executeUpdate();

                System.out.println("批量删除成功");
            }
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【0】冒泡排序

前言 通过函数模板技术设计一个冒泡排序算法&#xff0c;领悟泛型编程的思想和冒泡排序的思想&#xff0c;然后使用QTest测试各种输入值&#xff0c;养成先写测试代码&#xff0c;后写程序代码的习惯 0x0 编写一个int版本的冒泡函数 1.不管要排序的数组长度多长&#xff0c;外…

内核机制在以下方面发挥作用:

进程间通信&#xff1a;内核提供了特定的机制和接口&#xff0c;用于实现进程间的通信。这可以包括共享内存、管道、消息队列、信号量等。通过这些机制&#xff0c;进程可以在内核的帮助下进行数据传输和同步&#xff0c;实现进程间的协作和通信。 进程调度&#xff1a;内核负…

SpringCloud学习路线(1)—— 从头开始的微服务

一、服务架构的历史 现有的服务框架&#xff1a; 单体架构 概念&#xff1a; 将业务所有功能集中在一个项目中开发&#xff0c;打包部署优点&#xff1a; 架构简单&#xff0c;部署成本低缺陷&#xff1a; 耦合度高 分布式架构 概念&#xff1a; 根据业务功能对系统进行拆分&a…

如何在 Excel 中快速生成随机密码?

有时&#xff0c;我们可能想创建随机密码来保护某些重要内容。 但是&#xff0c;您有什么技巧可以在Excel中快速生成随机密码&#xff1f; 在这里&#xff0c;我有一些可以在Excel工作表中处理的方法。 用公式生成随机密码 使用插入随机数据生成随机密码​编辑 用公式生成随机…

从实体按键看 Android 车载的自定义事件机制

在汽车数字化、智能化变革的进程中&#xff0c;越来越多的车机设计或部分、或全部地舍弃了实体按键&#xff0c;进而把车主操作的入口转移到了车机 UI 以及语音助手。 但统一、高效的零层级 UI 颇为困难&#xff0c;语音的准确率、覆盖率亦不够完善&#xff0c;那么在当下的阶段…

STL源码刨析_stack _queue

目录 一. 介绍 1. stack 介绍 2. queue 介绍 二. 模拟实现 1. stack 模拟实现 2. queue 模拟实现 三. deque 1. deque 接口 2. 底层 一. 介绍 1. stack 介绍 stack&#xff08;栈&#xff09;是一种容器适配器&#xff0c;它提供了一种后进先出&#xff08;LIFO&#xff0…

arcgis建筑物平均高度

主要用到相交和属性表的汇总功能。 路网 建筑物栋 相交结果 右键&#xff0c;bh列汇总 原始块有392&#xff0c;这里只有389&#xff0c;说明有的地块没有建筑&#xff0c;所以应该将表连接到原始街区上检查是否合理&#xff0c;以及随机验证一个结果是否正确。 连接结果&…

【SpringBoot应用篇】SpringBoot集成atomikos实现多数据源配置和分布式事务管理

【SpringBoot应用篇】SpringBoot集成atomikos实现多数据源配置和分布式事务管理 分布式事务概念XA和JTA概述SpringBoot集成atomikos数据库结构pom通用工具类RBaseControllerBaseExceptionCodeExceptionCodeBaseExceptionBaseUncheckedExceptionBizException application.yml数据…

C++初阶 - 3.类和对象(中)

目录 1.类的6个默认成员函数 2.构造函数 2.2特性 3.析构函数 3.1 概念 3.2 特性 4. 拷贝构造函数 4.1 概念 4.2 特征 5.赋值运算符重载 5.1运算符重载 5.2 赋值运算符重载 5.3 前置和后置重载 6.日期类的实现 7.const成员 8.取地址及const取地址操作符重载 1.类…

为什么项目可见性难以实现?该如何提高?

在项目和专业服务管理中&#xff0c;失败有时难以避免。沟通不足和需求定义不明确被认为是造成失败的最大原因&#xff0c;这意味着项目可见性和信息流动至关重要。 什么是项目可见性&#xff1f; 项目可见性是组织项目相关信息的方式&#xff0c;以便所有团队成员、项目经理…

使用Jenkins自由风格的软件项目实现接口自动化测试持续集成

这里写目录标题 一、JOB项目配置1、添加描述2、限制项目的运行节点3、源码管理4、构建触发器5、构建步骤6、构建后操作 一、JOB项目配置 1、添加描述 可选选项可填可不填 2、限制项目的运行节点 节点中要有运行环境所需的配置 节点配置教程&#xff1a;https://blog.csdn…

Go语言之并发编程练习,GO协程初识,互斥锁,管道:channel的读写操作,生产者消费者

GO协程初识 package mainimport ("fmt""sync""time" )func read() {defer wg.Done()fmt.Println("read start")time.Sleep(time.Second * 3)fmt.Println("read end") }func listenMusci() {defer wg.Done()fmt.Println(&qu…

在云计算环境中,保护Java应用程序可用的有效措施和工具

云计算&#xff08;Cloud&#xff09;技术是近年来计算机科学的一个重要突破。大多数组织已经通过将自己的应用程序移入云平台而获益。不过&#xff0c;如何保证应用程序在第三方服务器上的安全性&#xff0c;是一项艰巨的挑战。 在本文中&#xff0c;我们将重点讨论Java&…

长沙打造“全球研发中心城市”,智能网联产业如何交卷?

作者 | 魏启扬 来源 | 洞见新研社 知乎上有一个浏览超百万的热门问题——“大家怎么看待长沙这个城市&#xff1f;” 答主“星球研究所”的回答获得了高赞&#xff0c;“这是一个天性如火的城市”。 网红城市的外衣下&#xff0c;从湖南卫视的综艺节目&#xff0c;到网红美…

数据结构--栈

一、栈 数组是一种连续存储、随机访问的线性表&#xff0c;链表属于分散存储、连续访问的线性表。它们每个数据都有其相对位置&#xff0c;有至多一个直接前驱和之多一个直接后继。栈&#xff08;Stack&#xff09;和队列&#xff08;Queue&#xff09;也属于线性表&#xff0c…

Fortinet Accelerate 2023·中国区巡展收官丨让安全成就未来

7月18日&#xff0c;2023 Fortinet Accelerate Summit在上海成功举办&#xff01;这亦象征着“Fortinet Accelerate2023中国区巡展”圆满收官。Fortinet携手来自多个典型行业的百余位代表客户&#xff0c;以及亚马逊云科技、Telstra - PBS 太平洋电信、Tenable等多家生态合作伙…

字幕切分视频

Whisper 仓库地址&#xff1a; https://github.com/openai/whisper 可用模型信息&#xff1a; 测试视频&#xff1a;18段&#xff0c;总共447S视频&#xff08;11段前&#xff1a;有11段开头有停顿的视频&#xff09; Tiny: 跑完&#xff1a;142S &#xff0c;11段前&#xf…

学习opencv.js之基本使用方法(读取,显示,灰度化,边缘检测,特征值点检测)

opencv.js是什么 OpenCV.js 是 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;的 JavaScript 版本。OpenCV 是一个广泛使用的计算机视觉和图像处理库&#xff0c;提供了一系列功能强大的算法和工具&#xff0c;用于处理图像、视频、特征提取、对象识别等…

无虚拟 DOM 版 Vue 进行到哪一步了?

前言 就在一年前的 Vue Conf 2022&#xff0c;尤雨溪向大家分享了一个非常令人期待的新模式&#xff1a;无虚拟 DOM 模式&#xff01; 我看了回放之后非常兴奋&#xff0c;感觉这是个非常牛逼的新 feature&#xff0c;鉴于可能会有部分人还不知道或者还没听过什么是 Vue 无虚…

智能电表数据采集器

智能电表数据采集器是一种用于采集智能电表数据的设备&#xff0c;它可以将智能电表的数据传输到远程服务器上&#xff0c;以便进行数据分析和监控。智能电表数据采集器的主要功能是采集智能电表的实时数据&#xff0c;并将其发送到远程服务器上&#xff0c;从而实现对智能电表…