万级数据优化EasyExcel+mybatis流式查询导出封装

news2025/6/21 19:18:35

文章目录

    • 前言.千万级数据优化
    • 一. 直接上流式查询封装工具代码
    • 二. 传统分页导出查询
    • 三. 流式查询概念
    • 游标查询


前言.千万级数据优化

我们不妨先给大家讲一个概念,利用此概念我们正好给大家介绍一个数据库优化的小技巧: 需求如下:将一个地市表的数据导出70万条。

在这里插入图片描述

如果你不假思索,直接一条sql语句搞上去,直接就会内存溢出,因为mysql会将结果记录统一查询出来然后返还给内存:那内存可能直接OOM!

@Test
public void testQuery1()  {

    // 1、定义资源
    Connection connection = null;
    ResultSet resultSet = null;
    PreparedStatement statement = null;
    String sql = "select * from user";
    try {
        // 获取连接
        connection = DBUtil.getConnection();
        // 获取使用预编译的statement
        statement = connection.prepareStatement(sql);

        long start = System.currentTimeMillis();
        resultSet = statement.executeQuery();
        while (resultSet.next()){
            System.out.println("name---->" + resultSet.getString("nick_name") );
        }
        long end = System.currentTimeMillis();
        System.out.println(end -start);
    } catch (SQLException e){
        e.printStackTrace();
    } finally {
        // 关闭资源
        DBUtil.closeAll(connection,statement,resultSet);
    }
}

所以我们通常有如下几种解决方案:

一. 直接上流式查询封装工具代码

使用2核4G云服务器
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

表格美化 CustomCellWeightStrategy.class
/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.common.utils.easyexcel
 * @className com.vector.common.utils.easyexcel.CustomCellWeightStrategy
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/8/28 17:58
 */
public class CustomCellWeightStrategy extends AbstractColumnWidthStyleStrategy {
    private final Map<Integer, Map<Integer, Integer>> CACHE = new HashMap<>();

    @Override
    protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell, Head head, Integer relativeRowIndex, Boolean isHead) {
        boolean needSetWidth = isHead || !CollectionUtils.isEmpty(cellDataList);
        if (needSetWidth) {
            Map<Integer, Integer> maxColumnWidthMap = CACHE.computeIfAbsent(writeSheetHolder.getSheetNo(), k -> new HashMap<>());

            int columnWidth = this.dataLength(cellDataList, cell, isHead)+8;
            if (columnWidth >= 0) {
                if (columnWidth > 254) {
                    columnWidth = 254;
                }

                Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
                if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
                    maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
                    Sheet sheet = writeSheetHolder.getSheet();
                    sheet.setColumnWidth(cell.getColumnIndex(), columnWidth * 200);
                }

                //设置单元格类型
                cell.setCellType(CellType.STRING);
                // 数据总长度
                int length = cell.getStringCellValue().length();
                // 换行数
                int rows = cell.getStringCellValue().split("\n").length;
                // 默认一行高为20
                cell.getRow().setHeightInPoints(rows * 20);
            }
        }
    }


    /**
     * 计算长度
     *
     * @param cellDataList
     * @param cell
     * @param isHead
     * @return
     */
    private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
        if (isHead) {
            return cell.getStringCellValue().getBytes().length;
        } else {
            CellData<?> cellData = cellDataList.get(0);
            CellDataTypeEnum type = cellData.getType();
            if (type == null) {
                return -1;
            } else {
                switch (type) {
                    case STRING:
                        // 换行符(数据需要提前解析好)
                        int index = cellData.getStringValue().indexOf("\n");
                        return index != -1 ?
                                cellData.getStringValue().substring(0, index).getBytes().length + 1 : cellData.getStringValue().getBytes().length + 1;
                    case BOOLEAN:
                        return cellData.getBooleanValue().toString().getBytes().length;
                    case NUMBER:
                        return cellData.getNumberValue().toString().getBytes().length;
                    default:
                        return -1;
                }
            }
        }
    }
}
封装工具类EasyExcelUtil.class
/**
 * @author YuanJie
 * @projectName vector-server
 * @package com.vector.common.utils
 * @className com.vector.common.utils.easyexcel.EasyExcelUtil
 * @copyright Copyright 2020 vector, Inc All rights reserved.
 * @date 2023/8/26 1:17
 */
@Slf4j
public class EasyExcelUtil {

    private static final String DATE_FORMAT = "yyyy-MM-dd";


    /**
     * 设置批量存储最大值,也影响sheet页数
     */
    private static final Integer MAX_SHEET_DATA = 50000;
    /**
     * 设置内存最大值
     */
    private static final Integer MAX_MEMORY_DATA = 1000;

    /**
     * 使用EasyExcel生成Excel  xls
     *
     * @param response      响应对象
     * @param fileNameParam 文件名
     * @param sheetName     表格名
     * @param clazz         导出实体类
     * @param t             查库入参
     * @param func          流式查询方法 Cursor<ResultVo> listOrders(@Param("userName") String userName);
     */
    public static <T> void writeExcelXls(HttpServletResponse response, String fileNameParam,
                                         String sheetName, Class<?> clazz, T t,
                                         Function<T, Cursor<?>> func) throws Exception {
        streamExportExcel(response, fileNameParam, sheetName, clazz, ExcelTypeEnum.XLS.getValue(), t, func);
    }

    /**
     * 使用EasyExcel生成Excel  xlsx
     *
     * @param response      响应对象
     * @param fileNameParam 文件名
     * @param sheetName     表格名
     * @param clazz         导出实体类
     * @param t             查库入参
     * @param func          流式查询方法 Cursor<ResultVo> listOrders(@Param("userName") String userName);
     */
    public static <T> void writeExcelXlsx(HttpServletResponse response, String fileNameParam,
                                          String sheetName, Class<?> clazz, T t,
                                          Function<T, Cursor<?>> func) throws Exception {
        streamExportExcel(response, fileNameParam, sheetName, clazz, ExcelTypeEnum.XLSX.getValue(), t, func);
    }

    /**
     * 流式导出 Excel
     *
     * @param response      响应对象
     * @param fileNameParam 文件名
     * @param sheetName     表格名
     * @param clazz         导出实体类
     * @param excelType     导出类型
     * @param t             查库入参
     * @param func          流式查询方法 Cursor<ResultVo> listOrders(@Param("userName") String userName);
     * @throws Exception 异常
     */
    private static <T> void streamExportExcel(HttpServletResponse response, String fileNameParam,
                                              String sheetName, Class<?> clazz, String excelType,
                                              T t, Function<T, Cursor<?>> func) throws Exception {
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATE_FORMAT);
        String fileName = fileNameParam + dateTimeFormatter.format(LocalDateTime.now()) + excelType;
        ExcelWriter excelWriter = EasyExcel
                .write(getOutputStream(fileName, response, excelType), clazz)
                .registerWriteHandler(new CustomCellWeightStrategy())
                .build();

        // 内容样式
        WriteCellStyle contentWriteCellStyle = new WriteCellStyle();
        // 水平居中
        contentWriteCellStyle.setHorizontalAlignment(HorizontalAlignment.CENTER);
        // 垂直居中
        contentWriteCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        // 设置自动换行,前提内容中需要加「\n」才有效
        contentWriteCellStyle.setWrapped(true);
        // 这个策略是 头是头的样式 内容是内容的样式 其他的策略可以自己实现
        HorizontalCellStyleStrategy horizontalCellStyleStrategy =
                new HorizontalCellStyleStrategy(null, contentWriteCellStyle);
        Cursor<?> cursor;
        List<Object> list = new ArrayList<>();
        int page = 0;
        WriteSheet writeSheet = EasyExcel
                .writerSheet(++page, sheetName + page)
                .registerWriteHandler(horizontalCellStyleStrategy)
                .build();
        // 流式数据库查询
        cursor = func.apply(t);
        int count = 0;
        try {
            for (Object o : cursor) {
                list.add(o);
                if(list.size() == MAX_MEMORY_DATA){
                    count += list.size();
                    excelWriter.write(list, writeSheet);
                    list.clear();
                    // 每个sheet页最大存储MAX_SHEET_DATA条数据
                    if (count == MAX_SHEET_DATA) {
                        writeSheet = EasyExcel
                                .writerSheet(++page, sheetName + page)
                                .registerWriteHandler(horizontalCellStyleStrategy)
                                .build();
                    }
                }
            }
            // 处理最后不足MAX_SHEET_DATA的数据
            if (list.size() > 0) {
                writeSheet = EasyExcel
                        .writerSheet(++page, sheetName + page)
                        .registerWriteHandler(horizontalCellStyleStrategy)
                        .build();
                excelWriter.write(list, writeSheet);
                list.clear();
            }
        } catch (Exception e) {
            // 重置response
            response.reset();
            response.setContentType("application/json");
            response.setCharacterEncoding("utf-8");
            String json = JacksonInstance.toJson(R.errorResult(EnumHttpCode.SYSTEM_ERROR, "下载文件失败" + e.getMessage()));
            response.getWriter().println(json);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
            if (excelWriter != null) {
                excelWriter.finish();
            }
        }
    }

    /**
     * 导出文件时为Writer生成OutputStream
     *
     * @param finalName 文件名
     * @param response 响应对象
     * @param excelType 导出文件类型
     * @return OutputStream
     */
    private static OutputStream getOutputStream(String finalName, HttpServletResponse response, String excelType) throws Exception {
        response.reset();
        finalName = URLEncoder.encode(finalName, StandardCharsets.UTF_8);
        if (ExcelTypeEnum.XLS.getValue().equals(excelType)) {
            response.setContentType("application/vnd.ms-excel");
        } else if (ExcelTypeEnum.XLSX.getValue().equals(excelType)) {
            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        }
        response.setCharacterEncoding("utf8");
        response.setHeader("Content-Disposition", "attachment; filename=" + finalName);
        response.setHeader("Pragma", "public");
        response.setHeader("Cache-Control", "no-store");
        response.addHeader("Cache-Control", "max-age=0");
        return response.getOutputStream();
    }
}
导出实体 Dto.class
@Data
public class Dto {
    @NumberFormat("#")
    @ExcelProperty(value = "地市编码", index = 0)
    Long code;
    @ExcelProperty(value = "地市名称", index = 1)
    String name;
    @NumberFormat("#")
    @ExcelProperty(value = "地市级别", index = 2)
    Integer level;
    @NumberFormat("#")
    @ExcelProperty(value = "地市父编码", index = 3)
    Long pcode;
    @NumberFormat("#")
    @ExcelProperty(value = "地市父名称", index = 4)
    Integer category;
}
测试用例 MeTestController.class
    @Resource
    private ExportMapper exportMapper;

    @Resource
    private HttpServletRequest request;
    @Resource
    private HttpServletResponse response;
    @GetMapping("/export")
    @Transactional
    public void export() throws Exception {
        Long params = 110101001000L;
        EasyExcelUtil.writeExcelXlsx(
                response,
                "地市信息",
                "地市区域",
                Dto.class,
                params,
                param -> exportMapper.export(null));
    }
测试Dao ExportMapper.class
public interface ExportMapper {
    @Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
    @ResultType(Dto.class)
    @Select("select * from area_code_2023")
    Cursor<Dto> export(@Param("params") Long params);
}


二. 传统分页导出查询

大表的深度分页性能很差,也受制于表设计的影响
@Test
public void testQuery2()  {

    // 1、定义资源
    Connection connection = null;
    ResultSet resultSet = null;
    PreparedStatement statement = null;
    String sql = "select * from user limit ?,?";
    try {
        // 获取连接
        connection = DBUtil.getConnection();
        // 获取使用预编译的statement
        statement = connection.prepareStatement(sql);
        // 获取结果集
        long start = System.currentTimeMillis();
        long begin = 0L, offset = 10000L;
        while (true){
            statement.setLong(1,begin);
            statement.setLong(2,offset);
            begin += offset;
            resultSet = statement.executeQuery();
            boolean flag = resultSet.next();
            if(!flag) break;
            while (flag){
                System.out.println("name---->" + resultSet.getString("nick_name") );
                flag = resultSet.next();
            }
        }
        long end = System.currentTimeMillis();
        System.out.println(end -start);
    } catch (SQLException e){
        e.printStackTrace();
    } finally {
        // 关闭资源
        DBUtil.closeAll(connection,statement,resultSet);
    }
}

三. 流式查询概念

采用传统的Stream流式思想,将直接提供数据替换成提供获取数据的管道,客户端读取数据时直接从管道中遍历获取;整个读取的过程需要客户端保持和服务端的连接,也很好理解,它实际是一个管道,管道得通着才能取数据。

采用传统的Stream流式思想,将直接提供数据替换成提供获取数据的管道,客户端读取数据时直接从管道中遍历获取;整个读取的过程需要客户端保持和服务端的连接,也很好理解,它实际是一个管道,管道得通着才能取数据。

流式查询有两种使用方式,一种是用Cursor作为返回值,对数据进行遍历操作;一种是不设置返回值,在入参中传入一个ResultHandler作为回调处理数据。本文将基于Mybatis具体介绍使用方法。 这两种返回值的使用方式是相似的,唯一区别就是返回值不同。Mybatis查询有两种方式,一种是基于注解加在Mapper接口上方,一种是写在xml文件中,主要需要设置以下几个属性:
ResultSetType结果集读取方式
FetchSizeMySQL服务端单次发送至客户端的数据条数
ResultType这个眼熟吧,设置返回实体类映射

ResultSetType有4种可选项

DEFAULT(-1),
FORWARD_ONLY(1003),
SCROLL_INSENSITIVE(1004),
SCROLL_SENSITIVE(1005);

FORWARD_ONLY顾名思义只能向前,即数据只能向前读取,是不是就类似一个流水的管道,读一条就相当于水流过去一些。也是我们需要选用的。
SCROLL_INSENSITIVE不敏感滚动,和下面那个差不多,都是可以向后读或向前读;这意味着已读取过的数据不能丢掉,要继续保存在内存中,因为有可能会回去再次读取他们。
SCROLL_SENSITIVE敏感滚动,和上面那个差不多。
这么一比较就看得出来,当选的一定是FORWARD_ONLY,我们亟需解决的就是大数据量对内存的影响,再用后面两个还是会放在内存中。

FetchSize这个概念在许多服务中都有提及,例如RabbitMQ中是消费者取过来预处理的消息数量,但在MySQL中完全不是一个概念。MySQL的数据传输是基于C/S的阻塞机制,即Client设置FetchSize = 1000,而Server查出来10000条数据,按照常理应该是Server智能地使用分页策略1000条1000条取;实际不是,Server查出来多少就是多少,他会放在自己特定的内存空间内,只是会根据FetchSize的大小一点一点传送给Client——利用C/S的通讯阻塞,发1000条、堵一下、发1000条、堵一下……。

JDBC官方给出的答案是设置为“Integer.MIN_VALUE”,具体原因不清楚,但我猜是为了和游标查询区分开,因为一会你会发现流式查询和游标查询唯一的区别就是FetchSize的大小。

注解式

@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(ResultVo.class)
@Select("SELECT *, 0 orderType FROM `table`\n" +
        "        WHERE username = #{userName}")
Cursor<ResultVo> listOrders(@Param("userName") String userName);
 
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = Integer.MIN_VALUE)
@ResultType(ResultVo.class)
@Select("SELECT *, 0 orderType FROM `table`\n" +
        "        WHERE username = #{userName}")
void listOrders2(@Param("userName") String userName, ResultHandler<ResultVo> handler);

使用Mybatis的注解,在 @Options 中指定查询配置参数,在@ResultType中指定返回值类型 ,在 @Select中指定查询语句。最后用Cursor接收返回值,Cursor是可遍历的,所以直接Foreach遍历即可;或者返回void 用ResultHandler处理数据回调,在调用方式时传入new ResultHandler并写明处理逻辑。

xml式

<select id="listOrders" resultType="com.vo.ResultVo" resultSetType="FORWARD_ONLY" fetchSize="Integer.MIN_VALUE">
    SELECT *, 1 stuffCount, 1 orderType FROM `table`
    WHERE username = #{userName}
</select>

需要注意的是,不可以注解 + xml混合使用,比如注解指定fetchSize,xml只写查询语句,这种只有xml语句会生效!!!要不全用注解,要不全用xml!!!

流式查询由于需要保持客户端与服务端的连接,而一般查询提交完连接就会关闭;因此我们需要保持事务开启,否则会报“A Cursor is already closed.”,即Cursor已经关闭,没法再读取了。最简单的方法就是在方法上加@Transactional,在查询完毕以前事务会一直持有这个数据库连接,但我们在使用完毕后也要自行关闭连接,显式调用Cursor.close(),或者用try with resource语句。

游标查询和流式查询的区分是fetchSize = Integer.MIN_VALUE

Cursor 还提供了三个方法:

  1. isOpen():用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
  2. isConsumed():用于判断查询结果是否全部取完;
  3. getCurrentIndex():返回已经获取了多少条数据。
try(Cursor cursor = OrderMapper.listOrders()) {
  cursor.forEach(rowObject -> {
      // ...
  });
}

  OrderMapper.listOrders2(queryWrapper,resultContext -> {
    ResultVo result = resultContext.getResultObject();
   //这边循环调用就可以实现业务了

}

游标查询

和流式查询类似fetchSize不设置为MIN_VALUE即可
JDBC查询默认是不支持FetchSize属性的,需要在JDBC连接URL后面加上**“useCursorFetch=true”。**

useCursorFetch=true 是针对 MySQL 数据库的 JDBC 连接参数,用于启用服务器端游标获取数据。在 MyBatis 中,当使用流式查询(例如:分页查询、结果集处理和使用游标等)时,这个配置可以帮助逐行从服务器检索数据,而不是一次性将所有数据加载到内存中,从而降低内存占用。

当使用 MySQL 数据库时,在 JDBC 连接字符串中加入 useCursorFetch=true,并结合设置合适的 fetchSize,可以避免因一次性加载过多数据导致的内存溢出问题。注意,此配置仅对 MySQL 数据库有效。 如果不设置 useCursorFetch=true 这个配置,仅使用之前提到的那些配置(如设置 defaultFetchSize、分页查询、结果集处理和使用游标等),在大多数情况下,这些配置仍然可以有效地避免查询导致的内存溢出。

但需要注意的是,对于 MySQL 数据库,如果不启用服务器端游标获取数据,这可能会影响到流式查询的效果。因为在默认情况下,MySQL JDBC 驱动会一次性将所有数据加载到内存中。此时,即使使用了其他配置,也可能无法达到预期的内存优化效果。

总的来说,在使用 MySQL 数据库时,推荐在 JDBC 连接字符串中加入 useCursorFetch=true 配置,以更好地支持流式查询和降低内存占用。在其他数据库中,可以根据实际需求和场景选择合适的配置和策略来避免查询导致的内存溢出。

还要知道如何判断自己是否使用了流式查询或游标查询,下面是几个数据集的对应关系

普通分页ResultsetRowsStaticRowDataStatic
查询方式结果集类型行数据类型
流式查询ResultsetRowsStreamingRowDataDynamic
游标查询ResultsetRowsCursorRowDataCursor

这3种查询方式,常规非大数据模式下普通查询最快,其次是流式查询,最次是游标查询.

主要是由于游标查询需要和数据库进行多次网络交互,Client处理完这部分后再拉取下一部分数据,因此会比较慢。但是流式查询又会长时间占用同一个数据库连接,因此要取舍一下是能接受连接一直持有但是可能会堵住导致响应慢,还是可能占用较多连接数但单次响应快。当通过流式查询获取一个ResultSet后,在你通过next迭代出所有元素之前或者调用close关闭它之前,你不能使用同一个数据库连接去发起另外一个查询,否者抛出异常(第一次调用的正常,第二次的抛出异常)。

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

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

相关文章

Docker Compose 安装使用 教程

Docker Compose 1.1 简介 Compose 项目是 Docker 官方的开源项目&#xff0c;负责实现对 Docker 容器集群的 快速编排 。从功能上看&#xff0c;跟 OpenStack 中的 Heat 十分类似。 其代码目前在 https://github.com/docker/compose 上开源。 Compose 定位是 「定义和运行多个…

list【1】介绍与使用(超详解哦)

list的介绍与使用 引言list介绍接口使用默认成员函数迭代器容量元素访问数据修改 list的算法接口总结 引言 继vector之后&#xff0c;我们继续来介绍STL容器&#xff1a;list 对于容器的使用其实有着类似的模式&#xff0c;参考之前vector的使用可以让我们更快的上手&#xff…

Focal Loss-解决样本标签分布不平衡问题

文章目录 背景交叉熵损失函数平衡交叉熵函数 Focal Loss损失函数Focal Loss vs Balanced Cross EntropyWhy does Focal Loss work? 针对VidHOI数据集Reference 背景 Focal Loss由何凯明提出&#xff0c;最初用于图像领域解决数据不平衡造成的模型性能问题。 交叉熵损失函数 …

嵌入式底层驱动需要知道的基本知识

先说结论&#xff0c;能&#xff0c;肯定能&#xff0c;必须能&#xff01; 但是&#xff0c;问题重点在于坚持&#xff0c;程序员这一行 &#xff0c;下班回家一般都要10点了&#xff0c;再刷两个小时枯燥的学习视频&#xff0c;我想大多数人是坚持不下来的。 但是&#xff…

ABB D674A906U01流量计变送器模块

流量测量&#xff1a; 该模块用于准确测量液体或气体的流量&#xff0c;通常以标准单位&#xff08;如立方米每小时或加仑每分钟&#xff09;表示。 传感器技术&#xff1a; 它通常使用各种传感器技术&#xff08;例如涡轮、电磁、超声波等&#xff09;来检测流体的流动并进行…

冠达管理:股票停牌后会大涨吗?

股票停牌是指证券买卖所为了保护市场秩序、保护出资者利益等原因暂时中止某些股票的买卖。但是&#xff0c;股票停牌前的股价与停牌后的股价会有什么不同呢&#xff1f;股票停牌后是否会大涨呢&#xff1f;在本文中&#xff0c;咱们将从多个视点进行剖析&#xff0c;以帮助人们…

合宙Air724UG LuatOS-Air LVGL API控件--按钮 (Button)

按钮 (Button) 按钮控件&#xff0c;这个就不用多说了&#xff0c;界面的基础控件之一。 示例代码 – 按键回调函数 event_handler function(obj, event) if event lvgl.EVENT_CLICKED then print(“Clicked\n”) elseif event lvgl.EVENT_VALUE_CHANGED then print(“To…

java.lang.IllegalStateException: Unable to find

java.lang.IllegalStateException: Unable to find a SpringBootConfiguration, you need to use ContextConfiguration or SpringBootTest(classes…) with your test 错误场景&#xff1a;在使用mybatisplus做测试时&#xff0c;出现此错误 解决方案&#xff1a;SpringBoot…

【MCU】SD NAND芯片之国产新选择

文章目录 前言传统SD卡和可贴片SD卡传统SD卡可贴片SD卡 实际使用总结 前言 随着目前时代的快速发展&#xff0c;即使是使用MCU的项目上也经常有大数据存储的需求。可以看到经常有小伙伴这样提问&#xff1a; 大家好&#xff0c;请问有没有SD卡芯片&#xff0c;可以直接焊接到P…

python可视化matplotlib——绘制正弦和余弦

这是一个使用matplotlib库绘制正弦和余弦函数曲线的代码示例。代码中导入了需要的库&#xff0c;并设置了x轴和y轴的标签字体为华文楷体。然后&#xff0c;使用numpy生成一组x轴上的值t&#xff0c;并使用正弦函数生成对应的y轴值s&#xff0c;再使用余弦函数生成对应的y轴值z。…

使用Tampermonkey(篡改猴)向页面注入js脚本

一、Tampermonkey 简单介绍 Tampermonkey是一款浏览器插件&#xff0c;适用于Chrome、Microsoft Edge、Safari、Opera Next 和 Firefox。他允许我们自定义javascript给指定网页添加功能&#xff0c;或修改现有功能。也可以用来辅助调试&#xff0c;或去除网页广告等。 官网地…

Vulkan LoaderLayer

目录 一、Loader The Loader 二、Layer 调度链Dispatch Chains JSON 一、Loader Vulkan是一个层架构&#xff0c;由Vulkan ApplicationLoaderLayerICDs(Installable Client Drivers)组成。 Vulkan 是一个显式 API&#xff0c;可以直接控制 GPU 的实际工作方式。因此&#x…

get请求报错400 非法参数

get请求报错400 非法参数 背景&#xff1a;get请求数据&#xff0c;SpringBoot提供接口&#xff0c;返回400&#xff0c;报错非法参数此种情况排除接口本身错误之外&#xff0c;检查参数中有没有特殊字符 " < > [ \ ] ^ { | } 我这边就是因为其中一个参数中有中括…

数字货运保持深层角力,满帮业绩与投资价值双丰收

上半年&#xff0c;经济持续活跃&#xff0c;货运物流行业承担着帮助经济要素流通的职责&#xff0c;也成为最直接的受益者。 数字货运平台满帮8月23日发布的财报显示&#xff0c;2023年第二季度&#xff0c;其平台单量、用户量均取得显著增长&#xff0c;并带动平台业绩创下新…

【网络设备】交换机的概念、工作原理、功能以及以太网帧格式

个人主页&#xff1a;insist--个人主页​​​​​​ 本文专栏&#xff1a;网络基础——带你走进网络世界 本专栏会持续更新网络基础知识&#xff0c;希望大家多多支持&#xff0c;让我们一起探索这个神奇而广阔的网络世界。 目录 一、认识交换机 二、交换机的主要功能 1、数…

Android——基本控件(下)(十九)

1. 菜单&#xff1a;Menu 1.1 知识点 &#xff08;1&#xff09;掌握Android中菜单的使用&#xff1b; &#xff08;2&#xff09;掌握选项菜单&#xff08;OptionsMenu&#xff09;的使用&#xff1b; &#xff08;3&#xff09;掌握上下文菜单&#xff08;ContextMenu&am…

STM32F4_SD卡

目录 前言 1. SDIO协议简介 2. SDIO命令及响应 3. SD卡的操作模式及切换 4. STM32的SDIO接口 5. SDIO结构体 6. SDIO相关寄存器 7. 实验程序 7.1 main.c 7.2 SDIO_Card.c 7.3 SDIO_Card.h 前言 在之前的单片机学习过程中&#xff0c;我们已经了解到了单片机系统都需…

SQL server开启变更数据捕获(CDC)

一、CDC简介 变更数据捕获&#xff08;Change Data Capture &#xff0c;简称 CDC&#xff09;&#xff1a;记录 SQL Server 表的插入、更新和删除操作。开启cdc的源表在插入、更新和删除操作时会插入数据到日志表中。cdc通过捕获进程将变更数据捕获到变更表中&#xff0c;通过…

java子类继承父类方法、或者接口中方法的javadoc注释

说明 详情可以阅读&#xff1a; https://docs.oracle.com/en/java/javase/19/docs/specs/javadoc/doc-comment-spec.html#method-comment-inheritance 子类继承父类、或者子类实现接口&#xff0c;在子类中为了避免重复写注释&#xff0c;可以在子类方法注释的主要描述部分、或…

基于GitHooks实现项目自动实时部署

目录 基于GitHooks实现项目自动部署 基于SVNJenkins发布项目 基于GitHooks实现项目自动部署 以上创建的所有任务&#xff0c;构建工作是基于在开发人员提交完代码到远程仓库完成&#xff0c;通知运维后&#xff0c;需要手动执行构建任务&#xff0c;这样就有些不太方便。我们…