JavaAgent 技术原理及实战

JavaAgent 技术原理及实战

  • 1、引子
  • 2、JavaAgent 简单示例:方法开始和结束时打印日志
    • 2.1 创建 Agent
    • 2.2 编写验证 agent 功能的测试类
      • 2.2.1 使用JavaAgent 静态加载方式
      • 2.2.2 使用 JavaAgent 动态加载方式
    • 2.3、小结
  • 3、JavaAgent
    • 3.1 JavaAgent是什么?
    • 3.2 原理解析
      • 3.2.1 JVMTI
      • 3.2.2 Instrumentation
      • 3.2.3 JavaAgent 的加载
        • 3.2.3.1 静态加载
        • 3.2.3.2 动态加载
  • 4、实战:使用JavaAgent实现全链路监控基础版
    • 4.1 概述
    • 4.2 技术环境
    • 4.3、常见问题
      • 4.3.1 JavaAgent
      • 4.3.2 常用的修改字节码的工具?
      • 4.3.3 为什么跨线程不能透传traceId?如何解决?
      • 4.3.4 ThreadLocal、InheritableThreadLocal和TransmittableThreadLocal三者区别?
      • 4.3.5 Spring Boot Starter?
      • 4.3.6 JavaAgent与Spring AOP和AspectJ之间有什么区别?
  • 源码下载

在这里插入图片描述

1、引子

线上故障群发来一条用户投诉:用户抱怨页面加载时间过长,有时甚至超时。这时,你首先检查了服务器和数据库,但并未发现有问题。你尝试在开发环境中复现问题,但一切运行正常。这个问题只在生产环境的特定时段出现,常规的Debug方式并不奏效。

此刻的你感到困惑和无助,不禁开始怀疑:是不是应用程序的某个部分在增加系统的延迟?有没有方法可以帮助你追踪代码的运行过程,看一看到底哪里出现了问题?

此时,JavaAgent技术闪耀登场。这项技术可以帮助你深入观察应用程序的运行状态,助你洞察问题的根源。那么我们就一起来探索一下JavaAgent技术。

2、JavaAgent 简单示例:方法开始和结束时打印日志

2.1 创建 Agent

创建javaagent-demo工程,目录结构如下:
在这里插入图片描述
新建 pom.xml​,引入 javassist​ 用来修改目标类的字节码,增加自定义代码。通过 maven-assembly-plugin 插件打包自定义的 agent jar。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <parent>
        <artifactId>javaagent-demo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <artifactId>agent-demo</artifactId>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.javassist</groupId>
            <artifactId>javassist</artifactId>
            <version>3.25.0-GA</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.1</version>
                <configuration>
                    <descriptorRefs>
                        <!--将应用的所有依赖包都打到jar包中。如果依赖的是 jar 包,jar 包会被解压开,平铺到最终的 uber-jar 里去。输出格式为 jar-->
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <archive>
                        <!-- 设置manifest配置文件-->
                        <manifestEntries>
                            <!--Premain-Class: 代表 Agent 静态加载时会调用的类全路径名。-->
                            <Premain-Class>com.atu.DemoAgent</Premain-Class>
                            <!--Agent-Class: 代表 Agent 动态加载时会调用的类全路径名。-->
                            <Agent-Class>com.atu.DemoAgent</Agent-Class>
                            <!--Can-Redefine-Classes: 是否可进行类定义。-->
                            <Can-Redefine-Classes>true</Can-Redefine-Classes>
                            <!--Can-Retransform-Classes: 是否可进行类转换。-->
                            <Can-Retransform-Classes>true</Can-Retransform-Classes>
                        </manifestEntries>
                    </archive>
                </configuration>
                <executions>
                    <execution>
                        <!--绑定到package生命周期阶段上-->
                        <phase>package</phase>
                        <goals>
                            <!--绑定到package生命周期阶段上-->
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.1</version>
                <configuration>
                    <source>${maven.compiler.source}</source>
                    <target>${maven.compiler.target}</target>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

编写agent核心代码 DemoAgent.java,我们使用了premain()​静态加载方式,agentmain()​动态加载方式。并用到了Instrumentation​类结合javassist代码生成库进行字节码的修改。

public class DemoAgent {
    /**
     * 被转换的类
     */
    private static String TRANSFORM_CLASS = "com.atu.Test";

    public static void premain(String agentArgs, Instrumentation inst) {
        // 在这里可以对应用程序进行字节码转换
        // 例如,添加一个Transformer
        inst.addTransformer(new MyClassTransformer());
    }

    /**
     * 动态加载。Java agent指定的premain方法,会在main方法之前被调用
     */
    public static void agentmain(String args, Instrumentation inst) {
        System.out.println("agentmain start!");
        inst.addTransformer(new MyClassTransformer());
        Class<?>[] classes = inst.getAllLoadedClasses();
        if (classes != null) {
            for (Class<?> c : classes) {
                if (c.isInterface() || c.isAnnotation() || c.isArray() || c.isEnum()) {
                    continue;
                }
                if (c.getName().equals(TRANSFORM_CLASS)) {
                    try {
                        System.out.println("retransformClasses start, class: " + c.getName());
                        /*
                         * retransformClasses()对JVM已经加载的类重新触发类加载。使用的就是上面注册的Transformer。
                         * retransformClasses()可以修改方法体,但是不能变更方法签名、增加和删除方法/类的成员属性
                         */
                        inst.retransformClasses(c);
                        System.out.println("retransformClasses end, class: " + c.getName());
                    } catch (UnmodifiableClassException e) {
                        System.out.println("retransformClasses error, class: " + c.getName() + ", ex:" + e);
                        e.printStackTrace();
                    }
                }
            }
        }
        System.out.println("agentmain end!");
    }
}
public class MyClassTransformer implements ClassFileTransformer {
    /**
     * 被转换的类
     */
    private static String TRANSFORM_CLASS = "com.atu.Test";

    @Override
    public byte[] transform(ClassLoader loader, String className,
                            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
        // 在这里可以修改字节码
        // 例如,打印类名
        try {
            className = className.replace("/", ".");
            if (className.equals(TRANSFORM_CLASS)) {
                final ClassPool classPool = ClassPool.getDefault();
                final CtClass clazz = classPool.get(TRANSFORM_CLASS);

                for (CtMethod method : clazz.getMethods()) {
                    /*
                     * Modifier.isNative(methods[i].getModifiers())过滤本地方法,否则会报
                     * javassist.CannotCompileException: no method body  at javassist.CtBehavior.addLocalVariable()
                     * 报错原因如下
                     * 来自Stack Overflow网友解答
                     * Native methods cannot be instrumented because they have no bytecodes.
                     * However if native method prefix is supported ( Transformer.isNativeMethodPrefixSupported() )
                     * then you can use Transformer.setNativeMethodPrefix() to wrap a native method call inside a non-native call
                     * which can then be instrumented
                     */
                    if (Modifier.isNative(method.getModifiers())) {
                        continue;
                    }

                    method.insertBefore("System.out.println(\"" + clazz.getSimpleName() + "."
                            + method.getName() + " start.\");");
                    method.insertAfter("System.out.println(\"" + clazz.getSimpleName() + "."
                            + method.getName() + " end.\");", false);
                }

                return clazz.toBytecode();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return classfileBuffer;
    }
}

编译打包:

在这里插入图片描述

2.2 编写验证 agent 功能的测试类

在这里插入图片描述

public class Test {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("helloworld!");
        Thread.sleep(20000);
    }
}

2.2.1 使用JavaAgent 静态加载方式

在 IDEA 的 Run/Debug Configurations 中,点击 Modify options,勾选上 add VM options,在 VM options 栏增加 -javaagent:全路径\agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar

运行 Test.java 的 main 方法,可以看到控制台日志:
在这里插入图片描述

2.2.2 使用 JavaAgent 动态加载方式

动态加载不是通过 -javaagent: 的方式实现,而是通过 Attach API 的方式。

编写调用 Attach API 的测试类:

public class AttachMain {

    public static void main(String[] args) throws Exception {
        // agentmain()方法所在jar包
        String jar = "E:\\projects\\mycode\\javaagent-demo\\agent-demo\\target\\agent-demo-1.0-SNAPSHOT-jar-with-dependencies.jar";

        for (VirtualMachineDescriptor virtualMachineDescriptor : VirtualMachine.list()) {
            // 针对指定名称的JVM实例
            if (virtualMachineDescriptor.displayName().equals("com.atu.Test")) {
                System.out.println("将对该进程的vm进行增强:com.atu.Test的vm进程, pid=" + virtualMachineDescriptor.id());
                // attach到新JVM
                VirtualMachine vm = VirtualMachine.attach(virtualMachineDescriptor);
                // 加载agentmain所在的jar包
                vm.loadAgent(jar);
                // detach
                vm.detach();
            }
        }
    }
}

先直接运行 com.atu.Test#main​,注意不用加 -javaagent: 启动参数。
在这里插入图片描述约 5 秒后,再运行 com.atu.AttachMain#main,可以看到 com.atu.AttachMain#main 打印的日志:

将对该进程的vm进行增强:com.atu.Test的vm进程, pid=15544

之后可以看到 com.atu.Test#main打印的日志中多了记录方法运行开始和结束的内容。

helloworld!
agentmain start!
retransformClasses start, class: com.atu.Test
retransformClasses end, class: com.atu.Test
agentmain end!

2.3、小结

可以看到静态加载或动态加载相同的 agent,都能实现了记录记录方法运行开始和结束日志的功能。

下面进入正题:什么是JavaAgent?

3、JavaAgent

3.1 JavaAgent是什么?

JavaAgent 是一种特殊的类,它提供了一种能力,使得我们可以在Java程序运行期间,对加载到JVM中的类进行字节码层面的修改和增强。

简单来说,JavaAgent能够“拦截”类加载过程,在类被使用前“改变”它的行为。

JavaAgent本质上可以理解为一个插件,该插件就是一个精心提供的jar包,这个jar包通过JVMTI(JVM Tool Interface)完成加载,最终借助JPLISAgent(Java Programming Language Instrumentation Services Agent)完成对目标代码的修改。

JavaaAgent通常用在如性能监控(Profiler)、代码热替换、动态追踪等领域。一些知名的工具,比如JRebel(代码热替换)、SkyWalking(性能监控)就是基于JavaAgent 实现的。

3.2 原理解析

3.2.1 JVMTI

JVMTI​ (JVM Tool Interface)是 Java 虚拟机对外提供的 Native 编程接口,通过 JVMTI ,外部进程可以获取到运行时 JVM 的诸多信息,比如线程、GC 等。

JVMTI是基于事件驱动的,JVM每执行一定的逻辑就会触发一些事件的回调接口,通过这些回调接口,用户可以自行扩展实现自己的逻辑。

JVMTI 是一套 Native 接口,在 Java SE 5 之前,要实现一个 Agent 只能通过编写 Native 代码来实现。从 Java SE 5 开始,可以使用 Java 的Instrumentation 接口(java.lang.instrument)来编写 Agent。无论是通过 Native 的方式还是通过 Java Instrumentation 接口的方式来编写 Agent,它们的工作都是借助 JVMTI 来进行完成。

Instumentation API 可以支持 Java 语言实现 agent 功能,但是 JVMTI 功能比 Instumentation API 更强大。

3.2.2 Instrumentation

Instrumentation 是 Java 提供的 JVM 接口,该接口提供了一系列查看和操作 Java 类定义的方法,例如修改类的字节码、向 classLoader 的 classpath 下加入 jar 文件等。使得开发者可以通过 Java 语言来操作和监控 JVM 内部的一些状态,进而实现 Java 程序的监控分析,甚至实现一些特殊功能(如 AOP、热部署)。

Instrumentation接口中最常用的方法是 addTransformer(ClassFileTransformer transformer)​,这个方法可以在类加载时做拦截,对输入的类的字节码进行修改,其参数是一个ClassFileTransformer接口:

public interface ClassFileTransformer {

    /**
     * 传入参数表示一个即将被加载的类,包括了classloader,classname和字节码byte[]
     * 返回值为需要被修改后的字节码byte[]
     */
    byte[]
    transform(  ClassLoader         loader,
                String              className,
                Class<?>            classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer)
        throws IllegalClassFormatException;
}

3.2.3 JavaAgent 的加载

JavaAgent 支持静态加载和动态加载。

3.2.3.1 静态加载

静态加载,即 JVM 启动时加载,对应的是 premain()​ 方法。通过 vm 启动参数-javaagent 将 agent jar 挂载到目标 JVM 程序,随目标 JVM 程序一起启动。

  1. 加载 JavaAgent:JavaAgent 中的 class 通常是由 system calss loader(默认AppClassLoader) 加载。
  2. 调用 premain方法: JVM 加载完成 JavaAgent 的入口类之后,会调用其 premain 方法
  3. 使用 Instrumentation API:通过 premain 方法,JavaAgent 得到了 Instrumentation 的实例。JavaAgent 可以使用这个 Instrumentation 实例来注册一个或多个 ClassFileTransformer,或者进行其它需要的操作。这是 JavaAgent 真正开始发挥作用的地方。
  4. Instrumentation API 调用 JVMTI: JVMTI 是 JNI (Java Native Interface) 的一部分,提供了丰富的接口供 Instrumentation API 和 JVM 进行交互。
  5. 执行 ClassFileTransformer:当 Instrumentation 需要载入类时,如果 Java Agent 对该类注入了 ClassFileTransformer,JVMTI 会回调 Instrumentation API,然后调用对应的 ClassFileTransformer.transform 方法。在 transform 方法中,JavaAgent 可以修改类的字节码。
  6. premain() 方法会调用 Instrumentation API,然后 Instrumentation API 调用 JVMTI(JVMTI 的内容将在后面补充),在需要加载的类需要被加载时,会回调 JVMTI,然后回调 Instrumentation API,触发 ClassFileTransformer.transform(),最终修改 class 的字节码。
  7. 加载主类并启动应用: 所有的 JavaAgent 加载和初始化完成后,JVM 会准备开始加载主类并执行其 main 方法。

在这里插入图片描述

ClassFileTransformer.transform:是 Java Instrumentation API 中的核心方法,它的作用是在类文件被 JVM 加载之前,对其进行字节码级别的转换和修改。

3.2.3.2 动态加载

JVM运行时加载,可以在 main 函数开始运行之后再运行。通过Attach API​动态地加载 JavaAgent,对应的是 agentmain() 方法。

基本流程:

  1. 创建一个 VirtualMachine 实例:首先,使用 VirtualMachineDescriptor 获取要附加的Java虚拟机的描述,并通过 VirtualMachine.attach() 方法连接到该JVM。
  2. 加载JavaAgent:在连接到 JVM 后,使用 VirtualMachine.loadAgent() 方法加载JavaAgent的jar包。
  3. 修改字节码
  4. 断开连接:改完成后,通过 VirtualMachine.detach() 方法断开与JVM的连接。所有的更改都会在目标JVM中保存,不会影响到运行Agent的JVM。

4、实战:使用JavaAgent实现全链路监控基础版

4.1 概述

  1. 利用 javassist 对 Log 框架进行切面增强。
  2. 利用 Spring 的拦截器技术实现了 Web 请求的 traceId 初始赋值。
  3. 通过整合 dubbo SPI,结合 dubbox 调用拦截器,实现 traceId 的拦截及赋值。
  4. 引入TransmittableThreadLocal 解决父子线程上下文传递的问题。

4.2 技术环境

Javassist 3.25.0-GA+Dubbox 2.6.5+Spring Boot 2.0.5.RELEASE+Transmittable 2.12.2

4.3、常见问题

4.3.1 JavaAgent

JavaAgent 是一种特殊的 Java 程序,它利用 Java 的 Instrumentation 机制在运行时改变或分析其他Java 程序的行为,并对其进行监控、调试、或性能优化等操作。

有了 JavaAgent 技术,可以在字节码这个层面对类和方法进行修改,可以把 JavaAgent 理解成一种代码注入的方式,或者可以说 JavaAgent 就是 JVM 层面的代理程序。

4.3.2 常用的修改字节码的工具?

  1. ASM
  2. Javassist
  3. ByteBuddy

4.3.3 为什么跨线程不能透传traceId?如何解决?

我们常用的日志框架,比如 Logback,Log4j 等,通过使用 MDC(Mapped Diagnostic Context,映射调试上下文)在多线程环境下记录日志。

我们的请求链路ID,也是借助 MDC 实现传递的。

MDC 是以线程为基础的存储结构,每个线程都有其自己的一份独立的 MDC 数据。这是通过底层的 ThreadLocal 实现的,ThreadLocal 为每个线程提供了一个独立的数据副本,每个线程都只能看到及修改自己的 ThreadLocal 副本数据,而看不到其他线程的数据,这样可以有效避免数据之间的相互影响。

那么当异步方法切换线程的时候,就会出现上下文信息传递丢失的问题。从而导致 TraceID 丢失的问题。

我们需要在父线程中手动获取并传递 MDC 数据到子线程,解决数据跨线程传递的问题。

4.3.4 ThreadLocal、InheritableThreadLocal和TransmittableThreadLocal三者区别?

1)、ThreadLocal

ThreadLocal主要是为每个ThreadLocal对象创建一个ThreadLocalMap来保存对象和线程中的值的映射关系。

当创建一个ThreadLocal对象时会调用get()或set()方法,在当前线程的中查找这个ThreadLocal对象对应的Entry对象,如果存在,就获取或设置Entry中的值;否则,在ThreadLocalMap中创建一个新的Entry对象。

ThreadLocal类的实例被多个线程共享,每个线程都拥有自己的ThreadLocalMap对象,存储着自己线程中的所有ThreadLocal对象的键值对。

ThreadLocal的实现比较简单,但需要注意的是,如果使用不当,可能会出现内存泄漏问题,因为ThreadLocalMap中的Entry对象并不会自动删除。

2)、InheritableThreadLocal

InheritableThreadLocal的实现方式和ThreadLocal类似,但不同之处在 Thread 类的 init() 方法中,当创建新的线程时,会调用 inheritThreadLocals(parentThread) 方法,这个方法就是将父线程的 InheritableThreadLocalMap (注意这里并不是 ThreadLocalMap)复制一份到子线程中。

局限性:InheritableThreadLocal 支持子线程访问在父线程的核心思想是在创建线程的时候将父线程中的本地变量值复制到子线程,即复制的时机为创建子线程时。

线程池能够复用线程,减少线程的频繁创建与销毁,如果使用 InheritableThreadLocal ,那么线程池中的线程拷贝的数据来自于第一个提交任务的外部线程,即后面的外部线程向线程池中提交任务时,子线程访问的本地变量都来源于第一个外部线程,造成线程本地变量混乱。

3)、TransmittableThreadLocal

TransmittableThreadLocal 是阿里巴巴开源的专门解决 InheritableThreadLocal 的局限性,实现线程本地变量在线程池的执行过程中,能正常的访问父线程设置的线程变量。

TTL 的设计理念在于,每次线程执行任务时,都会备份当前线程的 TTL 值,然后从提交任务的线程那里拷贝一份新的 TTL 值到当前线程,
任务执行完成后,再将备份的 TTL 值恢复回当前线程。

具体流程如下:

  1. 任务提交到线程池时,首先会将提交任务的线程(父线程)的 TTL 值拷贝一份,然后作为本次任务执行的上下文。
  2. 线程获取到任务执行时,先将线程原先的 TTL 值进行备份,然后将第一步拷贝的 TTL 值设置到线程中。
  3. 任务执行完成后,线程将自身的 TTL 值设置回第二步备份的值。

通过这种方式,TTL 成功解决了在使用 InheritableThreadLocal 时线程池中由于线程复用导致的问题,确保了每次任务执行时,线程内的 TTL 值都是我们期望的那个值。

4.3.5 Spring Boot Starter?

Spring Boot Starter 是 Spring Boot 框架提供的一种特性,它是一种提供依赖项的方式,可以帮助开发人员快速集成各种第三方库和框架。

Spring Boot Starter 的目的是简化 Spring 应用程序的依赖管理,将一组相关的依赖项打包在一起,并提供一个依赖项描述文件,使开发人员可以快速集成。

Spring Boot Starter 本质上是一个包含了必要依赖和自动配置类的 Maven 依赖(是一系列依赖集合),它能够自动配置应用程序的运行环境,并提供默认的配置选项,让开发人员可以快速开始开发。

举个例子,如果在 Spring Boot 项目中使用 Spring MVC,需要引入多个与Spring MVC相关的依赖,包括 spring-webmvc、spring-web等,这时候如果使用 spring-boot-starter-web 这个starter,只需要添加一个依赖就可以了,它会包含使用 Spring MVC 所需要的所有依赖。

4.3.6 JavaAgent与Spring AOP和AspectJ之间有什么区别?

1)、JavaAgent

Java Agent 是 Java 5 引入的一种机制,它能够通过预处理(Pre-processing)和类转换(Class Transformation)的方式,修改已有的字节码。

Java Agent 通常在 JVM 启动或者类加载时进行操作。每当一个类被 JVM 加载,都会调用 Java Agent 的代理方法,以便进行类字节码的转换。
因此,Java Agent 更底层,使用复杂,但功能十分强大。

2)、AspectJ

AspectJ 是最早的切面编程框架之一,并且它提供了非常强大的切面编程能力。
AspectJ 通过类似于 Java 语言的 AspectJ 语言来书写切面,并且提供了一个 AspectJ 编译器,将 AspectJ 代码编译成可以运行的字节码。
AspectJ 支持更多更细粒度的切入点,如方法调用,实例创建等,能够在运行时进行热替换和精细控制,其功能强大,但使用和学习成本较高。

3)、Spring AOP

Spring AOP 是 Spring 框架提供的切面编程实现。
它主要利用 Java 的动态代理机制以及 CGLIB 库来在运行时动态地创建对象的代理。
与 AspectJ 相比,Spring AOP 更轻量级且简单,但其切入点种类有限,主要支持方法执行切点,不能做到类似 AspectJ 的构造函数或属性切入。
使用便捷,适用于一般的日志、事务等场景,对于复杂的切点和切面控制较为局限。

源码下载

源码

【文章参考】
【JVM】Java agent超详细知识梳理

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

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

相关文章

使用Detours进行HOOK

文章目录 Detours介绍Detours配置Detours进行Sleep Hook Detours介绍 Detours是微软研究院开发的一款软件工具&#xff0c;用于Windows平台上的应用程序重定向和修改。 它可以在运行时修改应用程序的执行路径&#xff0c;允许开发人员注入自定义代码来改变应用程序的 行为&…

实验2:CLI的使用与IOS基本命令

1、实验目的 通过本实验可以掌握&#xff1a; CLI的各种工作模式个CLI各种编辑命令“?” 和【Tab】键使用方法IOS基本命令网络设备访问限制查看设备的相关信息 2、实验拓扑 CLI的使用与IOS基本命令使用拓扑如下图所示。 3、实验步骤 &#xff08;1&#xff09;CLI模式的切…

docker-compose编排lnmp环境

&#xff08;1&#xff09;创建一个目录lnmpcompose [rootlocalhost ~]# tree lnmpcompose lnmpcompose ├── conf │ └── nginx.conf ├── docker-compose.yml └── html ├── index.html ├── mysql.php └── test.php &#xff08;2&#xff09;在创建的目…

用ENIGMA-toolbox作图

之前一直使用ggseg呈现结果&#xff0c;最近想试一试其他绘图工具。ENIGMA-toolbox有所了解&#xff0c;绘图功能看起来门槛不高&#xff0c;所以就试着用它呈现一些结果。Matlab版本的ENIGMA-toolbox直接使用就是SurfStat的功能绘图&#xff0c;Python版本的绘图功能应该是根据…

Spring Boot页面国际化

GitHub&#xff1a;SpringBootDemo Gitee&#xff1a;SpringBootDemo 微信公众号&#xff1a; 0 开发环境 JDK&#xff1a;1.8Spring Boot&#xff1a;2.7.18 1 检查配置 确保File -> Settings… -> Editor -> File Encodings 配置中编码为UTF-8&#xff0c;否则…

Python | Leetcode Python题解之第2题两数相加

题目&#xff1a; 题解&#xff1a; # Definition for singly-linked list. # class ListNode: # def __init__(self, val0, nextNone): # self.val val # self.next next class Solution:def addTwoNumbers(self, l1: Optional[ListNode], l2: Optiona…

攻防世界-easyphp

题目信息 题目信息如下&#xff1a; 可以看到&#xff0c;key1和key2的值都为1的时候&#xff0c;才能拿到flag。再网上看&#xff0c;发现key1和key2的值取决于a,b和c的值。 <?php highlight_file(__FILE__); $key1 0; $key2 0;$a $_GET[a]; $b $_GET[b];if(isset($a…

Java编程实战:疫情物资分配系统的设计与实现

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

分治——归并排序算法

例题一 解法&#xff08;归并排序&#xff09;&#xff1a; 算法思路&#xff1a; 归并排序的流程充分的体现了「分⽽治之」的思想&#xff0c;⼤体过程分为两步&#xff1a; ◦ 分&#xff1a;将数组⼀分为⼆为两部分&#xff0c;⼀直分解到数组的⻓度为 1 &#xff0c;使…

vulhub打靶记录——driftingbox

文章目录 主机发现端口扫描目录扫描爆破子域名提权总结 主机发现 使用nmap扫描局域网内存活的主机&#xff0c;命令如下&#xff1a; nmap -sP 192.168.56.0/24192.168.56.1&#xff1a;主机IP&#xff1b;192.168.56.100&#xff1a;DHCP服务器IP&#xff1b;192.168.56.101…

回溯算法|39.组合总和

力扣题目链接 class Solution { private:vector<vector<int>> result;vector<int> path;void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {if (sum > target) {return;}if (sum target) {result.push_back…

IDEA 详细设置

详细设置 如何打开详细配置界面 1、显示工具栏 2、选择详细配置菜单或按钮 系统设置 默认启动项目配置 启动IDEA时&#xff0c;默认自动打开上次开发的项目&#xff1f;还是自己选择&#xff1f; 如果去掉Reopen projects on startup前面的对勾&#xff0c;每次启动IDEA就会…

【软件安装教程】IDEA

【软件安装教程】IDEA 系统: Windows11 64位版本: ideaIC-2023.3.6官方地址: Jetbrains网盘地址: 百度网盘 下载 处于成本考虑就直接用社区版了&#xff0c;如果专业版配置过程都一样 安装 双击下载的文件 根据电脑存储选择一个合适的地址后点击下一步 选择配置&#…

鸿蒙OS(ArkTS) 案例:【使用http网络请求框架加载验证码】

需求&#xff1a;加载验证码&#xff1b;1.下载验证码图像文件&#xff1b;2.获取header里面验证码ID 踩坑--踩坑--踩坑 根据文档使用 request.downloadFile 请求&#xff0c;官方示例: // pages/xxx.ets // 将网络资源文件下载到应用文件目录并读取一段内容 import common …

Mac使用“Workstation”安装双系统

## 选择虚拟机 Mac推荐使用“VMware” 优点 1.个人版是免费的 2.界面清晰&#xff0c;运行流程 3.使用人群广&#xff0c;遇到问题容易解决 版本比较 VMware Workstation Pro 和 VMware Workstation Player 个人使用推荐 VMware Workstation Player &#xff0c;因为个人的…

二十四种设计模式与六大设计原则(四):【状态模式、原型模式、中介者模式、解释器模式、享元模式、备忘录模式】的定义、举例说明、核心思想、适用场景和优缺点

接上次博客&#xff1a;二十四种设计模式与六大设计原则&#xff08;三&#xff09;&#xff1a;【装饰模式、迭代器模式、组合模式、观察者模式、责任链模式、访问者模式】的定义、举例说明、核心思想、适用场景和优缺点-CSDN博客 目录 状态模式【State Pattern】 定义 举…

Python之numpy:常用运算广播机制

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 一、numpy运算二、常见运算1.ufunc函数2.复合赋值运算符3.判断符 二、聚合函数三、唯一化、集合四、numpy广播机制 一、numpy运算 numpy有两种基本对象&#xff1a…

【linux】AMD GPU和NVIDIA GPU驱动安装

AMD GPUs - Radeon™ PRO W7900的驱动安装过程 要在Linux系统上安装AMD的Radeon™ PRO W7900显卡驱动程序&#xff0c;通常需要执行以下步骤。以下示例基于Ubuntu系统&#xff1b;其他Linux发行版的具体步骤可能有所不同。 1. 更新系统 打开一个终端窗口&#xff0c;并输入…

redis学习-主从复制和哨兵模式

目录 1. 主从复制&#xff0c;读写分离 1.1 介绍 1.2 使用命令介绍 1.3 实现 1.4全量复制和增量复制 2.哨兵模式 1. 主从复制&#xff0c;读写分离 1.1 介绍 指的是将一台redis服务器中的数据复制到其他redis服务器&#xff0c;前者称为主机&#xff0c;后者称为从机&#xf…

C++刷题篇——04找等值元素

一、题目 二、解题思路 1、分割后放进二维数组 2、使用map&#xff0c;key为数值&#xff0c;value为其坐标 3、遍历二维数组元素&#xff0c;再在map中找该元素对应的value值&#xff08;二维数组形式&#xff09;&#xff0c;倘若value.size为1&#xff0c;那直接返回-1&…