Java探针技术详解

news2024/11/26 0:31:38

简介

在JVM中运行中,类是通过classLoader加载.class文件进行生成的。在类加载器加载.class文件生成对应的类对象之前时,我们可以通过修改.class文件内容(就是字节码修改技术),达到修改类的目的。JDK提供了对字节码进行操作的一系列api,而使用这些api开发出的程序就可以称之为java agent。
java agent能做什么?
不修改目标应用达到代码增强的目的,就好像spring的aop一样,但是java agent是直接修改字节码,而不是通过创建代理类。例如skywalking就是使用java agent技术,为目标应用代码植入监控代码,监控代码进行数据统计上报的。这种方式实现了解耦,通用的功能。

探针说白了,就是在应用启动之前,比你的应用 main 方法更早启动的一个系统,它可以对你的系统的类进行拦截,你可以将它类比为一个更强大的 AOP 工具。

使用

入门例子

1.创建一个maven工程

2.创建DemoAgent类。

public class DemoAgent {
    /**
     * 该方法在main方法之前运行
     */
    public static void premain(String arg, Instrumentation instrumentation) {
        System.out.println("执行premain方法");
    }
}

3.创建TestCache类。

public class TestCache {
    public static void main(String[] args) {
        System.out.println("测试类");
    }
}

4.加入pom插件依赖

    <build>
        <finalName>agent</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <appendAssemblyId>false</appendAssemblyId>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <outputDirectory>target</outputDirectory>
                    <archive>
                        <manifestEntries>
                            <!-- 包含premain方法的类 -->
                            <Premain-Class>DemoAgent</Premain-Class>
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>

如上依赖,注意,我去掉了我项目中的版本信息,你自己加上,另外,加了 maven-assembly-plugin 插件,指定了 DemoAgent 的位置,这非常重要,有点类似 SPI 的服务发现机制,如果没加,不会添加到 META-INF/MAINIFEST.MF  文件,也就不会加载 。

5.打包。

6.运行TestCache加入运行VM参数。

-javaagent:D:\idea\test\untitled\target\agent.jar

 

7.运行TestCache结果。

 

原理

两个重要的类 

Instrumentation: 由JDK提供的一个探针类,它会负责加载用户自定义的ClassFileTransformer。

public interface Instrumentation {
    //注册一个转换器,类加载事件会被注册的转换器所拦截
     void addTransformer(ClassFileTransformer transformer, boolean canRetransform);
    //重新触发类加载
     void retransformClasses(Class<?>... classes) throws UnmodifiableClassException;
    //直接替换类的定义
     void redefineClasses(ClassDefinition... definitions) throws  ClassNotFoundException, UnmodifiableClassException;
}

ClassFileTransformer: 字节码转换类,jvm在加载class文件前会先调用它,对所有类加载器有效。

总结:JVM探针只是提供了一种让开发人员能够在类加载加载class文件前主动介入的一种方法,具体如何操作需要开发人员了解Java虚拟机规范以及字节码的相关知识。

栈帧与指令集

栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构。它是虚拟机运行时数据区中的虚拟机栈的栈元素。栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程。

在编译程序代码的时候,栈帧中需要多大的局部变量表,多深的操作数栈都已经完全确定了。因此一个栈帧需要分配多少内存,不会受到程序运行期变量数据的影响,而仅仅取决于具体的虚拟机实现。

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内部定义的局部变量。并且在Java编译为Class文件时,就已经确定了该方法所需要分配的局部变量表的最大容量。局部变量表类似一个数组结构,虚拟机在访问局部变量表的时候会使用下标作为引用,普通方法的局部变量表中第0位索引默认是用于传递方法所属对象实例的引用this。

操作数栈(Operand Stack)和局部变量表一样,在编译时期就已经确定了该方法所需要分配的局部变量表的最大容量。当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法执行的过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈/入栈操作。例如,在做算术运算的时候是通过操作数栈来进行的,又或者在调用其它方法的时候是通过操作数栈来进行参数传递的。

动态链接(Dynamic Linking)每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支付方法调用过程中的动态连接。在类加载阶段中的解析阶段会将符号引用转为直接引用,这种转化也称为静态解析。另外的一部分将在每一次运行时期转化为直接引用,这部分称为动态连接。

返回地址:当一个方法开始执行后,只有2种方式可以退出这个方法,方法返回指令和异常退出。无论采用任何退出方式,在方法退出之后,都需要返回到方法被调用的位置,程序才能继续执行,方法返回时可能需要在栈帧中保存一些信息。一般来说,方法正常退出时,调用者的PC计数器的值可以作为返回地址,栈帧中会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中一般不会保存这部分信息。

JVM指令集并非是对Java语句的直接翻译,由于指令只使用1个字节表示,所以指令集最多只能包含256种指令。因此,一条Java语句一般会对应多条底层指令。每一条指令都有与之对应的助记符,我们可以通过官方资料查看它们对应关系:https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-6.html。为了帮助大家更加直观的理解字节码指令,我将通过三个用例分别解释。

从一个简单的加法函数开始,我们可以使用javac将.java文件编译成.class,再通过javap -c查看它的字节码文件

public int add(int x, int y) {  
    return x + y;  
}
public add(II)I
    ILOAD 1 // 将局部变量表中#1变量入栈
    ILOAD 2 // 将局部变量表中#2变量入栈
    IADD // 调用整型数相加(两个数出栈,再将结果入栈)
    IRETURN // 返回栈顶的结果
    MAXSTACK = 2 // 最大栈数2
    MAXLOCALS = 3 // 最大本地变量数3

第一行是它的函数签名,2~7行的注释分别是对指令的解释。ILOAD,IADD,IRETURN分别是整型数的入栈,加法和返回操作。大家可以将add方法修改为静态函数后重新编译,看看MAXLOCALS是否有变化。

接下来我们把函数变得复杂一些,尝试对函数的执行时间做一个计算并输出。

public int add(int x, int y) {  
  long t = System.nanoTime();  
  int ret = x + y;  
  t = System.nanoTime() - t;  
  System.out.println(t);  
  return ret;  
}
public add(II)I
    INVOKESTATIC java/lang/System.nanoTime ()J // 调用静态函数,结果long入栈
    LSTORE 3 // 将栈顶的long保存到局部变量#3
    ILOAD 1
    ILOAD 2
    IADD
    ISTORE 5 // 将栈顶的int保存到局部变量#5
    INVOKESTATIC java/lang/System.nanoTime ()J
    LLOAD 3 // 局部变量#3入栈
    LSUB // 从栈顶弹出两个long相减
    LSTORE 3 // 结果保存到变量#3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream; // 获取静态引用
    LLOAD 3 // 局部变量#3入栈
    INVOKEVIRTUAL java/io/PrintStream.println (J)V // 调用函数
    ILOAD 5 //  局部变量#5入栈
    IRETURN
    MAXSTACK = 4
    MAXLOCALS = 6

第2行结尾的J表示函数返回值是long类型。第14行结尾的V表示println函数的返回值是void。第12行到第14行的指令对应代码的System.out.println(t),特别需要注意的是INVOKEVIRTUAL指令实际上需要从操作数栈获取两个数,第一个数是在执行了GETSTATIC后入栈的对象引用。

我们再次修改函数,这一次我们引入比较和循环语句,尽管代码的逻辑不太正常,但这并不妨碍我们理解。

public int add(int x, int y) {  
   if(x > 1) {  
       return x + y;  
   }  
   for(int i = 0; i < y; i++) {  
       x ++;  
   }  
   return x - y;  
}  
public add(II)I
    ILOAD 1
    ICONST_1 // 将一个常整型数1入栈
    IF_ICMPLE L0 // 比较如果操两个操作数是小于等于的关系则成立,否则跳转到L0的位置继续
    ILOAD 1
    ILOAD 2
    IADD
    IRETURN
   L0
    ICONST_0 // 将常整型数0入栈
    ISTORE 3 // 栈顶数保存到局部变量#3
   L1
    ILOAD 3
    ILOAD 2
    IF_ICMPGE L2 // 比较栈顶的两个操作数是否是大于等于的关系,如果不成立则跳转到L2
    IINC 1 1 // 局部变量#1 自增1
    IINC 3 1 // 局部变量#3 自增1
    GOTO L1 // 跳转到L1执行
   L2
    ILOAD 1
    ILOAD 2
    ISUB
    IRETURN
    MAXSTACK = 2
    MAXLOCALS = 4

当我们使用字节码直接操作虚拟机中的底层代码的时候,基本上就是通过改变局部变量表和操作数栈来改变程序的逻辑。还记得根据Java虚拟机规范,MAXSTACK和MAXLOCALS是在.java文件被编译成.class就被确定下来的吗,如果我们要对方法做出修改势必会引入新的局部变量,这时就难免需要对MAXSTACK和MAXLOCALS做重新计算。好在目前流行的字节码框架已经可以自动帮助我们完成这项任务。

ASM框架

ASM是一个比较硬核的字节码框架,也是转换效率最高的工具。下面是常用类的介绍:

1. ClassReader
按照Java虚拟机规范(JVMS)中定义的方式来解析class文件中的内容,在遇到合适的字段时调用ClassVisitor中相对应的方法。

ClassReader(final byte[] classFile)
构造方法,通过class字节码数据加载
ClassReader(final String className) throws IOException
通过class全路径名从ClassLoader加载

2. ClassVisitor

java中类的访问者,提供一系列方法由ClassReader调用。调用的顺序如下:visit -> visitSource -> visitModule -> visitNestHost -> visitOuterClass -> visitAnnotation -> visitTypeAnnotation -> visitAttribute -> visitNestMember -> visitPermittedSubclass -> visitInnerClass -> visitRecordComponent -> visitField -> visitMethod -> visitEnd

3. ClassWriter

ClassVisitor的子类,通过它生成最后的字节码。并且它可以帮助重新计算MAXSTACK和MAXLOCALS

4. ModuleVisitor

Java中模块的访问者,作为ClassVisitor.visitModule方法的返回值

5. AnnotationVisitor

Java中注解的访问者,作为ClassVisito中visitTypeAnnotation和visitTypeAnnotation的返回值

6. FieldVisitor

Java中字段的访问者,作为ClassVisito.visitField的返回值

7. MethodVisitor

Java中方法的访问者,作为ClassVisito.visitMethod的返回值

visitMethodInsn 方法调用指令
visitVarInsn 局部变量调用指令
visitInsn(int) 访问一个零参数要求的字节码指令,如LSUB
visitLdcInsn 把一个常量放到栈顶
visitInvokeDynamicInsn 动态方法调用
visitFieldInsn 调用/访问某个字段
8. AnalyzerAdapter

MethodVisitor的子类,使用它重新计算最大操作数栈(MAXSTACK)

9. LocalVariablesSorter

MethodVisitor的子类,使用它重新计算局部变量表(MAXLOCALS)的索引

newLocal 创建局部变量
通过IDEA的Plugins安装ASM Bytecode Viewer Support Kotlin,我们可以借助这个插件来帮助我们生成大部分代码,具体用法这里就赘述了。

一个计算函数执行时间的完整用例

1.加入pom依赖

    <dependencies>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>9.2</version>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm-commons</artifactId>
            <version>9.2</version>
        </dependency>
        <!-- Oshi 监听服务器状态-->
        <dependency>
            <groupId>com.github.oshi</groupId>
            <artifactId>oshi-core</artifactId>
            <version>5.6.1</version>
        </dependency>
    </dependencies>

    <build>
        <finalName>agent</finalName>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <appendAssemblyId>false</appendAssemblyId>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                    <outputDirectory>target</outputDirectory>
                    <archive>
                        <manifestEntries>
                            <Premain-Class>DemoAgent</Premain-Class>
                            <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
                            <addDefaultSpecificationEntries>true</addDefaultSpecificationEntries>
                        </manifestEntries>
                    </archive>
                </configuration>
            </plugin>
        </plugins>
    </build>
2.DemoAgent类。
import java.lang.instrument.Instrumentation;

public class DemoAgent {
    /**
     * 该方法在main方法之前运行
     */
    public static void premain(String arg, Instrumentation instrumentation) {
        instrumentation.addTransformer(new XClassFileTransformer());
    }
}

3.XClassFileTransformer类。

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;

public class XClassFileTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer){
        try {
            ClassReader cr = new ClassReader(classfileBuffer);
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            cr.accept(new NanoTimerClassVisitor(cw), ClassReader.SKIP_DEBUG);
            byte[] cc = cw.toByteArray();
            return cc;
        } catch (Exception e) {
            return null;
        }
    }
}

transform方法返回null或者new byte[0]表示对当前字节码文件不进行修改。ClassWriter.COMPUTE_MAXS表示框架会自动计算MAXSTACK和MAXLOCALS,ClassReader.SKIP_DEBUG表示当字节码中包含调试信息的时候,会忽略不会触发回调。

4.NanoTimerClassVisitor类。


import org.objectweb.asm.*;
import org.objectweb.asm.commons.AnalyzerAdapter;
import org.objectweb.asm.commons.LocalVariablesSorter;

import java.util.Objects;

import static org.objectweb.asm.Opcodes.*;

public class NanoTimerClassVisitor extends ClassVisitor {
    private String className;

    public NanoTimerClassVisitor(ClassVisitor classVisitor) {
        super(ASM9, classVisitor);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        this.className = name;
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
        if (Objects.nonNull(mv) && !name.equals("<init>") && !name.equals("<clinit>")) {
            NanoTimerMethodVisitor methodVisitor = new NanoTimerMethodVisitor(mv, className, access, name, descriptor);
            return methodVisitor.refactor();
        }
        return mv;
    }

    class NanoTimerMethodVisitor extends MethodVisitor {
        private AnalyzerAdapter analyzerAdapter;
        private LocalVariablesSorter localVariablesSorter;
        private int timeOpcode;
        private int outOpcode;
        private String className;
        private int methodAccess;
        private String methodName;
        private String methodDescriptor;

        public NanoTimerMethodVisitor(MethodVisitor methodVisitor, String className, int methodAccess,
                                      String methodName, String methodDescriptor) {
            super(ASM9, methodVisitor);
            this.className = className;
            this.methodAccess = methodAccess;
            this.methodName = methodName;
            this.methodDescriptor = methodDescriptor;
            // 使用AnalyzerAdapter计算最大操作数栈
            analyzerAdapter = new AnalyzerAdapter(className, methodAccess, methodName, methodDescriptor, this);
            // LocalVariablesSorter重新计算局部变量的索引并自动更新字节码中的索引引用
            localVariablesSorter = new LocalVariablesSorter(methodAccess, methodDescriptor, analyzerAdapter);
        }

        public MethodVisitor refactor() {
            return localVariablesSorter;
        }

        @Override
        public void visitCode() {
            super.visitCode();
            mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
            timeOpcode = localVariablesSorter.newLocal(Type.LONG_TYPE);
            mv.visitVarInsn(LSTORE, timeOpcode);
        }

        @Override
        public void visitInsn(int opcode) {
            if ((opcode >= IRETURN && opcode <= RETURN) || opcode == ATHROW) {
                mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "nanoTime", "()J", false);
                mv.visitVarInsn(LLOAD, timeOpcode);
                mv.visitInsn(LSUB);
                mv.visitVarInsn(LSTORE, timeOpcode);

                mv.visitLdcInsn(className + "." + methodName + "(ns):");
                outOpcode = localVariablesSorter.newLocal(Type.getType(String.class));
                mv.visitVarInsn(ASTORE, outOpcode);

                mv.visitVarInsn(ALOAD, outOpcode);
                mv.visitVarInsn(LLOAD, timeOpcode);
                mv.visitInvokeDynamicInsn("makeConcatWithConstants", "(Ljava/lang/String;J)Ljava/lang/String;", new Handle(Opcodes.H_INVOKESTATIC, "java/lang/invoke/StringConcatFactory", "makeConcatWithConstants", "(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite;",false), new Object[]{"\u0001\u0001"});
                mv.visitVarInsn(ASTORE, outOpcode);

                mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
                mv.visitVarInsn(ALOAD, outOpcode);
                mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
            }
            super.visitInsn(opcode);
        }
    }
}

5.测试类TestCache。

public class TestCache {
    public static void main(String[] args) {
        System.out.println("测试类");
    }
}

6.打包。

7.运行代码。

-javaagent:D:\idea\test\untitled\target\agent.jar

8. 通过assembly插件对项目进行打包生成:untitled-1.0-SNAPSHOT-jar-with-dependencies.jar或者agent.jar

9. 运行一个目标项目,并添加虚拟机指令-javaagent,就可以看到执行效果。

如何查看生成后的代码

计算函数执行时间是一个非常简单的功能,我们很容易一次性写正确。但是如果需要代理的逻辑比较复杂,而探针程序又不像普通程序一样方便做断点调试。我们如何才能够很方便知道生成的代码是否正确呢?这里告诉大家一个诀窍。回到我们XClassFileTransformer类,增加两行代码:

public class XClassFileTransformer implements ClassFileTransformer {
    @Override
    public byte[] transform(ClassLoader loader,
                            String className,
                            Class<?> classBeingRedefined,
                            ProtectionDomain protectionDomain,
                            byte[] classfileBuffer) throws IllegalClassFormatException {
        try {
            ClassReader cr = new ClassReader(classfileBuffer);
            ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
            cr.accept(new NanoTimerClassVisitor(cw), ClassReader.SKIP_DEBUG);
            byte[] cc = cw.toByteArray();
            FileOutputStream fos = new FileOutputStream("./cc.class");
            fos.write(cc);
            return cc;
        } catch (IOException e) {

        }
        return null;
    }
}

 

第13、14行代码的功能是将生成的字节码输出到本地文件中,然后我们通过IDEA打开这个.class文件,看看新增加的代码是否如我们预期的那样。

总结:JVM代理发生在类加载器加载.class文件前,因此我们能够动态修改字节码。通过ASM这类字节码框架,使得开发人员即使对字节码指令不是很熟悉依然能够操作。当然,Java的探针技术除了和被代理的项目同时启动以外还提供了一种热部署的方案。

参考

深入学习JVM探针与字节码技术_Java架构-大仙的博客-CSDN博客_jvm 探针

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

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

相关文章

rhel配置zfs

rhel配置zfs环境声明ZFS简介ZFS块指针的结构ZFS的存储池布局rhel8/rhel9安装zfs始终在启动时加载OpenZFS模块开机自启zfs服务zfs基本操作查看磁盘创建pool和ZFS文件系统创建一个存储池设置zfs挂载点为多个用户创建起始目录向池中添加更多空间修改文件系统名称删除文件系统修改存…

数据库的存储引擎和事务

Mysql默认使用INNODB存储引擎 数据库中的各表均被&#xff08;在创建表时&#xff09;指定的存储引擎来处理。 服务器可用的引擎依赖于以下因素&#xff1a;MySql的版本、服务器在开发时如何被配置、启动选项 为了解当前服务器中有哪些存储引擎可用&#xff0c;可使用show e…

2022 年 项目中常用的工具函数

目录1. 数字千分位化&#xff0c;支持保留小数点2. 前端生成 uuid3. 传入日期换算出是周几4. 通过计算 text-indent 偏移的负值&#xff0c;实现文字居右隐藏, 右侧对齐 ...text5. 时间格式化函数6. 防抖和节流7. 前端文件流下载8. 后端返回 无格式化时间处理为格式化 20220101…

回收租赁商城系统功能拆解03讲-商品分类

回收租赁系统适用于物品回收、物品租赁、二手买卖交易等三大场景。 可以快速帮助企业搭建类似闲鱼回收/爱回收/爱租机/人人租等回收租赁商城。 回收租赁系统支持智能评估回收价格&#xff0c;后台调整最终回收价&#xff0c;用户同意回收后系统即刻放款&#xff0c;用户微信零…

【C语言进阶】指针进阶-回调函数

作者:匿名者Unit 目录一.函数指针数组1.定义2.转移表二.回调函数1.定义2.qsort的使用3.冒泡模拟实现qsort一.函数指针数组 1.定义 在之前我们已经了解过了函数指针: int(*p)(int,int)&add;我们还可以将函数的地址存放在数组&#xff0c;也就是函数指针数组 int (*p[10…

Qt音视频开发10-ffmpeg内核硬解码

一、前言 为了极大的降低CPU的占用&#xff0c;实现硬解码&#xff08;也叫硬件加速&#xff09;非常有必要&#xff0c;一个视频文件或者一路视频流还好&#xff0c;如果增加到64路视频流呢&#xff0c;如果是4K、8K这种高分辨率的视频呢&#xff0c;必须安装上硬解码才是上上…

初始Spring

初始Spring SSM框架的老大是&#xff1a;Spring大管家&#xff0c;无处不在 Spring是应用了很多优秀的设计模式,对于项目的实现,提供了优秀的解决方案;Spring是一个轻量级(低侵入) 框架.类与类之间的解耦合 IOC控制反转 实现大管家 AOP 增强&#xff0c;面向切面编程&…

QT—QPalette调色板类

Qt提供的调色板类QPalette专门用于管理部件的外观显示&#xff0c;相当于部件或对话框的调色板&#xff0c;管理他们所有的颜色信息。每个部件都包含一个QPalette对象&#xff0c;在显示时&#xff0c;按照它的QPalette对象中对各部分各状态下的颜色的描述进行绘制。示例点击左…

C语言-数据的存储-浮点数的存储(8.2)​​​​​​​

目录​​​​​​​ 思维导图&#xff1a; 浮点型在内存中的存储 1.1一个经典的例子 1.2 浮点数存储规则 1.3实践举例 写在最后&#xff1a; 思维导图&#xff1a; 浮点型在内存中的存储 1.1一个经典的例子 #include <stdio.h>int main() {int n 9;//以整形的形式…

SHELL脚本学习 --- 第七次作业(awk)

SHELL脚本学习 — 第七次作业 思路&#xff1a; 1&#xff0c;df -h获取磁盘情况&#xff0c;先用grep过滤出根分区所属行&#xff0c;然后awk打印相应的域 2&#xff0c;首先把多个空格缩成一个&#xff0c;然后用grep找到ens160所属的IP&#xff0c;在awk中使用正则匹配inet…

C++ STL 容器类和迭代器

一、STL容器类 1.1 STL介绍 容器就是盛放东西的东西&#xff0c;这里被盛放的一般是数据对象&#xff0c;用来盛放的是容器类容器类的内核就是&#xff1a;数据结构 算法STL(Standard Template Library,标准模板库)STL从广义上分为:容器(container)算法(algorithm)迭代器(it…

在泰国旅居的第5天,我定了两个新目标

点击上方 "大数据肌肉猿"关注, 星标一起成长点击下方链接&#xff0c;进入高质量学习交流群今日更新| 1052个转型案例分享-大数据交流群我在12月14号写了2023年&#xff0c;重新扬帆起航&#xff01;&#xff0c;里面说了2023年开始全球旅居办公&#xff0c;而在元旦…

历史大讲堂:这是老古董 苹果第一代Macintosh详解

还记得这个苹果吗&#xff1f;这是美国苹果公司的图标。今天我们就来借着苹果最出名的第一代个人电脑唠一唠苹果的前世今生。 这一代个人电脑 已经有鼠标了&#xff08;右下角&#xff09;&#xff0c;虽然非常的古老 macOS第一代系统就运行在这个上。这里我们也来唠一唠Macin…

Spring Boot学习笔记(十二)Spring Boot整合Quartz

一、自定义配置类 不使用springBoot的自动配置类&#xff0c;而是自定义配置类。 1、导入依赖 pom文件&#xff1a; <dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifact…

Android---Toolbar

目录 Toolbar Toolbar 加上 menu Toolbar 设置 Theme 完整 Demo Toolbar Toolbar 是在 Android 5.0 开始推出的一个 Materal Design 风格的导航控件 &#xff0c;Google 非常推荐大家使用 Toobar 来作为 Android 客户端的导航栏&#xff0c;以此来取代之前的 Actionbar。与 …

联想昭阳E4电脑U盘安装Win10系统操作教学

联想昭阳E4电脑U盘安装Win10系统操作教学分享。有用户想要将自己的联想昭阳E4电脑重装到Win10系统来使用。那么今天教大家一个U盘重装系统的方法&#xff0c;使用这个方法能够在系统出现问题的时候进行系统的重置&#xff0c;解决系统问题。一起来看看具体的重装教学吧。 准备工…

Exynos_4412——PWM实验

目录 一、PWM简介 1.1蜂鸣器工作原理 有源蜂鸣器 无源蜂鸣器 1.2使用GPIO控制 1.3PWM控制 1.4PWM参数 周期​ 占空比 二、Exynos_4412下的PWM控制器 三、PWM寄存器详解 四、PWM编程 一、PWM简介 1.1蜂鸣器工作原理 有源蜂鸣器 有源蜂鸣器只要接上额定电源就可以发…

主流的4种跨隔离网文件摆渡方式对比介绍

网络上承载了太多企业的业务&#xff0c;其安全性一定要得到保障&#xff0c;所以很多企业和机构都会选择将网络进行隔离划分&#xff0c;比如内外网隔离&#xff0c;办公网、研发网隔离等&#xff0c;也有不少企业会选择用云桌面的形式。 然而网络的建设就是为了互通的&#x…

【学vue跟玩一样】快速搞懂vue渲染

Vue的渲染分为条件渲染和列表渲染&#xff0c;那究竟什么式渲染呢?1.条件渲染1.v-if写法:(1)v-if"表达式"(2)v-else-if"表达式"(3)v-else"表达式"&#xff08;和我们曾经学过的JavaScript里面的if语句几乎一样&#xff09;适用于:切换频率较低的…

React umi中使用sass

umi默认支持less和css&#xff0c;如果想要使用sass&#xff0c;需要安装插件以及配置 一、安装umi的sass插件 yarn add umijs/plugin-sass 二、安装sass依赖 yarn add sass-loader node-sass 三、配置sass 在config/config.js或者 .umirc.ts文件中配置如下&#xff1a; sa…