如何用java程序实现加密的序列号

如何用java程序实现加密的序列号,第1张

Java是一种跨平台的、解释型语言。Java 源代码编译中间“字节码”存储于class文件中。Class文件是一种字节码形式的中间代码,该字节码中包括了很多源代码的信息,例如变量名、方法名等。因此,Java中间代码的反编译就变得非常轻易。目前市场上有许多免费的、商用的反编译软件,都能够生成高质量的反编译后的源代码。所以,对开发人员来说,如何保护Java程序就变成了一个非常重要的挑战。本文首先讨论了保护Java程序的基本方法,然后对代码混淆问题进行深入研究,最后结合一个实际的应用程序,分析如何在实践中保护Java程序。

反编译成为保护Java程序的最大挑战通常C、C++等编程语言开发的程序都被编译成目标代码,这些目标代码都是本机器的二进制可执行代码。通常所有的源文件被编译、链接成一个可执行文件。在这些可执行文件中,编译器删除了程序中的变量名称、方法名称等信息,这些信息往往是由内存地址表示,例如假如需要使用一个变量,往往是通过这个变量的地址来访问的。因此,反编译这些本地的目标代码就是非常困难的。

Java语言的出现,使得反编译变得非常轻易而有效。原因如下:1.由于跨平台的需求,Java的指令集比较简单而通用,较轻易得出程序的语义信息;2.Java编译器将每一个类编译成一个单独的文件,这也简化了反编译的工作;3.Java 的Class文件中,仍然保留所有的方法名称、变量名称,并且通过这些名称来访问变量和方法,这些符号往往带有许多语义信息。由于Java程序自身的特点,对于不经过处理的Java程序反编译的效果非常好。

目前,市场上有许多Java的反编译工具,有免费的,也有商业使用的,还有的是开放源代码的。这些工具的反编译速度和效果都非常不错。好的反编译软件,能够反编译出非常接近源代码的程序。因此,通过反编译器,黑客能够对这些程序进行更改,或者复用其中的程序。因此,如何保护Java程序不被反编译,是非常重要的一个问题。

常用的保护技术由于Java字节码的抽象级别较高,因此它们较轻易被反编译。本节介绍了几种常用的方法,用于保护Java字节码不被反编译。通常,这些方法不能够绝对防止程序被反编译,而是加大反编译的难度而已,因为这些方法都有自己的使用环境和弱点。

隔离Java程序最简单的方法就是让用户不能够访问到Java Class程序,这种方法是最根本的方法,具体实现有多种方式。例如,开发人员可以将要害的Java Class放在服务器端,客户端通过访问服务器的相关接口来获得服务,而不是直接访问Class文件。这样黑客就没有办法反编译Class文件。目前,通过接口提供服务的标准和协议也越来越多,例如 HTTP、Web Service、RPC等。但是有很多应用都不适合这种保护方式,例如对于单机运行的程序就无法隔离Java程序。这种保护方式见图1所示。

图1隔离Java程序示意图对Class文件进行加密为了防止Class文件被直接反编译,许多开发人员将一些要害的Class文件进行加密,例如对注册码、序列号治理相关的类等。在使用这些被加密的类之前,程序首先需要对这些类进行解密,而后再将这些类装载到JVM当中。这些类的解密可以由硬件完成,也可以使用软件完成。

在实现时,开发人员往往通过自定义ClassLoader类来完成加密类的装载(注重由于安全性的原因,Applet不能够支持自定义的ClassLoader)。自定义的ClassLoader首先找到加密的类,而后进行解密,最后将解密后的类装载到JVM当中。在这种保护方式中,自定义的ClassLoader是非常要害的类。由于它本身不是被加密的,因此它可能成为黑客最先攻击的目标。假如相关的解密密钥和算法被攻克,那么被加密的类也很轻易被解密。这种保护方式示意图见图2。

图2 对Class文件进行加密示意图转换成本地代码将程序转换成本地代码也是一种防止反编译的有效方法。因为本地代码往往难以被反编译。开发人员可以选择将整个应用程序转换成本地代码,也可以选择要害模块转换。假如仅仅转换要害部分模块,Java程序在使用这些模块时,需要使用JNI技术进行调用。

当然,在使用这种技术保护Java程序的同时,也牺牲了Java的跨平台特性。对于不同的平台,我们需要维护不同版本的本地代码,这将加重软件支持和维护的工作。不过对于一些要害的模块,有时这种方案往往是必要的。

为了保证这些本地代码不被修改和替代,通常需要对这些代码进行数字签名。在使用这些本地代码之前,往往需要对这些本地代码进行认证,确保这些代码没有被黑客更改。假如签名检查通过,则调用相关JNI方法。这种保护方式示意图见图3。

代码混淆 图3 转换成本地代码示意图代码混淆是对Class文件进行重新组织和处理,使得处理后的代码与处理前代码完成相同的功能(语义)。但是混淆后的代码很难被反编译,即反编译后得出的代码是非常难懂、晦涩的,因此反编译人员很难得出程序的真正语义。从理论上来说,黑客假如有足够的时间,被混淆的代码仍然可能被破解,甚至目前有些人正在研制反混淆的工具。但是从实际情况来看,由于混淆技术的多元化发展,混淆理论的成熟,经过混淆的Java代码还是能够很好地防止反编译。下面我们会具体介绍混淆技术,因为混淆是一种保护Java程序的重要技术。图4是代码混淆的示意图。

图4 代码混淆示意图几种技术的总结以上几种技术都有不同的应用环境,各自都有自己的弱点,表1是相关特点的比较。

混淆技术介绍表1 不同保护技术比较表 到目前为止,对于Java程序的保护,混淆技术还是最基本的保护方法。Java混淆工具也非常多,包括商业的、免费的、开放源代码的。Sun公司也提供了自己的混淆工具。它们大多都是对Class文件进行混淆处理,也有少量工具首先对源代码进行处理,然后再对Class进行处理,这样加大了混淆处理的力度。目前,商业上比较成功的混淆工具包括JProof公司的1stBarrier系列、Eastridge公司的JShrink和

4thpass.com

的SourceGuard等。主要的混淆技术按照混淆目标可以进行如下分类,它们分别为符号混淆(Lexical Obfuscation)、数据混淆(Data Obfuscation)、控制混淆(Control Obfuscation)、预防性混淆(Prevent Transformation)。

符号混淆在Class中存在许多与程序执行本身无关的信息,例如方法名称、变量名称,这些符号的名称往往带有一定的含义。例如某个方法名为getKeyLength(),那么这个方法很可能就是用来返回Key的长度。符号混淆就是将这些信息打乱,把这些信息变成无任何意义的表示,例如将所有的变量从vairant_001开始编号;对于所有的方法从method_001开始编号。这将对反编译带来一定的困难。对于私有函数、局部变量,通常可以改变它们的符号,而不影响程序的运行。但是对于一些接口名称、公有函数、成员变量,假如有其它外部模块需要引用这些符号,我们往往需要保留这些名称,否则外部模块找不到这些名称的方法和变量。因此,多数的混淆工具对于符号混淆,都提供了丰富的选项,让用户选择是否、如何进行符号混淆。

数据混淆 图5 改变数据访问数据混淆是对程序使用的数据进行混淆。混淆的方法也有多种,主要可以分为改变数据存储及编码(Store and Encode Transform)、改变数据访问(Access Transform)。

改变数据存储和编码可以打乱程序使用的数据存储方式。例如将一个有10个成员的数组,拆开为10个变量,并且打乱这些变量的名字;将一个两维数组转化为一个一维数组等。对于一些复杂的数据结构,我们将打乱它的数据结构,例如用多个类代替一个复杂的类等。

另外一种方式是改变数据访问。例如访问数组的下标时,我们可以进行一定的计算,图5就是一个例子。

在实践混淆处理中,这两种方法通常是综合使用的,在打乱数据存储的同时,也打乱数据访问的方式。经过对数据混淆,程序的语义变得复杂了,这样增大了反编译的难度。

控制混淆控制混淆就是对程序的控制流进行混淆,使得程序的控制流更加难以反编译,通常控制流的改变需要增加一些额外的计算和控制流,因此在性能上会给程序带来一定的负面影响。有时,需要在程序的性能和混淆程度之间进行权衡。控制混淆的技术最为复杂,技巧也最多。这些技术可以分为如下几类:增加混淆控制 通过增加额外的、复杂的控制流,可以将程序原来的语义隐藏起来。例如,对于按次序执行的两个语句A、B,我们可以增加一个控制条件,以决定B的执行。通过这种方式加大反汇编的难度。但是所有的干扰控制都不应该影响B的执行。图6就给出三种方式,为这个例子增加混淆控制。

图6 增加混淆控制的三种方式控制流重组 重组控制流也是重要的混淆方法。例如,程序调用一个方法,在混淆后,可以将该方法代码嵌入到调用程序当中。反过来,程

一、用属性代替可访问的字段

1、.NET数据绑定只支持数据绑定,使用属性可以获得数据绑定的好处;

2、在属性的get和set访问器重可使用lock添加多线程的支持。

二、readonly(运行时常量)和const(编译时常量)

1、const只可用于基元类型、枚举、字符串,而readonly则可以是任何的类型;

2、const在编译时将替换成具体的常量,这样如果在引用中同时使用了const和readonly两种值,则对readonly的再次改变将会改变设计的初衷,这是需要重新编译所更改的程序集,以重新引用新的常量值。

3、const比readonly效率高,但失去了应用的灵活性。

三、is与as

1、两者都是在运行时进行类型的转换,as *** 作符只能使用在引用类型,而is可以使用值和引用类型;

2、通常的做法是用is判断类型,然后选择使用as或强类型转换 *** 作符(用operater定义的转换)有选择地进行。

四、ConditionalAttribute代替#if #endif条件编译

1、ConditionalAttribute只用于方法级,对其他的如类型、属性等的添加都是无效的;而#if #endif则不受此限制;

2、ConditionalAttribute可以添加多个编译条件的或(OR) *** 作,而#if #endif则可以添加与(AND)[这里可以完全定义为另一个单独的符号];

3、ConditioanlAttribute定义可以放在一个单独的方法中,使得程序更为灵活。

五、提供ToString()方法

1、可以更友好的方式提供用户详细的信息;

2、使用IFormatter.ToString()方法提供更灵活的定制,如果添加IFormatProvider 和ICustomFormatter接口则更有意义的定制消息输出。

六、值和引用类型的区别

1、值类型不支持多态,适合存储应用程序 *** 作的数据,而引用则支持多态,适用于定义应用程序的行为;

2、对于数组定义为值类型可以显著提高程序的性能;

3、值类型具有较少的堆内存碎片、内存垃圾和间接访问时间,其在方法中的返回是以复制的方式进行,避免暴露内部结构到外界;

4、值类型应用在如下的场景中:类型的职责主要是用于数据存储;公共接口完全由一些数据成员存取属性定义;永远没有子类;永远没有多态行为。

七、值类型尽可能实现为常量性和原子性的类型

1、使我们的代码更易于编写和维护;

2、初始化常量的三种策略:在构造中;工厂方法;构造一个可变的辅助类(如StringBuilder)。

八、确保0为值得有效状态

1、值类型的默认状态应为0;

2、枚举类型的0不应为无效的状态;在FlagsAttribute是应确保0值为有效地状态;

3、在字符串为为空时可以返回一个string.Empty的空字符串;

九、相等判断的多种表示关系

1、ReferenceEquals()判断引用相等,需要两个是引用同一个对象时方可返回true;

2、静态的Equals()方法先进性引用判断再进行值类型判断的;

3、对于引用类型的判断可以在使用值语义时使用重写Equals()方法;

4、重写Equals()方法时也应当重写GetHashCode()方法,同时提供operater==() *** 作。

十、理解GetHashCode()方法的缺陷

1、GetHashCode()仅应用在基于散列的集合定义键的散列值,如HashTable或Dictionary;

2、GetHashCode()应当遵循相应的三条规则:两个相等对象应当返回相同的散列码;应当是一个实例不变式;散列函数应该在所有的整数中产生一个随机的分布;

十一、优先使用foreach循环语句

1、foreach可以消除编译器对for循环对数组边界的检查;

2、foreach的循环变量是只读的,且存在一个显式的转换,在集合对象的对象类型不正确时抛出异常;

3、foreach使用的集合需要有:具备公有的GetEnumberator()方法;显式实现了IEnumberable接口;实现了IEnumerator接口;

4、foreach可以带来资源管理的好处,因为如果编译器可以确定IDisposable接口时可以使用优化的try…finally块;

十二、默认字段的初始化优于赋值语句

1、字段生命默认会将值类型初始化为0,引用类型初始化为null;

2、对同一个对象进行多次初始化会降低代码的执行效率;

3、将字段的初始化放到构造器中有利于进行异常处理。

十三、使用静态构造器初始化静态成员

1、静态构造器会在一个类的任何方法、变量或者属性访问之前执行;

2、静态字段同样会在静态构造器之前运行,同时静态构造器有利于异常处理。

十四、利用构造器链(在.NET 4.0已经用可选参数解决了这个问题)

1、用this将初始化工作交给另一个构造器,用base调用基类的构造器;

2、类型实例的 *** 作顺序是:将所有的静态字段都设置为0;执行静态字段初始化器;执行基类的静态构造器;执行当前类型的静态构造器;

将所有的实例字段设置为0;执行实例字段初始化器;执行合适的基类实例构造器;执行当前类型的实例构造器。

十五、利用using和try/finally语句来清理资源

在IDisposable接口的Dispose()方法中用GC.SuppressFinalize()可通知垃圾收集器不再执行终结 *** 作。

十六、尽量减少内存垃圾

1、分配和销毁一个对上的对象都要花费额外的处理器时间;

2、减少分配对象数量的技巧:经常使用的局部变量提升为字段;提供一个类,用于存储Singleton对象来表达特定类型的常用实例。

3、用StringBuilder进行复杂的字符串 *** 作。

十七、尽量减少装箱和拆箱

1、关注一个类型到System.Object的隐式转换,同时值类型不应该被替换为System.Object类型;

2、使用接口而不是使用类型可以避免装箱,即将值类型从接口实现,然后通过接口调用成员。

十八、实现标准Dispose模式

1、使用非内存资源,它必须有一个终结器,垃圾收集器在完成没有终结其的内存对象后会将实现了终结器对象的添加到终结队列中,然后垃圾收集器会启动一个新的线程来运行这些对象上的终结器,这种防御性的变成方式是因为如果用户忘记了调用Dispose()方法,垃圾回收器总是会调用终结器方法的,这样可以避免出现非托管的内存资源不被释放引起内存泄漏的问题;

2、使用IDisposable.Dispose()方法需要做四个方面的工作:释放所有的非托管资源;释放所有的托管资源;设置一个状态标记来表示是否已经执行了Dispose();调用GC.SuppressFinalize(this)取消对象的终结 *** 作;

3、为需要多态的类型添加一个受保护的虚方法Dispose(),派生类通过重写这个方法来释放自己的任务;

4、在需要IDisoposable接口的类型中,即使我们不需要一个终结器也应该实现一个终结器。

十九、定义并实现接口优于继承类型

1、不相关的类型可以共同实现一个共同的接口,而且实现接口比继承更容易;

2、接口比较稳定,他将一组功能封装在一个接口中,作为其他类型的实现合同,而基类则可以随着时间的推移进行扩展。

二十、明辨接口实现和虚方法重写

1、在基类中实现一个接口时,派生类需要使用new来隐藏对基类方法的使用;

2、可以将基类接口的方法申明为虚方法,然后再派生类中实现。

二十一、使用委托表达回调

1、委托对象本身不提供任何异常捕获,所以任何的多播委托调用都会结束整个调用链;

2、通过显示调用委托链上的每个委托目标可以避免多播委托仅返回最后一个委托的输出。

二十二、使用事件定义外部接口

1、应当声明为共有的事件,让编译器为我们创建add和renmove方法;

2、使用System.ComponentModel.EventHandlerList容器来存储各个事件处理器,在类型中包含大量事件时可以使用他来隐藏所有事件的复杂性。

二十三、避免返回内部类对象的引用

1、由于值类型对象的访问会创建一个该对象的副本,所以定义一个值类型的的属性完全不会改变类型对象内部的状态;

2、常量类型可以避免改变对象的状态;

3、定义接口将访问限制在一个子集中从而最小化对对象内部状态的破坏;

4、定义一个包装器对象来限制另一个对象的访问;

5、希望客户代码更改内部数据元素时可以实现Observer模式,以使对象可以对更改进行校验或相应。

二十四、声明式编程优于命令式编程

可以避免在多个类似的手工编写的算法中犯错误的可能性,并提供清晰和可读的代码。

二十五、尽可能将类型实现为可序列化的类型

1、类型表示的不是UI控件、窗口或者表单,都应使类型支持序列化;

2、在添加了NonSerializedAttribute的反序列化的属性时可以通过实现IDeserializationCallback的OnDeserialization()方法装入默认值;

3、在版本控制中可以使用ISerializable接口来进行灵活的控制,同时提供一个序列化的构造器来根据流中的数据初始化对象,在实现时还要求SerializationFormatter异常的许可。

4、如果需要创建派生类则需要提供一个挂钩方法供派生类使用。

二十六、使用IComparable和IComparer接口实现排序关系

1、IComparable接口用于为类型实现最自然的排序关系,重载四个比较 *** 作符,可以提供一个重载版的CompareTo()方法,让其接受具体类型作为参数;

2、IComparer用于提供有别于IComparable的排序关系,或者为我们提供类型本身说没有实现的排序关系。

二十七、避免ICloneable接口

1、对于值类型永远不需要支持ICloneable接口使用默认的赋值 *** 作即可;

2、对于可能需要支持ICloneable接口的基类,应该为其创造一个受保护的复制构造器,并应当避免支持IConeable接口。

二十八、避免强制转换 *** 作符

通过使用构造器来代替转换 *** 作符可以使转换工作变得更清晰,由于在转换后使用的临时对象,容易导致一些诡异的BUG。

二十九、只有当新版积累导致问题是才考虑使用new修饰符

三十、尽可能实现CLS兼容的程序集

1、创建一个兼容的程序集需要遵循两条规则:程序集中所有公有和受保护成员所使用的参数和返回值类型都必须与CLS兼容;任何与CLS不兼容的公有和受保护成员都必须有一个与CLS兼容的替代品;

2、可以通过显式实现接口来避开CLS兼容类型检查,及CLSCompliantAttribute不会检查私有的成员的CLS兼容性。

三十一、尽可能实现短小简洁的方法

1、JIT编译器以方法为单位进行编译,没有被调用的方法不会被JIT编译;

2、如果将较长的Switch中的Case语句的代码替换成一个一个的方法,则JIT编译器所节省的时间将成倍增加;

3、短小精悍的方法并选择较少的局部变量可以获得优化的寄存器使用;

4、方法内的控制分支越少,JIT编译器越容易将变量放入寄存器。

三十二、尽可能实现小尺寸、高内聚的程序集

1、将所有的公有类以及共用的基类放到一些程序集中,把为公有类提供功能的工具类也放入同样的程序集中,把相关的公有接口打包到他们自己的程序集中,最后处理遍布应用程序中水平位置的类;

2、原则上创建两种组件:一种为小而聚合、具有某项特定功能的程序集,另一种为大而宽、包含共用功能的程序集。

三十三、限制类型的可见性

1、使用接口来暴露类型的功能,可以使我们更方便地创建内部类,同时又不会限制他们在程序集外的可用性;

2、向外暴露的公有类型越少,未来扩展和更改实现所拥有的选择就越多。

三十四、创建大粒度的Web API

这是在机器之间的交易的频率和载荷都降到最低,将大的 *** 作和细粒度的执行放到服务器执行。

三十五、重写优于事件处理器

1、一个事件处理器抛出异常,则事件链上的其他处理器将不会被调用,而重写的虚方法则不会出现这种情况;

2、重写要比关联事件处理器高效得多,事件处理器需要迭代整个请求列表,这样占用了更多的CPU时间;

3、事件能在运行时响应,具有更多的灵活性,可以对同一个事件关联多个响应;

4、通行的规则是处理一个派生类的事件是,重写方式较好。

三十六、合理使用.NET运行时诊断

1、System.Diagnostics.DebugTraceEventLog为运行时提供了程序添加诊断信息所需要的所有工具,EventLog提供入口时的应用程序能写到系统事件日志中;

2、最后不要写自己的诊断库,.NET FCL 已经拥有了我们需要的核心库。

三十七、使用标准配置机制

1、.NET框架的System.Windows.Application类为我们定义了建立通用配置路径的属性;

2、Application.LocalAppDataPath和Application.userDataPath 会生成本地数据目录和用户数据的路径名;

3、不要在ProgramFiles和Windows系统目录中写入数据,这些位置需要更高的安全权限,不要指望用户拥有写入的权限。

三十八、定制和支持数据绑定

1、BindingMananger和CurrencyManager这两个对象实现了控件和数据源之间的数据传输;

2、数据绑定的优势:使用数据绑定要比编写自己的代码简单得多;应该将它用于文本数据项之外的范围-其他显示属性也可以被绑定;对于Windowos Forms 数据绑定能够处理多个控件同步的检查相关数据源;

3、在对象不支持所需的属性时可以通过屏蔽当前的对象然后添加一个想要的对象来支持数据绑定。

三十九、使用.NET验证

1、ASP.NET中有五种控件来验证有效性,可以用CustomValidator派生一个新类来增加自己的认证器;

2、Windows验证需要子System.Windows.Forms.Control.Validating些一个事件处理器。

四十、根据需要选用恰当的集合

1、数组有两个比较明显的缺陷:不能动态的调整大小;调整大小非常耗时;

2、ArrayList混合了一维数组和链表的特征,Queue和Stack是建立在Array基础上的特殊数组;

3、当程序更加灵活的添加和删除项时,可以使更加健壮的集合类型,当创建一个模拟集合的类时,应当为其实现索引器和IEnumberable接口。

四十一、DataSet优于自定义结构

1、DataSet有两个缺点个:使用XML序列化机制的DataSet与非.NET 代码之间的交互不是很好;DataSet是一个非常通用的容器;

2、强类型的DataSet打破了更多的设计规则,其获得的开发效率要远远高于自己编写的看上去更为优雅的设计。

四十二、利用特性简化反射

通过设计和实现特性类,强制开发人员用他们来声明可被动态使用的类型、方法和属性,可以减少应用程序的运行时错误,提高软件的用户满意度。

四十三、避免过度使用反射

1、Invoke成员使用的参数和返回值都是System.Object,在运行时进行类型的转换,但出现问题的可能性也变得更多了;

2、接口使我们可以得到一个更为清晰、也更具可维护性的系统,反射式一个很强大的晚期绑定机制.NET框架使用它来实现Windows控件和Web控件的数据绑定。

四十四、为应用程序创建特定的异常类

1、需要不同的异常类的唯一原因是让用户在编写catch处理器时能够方便地对不同的错误采取不同的做法;

2、可能有不同的修复行为时我们才应该创建多种不同的异常类,通过提供异常基类所支持的所有构造器,可以为应用程序创建功能完整的异常类,使用InnerException属性可以保存更低级别错误条件所产生的所有错误信息。

四十五、优先选择异常安全保证

1、强异常保证在从异常中恢复和简化异常处理之间提供了最好的平衡,在 *** 作因为异常而中断,程序的状态保留不变;

2、对将要修改的数据做防御性的复制,对这些数据的防御性复制进行修改,这中间的 *** 作可能会引发异常,将临时的副本和原对象进行交换;

3、终结器、Dispose()方法和委托对象所绑定的目标方法在任何情况下都应当确保他们不会抛出异常。

四十六、最小化互 *** 作

1、互 *** 作有三个方面的代价:数据在托管堆和非托管堆之间的列举成本,托管代码和非托管代码之间切换的成本,对开发人员来说与混合环境打交道的开发工作;

2、在interop中使用blittable类型可以有效地在托管和非托管环境中来回复制,而不受对象内部结构的影响;

3、使用In/Out特性来确保最贴切的不必要的多次复制,通过声明数据如何被列举来提高性能;

4、使用COM Interop用最简单的方式实现和COM组件的互 *** 作,使用P/Invoke调用Win32 API,或者使用C++编译器的/CLR开关来混合托管和非托管的代码;

四十七、优先选择安全代码

1、尽可能的避免访问非托管内存,隔离存储不能防止来自托管代码和受信用户的访问;

2、程序集在Web上运行时可以考虑使用隔离存储,当某些算法确实需要更高的安全许可时,应该将那些代码隔离在一个单独的程序集中。

四十八、掌握相关工具与资源

1、使用NUnit建立自动单元测试(集成在VS2010 中了);

2、FXCop工具会获取程序集中的IL代码,并将其与异族编码规则和最佳实践对照分析,最后报告违例情况;

3、ILDasm是一个IL反汇编工具,可以帮助我们洞察细节;

4、Shared Source CLI是一个包含.NET框架内核和C#编译器的实现源码。

四十九、为C#2.0做准备(这个规则现在已经没什么意义了,毕竟现在已经到了4.0 )

五十、了解ECMA标准


欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/yw/11629164.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-05-17
下一篇 2023-05-17

发表评论

登录后才能评论

评论列表(0条)

保存