阅读:5758次   评论:3条   更新时间:2011-05-26    
--------------------------------------------------------------

一、任务描述 Top

我们的一个Web项目,我一开始就打算用RoR来开发,毕竟开发效率会比java高很多。但是这个项目,不是运行在我们自己的服务器上,而是跑在客户的机器上的。

一开始我也没怎么在意,毕竟客户的技术能力不强,未必他们就看得懂我们的代码。但是老板不这么想,他希望我能够想出某种办法来,给我们的代码加密。

我们的项目,一开始是基于jruby 1.0.3的,我自己简单弄了一下,就把加密搞出来了,现在要写这篇文档,我就打算修改最新的jruby 1.1.6的代码,来尝试一下。

二、环境搭建 Top

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。
引用
com.sun.mirror.apt.*
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、以上步骤的目的,主要是为了确认,在我们的开发环境上,能够继续修改人家的源代码,而且,在修改之后,能够切实的用上。

三、代码阅读 Top

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个文件,我们需要采用排除法,来缩减阅读范围。

引用
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


以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!


于是,整个调用序列,就一目了然了。

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,一律换成了'/'。

四、修改计划 Top

在基本上理解了jruby的相关代码之后,我们可以考虑如何来修改它的代码了。

最简单粗暴的办法,自然是直接修改它的InputStreamLexerSource.java。因为所有的读ruby源文件的操作,总是会调用到InputStreamLexerSource的read()系方法,从这里下手,自然是最简单的。

即使这种简单的办法,也需要考虑到一个兼容性的问题,也就是说:“我们需要决定,被修改之后的jruby,是否能够读取原来正常的ruby文件?”

我想,还是应该的吧。否则,我就得将系统里所有的ruby代码,都做一遍加密,而且将来想要给jruby加点新的lib,给rails加点plugins,都得一个不少的给他们做加密,这样也太麻烦了。

因此,我需要设计一个加密文件的格式,当他被我的jruby读取的时候,在文件的头部,如果能够找到一个特殊的标记,那么我就按照加密文件去读它,否则,就还是按照普通文件来处理。

当然,还可以通过不同的文件后缀名来做区分,但是,这样就需要修改lib查找部分的代码,想来可能也挺麻烦的,就先不深入想下去了。

这个工作如果要做得漂亮,自然应该通过修改JRubyFile的代码来完成,要是做得难看一点呢?就可以直接在InputStreamLexerSource里加些代码。

再进一步的考虑,是另外写一个class,扩展LexerSource这个抽象类。而不是粗暴的直接改写InputStreamLexerSource。

还可以扩展这个思路,写N个不同的解码LexerSource,以面向各种不同加密格式的文件。而这个,还可以进一步通过修改RubyInstanceConfig,来实现可配置的加密方案。

当然,这一切的计划,必须建立在对于InputStreamLexerSource代码的彻底理解的基础上。
  • 大小: 14.8 KB
  • 大小: 42 KB
  • 大小: 27.4 KB
  • 大小: 16 KB
  • 大小: 16.5 KB
  • 大小: 9.3 KB
  • 大小: 34.8 KB
  • 大小: 9 KB
  • 大小: 8.5 KB
  • 大小: 9.4 KB
  • 大小: 9.4 KB
  • 大小: 5.2 KB
  • 大小: 7.4 KB
  • 大小: 7.4 KB
评论 共 3 条 请登录后发表评论
3 楼 ruby_windy 2012-07-21 19:44
这需求是蛮蛋疼的,不过楼主这处理的办法很有针对性。值得记下~
2 楼 qichunren 2009-06-17 13:40
我想不明白为什么这么麻烦啊?
jruby不是可以将rb代码编译到class文件的么?

发表评论

您还没有登录,请您登录后再发表评论

文章信息

Global site tag (gtag.js) - Google Analytics