JVM第三讲:JVM 基础-字节码的增强技术详解

JVM 基础-字节码的增强技术详解

本文是JVM第三讲,JVM 基础-字节码的增强技术。在上文中,着重介绍了字节码的结构,这为我们了解字节码增强技术的实现打下了基础。字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。接下来,我们将从最直接操纵字节码的实现方式开始深入进行剖析。

文章目录

  • JVM 基础-字节码的增强技术详解
    • 1、字节码增强技术有哪些?
      • 1.1、ASM
        • 1、ASM API
          • 核心API
          • 树形API
        • 2、直接利用ASM实现AOP
        • 3、ASM工具
      • 1.2、 Javassist
    • 2、运行时类的重载
      • 2.1、问题引出
      • 2.2、Instrument ☆
      • 2.3、JVMTI & Agent & Attach API
      • 2.4、使用场景
    • 3、总结
    • 4、参考文献
    • 5、作者简介

1、字节码增强技术有哪些?

在上文中,着重介绍了字节码的结构,这为我们了解字节码增强技术的实现打下了基础。字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。接下来,我们将从最直接操纵字节码的实现方式开始深入进行剖析

在这里插入图片描述

1.1、ASM

对于需要手动操纵字节码的需求,可以使用ASM,它可以直接生产 .class字节码文件,也可以在类被加载入JVM之前动态修改类行为(如下图所示)。ASM的应用场景有AOP(Cglib就是基于ASM)、热部署、修改其他jar包中的类等。当然,涉及到如此底层的步骤,实现起来也比较麻烦。接下来,本文将介绍ASM的两种API,并用ASM来实现一个比较粗糙的AOP。但在此之前,为了让大家更快地理解ASM的处理流程,强烈建议读者先对访问者模式进行了解。简单来说,访问者模式主要用于修改或操作一些数据结构比较稳定的数据,而通过第一章,我们知道字节码文件的结构是由JVM固定的,所以很适合利用访问者模式对字节码文件进行修改。

  • 访问者模式可以参考这篇文章:JAVA设计模式第四讲:行为型设计模式 第7节
    在这里插入图片描述
1、ASM API
核心API

ASM Core API 可以类比解析XML文件中的SAX方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用Core API。在Core API中有以下几个关键类:

  • ClassReader:用于读取已经编译好的.class文件。
  • ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。
  • 各种Visitor类:如上所述,CoreAPI根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的Visitor,比如用于访问方法的 MethodVisitor、用于访问类变量的FieldVisitor、用于访问注解的 AnnotationVisitor 等。为了实现AOP,重点要使用的是MethodVisitor
树形API

ASM Tree API可以类比解析XML文件中的DOM方式,把整个类的结构读取到内存中,缺点是消耗内存多,但是编程比较简单。TreeApi不同于CoreAPI,TreeAPI通过各种Node类来映射字节码的各个区域,类比DOM节点,就可以很好地理解这种编程方式。

2、直接利用ASM实现AOP

利用ASM的CoreAPI来增强类。这里不纠结于AOP的专业名词如切片、通知,只实现在方法调用前、后增加逻辑,通俗易懂且方便理解。首先定义需要被增强的Base类:其中只包含一个process()方法,方法内输出一行“process”。增强后,我们期望的是,方法执行前输出“start”,之后输出”end”。

public class Base {public void process(){System.out.println("process");}
}

为了利用ASM实现AOP,需要定义两个类:一个是MyClassVisitor类,用于对字节码的visit以及修改;另一个是Generator类,在这个类中定义 ClassReader 和 ClassWriter,其中的逻辑是,classReader读取字节码,然后交给MyClassVisitor类处理,处理完成后由ClassWriter 写字节码并将旧的字节码替换掉。Generator类较简单,我们先看一下它的实现,如下所示,然后重点解释 MyClassVisitor 类。

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;public class Generator {public static void main(String[] args) throws Exception {//读取ClassReader classReader = new ClassReader("meituan/bytecode/asm/Base");ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);//处理ClassVisitor classVisitor = new MyClassVisitor(classWriter);classReader.accept(classVisitor, ClassReader.SKIP_DEBUG);byte[] data = classWriter.toByteArray();//输出File f = new File("operation-server/target/classes/meituan/bytecode/asm/Base.class");FileOutputStream fout = new FileOutputStream(f);fout.write(data);fout.close();System.out.println("now generator cc success!!!!!");}
}

MyClassVisitor 继承自 ClassVisitor,用于对字节码的观察。它还包含一个内部类MyMethodVisitor,继承自MethodVisitor用于对类内方法的观察,它的整体代码如下:

import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;// 增强类
public class MyClassVisitor extends ClassVisitor implements Opcodes {public MyClassVisitor(ClassVisitor cv) {super(ASM5, cv);}@Overridepublic void visit(int version, int access, String name, String signature,String superName, String[] interfaces) {cv.visit(version, access, name, signature, superName, interfaces);}@Overridepublic MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {MethodVisitor mv = cv.visitMethod(access, name, desc, signature,exceptions);// Base类中有两个方法:无参构造以及process方法,这里不增强构造方法if (!name.equals("<init>") && mv != null) {mv = new MyMethodVisitor(mv);}return mv;}// 增强方法class MyMethodVisitor extends MethodVisitor implements Opcodes {public MyMethodVisitor(MethodVisitor mv) {super(Opcodes.ASM5, mv);}@Overridepublic void visitCode() {super.visitCode();mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("start");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);}// 访问到无参数指令@Overridepublic void visitInsn(int opcode) {if ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN)|| opcode == Opcodes.ATHROW) {// 方法在返回之前,打印"end"mv.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");mv.visitLdcInsn("end");mv.visitMethodInsn(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);}mv.visitInsn(opcode);}}
}

利用这个类就可以实现对字节码的修改。详细解读其中的代码,对字节码做修改的步骤是:

  • 首先通过MyClassVisitor类中的 visitMethod方法,判断当前字节码读到哪一个方法了。跳过构造方法 <init>后,将需要被增强的方法交给内部类 MyMethodVisitor 来进行处理。
  • 接下来,进入内部类 MyMethodVisitor 中的 visitCode方法,它会在ASM开始访问某一个方法的Code区时被调用,重写visitCode方法,将AOP中的前置逻辑就放在这里。 MyMethodVisitor 继续读取字节码指令,每当ASM访问到无参数指令时,都会调用MyMethodVisitor 中的 visitInsn 方法。我们判断了当前指令是否为无参数的“return”指令,如果是就在它的前面添加一些指令,也就是将AOP的后置逻辑放在该方法中
  • 综上,重写 MyMethodVisitor 中的两个方法,就可以实现AOP了,而重写方法时就需要用ASM的写法,手动写入或者修改字节码。通过调用methodVisitor的visitXXXXInsn()方法就可以实现字节码的插入,XXXX对应相应的操作码助记符类型,比如mv.visitLdcInsn(“end”)对应的操作码就是ldc “end”,即将字符串“end”压入栈。 完成这两个visitor类后,运行Generator中的main方法完成对Base类的字节码增强,增强后的结果可以在编译后的target文件夹中找到Base.class 文件进行查看,可以看到反编译后的代码已经改变了。然后写一个测试类MyTest,在其中new Base(),并调用 base.process() 方法,可以看到下图右侧所示的AOP实现效果:
    在这里插入图片描述
3、ASM工具

利用ASM手写字节码时,需要利用一系列visitXXXXInsn()方法来写对应的助记符,所以需要先将每一行源代码转化为一个个的助记符,然后通过ASM的语法转换为visitXXXXInsn()这种写法。第一步将源码转化为助记符就已经够麻烦了,不熟悉字节码操作集合的话,需要我们将代码编译后再反编译,才能得到源代码对应的助记符。第二步利用ASM写字节码时,如何传参也很令人头疼。ASM社区也知道这两个问题,所以提供了工具ASM ByteCode Outline。

安装后,右键选择“Show Bytecode Outline”,在新标签页中选择“ASMified”这个tab,如图19所示,就可以看到这个类中的代码对应的ASM写法了。图中上下两个红框分别对应AOP中的前置逻辑于后置逻辑,将这两块直接复制到visitor中的visitMethod()以及visitInsn()方法中,就可以了

在这里插入图片描述

1.2、 Javassist

ASM是在指令层次上操作字节码的,阅读上文后,我们的直观感受是在指令层次上操作字节码的框架实现起来比较晦涩。故除此之外,我们再简单介绍另外一类框架:强调源代码层次操作字节码的框架Javassist

利用Javassist实现字节码增强时,可以无须关注字节码刻板的结构,其优点就在于编程简单。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构或者动态生成类。其中最重要的是ClassPool、CtClass、CtMethod、CtField这四个类:

  • CtClass(compile-time class):编译时类信息,它是一个class文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个CtClass对象,用来表示这个类文件
  • ClassPool:从开发视角来看,ClassPool是一张保存CtClass信息的HashTable,key为类名,value为类名对应的CtClass对象。当我们需要对某个类进行修改时,就是通过pool.getCtClass(“className”)方法从pool中获取到相应的CtClass。
  • CtMethod、CtField:这两个比较好理解,对应的是类中的方法和属性。

了解这四个类后,我们可以写一个小Demo来展示 Javassist 简单、快速的特点。我们依然是对Base中的 process() 方法做增强,在方法调用前后分别输出“start”和“end”,实现代码如下。我们需要做的就是从pool中获取到相应的 CtClass对象 和 其中的方法,然后执行method.insertBefore 和 insertAfter 方法,参数为要插入的Java代码,再以字符串的形式传入即可,实现起来也极为简单。

import com.meituan.mtrace.agent.javassist.*;public class JavassistTest {public static void main(String[] args) throws NotFoundException, CannotCompileException, IllegalAccessException, InstantiationException, IOException {ClassPool cp = ClassPool.getDefault();CtClass cc = cp.get("meituan.bytecode.javassist.Base");CtMethod m = cc.getDeclaredMethod("process");m.insertBefore("{ System.out.println(\"start\"); }");m.insertAfter("{ System.out.println(\"end\"); }");// JVM不允许运行时重新加载类信息,该行代码会报错Class c = cc.toClass();cc.writeFile("/Users/zen/projects");Base h = (Base)c.newInstance();h.process();}
}

2、运行时类的重载

2.1、问题引出

上一章重点介绍了两种不同类型的字节码操作框架,且都利用它们实现了较为粗糙的AOP。其实,为了方便大家理解字节码增强技术,在上文中我们避重就轻将ASM实现AOP的过程分为了两个main方法:第一个是利用MyClassVisitor对已编译好的class文件进行修改,第二个是new对象并调用。这期间并不涉及到JVM运行时对类的重加载,而是在第一个main方法中,通过ASM对已编译类的字节码进行替换,在第二个main方法中,直接使用已替换好的新类信息。另外在Javassist的实现中,我们也只加载了一次Base类,也不涉及到运行时重加载类。

如果我们在一个JVM中,先加载了一个类,然后又对其进行字节码增强并重新加载会发生什么呢?模拟这种情况,只需要我们在上文中Javassist 的Demo中main()方法的第一行添加 Base b=new Base(),即在增强前就先让 JVM加载Base类,然后在执行到c.toClass()方法时会抛出错误,如下图所示。跟进c.toClass()方法中,我们会发现它是在最后调用了ClassLoader的native方法 defineClass() 时报错。也就是说,JVM是不允许在运行时动态重载一个类的。

在这里插入图片描述

显然,如果只能在类加载前对类进行强化,那字节码增强技术的使用场景就变得很窄了。我们期望的效果是:在一个持续运行并已经加载了所有类的JVM中,还能利用字节码增强技术对其中的类行为做替换并重新加载。为了模拟这种情况,我们将Base类做改写,在其中编写main方法,每五秒调用一次process()方法,在process()方法中输出一行“process”。

我们的目的就是,在JVM运行中的时候,将process()方法做替换,在其前后分别打印“start”和“end”。也就是在运行中时,每五秒打印的内容由”process”变为打印”start process end”。那如何解决JVM不允许运行时重新加载类信息的问题呢?为了达到这个目的,我们接下来一一来介绍需要借助的Java类库。

import java.lang.management.ManagementFactory;public class Base {public static void main(String[] args) {String name = ManagementFactory.getRuntimeMXBean().getName();String s = name.split("@")[0];//打印当前PidSystem.out.println("pid:"+s);while (true) {try {Thread.sleep(5000L);} catch (Exception e) {break;}process();}}public static void process() {System.out.println("process");}
}

2.2、Instrument ☆

instrument是JVM提供的一个可以修改已加载类的类库,专门为Java语言编写的插桩服务提供支持。它需要依赖JVMTI的Attach API机制实现,JVMTI这一部分,我们将在下一小节进行介绍。在JDK 1.6以前,instrument只能在JVM刚启动开始加载类时生效,而在JDK 1.6之后,instrument支持了在运行时对类定义的修改。要使用instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。接口中的transform() 方法会在类文件被加载时调用,而在transform方法里,我们可以利用上文中的ASM 或 Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回

我们定义一个实现了 ClassFileTransformer 接口的类 TestTransformer,依然在其中利用Javassist对Base类中的process()方法进行增强,在前后分别打印“start”和“end”,代码如下:

import java.lang.instrument.ClassFileTransformer;public class TestTransformer implements ClassFileTransformer {@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {System.out.println("Transforming " + className);try {ClassPool cp = ClassPool.getDefault();CtClass cc = cp.get("meituan.bytecode.jvmti.Base");CtMethod m = cc.getDeclaredMethod("process");m.insertBefore("{ System.out.println(\"start\"); }");m.insertAfter("{ System.out.println(\"end\"); }");return cc.toBytecode();} catch (Exception e) {e.printStackTrace();}return null;}
}

现在有了Transformer,那么它要如何注入到正在运行的JVM呢?还需要定义一个Agent,借助Agent的能力将 Instrument 注入到JVM中。我们将在下一小节介绍Agent,现在要介绍的是Agent中用到的另一个类Instrumentation。在JDK 1.6之后,Instrumentation 可以做启动后的Instrument、本地代码(Native Code)的Instrument,以及动态改变Classpath等等。我们可以向Instrumentation中添加上文中定义的Transformer,并指定要被重加载的类,代码如下所示。这样,当Agent被Attach到一个JVM中时,就会执行类字节码替换并重载入JVM的操作

import java.lang.instrument.Instrumentation;public class TestAgent {public static void agentmain(String args, Instrumentation inst) {// 指定我们自己定义的Transformer,在其中利用 Javassist 做字节码替换inst.addTransformer(new TestTransformer(), true);try {//重定义类并载入新的字节码inst.retransformClasses(Base.class);System.out.println("Agent Load Done.");} catch (Exception e) {System.out.println("agent load failed!");}}
}

2.3、JVMTI & Agent & Attach API

上一小节中,我们给出了Agent类的代码,追根溯源需要先介绍JPDA(Java Platform Debugger Architecture)。如果JVM启动时开启了JPDA,那么类是允许被重新加载的。在这种情况下,已被加载的旧版本类信息可以被卸载,然后重新加载新版本的类。正如JDPA名称中的Debugger,JDPA其实是一套用于调试Java程序的标准,任何JDK都必须实现该标准。

JPDA定义了一整套完整的体系,它将调试体系分为三部分,并规定了三者之间的通信接口。三部分由低到高分别是Java 虚拟机工具接口(JVMTI),Java 调试协议(JDWP)以及 Java 调试接口(JDI),三者之间的关系如下图所示:

在这里插入图片描述

现在回到正题,我们可以借助JVMTI的一部分能力,帮助动态重载类信息。JVM TI(JVM TOOL INTERFACE,JVM工具接口)是JVM提供的一套对JVM进行操作的工具接口。通过JVMTI,可以实现对JVM的多种操作,它通过接口注册各种事件勾子,在JVM事件触发时,同时触发预定义的勾子,以实现对各个JVM事件的响应,事件包括类文件加载、异常产生与捕获、线程启动和结束、进入和退出临界区、成员变量修改、GC开始和结束、方法调用进入和退出、临界区竞争与等待、VM启动与退出等等。

而Agent就是JVMTI的一种实现,Agent有两种启动方式,一是随Java进程启动而启动,经常见到的java -agentlib就是这种方式;二是运行时载入,通过attach API,将模块(jar包)动态地Attach到指定进程id的Java进程内。

Attach API 的作用是提供JVM进程间通信的能力,比如说我们为了让另外一个JVM进程把线上服务的线程Dump出来,会运行jstack或jmap的进程,并传递pid的参数,告诉它要对哪个进程进行线程Dump,这就是Attach API做的事情。在下面,我们将通过Attach API的 loadAgent()方法,将打包好的Agent jar包动态Attach到目标JVM上。具体实现起来的步骤如下:

  • 定义Agent,并在其中实现AgentMain方法,如上一小节中定义的代码块7中的TestAgent类;
  • 然后将TestAgent类打成一个包含MANIFEST.MF的jar包,其中MANIFEST.MF文件中将Agent-Class属性指定为TestAgent的全限定名,如下图所示;

在这里插入图片描述

  • 最后利用Attach API,将我们打包好的jar包Attach到指定的JVM pid上,代码如下:
import com.sun.tools.attach.VirtualMachine;public class Attacher {public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException, AgentInitializationException {// 传入目标 JVM pidVirtualMachine vm = VirtualMachine.attach("39333");vm.loadAgent("/Users/zen/operation_server_jar/operation-server.jar");}
}
  • 由于在MANIFEST.MF中指定了Agent-Class,所以在Attach后,目标JVM在运行时会走到TestAgent类中定义的agentmain()方法,而在这个方法中,我们利用Instrumentation,将指定类的字节码通过定义的类转化器TestTransformer做了Base类的字节码替换(通过javassist),并完成了类的重新加载。由此,我们达成了“在JVM运行时,改变类的字节码并重新载入类信息”的目的。

以下为运行时重新载入类的效果:先运行Base中的main()方法,启动一个JVM,可以在控制台看到每隔五秒输出一次”process”。接着执行Attacher中的main()方法,并将上一个JVM的pid传入。此时回到上一个main()方法的控制台,可以看到现在每隔五秒输出”process”前后会分别输出”start”和”end”,也就是说完成了运行时的字节码增强,并重新载入了这个类。

在这里插入图片描述

2.4、使用场景

至此,字节码增强技术 的可使用范围就不再局限于JVM加载类前了。通过上述几个类库,我们可以在运行时对JVM中的类进行修改并重载了。通过这种手段,可以做的事情就变得很多了:

  • 热部署:不部署服务而对线上服务做修改,可以做打点、增加日志等操作。

    • javassist + instrument 实现热部署
  • Mock:测试时候对某些服务做Mock。

  • 性能诊断工具比如bTrace就是利用Instrument,实现无侵入地跟踪一个正在运行的JVM,监控到类和方法级别的状态信息。

3、总结

字节码增强技术相当于是一把打开运行时JVM的钥匙,利用它可以动态地对运行中的程序做修改,也可以跟踪JVM运行中程序的状态。此外,我们平时使用的动态代理、AOP也与字节码增强密切相关,它们实质上还是利用各种手段生成符合规范的字节码文件。综上所述,掌握字节码增强后可以高效地定位并快速修复一些棘手的问题(如线上性能问题、方法出现不可控的出入参需要紧急加日志等问题),也可以在开发中减少冗余代码,大大提高开发效率。

4、参考文献

  • 《ASM4-Guide》
  • Oracle:The class File Format
  • Oracle:The Java Virtual Machine Instruction Set
  • javassist tutorial
  • JVM Tool Interface - Version 1.2

5、作者简介

泽恩,美团点评研发工程师。

文章来源:美团技术团队 字节码增强技术探索

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

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

相关文章

Stable Diffusion 动画animatediff-cli-prompt-travel

基于 sd-webui-animatediff 生成动画或者动态图的基础功能,animatediff-cli-prompt-travel突破了部分限制,能让视频生成的时间更长,并且能加入controlnet和提示词信息控制每个片段,并不像之前 sd-webui-animatediff 的一套关键词控制全部画面。 动图太大传不上来,凑合看每…

10月14日,每日信息差

今天是2023年10月14日&#xff0c;以下是为您准备的8条信息差 第一、中国石油摘得日本碳信用实货交易首单。据了解&#xff0c;日本交易所集团旗下的东京证券交易所11日宣布&#xff0c;交易二氧化碳排放量的“碳信用市场”正式开始运营 第二、前三季度全国铁路投产新线1402公…

【C++】哈希对unordered_map和unodered_set的封装

&#x1f680;write in front&#x1f680; &#x1f4dc;所属专栏&#xff1a; C学习 &#x1f6f0;️博客主页&#xff1a;睿睿的博客主页 &#x1f6f0;️代码仓库&#xff1a;&#x1f389;VS2022_C语言仓库 &#x1f3a1;您的点赞、关注、收藏、评论&#xff0c;是对我最大…

Godot 单元测试

前言 单元测试是我们常用的功能&#xff0c;Godot作为一个游戏&#xff0c;单元测试和热重载是我们常用的功能。这里我们讲解最简单的单元测试的情况。 Godot 配置 我们添加一个最简单的节点&#xff0c;挂载一个最简单的脚本。 添加测试方法&#xff08;只能是静态方法&…

【Python】Python语言基础(中)

第十章 Python的数据类型 基本数据类型 数字 整数 整数就是整数 浮点数 在编程中&#xff0c;小数都称之为浮点数 浮点数的精度问题 print(0.1 0.2) --------------- 0.30000000000000004 ​​1.可以通过round()函数来控制小数点后位数 round(a b)&#xff0c;则表示…

Linux该如何学习,给你支招

如果你已经确定对 Linux 产生了兴趣&#xff0c;那么接下来我们介绍一下学习 Linux 的方法。这只是自己关于学习Linux的建议。 一、如何去学习 学习大多类似庖丁解牛&#xff0c;对事物的认识一般都是由浅入深、由表及里的过程&#xff0c;循序才能渐进。学习 Linux 同样要有一…

关于RNNoise、webrtc_ns、三角带通滤波器、对数能量

语音特征参数MFCC提取过程详解 其中讲解了&#xff1a;三角带通滤波器 、计算每个滤波器组输出的对数能量、对数能量、经离散余弦变换&#xff08;DCT&#xff09;得到MFCC系数 推荐阅读某乎这位大佬的全部文章&#xff1a; 下面是几篇出自这位大佬的很好的文章&#xff1a; …

SSH 基础学习使用

什么是SSH 1.SSH SSH&#xff08;Secure Shell&#xff09; 是较可靠&#xff0c;专为远程登录会话和其他网络服务提供安全性的协议&#xff0c;利用 SSH 协议可以有效防止远程管理过程中的信息泄露问题。 实际应用中&#xff0c;主要用于保证远程登录和远程通信的安全&#…

微信小程序入门讲解【超详细】

一. 微信小程序简介 1.1 什么是小程序 2017年度百度百科十大热词之一 微信小程序&#xff08;wei xin xiao cheng xu&#xff09;&#xff0c;简称小程序&#xff0c;英文名Mini Program&#xff0c;是一种不需要下载安装即可使用的应用( 张小龙对其的定义是无需安装&#xf…

如何创建自定义前端组件?

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

VScode运行C/C++

VScode运行C/C VScode的安装这里不讲 一、mingw64的下载 二、VS code打开文件夹与创建C文件 ----------------这一步给萌新看&#xff0c;有C和VScode的基础可跳过---------------- 1.创建一个文件夹 2.vscode打开刚刚创建的文件夹 3.新建文件&#xff0c;在输入文件名1.c后…

Unity实现摄像机向屏幕中间发射射线射击物体

1.创建一个准星放在屏幕中间 外部找个PNG透明图&#xff0c;拖到Unity文件夹&#xff0c;右上角改成精灵sprite2d 2.添加到UI画布 3.写脚本 首先&#xff0c;我们需要引入一些 "工具"&#xff0c;就像我们在玩游戏时要先下载游戏客户端一样。这里的 "工具&quo…

iOS- flutter flavor 多环境Configurations配置

一、点击PROJECT的Runner&#xff0c;选择Info选项&#xff0c;在Configurations下方的号添加不同环境的配置&#xff0c;如下图&#xff1a; 二、选择TAGETS的Runner项目&#xff0c;选择Build Settings选项&#xff0c;在输入框输入package&#xff0c;为不同环境配置相应的…

UML组件图综合指南:设计清晰、可维护的软件系统

介绍&#xff1a; UML&#xff08;Unified Modeling Language&#xff09;组件图是软件系统设计中的重要工具&#xff0c;用于描绘系统的物理结构和组件之间的关系。在软件工程中&#xff0c;通过创建清晰的组件图&#xff0c;团队能够更好地理解系统的模块化结构和组织关系&a…

二十四、【参考素描三大面和五大调】

文章目录 三种色面(黑白灰)五种色调 这个可以参考素描对物体受光的理解&#xff1a;素描调子的基本规律与素描三大面五大调物体的明暗规律 三种色面(黑白灰) 如下图所示&#xff0c;我们可以看到光源是从亮面所对应的方向射过来的,所以我们去分析图形的时候&#xff0c;首先要…

C# excel操作

使用库 Spire.Xls 下载 示例数据 代码示例 1.删除列 代码 private static void DeleteExcelColumns1(string excelPath) {if (excelPath.Length 0) {Console.WriteLine("excel文件路径为空");}else{Console.WriteLine("删除列方法1&#xff1a;保留第一列&…

好的摄影师都会iPhone 8和iOS 11的这三项功能

众所周知&#xff0c;苹果的手机像素一直处于智能手机摄影的前沿&#xff0c;在即将到来的九月&#xff0c;苹果公司准备证明他拥有最好的相机技术。 虽然我们还不知道iPhone 8摄像头的具体细节&#xff0c;如几百万像素、光学变焦是多少&#xff0c;但我们确实知道苹果正在给i…

Webmin(CVE-2019-15107)远程命令执行漏洞复现

漏洞编号 CVE-2019-15107 webmin介绍 什么是webmin Webmin是目前功能最强大的基于Web的Unix系统管理工具。管理员通过浏览器访问Webmin的各种管理功能并完成相应的管理动作http://www.webmin.com/Webmin 是一个用 Perl 编写的基于浏览器的管理应用程序。是一个基于Web的界面…

Rust入门基础

文章目录 Rust相关介绍为什么要用Rust&#xff1f;Rust的用户和案例 开发环境准备安装Rust更新与卸载Rust开发工具 Hello World程序编写Rust程序编译与运行Rust程序 Cargo工具Cargo创建项目Cargo构建项目Cargo构建并运行项目Cargo检查项目Cargo为发布构建项目 Rust相关介绍 为…

Spring Boot自动加载

问&#xff1a;自动装配如何实现的&#xff1f; 答&#xff1a;简单来说就是自动去把第三方组件的Bean装载到IOC容器中&#xff0c;不需要开发人员再去写Bean相关的配置&#xff0c;在springboot应用里面只需要在启动类上去加上SpringBootApplication注解&#xff0c;就可以去实…