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

news2025/1/11 17:59:57

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 字段。

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

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

相关文章

一个实现跳转到更多页面的黏性交互的通用组件

本文字数&#xff1a;3344字预计阅读时间&#xff1a;9分钟背景和现状随着移动互联网的快速发展&#xff0c;通信费用大幅降低&#xff0c;信息爆炸&#xff0c;应用软件展示的信息越来越来&#xff0c;为了有效地组织和展示信息&#xff0c;各大移动平台都提供了列表滚动组件方…

No.038<软考>《(高项)备考大全》【第22章】信息安全管理

【第22章】信息安全管理1 考试相关2 信息安全管理2.1 安全策略2.2 信息系统安全等级保护2.3 安全的概念适度安全的观点&#xff1a;木桶效应的观点&#xff1a;2.4 安全策略设计2.5 信息安全系统工程能力成熟度模型ISSE-CMM2.6数字证书护照和签证2.7访问控制授权方案2.8 安全审…

评估数据质量的指标总结1

评估数据质量的指标总结1 1、RMSE&#xff08;root mean square error&#xff09;均方根误差 作用&#xff1a;RMSE是估计的度量值与“真实”值之间的距离的度量。 计算方法&#xff1a; 2、相关系数r(coefficient of correlation ) 作用&#xff1a;皮尔逊相关系数&#xff…

LeetCode算法小抄--二叉树的各种构造

LeetCode算法小抄--各种情况的构造二叉树构造二叉树构造最大二叉树[654. 最大二叉树](https://leetcode.cn/problems/maximum-binary-tree/)从前序与中序遍历构造二叉树[105. 从前序与中序遍历序列构造二叉树](https://leetcode.cn/problems/construct-binary-tree-from-preord…

Unity和UE有啥区别?哪个更适合游戏开发

游戏制作软件中最著名的两个游戏引擎是 Unity 和 Unreal Engine。从独立游戏到大型工作室&#xff0c;许多游戏开发商都在使用它们。如果你打算从事游戏行业工作&#xff0c;你肯定曾经问过自己“我的游戏应该使用 Unity 还是 Unreal Engine&#xff1f;” ” 让我们来了解和比…

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

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

AI绘画兴起,Stable Diffusion脱颖而出,来一探究竟

近几年&#xff0c;AI图像生成风靡全球&#xff0c;它能够根据文字描述生成精美图像&#xff0c;这极大地改变了人们的图像创作方式。众多专业人士说该技术正在引领着新一轮深度学习创意工具浪潮&#xff0c;并有望彻底改变视觉媒体的创作。 AI绘画兴起 Stable Diffusion脱颖…

[Django] 后台管理系统

浏览之前&#xff0c;请先阅读以下文章 1.Django项目创建 2.Django路由系统 在项目目录下的urls.py文件中&#xff0c;我们会看到这样一个url的配置 启动服务&#xff0c;在浏览器中输入网址http://127.0.0.1:8000/admin/&#xff0c;结果如下 Django提供了一个非常强大的管…

前端解析Excel中的数据进行操作

技术要点&#xff1a;Vue、Element、JSON 功能描述&#xff1a;读取Excel中的数据&#xff0c;利用JavaScript技术奖数据转成Json格式进行操作&#xff01; 功能描述&#xff1a;只能用前端操作数据&#xff0c;并未实现将数据传送至后端处理&#xff01; 注意注意注意 如果…

Dapper——分布式跟踪系统

分布式跟踪系统 背景 当代的互联网的服务&#xff0c;通常都是用复杂的、大规模分布式集群来实现的。互联网应用构建在不同的软件模块集上&#xff0c;这些软件模块&#xff0c;有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器&#xff0…

常用加密算法

目录 常见的加密算法可以分成三种&#xff1a; 对称加密算法 DES 3DES AES 非对称加密 RSA ECC Hash算法 MD5 SHA1 算法对比 算法选择 常见的加密算法可以分成三种&#xff1a; 对称加密算法&#xff1b;非对称加密算法&#xff1b;Hash算法&#xff1b;接下来我们…

论文 : Multi-Kernel Broad Learning systems Based on Random Features

Multi-Kernel Broad Learning systems Based on Random Features:A Novel Expansion for Nonlinear Feature Nodes 基于核方法的强大性能&#xff0c;本文提出了一种基于多核的BLS系统扩展方法。首先&#xff0c;将多核形式的非线性特征映射合并为广义学习系统的特征节点; 然后…

液晶显示器输入信号接口(一) —— VGA

VGA1. 简介2. VGA接口3. VGA线4. 技术原理4.1 信号传输4.2 模拟信号的扫描方式4.3 参数本文主要介绍VGA&#xff0c;其它输入输出信号接口介绍可从以下链接跳转&#xff1a; 液晶显示器输入信号接口(二) —— DVI 液晶显示器输入信号接口(三) —— HDMI 液晶显示器输入信号接口…

Mysql高级 学习笔记分享

索引&#xff1a;Index是帮助Mysql高效获取数据的数据结构 索引是一种数据结构 ---排好序的快速查找数据结构 、 某种满足特定查找算法的数据结构、以某种方式指向数据 两大功能&#xff1a;查找快、排好序 目的在于提高查询效率&#xff0c;类比字典 如果没有索引&a…

【Vue】学习笔记-列表渲染/数据监视

列表渲染/数据监视基本列表Key的作用与原理列表过滤列表排序Vue 数据监视原理基本列表 v-for指令 用于展示列表数据语法&#xff1a;v-for“(item,index) in xxx” :key“yyy”可遍历&#xff1a;数组&#xff0c;对象&#xff0c;字符串&#xff08;用的很少&#xff09;&am…

中国农业大学821数据结构经验贴

中国农业大学821经验贴目录个人情况说明初试复习经验数学二英语二政治数据结构复试复习经验目录 个人情况说明 初试成绩 复试成绩 复试成绩在平均水平&#xff0c;综合成绩第五 本科经历 河南双非GPA&#xff1a;3.79&#xff0c;专业排名第6竞赛经历&#xff1a;CCPC省银&…

Matlab进阶绘图第16期—三维填充折线图

三维填充折线图是在三维折线图的基础上&#xff0c;对其与XOY平面之间的部分进行颜色填充&#xff0c;从而能够更好地刻画细节变化。 由于Matlab中未收录三维填充折线图的绘制函数&#xff0c;因此需要大家自行设法解决。 本文使用自制的FilledPlot3小工具进行三维填充折线图…

计算机网络第一章(概述)【湖科大教书匠】

1. 各种网络 网络(Network)由若干**结点(Node)和连接这些结点的链路(Link)**组成多个网络还可以通过路由器互连起来&#xff0c;这样就构成了一个覆盖范围更大的网络&#xff0c;即互联网(互连网)。因此&#xff0c;互联网是"网络的网络(Network of Networks)"**因特…

扩散模型的Prompt指南:如何编写一个明确提示

Prompt&#xff08;提示&#xff09;是扩散模型生成图像的内容来源&#xff0c;构建好的提示是每一个Stable Diffusion用户需要解决的第一步。本文总结所有关于提示的内容&#xff0c;这样可以让你生成更准确&#xff0c;更好的图像 一个好的提示 首先我们看看什么是好的提示…

MCM箱模型建模方法及大气O3来源解析

详情点击链接&#xff1a;MCM箱模型建模方法及大气O3来源解析一、大气中O3形成、MCM和Atchem 2原理及Linux系统安装1.大气中O3形成的原理 2、MCM原理及基本流程3、Atchem 2 下载安装4、Linux系统安装5、Atchem 2 运行需要的其他工具A、Fortran&#xff1b;B、Python&#xff1…