深入源码:解析SpotBugs (4)如何自定义一个 SpotBugs plugin

news2025/2/24 23:43:43

自定义一个 spotbugs 的插件,官方有比较详细的说明:
https://spotbugs.readthedocs.io/en/stable/implement-plugin.html
本篇是跟随官网demo的足迹,略显无聊,可跳过。

创建工程

执行maven 命令

mvn archetype:generate -DarchetypeArtifactId=spotbugs-archetype -DarchetypeGroupId=com.github.spotbugs -DarchetypeVersion=0.2.3

项目给出了很清晰的代码结构和单元测试用例:
在这里插入图片描述
pom的依赖引用,除了 spotbugs 之外,还有字节码工具 asm 等,也就是说依赖字节码的解析,需要定义插件的童鞋有一定的字节码功底。
在这里插入图片描述
实例
在这里插入图片描述
bug 判断区:
在这里插入图片描述
基本框架就已经搭好,如果有自定义需求,就可以根据demo改写自己的业务逻辑。

下面我们来看看,sawOpcode 是从什么地方调用,需要我们注意什么,能拿到什么信息:
这段代码非常长,看这段代码需要点耐心。
这个方法是CodeVisitor类的一个实现,用于遍历Java字节码并处理各种操作。

  • 首先,代码定义了一些变量,如sizePrevOpcodeBuffer、currentPosInPrevOpcodeBuffer等,用于处理字节码的读取和操作。
  • 然后,代码尝试读取一个字节,并将其存储在变量opcode中。这个字节表示当前操作码,用于后续的处理。
  • 接下来,代码根据opcode的值进行不同的处理。如果opcode表示的是一个wide操作码(例如,Const.WIDE),那么需要读取下一个字节,并将两个字节组合成一个整数。
  • 代码处理各种操作码,如加载(ILOAD)、存储(ISTORE)等。这些操作通常涉及到寄存器的操作,因此需要处理registerOperand变量。
  • 如果opcode表示的是一个跳转操作(如Const.GOTO、Const.IF_A等),那么需要处理跳转目标。
  • 如果opcode表示的是一个表跳转操作(如Const.TABLESWITCH、Const.LOOKUPSWITCH),那么需要处理表跳转的偏移量、标签等。
  • 最后,代码处理完当前操作码后,继续处理下一个操作码。

需要注意的是,这段代码仅处理了Java字节码的读取和简单的操作,而没有处理字节码中的各种变量、方法、类等符号。这些符号需要通过其他方式(如Const.getOpcodeName(opcode))进行处理。

    public void visit(Code obj) {
        //        if (getXMethod().usesInvokeDynamic()) {
        //            AnalysisContext.currentAnalysisContext().analysisSkippedDueToInvokeDynamic(getXMethod());
        //            return;
        //        }
        sizePrevOpcodeBuffer = 0;
        currentPosInPrevOpcodeBuffer = prevOpcode.length - 1;

        int switchLow = 1000000;
        int switchHigh = -1000000;
        codeBytes = obj.getCode();
        DataInputStream byteStream = new DataInputStream(new ByteArrayInputStream(codeBytes));

        lineNumberTable = obj.getLineNumberTable();

        try {
            for (int i = 0; i < codeBytes.length;) {
                resetState();
                PC = i;
                opcodeIsWide = false;
                opcode = byteStream.readUnsignedByte();

                sizePrevOpcodeBuffer++;
                currentPosInPrevOpcodeBuffer++;
                if (currentPosInPrevOpcodeBuffer >= prevOpcode.length) {
                    currentPosInPrevOpcodeBuffer = 0;
                }
                prevOpcode[currentPosInPrevOpcodeBuffer] = opcode;
                i++;
                // System.out.println(Const.getOpcodeName(opCode));
                int byteStreamArgCount = Const.getNoOfOperands(opcode);
                if (byteStreamArgCount == Const.UNPREDICTABLE) {

                    if (opcode == Const.LOOKUPSWITCH) {
                        int pad = 4 - (i & 3);
                        if (pad == 4) {
                            pad = 0;
                        }
                        int count = pad;
                        while (count > 0) {
                            count -= byteStream.skipBytes(count);
                        }
                        i += pad;
                        defaultSwitchOffset = byteStream.readInt();
                        branchOffset = defaultSwitchOffset;
                        branchTarget = branchOffset + PC;
                        i += 4;
                        int npairs = byteStream.readInt();
                        i += 4;
                        switchOffsets = new int[npairs];
                        switchLabels = new int[npairs];
                        for (int o = 0; o < npairs; o++) {
                            switchLabels[o] = byteStream.readInt();
                            switchOffsets[o] = byteStream.readInt();
                            i += 8;
                        }
                        sortByOffset(switchOffsets, switchLabels);
                    } else if (opcode == Const.TABLESWITCH) {
                        int pad = 4 - (i & 3);
                        if (pad == 4) {
                            pad = 0;
                        }
                        int count = pad;
                        while (count > 0) {
                            count -= byteStream.skipBytes(count);
                        }
                        i += pad;
                        defaultSwitchOffset = byteStream.readInt();
                        branchOffset = defaultSwitchOffset;
                        branchTarget = branchOffset + PC;
                        i += 4;
                        switchLow = byteStream.readInt();
                        i += 4;
                        switchHigh = byteStream.readInt();
                        i += 4;
                        int npairs = switchHigh - switchLow + 1;
                        switchOffsets = new int[npairs];
                        switchLabels = new int[npairs];
                        for (int o = 0; o < npairs; o++) {
                            switchLabels[o] = o + switchLow;
                            switchOffsets[o] = byteStream.readInt();
                            i += 4;
                        }
                        sortByOffset(switchOffsets, switchLabels);
                    } else if (opcode == Const.WIDE) {
                        opcodeIsWide = true;
                        opcode = byteStream.readUnsignedByte();
                        i++;
                        switch (opcode) {
                        case Const.ILOAD:
                        case Const.FLOAD:
                        case Const.ALOAD:
                        case Const.LLOAD:
                        case Const.DLOAD:
                        case Const.ISTORE:
                        case Const.FSTORE:
                        case Const.ASTORE:
                        case Const.LSTORE:
                        case Const.DSTORE:
                        case Const.RET:
                            registerOperand = byteStream.readUnsignedShort();
                            i += 2;
                            break;
                        case Const.IINC:
                            registerOperand = byteStream.readUnsignedShort();
                            i += 2;
                            intConstant = byteStream.readShort();
                            i += 2;
                            break;
                        default:
                            throw new IllegalStateException(String.format("bad wide bytecode %d: %s", opcode, Const.getOpcodeName(opcode)));
                        }
                    } else {
                        throw new IllegalStateException(String.format("bad unpredicatable bytecode %d: %s", opcode, Const.getOpcodeName(opcode)));
                    }
                } else {
                    if (byteStreamArgCount < 0) {
                        throw new IllegalStateException(String.format("bad length for bytecode %d: %s", opcode, Const.getOpcodeName(opcode)));
                    }
                    for (int k = 0; k < Const.getOperandTypeCount(opcode); k++) {

                        int v;
                        int t = Const.getOperandType(opcode, k);
                        int m = MEANING_OF_OPERANDS[opcode][k];
                        boolean unsigned = (m == M_CP || m == M_R || m == M_UINT);
                        switch (t) {
                        case Const.T_BYTE:
                            if (unsigned) {
                                v = byteStream.readUnsignedByte();
                            } else {
                                v = byteStream.readByte();
                            }
                            /*
                             * System.out.print("Read byte " + v);
                             * System.out.println(" with meaning" + m);
                             */
                            i++;
                            break;
                        case Const.T_SHORT:
                            if (unsigned) {
                                v = byteStream.readUnsignedShort();
                            } else {
                                v = byteStream.readShort();
                            }
                            i += 2;
                            break;
                        case Const.T_INT:
                            v = byteStream.readInt();
                            i += 4;
                            break;
                        default:
                            throw new IllegalStateException();
                        }
                        switch (m) {
                        case M_BR:
                            branchOffset = v;
                            branchTarget = v + PC;
                            branchFallThrough = i;
                            break;
                        case M_CP:
                            constantRefOperand = getConstantPool().getConstant(v);
                            if (constantRefOperand instanceof ConstantClass) {
                                ConstantClass clazz = (ConstantClass) constantRefOperand;
                                classConstantOperand = getStringFromIndex(clazz.getNameIndex());
                                referencedClass = DescriptorFactory.createClassDescriptor(classConstantOperand);

                            } else if (constantRefOperand instanceof ConstantInteger) {
                                intConstant = ((ConstantInteger) constantRefOperand).getBytes();
                            } else if (constantRefOperand instanceof ConstantLong) {
                                longConstant = ((ConstantLong) constantRefOperand).getBytes();
                            } else if (constantRefOperand instanceof ConstantFloat) {
                                floatConstant = ((ConstantFloat) constantRefOperand).getBytes();
                            } else if (constantRefOperand instanceof ConstantDouble) {
                                doubleConstant = ((ConstantDouble) constantRefOperand).getBytes();
                            } else if (constantRefOperand instanceof ConstantString) {
                                int s = ((ConstantString) constantRefOperand).getStringIndex();

                                stringConstantOperand = getStringFromIndex(s);
                            } else if (constantRefOperand instanceof ConstantInvokeDynamic) {
                                ConstantInvokeDynamic id = (ConstantInvokeDynamic) constantRefOperand;
                                ConstantNameAndType sig = (ConstantNameAndType) getConstantPool().getConstant(
                                        id.getNameAndTypeIndex());
                                nameConstantOperand = getStringFromIndex(sig.getNameIndex());
                                sigConstantOperand = getStringFromIndex(sig.getSignatureIndex());
                            } else if (constantRefOperand instanceof ConstantCP) {
                                ConstantCP cp = (ConstantCP) constantRefOperand;
                                ConstantClass clazz = (ConstantClass) getConstantPool().getConstant(cp.getClassIndex());
                                classConstantOperand = getStringFromIndex(clazz.getNameIndex());
                                referencedClass = DescriptorFactory.createClassDescriptor(classConstantOperand);
                                referencedXClass = null;
                                ConstantNameAndType sig = (ConstantNameAndType) getConstantPool().getConstant(
                                        cp.getNameAndTypeIndex());
                                nameConstantOperand = getStringFromIndex(sig.getNameIndex());
                                sigConstantOperand = getStringFromIndex(sig.getSignatureIndex());
                                refConstantOperand = null;
                            }
                            break;
                        case M_R:
                            registerOperand = v;
                            break;
                        case M_UINT:
                        case M_INT:
                            intConstant = v;
                            break;
                        case M_PAD:
                            break;
                        default:
                            throw new IllegalStateException("Unexpecting meaning " + m);
                        }
                    }

                }
                switch (opcode) {
                case Const.IINC:
                    isRegisterLoad = true;
                    isRegisterStore = true;
                    break;
                case Const.ILOAD_0:
                case Const.ILOAD_1:
                case Const.ILOAD_2:
                case Const.ILOAD_3:
                    registerOperand = opcode - Const.ILOAD_0;
                    isRegisterLoad = true;
                    break;

                case Const.ALOAD_0:
                case Const.ALOAD_1:
                case Const.ALOAD_2:
                case Const.ALOAD_3:
                    registerOperand = opcode - Const.ALOAD_0;
                    isRegisterLoad = true;
                    break;

                case Const.FLOAD_0:
                case Const.FLOAD_1:
                case Const.FLOAD_2:
                case Const.FLOAD_3:
                    registerOperand = opcode - Const.FLOAD_0;
                    isRegisterLoad = true;
                    break;

                case Const.DLOAD_0:
                case Const.DLOAD_1:
                case Const.DLOAD_2:
                case Const.DLOAD_3:
                    registerOperand = opcode - Const.DLOAD_0;
                    isRegisterLoad = true;
                    break;

                case Const.LLOAD_0:
                case Const.LLOAD_1:
                case Const.LLOAD_2:
                case Const.LLOAD_3:
                    registerOperand = opcode - Const.LLOAD_0;
                    isRegisterLoad = true;
                    break;
                case Const.ILOAD:
                case Const.FLOAD:
                case Const.ALOAD:
                case Const.LLOAD:
                case Const.DLOAD:
                    isRegisterLoad = true;
                    break;

                case Const.ISTORE_0:
                case Const.ISTORE_1:
                case Const.ISTORE_2:
                case Const.ISTORE_3:
                    registerOperand = opcode - Const.ISTORE_0;
                    isRegisterStore = true;
                    break;

                case Const.ASTORE_0:
                case Const.ASTORE_1:
                case Const.ASTORE_2:
                case Const.ASTORE_3:
                    registerOperand = opcode - Const.ASTORE_0;
                    isRegisterStore = true;
                    break;

                case Const.FSTORE_0:
                case Const.FSTORE_1:
                case Const.FSTORE_2:
                case Const.FSTORE_3:
                    registerOperand = opcode - Const.FSTORE_0;
                    isRegisterStore = true;
                    break;

                case Const.DSTORE_0:
                case Const.DSTORE_1:
                case Const.DSTORE_2:
                case Const.DSTORE_3:
                    registerOperand = opcode - Const.DSTORE_0;
                    isRegisterStore = true;
                    break;

                case Const.LSTORE_0:
                case Const.LSTORE_1:
                case Const.LSTORE_2:
                case Const.LSTORE_3:
                    registerOperand = opcode - Const.LSTORE_0;
                    isRegisterStore = true;
                    break;
                case Const.ISTORE:
                case Const.FSTORE:
                case Const.ASTORE:
                case Const.LSTORE:
                case Const.DSTORE:
                    isRegisterStore = true;
                    break;
                default:
                    break;
                }

                switch (opcode) {
                case Const.ILOAD:
                case Const.FLOAD:
                case Const.ALOAD:
                case Const.LLOAD:
                case Const.DLOAD:
                    // registerKind = opcode - ILOAD;
                    break;
                case Const.ISTORE:
                case Const.FSTORE:
                case Const.ASTORE:
                case Const.LSTORE:
                case Const.DSTORE:
                    // registerKind = opcode - ISTORE;
                    break;
                case Const.RET:
                    // registerKind = R_REF;
                    break;
                case Const.GETSTATIC:
                case Const.PUTSTATIC:
                    refFieldIsStatic = true;
                    break;
                case Const.GETFIELD:
                case Const.PUTFIELD:
                    refFieldIsStatic = false;
                    break;
                default:
                    break;
                }

                nextPC = i;
                if (beforeOpcode(opcode)) {
                    sawOpcode(opcode);
                }
                afterOpcode(opcode);

                if (opcode == Const.TABLESWITCH) {
                    sawInt(switchLow);
                    sawInt(switchHigh);
                    //                    int prevOffset = i - PC;
                    for (int o = 0; o <= switchHigh - switchLow; o++) {
                        sawBranchTo(switchOffsets[o] + PC);
                        //                        prevOffset = switchOffsets[o];
                    }
                    sawBranchTo(defaultSwitchOffset + PC);
                } else if (opcode == Const.LOOKUPSWITCH) {
                    sawInt(switchOffsets.length);
                    //                    int prevOffset = i - PC;
                    for (int o = 0; o < switchOffsets.length; o++) {
                        sawBranchTo(switchOffsets[o] + PC);
                        //                        prevOffset = switchOffsets[o];
                        sawInt(switchLabels[o]);
                    }
                    sawBranchTo(defaultSwitchOffset + PC);
                } else {
                    for (int k = 0; k < Const.getOperandTypeCount(opcode); k++) {
                        int m = MEANING_OF_OPERANDS[opcode][k];
                        switch (m) {
                        case M_BR:
                            sawBranchTo(branchOffset + PC);
                            break;
                        case M_CP:
                            if (constantRefOperand instanceof ConstantInteger) {
                                sawInt(intConstant);
                            } else if (constantRefOperand instanceof ConstantLong) {
                                sawLong(longConstant);
                            } else if (constantRefOperand instanceof ConstantFloat) {
                                sawFloat(floatConstant);
                            } else if (constantRefOperand instanceof ConstantDouble) {
                                sawDouble(doubleConstant);
                            } else if (constantRefOperand instanceof ConstantString) {
                                sawString(stringConstantOperand);
                            } else if (constantRefOperand instanceof ConstantFieldref) {
                                sawField();
                            } else if (constantRefOperand instanceof ConstantMethodref) {
                                sawMethod();
                            } else if (constantRefOperand instanceof ConstantInterfaceMethodref) {
                                sawIMethod();
                            } else if (constantRefOperand instanceof ConstantClass) {
                                sawClass();
                            }
                            break;
                        case M_R:
                            sawRegister(registerOperand);
                            break;
                        case M_INT:
                            sawInt(intConstant);
                            break;
                        default:
                            break;
                        }
                    }
                }
            }
        } catch (IOException e) {
            AnalysisContext.logError("Error while dismantling bytecode", e);
            assert false;
        }

        try {
            byteStream.close();
        } catch (IOException e) {
            assert false;
        }
    }

调用点就在这里,visit 方法是遍历codeBytes.length
在这里插入图片描述

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

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

相关文章

关于Jenkins自动化部署Maven项目:

jenkins介绍: Jenkins是一个开源软件项目&#xff0c;是基于Java开发的一种持续集成工具&#xff0c;用于监控持续重复的工作&#xff0c;旨在提供一个开放易用的软件平台&#xff0c;使软件项目可以进行持续集成。 jenkins作用:更好的利于测试工程师测试项目(自动部署) 安装je…

游戏UI设计大师课:3款游戏 UI 设计模板

很多时候&#xff0c;做设计需要找素材。假如是普通的 UI 界面或者 Banner 等等&#xff0c;在Dribbble、Pinterest、即时设计、Behance 翻看这样的网站&#xff0c;至少可以梳理出一些想法和思路。如果你需要一个更规范的指南&#xff0c;此时&#xff0c;在各种设计规范、官方…

【网络安全】AWS S3 Bucket配置错误导致敏感信息泄露

未经许可&#xff0c;不得转载。 文章目录 前言技术分析正文 前言 AWS&#xff08;Amazon Web Services&#xff09;是亚马逊公司提供的一个安全的云服务平台&#xff0c;旨在为个人、公司和政府机构提供计算能力、存储解决方案、内容交付和其他功能。作为全球领先的云服务提供…

electron调试

electron 调试 electron 的调试分两步&#xff0c;界面的调试&#xff0c;和主进程的调试。 界面调试类似浏览器F12&#xff0c;可是调试不到主进程。 主进程调试有vscode、命令行提示和外部调试器调试。 本篇记录的练习是vscode调试。命令行和外部调试器的方式可以参考官网&a…

模拟实现c++中的vector模版

目录 一vector简述&#xff1a; 二vector的一些接口函数&#xff1a; 1初始化&#xff1a; 2.vector增长&#xff1a; 3vector增删查改&#xff1a; 三vector模拟实现部分主要函数&#xff1a; 1.size,capacity,empty,clear接口&#xff1a; 2.reverse的实现&#xff1…

青少年科普平台-计算机毕业设计源码76194

摘 要 对于搭建一个青少年科普平台&#xff0c;您可以考虑使用Spring Boot作为后端框架。Spring Boot是一个能够简化Spring应用开发的框架&#xff0c;能够帮助您快速搭建稳定、高效的后端服务。您可以利用Spring Boot的特性来构建一个可靠的数据服务&#xff0c;用于展示和传播…

docker镜像下载

1、搜索镜像 [rootlocalhost ~]# docker search ubuntu 2、下载ubuntu系统镜像(容器架构与宿主机相同) [rootlocalhost ~]# docker pull ubuntu #选择stars值最高镜像下载&#xff0c;默认为latest版 [rootlocalhost ~]# docker images 3、查看镜像支持的架构 [rootlocalh…

1143. 最长公共子序列(详细版)

目录 dp解法&#xff1a; 1.状态代表什么&#xff1a; 2. 状态转移方程 3.初始化 3. so为什么要这样&#xff1f; 代码实现&#xff1a; 给定两个字符串 text1 和 text2&#xff0c;返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 &#xff0c;返回 0…

protobuf2.5升级protobuf3.14.0

这个升级搞得心力憔悴&#xff0c;我VS2010升级到了VS2017&#xff0c;所有的库都要编译一下&#xff0c;想着顺便把其他的三方库也升级了。搞了好几天&#xff0c;才升级完&#xff0c;因为不仅要搞windows还要搞linux版本各种库的升级。hpsocket的升级&#xff0c;jsoncpp的升…

stm32h7串口发送寄存器空中断

关于stm32串口的发送完成中断UART_IT_TC网上资料挺多的&#xff0c;但是使用发送寄存器空中断UART_IT_TXE的不太多 UART_IT_TC 和 UART_IT_TXE区别 UART_IT_TC 和 UART_IT_TXE 是两种不同的 UART 中断源&#xff0c;用于表示不同的发送状态。它们的主要区别如下&#xff1a; …

安卓单机游戏:龙之矛内置菜单,【免费分享】白嫖!

龙之矛是一款传统的横版卷轴RPG游戏。在游戏中玩家将探索被梦魇蹂躏的世界&#xff0c;和朋友一起猎杀强大的BOSS&#xff0c;如果你杀死了boss&#xff0c;那么你可以制作具有boss力量的装备。游戏内所有装备都有独特的外观&#xff0c;你可以用各种华丽的装备打扮你的角色。英…

并发基础——Java全栈知识(37)

1、并发基础 1、进程和线程的区别 程序由指令和数据组成&#xff0c;但这些指令要运行&#xff0c;数据要读写&#xff0c;就必须将指令加载至 CPU&#xff0c;数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备。进程就是用来加载指令、管理内存、管理 IO 的。当…

【LLM】-10-部署llama-3-chinese-8b-instruct-v3 大模型

目录 1、模型下载 2、下载项目代码 3、启动模型 4、模型调用 4.1、completion接口 4.2、聊天&#xff08;chat completion&#xff09; 4.3、多轮对话 4.4、文本嵌入向量 5、Java代码实现调用 由于在【LLM】-09-搭建问答系统-对输入Prompt检查-CSDN博客 关于提示词注入…

GPT-4o Mini 模型的性能与成本优势全解析

GPT-4o Mini 模型的性能与成本优势全解析 &#x1f4c8; &#x1f31f; GPT-4o Mini 模型的性能与成本优势全解析 &#x1f4c8;摘要引言正文内容GPT-4o Mini 模型简介 &#x1f680;性能测试与对比 &#x1f4ca;应用场景 &#x1f310;自然语言处理对话系统内容生成 ✍️ &am…

Lesson 51 A pleasant climate

Lesson 51 A pleasant climate 词汇 Greece n. 希腊 Greek a. 希腊的&#xff0c;希腊语 搭配&#xff1a;Greek gift 不怀好意的礼物 例句&#xff1a;他的电脑是不怀好意的礼物。    His computer is a Greek gift. climate n. 气候 长时间&#xff0c;不容易更改的 we…

生成式AI和LLM的革命:Transformer架构

近年来&#xff0c;随着一篇名为“Attention is All You Need”论文的出现&#xff0c;自然语言处理&#xff08;NLP&#xff09;领域经历了一场巨大的变革。2017年&#xff0c;在谷歌和多伦多大学发表了这篇论文后&#xff0c;Transformer架构出现了。这一架构不仅显著提升了N…

.net 连接达梦数据库开发环境部署

.net 开发环境部署 1. 环境准备 测试工具 Visual Studio2022 数据库版本 dm8 2. 搭建过程 1 &#xff09;创建新项目 2 &#xff09;选择创建空项目 3 &#xff09;配置新项目 4 &#xff09;右键 DM1 新建一个项 5 &#xff09;加 载 驱 动 &#xff0c; 新 建 …

移动恶意软件的崛起

一.介绍 随着手机的出现&#xff0c;我们的日常生活发生了变化&#xff0c;无论是我们的工作方式还是我们过去相互交流的方式&#xff0c;一切都随着移动技术的进步而改变。但是&#xff0c;随着技术的进步&#xff0c;恶意软件也被引入&#xff0c;随着时间的推移它也变得更加…

用excel能做出这些报表吗?

用excel能做出这些报表吗&#xff1f; 有什么办法不安装OFFICE也能显示出来&#xff1f;

免费【2024】springboot 城市交通管理系统的设计与实现

博主介绍&#xff1a;✌CSDN新星计划导师、Java领域优质创作者、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和学生毕业项目实战,高校老师/讲师/同行前辈交流✌ 技术范围&#xff1a;SpringBoot、Vue、SSM、HTML、Jsp、PHP、Nodejs、Python、爬虫、数据可视化…