使用Java混淆工具yguard
来源:互联网 发布:mac如何放大图标 编辑:程序博客网 时间:2024/06/06 00:49
张昭平 MSN:zzpoak◎hotmail.com
在某些情况下,java开发者可能希望保护自己的劳动成果,防止自己编写的源代码被竞争对手或者其他组织和个人轻易获取而危害自己的利益,最简单有效的办法就是对编译后的java类文件进行混淆处理。本文介绍一款这样的工具yguard。
yGruard是一个功能比较强大的java类文件的混淆工具,特别适合与ant工具集成使用。
本文对yguard的基本元素做一些简单的介绍,并列举了一些简单的ant任务例子,在实际工程项目中可以参考这些样例。
1. 安装
在使用yGruard之前,必须首先确保系统中已经有可以正常使用的Ant工具。Ant工具的使用的介绍不属于本文的范畴。
然后在官方网站http://www.yworks.com/products/yguard下载yguard的工具包,解压出yguard.jar类库,yguard当前只有一个jar包。
解压出之后,把它放置在你的ant工具能够找到的路径中。你可以使用绝对路径,但是在下面的例子中。
为了在你现有的Ant任务中使用yguard进行混淆工作,你可以采取下边两种做法之一。一、你可以在你的build脚本中直接插入下面的代码片。
<taskdef name="obfuscate" classname="com.yworks.yguard.ObfuscatorTask"
classpath="yguard.jar"/>
<target name="obfuscate">
<obfuscate>
<!--根据你的需求修改 obfuscate元素属性 -->
</obfuscate>
</target>
二、你也可以把taskdef元素放置在obfuscate元素中,示例如下:
<target name="obfuscate">
<taskdef name="obfuscate"
classname="com.yworks.yguard.ObfuscatorTask" classpath="yguard.jar" />
<obfuscate>
<!--根据你的需求修改 obfuscate元素属性 -->
</obfuscate>
</target>
2. 创建初始ant任务
我们从经典的helloworld示例入手,逐步学习使用yguard混淆器工具。
2.1. 编写helloword程序
我们先编写一个简单的HelloWorld程序,代码如下:
package helloworld;
public class HelloWorld
{
public String publicAttribute = "i am a public attrubute.";
protected String protectedAttribute = "i am a protected attrubute.";
private String privateAttribute = "i am a private attrubute.";
public void publicSayHello()
{
System.out.println("hello world in public . ");
}
protected void protectedSayHello()
{
System.out.println("hello world in protected . ");
}
private void privateSayHello()
{
System.out.println("hello world in private . ");
}
public static void main(String args[])
{
HelloWorld hello = new HelloWorld();
hello.publicSayHello();
}
}
这个HelloWorld程序有不同可见性的属性,不同可见性的方法。在下面的过程中,我们将逐步演示yguard的强大功能。
2.2. 创建ant任务
创建一个初始的ant脚本任务。
<?xml version="1.0" encoding="UTF-8"?>
<project name="project" default="init" basedir=".">
<!-- 设置我们Ant任务的初始化工作 -->
<target name="init">
<!--工程名称为helloworld-->
<property name="project_name" value="helloworld" />
<!--java源文件所在路径是当前路径的src-->
<property name="srcDir" value="." />
<!--编译生成的class文件在当前路径的classes目录下-->
<property name="classDir" value="classes" />
<!--jar包名称为本工程的名称加上.jar后缀名-->
<property name="jar" value="${project_name}_temp.jar" />
<!--jar包名称为本工程的名称加上.jar后缀名-->
<property name="obfjar" value="${project_name}.jar" />
<!--yguard混淆器工作产生的日志文件-->
<property name="obfuscationlog" value="${project_name}_obf_log.xml" />
<mkdir dir="${classDir}" />
</target>
<!-- 编译 -->
<target name="compile" depends="init">
<javac srcdir="${srcDir}" destdir="${classDir}">
</javac>
</target>
<!-- 打包 -->
<target name="jar" depends="compile">
<jar jarfile="${jar}" basedir="${classDir}" includes="helloworld/**">
</jar>
</target>
<!-- 删除所有已经编译的class文件 -->
<target name="clean" depends="init">
<delete dir="${classDir}" includeEmptyDirs="true" />
</target>
</project>
<!-- build.xml 文件结束 -->
2.3. 执行ant任务
执行这个脚本,可以看到控制台有如下相似的输出:
Buildfile: D:/work/lspworkspace/helloworld/build.xml
init:
init:
compile:
[javac] Compiling 1 source file to D:/work/lspworkspace/helloworld/classes
init:
compile:
[javac] Compiling 1 source file to D:/work/lspworkspace/helloworld/classes
jar:
[jar] Building jar: D:/work/lspworkspace/helloworld/helloworld_temp.jar
BUILD SUCCESSFUL
Total time: 4 seconds
如果你是完全copy的话,这个过程理论上是不会出错的,如果有错误,请考虑是不是你的环境配置问题。
3. 第一个混淆任务
3.1. 执行混淆任务
我们按照前面讲到的安装方法,在这个可执行的Ant任务中加入混淆任务,加入的具体内容如下:
<!-- 混淆任务 -->
<target name="obfuscate" depends="jar">
<taskdef name="obfuscate" classname="com.yworks.yguard.ObfuscatorTask" classpath="yguard.jar" />
<!-- 不同工程需要修改的地方 -->
<obfuscate mainclass="${mainclass}" logfile="${obfuscationlog}" replaceclassnamestrings="true">
<inoutpair in="${jar}" out="${obfjar}" />
</obfuscate>
</target>
我们执行这个修改过的ant任务,在设定的输出目录下,至少会产生如下几个文件,
helloworld.jar,混淆后的jar文件;
helloworld_temp.jar,混淆前的原始jar文件;
helloworld_obf_log.xml,产生的日志文件。
下面我们打开jar包,可以发现包名、类名混淆前后是不一样的,我们反编译其中class文件,
这是我们在混淆之前根据编译后的class文件反编译产生的源文件。
package helloworld;
import java.io.PrintStream;
public class HelloWorld
{
public HelloWorld()
{
publicAttribute = "i am a public attrubute.";
protectedAttribute = "i am a protected attrubute.";
privateAttribute = "i am a private attrubute.";
}
public void publicSayHello()
{
System.out.println("hello world in public . ");
}
protected void protectedSayHello()
{
System.out.println("hello world in protected . ");
}
private void privateSayHello()
{
System.out.println("hello world in private . ");
}
public static void main(String args[])
{
HelloWorld helloworld = new HelloWorld();
helloworld.publicSayHello();
}
public String publicAttribute;
protected String protectedAttribute;
private String privateAttribute;
}
这是我们根据混淆后的class文件产生的源代码,我们可以发现类的包名、方法名、属性名已经发生了变化。
package A;
import java.io.PrintStream;
public class A
{
public A()
{
B = "i am a public attrubute.";
A = "i am a protected attrubute.";
C = "i am a private attrubute.";
}
public void B()
{
System.out.println("hello world in public . ");
}
protected void C()
{
System.out.println("hello world in protected . ");
}
private void A()
{
System.out.println("hello world in private . ");
}
public static void A(String as[])
{
A a = new A();
a.B();
}
public String B;
protected String A;
private String C;
}
下面,我们来学习obfuscate及其相关元素。
4. Obfuscate元素
Obfuscate是整个混淆任务定义的元素,下面我们对它及其子元素进行详细的介绍。
4.1. Obfuscate元素
在当前版本中,Obfuscate元素有如下一些属性,
l
mainclass
:简写你的应用的主程序类名,主类的类名和它的主方法名都会被修改。你可能想仅仅暴露主方法(main),如果你的jar文件描述文件中MANIFEST.MF包含了
Main-Class
属性,yguard将会自动调整成混淆后的主类名。
l logfile:混淆过程产生的日志文件名称,这个日志文件包含了混淆过程的任何警告信息和映射信息。
l conservemanifest
:(取值为boolean类型-true/false),表示混淆器是否应改保持jar包的
manifest清单文件不变。缺省值为false,表示这个清单文件将会被混淆器修改用来反映新的信息摘要。
l
replaceClassNameStrings
:
(也是一个boolean属性值),设定yguard是否需要取代某些硬编码的字符串。(本文英文水平有限,这个属性没有理解清楚)。
4.2. inoutpair元素
inoutpair元素,每一个混淆任务(obfuscation task)都必须至少有一个inoutpair元素。该元素指定了要被混淆的源jar包和产生的目标jar包的名称和路径,要注意的是只有class文件能够被混淆,其他的如资源文件,只是简单的从源jar包copy到目标jar包。
In属性指定包含了需要被混淆jar包。
Out属性指定了混淆后产生的新jar包。
4.3.
Externalclasses
元素
如果这个被混淆的jar包需要依赖其他外部class文件或者jar包,externalclasses用来指定这些被依赖的实体的具体路径,这些实体不会被混淆。样例代码片如下:
<!--设置所有用到得第三方库,不混淆它们-->
<externalclasses>
<pathelement location="${third-party-lib-path}/log4j-1.2.8.jar" />
<pathelement location="${third-party-lib-path}/commons-logging-1.0.4.jar" />
<pathelement location="${third-party-lib-path}/jbossall-client.jar" />
<pathelement location="${third-party-lib-path}/commons-beanutils.jar" />
<pathelement location="${third-party-lib-path}/commons-pool.jar" />
</externalclasses>
4.4.
Property
元素
Property
元素用来给混淆引擎一些提示, 不同的
yGuard版本可能会有不同的提示,混淆任务可以根据这些提示来控制混淆的过程。
它有两个强制的属性:
l Name 混淆任务能够理解的一个特定键。
l Value 键对应的值。
Yguard 1.5.2.0_03,支持下面的属性:
l Error-checking属性告诉yguard有任何错误的时候就终止正常任务。当前可用的值为:
Pedantic 表示错误发生,混淆任务将失败。
l
Naming-scheme
属性告诉yguard在混淆过程中用到的命名模式,这个属性只能取下列值之一:
Ø
Small
产生尽可能短的名字,这能使产生的结果jar包尽可能的小。
Ø
Best
产生的名字在反编译后看起来可能象是被过去分词化,使用该模式产生的jar包不一定能在所有的文件系统下能够被成功的解压,而且使用该模式也会占用很多的空间使得结果jar很可能变的很大(通常是两倍左右大小)。
Ø
Mix
混合了前面两种模式,它产生的jar包有一个比较合理的大小,也很难反编译。
l
Language-conformity
属性告诉yguard产生大多数反编译器能够被反编译的名字,换而言之,yguard产生的class文件能够被今天多数虚拟机验证和执行;但是在反编译时会产生一些完全没用的垃圾语句来混淆。该属性当前可能的取值:
Ø
Compatible
产生的名字(包括java、jar、manifest文件),,大多数编译器都能理解,大多数文件系统能解压。
Ø
Legal
产生的名字一部分反编译器可以理解。
Ø
Illegal
产生的名字可能虚拟机工作正常,但是可能使一些工具(如jbuilder)崩溃。
l
Obfuscation-prefix
用指定的包名称混淆现在的包名称。
l
Expose-attributes
指明除标准属性之外的应该被暴露在外的属性列表,缺省情况下,yguard会从方法中删除不需要的属性,如:deprecated。这个值可能是一个逗号分割的列表,该列表在
Section 4.7 of the VM Specification of the .class File Format中定义。 保持“deprecated”的代名片如下:
<property name="expose-attributes" value="Deprecated"/>
4.5. Expose元素
Expose是obfuscate的子元素,它用来规定classes、methods、fields、attributes这些元素是否应该被暴露。
Obfuscator 任务可能会删除很多class文件不需要的信息,这些零散的信息可能不应该被反编译。有一些boolean值形式的属性来控制它们是否可以暴露,也就是不被混淆或删除。
这些设置将影响所有的class文件,控制这些属性的最好办法是在classes元素中使用attribute元素。
l
sourcefile 原始源文件的名称信息是否应该包含在被混淆的类中,缺省值为false,表示信息将被删除,如果取值为true,表示原始源文件的名称信息将保留,反编译后一般在文件头以注释的方式在头注释的最后一行出现。
l
Linenumbertable在混淆后的类文件中是否保存原始类文件的每个操作字节码对应的原始源文件行号信息的相关信息。缺省值为false,表示不保存。
l localvariabletable
决定是否保留本地变量表,这个表包含了每一个在在原始源代码中使用的本地变量名称和混淆后的类中变量名称的映射关系。缺省值为false,表示将删除该信息。
l Localvariabletypetable 是否保留本地变量类型表,这个表包含了在原始源代码中使用的本地变量的类型与混淆后的类中变量的映射关系。缺省值为false,不保存,也就是说,混淆过程中将删除该信息。
4.6.
Class
元素
Class
元素用来设定如何暴露类的相关信息,这些信息包括类的名称、方法的名称、属性的名称。下面的两个属性来决定特定的类混淆级别。
l
Methods
属性决定可见性在什么级别的方法可以被暴露。
l
Fields
属性决定可见性在何种级别的属性字段可以被暴露。
下表列出了属性可能的取值,以及对应于该值,类的哪些元素将被暴露。‘*’表示给定可见性的元素会被暴露,‘-’表示相应的元素将被混淆。
Value/Visibility
public
protected
friendly
private
none
-
-
-
-
public
*
-
-
-
protected
*
*
-
-
friendly
*
*
*
-
private
*
*
*
*
我们可以看到none表示所有的元素都会被混淆。
指定类的混淆有3中方式:
1. 使用完全限定的java类名指定特定的类。如:
<class name="mypackage.MyClass"/>
2.使用patternset元素设定多个java类,patternset的include和exclude元素必须符合java语法,允许使用通配符。如:
<class>
<patternset>
<include name="com.mycompany.**.*Bean"/>
<exclude name="com.mycompany.secretpackage.*"/>
<exclude name="com.mycompany.myapp.SecretBean"/>
</patternset>
</class>
上边的代码表示将暴露除com.mycompany.secretpackage包中类和com.mycompany.myapp.SecretBean类之外包com.mycompany及其子包中所有的类。
<class>
<patternset>
<include name="com.mycompany.myapp.MainClass"/>
<include name="org.w3c.sax?."/>
<exclude name="org.w3c.sax?.**.*$*"/>
</patternset>
</class>
这段代码片表示将暴露MainClass类,org.w3c.sax1, org.w3c.sax2, org.w3c.saxb这种命名形式的包中出内部类之外的所有类。
注:‘$’符号是java中外部类和内部类的分隔符,在ant中‘$’是一个特殊字符,如果我们想把‘$’字符作为参数传递给ant任务,必须使用连续两个‘$’也就是'$$'来表示。
3.最后一种方法是我们用类的可见性来设定,如:
<class classes="protected">
<patternset>
<include name="com.mycompany.myapi."/>
</patternset>
</class>
上例表示将暴露包com.mycompany.myapi及其子包中的所有public、protected元素(也就是说保留它们不变,不混淆这些元素)。
<class classes="protected" methods="protected"
fields="protected">
<patternset>
<include name="**.*"/>
</patternset>
</class>
这个例子演示了如何暴露public类型API的通用方法,表示所有的public和protected类型的类,以及它们中所有声明为public和protected的属性和方法都会被暴露。
最后一个例子演示了怎么只暴露类的公共方法,而不暴露其类名和属性。
<class classes="none" methods="public" fields="none">
<patternset>
<include name="com.mycompany.myapi."/>
</patternset>
</class>
4.7. Method 元素
Method用来设定被暴露的方法签名,一般我们用不到这样的级别。这个元素有两个属性:
l class
属性用来指定拥有该方法的类,通常使用java语法的完全限定类名,这个属性可以被忽略,
l name
属性指明了被暴露(不混淆)的方法。注意:方法的参数和返回类型必须是完全限定的java类名,即使是void类型。
一些例子如下:
<method class="com.mycompany.myapp.MyClass"
name="void main(java.lang.String[])"/>
<method class="com.mycompany.myapp.MyClass"
name="int foo(double[][], java.lang.Object)"/>
<method name="void writeObject(java.io.ObjectOutputStream)">
<patternset>
<include name="com.mycompany.myapp.data.*"/>
</patternset>
</method>
<method name="void readObject(java.io.ObjectInputStream)">
<patternset>
<include name="com.mycompany.myapp.data.*"/>
</patternset>
</method>
暴露MyClass 的main方法和foo方法,com.mycompany.myapp.data包中所有类的所有的writeObject和readObject方法都将被暴露,
4.8. Field元素
4.9.
Sourcefile
元素
Sourcefile 设定源文件可以暴露哪些信息,用在堆栈跟踪上,这个元素没有属性,
<sourcefile>
<patternset>
<include name="com.mycompany.myapp.**"/>
</patternset>
</sourcefile>
包com.mycompany.myapp及其子包中所有类的类名将被暴露,注意这将阻止正常的混淆,因为源文件的名字和未混淆的类名捆绑的很紧密。
<sourcefile>
<property name="mapping" value="y"/>
<patternset>
<include name="com.mycompany.myapp.**"/>
</patternset>
</sourcefile>
这将把包名称映射成一个字母y。 (mapping属性是当前版本sourcefile元素支持唯一属性)。
4.10.
Linenumbertable
元素
在上边expose元素中也有一个linenumbertable,在那里它是一个属性,在这里它指定哪些类的行号表将被保留而不被混淆,主要用在堆栈跟踪。
<linenumbertable>
<patternset>
<include name="com.mycompany.myapp.**"/>
</patternset>
</linenumbertable>
这将暴露com.mycompany.myapp及其子包中所有类的行号,这些类的sourcefile
属性也必须被暴露,否则,JVM将显示未知的行号信息Unknown source
。
<linenumbertable>
<property name="mapping-scheme" value="scramble"/>
<property name="scrambling-salt" value="1234"/>
<patternset id="CompanyPatternSet">
<include name="com.mycompany.myapp.**"/>
</patternset>
</linenumbertable>
<sourcefile>
<property name="mapping" value="y"/>
<patternset refid="CompanyPatternSet"/>
</sourcefile>
4.11.
Adjust
元素
Adjust元素主要是把资源文件中引用的被混淆的类的名称作适当的调整,这是一个比较有用的元素。
注意:它将仅仅调整inoutpair中out-jar元素设定的包,也就是混淆过后生成的jar包。它有3个属性:
l
replaceName 属性设定资源名称是否应该被调整,缺省值为false;
l
replaceContent设定资源的内容是否应该被调整,缺省值为false;
l
replacePath 设定资源文件的路径是否应该被调整,缺省值为true。
一些例子如下:
<!--调整jar包中所有java属性文件的名称 -->
<adjust replaceName="true">
<include name="**/*.properties" />
</adjust>
<!--调整jar包的某个xml文件中的引用的类名 -->
<adjust file="plugins.xml" replaceContent="true" />
<!--不调整jar包中资源文件的路径com/mycompany/myapp/resource. -->
<!--包 com.mycomSpany.myapp 也可能被混淆 -->
<adjust replacePath="false">
<include name="com/mycompany/myapp/resource/*" />
</adjust>
5. 一个实际项目中的例子
这个例子可以作为我们大多数普通项目的模板,根据情况稍加修改就可以了。
在我的项目中,我有3个文件夹:
l Src-lib 保存未混淆前的jar包,我的文件夹下有framework-common.jar,framework-ejb.jar,cbm-common.jar,cbm-ejb.jar四个jar包需要混淆;
l Target-lib 放置混淆后产生的jar包;
l third-party-lib 放置我的项目用到的第三方库,以及yguard混淆工具库。
另外有一个定义ant任务的build.xml文件,文件内容如下:
<?xml version="1.0" encoding="gb2312"?>
<project name="project" default="init" basedir=".">
<!-- 设置我们Ant任务的初始化工作 -->
<target name="init">
<!--工程名称-->
<property name="project_name" value="cpht" />
<!--源jar包所在路径-->
<property name="src-lib" value="src-lib" />
<!--目标jar包所在得路径-->
<property name="target-lib" value="target-lib" />
<!--yguard混淆器工作产生的日志文件-->
<property name="obfuscationlog" value="${project_name}_obf_log.xml" />
<!--第三方库所在的路径-->
<property name="third-party-lib-path" value="third-party-lib" />
</target>
<!-- ***********************************混淆任务 **************************-->
<target name="obfuscate">
<taskdef name="obfuscate" classname="com.yworks.yguard.ObfuscatorTask" classpath="${third-party-lib-path}/yguard.jar" />
<!-- 不同工程需要修改的地方 -->
<obfuscate logfile="${obfuscationlog}" >
<!--有错误时将中断该target-->
<property name="error-checking" value="pedantic" />
<property name="language-conformity" value="illegal" />
<!--采用混合方式产生名字-->
<property name="naming-scheme" value="mix" />
<!--设置需要暴露哪些信息,这些信息将不会被混淆-->
<expose>
<!--仅保持类名不变,完全混淆类的其他信息-->
<class classes="private" methods="none" fields="none">
<patternset id="catic">
<include name="com.catic.**" />
<include name="catic.**" />
</patternset>
</class>
<!-- 保持所有的属性(过时的方法,文件名,行号、本地变量表),在系统允许过程中,可能会发生错误信息,
如果不保存这些信息,出错时很难跟踪代码,为方便调试保留了这些信息 -->
<attribute name="Deprecated, SourceFile, LineNumberTable, LocalVariableTable">
<patternset refid="catic" />
</attribute>
</expose>
<!--设置所有用到得第三方库,不混淆它们-->
<externalclasses>
<pathelement location="${third-party-lib-path}/log4j-1.2.8.jar" />
<pathelement location="${third-party-lib-path}/commons-logging-1.0.4.jar" />
<pathelement location="${third-party-lib-path}/jbossall-client.jar" />
<pathelement location="${third-party-lib-path}/commons-beanutils.jar" />
<pathelement location="${third-party-lib-path}/commons-pool.jar" />
</externalclasses>
<!--输入和输出jar包,可以有多行-->
<!--*********************输入、输出设置开始*************************-->
<inoutpair in="${src-lib}/framework-common.jar" out="${target-lib}/framework-common-obf.jar" />
<inoutpair in="${src-lib}/framework-ejb.jar" out="${target-lib}/framework-ejb-obf.jar" />
<inoutpair in="${src-lib}/cbm-common.jar" out="${target-lib}/cbm-common-obf.jar" />
<inoutpair in="${src-lib}/cbm-ejb.jar" out="${target-lib}/cbm-ejb-obf.jar" />
<!--*********************输入、输出设置结束*************************-->
<!--调整jar包的资源文件中的引用的类名 -->
<adjust replaceContent="true">
<include name="**/*.xml" />
<include name="./**/*.xml" />
</adjust>
</obfuscate>
<!-- 不同工程需要修改的地方 -->
</target>
<!-- ***********************************混淆任务结束 ************************-->
</project>
<!-- build.xml 文件结束 -->
第一target是设置环境变量,第二个target是执行混淆任务。
<property name="naming-scheme" value="mix" />配置混淆时产生的名字的方式为混合方式,我们前面讲到有3中方式small,best,mix,这里我们选择mix。
Expose元素设置了我们只保持类名不变,类的方法和属性需要混淆,在系统运行过程中,可能会发生一些意想不到的错误信息,如果完全混淆类,出错时很难跟踪代码,为方便调试保留了类名、行号表、本地变量表等这些信息。
Externalclasses 元素指定了我们使用到的第三方库。
Inoutpair元素设置我们要混淆的jar包,以及混淆后产生的对应jar包名称和路径。
Adjust元素调整资源文件中引用的类信息为我们混淆后的类信息。
6. 结束语
本文只是简单的介绍了yguard的使用,更多用法请参考官方相关的文档。
一般来讲,我们只需要作这些简单事情就足够了,因为没有任何文档参考的情况下,没有混淆的代码已经不容易看懂了,如果再稍微混淆一下,相信java世界中,没有多少人会真正花大量的精力去阅读这种经混淆后再反编译产生的代码。
- 使用Java混淆工具yguard
- 使用Java混淆工具yguard
- 使用Java混淆工具yguard
- 使用Java混淆工具yguard
- 使用Java混淆工具yguard
- 使用Java混淆工具yguard
- JAVA代码混淆-yguard ant配置
- 使用yguard混淆,名字出现超长字符 map=“ooooooooooooooo”
- java代码混淆工具jocky的使用
- 使用ProGround混淆工具混淆jar包
- 使用yGuard保护你的代码
- JAVA混淆编译工具总汇
- android sdk java混淆工具
- Zelix KlassMaster混淆工具使用摘要
- DotNetReactor使用.net代码混淆工具
- 混淆——Proguard工具的使用
- DotNetReactor使用.net代码混淆工具
- Java 反编译工具 和 混淆器
- Error importing repomd.xml for updates: Damaged repomd.xml file 解决方法
- 优先使用const 而不是 define
- socket客户端
- linux磁盘容量变为64Z
- VI中的多行删除与复制
- 使用Java混淆工具yguard
- 【男人“例假”症状】
- 10个快速开发Android/Iphone web App应用的框架
- Cannot retrieve metalink for repository: fedora. Please verify its path and try again 解决方法
- Memory Pool 预习知识-Windows内存管理
- SQL多条件查询拼接in中条件方法
- 防止SQL注入解决方案
- 统计整个工程代码行数
- 亚马逊 $79Kindle软件升级 纸质阅读体验更强