ASM字节码处理工具原理及实践(一)

1. ASM简介

我们知道程序的分析。生成和转换是很有用的技术,可以用于很多场景。ASM作为一个Java字节码处理工具,它被设计用于处理已编译的Java类。ASM不是生成和转变已编译的Java类的唯一工具,但它是最新且最有效的工具之一。特点是体积小,速度快,开源。它的作用和JVM的动态性相辅相成,在许多场景下有很好的表现,例如相比于AOP的编译期织入,ASM的操作速度更快。

2. ASM导入

2.1 maven导入

<properties>
    <asm.version>9.1</asm.version>
</properties>

<dependencies>
    <dependency>
        <artifactId>asm</artifactId>
        <groupId>org.ow2.asm</groupId>
        <version>${asm.version}</version>
    </dependency>
    <dependency>
        <artifactId>asm-commons</artifactId>
        <groupId>org.ow2.asm</groupId>
        <version>${asm.version}</version>
    </dependency>
</dependencies>

2.2 gradle导入

dependencies {
    implementation 'org.ow2.asm:asm:9.1'
    implementation 'org.ow2.asm:asm-commons:9.1'
}

3. 字节码结构

3.1 Class字节码文件结构

说明长度个数
魔数识别文件格式u41
版本号主版本号(大版本)u21
副版本号(小版本)u21
常量池集合常量池计数器u21
常量池表n计数器-1
访问标识访问标识符u21
索引集合类索引u21
父类索引u21
接口计数器u21
接口索引表n接口计数器
字段表集合(Field)字段计数器u21
字段表n字段计数器
方法表集合(Method)方法计数器u21
方法表n方法计数器
属性表集合属性计数器u21
属性表n属性计数器

3.2 版本号对应关系

主版本副版本编译器版本ASM版本
4531.1V1_1
4601.2V1_2
4701.3V1_3
4801.4V1_4
4901.5V1_5
5001.6V1_6
5101.7V1_7
5201.8V1_8
5301.9V9
5401.10V10

后续的依照表规律类推,Java的版本号从45开始,JDK1.1之后的每个JDK大版本发布主版本号向上加1。

不同版本的Java编译器编译的Class文件的版本是不同的。高版本的JVM可以执行低版本编译器生成的Class文件,否则不行,并抛出 java.lang.UnsupportedClassVersionError 异常。

3.3 类型描述符

描述符的作用用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)以及返回值。对应关系如下:

Java类型类型描述符
booleanZ
charC
byteB
shortS
intI
floatF
longJ
doubleD
ObjectLjava/lang/Object;
int[][I
Object[][][[Ljava/lang/Object;

3.4 方法描述符

方法描述符是一个类型描述符的列表,它在单个字符串中描述了一个方法的参数类型和返回类型。传入参数由括号包围,紧接着为返回类型的类型描述符。例如如下几种示例;

源文件中的方法声明方法描述符
void m(int i, float f)(IF)V
int m(Object o)(Ljava/lang/Object;)I
int[] m(int i, String s)(ILjava/lang/String;)[I
Object m(int[] i)([I)Ljava/lang/Object;

3.5 访问标识符

标志名称标志值含义
ACC_PUBLIC0x0001标志为public类型
ACC_FINAL0x0010标志被声明为final,只有类可以设置
ACC_SUPER0x0020标志允许使用invokespecial字节码指令的新语义,JDK1.0.2之后编译出来的类的这个标志默认为真。(使用增强的方法调用父类方法)
ACC_INTERFACE0x0200标志这是一个接口
ACC_ABSTRACT0x0400是否为abstract类型,对于接口或者抽象类来说,次标志值为真,其他类型为假
ACC_SYNTHETIC0x1000标志此类并非由用户代码产生(即:由编译器产生的类,没有源码对应)
ACC_ANNOTATION0x2000标志这是一个注解
ACC_ENUM0x4000标志这是一个枚举

当有多个访问标识符来修饰时,采用加法结合(或者 | 或运算),比如 public final 修饰的类,其标记为 ACC_PUBLIC | ACC_FINAL,其值为 0x0011,对应的十进制为 17 。

补充

  1. 带有 ACC_INTERFACE 标志的 class 文件表示的是接口而不是类
    1. 如果一个class文件被设置了 ACC_INTERFACE 标志,同时也得设置 ACC_ABSTRACT标志。同时它不能再设置ACC_FINAL、ACC_SUPER 或ACC_ENUM标志。
    2. 如果没有设置ACC_INTERFACE标志,那么这个class文件可以具有上表中除ACC_ANNOTATION外的其他所有标志。当然,ACC_FINAL和ACC_ABSTRACT这类互斥的标志除外。这两个标志不得同时设置。
  2. ACC_SUPER标志用于确定类或接口里面的invokespecial指令使用的是哪一种执行语义,Java虚拟机默认为每个class文件都设置了 ACC_SUPER 标志
  3. ACC_SYNTHETIC标志意味着该类或接口是由编译器生成的,而不是由源代码生成的。
  4. 注解类型必须设置ACC_ANNOTATION标志。如果设置了ACC_ANNOTATION标志,那么也必须设置ACC_INTERFACE标志。
  5. ACC_ENUM标志表明该类或其父类为枚举类型。

4. ASM 解析类信息

4.1 ASM组件

ASM提供了基于 ClassVisitor API的三个基本组件用来生成/转换class:

  • ClassReader: 用于解析class文件,通过 accept 方法可以开始解析,并调用传入的 ClassVisitor 的 visitXxx 方法回调解析结果。ClassReader可以认为是解析事件的生产者。
  • ClassWriter: 这是 ClassVisitor 的一个实现类。它可以生成class字节码文件的byte数组,生成的byte数组可以通过 toByteArray 方法获取。ClassWriter可以认为是解析事件的消费者
  • ClassVisitor: 它所接收到的方法调用需要委托给另一个 ClassVisitor 实例。ClassVisitor可以看做是解析事件的过滤器。

4.2 解析一个类 - ClassReader + ClassVisitor

根据4.1 ASM 组件的介绍,我们知道:ClassReader作为解析事件的发生者,用于读取类信息。ClassVisitor作为解析事件的访问者/过滤器,可以接收来自ClassReader发来的回调,并将这个回调转发给其他ClassVisitor实例,访问到当前ClassReader读取到的类信息。ClassVisitor是一个抽象类,提供了各种访问信息的回调接口,列出最基本的几个内容:

public abstract class ClassVisitor {
    public ClassVisitor(int api);
    public ClassVisitor(int api, ClassVisitor cv);
    public void visit(int version, int access, String name,
                      String signature, String superName, String[] interfaces);
    public void visitSource(String source, String debug);
    public void visitOuterClass(String owner, String name, String desc);
    AnnotationVisitor visitAnnotation(String desc, boolean visible);
    public void visitAttribute(Attribute attr);
    public void visitInnerClass(String name, String outerName,
                                String innerName, int access);
    public FieldVisitor visitField(int access, String name, String desc,
                                   String signature, Object value);
    public MethodVisitor visitMethod(int access, String name, String desc,
                                     String signature, String[] exceptions);
    void visitEnd();
}

实现一个ClassVisitor的子类ClassPrinter来打印读取到的类信息,这个 ClassPrinter 并不做任何过滤操作,仅将所有回调内容都打印出来:

public class ClassPrinter extends ClassVisitor {
    public ClassPrinter() {
        super(Opcodes.ASM4);
    }

    @Override
    public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
        System.out.println("----visit");
        System.out.println(version+" "+access+" "+name+" "+signature+" "+superName+" "+ Arrays.toString(interfaces));
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public void visitSource(String source, String debug) {

        System.out.println("----visitSource");
        System.out.println(source+" "+debug);
        super.visitSource(source, debug);
    }

    @Override
    public void visitOuterClass(String owner, String name, String descriptor) {
        System.out.println("----visitOuterClass");
        System.out.println(owner+" "+name+" "+descriptor);
        super.visitOuterClass(owner, name, descriptor);

    }

    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        System.out.println("----visitAnnotations");
        System.out.println(descriptor+" "+visible);
        return super.visitAnnotation(descriptor, visible);
    }

    @Override
    public void visitAttribute(Attribute attribute) {
        System.out.println("----visitAttribute");
        System.out.println(attribute);
        super.visitAttribute(attribute);
    }

    @Override
    public void visitInnerClass(String name, String outerName, String innerName, int access) {
        System.out.println("----visitInnerClass");
        System.out.println(name+" "+outerName+" "+innerName+" "+access);
        super.visitInnerClass(name, outerName, innerName, access);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        System.out.println("----visitField");
        System.out.println(access+" "+name+" "+descriptor+" "+signature+" "+value);
        return super.visitField(access, name, descriptor, signature, value);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        System.out.println("----visitMethod");
        System.out.println(access+" "+name+" "+descriptor+" "+signature+" "+ Arrays.toString(exceptions));
        return super.visitMethod(access, name, descriptor, signature, exceptions);
    }

    @Override
    public void visitEnd() {
        System.out.println("----visitEnd");
        super.visitEnd();
    }
}

我们定义一个很简单的类:

@MyAnnotation("value")
public class ASMTest {
    public int num;

    public final int getNum() {
        return num;
    }

    private void setNum(int num) {
        this.num = num;
    }
}

结合ClassReader和ClassPrinter来解析它:

//ClassVisitor的实现类,打印所有visitXxx的回调参数
ClassPrinter cp = new ClassPrinter();
try {
    //ClassReader是类信息的读取者,解析事件的产生者
    ClassReader cr = new ClassReader("asmcore.base.ASMTest");
    //通过accept方法开始解析,并在解析到对应元素时回调传入的cp的visitXxx方法
    cr.accept(cp,0);
} catch (IOException e) {
    e.printStackTrace();
}

得到解析的结果:

----visit
52 33 asmcore/base/ASMTest null java/lang/Object []
----visitSource
ASMTest.java null
----visitAnnotations
Lasmcore/base/MyAnnotation; true
----visitField
1 num I null null
----visitMethod
1 <init> ()V null null
----visitMethod
17 getNum ()I null null
----visitMethod
2 setNum (I)V null null
----visitEnd

Process finished with exit code 0

可以看到,class文件的所有关键信息都被打印了出来。需要我们注意的是 visitXxx 方法的回调关系(先后回调顺序为表格从上到下):

方法名调用次数
visit1
visitSource最多一次
visitOuterClass最多一次
(visitAnnotation | visitAttribute)0次或更多
(visitInnerClass | visitField | visitMethod)0次或更多
visitEnd1

其中(A|B)形式表示A和B的出现顺序不是保证有先后的。但表格中从上到下整体的顺序是严格的回调先后的顺序。

4.3 生成一个类 - ClassWriter

我们知道 ClassWriter 继承自 ClassVisitor,它的任务是将访问回调的结果保存到byte数组中,直到 visitEnd 方法被回调表示解析/访问事件结束,也就意味着一个Class文件对应的byte数组数据也就保存完成,可以通过 ClassWriter 的 toByteArray 方法获取到保存的byte数组。

我们假装自己是一个 ClassReader,严格根据 4.2 总结的回调顺序回调 ClassWriter 的 visitXxx 顺序,由此来让 ClassWriter 去构建一个类:

ClassWriter cw = new ClassWriter(0);
//先模拟类访问开始
//package pkg;
//public interface Comparable extends Measure
cw.visit(V1_8,
         ACC_PUBLIC|ACC_ABSTRACT|ACC_INTERFACE,
         "Comparable",
         null,
         "java/lang/Object",
         new String[]{"Measurable"});
//模拟访问回调字段信息
//int TAG = 0; //接口中的字段描述符默认为 public final static
cw.visitField(ACC_PUBLIC|ACC_FINAL|ACC_STATIC,
              "TAG","I",null,0)
    .visitEnd();
//模拟访问回调方法信息
//int compareTo(Object o); //接口中方法描述符默认为 public abstract
cw.visitMethod(ACC_PUBLIC|ACC_ABSTRACT,
               "compareTo","(Ljava/lang/Object;)I",null,null)
    .visitEnd();
//模拟类访问结束
cw.visitEnd();
//这个byte[]就是class文件的字节流了
byte[] b = cw.toByteArray();

最后得到的字节码反编译后结果为:

public interface Comparable extends Measurable {
    int TAG = 0;

    int compareTo(Object var1);
}

需要注意的是 visitMethod 和 visitField 之后需要跟上 visitEnd,这两个方法的返回值是 MethodVisitor 和 FieldVisitor 实例,他们事件访问结束的标识为 visitEnd 的回调。

4.4 使用生成的class文件字节流 - ClassLoader

我们回顾到类加载机制,JVM通过类加载器,将class文件加载入JVM,class文件的来源可以有很多,本地或者网络,只要是符合规范的class文件,就可以通过字节流的方式进行读取,ClassLoader的 defineClass 方法将byte[]加载为Class对象。

public class MyClassLoader extends ClassLoader {
    public MyClassLoader(ClassLoader parent){
        super(parent);
    }
    
    public Class<?> generateClass(String name,byte[] b){
        return defineClass(name,b,0,b.length);
    }
}

需要注意这里的类加载器需要传入父类加载器,如果要加载的class文件中使用了项目中其他自定义类,则要通过双亲委派机制让父类加载器去完成加载。

4.5 删除、添加类成员 - ClassReader+ClassVisitor+ClassWriter

根据前面的介绍,我们知道 ClassVisitor 可以作为一个事件的过滤器,这句话的意思可以理解为,ClassVisitor 访问事件的回调可以进行分发!它有点像装饰器的代码增强,给出下面的代码或许可以更好的描述我的意思:

public class ClassVisitorFilter extends ClassVisitor{
    //父类的 ClassVisitor cv 可以理解为装饰器的被装饰者
    public ClassVisitorFilter(ClassVisitor downstream){
        super(V1_8,downstream);
    }
    
    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value){
        //before
        pre();
        //转发事件到被装饰者的该方法
        FiledVisitor fv = super.visitField(access,name,descriptor,signature,value);
        //不对被装饰者/下游分发该方法,将上述代码注释掉,如下:
        //FiledVisitor fv = super.visitField(access,name,descriptor,signature,value);
        //after
        after();
        return fv;
    }
}

我们给一个类结构如下:

package asmcore.base;

public class Student {
    public String name;
    
    public String getName(){
        return name;
    }
}

我们现在要增加一个 int 类型的字段 age,并删除 name 字段和 getName 方法。我们首先通过 ClassReader 发起一个解析事件,并将回调交给事件过滤器 ClassVisitor,我们这里把它命名为 XxxAdapter,通过责任链模式的过滤处理后,最后将事件转发给最后的事件处理器 ClassWriter。流程图如下:

请添加图片描述

针对字段增加和删除的过滤处理,我们写一个 ClassVisitor 的实现类 FieldAdapter:

public class FieldAdapter extends ClassVisitor {

    private int fAcc;
    private String fName;
    private String fDesc;
    private boolean isFieldPresent;
    private boolean isAdd;

    public FieldAdapter(ClassVisitor cv, int fAcc, String fName, String fDesc,boolean isAdd) {
        super(ASM4,cv);
        this.fAcc = fAcc;
        this.fName= fName;
        this.fDesc = fDesc;
        this.isAdd = isAdd;
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
        //-------代码增强---------
        if (!isAdd && name.equals(fName)){
            //如果不是添加,就将这个field删除
            return null;
        }
        //如果是添加field,就先判断这个field是否出现过,如果没有出现过,最后可以在visitEnd的时候添加
        if (name.equals(fName)){
            isFieldPresent = true;
        }
        //-------代码增强---------
        //回调给下一层
        return cv.visitField(access, name, descriptor, signature, value);
    }

    @Override
    public void visitEnd() {
        if (!isFieldPresent && isAdd){
            FieldVisitor fv = cv.visitField(fAcc, fName, fDesc, null, null);
            if (fv != null){
                fv.visitEnd();
            }
        }
        super.visitEnd();
    }
}

需要注意的是我们要避免重名的字段出现,所以要在扫描过所有字段过后,确保没有该字段出现,才往其中添加目标字段。visitEnd 方法的回调可以确保之前 visitField 已经完全结束,此时再转发给下游 visitField() 模拟真的访问到了要添加的目标字段,从而达到添加字段的效果。

类似的思路,我们可以定义一个删除方法的事件过滤器,其实就是不向下游转发 visitMethod 即可,就达成了模拟没有访问到该方法的效果。

public class RemoveMethodAdapter extends ClassVisitor {
    private String mName;
    private String mDesc;

    public RemoveMethodAdapter(ClassVisitor cv,String name,String desc) {
        super(ASM4,cv);
        this.mName = name;
        this.mDesc = desc;
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
        //--------代码增强--------
        if (name.equals(mName) && descriptor.equals(mDesc)){
            //如果是要删除的方法,则不回调 MethodVisitor
            return null;
        }
        //--------代码增强--------
        return cv.visitMethod(access, name, descriptor, signature, exceptions);
    }
}

这是装饰器模式,配合责任链,我们可以实现过滤处理:

ClassWriter cw = new ClassWriter(0);
//添加一个 age 字段
FieldAdapter addFieldAdapter = new FieldAdapter(cw, ACC_PUBLIC, "age", "I",true);
//删除一个 name 字段
FieldAdapter removeFieldAdapter = new FieldAdapter(addFieldAdapter,ACC_PUBLIC,"name","Ljava/lang/String;",false);
//删除一个 getName 方法
RemoveMethodAdapter removeMethodAdapter = new RemoveMethodAdapter(removeFieldAdapter, "getName", "()Ljava/lang/String;");
try {
    ClassReader cr = new ClassReader("asmcore.base.Student");
    //装饰器责任链
    cr.accept(removeMethodAdapter,0);
    byte[] bytes = cw.toByteArray();
    save(bytes,"Student");
} catch (IOException e) {
    e.printStackTrace();
}

最后我们得到处理后的Student类字节码文件:

package asmcore.base;

public class Student {
    public int age;

    public Student() {
    }
}

可以发现成功地把 name 字段和 getName 方法删除,并添加了 int 类型的 age 字段。

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

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

相关文章

wsl的图像化实现,在wsl中启动浏览器

最近在学习wsl&#xff0c;原本我看以前的教程说wsl和vmware的区别有一点就是&#xff0c;wsl只能使用命令行&#xff0c;而vmware可以实现图像化&#xff0c;结果我在 microsoft 官方发现现在的wsl 2已经实现了 GUI 界面&#xff0c;所以就来记录一下吧。 wsl 的 GUI 实现 首…

web后端-请求响应

概述 我们之前在Spring写的 Java类&#xff0c;因为没有继承任何的接口 所以tomcat其实是不识别的&#xff0c;也不能直接运行 但是tomcat识别JavaEE的一项规范-Servlet,因为tomcat就相当于一个Servlet容器 SpringBoot底层提供了一个DisPatcherServlet类(实现了servlet接口)…

【linux】 安装 java 环境

目录 1.检查linux 下是否安装java(jdk)环境2.查看 linux 的操作系统版本3.下载jdk4.新建java文件夹用于安装jdk5.将下载到本地的jdk压缩包上传到linux服务器6.配置环境变量 1.检查linux 下是否安装java(jdk)环境 可通过下面五条命令来查看linux 系统是否安装了java 环境 1、jav…

建筑行业为什么要数字化转型?

建筑行业为什么要数字化转型&#xff1f; 建筑行业作为国民经济的重要支柱产业之一&#xff0c;其生产过程繁琐、复杂&#xff0c;且产业链条长&#xff0c;涉及众多领域。 然而&#xff0c;目前传统的建筑模式已经无法适应行业的发展需求&#xff0c;建筑行业的数字化转型已…

java SimpleDateFormat和Calendar日期类

目录 一、SimpleDateFormat使用二、Calendar使用 一、SimpleDateFormat使用 使用Date直接输出日期时&#xff0c;是使用系统默认的格式输出&#xff0c;所以需要使用SimpleDateFormat来格式化日期。 那么SimpleDateFormat类怎么使用呢&#xff0c;我们需要先了解此类的格式化符…

8.Java面向对象---类、属性和方法

Java面向对象—类、属性和方法 在生活中&#xff0c;说到类&#xff0c;可以联想到类别&#xff0c;同类&#xff0c;会想到一类人&#xff0c;一类事物等等。而这一类人或事物都是具有相同特征或特点和行为的&#xff0c;我们根据不同的特征或特点和行为将他们归类或分类。同…

Java多线程

一&#xff1a;进程与线程 概述&#xff1a;几乎任何的操作系统都支持运行多个任务&#xff0c;通常一个任务就是一个程序&#xff0c;而一个程序就是一个进程。当一个进程运行时&#xff0c;内部可能包括多个顺序执行流&#xff0c;每个顺序执行流就是一个线程。 进程&#x…

Faster-RCNN代码解读6:主要文件解读-中

Faster-RCNN代码解读6&#xff1a;主要文件解读-中 前言 ​ 因为最近打算尝试一下Faster-RCNN的复现&#xff0c;不要多想&#xff0c;我还没有厉害到可以一个人复现所有代码。所以&#xff0c;是参考别人的代码&#xff0c;进行自己的解读。 ​ 代码来自于B站的UP主&#xff…

【Linux系统】理解Linux中进程间通信

Linux进程间通信 1 进程间通信的介绍1.1为什么要有进程间通信1.2 为什么能进程间通信 2 进程间通信的框架2.1 进程间通信机制的结构2.2 进程间通信机制的类型2.2.1 共享内存式2.2.2 消息传递式 2.3 进程间通信的接口设计 3 进程间通信机制简介4 详细讲解进程间通信部分机制&…

文献管理软件Endnote、Mendeley、Zotero比较及选择,Zotero基础使用技巧

引言 大家好&#xff0c;我是比特桃。日常开发的项目分为两种&#xff0c;一种是成熟化的工程项目&#xff0c;只需要与具体的业务紧密结合及应用&#xff0c;难点也比较偏向于软件工程或者互联网高并发的方向。这种项目我们通常不会选择去查文献去寻找问题的解决办法&#xf…

Altium Designer 20 导出 Gerber 制造文件流程及注意事项

一、导出 Gerber 流程 设置原点&#xff1a;在Edit菜单中选择Origin&#xff0d;Set &#xff08;快捷键E-O-S&#xff09;定好原点&#xff0c;一般放在左下角附近即可。 放置分孔图表&#xff1a;在Place菜单中选择String放置“.Legend”&#xff08;快捷键P-S&#xff09;…

【java】maven引用外部 jar 包,以RXTXcomm.jar为例

目录 1.作为资源文件夹内的资源引用2.将jar包手动安装到maven仓库 工具&#xff1a;IntelliJ IDEA 2020.2.3 x64 1.作为资源文件夹内的资源引用 1.在项目根路径新建文件夹lib, 将资源文件复制到该文件夹。 2.将文件夹设置为资源文件夹&#xff1a;选中文件夹lib右键 -> Mak…

Activity启动模式

Activity的启动模式 首先activity启动之后是放在任务栈中的&#xff0c;task stack&#xff0c;既然是栈&#xff0c;遵循先进后出原则。有玩家比喻oncreate是入栈&#xff0c;onDestroy是出栈。 同一个APP中&#xff0c;不同的activity可以设置为不同的启动模式。在manifest…

【Kafka】SASL认证的Kafka客户端代码示例(spring-kafka和原生客户端)

文章目录 spring-kafka原生客户端Tips spring-kafka 添加依赖&#xff1a; <dependency><groupId>org.springframework.kafka</groupId><artifactId>spring-kafka</artifactId><version>2.6.3</version></dependency>添加spr…

【springcloud 微服务】Spring Cloud Alibaba Nacos使用详解

目录 一、前言 二、nacos介绍 2.1 什么是 Nacos 2.2 nacos 核心能力 2.2.1 服务发现和服务健康监测 2.2.2 动态配置服务 2.2.3 动态 DNS 服务 2.2.4 服务及其元数据管理 2.2.5 nacos生态地图 2.3 与其他配置中心对比 三、nacos快速部署 3.1 获取安装包 3.2 修改脚…

电阻器的原理、类型、参数以及生活中常见的应用

电阻器是电子电路中最基本的元件之一&#xff0c;它的作用是限制电流流过的大小&#xff0c;在电子电路中广泛应用于电流控制、电压分压、信号衰减等方面。在本文中&#xff0c;我们将详细介绍电阻器的原理、类型、参数以及生活中常见的应用。 一、电阻器的原理 电阻器是一种…

go语言切片做函数参数传递+append()函数扩容

go语言切片函数参数传递append()函数扩容 给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 二叉树递归go代码&#xff1a; var ans [][]int func pathSum(root *TreeNode, targetSum int) ( [][…

ActiveMQ使用(四):在JavaScript中发送的MQTT消息在SpringBoot中变为字节数组

ActiveMQ使用(四):在JavaScript中发送的MQTT消息在SpringBoot中变为字节数组 1. 问题描述 JmsListener(destination "test_producer", containerFactory "topicListenerContainer")public void receiveTestProducer(String message) throws JMSExceptio…

spring bean

图灵课堂学习笔记 1. BeanFactory与ApplicationContext的关系 p56 ApplicationContext在BeanFactory基础上对功能进行了扩展&#xff0c;例如&#xff1a;监听功能、国际化功能等。BeanFactory的API更偏向底层&#xff0c;ApplicationContext的API大多数是对这些底层API的封…

JVM OOM问题排查与解决思路

OOM原因 1. 堆溢出 报错信息&#xff1a; java.lang.OutOfMemoryError: Java heap space 代码中可能存在大对象分配&#xff0c;无法获得足够的内存分配 可能发生内存泄露&#xff0c;导致内存被无效占用以至于耗尽 2. 永久代/元空间溢出 报错信息&#xff1a; java.lang.O…