阅读:5758次
评论:3条
更新时间:2011-05-26
--------------------------------------------------------------
一、任务描述
我们的一个Web项目,我一开始就打算用RoR来开发,毕竟开发效率会比java高很多。但是这个项目,不是运行在我们自己的服务器上,而是跑在客户的机器上的。
一开始我也没怎么在意,毕竟客户的技术能力不强,未必他们就看得懂我们的代码。但是老板不这么想,他希望我能够想出某种办法来,给我们的代码加密。
我们的项目,一开始是基于jruby 1.0.3的,我自己简单弄了一下,就把加密搞出来了,现在要写这篇文档,我就打算修改最新的jruby 1.1.6的代码,来尝试一下。
一开始我也没怎么在意,毕竟客户的技术能力不强,未必他们就看得懂我们的代码。但是老板不这么想,他希望我能够想出某种办法来,给我们的代码加密。
我们的项目,一开始是基于jruby 1.0.3的,我自己简单弄了一下,就把加密搞出来了,现在要写这篇文档,我就打算修改最新的jruby 1.1.6的代码,来尝试一下。
二、环境搭建
1、到jruby的网站,下载他的源代码,并解压缩到E:\RubyWork下
http://dist.codehaus.org/jruby/1.1.6/jruby-src-1.1.6.tar.gz
2、在NetBeans 6.5中打开这个项目
jruby在java的开源项目里,是做得非常棒的一个,从他能够这么容易的被NetBeans打开,就可以看出来。
当然,我猜,开发jruby的人,十有八九,也都是用的NetBeans吧。
3、解决第一个麻烦
在打开项目之后,我发现,有一个类org.jruby.anno.AnnotationBinder无法被编译。因为缺少一组lib。
而我根本不知道这些lib,是干什么用的。只能动用Google大法了。
这么看来,这是一个叫做Mirror API的东西。继续搜。。。
点击第一个看起来就很像的link。
然后,就可以下载到一个apt-mirror-api.tar的文件了。
解压缩之后,复制到JRuby的source目录里去。
OK,麻烦解决了。
3、第二个小麻烦
在Test文件夹里,还有一些类,无法正确编译,打开一看,是有些Source里的类,他这里找不到。看来是因为Source没有经过完全的编译。
NetBeans里调用Ant Task,还是非常方便的。
然后,整个项目里的红颜色的叹号,就会消失了,这样心里面会感觉舒服很多
继续调用Ant的jar任务,如下图所示:
于是,我们就得到了自己编译出来的一个jruby.jar。以后要对jruby再做什么修改,只要再执行一次jar任务,就OK了。
4、以上步骤的目的,主要是为了确认,在我们的开发环境上,能够继续修改人家的源代码,而且,在修改之后,能够切实的用上。
http://dist.codehaus.org/jruby/1.1.6/jruby-src-1.1.6.tar.gz
2、在NetBeans 6.5中打开这个项目
jruby在java的开源项目里,是做得非常棒的一个,从他能够这么容易的被NetBeans打开,就可以看出来。
当然,我猜,开发jruby的人,十有八九,也都是用的NetBeans吧。
3、解决第一个麻烦
在打开项目之后,我发现,有一个类org.jruby.anno.AnnotationBinder无法被编译。因为缺少一组lib。
引用
com.sun.mirror.apt.*
com.sun.mirror.declaration.*
com.sun.mirror.util.*
com.sun.mirror.declaration.*
com.sun.mirror.util.*
而我根本不知道这些lib,是干什么用的。只能动用Google大法了。
这么看来,这是一个叫做Mirror API的东西。继续搜。。。
点击第一个看起来就很像的link。
然后,就可以下载到一个apt-mirror-api.tar的文件了。
解压缩之后,复制到JRuby的source目录里去。
OK,麻烦解决了。
3、第二个小麻烦
在Test文件夹里,还有一些类,无法正确编译,打开一看,是有些Source里的类,他这里找不到。看来是因为Source没有经过完全的编译。
NetBeans里调用Ant Task,还是非常方便的。
然后,整个项目里的红颜色的叹号,就会消失了,这样心里面会感觉舒服很多
继续调用Ant的jar任务,如下图所示:
于是,我们就得到了自己编译出来的一个jruby.jar。以后要对jruby再做什么修改,只要再执行一次jar任务,就OK了。
4、以上步骤的目的,主要是为了确认,在我们的开发环境上,能够继续修改人家的源代码,而且,在修改之后,能够切实的用上。
三、代码阅读
jruby项目,发展到了1.1.6版,目前的代码已经太多了。仅仅/src/下的java代码,就有72个文件夹,715个文件。总的大小有7.40M。虽然不能称之为浩如烟海,但是要全部读完,也是非常难以想象的工作量。
因此,如何快速找到需要阅读的代码,成为一种重要的技巧。
1、浏览文件夹
如果一个开源代码组织得非常好,那么他的source就会有非常清晰、合理、明确的命名。jruby这方面做得确实不错。除了org.jruby下的82个文件之外,org.jruby下还有22个目录。其中最像的,自然是lexer,即词法分析器。
如果对编译原理略有了解(听说过一些要紧的名词即可),就会知道,编译或解释程序,都是首先从词法分析开始,然后是语法分析(Parser)。分析得到的结果,则是一个抽象语法树(AST),随后可以进行解释执行,或编译执行(Compiler)
因此,首先需要考虑的,就是lexer文件夹下的文件。
2、通过排除法,找到入手点
在lexer文件夹里,没有文件,倒是有一个yacc目录。这确实是一个奇怪的命名。yacc(Yet Another Compiler Compiler)。根据wikipedia的定义,yacc是一个用来生成编译器的编译器(编译器代码生成器)。在Unix/Linux上,yacc通常与词法分析器lex一起使用。你这个jruby,怎么会在lexer目录下,加一个叫做yacc的目录呢?
在yacc目录下,有18个文件,我们需要采用排除法,来缩减阅读范围。
以ISourcePosition开头的三个,都是interface,看不到实现代码的,先略过。
IDESourcePosition、IDESourcePositionFactory、SimplePositionFactory、SimpleSourcePosition,是前面几个接口的实现类,而且是关于定位的,可以先不看。
StringTerm、StrTerm、HeredocTerm、Token、是在词法分析中,需要用到的,可以先不看。term(名词,词素);token(记号,代号)。
SyntaxException,是一个异常类,不用看。
ByteListLexerSource、CapturingByteListLexerSource、InputStreamLexerSource、LexerSource,看上去很重要,大概可以从LexerSource开始分析。
RubyYaccLexer,看名字也很重要,需要看看。
3、RubyYaccLexer快扫
这个类里,比较引人注目的是:在class一开始的map定义。明显是一堆ruby语法相关的关键字。可以猜测,他会从某个Source读取文件内容,然后与ruby的关键词比对。
这是一个关键。
在RubyYaccLexer中,我们随处可以看到src.read();或者src.unread(c);这样的代码。可以看出:LexerSource就是我们要分析的关键代码了。
4、LexerSource分析
这个class的第一行,就让我改变了详细看看LexerSource的念头。这是一个抽象类。那么,传递给RubyYaccLexer的LexerSource,肯定是由某一个扩展了LexerSource的类创建的实例。总要先找到这些扩展类,才能搞清楚整个LexerSource。
这是NetBeans IDE提供的一个查找功能,选择查找所有的子类型,点击确定。
看来一共有三个类:ByteListLexerSource.java,CapturingByteListLexerSource.java,InputStreamLexerSource.java。
那么,究竟这三个类,分别在什么时候会被使用呢?再次动用NetBeans的查找功能:
初步判断:ByteListLexerSource.java,CapturingByteListLexerSource.java,就是两个死类,没有什么别的代码用到他们。因此,真正有用的,就只剩下两个类了:
LexerSource.java、InputStreamLexerSource.java
5、运用暴力法了解执行过程
在LexerSource里,我们可以看到上面这段代码,那么,InputStream content这个参数,是从哪里来的呢?
我在工作中,通常会采用暴力法,来做这个事情,相比debug,似乎更加快捷一些。
在getSource函数的第一行,加上这么一句:(new Exception()).printStackTrace();
然后,编译出一个新的jruby.jar来,运行一个ruby的hello world程序。
于是,整个调用序列,就一目了然了。
6、找到InputStream的源头
通过上面的抛出异常,我们可以倒着读回去:
org.jruby.parser.Parser.parse(Parser.java:122)
org.jruby.Ruby.parseFile(Ruby.java:1965)
org.jruby.Ruby.parseFile(Ruby.java:1969)
org.jruby.Ruby.parseFromMain(Ruby.java:364)
org.jruby.Ruby.runFromMain(Ruby.java:327)
org.jruby.Main.run(Main.java:214)
如此看来,还得去读config.getScriptSource()函数了。
org.jruby.RubyInstanceConfig.getScriptSource(RubyInstanceConfig.java:1067)
这个函数中,与我们的目的有关的,也就两行:
File file = JRubyFile.create(getCurrentDirectory(), getScriptFileName());
return new BufferedInputStream(new FileInputStream(file));
7、JRubyFile.create分析
通过JRubyFile.java的注释我们得知,这个类的作者是“Ola Bini”本人。可见他的水平,真的很次啊!
调用的JRubyFile.create的代码写的是:
File file = JRubyFile.create(getCurrentDirectory(), getScriptFileName());
非常清楚,第一个参数是当前路径,第二个参数是脚本文件名。
而在JRubyFile里,create的声明是这么写的:
public static JRubyFile create(String cwd, String pathname)
cwd我还能懂,pathname是什么?路径名?
根源在于,SUN的File.java也写得极烂
220行: public File(String pathname)
这样的参数命名,就被Ola Bini继承下来了。
而且这个create,只有一行代码。
return createNoUnicodeConversion(cwd, pathname);
这是什么风格?用create带代替一个又臭又长的方法名?
什么叫做“创建非Unicode转换”呢?
纵观整个函数,我看不到一行与Unicode有关的代码!
再看这个JRubyFile(internal)做了什么事情。
那么,在createNoUnicodeConversion里,他为什么就不能直接写成:return new JRubyFile(internal.getAbsolutePath());呢?
说到底,这个JRubyFile究竟为什么存在呢?
public class JRubyFile extends File
经过我初步的分析,他最重要的作用,就是覆盖了N多的File方法,将系统分隔符,File.separatorChar,一律换成了'/'。
因此,如何快速找到需要阅读的代码,成为一种重要的技巧。
1、浏览文件夹
如果一个开源代码组织得非常好,那么他的source就会有非常清晰、合理、明确的命名。jruby这方面做得确实不错。除了org.jruby下的82个文件之外,org.jruby下还有22个目录。其中最像的,自然是lexer,即词法分析器。
如果对编译原理略有了解(听说过一些要紧的名词即可),就会知道,编译或解释程序,都是首先从词法分析开始,然后是语法分析(Parser)。分析得到的结果,则是一个抽象语法树(AST),随后可以进行解释执行,或编译执行(Compiler)
因此,首先需要考虑的,就是lexer文件夹下的文件。
2、通过排除法,找到入手点
在lexer文件夹里,没有文件,倒是有一个yacc目录。这确实是一个奇怪的命名。yacc(Yet Another Compiler Compiler)。根据wikipedia的定义,yacc是一个用来生成编译器的编译器(编译器代码生成器)。在Unix/Linux上,yacc通常与词法分析器lex一起使用。你这个jruby,怎么会在lexer目录下,加一个叫做yacc的目录呢?
在yacc目录下,有18个文件,我们需要采用排除法,来缩减阅读范围。
引用
ByteListLexerSource.java
CapturingByteListLexerSource.java
HeredocTerm.java
IDESourcePosition.java
IDESourcePositionFactory.java
InputStreamLexerSource.java
ISourcePosition.java
ISourcePositionFactory.java
ISourcePositionHolder.java
LexerSource.java
RubyYaccLexer.java
SimplePositionFactory.java
SimpleSourcePosition.java
StackState.java
StringTerm.java
StrTerm.java
SyntaxException.java
Token.java
CapturingByteListLexerSource.java
HeredocTerm.java
IDESourcePosition.java
IDESourcePositionFactory.java
InputStreamLexerSource.java
ISourcePosition.java
ISourcePositionFactory.java
ISourcePositionHolder.java
LexerSource.java
RubyYaccLexer.java
SimplePositionFactory.java
SimpleSourcePosition.java
StackState.java
StringTerm.java
StrTerm.java
SyntaxException.java
Token.java
以ISourcePosition开头的三个,都是interface,看不到实现代码的,先略过。
IDESourcePosition、IDESourcePositionFactory、SimplePositionFactory、SimpleSourcePosition,是前面几个接口的实现类,而且是关于定位的,可以先不看。
StringTerm、StrTerm、HeredocTerm、Token、是在词法分析中,需要用到的,可以先不看。term(名词,词素);token(记号,代号)。
SyntaxException,是一个异常类,不用看。
ByteListLexerSource、CapturingByteListLexerSource、InputStreamLexerSource、LexerSource,看上去很重要,大概可以从LexerSource开始分析。
RubyYaccLexer,看名字也很重要,需要看看。
3、RubyYaccLexer快扫
这个类里,比较引人注目的是:在class一开始的map定义。明显是一堆ruby语法相关的关键字。可以猜测,他会从某个Source读取文件内容,然后与ruby的关键词比对。
public void setSource(LexerSource source) { this.src = source; }
这是一个关键。
在RubyYaccLexer中,我们随处可以看到src.read();或者src.unread(c);这样的代码。可以看出:LexerSource就是我们要分析的关键代码了。
4、LexerSource分析
public abstract class LexerSource {
这个class的第一行,就让我改变了详细看看LexerSource的念头。这是一个抽象类。那么,传递给RubyYaccLexer的LexerSource,肯定是由某一个扩展了LexerSource的类创建的实例。总要先找到这些扩展类,才能搞清楚整个LexerSource。
这是NetBeans IDE提供的一个查找功能,选择查找所有的子类型,点击确定。
看来一共有三个类:ByteListLexerSource.java,CapturingByteListLexerSource.java,InputStreamLexerSource.java。
那么,究竟这三个类,分别在什么时候会被使用呢?再次动用NetBeans的查找功能:
初步判断:ByteListLexerSource.java,CapturingByteListLexerSource.java,就是两个死类,没有什么别的代码用到他们。因此,真正有用的,就只剩下两个类了:
LexerSource.java、InputStreamLexerSource.java
5、运用暴力法了解执行过程
public static LexerSource getSource(String name, InputStream content, List<String> list,ParserConfiguration configuration) { return new InputStreamLexerSource(name, content, list, configuration.getLineNumber(),configuration.hasExtraPositionInformation()); }
在LexerSource里,我们可以看到上面这段代码,那么,InputStream content这个参数,是从哪里来的呢?
我在工作中,通常会采用暴力法,来做这个事情,相比debug,似乎更加快捷一些。
在getSource函数的第一行,加上这么一句:(new Exception()).printStackTrace();
然后,编译出一个新的jruby.jar来,运行一个ruby的hello world程序。
引用
E:\RubyWork\ruby-test>..\jruby-1.1.6\bin\jruby.bat 1.rb
java.lang.Exception
at org.jruby.lexer.yacc.LexerSource.getSource(LexerSource.java:147)
at org.jruby.parser.Parser.parse(Parser.java:122)
at org.jruby.Ruby.parseFile(Ruby.java:1965)
at org.jruby.Ruby.parseFile(Ruby.java:1969)
at org.jruby.Ruby.parseFromMain(Ruby.java:364)
at org.jruby.Ruby.runFromMain(Ruby.java:327)
at org.jruby.Main.run(Main.java:214)
at org.jruby.Main.run(Main.java:100)
at org.jruby.Main.main(Main.java:84)
hello world!
java.lang.Exception
at org.jruby.lexer.yacc.LexerSource.getSource(LexerSource.java:147)
at org.jruby.parser.Parser.parse(Parser.java:122)
at org.jruby.Ruby.parseFile(Ruby.java:1965)
at org.jruby.Ruby.parseFile(Ruby.java:1969)
at org.jruby.Ruby.parseFromMain(Ruby.java:364)
at org.jruby.Ruby.runFromMain(Ruby.java:327)
at org.jruby.Main.run(Main.java:214)
at org.jruby.Main.run(Main.java:100)
at org.jruby.Main.main(Main.java:84)
hello world!
于是,整个调用序列,就一目了然了。
6、找到InputStream的源头
通过上面的抛出异常,我们可以倒着读回去:
org.jruby.parser.Parser.parse(Parser.java:122)
public Node parse(String file, InputStream content, DynamicScope blockScope, ParserConfiguration configuration) { .... LexerSource lexerSource = LexerSource.getSource(file, content, list, configuration); .... }
org.jruby.Ruby.parseFile(Ruby.java:1965)
public Node parseFile(InputStream in, String file, DynamicScope scope, int lineNumber) { if (parserStats != null) parserStats.addLoadParse(); return parser.parse(file, in, scope, new ParserConfiguration(getKCode(), lineNumber, false, false, true, config.getCompatVersion())); }
org.jruby.Ruby.parseFile(Ruby.java:1969)
public Node parseFile(InputStream in, String file, DynamicScope scope) { return parseFile(in, file, scope, 0); }
org.jruby.Ruby.parseFromMain(Ruby.java:364)
public Node parseFromMain(InputStream inputStream, String filename) { if (config.isInlineScript()) { return parseInline(inputStream, filename, getCurrentContext().getCurrentScope()); } else { return parseFile(inputStream, filename, getCurrentContext().getCurrentScope()); } }
org.jruby.Ruby.runFromMain(Ruby.java:327)
public void runFromMain(InputStream inputStream, String filename) { .... Node scriptNode = parseFromMain(inputStream, filename); .... }
org.jruby.Main.run(Main.java:214)
public int run() { .... InputStream in = config.getScriptSource(); .... runtime.runFromMain(in, filename); .... }
如此看来,还得去读config.getScriptSource()函数了。
org.jruby.RubyInstanceConfig.getScriptSource(RubyInstanceConfig.java:1067)
public InputStream getScriptSource() { try { // KCode.NONE is used because KCODE does not affect parse in Ruby 1.8 // if Ruby 2.0 encoding pragmas are implemented, this will need to change if (hasInlineScript) { return new ByteArrayInputStream(inlineScript()); } else if (isSourceFromStdin()) { // can't use -v and stdin if (isShowVersion()) { return null; } return getInput(); } else { File file = JRubyFile.create(getCurrentDirectory(), getScriptFileName()); return new BufferedInputStream(new FileInputStream(file)); } } catch (IOException e) { throw new MainExitException(1, "Error opening script file: " + e.getMessage()); } }
这个函数中,与我们的目的有关的,也就两行:
File file = JRubyFile.create(getCurrentDirectory(), getScriptFileName());
return new BufferedInputStream(new FileInputStream(file));
7、JRubyFile.create分析
通过JRubyFile.java的注释我们得知,这个类的作者是“Ola Bini”本人。可见他的水平,真的很次啊!
调用的JRubyFile.create的代码写的是:
File file = JRubyFile.create(getCurrentDirectory(), getScriptFileName());
非常清楚,第一个参数是当前路径,第二个参数是脚本文件名。
而在JRubyFile里,create的声明是这么写的:
public static JRubyFile create(String cwd, String pathname)
cwd我还能懂,pathname是什么?路径名?
根源在于,SUN的File.java也写得极烂
220行: public File(String pathname)
这样的参数命名,就被Ola Bini继承下来了。
而且这个create,只有一行代码。
return createNoUnicodeConversion(cwd, pathname);
这是什么风格?用create带代替一个又臭又长的方法名?
什么叫做“创建非Unicode转换”呢?
private static JRubyFile createNoUnicodeConversion(String cwd, String pathname) { if (pathname == null || pathname.equals("") || Ruby.isSecurityRestricted()) { return JRubyNonExistentFile.NOT_EXIST; } File internal = new File(pathname); if(!internal.isAbsolute()) { internal = new File(cwd,pathname); if(!internal.isAbsolute()) { throw new IllegalArgumentException("Neither current working directory ("+cwd+") nor pathname ("+pathname+") led to an absolute path"); } } return new JRubyFile(internal); }
纵观整个函数,我看不到一行与Unicode有关的代码!
再看这个JRubyFile(internal)做了什么事情。
private JRubyFile(File file) { this(file.getAbsolutePath()); } protected JRubyFile(String filename) { super(filename); }
那么,在createNoUnicodeConversion里,他为什么就不能直接写成:return new JRubyFile(internal.getAbsolutePath());呢?
说到底,这个JRubyFile究竟为什么存在呢?
public class JRubyFile extends File
经过我初步的分析,他最重要的作用,就是覆盖了N多的File方法,将系统分隔符,File.separatorChar,一律换成了'/'。
四、修改计划
在基本上理解了jruby的相关代码之后,我们可以考虑如何来修改它的代码了。
最简单粗暴的办法,自然是直接修改它的InputStreamLexerSource.java。因为所有的读ruby源文件的操作,总是会调用到InputStreamLexerSource的read()系方法,从这里下手,自然是最简单的。
即使这种简单的办法,也需要考虑到一个兼容性的问题,也就是说:“我们需要决定,被修改之后的jruby,是否能够读取原来正常的ruby文件?”
我想,还是应该的吧。否则,我就得将系统里所有的ruby代码,都做一遍加密,而且将来想要给jruby加点新的lib,给rails加点plugins,都得一个不少的给他们做加密,这样也太麻烦了。
因此,我需要设计一个加密文件的格式,当他被我的jruby读取的时候,在文件的头部,如果能够找到一个特殊的标记,那么我就按照加密文件去读它,否则,就还是按照普通文件来处理。
当然,还可以通过不同的文件后缀名来做区分,但是,这样就需要修改lib查找部分的代码,想来可能也挺麻烦的,就先不深入想下去了。
这个工作如果要做得漂亮,自然应该通过修改JRubyFile的代码来完成,要是做得难看一点呢?就可以直接在InputStreamLexerSource里加些代码。
再进一步的考虑,是另外写一个class,扩展LexerSource这个抽象类。而不是粗暴的直接改写InputStreamLexerSource。
还可以扩展这个思路,写N个不同的解码LexerSource,以面向各种不同加密格式的文件。而这个,还可以进一步通过修改RubyInstanceConfig,来实现可配置的加密方案。
当然,这一切的计划,必须建立在对于InputStreamLexerSource代码的彻底理解的基础上。
最简单粗暴的办法,自然是直接修改它的InputStreamLexerSource.java。因为所有的读ruby源文件的操作,总是会调用到InputStreamLexerSource的read()系方法,从这里下手,自然是最简单的。
即使这种简单的办法,也需要考虑到一个兼容性的问题,也就是说:“我们需要决定,被修改之后的jruby,是否能够读取原来正常的ruby文件?”
我想,还是应该的吧。否则,我就得将系统里所有的ruby代码,都做一遍加密,而且将来想要给jruby加点新的lib,给rails加点plugins,都得一个不少的给他们做加密,这样也太麻烦了。
因此,我需要设计一个加密文件的格式,当他被我的jruby读取的时候,在文件的头部,如果能够找到一个特殊的标记,那么我就按照加密文件去读它,否则,就还是按照普通文件来处理。
当然,还可以通过不同的文件后缀名来做区分,但是,这样就需要修改lib查找部分的代码,想来可能也挺麻烦的,就先不深入想下去了。
这个工作如果要做得漂亮,自然应该通过修改JRubyFile的代码来完成,要是做得难看一点呢?就可以直接在InputStreamLexerSource里加些代码。
再进一步的考虑,是另外写一个class,扩展LexerSource这个抽象类。而不是粗暴的直接改写InputStreamLexerSource。
还可以扩展这个思路,写N个不同的解码LexerSource,以面向各种不同加密格式的文件。而这个,还可以进一步通过修改RubyInstanceConfig,来实现可配置的加密方案。
当然,这一切的计划,必须建立在对于InputStreamLexerSource代码的彻底理解的基础上。
3 楼 ruby_windy 2012-07-21 19:44
2 楼 qichunren 2009-06-17 13:40
jruby不是可以将rb代码编译到class文件的么?