深入解析:Java WAR 包反解析与其实现详解

举报
bug菌 发表于 2024/09/14 17:57:14 2024/09/14
【摘要】 咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~🏆本文收录于「滚雪球学Java」专栏中,这个专栏专为有志于提升Java技能的你打造,覆盖Java编程的方方面面,助你从零基础到掌握Java开发的精髓。赶紧关注,收藏,学习吧!环境说明...

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE相关知识点了,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~


🏆本文收录于「滚雪球学Java」专栏中,这个专栏专为有志于提升Java技能的你打造,覆盖Java编程的方方面面,助你从零基础到掌握Java开发的精髓。赶紧关注,收藏,学习吧!

环境说明:Windows 10 + IntelliJ IDEA 2021.3.2 + Jdk 1.8

前言

在 Java Web 开发中,WAR (Web Application Archive) 包是我们常见的部署格式之一。它将所有 Web 应用程序的文件(如 JSP 页面、Servlet 类、HTML、CSS、JS 文件等)打包成一个压缩文件,以便在 Web 容器(如 Tomcat、Jetty 等)中运行。开发人员通常通过构建工具生成 WAR 包,但有时,我们需要反向解析 WAR 包,即从一个现成的 WAR 包中提取和分析其内容,以了解应用的结构、配置或代码实现。

本文将深入探讨 Java WAR 包的反解析过程,讲解其技术细节、应用场景,并提供具体的代码示例,帮助开发者更好地掌握该技术。在文章的后续部分,我们还将从技术深度和广度两个角度进行扩展,分析相关工具的使用、常见问题及优化策略。

什么是 WAR 包?

WAR 包 (Web Application Archive) 是 Java Web 应用的部署单元,其实质上是一个符合特定结构的 ZIP 压缩文件,包含了 Web 应用程序的所有资源、配置文件和代码。它的典型结构如下:

myapp.war
│
├── META-INF/
│   └── MANIFEST.MF  # 描述 WAR 包的元数据
│
├── WEB-INF/
│   ├── web.xml      # Web 应用的核心配置文件
│   ├── classes/     # 编译后的 Java 类文件
│   ├── lib/         # 所依赖的 JAR 包
│   └── ...
│
├── static resources # Web 项目中的静态资源 (HTML, CSS, JS, images等)
└── ...

WAR 包将项目文件和依赖打包在一起,确保在服务器环境中可以无缝部署和运行。

为什么要进行 WAR 包反解析?

WAR 包反解析的需求通常出现在以下场景中:

  1. 问题诊断:当系统出问题时,通过反解析 WAR 包可以快速定位问题源头,检查依赖文件、配置或代码的正确性。
  2. 代码审查:在无法直接获取源代码的情况下,通过解压和反编译 WAR 包,可以分析某个应用的业务逻辑、使用的技术和开发模式。
  3. 安全审计:通过分析 WAR 包中的依赖库和配置,检查是否有潜在的安全漏洞或配置错误。

WAR 包反解析过程概述

WAR 包反解析的核心在于解压和分析其内容。步骤可以总结如下:

  1. 解压 WAR 包:WAR 包本质是 ZIP 文件,因此可以用 ZIP 解压工具打开。
  2. 读取和分析:从解压的目录中读取关键文件(如 web.xmlJSP 文件,Servlet 类等),分析应用程序的配置与代码逻辑。
  3. 反编译 Java 类:如果需要查看 WAR 包中的 Java 类,可以通过反编译工具将 .class 文件转为 .java 文件,帮助我们理解代码逻辑。

代码示例:如何反解析 WAR 包

1. 解压 WAR 包

首先,我们需要使用 Java 提供的 java.util.zip 包来解压 WAR 文件,逐步提取其中的文件和目录。

代码示例

import java.io.*;
import java.util.zip.*;

public class WarExtractor {
    
    public static void main(String[] args) {
        String warFilePath = "path/to/your/warfile.war";  // WAR 文件路径
        String outputDir = "path/to/output/directory";    // 解压后的输出目录
        
        try (ZipInputStream zis = new ZipInputStream(new FileInputStream(warFilePath))) {
            ZipEntry entry;
            while ((entry = zis.getNextEntry()) != null) {
                String filePath = outputDir + File.separator + entry.getName();
                
                if (!entry.isDirectory()) {
                    extractFile(zis, filePath);
                } else {
                    File dir = new File(filePath);
                    dir.mkdirs();
                }
                zis.closeEntry();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private static void extractFile(ZipInputStream zis, String filePath) throws IOException {
        BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
        byte[] bytesIn = new byte[4096];
        int read;
        while ((read = zis.read(bytesIn)) != -1) {
            bos.write(bytesIn, 0, read);
        }
        bos.close();
    }
}

代码解析

在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。

这段 Java 代码的主要功能是解压一个 WAR 文件到指定的输出目录。WAR 文件 (Web Application Archive) 是用于 Java Web 应用程序的打包格式,它本质上是一个 ZIP 文件。以下是代码的详细解析:

1. 代码功能概述

目标:将一个 WAR 文件解压到指定的目录。

主要步骤:

  1. 读取 WAR 文件:通过 ZipInputStream 读取压缩文件的内容。
  2. 遍历条目:遍历 WAR 文件中的每个条目(文件或文件夹)。
  3. 解压文件:对于每个文件条目,将其内容解压到指定的输出目录。
  4. 创建文件夹:如果条目是文件夹,则在输出目录中创建相应的文件夹。
2. 代码结构解析
导入必要的包
import java.io.*;
import java.util.zip.*;
  • java.io.*:提供文件输入/输出操作所需的类,如 FileInputStreamFileOutputStreamBufferedOutputStream 等。
  • java.util.zip.*:提供处理压缩文件的类,如 ZipInputStreamZipEntry
main 方法解析
public static void main(String[] args) {
    String warFilePath = "path/to/your/warfile.war";  // WAR 文件路径
    String outputDir = "path/to/output/directory";    // 解压后的输出目录
  • warFilePath:指定 WAR 文件的路径。这个路径可以是本地文件路径,也可以是远程路径。
  • outputDir:指定解压后文件存放的目标目录。
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(warFilePath))) {
    ZipEntry entry;
    while ((entry = zis.getNextEntry()) != null) {
        String filePath = outputDir + File.separator + entry.getName();
  • ZipInputStream:用来读取压缩文件内容,构造函数接受一个 FileInputStream,以读取指定路径下的 WAR 文件。
  • zis.getNextEntry():获取下一个条目(ZipEntry)——该条目可以是文件或文件夹。
if (!entry.isDirectory()) {
    extractFile(zis, filePath);
} else {
    File dir = new File(filePath);
    dir.mkdirs();
}
zis.closeEntry();
  • entry.isDirectory():判断当前条目是否为文件夹。如果是文件夹,调用 mkdirs() 方法创建该文件夹;否则,调用 extractFile() 方法解压该文件。
  • zis.closeEntry():关闭当前条目的输入流。
extractFile 方法解析
private static void extractFile(ZipInputStream zis, String filePath) throws IOException {
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
    byte[] bytesIn = new byte[4096];
    int read;
    while ((read = zis.read(bytesIn)) != -1) {
        bos.write(bytesIn, 0, read);
    }
    bos.close();
}
  • BufferedOutputStream:用于将解压后的数据写入磁盘,使用缓冲区提高性能。
  • byte[] bytesIn = new byte[4096]:定义了一个 4096 字节大小的缓冲区,以流的形式读取压缩文件内容并写入目标文件。
  • while ((read = zis.read(bytesIn)) != -1):循环读取当前条目的内容,直到读取完毕。
  • bos.write(bytesIn, 0, read):将读取到的字节写入目标文件中。
3. 代码运行流程
  1. 读取 WAR 文件:首先,通过 ZipInputStream 打开指定的 WAR 文件,并逐个读取其中的条目(包括文件和文件夹)。
  2. 解压文件或创建文件夹
    • 如果条目是文件,则调用 extractFile() 方法,将文件内容写入磁盘。
    • 如果条目是文件夹,则在输出目录中创建对应的文件夹。
  3. 逐条处理:每次处理完一个条目,关闭当前条目,继续处理下一个条目,直到所有条目都处理完毕。
  4. 关闭流:完成所有操作后,ZipInputStream 会在 try-with-resources 块中自动关闭,确保资源得到正确释放。
4. 扩展与优化
1. 处理大文件的性能优化

解压大文件时,可以通过增加缓冲区的大小或使用多线程并发解压以提高性能。此外,可以使用压缩库的高级功能,如 java.nio.file 提供的更高效的 I/O 操作。

2. 异常处理

当前代码中的异常处理较为基础。可以改进为自定义异常或日志记录,使得当解压失败时能够准确定位问题。

3. WAR 文件结构检查

在处理 WAR 文件时,可以先读取 WEB-INF/web.xml 文件,确保这是一个有效的 Web 应用程序包,并根据应用程序的需求进行处理。

5. 使用场景
  • Web 应用部署:在服务器上解压 WAR 包是常见的 Web 应用部署方式之一,通过这种方式,可以将应用的所有资源放置到指定的服务器目录中。
  • 自动化脚本:在 DevOps 环境中,这段代码可以用于自动化解压 WAR 包,并将其部署到应用服务器(如 Tomcat)的 Web 应用目录中。
  • 分析 WAR 包内容:通过这段代码可以方便地提取 WAR 包中的文件,便于进行文件分析、安全检查或应用调试。

2. 分析关键文件

解压后,我们可以检查 WAR 包中的 WEB-INF/web.xml 文件。该文件定义了 Web 应用程序的核心配置,如 Servlet 映射、过滤器等。通过读取该文件,我们可以了解应用的结构和服务端的逻辑。

代码示例

import java.io.*;

public class WebXmlReader {

    public static void main(String[] args) {
        String webXmlPath = "path/to/output/directory/WEB-INF/web.xml";

        try (BufferedReader br = new BufferedReader(new FileReader(webXmlPath))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);  // 输出 web.xml 内容
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

代码解析

接着我将对上述代码逐句进行一个详细解读,希望能够帮助到同学们,能以最快的速度对其知识点掌握于心,这也是我写此文的初衷,授人以鱼不如授人以渔,只有将其原理摸透,日后应对场景使用,才能得心应手,如鱼得水。所以如果有基础的同学,可以略过如下代码解析,针对没基础的同学,还是需要加强对代码的逻辑与实现,方便日后的你能更深入理解它并常规使用不受限制。

这段代码的主要功能是读取并输出 Java Web 应用中的 web.xml 文件。web.xml 文件位于 Java Web 应用程序的 WEB-INF 目录中,用于定义应用的配置,如 Servlet、过滤器、监听器等。代码通过使用 BufferedReader 按行读取 web.xml 文件的内容,并将其打印到控制台。

1. 代码功能概述

目标:读取 web.xml 文件并输出其内容。

主要步骤:

  1. 指定 web.xml 文件路径:定义要读取的 web.xml 文件的路径。
  2. 读取文件内容:使用 BufferedReader 按行读取文件内容。
  3. 输出文件内容:逐行输出读取到的内容到控制台。
2. 代码结构解析
导入必要的包
import java.io.*;
  • java.io.*:导入 Java IO 包,提供读取文件内容所需的类,例如 BufferedReaderFileReaderIOException
主函数解析
public class WebXmlReader {
    
    public static void main(String[] args) {
        String webXmlPath = "path/to/output/directory/WEB-INF/web.xml";
  • webXmlPath:字符串变量,用于存储 web.xml 文件的路径。在 Web 应用程序中,web.xml 文件通常位于 WEB-INF 目录下。
try (BufferedReader br = new BufferedReader(new FileReader(webXmlPath))) {
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);  // 输出 web.xml 内容
    }
} catch (IOException e) {
    e.printStackTrace();
}
  • BufferedReaderFileReaderBufferedReader 包装了 FileReader,用于以较高效的方式读取文件。FileReader 负责打开并读取文件的内容,而 BufferedReader 提供了 readLine() 方法,可以逐行读取文件内容。

    • readLine():每次读取一行,直到文件末尾返回 null
  • try-with-resources:通过 try-with-resources 语法,确保 BufferedReader 在读取完文件后自动关闭,以防止文件泄漏或占用系统资源。

  • 异常处理

    • IOException:当文件路径不存在或者无法读取时,会抛出 IOException,并通过 e.printStackTrace() 输出详细的异常信息。
3. 代码工作流程
  1. 指定文件路径:首先,通过字符串 webXmlPath 定义 web.xml 文件的路径。
  2. 打开文件并读取内容:使用 BufferedReaderFileReader 读取文件。通过 readLine() 循环读取文件的每一行,直到文件结束。
  3. 输出内容:每读取一行内容,就输出该行到控制台。
  4. 处理异常:如果文件不存在或读取失败,捕获并输出异常堆栈信息。
4. 示例输出

假设 web.xml 文件包含以下内容:

<web-app>
    <servlet>
        <servlet-name>MyServlet</servlet-name>
        <servlet-class>com.example.MyServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>MyServlet</servlet-name>
        <url-pattern>/myServlet</url-pattern>
    </servlet-mapping>
</web-app>

运行程序后,控制台将输出:

<web-app>
    <servlet>
        <servlet-name>MyServlet</servlet-name>
        <servlet-class>com.example.MyServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>MyServlet</servlet-name>
        <url-pattern>/myServlet</url-pattern>
    </servlet-mapping>
</web-app>
5. 扩展与优化
1. 文件不存在处理

在当前代码中,如果 web.xml 文件不存在,将抛出 IOException 并输出错误堆栈信息。为了改进用户体验,可以在捕获异常后输出更友好的提示:

catch (IOException e) {
    System.out.println("Error: Unable to read the web.xml file. Please check the file path.");
    e.printStackTrace();
}
2. 分析 web.xml 文件内容

除了简单输出文件内容外,还可以对文件内容进行分析。例如,使用 XML 解析器(如 javax.xml.parsers.DocumentBuilderFactory)来解析 web.xml 文件,提取其中的关键信息,如 Servlet 映射、过滤器、监听器等。

3. 大文件处理

如果 web.xml 文件较大,可以考虑使用更高效的文件处理方法,例如 NIO(非阻塞 IO)库中的 Files.newBufferedReader(),提高读取效率。

6. 使用场景
1. Web 应用调试

在调试 Web 应用时,开发者可能需要检查 web.xml 中的配置,了解具体的 Servlet、过滤器和映射。通过该代码,开发者可以方便地读取并查看配置。

2. 自动化工具

可以将这段代码集成到自动化工具中,用于读取并分析 WAR 包中的 web.xml 文件,帮助自动化运维或 Web 应用部署时检查配置的正确性。

3. 配置检查

开发者可以使用此程序读取并输出 web.xml 文件,确保配置项的正确性,特别是在手动修改该文件后,检查是否存在拼写错误或语法错误。

7. 总结

这段代码展示了如何通过 BufferedReader 读取 Java Web 应用中的 web.xml 文件,并将其内容逐行输出到控制台。通过合理的异常处理,确保程序在文件不存在或读取失败时能够提供错误提示。在实际应用中,开发者可以根据需要扩展该程序,对 web.xml 内容进行进一步的分析与处理。

3. 反编译 Java 类

对于 WAR 包中的 .class 文件,我们可以使用 Java 反编译工具 (如 JD-GUI、CFR、Procyon 等)将其反编译为 Java 源代码。以下是使用 Procyon 反编译的示例代码:

代码示例

import com.strobel.decompiler.Decompiler;

public class ClassDecompiler {

    public static void main(String[] args) {
        String classFilePath = "path/to/output/directory/WEB-INF/classes/com/example/MyServlet.class";
        String outputFilePath = "path/to/output/directory/com/example/MyServlet.java";
        
        try {
            Decompiler.decompile(classFilePath, new PrintWriter(new FileWriter(outputFilePath)));
            System.out.println("Class file decompiled successfully!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在这个过程中,我们通过 ProcyonDecompiler.decompile() 方法将 .class 文件反编译为 .java 文件,输出到指定目录中。

代码解析

在本次的代码演示中,我将会深入剖析每句代码,详细阐述其背后的设计思想和实现逻辑。通过这样的讲解方式,我希望能够引导同学们逐步构建起对代码的深刻理解。我会先从代码的结构开始,逐步拆解每个模块的功能和作用,并指出关键的代码段,并解释它们是如何协同运行的。通过这样的讲解和实践相结合的方式,我相信每位同学都能够对代码有更深入的理解,并能够早日将其掌握,应用到自己的学习和工作中。

这段代码展示了如何使用 Procyon 反编译工具对 .class 文件进行反编译,并将其转换为 .java 源文件。代码的目标是读取 Java 字节码文件(.class),并生成对应的 Java 源代码文件(.java)。接下来我们进行详细解析。

1. 导入必要的包
import com.strobel.decompiler.Decompiler;
  • com.strobel.decompiler.Decompiler:这个类来自 Procyon 反编译器库,专门用于将 Java 字节码文件反编译为 Java 源代码。Procyon 是一种高效的 Java 字节码反编译工具,支持较新的 Java 特性,例如 Lambda 表达式。
2. 主函数解析
public class ClassDecompiler {
    
    public static void main(String[] args) {
        String classFilePath = "path/to/output/directory/WEB-INF/classes/com/example/MyServlet.class";
        String outputFilePath = "path/to/output/directory/com/example/MyServlet.java";
  • classFilePath:该变量存储了要反编译的 .class 文件的路径。在这个例子中,是 MyServlet.class 文件。这个 .class 文件通常位于 WEB-INF/classes/ 目录下,这是 Java Web 应用程序的常规目录结构。

  • outputFilePath:存储生成的 .java 源代码文件的路径,反编译后的源代码将写入这个路径下的文件。

3. 反编译逻辑
try {
    Decompiler.decompile(classFilePath, new PrintWriter(new FileWriter(outputFilePath)));
    System.out.println("Class file decompiled successfully!");
} catch (IOException e) {
    e.printStackTrace();
}
  • Decompiler.decompile(classFilePath, new PrintWriter(new FileWriter(outputFilePath))):这一行是反编译的核心。

    • Decompiler.decompile():这是 Procyon 的反编译方法,负责将 .class 文件转换为 .java 文件。
    • classFilePath:作为参数,指定需要反编译的 .class 文件。
    • new PrintWriter(new FileWriter(outputFilePath)):通过 PrintWriterFileWriter 将反编译生成的源代码写入指定的 Java 文件。FileWriter 负责创建或打开指定的文件,PrintWriter 则用于写入数据。

    当反编译过程成功完成时,生成的 .java 文件将保存到 outputFilePath 指定的路径,并输出成功消息。

4. 异常处理
catch (IOException e) {
    e.printStackTrace();
}
  • IOException:此异常处理块捕获并处理文件读取和写入过程中的 I/O 异常。如果在文件操作过程中出现问题(如路径不存在、文件无法创建等),e.printStackTrace() 将打印异常的详细信息。
5. 代码工作流程总结
  1. 设定文件路径:程序首先指定要反编译的 .class 文件路径(classFilePath),以及要输出的 .java 源文件路径(outputFilePath)。
  2. 调用 Procyon 反编译器Decompiler.decompile() 方法将 .class 文件反编译为 Java 源文件,并写入到指定的 .java 文件中。
  3. 输出结果:反编译成功后,输出一条信息提示反编译成功;如果发生异常,则打印异常堆栈信息。
6. 扩展:如何使用 Procyon 反编译库
  • 添加 Procyon 依赖:为了使代码正常工作,必须将 Procyon 反编译库添加到项目中。在 Maven 项目中,可以通过以下依赖添加 Procyon:
    <dependency>
        <groupId>com.strobel</groupId>
        <artifactId>procyon-compilertools</artifactId>
        <version>0.5.36</version>
    </dependency>
    
7. 使用场景
  • 代码审计:可以通过反编译工具检查编译后的 Java 类,分析它们的实现和逻辑。
  • 遗失源代码:当某个项目的源代码丢失或无法找到时,可以通过反编译生成 Java 源代码。
  • 安全分析:反编译是逆向工程的一部分,通常用于分析 WAR 包或其他 Java 项目中的安全漏洞。
总结

这段代码展示了如何通过 Procyon 反编译工具,将 Java 字节码(.class 文件)反编译为 Java 源文件。通过指定文件路径,程序可以轻松生成 Java 代码,便于开发者分析和处理编译后的代码。在实际项目中,反编译工具为代码审查、安全分析和调试提供了极大便利。

拓展:常用工具与优化策略

1. 常用反编译工具

  • JD-GUI:简单易用的 GUI 反编译工具,适合快速浏览反编译后的 Java 类。
  • CFR:一个功能强大且准确率高的反编译工具,支持最新的 Java 版本。
  • Procyon:能够处理复杂的字节码结构,并支持反编译较新的 Java 特性,如 lambdas 和方法引用。

2. 常见问题与解决方法

  • 类文件混淆:有些 WAR 包中的类文件可能经过了代码混淆,导致反编译后的代码可读性极差。此时,可以尝试使用专门的反混淆工具,如 ProGuard 的反混淆功能。
  • 依赖丢失:有时解压 WAR 包后,可能无法直接运行或分析代码,因为某些依赖可能打包在外部 JAR 中。在这种情况下,需要从 WEB-INF/lib 文件夹中提取依赖,并确保它们在类路径中。

3. 内存和性能优化

处理大规模 WAR 包时,内存和性能可能成为瓶颈。可以采用以下策略优化:

  • 分块读取大文件:避免一次性加载整个 WAR 包,特别是处理非常大的 WAR 文件时。
  • 多线程处理:可以使用多线程并发处理每个子目录的解压与分析,提高效率。
  • 缓存机制:通过缓存一些已经反解析过的类或配置文件,避免重复处理,提高性能。

总结

通过本文的详细讲解,我们了解了如何进行 Java WAR 包的反解析,并掌握了其背后的技术细节和具体实现方式。从解压 WAR 包、分析文件结构、反编译 Java 类到解决常见问题,本文提供了全方位的讲解和示例代码。对于需要深入分析和诊断 Java Web 应用的开发者来说,掌握 WAR 包反解析的技能无疑将带来极大的帮助。

此外,我们还探讨了常用的反编译工具和一些常见的优化策略,为开发者在实际项目中处理大规模 WAR 包提供了实用的指导。通过这些技术手段,开发者不仅能够更好地维护和优化现有系统,还可以在遇到问题时快速定位并解决问题。

☀️建议/推荐你

无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学Java」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门Java编程,就像滚雪球一样,越滚越大,指数级提升。

码字不易,如果这篇文章对你有所帮助,帮忙给bug菌来个一键三连(关注、点赞、收藏) ,您的支持就是我坚持写作分享知识点传播技术的最大动力。
  同时也推荐大家关注我的硬核公众号:「猿圈奇妙屋」 ;以第一手学习bug菌的首发干货,不仅能学习更多技术硬货,还可白嫖最新BAT大厂面试真题、4000G Pdf技术书籍、万份简历/PPT模板、技术文章Markdown文档等海量资料,你想要的我都有!

📣关于我

我是bug菌,CSDN | 掘金 | infoQ | 51CTO 等社区博客专家,历届博客之星Top30,掘金年度人气作者Top40,51CTO年度博主Top12,掘金等平台签约作者,华为云 | 阿里云| 腾讯云等社区优质创作者,全网粉丝合计30w+ ;硬核微信公众号「猿圈奇妙屋」,欢迎你的加入!免费白嫖最新BAT互联网公司面试题、4000G pdf电子书籍、简历模板等海量资料。


–End

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。