在Spring Boot项目中通过自定义注解实现多数据源以及主备数据库切换

在现代的企业应用开发中,使用多数据源是一个常见的需求。尤其在关键应用中,设置主备数据库可以提高系统的可靠性和可用性。在这篇博客中,我将展示如何在Spring Boot项目中通过自定义注解实现多数据源以及主备数据库切换。

在此说明

我这里以dm6、dm7来举例多数据源 ,以两个dm6来举例主备数据库,基本大部分数据库都通用,举一反三即可。

对于dm6不熟悉但是又要用的可以看我这篇博客

Spring Boot项目中使用MyBatis连接达梦数据库6

1. 环境依赖

首先,确保你的Spring Boot项目中已经添加了以下依赖:

  <!-- Lombok依赖,用于简化Java代码 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- MyBatis Spring Boot Starter依赖,用于集成MyBatis和Spring Boot -->
        <!-- 注意:这里使用1.3.0版本,因为DM6不支持1.3以上版本 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.0</version>
        </dependency>

        <!-- Spring Boot Starter AOP依赖,用于实现AOP功能 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

        <!-- DM6 JDBC驱动,用于连接DM6数据库 -->
        <dependency>
            <groupId>com.github.tianjing</groupId>
            <artifactId>Dm6JdbcDriver</artifactId>
            <version>1.0.0</version>
        </dependency>

        <!-- DM8 JDBC驱动,用于连接DM8数据库 -->
        <dependency>
            <groupId>com.dameng</groupId>
            <artifactId>DmJdbcDriver18</artifactId>
            <version>8.1.3.62</version>
        </dependency>

        <!-- Hutool工具类库,用于简化Java开发 -->
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.27</version>
        </dependency>

2. 配置文件

spring.datasource:
  dmprimary:
    driver-class-name: dm6.jdbc.driver.DmDriver # 驱动类名称,用于连接 DM6 数据库
    jdbc-url: jdbc:dm6://localhost:12345/xxxx  # JDBC URL,指定 DM6 数据库的地址和端口
    username: xxxx  # 数据库用户名
    password: xxxxxxx  # 数据库密码
    connection-test-query: select 1  # 用于测试数据库连接的查询语句
    type: com.zaxxer.hikari.HikariDataSource  # 使用 HikariCP 作为连接池实现
    maximum-pool-size: 8  # 最大连接池大小
    minimum-idle: 2  # 最小空闲连接数
    idle-timeout: 600000  # 空闲连接的超时时间,单位毫秒
    max-lifetime: 1800000  # 连接的最大生命周期,单位毫秒
    connection-timeout: 3000  # 获取连接的超时时间,单位毫秒
    validation-timeout: 3000  # 验证连接的超时时间,单位毫秒
    initialization-fail-timeout: 1  # 初始化失败时的超时时间,单位毫秒
    leak-detection-threshold: 0  # 连接泄漏检测的阈值,单位毫秒
  dmbackup:
    driver-class-name: dm6.jdbc.driver.DmDriver
    jdbc-url: jdbc:dm6://8.8.8.8:12345/xxxx
    username: xxxxxxx
    password: xxxxx
    connection-test-query: select 1
    type: com.zaxxer.hikari.HikariDataSource
    maximum-pool-size: 8
    minimum-idle: 2
    idle-timeout: 600000
    max-lifetime: 1800000
    connection-timeout: 30000
    validation-timeout: 5000
    initialization-fail-timeout: 1
    leak-detection-threshold: 0

  dm7:
    driver-class-name: dm.jdbc.driver.DmDriver
    jdbc-url: jdbc:dm://localhost:5236/xxxx
    password: xxxxxxxxx
    username: xxxxxx
    connection-test-query: select 1
    type: com.zaxxer.hikari.HikariDataSource
    maximum-pool-size: 10
    minimum-idle: 2
    idle-timeout: 600000
    max-lifetime: 1800000
    connection-timeout: 30000
    validation-timeout: 5000
    initialization-fail-timeout: 1
    leak-detection-threshold: 0

mybatis:
  mapper-locations: classpath:/mappers/*.xml  # 修改为你的 MyBatis XML 映射文件路径
  configuration:
    #    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    map-underscore-to-camel-case: true


3. 定义数据源相关的常量

/**
 * 定义数据源相关的常量
 * @Author: 阿水
 * @Date: 2024-05-24
 */
public interface DataSourceConstant {
    String DB_NAME_DM6 = "dm";
    String DB_NAME_DM6_BACKUP = "dmBackup";
    String DB_NAME_DM7 = "dm7";
}

4. 创建自定义注解

 
import java.lang.annotation.*;
/**
 * 数据源切换注解
 * @Author: 阿水
 * @Date: 2024-05-24
 */
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {

    String value() default DataSourceConstant.DB_NAME_DM6;

}

5. 动态数据源类

/**
 * 动态数据源类
 * @Author: 阿水
 * @Date: 2024-05-24
 */
public class DynamicDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceUtil.getDB();
    }
}

动态数据源切换的核心实现

在多数据源配置中,我们需要一个类来动态决定当前使用的数据源,这就是 DynamicDataSource 类。它继承自 Spring 提供的 AbstractRoutingDataSource,通过覆盖 determineCurrentLookupKey 方法,从 ThreadLocal 中获取当前数据源的标识符,并返回该标识符以决定要使用的数据源。

6. 数据源工具类

/**
 * 数据源工具类
 * @Author: 阿水
 * @Date: 2024-05-24
 */
public class DataSourceUtil {
    /**
     *  数据源属于一个公共的资源
     *  采用ThreadLocal可以保证在多线程情况下线程隔离
     */
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    /**
     * 设置数据源名
     * @param dbType
     */
    public static void setDB(String dbType) {
        contextHolder.set(dbType);
    }

    /**
     * 获取数据源名
     * @return
     */
    public static String getDB() {
        return (contextHolder.get());
    }

    /**
     * 清除数据源名
     */
    public static void clearDB() {
        contextHolder.remove();
    }
}

7. 数据源配置类


/**
 * 数据源配置类,用于配置多个数据源,并设置动态数据源。
 * @Author: 阿水
 * @Date: 2024-05-24
 */
@Configuration
public class DataSourceConfig {

    @Bean(name = "primaryDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.dmprimary")
    public DataSource primaryDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "backupDataSource")
    @ConfigurationProperties(prefix = "spring.datasource.dmbackup")
    public DataSource backupDataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "dm7")
    @ConfigurationProperties(prefix = "spring.datasource.dm7")
    public DataSource dataSourceDm7() {
        return DataSourceBuilder.create().build();
    }
    /**
     * 配置动态数据源,将多个数据源加入到动态数据源中
     * 设置 primaryDataSource 为默认数据源
     */
    @Primary
    @Bean(name = "dynamicDataSource")
    public DataSource dynamicDataSource() {
        DynamicDataSource dynamicDataSource = new DynamicDataSource();
        dynamicDataSource.setDefaultTargetDataSource(primaryDataSource());
        Map<Object, Object> dsMap = new HashMap<>();
        dsMap.put(DataSourceConstant.DB_NAME_DM6, primaryDataSource());
        dsMap.put(DataSourceConstant.DB_NAME_DM6_BACKUP, backupDataSource());
        dsMap.put(DataSourceConstant.DB_NAME_DM7, dataSourceDm7());
        dynamicDataSource.setTargetDataSources(dsMap);
        return dynamicDataSource;
    }
    /**
     * 配置事务管理器,使用动态数据源
     */
    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dynamicDataSource());
    }
}

8. 数据源切换器

 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.annotation.PostConstruct;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
/**
 * 数据源切换器
 * @Author: 阿水
 * @Date: 2024-05-24
 */

@Configuration
public class DataSourceSwitcher extends AbstractRoutingDataSource {

    @Autowired
    private DataSource primaryDataSource;

    @Autowired
    private DataSource backupDataSource;

    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();

    @PostConstruct
    public void init() {
        this.setDefaultTargetDataSource(primaryDataSource);
        Map<Object, Object> dataSourceMap = new HashMap<>();
        dataSourceMap.put("primary", primaryDataSource);
        dataSourceMap.put("backup", backupDataSource);
        this.setTargetDataSources(dataSourceMap);
        this.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return CONTEXT_HOLDER.get();
    }

    public static void setDataSource(String dataSource) {
        CONTEXT_HOLDER.set(dataSource);
    }

    public static void clearDataSource() {
        CONTEXT_HOLDER.remove();
    }

    public boolean isPrimaryDataSourceAvailable() {
        return isDataSourceAvailable(primaryDataSource);
    }

    public boolean isBackupDataSourceAvailable() {
        return isDataSourceAvailable(backupDataSource);
    }

    private boolean isDataSourceAvailable(DataSource dataSource) {
        try (Connection connection = dataSource.getConnection()) {
            return true;
        } catch (RuntimeException | SQLException e) {
            return false;
        }
    }
}

这个类通过继承 AbstractRoutingDataSource 实现了动态数据源切换的功能。它使用 ThreadLocal 变量实现线程隔离的数据源标识存储,并提供了设置和清除当前数据源的方法。在 Bean 初始化时,它将主数据源设为默认数据源,并将主数据源和备用数据源添加到数据源映射中。该类还提供了检查数据源可用性的方法,通过尝试获取连接来判断数据源是否可用。

这个类是实现动态数据源切换的核心部分,配合 Spring AOP 可以实现基于注解的数据源切换逻辑,从而实现多数据源和主备数据库的切换功能。

9. AOP切面类


import cn.hutool.core.util.ObjUtil;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
 * AOP切面
 * @Author: 阿水
 * @Date: 2024-05-24
 */
@Aspect
@Component
@Slf4j
@EnableAspectJAutoProxy
public class DataSourceAspect {

    @Autowired
    private DataSourceSwitcher dataSourceSwitcher;

    @Autowired
    private TimeCacheConfig cacheConfig;

    @Pointcut("@annotation(com.lps.config.DataSource) || @within(com.lps.config.DataSource)")
    public void dataSourcePointCut() {
    }

    /**
     * AOP环绕通知,拦截标注有@DataSource注解的方法或类
     * @param point 连接点信息
     * @return 方法执行结果
     * @throws Throwable 异常信息
     */
    @Around("dataSourcePointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        // 获取需要切换的数据源
        DataSource dataSource = getDataSource(point);
        log.info("初始数据源为{}", dataSource != null ? dataSource.value() : "默认数据源");

        // 设置数据源
        if (dataSource != null) {
            DataSourceUtil.setDB(dataSource.value());
        }

        // 处理主数据源逻辑
        if (DataSourceUtil.getDB().equals(DataSourceConstant.DB_NAME_DM6)) {
            handlePrimaryDataSource();
        }

        // 获取当前数据源
        String currentDataSource = DataSourceUtil.getDB();
        log.info("最终数据源为{}", currentDataSource);

        try {
            // 执行被拦截的方法
            return point.proceed();
        } finally {
            // 清除数据源
            DataSourceUtil.clearDB();
            log.info("清除数据源");
        }
    }

    /**
     * 处理主数据库的数据源切换逻辑
     */
    private void handlePrimaryDataSource() {
        // 检查缓存中是否有主数据库挂掉的标记
        if (ObjUtil.isNotEmpty(cacheConfig.timeCacheHc().get("dataSource", false))) {
            // 切换到备用数据源
            DataSourceUtil.setDB(DataSourceConstant.DB_NAME_DM6_BACKUP);
            log.info("切换到备用数据源");
        } else {
            // 检查主数据库状态并切换数据源
            checkAndSwitchDataSource();
        }
    }

    /**
     * 检查主数据库状态并在必要时切换到备用数据库
     */
    private void checkAndSwitchDataSource() {
        try {
            // 检查主数据库是否可用
            if (dataSourceSwitcher.isPrimaryDataSourceAvailable()) {
                log.info("主数据源没有问题,一切正常");
            } else {
                // 主数据库不可用,更新缓存并切换到备用数据源
                cacheConfig.timeCacheHc().put("dataSource", "主数据库挂了,boom");
                log.info("主数据源存在问题,切换备用数据源");
                DataSourceUtil.setDB(DataSourceConstant.DB_NAME_DM6_BACKUP);
            }
        } catch (Exception e) {
            // 主数据库和备用数据库都不可用,抛出异常
            throw new RuntimeException("两个数据库都有问题 GG", e);
        }
    }

    /**
     * 获取需要切换的数据源
     * @param point 连接点信息
     * @return 数据源注解信息
     */
    private DataSource getDataSource(ProceedingJoinPoint point) {
        MethodSignature signature = (MethodSignature) point.getSignature();
        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
        if (Objects.nonNull(dataSource)) {
            return dataSource;
        }
        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
    }
}

10. 缓存配置类

/**
 * 缓存配置类
 * @Author: 阿水
 * @Date: 2024-05-24
 */
@Configuration
public class TimeCacheConfig {
    @Bean
    public TimedCache timeCacheHc() {
        return CacheUtil.newTimedCache(5 * 60 * 1000);
    }
}

定时缓存,对被缓存的对象定义一个过期时间,当对象超过过期时间会被清理。此缓存没有容量限制,对象只有在过期后才会被移除,详情可以翻阅hutool官方文档

超时-TimedCache

11. 运行结果:

我dmprimary的信息随便写的,可以发现可以自动切换到备用数据库。

12. 结论

通过以上步骤,本次在Spring Boot项目中实现了自定义注解来管理多数据源,并且在主数据库不可用时自动切换到备用数据库。为了提升效率,我们还使用了缓存来记住主数据库的状态,避免频繁的数据库状态检查。这种设计不仅提高了系统的可靠性和可维护性,还能保证在关键时刻系统能够稳定运行。

希望这篇博客能对你有所帮助,如果你有任何问题或建议,欢迎留言讨论。(有问题可以私聊看到就会回)

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

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

相关文章

ICLR 2024现场精彩回顾 机器学习大牛们的“踩高跷秀”嗨翻全场

会议之眼 快讯 2024年5月7-11日&#xff0c;第12届ICLR(International Conference on Learning Representations)即国际学习表征会议已经在奥地利维也纳展览中心圆满结束&#xff01;国际学习表征会议&#xff08;ICLR&#xff09;作为机器学习领域的顶级会议之一&#xff0c;…

开源软件 | 一文彻底搞懂许可证的定义、起源、分类及八大主流许可证,让你选型不再头疼

为什么开源软件会存在许可证&#xff0c;许可证的起源与产生目的是为了解决什么问题&#xff1f;许可证的定义又是怎样的&#xff1f;什么是Copyleft&#xff0c;与Copyright有何区别&#xff1f;开源软件常见的许可证有哪些&#xff1f;这些许可证都有什么特点&#xff1f;接下…

C++中获取int最大与最小值(补)

上文中&#xff0c;我们学习了C中获取int最大与最小值的两种方法&#xff1a;C库和移位运算&#xff0c;这篇文章将解决在移位运算中遇到的各种报错&#xff0c;并提出一种新的生成int最值的方法 上文链接&#xff1a;http://t.csdnimg.cn/cn7Ad 移位运算取最值常见报错 Dev…

【Qt】修改QToolButton图标颜色

1. 目的 修改QToolButton的图标颜色&#xff0c;单一颜色&#xff0c;效果类似于Qt Creator左边选项卡。 2. 代码 QIcon MainWindow::setIconColor(QIcon icon, QColor color) {QPixmap pixmap icon.pixmap(QSize(64,64));QPainter painter(&pixmap);painter.setCompo…

Isaac Sim仿真平台学习(1)认识Isaac Sim

0.前言 上一个教程中我们下载好了Isaac Sim&#xff0c;这一章我们将来简单了解一下Isaac Sim平台。 isaac Sim仿真平台安装-CSDN博客 1.Isaac Sim是啥&#xff1f; What Is Isaac Sim? — Omniverse IsaacSim latest documentation Isaac Sim是NVDIA Omniverse平台的机器…

Window GDI+ API有BUG?GetBounds测不准?

文章目录 GraphicsPath的GetBounds测不准&#xff1f;方法一&#xff1a;GetBounds ()实战 方法二&#xff1a;GetBounds(Matrix)实战 GraphicsPath的GetBounds测不准?实战 .NET 版本的问题&#xff1f;C也一样&#xff0c;不是.NET的问题怀疑人生MiterLimit惹得祸完美结果结束…

深入解析力扣161题:相隔为 1 的编辑距离(逐字符比较与动态规划详解)

❤️❤️❤️ 欢迎来到我的博客。希望您能在这里找到既有价值又有趣的内容&#xff0c;和我一起探索、学习和成长。欢迎评论区畅所欲言、享受知识的乐趣&#xff01; 推荐&#xff1a;数据分析螺丝钉的首页 格物致知 终身学习 期待您的关注 导航&#xff1a; LeetCode解锁100…

Zabbix实现7x24小时架构监控

上篇&#xff1a;https://blog.csdn.net/Lzcsfg/article/details/138774511 文章目录 Zabbix功能介绍Zabbix平台选择安装Zabbix监控端部署MySQL数据库Zabbix参数介绍登录Zabbix WEBWEB界面概览修改WEB界面语言添加被控主机导入监控模板主机绑定模板查看主机状态查看监控数据解…

python实现对应分析的随笔记

文档来源&#xff1a; Correspondence analysis 1 对应分析 参考&#xff1a; SPSS&#xff08;十二&#xff09;SPSS对应分析&#xff08;图文数据集&#xff09;案例6&#xff1a;SPSS–对应分析10 对应分析 对应分析的实质&#xff08;理论很复杂&#xff0c;但是结果很明…

创新工具|AI革新内容营销:策略、工具与实施指南

探索如何利用人工智能&#xff08;AI&#xff09;提升内容营销策略&#xff0c;从SEO优化到个性化推荐。本指南详细介绍了11款顶尖AI工具&#xff0c;旨在帮助中国的中高级职场人士、创业家及创新精英高效地策划和生成引人入胜的内容&#xff0c;同时确保内容的专业性、权威性和…

靶机hackNos Os-Bytesec练习报告

hackNos: Os-Bytesec靶机练习实践报告 下载地址*&#x1f617; https://drive.google.com/open?id1yBuih2CsBx45oTUDpFr4JldrzkaOTTeZ https://download.vulnhub.com/hacknos/Os-ByteSec.ova https://download.vulnhub.com/hacknos/Os-ByteSec.ova.torrent ( Magnet) …

爬虫基础1

一、爬虫的基本概念 1.什么是爬虫&#xff1f; 请求网站并提取数据的自动化程序 2.爬虫的分类 2.1 通用爬虫&#xff08;大而全&#xff09; 功能强大&#xff0c;采集面广&#xff0c;通常用于搜索引擎&#xff1a;百度&#xff0c;360&#xff0c;谷歌 2.2 聚焦爬虫&#x…

集合框框框地架

这一次来介绍一下常用的集合&#xff1a; 首先是两种集合的《家庭系谱图》&#xff1a; 接下来介绍一下集合的种类&#xff1a; Collection Set SetTreeSet&#xff1a;基于红⿊树实现&#xff0c;⽀持有序性操作&#xff0c;例如&#xff1a;根据⼀个范围查找元素的操作。但…

LAMDA面试准备(2024-05-23)

有没有学习过机器学习&#xff0c;提问了 FP-Growth 相比 Apriori 的优点 1. 更高的效率和更少的计算量&#xff08;时间&#xff09; FP-Growth 通过构建和遍历 FP-树 (Frequent Pattern Tree) 来挖掘频繁项集&#xff0c;而不需要像 Apriori 那样生成和测试大量的候选项集。具…

这种电脑原来这么耗电……震惊了粉丝小姐姐

前言 在今年1月份的时候&#xff0c;一位来自重庆的小姐姐加了小白&#xff0c;咨询电脑的问题&#xff1a; 哦豁&#xff0c;这个电脑看着确实闪闪发光&#xff0c;是真的很漂亮&#xff5e;&#xff08;嗯&#xff0c;小姐姐也很漂亮&#xff09; 电脑无法开机&#xff0c;按…

Vue从入门到实战Day12

一、Pinia快速入门 1. 什么是Pinia Pinia是Vue的最新状态管理工具&#xff0c;是Vuex的替代品 1. 提供更加简单的API&#xff08;去掉了mutation&#xff09; 2. 提供符合组合式风格的API&#xff08;和Vue3新语法统一&#xff09; 3. 去掉了modules的概念&#xff0c;每一…

LiveGBS流媒体平台GB/T28181用户手册-用户管理:添加用户、编辑、关联通道、搜索、重置密码

LiveGBS流媒体平台GB/T28181用户手册-用户管理:添加用户、编辑、关联通道、搜索、重置密码 1、用户管理1.1、添加用户1.2、编辑用户1.3、关联通道1.4、重置密码1.5、搜索1.6、删除 2、搭建GB28181视频直播平台 1、用户管理 1.1、添加用户 添加用户&#xff0c;可以配置登陆用户…

自动驾驶---Tesla的自动驾驶技术进化史(PerceptionPlanning)

1 前言 笔者在专栏《自动驾驶Planning模块》中已经详细讲解了传统自动驾驶Planning模块的内容&#xff1a;包括行车的Behavior Planning和Motion Planning&#xff0c;以及低速记忆泊车的Planning&#xff08;最开始有15篇&#xff0c;目前逐渐更新到17篇&#xff09;。读者对整…

linux:信号深入理解

文章目录 1.信号的概念1.1基本概念1.2信号的处理基本概念1.3信号的发送与保存基本概念 2.信号的产生2.1信号产生的五种方式2.2信号遗留问题(core,temp等) 3.信号的保存3.1 信号阻塞3.2 信号特有类型 sigset_t3.3 信号集操作函数3.4 信号集操作函数的使用 4.信号的处理4.1 信号的…

SSRF攻击技术

1、SSRF形成原因 SSRF(Server-Side Request Forgery:服务器端请求伪造) 是一种由攻击者构造形成由服务端发起请求的一个安全漏洞。一般情况下&#xff0c;SSRF是要目标网站的内部系统。&#xff08;因为他是从内部系统访问的&#xff0c;所有可以通过它攻击外网无法访问的内部系…