手撸Mybatis(三)——收敛SQL操作到SqlSession

news2024/11/24 3:19:25

本专栏的源码:https://gitee.com/dhi-chen-xiaoyang/yang-mybatis。

引言

在上一章中,我们实现了读取mapper配置并构造相关的mapper代理对象,读取mapper.xml文件中的sql信息等操作,现在,在上一章的基础上,我们接着开始链接数据库,通过封装JDBC,来实现我们数据库操作。

数据库准备

我们创建一个user表,用于后续进行测试,user表的结构如下图所示:
image.png
user表的内容如下:
image.png

添加User类

我们根据表结构,创建对应的user类,user类的结构如下:

package com.yang.mybatis.test;

import java.io.Serializable;
import java.util.Date;

public class User implements Serializable {
    private Integer id;
    
    private String userName;
    
    private String password;
    
    private Integer age;
    
    private Date createTime;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

JDBC基础操作

在使用jdbc之前,我们先引入mysql的依赖

<dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.27</version>
        </dependency>

jdbc的操作,一般分为下面几个步骤:
1)加载JDBC驱动程序
2)创建数据库链接
3)创建一个preparedStatement
4)执行SQL语句
5)遍历数据集
6)处理异常,关闭JDBC对象资源
示例代码如下:

 public static void main(String[] args) throws SQLException {
        String url = "jdbc:mysql://localhost:3306/test?useSSL=false";
        String username = "用户名";
        String password = "密码";

        Connection conn = null;
        try {
           // 1. 加载驱动
            Class.forName("com.mysql.cj.jdbc.Driver");
           // 2. 创建数据库链接
            conn = DriverManager.getConnection(url, username, password);
            conn.setAutoCommit(false);

           // 3. 创建preparedStatement
            String sql = "select user_name from user where id = ?";
            PreparedStatement preparedStatement = conn.prepareStatement(sql);
            preparedStatement.setInt(1, 1);
            // 4. 执行sql
           ResultSet resultSet = preparedStatement.executeQuery();
           // 5. 遍历结果
           if (resultSet.next()) {
                System.out.println(resultSet.getString(1));
            }
            conn.commit();
        } catch (SQLException | ClassNotFoundException e) {
            conn.rollback();
            e.printStackTrace();
        } finally {
          // 6. 释放连接
            conn.close();
        }
    }

执行上述代码,结果如下:
image.png

将sql操作,收敛到SqlSession

在上一章,当我们调用mapper的方法时,最终是通过在MapperProxy中获取对应的MybatisStatement,然后打印出sql信息的,但是如果后续操作数据库是,也在MapperProxy中执行sql的话,不太方便管理。因此,我们添加一个IMybatisSqlSession类,后续对于数据库的操作,收敛到此类进行。
首先,我们添加IMybatisSqlSession:

package com.yang.mybatis.session;

public interface IMybatisSqlSession {

    <T> T execute(String method, Object parameter);

    <T> T getMapper(Class<T> type);
}

然后添加其默认实现:

package com.yang.mybatis.session;

import com.yang.mybatis.proxy.MapperProxyFactory;

public class DefaultMybatisSqlSession implements IMybatisSqlSession {
    private MapperProxyFactory mapperProxyFactory;

    public DefaultMybatisSqlSession(MapperProxyFactory mapperProxyFactory) {
        this.mapperProxyFactory = mapperProxyFactory;
    }

    @Override
    public <T> T execute(String method, Object parameter) {
        return (T)("你被代理了!" + method + ",入参:" + parameter);
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        return (T) mapperProxyFactory.newInstance(type, this);
    }
}

添加IMybatisSqlSession的工厂接口

package com.yang.mybatis.session;

public interface MybatisSqlSessionFactory {
    IMybatisSqlSession openSession();
}

添加工厂的默认实现:

package com.yang.mybatis.session;

import com.yang.mybatis.proxy.MapperProxyFactory;

public class DefaultMybatisSqlSessionFactory implements IMybatisSqlSessionFactory {
    private MapperProxyFactory mapperProxyFactory;

    public DefaultMybatisSqlSessionFactory(MapperProxyFactory mapperProxyFactory) {
        this.mapperProxyFactory = mapperProxyFactory;
    }

    @Override
    public IMybatisSqlSession openSession() {
        return new DefaultMybatisSqlSession(mapperProxyFactory);
    }
}

修改MapperProxyFactory,在新建MapperProxy的时候,将imybatisSqlSession传递给MapperProxy。

package com.yang.mybatis.proxy;

import com.yang.mybatis.config.MybatisConfiguration;
import com.yang.mybatis.session.IMybatisSqlSession;

import java.lang.reflect.Proxy;

public class MapperProxyFactory {
    private MybatisConfiguration mybatisConfiguration;


    public MapperProxyFactory(MybatisConfiguration mybatisConfiguration) {
        this.mybatisConfiguration = mybatisConfiguration;
    }

    public Object newInstance(Class mapperType, IMybatisSqlSession mybatisSqlSession) {
        MapperProxy mapperProxy = new MapperProxy(mapperType, mybatisSqlSession);
        return Proxy.newProxyInstance(mapperType.getClassLoader(),
                new Class[]{mapperType},
                mapperProxy);
    }

    public MybatisConfiguration getMybatisConfiguration() {
        return mybatisConfiguration;
    }

    public void setMybatisConfiguration(MybatisConfiguration mybatisConfiguration) {
        this.mybatisConfiguration = mybatisConfiguration;
    }
}

然后修改MapperProxy,在真正执行的时候,通过iMybatisSqlSession的execute,来执行sql操作,以此实现sql操作由iMybatisSqlSession来统一管理。

package com.yang.mybatis.proxy;

import com.yang.mybatis.session.IMybatisSqlSession;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MapperProxy<T> implements InvocationHandler {

    private Class<T> mapperInterface;

    private IMybatisSqlSession sqlSession;

    public MapperProxy(Class<T> mapperInterface, IMybatisSqlSession mybatisSqlSession) {
        this.mapperInterface = mapperInterface;
        this.sqlSession = mybatisSqlSession;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        String methodName = this.mapperInterface.getName() + "." + method.getName();
        return sqlSession.execute(methodName, args);
    }
}

最后,我们添加sqlSession工厂类的创建者,通过创建者模式,来创建SqlSession工厂

package com.yang.mybatis.session;

import com.yang.mybatis.config.MybatisConfiguration;
import com.yang.mybatis.config.MybatisSqlStatement;
import com.yang.mybatis.config.parser.IMybatisConfigurationParser;
import com.yang.mybatis.config.parser.IMybatisMapperParser;
import com.yang.mybatis.proxy.MapperProxyFactory;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class MybatisSqlSessionFactoryBuilder {
    private IMybatisConfigurationParser mybatisConfigurationParser;

    private IMybatisMapperParser mybatisMapperParser;

    private String configPath;

    public IMybatisSqlSessionFactory buildSqlSessionFactory() {
        if (configPath == null || configPath.isEmpty()) {
            throw new RuntimeException("配置文件路径不合法==========");
        }
        if (this.mybatisMapperParser == null || this.mybatisConfigurationParser == null) {
            throw new RuntimeException("缺少解析器=======");
        }
        MybatisConfiguration mybatisConfiguration = mybatisConfigurationParser.parser(configPath);
        List<String> mapperPaths = mybatisConfiguration.getMapperPaths();
        for (String mapperPath : mapperPaths) {
            List<MybatisSqlStatement> mybatisSqlStatements = this.mybatisMapperParser.parseMapper(mapperPath);
            Map<String, List<MybatisSqlStatement>> mapperNameGroupMap = mybatisSqlStatements.stream()
                    .collect(Collectors.groupingBy(MybatisSqlStatement::getNamespace));

            for (Map.Entry<String, List<MybatisSqlStatement>> entry : mapperNameGroupMap.entrySet()) {
                String mapperName = entry.getKey();
                List<MybatisSqlStatement> sqlSessionList = entry.getValue();
                mybatisConfiguration.putMybatisSqlStatementList(mapperName, sqlSessionList);
            }
        }

        MapperProxyFactory mapperProxyFactory = new MapperProxyFactory(mybatisConfiguration);
        return new DefaultMybatisSqlSessionFactory(mapperProxyFactory);
    }

    public MybatisSqlSessionFactoryBuilder setConfigPath(String configPath) {
        this.configPath = configPath;
        return this;
    }

    public MybatisSqlSessionFactoryBuilder setMybatisConfigurationParser(IMybatisConfigurationParser iMybatisConfigurationParser) {
        this.mybatisConfigurationParser = iMybatisConfigurationParser;
        return this;
    }

    public MybatisSqlSessionFactoryBuilder setMybatisMapperParser(IMybatisMapperParser iMybatisMapperParser) {
        this.mybatisMapperParser = iMybatisMapperParser;
        return this;
    }
}

添加测试代码,进行测试:

package com.yang.mybatis.test;

import com.yang.mybatis.config.parser.XmlMybatisConfigurationParser;
import com.yang.mybatis.config.parser.XmlMybatisMapperParser;
import com.yang.mybatis.session.IMybatisSqlSession;
import com.yang.mybatis.session.IMybatisSqlSessionFactory;
import com.yang.mybatis.session.MybatisSqlSessionFactoryBuilder;


public class Main {
    public static void main(String[] args) {
        String configPath = "mybatis-config.xml";
        IMybatisSqlSessionFactory mybatisSqlSessionFactory = new MybatisSqlSessionFactoryBuilder()
                .setMybatisMapperParser(new XmlMybatisMapperParser())
                .setMybatisConfigurationParser(new XmlMybatisConfigurationParser())
                .setConfigPath(configPath)
                .buildSqlSessionFactory();

        IMybatisSqlSession mybatisSqlSession = mybatisSqlSessionFactory.openSession();

        IUserMapper userMapper = mybatisSqlSession.getMapper(IUserMapper.class);
        System.out.println(userMapper.queryUserName(1));
    }
}

image.png

SqlSession获取执行方法对应的sql语句

首先,修改MybatisConfiguration,之前是将每一个mapper和他所拥有的MybatisSqlStatement列表关联起来,现在感觉粒度太大,因此,该为每一个mapper的方法和对应的MybatisSqlStatement关联。

package com.yang.mybatis.config;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class MybatisConfiguration implements Serializable {
    private Map<String, MybatisEnvironment> id2EnvironmentMap = new HashMap<>();

    private MybatisEnvironment defaultMybatisEnvironment;

    private List<String> mapperPaths = new ArrayList<>();

    private Map<String, MybatisSqlStatement> mapperMethod2SqlStatementsMap = new HashMap<>();

    public void putMapperMethod2MybatisSqlStatement(String mapperMethod, MybatisSqlStatement mybatisSqlStatement) {
        this.mapperMethod2SqlStatementsMap.put(mapperMethod, mybatisSqlStatement);
    }

    public MybatisSqlStatement getMybatisSqlStatement(String mapperMethod) {
        return this.mapperMethod2SqlStatementsMap.get(mapperMethod);
    }

    public Map<String, MybatisSqlStatement> getMapperMethod2SqlStatementsMap() {
        return this.mapperMethod2SqlStatementsMap;
    }


    public void addEnvironment(String id, MybatisEnvironment mybatisEnvironment) {
        this.id2EnvironmentMap.put(id, mybatisEnvironment);
    }

    public MybatisEnvironment getEnvironment(String id) {
        return id2EnvironmentMap.get(id);
    }

    public MybatisEnvironment getDefaultMybatisEnvironment() {
        return defaultMybatisEnvironment;
    }

    public void setDefaultMybatisEnvironment(MybatisEnvironment defaultMybatisEnvironment) {
        this.defaultMybatisEnvironment = defaultMybatisEnvironment;
    }

    public void addMapperPath(String mapperPath) {
        this.mapperPaths.add(mapperPath);
    }

    public List<String> getMapperPaths() {
        return this.mapperPaths;
    }

    public List<MybatisEnvironment> getEnvironments() {
        return new ArrayList<>(id2EnvironmentMap.values());
    }

}

然后修改MybatisSqlSessionFactoryBuilder:

package com.yang.mybatis.session;

import com.yang.mybatis.config.MybatisConfiguration;
import com.yang.mybatis.config.MybatisSqlStatement;
import com.yang.mybatis.config.parser.IMybatisConfigurationParser;
import com.yang.mybatis.config.parser.IMybatisMapperParser;
import com.yang.mybatis.proxy.MapperProxyFactory;

import java.util.List;

public class MybatisSqlSessionFactoryBuilder {
    private IMybatisConfigurationParser mybatisConfigurationParser;

    private IMybatisMapperParser mybatisMapperParser;

    private String configPath;

    public IMybatisSqlSessionFactory buildSqlSessionFactory() {
        if (configPath == null || configPath.isEmpty()) {
            throw new RuntimeException("配置文件路径不合法==========");
        }
        if (this.mybatisMapperParser == null || this.mybatisConfigurationParser == null) {
            throw new RuntimeException("缺少解析器=======");
        }
        MybatisConfiguration mybatisConfiguration = mybatisConfigurationParser.parser(configPath);
        List<String> mapperPaths = mybatisConfiguration.getMapperPaths();
        for (String mapperPath : mapperPaths) {
            List<MybatisSqlStatement> mybatisSqlStatements = this.mybatisMapperParser.parseMapper(mapperPath);
            for (MybatisSqlStatement mybatisSqlStatement : mybatisSqlStatements) {
                String methodName = mybatisSqlStatement.getNamespace() + "." + mybatisSqlStatement.getId();
                mybatisConfiguration.putMapperMethod2MybatisSqlStatement(methodName, mybatisSqlStatement);
            }
        }

        MapperProxyFactory mapperProxyFactory = new MapperProxyFactory(mybatisConfiguration);
        return new DefaultMybatisSqlSessionFactory(mapperProxyFactory);
    }

    public MybatisSqlSessionFactoryBuilder setConfigPath(String configPath) {
        this.configPath = configPath;
        return this;
    }

    public MybatisSqlSessionFactoryBuilder setMybatisConfigurationParser(IMybatisConfigurationParser iMybatisConfigurationParser) {
        this.mybatisConfigurationParser = iMybatisConfigurationParser;
        return this;
    }

    public MybatisSqlSessionFactoryBuilder setMybatisMapperParser(IMybatisMapperParser iMybatisMapperParser) {
        this.mybatisMapperParser = iMybatisMapperParser;
        return this;
    }
}

修改DefaultMybatisSqlSession类,获取方法对应的MybatisStatement

package com.yang.mybatis.session;

import com.yang.mybatis.config.MybatisSqlStatement;
import com.yang.mybatis.proxy.MapperProxyFactory;


public class DefaultMybatisSqlSession implements IMybatisSqlSession {
    private MapperProxyFactory mapperProxyFactory;

    public DefaultMybatisSqlSession(MapperProxyFactory mapperProxyFactory) {
        this.mapperProxyFactory = mapperProxyFactory;
    }

    @Override
    public <T> T execute(String method, Object parameter) {
        MybatisSqlStatement mybatisSqlStatement = this.mapperProxyFactory.getMybatisConfiguration().getMybatisSqlStatement(method);
        return (T)("method:" + method + "sql:"+ mybatisSqlStatement.getSql() + ",入参:" + parameter);
    }

    @Override
    public <T> T getMapper(Class<T> type) {
        return (T) mapperProxyFactory.newInstance(type, this);
    }
}

添加测试方法进行测试:

 public static void main(String[] args) {
        String configPath = "mybatis-config.xml";
        MybatisSqlSessionFactory mybatisSqlSessionFactory = new MybatisSqlSessionFactoryBuilder()
                .setMybatisMapperParser(new XmlMybatisMapperParser())
                .setMybatisConfigurationParser(new XmlMybatisConfigurationParser())
                .setConfigPath(configPath)
                .buildSqlSessionFactory();

        IMybatisSqlSession mybatisSqlSession = mybatisSqlSessionFactory.openSession();

        IUserMapper userMapper = mybatisSqlSession.getMapper(IUserMapper.class);
        System.out.println(userMapper.queryUserAge(1));
    }

测试结果如下:
image.png

参考文章

https://blog.csdn.net/weixin_43520450/article/details/107230205

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

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

相关文章

利用大语言模型(KIMI)构建控制信息模型

数字化的核心是数字化建模&#xff0c;为一个事物构建数字模型是一项十分复杂的工作。不同的应用场景&#xff0c;对事物的关注重点的不同的。例如&#xff0c;对于一个智能传感器而言&#xff0c;从商业的角度看&#xff0c;产品的信息模型中应该包括产品的类型&#xff0c;名…

IDEA 申请学生许可证

如果你有学生账号&#xff0c;并且账号是 EDU 结尾的&#xff0c;可以申请 IDEA 的学生许可证。 有效期一年&#xff0c;完全免费。 在界面上输入邮件地址&#xff0c;然后单击按钮提交。 邮件中单击链接 JetBrains 会把一个带有链接的邮件发送到你的邮箱中。 单击邮箱中的…

【Python文字识别】基于HyperLPR3实现车牌检测和识别(Python版本快速部署)

闲来无事&#xff0c;想复现一下网上的基于YOLO v5的单目测距算法。然后就突然想在这个场景下搞一下车牌识别&#xff0c;于是就有了这篇文章。今天就给大家分享基于HyperLPR3实现车牌检测和识别。 原创作者&#xff1a;RS迷途小书童 博客地址&#xff1a;https://blog.csdn.ne…

Qt 信号槽中信号重名解决办法

1、类似与Qt4中的写法&#xff1a; 2、函数指针 3、泛型 connect(ui->combox, QOverload<int>::of(&QCombox::currentIndexChanged), this ,&mainwindow::onindexchange);

【数学 排列组合】1643. 第 K 条最小指令

本文涉及知识点 数学 排列组合 LeetCode1643. 第 K 条最小指令 Bob 站在单元格 (0, 0) &#xff0c;想要前往目的地 destination &#xff1a;(row, column) 。他只能向 右 或向 下 走。你可以为 Bob 提供导航 指令 来帮助他到达目的地 destination 。 指令 用字符串表示&am…

每日一题-贪心算法

目录 前言 买入股票的最佳时机(1) 买入股票的最好时机(2) 前言 当你踏上贪心算法的旅程&#xff0c;仿佛置身于一场智慧的盛宴&#xff0c;每一步都是对问题解决方案的审慎选择&#xff0c;每一次决策都是对最优解的向往。贪心算法以其简洁高效的特性&#xff0c;被广泛运用于…

SSM+Vue在线OA办公系统

在线办公分三个用户登录&#xff0c;管理员&#xff0c;经理&#xff0c;员工。 SSM架构&#xff0c;maven管理工具&#xff0c;数据库Mysql&#xff0c;系统有文档&#xff0c;可有偿安装调试及讲解&#xff0c;项目保证质量。需要划到 最底 下可以联系到我。 功能如下&am…

Java 获取 Outlook 邮箱的日历事件

Java 获取 Outlook 邮箱的日历事件 1.需求描述2.实现方案3.运行结果 IDE&#xff1a;IntelliJ IDEA 2022.3.3 JDK&#xff1a;1.8.0_351 Outlook&#xff1a;Microsoft Office 2016 1.需求描述 比如现在需要获取 Outlook 邮箱中四月的全部的会议安排&#xff0c;如下图所示 …

指标完成情况对比查询sql

指标完成情况对比查询sql 1. 需求 2. SQL select--部门dept.name as bm,--年度指标任务-新签&#xff08;万元&#xff09;ndzbwh.nxqndzbrw as nxqndzbrw,--年度指标任务-收入&#xff08;万元&#xff09;ndzbwh.nsrndzbrw as nsrndzbrw,--年度指标任务-回款&#xff08;万…

软件工程毕业设计选题100例

文章目录 0 简介1 如何选题2 最新软件工程毕设选题3 最后 0 简介 学长搜集分享最新的软件工程业专业毕设选题&#xff0c;难度适中&#xff0c;适合作为毕业设计&#xff0c;大家参考。 学长整理的题目标准&#xff1a; 相对容易工作量达标题目新颖 1 如何选题 最近非常多的…

dnf游戏攻略:保姆级游戏攻略!

欢迎来到DNF&#xff0c;一个扣人心弦的2D横版格斗游戏世界&#xff01;无论你是新手还是老玩家&#xff0c;这篇攻略都将为你提供宝贵的游戏技巧和策略&#xff0c;助你在游戏中大展身手&#xff0c;成为一名强大的冒险者。 一、角色选择 在DNF中&#xff0c;角色的选择至关重…

Python的使用

1、打印&#xff1a;print&#xff08;‘hello’&#xff09; 2、Python的除法是数学意义上的除法 print&#xff08;2/3&#xff09; 输出&#xff1a;0.6666... 3、a18 a‘hello’ print(a) 可以直接输出 4、**2 表示2的平方 5、打印类型 print&#xff08;type&am…

安卓四大组件之Activity

目录 一、简介二、生命周期三、启动模式3.1 Standard3.2 Single Task3.3 SingleTop3.4 Single Instance3.5 启动模式的配置 四、Activity 的跳转和数据传递4.1 Activity 的跳转4.1.1 直接跳转4.1.2 回调 4.2 Activity 的数据传递4.2.1 传递普通数据4.2.2 传递一组数据4.2.3 传递…

【LinuxC语言】系统日志

文章目录 前言一、系统日志的介绍二、向系统日志写入日志信息三、示例代码总结 前言 在Linux系统中&#xff0c;系统日志对于监控和排查系统问题至关重要。它记录了系统的运行状态、各种事件和错误信息&#xff0c;帮助系统管理员和开发人员追踪问题、进行故障排除以及优化系统…

分割链表----一道题目的3种不同的解法

1.题目概述 以这个题目的事例作为例子&#xff0c;我们看一下这个题目到底是什么意思&#xff08;Leedcode好多小伙伴说看不懂题目是什么意思&#xff09;&#xff0c;就是比如一个x3&#xff0c;经过我们的程序执行之后&#xff1b;大于3的在这个链表的后面&#xff0c;小于3的…

Linux使用操作(二)

进程的管理_ps 程序运行在计算机操作系统中&#xff0c;由操作系统进行管理。为了管理正在运行的程序&#xff0c;每个程序在运行时都被注册到操作系统中&#xff0c;形成进程 每个进程都有一个独特的进程ID&#xff08;进程号&#xff09;&#xff0c;用来区别不同的进程。进…

C++初阶-----对运算符重载的进一步理解(2)

目录 1.对于加加&#xff0c;减减运算符的重载理解 2.const修饰的一些事情 3.日期对象之间的减法实现逻辑 1.对于加加&#xff0c;减减运算符的重载理解 &#xff08;1&#xff09;在C语言里面&#xff0c;我们已经知道并且了解加加&#xff0c;减减的一些基本的用法&#…

STM32H7 HSE时钟的使用方法介绍

目录 概述 1 STM32H750 HSE时钟介绍 2 使用STM32Cube创建Project 3 认识HSE时钟 3.1 HSE时钟的特性 3.2 HSE的典型应用电路 4 STM32Cube中配置时钟 4.1 时钟需求 4.2 配置参数 4.2.1 使能外围资源 4.2.2 使用STM32Cube注意项 4.2.3 配置参数 5 总结 概述 本文主要…

超强鉴别 cdn 小工具

最近做一个攻防演习&#xff0c;使用了一些工具收集域名&#xff0c;子域名&#xff0c;但是在将这些域名解析成 IP 这个过程遇到了一些小问题&#xff0c;默认工具给出的 cdn 标志根本不准&#xff0c;所以被迫写了这么一个小工具&#xff1a;get_real_ip.py PS&#xff1a;下…