JavaAgent 技术原理及实战

news2024/11/19 4:33:32

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超详细知识梳理

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

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

相关文章

搞学术研究好用免费的学术版ChatGPT网站-学术AI

https://chat.uaskgpt.com/mobile/?user_sn88&channelcsdn&sceneloginhttps://chat.uaskgpt.com/mobile/?user_sn88&channelcsdn&scenelogin 推荐一个非常适合中国本科硕士博士等学生老师使用的学术版ChatGPT&#xff0c; 对接了超大型学术模型&#xff0c…

考研数学|《1800题》基础练习基本不会,怎么办?

这其实是因为&#xff0c;知识点之间没有形成联结 这样只要题目难度提升&#xff0c;一个题目的知识点综合度变高&#xff0c;就不知道该怎么做了。 不要害怕&#xff0c;其实考研复习早起阶段&#xff0c;大家基本上都经过这个阶段&#xff0c;不过有的同学能够快速找到做题…

Vue ElementPlus Input 输入框

Input 输入框 通过鼠标或键盘输入字符 input 为受控组件&#xff0c;它总会显示 Vue 绑定值。 通常情况下&#xff0c;应当处理 input 事件&#xff0c;并更新组件的绑定值&#xff08;或使用v-model&#xff09;。否则&#xff0c;输入框内显示的值将不会改变&#xff0c;不支…

[C#]winform使用OpenCvSharp实现透视变换功能支持自定义选位置和删除位置

【透视变换基本原理】 OpenCvSharp 是一个.NET环境下对OpenCV原生库的封装&#xff0c;它提供了大量的计算机视觉和图像处理的功能。要使用OpenCvSharp实现透视变换&#xff08;Perspective Transformation&#xff09;&#xff0c;你首先需要理解透视变换的原理和它在图像处理…

WPF中动画教程(DoubleAnimation的基本使用)

实现效果 今天以一个交互式小球的例子跟大家分享一下wpf动画中DoubleAnimation的基本使用。该小球会移动到我们鼠标左键或右键点击的地方。 该示例的实现效果如下所示&#xff1a; 页面设计 xaml如下所示&#xff1a; <Window x:Class"AnimationDemo.MainWindow&qu…

mysql 正则表达式查询

学习了mysql 连接查询和子查询和myql join连接&#xff0c;接下来学习下正则表达式查询。正则表达式的规则都是相似的。 8&#xff0c;使用正则表达式查询 正则表达式通常被用来检索或替换那些符合某个模式的文本内容,根据指定的匹配模式匹配文本中符合要求的特殊字符串。例如从…

Android 高德地图

1.获取Key 进入高德开放平台控制台&#xff0c;创建一个新应用。在创建的应用上点击"添加key"按钮&#xff0c;在弹出的对话框中&#xff0c;依次输入key名称&#xff0c;选择服务平台为“Android平台”&#xff0c;输入发布版安全码 SHA1、以及 Package。 获取 S…

flutter获取手机中的系统路径信息

https://www.bilibili.com/video/BV1wE421g7sw获取系统中的路径 获取系统中的路径&#xff0c;并在这个路径中创建一个文本文件【str.txt】 然后进行写入【str.txt】 再读取这个文件【str.txt】 手机没有开通root权限无法看到写入到【应用程序文档目录】路径中的文件 用来…

案例分析-程序的机器级表示

案例一&#xff1a;关于编译优化 请自写一段if- else简单分支程序&#xff0c;分别尝试对它进行不带优化、-O1优化和-O2优化&#xff0c;比较它们的机器级表达&#xff0c;并讨论优劣。 图一为不带优化、图二为O1优化、图三为O2优化、图四为原始C代码。 &#xff08;1&#xff…

x-cmd-pkg | broot 是基于 Rust 开发的一个终端文件管理器

简介 broot 是基于 Rust 开发的一个终端文件管理器&#xff0c;它设计用于帮助用户在终端中更轻松地管理文件和目录&#xff0c;使用树状视图探索文件层次结构、操作文件、启动操作以及定义您自己的快捷方式。 同时它还集成了 ls, tree, find, grep, du, fzf 等工具的常用功能…

IDEA连接SqlServer数据库

目录 下载jar包 下载sqljdbc_12.6压缩包 解压 导入IDEA 新建文件夹 复制粘贴进JDBC文件夹并设为library 编写类及方法 代码 下载jar包 以sqljdbc_12.6为例 下载sqljdbc_12.6压缩包 最新地址&#xff1a;sqljdbc 官方最新地址 解压 解压即用 导入IDEA 新建文件夹 复制…

标题:探索AI绘画:使用深度学习生成艺术

正文&#xff1a; 随着计算机技术的发展&#xff0c;人工智能在各个领域取得了显著的成果。通过训练深度学习模型&#xff0c;AI可以学习大量的艺术作品&#xff0c;从而生成具有独特风格和创意的新作品。 本文将介绍如何使用Python和TensorFlow实现一个简单的AI绘画程序。 二、…

【国信华源2024年首场春季校园招聘面试会举办】

阳春三月&#xff0c;春意盎然&#xff0c;北京国信华源科技有限公司2024年校园招聘活动如期展开。4月2日&#xff0c;成功举办了“国信华源2024年首场春季校园招聘面试会”。 国信华源公司人力资源部热情接待了前来参加面试的同学们&#xff0c;并亲自陪同他们深入探访了企业。…

Docker 哲学 - compose.yaml 指令

compose.yaml 的 image commond working_dir 和 dockerfile的 from cmd workdir 区别在哪里 。为什么 dockerfile制定过了。compose还要再写一个。是处于个性化还是 有不同的意义 如果 dockerfile 的 from 是 node:16 &#xff0c;compose.yaml 的 images 是 node:18 那么 直接…

使用 Docker 部署 Photopea 在线 PS 工具

1&#xff09;Photopea 介绍 GitHub&#xff1a;https://github.com/photopea/photopea 官方手册&#xff1a;https://www.photopea.com/learn/ Adobe 出品的「PhotoShop」想必大家都很熟悉啦&#xff0c;但是「PhotoShop」现在对电脑配置要求越来越高&#xff0c;体积越来越大…

00-JAVA基础-反射机制

反射 什么是反射 在Java中&#xff0c;反射&#xff08;Reflection&#xff09;是Java语言的一个特性&#xff0c;它允许程序在运行时检查类、接口、字段和方法的信息。通过反射&#xff0c;Java代码能够动态地创建对象、调用方法、改变字段的值等&#xff0c;而无需在编译时…

基础篇3 浅试Python爬虫爬取视频,m3u8标准的切片视频

浅试Python爬取视频 1.页面分析 使用虾米视频在线解析使用方式&#xff1a;https://jx.xmflv.cc/?url目标网站视频链接例如某艺的视频 原视频链接 解析结果: 1.1 F12查看页面结构 我们发现页面内容中什么都没有&#xff0c;video标签中的src路径也不是视频的数据。 1.2 …

linux清理缓存垃圾命令和方法介绍

在Linux系统中&#xff0c;清理缓存和垃圾文件可以通过多种方法完成&#xff0c;这些方法旨在释放磁盘空间、提高系统性能。以下是一些常用的方法&#xff0c;结合了搜索结果中的信息&#xff1a; 1. 使用sync和echo命令清除RAM缓存和交换空间1 清除页面缓存&#xff08;Page …

不讲概念,讲实操,mysql 分表模糊查询、分页查询 及 merge 表的使用

1.Mysql merge合并表的要求 1.合并的分表必须是 MyISAM 引擎&#xff0c;MyISAN引擎是不支持事务的。2.Merge表只保证合表后数据唯一性&#xff0c;合表前的数据可能会存在重复。3.表的结构必须一致&#xff0c;包括索引、字段类型、引擎和字符集。4.删除 tb_member1 分表正确…

原理图设计的通用规范

原理图各页内容依次为&#xff1a;封面、目录、电源、时钟、CPU、存储器、逻辑、背板&#xff08;母板&#xff09;接口等。 原理图上所有的文字方向应该统一&#xff0c;文字的上方应该朝向原理图的上方&#xff08;正放文字&#xff09;或左方&#xff08;侧放文字&#xff…