• Druid数据库连接池使用体验


    写在前面

    在实际工作中我们我们使用较多的则是Spring默认的HikariDataSource数据库连接池,但是它无法提供可视化监控SQL这一能力,而这在很多场景下往往又是我们需要的功能,因此今天来学习阿里开源的一款优秀的数据库连接池---Druid。Druid能够提供强大的SQL监控和功能扩展能力,允许开发者根据需要进行二次开发。

    实战

    首先我们使用传统的方式,快速搭建一个具备查询用户信息的简单项目。

    项目初始化

    第一步,新建一个名为druid-sql的SpringBoot项目,选择spring webmybatis frameworkmysql driver依赖:

    第二步,修改application.properties配置文件信息:

    spring.datasource.driver-class-name=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql:///druid_sql?serverTimezone=Asia/Shanghai
    spring.datasource.username=root
    spring.datasource.password=root
    

    第三步,创建book这一数据表:

    USE druid_sql;
    DROP TABLE IF EXISTS book;
    CREATE TABLE book ( 
    id INT auto_increment PRIMARY KEY, 
    name VARCHAR ( 255 ), 
    price INT, 
    description VARCHAR ( 500 )
    ) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
    

    第四步,创建Book实体类:

    public class Book {
        private Integer id;
        private String name;
        private Integer price;
        private String description;
    
        //省略getter和setter方法
    }
    

    第五步,创建BookMapper的接口文件:

    @Mapper
    public interface BookMapper {
        List<Book> selectBookByName(String name);
    }
    

    第六步,创建BookMapper的XML文件:

    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.melody.druidsql.mapper.BookMapper">
        <select id="selectBookByName" resultType="com.melody.druidsql.entity.Book">
            select * from book where name=#{name}
        </select>
    </mapper>
    

    第七步,新建BookService类:

    @Service
    public class BookService {
        @Autowired
        private BookMapper bookMapper;
        
        public List<Book> selectBookByName(String name){
            return bookMapper.selectBookByName(name);
        }
    }
    

    第八步,新建BookController类:

    @RestController
    public class BookController {
        @Autowired
        private BookService bookService;
    
        @GetMapping("/book")
        public List<Book> selectBookByName(String name){
            return bookService.selectBookByName(name);
        }
    }
    

    第九步,添加数据进行测试,这些都是比较常规的操作了。通过查看源码,可以发现它使用的是HikariDataSource数据库连接池:

    接入Druid

    第一步,在POM文件中新增如下依赖:

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

    第二步,修改application.properties配置文件信息。首先配置WebStatFilter,它用于采集web-jdbc所关联的监控数据:

    # Druid相关配置
    # 开启WebStatFilter
    spring.datasource.druid.web-stat-filter.enabled=true
    # 配置拦截规则,这里设置所有
    spring.datasource.druid.web-stat-filter.url-pattern=/*
    # 排除一些不会涉及到 SQL 查询的 URL
    spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
    # 开启 session 统计功能
    spring.datasource.druid.web-stat-filter.session-stat-enable=true
    # 缺省 sessionStatMaxCount 值为 1000 ,开发者可按需进行配置
    spring.datasource.druid.web-stat-filter.session-stat-max-count=1000
    

    其次,配置StatViewServlet相关配置项,如下所示:

    # 配置StatViewServlet
    # 启用内置的监控页面
    spring.datasource.druid.stat-view-servlet.enabled=true
    # 内置监控页面的 URL 地址
    spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
    # 开启 Reset All 功能,注意值设置为false,按钮会显示,但是点击没有反应
    spring.datasource.druid.stat-view-servlet.reset-enable=true
    # 设置监控页面登录用户名
    spring.datasource.druid.stat-view-servlet.login-username=melody
    # 设置监控页面登录密码
    spring.datasource.druid.stat-view-servlet.login-password=melody
    # 设置白名单(如果allow没有配置或者为空,表示允许所有访问)
    spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
    # 黑名单(注意 deny 的优先级高于 allow,即只有在 deny 列表中,那么即使在 allow 列表中,也会被拒绝)
    spring.datasource.druid.stat-view-servlet.deny=
    

    最后设置过滤器, stat 过滤器用于监控 SQL 执行:

    # 过滤器设置
    spring.datasource.druid.filters=stat
    

    第三步,启动项目进行测试。首先访问http://localhost:8080/druid/login.html链接,页面如下所示:

    之后输入刚才设置的melody/melody信息进行登录,登录界面如下所示:

    可以看到Druid自带了很多功能,如:数据源、SQL监控、SQL防火墙、Web应用、URI监控、Session监控、Spring监控和JSON API等。

    先不管这些功能,我们先尝试访问一下如下链接:

    http://localhost:8080/book?name=java
    

    可以看到页面已经可以显示数据了:

    再看一下SQL监控,这里也将我们刚才调用接口执行SQL的执行过程给记录了:

    以上就是Druid的初体验了,接下来我们来分析Druid首页导航栏上的各个模块。

    Druid连接池介绍

    Druid连接池

    Druid连接池是阿里巴巴开源的数据库连接池项目。Druid连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防SQL注入,内置Loging能诊断Hack应用行为。

    竞品对比

    可以看出,Druid连接池在性能、监控、诊断、安全、扩展性这些方面远远超出竞品。

    对于连接池来说,连接池本身的性能消耗在整个调用链路中通常占比不大,连接池的性能关键点是,连接是否LRU的方式重用,是否支持PSCache(PreparedStatementCache)才是性能的关键点。当然DruidDataSource在没有使用Filter没有打开testOnBorrow的情况下,裸测也是极好。

    为监控而生

    Druid连接池最初就是为监控系统采集jdbc运行信息而生的,它内置了StatFilter功能,能采集非常完备的连接池执行信息。

    Druid连接池内置了能和Spring/Servlet关联监控的实现,使得监控Web应用特别方便。

    Druid连接池内置了一个监控页面,提供了非常完备的监控信息,可以快速诊断系统的瓶颈。

    监控信息采集的StatFilter

    Druid连接池的监控信息主要是通过StatFilter采集的,采集的信息非常全面,包括SQL执行、并发、慢查、执行时间区间分布等。

    监控不影响性能

    Druid增加StatFilter之后,能采集大量统计信息,同时对性能基本没有影响。StatFilter对CPU和内存的消耗都极小,对系统的影响可以忽略不计。监控不影响性能是Druid连接池的重要特性。

    SQL参数化合并监控

    实际业务中,如果SQL不是走PreparedStatement,SQL没有参数化,这时SQL需要参数化合并监控才能真实反映业务情况。如下SQL:

    select * from t where id = 1
    select * from t where id = 2
    select * from t where id = 3
    

    参数化后:

    select * from t where id = ?
    

    参数化合并监控是基于SQL Parser语法解析实现的,是Druid连接池独一无二的功能。

    执行次数、返回行数、更新行数和并发监控

    StatFilter能采集到每个SQL的执行次数、返回行数总和、更新行数总和、执行中次数和和最大并发。并发监控的统计是在SQL执行开始对计数器加一,结束后对计数器减一实现的。可以采集到每个SQL的当前并发和采集期间的最大并发。

    慢查监控

    缺省执行耗时超过3秒的被认为是慢查,统计项中有包括每个SQL的最后发生的慢查的耗时和发生时的参数。

    Exception监控

    如果SQL执行时抛出了Exception,SQL统计项上会Exception有最后的发生时间、堆栈和Message,根据这些信息可以很容易定位错误原因。

    区间分布

    SQL监控项上,执行时间、读取行数、更新行数都有区间分布,将耗时分布成8个区间:

    • 0 - 1 耗时0到1毫秒的次数
    • 1 - 10 耗时1到10毫秒的次数
    • 10 - 100 耗时10到100毫秒的次数
    • 100 - 1,000 耗时100到1000毫秒的次数
    • 1,000 - 10,000 耗时1到10秒的次数
    • 10,000 - 100,000 耗时10到100秒的次数
    • 100,000 - 1,000,000 耗时100到1000秒的次数
    • 1,000,000 - 耗时1000秒以上的次数

    记录耗时区间的发生次数,通过区间分布,可以很方便看出SQL运行的极好、普通和极差的分布。 耗时区分分布提供了“执行+RS时分布”,是将执行时间+ResultSet持有时间合并监控,这个能方便诊断返回行数过多的查询。

    诊断支持

    Druid连接池内置了LogFilter,将Connection/Statement/ResultSet相关操作的日志输出,可以用于诊断系统问题,也可以用于Hack一个不熟悉的系统。

    LogFilter可以输出连接申请/释放,事务提交回滚,Statement的Create/Prepare/Execute/Close,ResultSet的Open/Next/Close,通过LogFilter可以详细诊断一个系统的Jdbc行为。同时LogFilter有Log4j、Log4j2、Slf4j、CommsLog等实现。

    防SQL注入

    SQL注入攻击是黑客对数据库进行攻击的常用手段,Druid连接池内置了WallFilter提供防SQL注入功能,在不影响性能的同时防御SQL注入攻击。

    基于语意的防SQL注入

    Druid连接池内置了一个功能完备的SQL Parser,能够完整解析mysql、sql server、oracle、postgresql的语法,通过语意分析能够精确识别SQL注入攻击。

    极低的漏报率和误报率

    基于SQL语意分析,大量应用和反馈,使得Druid的防SQL注入拥有极低的漏报率和误报率。

    防注入对性能影响极小

    内置参数化后的Cache、高性能手写的Parser,使得打开防SQL注入对应用的性能基本不受影响。

    更多Druid的详细介绍,可以参考 这里 ,了解更多。

    首页

    Druid内置提供了一个StatViewServlet用于展示Druid的统计信息,这个StatViewServlet的用途包括:(1)提供监控信息展示的html页面;(2)提供监控信息的JSON API。

    首先我们查看一下这个StatViewServlet类的信息,可以发现它是一个静态内部类:

    public static class StatViewServlet {
        private boolean enabled;
        private String urlPattern;
        private String allow;
        private String deny;
        private String loginUsername;
        private String loginPassword;
        private String resetEnable;
    
        //setter和setter方法
    }
    

    也就是说,如果我们要配置StatViewServlet,启用内置的监控页面,首先需要在application.properties文件中新增如下配置:

    spring.datasource.druid.stat-view-servlet.enabled=true
    

    其次设置监控页面的访问URL地址,可通过urlPattern属性设置,如下设置时的内置监控页面的首页是/druid/index.html

    spring.datasource.druid.stat-view-servlet.url-pattern=/druid/*
    

    开发者可以通过loginUsername和loginPassword属性来设置登录信息:

    spring.datasource.druid.stat-view-servlet.login-username=melody
    spring.datasource.druid.stat-view-servlet.login-password=melody
    

    由于StatViewSerlvet展示出来的监控信息是系统运行的内部情况,因此数据比较敏感,如果开发者想要实现访问控制,可以配置allow和deny这两个属性。

    allow用于设置白名单(如果allow没有配置或者为空,表示允许所有访问),而deny用于设置黑名单,注意 deny 的优先级高于 allow,如果在deny列表中,就算在allow列表中,也会被拒绝:

    spring.datasource.druid.stat-view-servlet.allow=127.0.0.1
    spring.datasource.druid.stat-view-servlet.deny=
    

    在StatViewSerlvet输出的html页面中,有一个功能是Reset All,即执行这个操作后,会清零所有计数器,并重新计数。开发者你可通过resetEnable属性来实现该功能的启用与否:

    spring.datasource.druid.stat-view-servlet.reset-enable=true
    

    如果开发者想要配置Web关联监控,那么可以查看后面关于 URI监控 模块内容;配置Spring关联监控,那么可以查看后面关于 Web应用 模块内容。

    更多详细配置项,可以参考 这里 ,了解更多。

    数据源

    数据源详细记录了当前项目所使用的数据源信息,如登录用户名、地址、数据库类型、驱动类型、filter类名、连接设置、事务设置等内容:

    DruidDataSource是数据源属性配置类,查看一下该类的源码:

    public class DruidDataSource extends DruidAbstractDataSource implements DruidDataSourceMBean, ManagedDataSource, Referenceable, Closeable, Cloneable, ConnectionPoolDataSource, MBeanRegistration {
        private static final Log LOG = LogFactory.getLog(DruidDataSource.class);
        private static final long serialVersionUID = 1L;
        private volatile long recycleErrorCount;
        private long connectCount;
        private long closeCount;
        private volatile long connectErrorCount;
        private long recycleCount;
        ......
    }
    

    可以看到该类继承自DruidAbstractDataSource类,这个类是数据源属性的抽象类,之所以将数据源定义为抽象类是为了让其他子类可以在此基础上进行扩展,因为DruidDataSource大部分属性都是参考DBCP的:

    一般来说,开发者只需配置url(并不是此处看到的jdbcUrl),username、password和max-active这四项:

    spring.datasource.druid.url=jdbc:mysql:///druid_sql?serverTimezone=Asia/Shanghai
    spring.datasource.druid.username=root
    spring.datasource.druid.password=root
    spring.datasource.druid.max-active=20
    

    当然了,Druid也会根据URL来自动识别驱动类名称,无须开发者手动添加。举个例子,如果使用的是常见数据库如MySQL,可以使用旧的连接信息:

    spring.datasource.url=jdbc:mysql:///druid_sql?serverTimezone=Asia/Shanghai
    spring.datasource.username=root
    spring.datasource.password=root
    

    不需要在加一层druid前缀,这一点还是很方便的。如果连接的数据库是非常见数据库,那么必须添加driverClassName属性。

    更多详细配置项,可以参考 这里 ,了解更多。

    SQL监控

    请注意,要想使用SQL监控,首先必须设置对应的过滤器,这些过滤器都需要实现com.alibaba.druid.filter接口,可以看到它也有很多子类:

    由于此处是监控SQL,因此需要配置StatFilter,即在application.properties文件中新增如下配置:

    spring.datasource.druid.filters=stat
    

    如果你想使用后续的SQL防火墙,就需要使用wall这一配置项,自然想使用log4j2,就配置这一项,这些在数据源模块的Filter类名中就可以得到体现:

    SQL监控模块主要用于监控SQL,并展示SQL的执行数、执行时间、最慢、事务执行、错误数、更新行数、读取行数、执行中、最大并发、执行时间分布、执行+RS时分布、读取行分布和更新行分布等信息,其实这就是让开发者对整个项目中执行的SQL都有一个全局的了解:

    StatFilter的别名是stat,这个别名映射配置信息保存在druid-xxx.jar!/META-INF/druid-filter.properties文件中。

    更多详细配置项,可以参考 这里 ,了解更多。

    内置Filter的别名

    druid-xxx.jar!/META-INF/druid-filter.properties文件中配置了如下Filter的别名,因为我们是通过对应Filter的类名来设置对应的Filter别名,多个Filter可以组合使用:

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

    SQL防火墙

    WallFilter用于实现SQL防火墙,首先我们查看一下这个WallFilter类的信息,可以发现它是一个类:

    public class WallFilter extends FilterAdapter implements WallFilterMBean {
        private static final Log LOG = LogFactory.getLog(WallFilter.class);
        private boolean inited = false;
        private WallProvider provider;
        private String dbTypeName;
        private WallConfig config;
        private volatile boolean logViolation = false;
        private volatile boolean throwException = true;
        public static final String ATTR_SQL_STAT = "wall.sqlStat";
        public static final String ATTR_UPDATE_CHECK_ITEMS = "wall.updateCheckItems";
        private static final ThreadLocal<List<Integer>> tenantColumnsLocal = new ThreadLocal();
        ......
    }
    

    如果开发者想要启用这个WallFilter,那么需要在application.properties配置文件中通过Filter类名来进行设置:

    spring.datasource.druid.filters=wall
    

    当然了,还可以结合其他Filter一起使用,如下:

    spring.datasource.druid.filters=wall,stat
    

    但是这样设置会造成拦截检测的时间不在StatFilter统计的SQL执行时间内,所以如果希望拦截检测的时间在StatFilter统计的SQL执行时间内,需要调整两者的执行顺序:

    spring.datasource.druid.filters=stat,wall
    

    SQL防火墙主要分为5大部分:防御统计、表访问统计、函数调用统计、SQL防御统计的黑白名单。

    更多详细配置项,可以参考 这里 ,了解更多。

    Web应用

    它可以展示当前应用中的相关信息,如ContextPath、最大并发、请求次数、Jdbc执行数、读取行数和更新行数等一系列信息。

    URI监控

    WebStatFilter用于采集web-jdbc关联监控的数据,首先我们查看一下这个WebStatFilter类的信息,可以发现它是一个静态内部类:

    public static class WebStatFilter {
        private boolean enabled;
        private String urlPattern;
        private String exclusions;
        private String sessionStatMaxCount;
        private String sessionStatEnable;
        private String principalSessionName;
        private String principalCookieName;
        private String profileEnable;
    
        //setter和setter方法
    }
    

    因此我们首先需要在application.properties配置文件中开启WebStatFilter:

    spring.datasource.druid.web-stat-filter.enabled=true
    

    如果开发者想要针对部分URL进行拦截,那么可以使用如下配置,此处拦截所有,即所有的API访问都会被记录:

    spring.datasource.druid.web-stat-filter.url-pattern=/*
    

    当然了有些不会涉及到 SQL 查询的API,我们是希望可以排除掉,那么可以配置如下:

    spring.datasource.druid.web-stat-filter.exclusions=*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*
    

    当我们需要开启session统计功能时,可以使用如下配置项:

    spring.datasource.druid.web-stat-filter.session-stat-enable=true
    

    想要设置sessionStatMaxCount的值时,可以使用如下配置项,该值缺省为1000:

    spring.datasource.druid.web-stat-filter.session-stat-max-count=1000
    

    如果开发者想让Druid知道当前的session的用户是谁,可以配置principalSessionName,注意这个参数的值是user信息保存在session中的 sessionName,如果你session中保存的是非string类型的对象,那么需要重载toString方法:

    spring.datasource.druid.web-stat-filter.principal-session-name=
    

    如果你的user信息是存在cookie中,那么可以使用下面的方式同样可以配置principalSessionName,进而也能让Druid知道当前的session的用户是谁:

    spring.datasource.druid.web-stat-filter.principal-cookie-name=
    

    自Druid0.2.7版本开始就支持profile,配置profileEnable就能够监控单个url调用的sql列表:

    spring.datasource.druid.web-stat-filter.profile-enable=
    

    可以开启一下profile试试,然后多次调用book这一API,结果如下所示:

    更多详细配置项,可以参考 这里 ,了解更多。

    Spring监控

    Druid提供了Spring和Jdbc的关联监控,在使用的时候需要先阅读文档然后进行相应的配置,可点击 这里 进行阅读。

    JSON API

    如果开发者觉得此处展示的数据不太美观,那么可以根据提供的API来获取信息后自行展示。

    去除广告

    一般来说为了支持开源,不建议随便把广告去掉,但是在企业里面这个广告实在是有损形象,所以考虑再三还是决定去掉广告。

    首先F12查看源码:

    然后分析发现广告是由commons.js文件带出来的,该文件存放于ruid-xxx.jar!/support/http/resources/js/common.js文件中,可以访问http://localhost:8080/druid/js/common.js链接进行确认:

    再来查看一下该网页的源代码:

    调用的是init方法,再阅读一下源码:

    init : function() {
     this.buildFooter();
     druid.lang.init();
    },
    
    buildFooter : function() {
     var html ='';
     $(document.body).append(html);
    },,
    

    确认一下,也就说这个buildFooter方法用于显示页面底部的广告,而这个方法则是在init方法中调用的:

    所以要想去除广告,不调用这个buildFooter方法即可,但是源码这种除非反编译,否则是无法修改的。不过可以换种实现方式,可以写一个过滤器,先过滤对于commons.js文件的请求,之后再读取commons.js文件内容,并将this.buildFooter();这行代码用空字符串取代,最后再将这个文件返回就行。

    手动注册方式

    定义一个filter包,并在里面定义一个DeleteADFilter类需要实现Filter接口并重写其中的doFilter方法:

    @WebFilter(urlPatterns ="/druid/js/common.js")
    public class DeleteADFilter implements Filter {
        @Override
        public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
            servletResponse.resetBuffer();
            String content =  Utils.readFromResource("support/http/resources/js/common.js");
            content =  content.replace("this.buildFooter();","");
            servletResponse.getWriter().write(content);
        }
    }
    

    注意过滤的url地址必须和原地址保持一致,然后在项目启动类上扫描一下我们添加的DeleteADFilter类:

    @SpringBootApplication
    @ServletComponentScan("com.melody.druidsql.filter")
    public class DruidSqlApplication {
        public static void main(String[] args) {
            SpringApplication.run(DruidSqlApplication.class, args);
        }
    }
    

    之后重新启动项目,访问首页或者其他页面,可以发现广告已经去掉了:

    注意不能将项目启动类上的@ServletComponentScan("com.melody.druidsql.filter")注解去掉,转而在DeleteADFilter类上添加@Component,因为前者是注册,后者则是扫描,作用不同。相反如果你使用了@ServletComponentScan注解,那么Servlet可以直接通过@WebServlet注解自动注册;Filter可以直接通过@WebFilter注解自动注册;Listener可以直接通过@WebListener注解自动注册。

    自动注册

    当然了除了上面的手动注册方式外,我们还可以使用自动注册的方式。只需定义一个DruidSqlConfiguration类,然后提供一个FilterRegistrationBean实例即可,我们可以在该方法中书写拦截广告的逻辑:

    @Configuration
    public class DruidSqlConfiguration {
        @Bean
        @ConditionalOnProperty(name = {"spring.datasource.druid.stat-view-servlet.enabled"},havingValue = "true")
        public FilterRegistrationBean removeAdFilterRegistrationBean(DruidStatProperties druidStatProperties){
            //获取Web监控页面的参数
            DruidStatProperties.StatViewServlet statViewServlet = druidStatProperties.getStatViewServlet();
            //提取common.js的配置路径
            String urlPattern = statViewServlet.getUrlPattern() != null? statViewServlet.getUrlPattern():"/druid/*";
            String commonJsPattern = urlPattern.replaceAll("\\*","js/common.js");
            //定义过滤器
            Filter filter = new Filter() {
                @Override
                public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
                    String content = Utils.readFromResource("support/http/resources/js/common.js");
                    content =  content.replace("this.buildFooter();","");
                    servletResponse.getWriter().write(content);
                }
            };
            FilterRegistrationBean registrationBean = new FilterRegistrationBean();
            registrationBean.setFilter(filter);
            registrationBean.addUrlPatterns(commonJsPattern);
            return registrationBean;
        }
    }
    

    可以看到这种方式相比于前面那种更为巧妙,尤其是在当还需要往Spring容器主注册其他Bean的时候。

    当然了,还有许多功能,如数据库多数据源、配置数据库加密、保存Druid的监控记录等等,这些都将会在后续文章中进行介绍。

    欢迎关注微信公众号“啃饼思录”,博主等你来撩!

  • 相关阅读:
    今天是1024,获取一下纪念牌
    日积月累,在迷茫彷徨中前进 —— 我的大学经历
    自己理解的TCP三次握手
    自定义模块和第三方模块,cnmp
    只分享这一次。阿里软件架构师深入底层手写JDK源码
    windows- 怎么查看本地网卡速度
    NFT 的 10 种实际用途
    汽车托运汽车会产生公里数吗?
    C++ 数字
    几行代码,让黑白老照片重获新生!
  • 原文地址:https://www.cnblogs.com/kenbings/p/16325041.html