`
Neil_yang
  • 浏览: 131072 次
  • 性别: Icon_minigender_1
  • 来自: 珠海
社区版块
存档分类
最新评论

Java深度历险(一)——Java字节代码的操纵

    博客分类:
  • JAVA
阅读更多

Java深度历险(一)——Java字节代码的操纵

在一般的Java应用开发过程中,开发人员使用Java的方式比较简单。打开惯用的IDE,编写Java源代码,再利用IDE提供的功能直接运行Java 程序就可以了。这种开发模式背后的过程是:开发人员编写的是Java源代码文件(.java),IDE会负责调用Java的编译器把Java源代码编译成平台无关的字节代码(byte code),以类文件的形式保存在磁盘上(.class)。Java虚拟机(JVM)会负责把Java字节代码加载并执行。Java通过这种方式来实现其“编写一次,到处运行(Write once, run anywhere)” 的目标。Java类文件中包含的字节代码可以被不同平台上的JVM所使用。Java字节代码不仅可以以文件形式存在于磁盘上,也可以通过网络方式来下载,还可以只存在于内存中。JVM中的类加载器会负责从包含字节代码的字节数组(byte[])中定义出Java类。在某些情况下,可能会需要动态的生成 Java字节代码,或是对已有的Java字节代码进行修改。这个时候就需要用到本文中将要介绍的相关技术。首先介绍一下如何动态编译Java源文件。

动态编译Java源文件

在一般情况下,开发人员都是在程序运行之前就编写完成了全部的Java源代码并且成功编译。对有些应用来说,Java源代码的内容在运行时刻才能确定。这个时候就需要动态编译源代码来生成Java字节代码,再由JVM来加载执行。典型的场景是很多算法竞赛的在线评测系统(如PKU JudgeOnline),允许用户上传Java代码,由系统在后台编译、运行并进行判定。在动态编译Java源文件时,使用的做法是直接在程序中调用Java编译器。

JSR 199引入了Java编译器API。如果使用JDK 6的话,可以通过此API来动态编译Java代码。比如下面的代码用来动态编译最简单的Hello World类。该Java类的代码是保存在一个字符串中的。

public class CompilerTest {
   public static void main(String[] args) throws Exception {      
      String source = "public class Main { public static void main(String[] args) {System.out.println(\"Hello World!\");} }";
      JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
      StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null);
      StringSourceJavaObject sourceObject = new CompilerTest.StringSourceJavaObject("Main", source);
      Iterable< extends JavaFileObject> fileObjects = Arrays.asList(sourceObject);
      CompilationTask task = compiler.getTask(null, fileManager, null, null, null, fileObjects);
      boolean result = task.call();
      if (result) {
         System.out.println("编译成功。");
      }
   }

   static class StringSourceJavaObject extends SimpleJavaFileObject {

      private String content = null;
      public StringSourceJavaObject(String name, String content) ??throws URISyntaxException {
         super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);
         this.content = content;
      }

      public CharSequence getCharContent(boolean ignoreEncodingErrors) ??throws IOException {
         return content;
      }
   }
}

 

如果不能使用JDK 6提供的Java编译器API的话,可以使用JDK中的工具类com.sun.tools.javac.Main,不过该工具类只能编译存放在磁盘上的文件,类似于直接使用javac命令。

另外一个可用的工具是Eclipse JDT Core提供的编译器。这是Eclipse Java开发环境使用的增量式Java编译器,支持运行和调试有错误的代码。该编译器也可以单独使用。Play框架在内部使用了JDT的编译器来动态编译Java源代码。在开发模式下,Play框架会定期扫描项目中的Java源代码文件,一旦发现有修改,会自动编译 Java源代码。因此在修改代码之后,刷新页面就可以看到变化。使用这些动态编译的方式的时候,需要确保JDK中的tools.jar在应用的 CLASSPATH中。

下面介绍一个例子,是关于如何在Java里面做四则运算,比如求出来(3+4)*7-10的值。一般的做法是分析输入的运算表达式,自己来模拟计算过程。考虑到括号的存在和运算符的优先级等问题,这样的计算过程会比较复杂,而且容易出错。另外一种做法是可以用JSR 223引入的脚本语言支持,直接把输入的表达式当做JavaScript或是JavaFX脚本来执行,得到结果。下面的代码使用的做法是动态生成Java源代码并编译,接着加载Java类来执行并获取结果。这种做法完全使用Java来实现。

private static double calculate(String expr) throws CalculationException  {
   String className = "CalculatorMain";
   String methodName = "calculate";
   String source = "public class " + className 
      + " { public static double " + methodName + "() { return " + expr + "; } }";
	  //省略动态编译Java源代码的相关代码,参见上一节
   boolean result = task.call();
   if (result) {
      ClassLoader loader = Calculator.class.getClassLoader(); 
      try {            
         Class<?> clazz = loader.loadClass(className);
         Method method = clazz.getMethod(methodName, new Class<?>[] {});
         Object value = method.invoke(null, new Object[] {});
         return (Double) value;
      } catch (Exception e) {
         throw new CalculationException("内部错误。");        
      }    
   } else {
      throw new CalculationException("错误的表达式。");    
   }
}

 

 

上面的代码给出了使用动态生成的Java字节代码的基本模式,即通过类加载器来加载字节代码,创建Java类的对象的实例,再通过Java反射API来调用对象中的方法。

Java字节代码增强

Java 字节代码增强指的是在Java字节代码生成之后,对其进行修改,增强其功能。这种做法相当于对应用程序的二进制文件进行修改。在很多Java框架中都可以见到这种实现方式。Java字节代码增强通常与Java源文件中的注解(annotation)一块使用。注解在Java源代码中声明了需要增强的行为及相关的元数据,由框架在运行时刻完成对字节代码的增强。Java字节代码增强应用的场景比较多,一般都集中在减少冗余代码和对开发人员屏蔽底层的实现细节上。用过JavaBeans的人可能对其中那些必须添加的getter/setter方法感到很繁琐,并且难以维护。而通过字节代码增强,开发人员只需要声明Bean中的属性即可,getter/setter方法可以通过修改字节代码来自动添加。用过JPA的人,在调试程序的时候,会发现实体类中被添加了一些额外的 域和方法。这些域和方法是在运行时刻由JPA的实现动态添加的。字节代码增强在面向方面编程(AOP)的一些实现中也有使用。

在讨论如何进行字节代码增强之前,首先介绍一下表示一个Java类或接口的字节代码的组织形式。

 

类文件 {
   0xCAFEBABE,小版本号,大版本号,常量池大小,常量池数组,
   访问控制标记,当前类信息,父类信息,实现的接口个数,实现的接口信息数组,域个数,
   域信息数组,方法个数,方法信息数组,属性个数,属性信息数组
}

 

 

如上所示,一个类或接口的字节代码使用的是一种松散的组织结构,其中所包含的内容依次排列。对于可能包含多个条目的内容,如所实现的接口、域、方法和属性等,是以数组来表示的。而在数组之前的是该数组中条目的个数。不同的内容类型,有其不同的内部结构。对于开发人员来说,直接操纵包含字节代码的字节数组的话,开发效率比较低,而且容易出错。已经有不少的开源库可以对字节代码进行修改或是从头开始创建新的Java类的字节代码内容。这些类库包括ASMcglibserpBCEL等。使用这些类库可以在一定程度上降低增强字节代码的复杂度。比如考虑下面一个简单的需求,在一个Java类的所有方法执行之前输出相应的日志。熟悉AOP的人都知道,可以用一个前增强(before advice)来解决这个问题。如果使用ASM的话,相关的代码如下:

 

ClassReader cr = new ClassReader(is);
ClassNode cn = new ClassNode();
cr.accept(cn, 0);
for (Object object : cn.methods) {    
   MethodNode mn = (MethodNode) object;   
   if ("<init>".equals(mn.name) || "<clinit>".equals(mn.name)) {        
      continue;    
   }    
   InsnList insns = mn.instructions;    
   InsnList il = new InsnList();   
   il.add(new FieldInsnNode(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;"));    
   il.add(new LdcInsnNode("Enter method -> " + mn.name));   
   il.add(new MethodInsnNode(INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V"));    
   insns.insert(il);  mn.maxStack += 3;
}
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] b = cw.toByteArray();

 

ClassWriter就可以获取到包含增强之后的字节代码的字节数组,可以把字节代码写回磁盘或是由类加载器直接使用。上述示例中,增强部分的逻辑比较简单,只是遍历Java类中的所有方法并添加对System.out.println方法的调用。在字节代码中,Java方法体是由一系列的指令组成的。而要做的是生成调用System.out.println方法的指令,并把这些指令插入到指令集合的最前面。ASM对这些指令做了抽象,不过熟悉全部的指令比较困难。ASM提供了一个工具类ASMifierClassVisitor,可以打印出Java类的字节代码的结构信息。当需要增强某个类的时候,可以先在源代码上做出修改,再通过此工具类来比较修改前后的字节代码的差异,从而确定该如何编写增强的代码。

对类文件进行增强的时机是需要在Java源代码编译之后,在JVM执行之前。比较常见的做法有:

  • 由IDE在完成编译操作之后执行。如Google App Engine的Eclipse插件会在编译之后运行DataNucleus来对实体类进行增强。
  • 在构建过程中完成,比如通过Ant或Maven来执行相关的操作。
  • 实现自己的Java类加载器。当获取到Java类的字节代码之后,先进行增强处理,再从修改过的字节代码中定义出Java类。
  • 通过JDK 5引入的java.lang.instrument包来完成。

java.lang.instrument

由于存在着大量对Java字节代码进行修改的需求,JDK 5引入了java.lang.instrument包并在JDK 6中得到了进一步的增强。基本的思路是在JVM启动的时候添加一些代理(agent)。每个代理是一个jar包,其清单(manifest)文件中会指定一个代理类。这个类会包含一个premain方法。JVM在启动的时候会首先执行代理类的premain方法,再执行Java程序本身的main方法。在 premain方法中就可以对程序本身的字节代码进行修改。JDK 6中还允许在JVM启动之后动态添加代理。java.lang.instrument包支持两种修改的场景,一种是重定义一个Java类,即完全替换一个 Java类的字节代码;另外一种是转换已有的Java类,相当于前面提到的类字节代码增强。还是以前面提到的输出方法执行日志的场景为例,首先需要实现java.lang.instrument.ClassFileTransformer接口来完成对已有Java类的转换。

static class MethodEntryTransformer implements ClassFileTransformer {
   public byte[] transform(ClassLoader loader, String className,
     Class<?> classBeingRedefined, ?ProtectionDomain protectionDomain, byte[] classfileBuffer) 
     throws  IllegalClassFormatException {
        try {
           ClassReader cr = new ClassReader(classfileBuffer);
           ClassNode cn = new ClassNode();            
           //省略使用ASM进行字节代码转换的代码            
           ClassWriter cw = new ClassWriter(0);
           cn.accept(cw); 
           return cw.toByteArray();       
        } catch (Exception e){            
           return null;
        }
   }
}

 

有了这个转换类之后,就可以在代理的premain方法中使用它。

public static void premain(String args, Instrumentation inst) {    
   inst.addTransformer(new MethodEntryTransformer());
}

 

把该代理类打成一个jar包,并在jar包的清单文件中通过Premain-Class声明代理类的名称。运行Java程序的时候,添加JVM启动参数-javaagent:myagent.jar。这样的话,JVM会在加载Java类的字节代码之前,完成相关的转换操作。

总结

操纵Java字节代码是一件很有趣的事情。通过它,可以很容易的对二进制分发的Java程序进行修改,非常适合于性能分析、调试跟踪和日志记录等任务。另外一个非常重要的作用是把开发人员从繁琐的Java语法中解放出来。开发人员应该只需要负责编写与业务逻辑相关的重要代码。对于那些只是因为语法要求而添加的,或是模式固定的代码,完全可以将其字节代码动态生成出来。字节代码增强和源代码生成是不同的概念。源代码生成之后,就已经成为了程序的一部分,开发人员需要去维护它:要么手工修改生成出来的源代码,要么重新生成。而字节代码的增强过程,对于开发人员是完全透明的。妥善使用Java字节代码的操纵技术,可以更好的解决某一类开发问题。

 

原文:http://www.infoq.com/cn/articles/cf-java-byte-code

分享到:
评论

相关推荐

    Java深度历险

    ### Java深度历险——Java字节代码的操纵 #### 引言 随着Java技术的不断发展与完善,越来越多的开发者开始关注于如何更深入地理解Java内部机制及其高级特性。本篇将聚焦于Java字节码层面的操作,为读者提供一个...

    java深度历险.pdf

    【Java深度历险——探索Java字节码操纵】 在Java编程世界中,深入理解Java字节码对于优化程序性能和实现高级功能至关重要。Java字节码是Java虚拟机(JVM)执行的基础,它是一种平台无关的中间表示,使得Java程序...

    刘嘉怡.中期检查.doc

    刘嘉怡.中期检查.doc

    COMSOL热电效应模型:基于MATLAB API的热电转换仿真与优化

    内容概要:本文详细介绍了如何使用COMSOL Multiphysics进行热电效应仿真的全过程。首先解释了热电效应的基本概念及其应用场景,如手机充电发烫、吹风机温度升高等。接着,通过具体实例展示了如何在COMSOL中建立热电模型,包括选择合适的物理场(焦耳热和热电效应)、设定材料属性(电导率、导热系数、塞贝克系数)、绘制几何形状以及设置边界条件。文中还提供了详细的MATLAB代码片段用于自动化建模流程,涵盖求解器配置、网格划分、后处理等方面的技术细节。此外,作者分享了一些常见问题的解决方案,如求解器不收敛、网格畸变等。 适合人群:对热电效应感兴趣的科研人员、工程技术人员及高校学生,尤其适用于有一定COMSOL和MATLAB基础的学习者。 使用场景及目标:帮助读者掌握热电效应的基本原理和COMSOL仿真技能,能够独立完成从模型构建到结果分析的完整流程。目标是提高热电转换系统的效率,优化设计参数,探索新材料的应用潜力。 其他说明:文章不仅提供了理论指导,还包括大量实战经验和技术技巧,有助于解决实际建模过程中遇到的问题。

    汽车内外饰模具设计规范详解:分型面、斜顶滑块及模架顶出系统的技术要点

    内容概要:本文深入探讨了汽车内外饰模具设计的关键要素,涵盖分型面设计、斜顶和滑块的应用、模架选择以及顶出系统的配置。针对每个部分,不仅提供了理论指导,还辅以Python、MATLAB等编程语言的实际代码示例,帮助理解和实施具体设计方案。例如,分型面设计强调了如何根据产品结构和外观要求确定最佳分型面位置;斜顶和滑块部分讨论了不同类型及其应用场景;模架和顶出系统则关注于结构稳定性和顶出效果的优化。 适合人群:从事汽车模具设计的专业人士,尤其是希望深入了解内外饰模具设计细节的新手设计师和技术人员。 使用场景及目标:适用于汽车内外饰模具设计项目,旨在提高模具设计的精度和效率,减少试错成本,确保产品质量。通过学习本文提供的技术和实践经验,能够更好地应对实际工作中遇到的各种挑战。 其他说明:文中提到的代码示例和经验公式均来源于实际工程案例,具有较高的参考价值。同时,作者还分享了许多宝贵的行业经验和技巧,有助于读者快速掌握模具设计的核心技能。

    python3.10以上 可安装pyside6(类似pyqt),具体安装操作步骤

    python3.10以上 可安装pyside6(类似pyqt),具体安装操作步骤

    【人工智能领域】DeepSeek AI深度探索平台的优势解析:多模态处理、低成本训练与广泛应用场景综述

    内容概要:DeepSeek AI是由杭州深度求索人工智能基础技术研究有限公司于2025年1月20日发布的深度探索AI技术。它具有多模态能力、多语言支持、长上下文理解、领域垂直优化、开源特性等多项技术突破,支

    IIS配置phpweb服务器所需VC-redist.x64.rar

    IIS配置phpweb服务器所需VC_redist.x64.rar

    云南移动5G-A网业战略发展探讨 -创新领航,千帆竞发,共同迈入5G-A新时代.pptx

    云南移动5G-A网业战略发展探讨 -创新领航,千帆竞发,共同迈入5G-A新时代.pptx

    C#学习之OpenCv实现模版匹配案例

    本文描述了如何使用C#基于OpenCvSharpe实现模版匹配功能,其中实现了下功能: 1、图像加载; 2、模版加载、绘制、保存功能; 3、模版匹配功能。

    【软件工程与数据分析】数据结构求职面试问题汇总:涵盖链表、树结构及算法复杂度分析的实战题目解析

    内容概要:本文档汇集了CSci 235软件设计与分析II课程中关于数据结构的面试题,由Stewart Weiss教授整理。文档涵盖了广泛的数据结构主题,包括但不限于链表(如单链表、双向链表、循环链表)、二叉树(如二叉搜索树、最小高度二叉搜索树)、栈、队列等。每个问题都旨在考察求职者对不同数据结构的理解及其应用场景。例如,选择合适的数据结构实现手机通讯录功能,或设计支持撤销功能的文本编辑器。此外,文档还探讨了复杂度分析(Big-O表示法),以及如何优化特定操作的时间复杂度。最后,文档提供了额外的学习资源链接,帮助求职者进一步准备面试。 适合人群:计算机科学专业的学生或有志于从事软件开发工作的求职者,特别是那些希望在技术面试中表现优异的人士。 使用场景及目标:①理解并掌握常见数据结构的基本概念和特性;②学会根据不同场景选择最合适的数据结构;③掌握常见数据结构操作的时间复杂度分析;④为技术面试做充分准备,提高面试成功率。 其他说明:文档中的问题不仅限于理论知识,还包括实际编码练习,建议读者在学习过程中动手实践,以加深理解和记忆。同时,文档提供的额外资源链接可以作为扩展阅读材料,帮助读者更全面地掌握相关知识。

    【路径规划】基于matlab A_Star融合灰狼算法GWO求解多仓库机器人送货路径规划【含Matlab源码 13134期】.zip

    Matlab领域上传的视频是由对应的完整代码运行得来的,完整代码皆可运行,亲测可用,适合小白; 1、从视频里可见完整代码的内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作

    帆软本地打印插件FinePrint 8.0版本

    帆软本地打印插件FinePrint 8.0版本,适用于FineReport8

    【嵌入式控制系统】基于EECS461课程的嵌入式控制技术在汽车领域的应用与发展:从基础概念到未来挑战了文档的主要内容

    内容概要:本文介绍了密歇根大学EECS 461课程——嵌入式控制系统的核心内容及其发展背景。课程旨在教授学生嵌入式控制系统的理论与实践,包括传感器和执行器接口、实时性能和安全要求、混合行为系统、分布式控制网络等方面的知识。文中特别强调了现代汽车作为嵌入式控制系统的典型应用,从1977年到2019年间,汽车技术经历了从模拟控制到微处理器控制的巨大变革,如今的汽车具备了更高效、更环保、更安全的特点。课程还涵盖了S32K144微控制器的开发环境、实验室练习(如数字I/O、PWM信号生成、虚拟墙模拟等)以及自动代码生成工具的使用。 适合人群:具备一定编程基础,特别是对嵌入式系统感兴趣的本科生和研究生,尤其是电气工程、计算机科学专业的高年级学生或硕士生。 使用场景及目标:①了解嵌入式控制系统的基本概念和发展历程;②掌握嵌入式控制系统的设计方法和技术手段,如实时操作系统、中断处理、网络通信协议(CAN)等;③通过实际项目操作,熟悉嵌入式硬件平台和开发工具链的应用。 其他说明:随着汽车行业向智能化、自动化方向发展,对于能够开发复杂嵌入式软件的人才需求日益增长。EECS 461不仅为学生提供了扎实的技术训练,也为他们未来的职业发展打下了坚实的基础。此外,课程还反映了跨学科教育的重要性,鼓励学生打破传统学术界限,培养解决实际问题的能力。

    C#与Halcon联合编程实现高效视觉几何定位与测量框架

    内容概要:本文详细介绍了如何利用C#与Halcon联合编程构建高效的视觉几何定位与测量框架。主要内容涵盖模板创建与匹配、圆测量、数据持久化以及图像采集等方面的技术细节。首先,通过创建形状模板并进行匹配,实现了工件的精确定位。接着,针对圆形物体的测量,提出了动态ROI绘制、亚像素边缘提取和稳健圆拟合的方法。此外,还讨论了模板管理和图像采集的最佳实践,确保系统的稳定性和高效性。最后,强调了Halcon对象的内存管理和错误处理机制,提供了实用的优化建议。 适合人群:具备一定编程基础,尤其是对C#和Halcon有一定了解的研发人员和技术爱好者。 使用场景及目标:适用于工业生产线上的自动化检测设备开发,旨在提高工件定位和尺寸测量的精度与效率。主要目标是帮助开发者掌握C#与Halcon联合编程的具体实现方法,从而构建稳定可靠的视觉检测系统。 其他说明:文中提供了大量实战代码片段和调试技巧,有助于读者快速理解和应用相关技术。同时,作者分享了许多实际项目中的经验和教训,使读者能够避开常见陷阱,提升开发效率。

    【人工智能领域】DeepSeek AI核心技术优势及广泛应用场景:推动全球AI创新与产业变革

    内容概要:本文深入探讨了DeepSeek AI的独特优势及其在全球AI领域的影响力。DeepSeek由中国深度求索公司开发,自2025年1月20日发布以来,凭借其卓越的性能和独特优势迅速吸引了全球关注。其核心优势包括:1) 极致成本效率,如低成本训练和高效推理;2) 强大的推理能力,涵盖多领域表现优异

    php连接sqlserver之VC-redist.x64.exe

    php连接sqlserver之VC_redist.x64.exe

    基于Matlab/Simulink的异步电动机恒压频比与转差频率控制仿真及其实现

    内容概要:本文详细介绍了利用Matlab/Simulink进行异步电动机交流调速系统的仿真实验,主要探讨了两种控制方式:恒压频比(V/F)开环控制和转差频率闭环控制。文中不仅提供了具体的数学模型和代码片段,还展示了不同控制方式下的仿真结果对比,包括转速响应、电流波形和谐波含量等方面的表现。此外,文章深入讲解了SVPWM(空间矢量脉宽调制)的应用,强调了其相对于传统SPWM的优势,并给出了详细的参数调整技巧和注意事项。 适合人群:从事电机控制系统设计的研究人员和技术人员,尤其是对Matlab/Simulink有一定基础并希望深入了解异步电动机调速系统的人群。 使用场景及目标:适用于需要进行电机控制算法开发和优化的场合,旨在帮助读者掌握异步电动机调速的基本原理和具体实现方法,提高仿真的准确性和效率。 其他说明:文章通过丰富的实例和图表,生动地展示了各种控制策略的特点和效果,有助于读者更好地理解和应用相关理论。同时,文中提供的调试技巧对于解决实际工程中的常见问题非常有帮助。

    电动汽车等速工况续驶里程仿真及Matlab实现详解

    内容概要:本文详细介绍了如何利用Matlab进行电动汽车等速工况续驶里程的仿真。首先解释了等速工况的概念及其重要性,接着展示了具体的参数设定,如车辆质量、风阻系数、电池容量等。然后深入探讨了核心算法,包括阻力计算、功率需求、能量消耗以及SOC(剩余电量)的变化过程。文中特别强调了一些常见的陷阱和注意事项,如单位换算错误、电机效率的动态变化等。最后,通过可视化工具展示了仿真结果,并讨论了可能的改进方向,如引入NEDC工况循环和其他动态因素。 适合人群:新能源汽车专业的学生、研究人员以及对电动汽车仿真感兴趣的工程师。 使用场景及目标:①帮助理解和掌握电动汽车等速工况续驶里程仿真的原理和方法;②提供详细的代码实现和注释,便于学习和修改;③用于课程设计、毕业设计或其他研究项目。 其他说明:本文不仅提供了完整的Matlab代码,还包括详细的参数说明和常见问题解析,确保使用者能够顺利运行并理解整个仿真过程。同时,作者还分享了许多实践经验,有助于提高仿真的准确性和实用性。

    【定稿】桂林电子科技大学第七届大学生思政课社会实践优秀成果展示活动实施方案 (1).zip

    【定稿】桂林电子科技大学第七届大学生思政课社会实践优秀成果展示活动实施方案 (1).zip

Global site tag (gtag.js) - Google Analytics