—— 这是我所理解的“工业化开发编程语言”的概念
很显然, java就是种典型的“工业语言”, 非常流行,很多企业靠它赚钱,很实际;
但java也是常年被人黑,光是对其开发效率的诟病就已经足够多,不过java始终屹立不倒;
这样的局面其实无所谓高兴还是担忧,理性的程序员有很多种,其中一种是向“钱”看的 —— 我写java代码,就是因为工作需要而已,能帮助我的组织搞定业务,做出项目,这很好;
当有人说java语言不好的时候,理性的程序员不会陷入宗教式的语言战争之中,他会思考这些人说的是否有道理;如果真的发现整个java平台大势已去,他会毫不犹豫地扭头就走,不过直到目前为止,还没有这种迹象出现;
那么,从这些无数次的口水之争中,我们能否从别人的“战场”上发现一些有用的东西, 来改进我们的开发方式,从而使得java这种已经成为一个“平台”的东西走得更远,赚更多的钱呢?
答案是“有的”,感谢那些参与口水战争的、各种阵营的年轻程序员们,有了你们,java speaker们才有了更多的思考;
我就只谈一个最实际的问题:
也就是说,到底是哪些主要特性直接导致了某些其它语言在语法上相对于java的优越感?
在groovy中定义map和list的惯用方式:
def list = [a, 2 ,3]
def map = [a:0, b:1]
而java呢?只能先new
一个list或map,再一个个add或put进去; 上面这种literal(字面量)形式的写法便捷得多;
而javascript在这方面做得更绝, 我们都用过json,而json其实就是literal形式的object
极端情况下,一门编程语言里的所有数据类型,包括”内建”的和用户自定义的,统统可以写成literal形式;
在这种情形下,其实这种语言连额外的对象序列化、反序列化机制都不需要了 —— 数据的序列化形式就是代码本身, “代码”和“数据”在形式上被统一了
java对这方面几乎没有任何支持,对于提高编码效率来讲,这是值得学习的一点, 起码“内建”数据结构需要literal写法支持
无论是js, 还是python/ruby,或是groovy,都可以将函数作为另一个函数的参数传入,以便后者根据执行情况判断是否要调用前者
或者能够将一个函数作为另一个函数的返回值返回,以便后续再对其进行调用
这种高阶函数特性,就不要再说java的匿名内部类“能够”实现了, 如果认为匿名内部类已经”够用”了的话,其实就已经与现在的话题“开发效率”相悖了
高阶函数显然是一种值得借鉴的特性,它会让你少写很多很多无聊的“包装”代码;
还有就是匿名函数(lambda)了
我不喜欢lambda、lambda地称呼这个东西,我更喜欢把它叫做“匿名函数”或者“函数字面量(literal)”, 因为它跟数学上的lambda演算还是有本质区别,叫”lambda”有误导的危险
函数字面量的意思就是说,你可以在任何地方,甚至另一个函数体的调用实参或内部,随时随地地定义另一个新的函数
这种定义函数的形式,除了“这个函数我只想在这里用一次,所以没必要给它起个名字”这种理由之外,还有一个更重要的理由就是“闭包”了
所谓闭包,其实也是一个函数,但是在这个函数被定义时,其内部所出现的所有”自由变量(即未出现在该函数的参数列表中的变量)”已被当前外层上下文给确定下来了(lexical), 这时候,这个函数拥有的东西不仅仅是一套代码逻辑,还带有被确定下来的、包含那些“自由变量”的一个上下文, 这样这个函数就成为了一个闭包
那么闭包这种东西有什么好呢?其实如果懒散而钻牛角尖地想,闭包的所有能力,是严格地小于等于一个普通的java对象的,也就是说,凡是可以用一个闭包实现的功能,就一定可以通过传入一个对象来实现,但反过来却不行 —— 因为闭包只有一套函数逻辑,而对象可以有很多套,其次很多语言实现的闭包其内部上下文不可变但对象内部属性可变
既然这样,java还要闭包这种东西来干嘛?其实这就又陷入了”匿名内部类可以实现高阶函数”的困境里了 —— 如果我在需要一个闭包的时候,都可以通过定义一个接口再传入一个对象来实现的话,这根本就跟今天的话题“开发效率”背道而驰了
显然,java是需要闭包的
这和开发效率有关么?
编程语言不是越“动态”,开发效率越高么?还需要强大而复杂的静态类型系统么?
试想一下这种api定义:
def eat(foo) {
...
}
这里面你认识的东西可能只有’吃’了, 你知道foo是什么么?你知道它想吃什么么?吃完后要不要产出点什么东西? —— 你什么都不知道
这种api极易调用出错,这就好比我去买饭,问你想吃什么你说“随便”,但买回肯德基你却说你实际想吃的是麦当劳一样
可能你还会反驳说,不是还有文档么?你把文档写好点不就行了么? —— 不要逼我再提“匿名内部类”的例子,如果给每个函数写上复杂详尽的文档是个好办法,那就显然 —— again, 与“开发效率”背道而驰了
那么,静态类型系统,这里显然就该用上了
静态类型系统在多人协作开发、甚至团队、组织间协作开发是非常有意义的;
拥有静态类型系统的编程语言通常都有强大的、带语法提示功能的IDE,这很正常,因为静态类型语言的语法提示功能好做;
只要把别人的库拿过来,导入IDE,各种函数签名只需扫一眼 —— 很多情况下根本不需要仔细看文档 —— 就已经知道这个函数是干嘛用的了, 合作效率成倍提升;
而且,作为”api”,作为“模块边界”,作为与其它程序员合作的“门面”, 函数签名上能将参数和返回值类型“卡”得越紧越好 —— 这样别人不用猜你这个函数需要传入什么类型,甚至他在IDE里一“点”,这里就给自动填上了 :)
要做到“卡得紧”,光有静态类型系统还不够,这个系统还需强大, 试想一下这个例子:
/**
* 我只吃香蕉和猪肉,请勿投食其它物品
*/
public void eat(List<Object> list) {
for(Object o: list) {
if(o instanceof Banana){
... // eating banana
} else if(o instanceof Pork) {
... // eating pork
} else {
throw new RuntimeException("System err.");
}
}
}
这段纯java代码已经是“定义精确”的静态类型了
但如果没有上面那行注释,你很可能会被System err.
无数次
而这行注释之所以是必需的,完全是因为我找不到一个比List<Object>
更好的表达“香蕉或猪肉”的形式, 这种情形足以让人开始想念haskell的either monad
在“强大而复杂的类型系统”这一点上,jvm平台上令人瞩目的当属scala了,可惜java没有,这是值得借鉴的
不过这一点的“借鉴”还需java的compiler team发力,我等也只是说说(按照java保守的改进速度,估计HM
类型系统是指望不上了)
刚说完静态类型,现在又来说动态类型系统合适么?
然而这与节操无关,我想表达的是,只要是有助于“开发效率”的,都能够借鉴,这是一个理性的java speaker的基本素质
我们在开发项目的时候,大量的编码发生在“函数”或“方法”的内部 —— 这就好比你在屋子里、在家里宅着一样, 是不是应该少一些拘束,多一些直截了当?
在这种情形下,动态类型系统要不要太爽? ——
Void visitAssert(AssertTree node, Void arg1) {
def ahooks = this.hooks[VisitAssertHook.class]
ahooks.each {it.beforeVisitCondition(node, errMsgs, this.ctx, resolveRowAndCol, setError)}
scan((Tree)node.getCondition(), arg1);
ahooks.each {it.afterVisitConditionAndBeforeDetail(node, errMsgs, this.ctx, resolveRowAndCol, setError)}
scan((Tree)node.getDetail(), arg1);
ahooks.each {it.afterVisitDetail(node, errMsgs, this.ctx, resolveRowAndCol, setError)}
return null;
}
你知道ahooks是什么类型么?你不知道但我(我是编码的人)知道
你知道ahooks身上有些什么方法可以调么?你同样不知道但我知道
你不知道没关系,只要我知道就行了,因为现在是我在写这段代码;
这段代码写完以后,我只会把Void visitAssert(AssertTree node, Void arg1)
这个类型明确的方法签名提供给你调用,我并不会给你看函数体里面的那坨东西,因此你知不知道上面这些真的没关系
方法内部满是def, 不用书写繁复的List<Map<String, List<Map<Banana, Foo>>>>
这种反人类反社会标语, 每个对象我知道它们身上能“点”出些什么来,我只管“点”,跑起来之后invokedynamic
会为我搞定一切
动态类型系统 —— 这就是方法内部实现应该有的样子
哪怕你的方法内部实现就是一坨shi,你也希望这坨shi能尽可能小只一点,这样看起来更清爽是吧?
不要说我太分裂,我要笑你看不穿 —— 静态类型和动态类型既然都有好处,那么他们能放在一起么?
能的,这里就需要点明这篇文章的政治目的了: “java与groovy混编”
而且,目前来看,jvm平台上,只有它二者的结合,才能完成动态静态混编的任务
曾经我发出过这样一段感叹:
公共api、对外接口声明、应用程序边界…这些对外的“脸面”部分代码,如果拥有scala般强大的类型系统…就好了;而私有代码、内部实现、各种内部算法、逻辑,如果拥有groovy般的动态、简单的类型系统…就好了;综上,如果有门语言,在接口和实现层面分别持有上述特性,就好了
这种“理想”中的语言或许某天我有空了会考虑实现一个
而现在,虽说不是scala,但我终于想要在java和groovy身上来试验一把这种开发方式了
这里我坦白一下为什么没用scala,原因很简单,我在技术选型方面是势利的,scala还不被大多数平均水平的java开发人员(参见”工业化开发编程语言”定义第一条)接受,这直接导致项目的推进会遇到困难
而相对来讲,我暂且相信大多数java开发人员都还算愿意跨出groovy
这一小步,当然这还需要时间证明
好了,下面还剩下一点点无关痛痒的牢骚 ——
macro, eval, 编译过程切入, 甚至method missing机制,这些都算“元编程”
元编程能力的强弱直接决定了使用这种语言创作“内部DSL”的能力
java在元编程方面的能力,几乎为0
这是值得借鉴的
与groovy的混编,顺便也能把groovy的元编程也带进来
语法糖,关起门来吃最美味,这也是一种使得“方法内部实现更敏捷”的附加手段
网上随便下载一份groovy的cheat sheet, 都会列举groovy的那些写代码方面的奇技淫巧
这些奇技淫巧,在各种脚本语言之间其实都大同小异, 因为他们本来就是抄来抄去的
结合方法内部的动态类型环境,这一定会进一步缩小方法内部实现代码的体积
我不去讨论什么语言才是The True Heir of Java, 那会使这篇文章变成一封战书,我只关心如何更好地利用现有开发资源完成项目,高效地帮组织实现利益
所以说java和groovy的混编是一种最“势利”的折衷,我不想强迫平均水平的开发人员去学习一种完全不同的语言,短期内不会对项目有任何好处,真正想去学的人他自己会找时间去学
而groovy,说它是java++也不为过,因为java代码直接就可以被groovy编译, groovy完全兼容java语法, 对一般java开发人员来说,这真是太亲切了
这里我要提一下我对“java和groovy混编”的一个个人性质的小尝试 —— kan-java项目
kan-java这个小工具,凡是用户在编码使用过程中能“碰”到的类和接口,全部都由java定义, 这确保用户拿到的东西都有精确的类型定义
凡是对上述接口的实现,都以groovy代码的形式存在
这贯彻了”接口静态类型,内部实现动态类型”的宗旨, 或者说“凡是要提供给另外一个人看、调用的地方(接口或接口类),使用java,否则就用groovy”
当然了,单元测试也完全由groovy代码实现
将kan-java的jar包引入到项目中使用时,就跟使用其它任何纯java实现的jar包一样 —— 接口清晰,参数类型明确,返回类型明确, 你不会也没有必要知道开发人员在具体实现的时候,使用动态语言爽过一把
对于java和groovy的混编,项目的pom.xml如何配置,除了可以参考kan-java的配置外,还可以参考这个gist: https://gist.github.com/pfmiles/2f2ab77f06d48384f113, 里面举例了两种配置方式,各有特色
具体的效果,还需要真正地去实际项目中体会
另外,kan-java也是一个有趣的工具,这个工具所实现的功能我也是从未见到java世界内有其它地方讨论过的,它可以辅助java做“内部DSL”,有场景的可以一试
想要安全地做“信任交换”,以从网站A免登到网站B举例,其实主要是要解决如下几个问题:
第一个问题,“免登目标地址的合法性”,这是需要网站A解决的;建议网站A建立一个“合法的免登目标地址”列表,用以检测用户发起的免登请求的目标地址是否合法
需要注意的是,一个“合法”的免登目标地址,不仅仅是域名合法就行了,也包括path部份;因为不排除会有人将用户骗至一个执行重要操作的目标url进行免登,比如”www.B.com/resetPassword.do”,这是应当避免的
一般来讲,免登的目标地址应该是一个“无副作用”,无任何额外操作的类似于“主页”的地方,这样能杜绝上述情况发生
第二个问题,“在任何一次跳转的过程中防止参数被篡改”,这显然是必要的;从http跳转这种行为本身来讲,每一次跳转,服务器端都失去一次对该请求行为的控制,客户端想要更改跳转请求的任何属性都轻而易举
可以采用”数字签名”技术防止这种篡改;需要考虑的是,哪些数据需要被签名?这取决于该免登需求“在意”哪些数据被更改,一般来讲,都会对请求参数进行签名,当然其它数据,如path也可以被包含在内,如果path带有业务信息的话
由网站A负责加签,等请求经由客户端,在转发到网站B时,网站B会对请求进行验签; 这需要网站A和网站B事先约定好加签验签的密钥,一般来讲,对称的密钥串已经足够安全,只要密钥不被泄漏
另外,如果需要确保每次跳转后,访问服务器的仍是同一个客户端,那么这个签名步骤也可将客户端的ip地址或“设备指纹”等数据一并加签,以便验证请求在跳转过程中是否被劫持;不过这么做的风险是,并不仅仅只有被劫持的情况才会在跳转过程中改变客户端的ip,也可能是其它正常情况,比如A、B网站之间不同的网络架构造成的获取客户端ip地址误差等等,需三思而后行
第三个问题,“任何一次跳转链接都要具有短效性和一次性”,这也是显而易见的,否则客户端或搜索引擎只要将某个跳转中间阶段的url给收录下来,那么就随时可以免登了;有不少实力雄厚的大网站都出过这样的问题
这个问题可以通过“一次性令牌”方案简单解决,也就是说,跳转之前生成一个唯一的、一次性的、短期时效性的一个“令牌”,跳转到目标网站后在验证、作废该令牌
至于是网站A还是网站B负责生成这个令牌,其实无所谓,只要保证令牌的生成过程是私密的就行了:
上述两种策略都可以,但看起来显然由跳转发起方来做令牌的生成和提供验证接口比较好,这样省去远程调用令牌生成接口的麻烦
注意,最后一次跳转,也就是跳转到最终目标页面的那次跳转,由于有登录验证,因此不需要一次性令牌验证了,但仍然需要签名验证(如果带有业务参数的话)
第四个问题,“免登目标所接受的参数的合法性”;一般来讲,免登目标url是不建议带有参数的,但如果一定要带,那么是必需要网站B验证这些参数的业务合法性,以免恶意用户从一开始就传送一些非法参数过来
第五个问题,在多次跳转请求间“保证客户端不被顶替”,这个听起来概念复杂,但实现起来挺简单:只要在加签、验签的时候把客户端ip也算在里面就行了,当然如果网站已经实现了“设备指纹”技术那就更好了,这个问题就转化为:“多次请求跳转期间,通过签名、验签保证客户端设备指纹不被篡改”
解决好了上述几个问题,也就是几次跳转,写好cookie,最终跳到目标页面上的事情了。
综上,一个典型的,由网站A免登到网站B,其跳转执行流程如下:
1.6之后JDK提供了一套compiler API,定义在JSR199中, 提供在运行期动态编译java代码为字节码的功能
简单说来,这一套API就好比是在java程序中模拟javac程序,将java源文件编译为class文件;其提供的默认实现也正是在文件系统上进行查找、编译工作的,用起来感觉与javac基本一致;
不过,我们可以通过一些关键类的继承、方法重写和扩展,来达到一些特殊的目的,常见的就是“与文件系统解耦”(就是在内存或别的地方完成源文件的查找、读取和class编译)
需要强调的是,compiler API的相关实现被放在tools.jar中,JDK默认会将tools.jar放入classpath而jre没有,因此如果发现compiler API相关类找不到,那么请检查一下tools.jar是否已经在classpath中;
当然我指的是jdk1.6以上的版本提供的tools.jar包
public static CompilationResult compile(String qualifiedName, String sourceCode,
Iterable<? extends Processor> processors) {
JavaStringSource source = new JavaStringSource(qualifiedName, sourceCode);
List<JavaStringSource> ss = Arrays.asList(source);
List<String> options = Arrays.asList("-classpath", HotCompileConstants.CLASSPATH);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
MemClsFileManager fileManager = null;
Map<String, JavaMemCls> clses = new HashMap<String, JavaMemCls>();
Map<String, JavaStringSource> srcs = new HashMap<String, JavaStringSource>();
srcs.put(source.getClsName(), source);
try {
fileManager = new MemClsFileManager(compiler.getStandardFileManager(null, null, null), clses, srcs);
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
StringWriter out = new StringWriter();
CompilationTask task = compiler.getTask(out, fileManager, diagnostics, options, null, ss);
if (processors != null) task.setProcessors(processors);
boolean sucess = task.call();
if (!sucess) {
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
out.append("Error on line " + diagnostic.getLineNumber() + " in " + diagnostic).append('\n');
}
return new CompilationResult(out.toString());
}
} finally {
try {
fileManager.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// every parser class should be loaded by a new specific class loader
HotCompileClassLoader loader = new HotCompileClassLoader(Util.getParentClsLoader(), clses);
Class<?> cls = null;
try {
cls = loader.loadClass(qualifiedName);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
return new CompilationResult(cls, loader);
}
解释一下这段程序:
这个static方法提供这样一种功能:输入希望的类名和String形式的java代码内容,动态编译并返回编译好的class对象; 其中CompilationResult
只是一个简单的pojo封装,用于包装返回结果和可能的错误信息
类名和源码首先被包装成一个JavaStringSource
对象, 该对象继承自javax.tools.SimpleJavaFileObject
类,是compiler API对一个“Java文件”(即源文件或class文件)的抽象;将源文件包装成这个类也就实现了“将java源文件放在内存中”的想法
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
这是取得一个默认的JavaCompiler工具的实例
由于我打算将编译好的class文件直接存放在内存中,因此我自定义了一个MemClsFileManager
:
public class MemClsFileManager extends ForwardingJavaFileManager<StandardJavaFileManager> {
private Map<String, JavaMemCls> destFiles;
private Map<String, JavaStringSource> srcFiles;
protected MemClsFileManager(StandardJavaFileManager fileManager, Map<String, JavaMemCls> destFiles,
Map<String, JavaStringSource> srcFiles){
super(fileManager);
this.destFiles = destFiles;
this.srcFiles = srcFiles;
}
public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind, FileObject sibling)
throws IOException {
if (!(Kind.CLASS.equals(kind) && StandardLocation.CLASS_OUTPUT.equals(location))) return super.getJavaFileForOutput(location,
className,
kind,
sibling);
if (destFiles.containsKey(className)) {
return destFiles.get(className);
} else {
JavaMemCls file = new JavaMemCls(className);
this.destFiles.put(className, file);
return file;
}
}
public void close() throws IOException {
super.close();
this.destFiles = null;
}
public Iterable<JavaFileObject> list(Location location, String packageName, Set<Kind> kinds, boolean recurse)
throws IOException {
List<JavaFileObject> ret = new ArrayList<JavaFileObject>();
if ((StandardLocation.CLASS_OUTPUT.equals(location) || StandardLocation.CLASS_PATH.equals(location))
&& kinds.contains(Kind.CLASS)) {
for (Map.Entry<String, JavaMemCls> e : destFiles.entrySet()) {
String pkgName = resolvePkgName(e.getKey());
if (recurse) {
if (pkgName.contains(packageName)) ret.add(e.getValue());
} else {
if (pkgName.equals(packageName)) ret.add(e.getValue());
}
}
} else if (StandardLocation.SOURCE_PATH.equals(location) && kinds.contains(Kind.SOURCE)) {
for (Map.Entry<String, JavaStringSource> e : srcFiles.entrySet()) {
String pkgName = resolvePkgName(e.getKey());
if (recurse) {
if (pkgName.contains(packageName)) ret.add(e.getValue());
} else {
if (pkgName.equals(packageName)) ret.add(e.getValue());
}
}
}
// 也包含super.list
Iterable<JavaFileObject> superList = super.list(location, packageName, kinds, recurse);
if (superList != null) for (JavaFileObject f : superList)
ret.add(f);
return ret;
}
private String resolvePkgName(String fullQualifiedClsName) {
return fullQualifiedClsName.substring(0, fullQualifiedClsName.lastIndexOf('.'));
}
public String inferBinaryName(Location location, JavaFileObject file) {
if (file instanceof JavaMemCls) {
return ((JavaMemCls) file).getClsName();
} else if (file instanceof JavaStringSource) {
return ((JavaStringSource) file).getClsName();
} else {
return super.inferBinaryName(location, file);
}
}
}
其中最主要的步骤就是重写了getJavaFileForOutput
方法,使其使用内存中的map来作为生成文件(class文件)的输出位置
CompilationTask task = compiler.getTask(out, fileManager, diagnostics, options, null, ss);
boolean sucess = task.call();
上面这两行是创建了一个编译task,并调用
最后使用自定义ClassLoader来加载编译好的类并返回:
HotCompileClassLoader loader = new HotCompileClassLoader(Util.getParentClsLoader(), clses);
Class<?> cls = null;
try {
cls = loader.loadClass(qualifiedName);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
return new CompilationResult(cls, loader);
而该ClassLoader的实现关键在于“到内存中(即之前存放编译好的class的map中)加载字节码”:
public class HotCompileClassLoader extends ClassLoader {
private Map<String, JavaMemCls> inMemCls;
public HotCompileClassLoader(ClassLoader parent, Map<String, JavaMemCls> clses){
super(parent);
this.inMemCls = clses;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] b = this.inMemCls.get(name).getClsBytes();
return defineClass(name, b, 0, b.length);
}
}
之后只要调用方法compile(className, source, null)
这样就算完成了一个基本的、不依赖实际的文件系统的动态编译过程
一个广义的、管理“文件”资源的接口,并不一定指“操作系统的磁盘文件系统”
其实JavaFileManager
只是一个接口,只要行为正确,那么就无所谓“文件”到底以何种形式、实际被存放在哪里
这是基于磁盘文件的JavaFileManager实现, 所有的文件查找、新文件输出位置都在磁盘上完成;也就是说,如果直接使用默认的StandardJavaFileManager
来做动态编译,那么得到的效果就跟命令行中直接使用javac编译差不多
如果想要将编译好的class文件放在内存中而不是磁盘上,那么需要使用一个ForwardingJavaFileManager
来包装默认的StandardJavaFileManager
并重写getJavaFileForOutput
方法,将其实现改为内存操作;这个实现可参考上面的MemClsFileManager
类
不过ForwardingJavaFileManager
还有许多别的方法,没有文档说明动态编译过程中到底那些方法会被调用,原则上讲,所有方法都有可能被调用
但具体哪些方法被调用了可以被实测出来,这样可以有选择性地重写其中一些方法
比如MemClsFileManager
中,重写了inferBinaryName
, list
, close
, getJavaFileForOutput
方法,因为这些方法都会被“class文件放在内存中”这一策略所影响,所以需要兼容
下面想介绍的其实是另一个更进一步的话题:如何在动态编译的过程中分析被编译的源代码
因为几乎所有打算用到java动态编译的应用场景,都会想要对被编译的代码做一些检查、review,以防编译运行了让人出乎意料的代码从而对系统造成破坏
那么这个事情,除了人工review之外,一些简单的验证,完全可以在编译期自动地做到;而JavaCompiler Tree API就是用来做这个静态编译期检查的
它的基本思路很简单,就是hook进编译的过程中,在java源码被parse成AST的时候,使用visitor模式对该AST做遍历分析,以找出你需要定位的语法结构,这样来达到验证目的; 比如”如果发现assert语句就报错”或“不允许定义嵌套类”这样的检查都很容易做
这之中的关键,当然是java的AST和其对应的visitor的实现了
http://docs.oracle.com/javase/7/docs/jdk/api/javac/tree/com/sun/source/tree/package-summary.html
上述链接给出了java的AST类结构,所有语法元素的节点都有
而对应的visitor接口TreeVisitor<R,P>
与AST形成了标准的visitor模式
TreeVisitor<R,P>
的默认实现及其用途:
有了上面这个visitor模式的脚手架,我们就能通过实现一个visitor来达到对java源码的分析了
比如下面这个visitor, 它继承自TreePathScanner
(ExprCodeChecker
是我自定义的一个TreePathScanner
的子类):
public class ForbiddenStructuresChecker extends ExprCodeChecker<Void, FbdStructContext> {
private StringBuilder errMsg = new StringBuilder();
public String getErrorMsg() {
return this.errMsg.toString();
}
public FbdStructContext getInitParam() {
return new FbdStructContext();
}
/**
* 禁止定义内部类
*/
public Void visitClass(ClassTree node, FbdStructContext p) {
if (p.isInClass) {
// 已经位于另一个外层class定义中了,直接报错返回,不再继续遍历子节点
this.errMsg.append("Nested class is not allowed in api expressions. Position: " + resolveRowAndCol(node)).append('\n');
return null;
} else {
boolean oldInClass = p.isInClass;
p.isInClass = true;
// 继续遍历子节点
super.visitClass(node, p);
p.isInClass = oldInClass;
return null;
}
}
/**
* 禁止定义'assert'语句
*/
public Void visitAssert(AssertTree node, FbdStructContext p) {
this.errMsg.append("Assertions are not allowed in api expressions. Position: " + this.resolveRowAndCol(node)).append('\n');
return null;
}
/**
* 禁止定义goto(break or continue followed by a label)语句
*/
public Void visitBreak(BreakTree node, FbdStructContext p) {
if (node.getLabel() != null) {
this.errMsg.append("'break' followed by a label is not allowed in api expressions. Position: "
+ this.resolveRowAndCol(node)).append('\n');
return null;
} else {
return super.visitBreak(node, p);
}
}
public Void visitContinue(ContinueTree node, FbdStructContext p) {
if (node.getLabel() != null) {
this.errMsg.append("'continue' followed by a label is not allowed in api expressions. Position: "
+ this.resolveRowAndCol(node)).append('\n');
return null;
} else {
return super.visitContinue(node, p);
}
}
// *************禁止定义goto end*************
/**
* 禁止定义死循环,for/while/do-while loop, 只限制常量类型的循环条件造成的明显死循环; 这种静态校验是不完善的,要做完善很复杂,没必要加大投入;若要做到更精确的控制应从动态期方案的方向考虑
*/
public Void visitDoWhileLoop(DoWhileLoopTree node, FbdStructContext p) {
boolean condTemp = p.isConstantTrueCondition;
boolean isLoopExpTemp = p.isLoopConditionExpr;
p.isLoopConditionExpr = true;
node.getCondition().accept(this, p);
if (p.isConstantTrueCondition) {
// 死循环
this.errMsg.append("Dead loop is not allowed in api expressions. Position: " + this.resolveRowAndCol(node)).append('\n');
}
p.isConstantTrueCondition = condTemp;
p.isLoopConditionExpr = isLoopExpTemp;
return super.visitDoWhileLoop(node, p);
}
public Void visitForLoop(ForLoopTree node, FbdStructContext p) {
if (node.getCondition() == null) {
// 无条件,相当于'true'
this.errMsg.append("Dead loop is not allowed in api expressions. Position: " + this.resolveRowAndCol(node)).append('\n');
} else {
boolean condTemp = p.isConstantTrueCondition;
boolean isLoopExpTemp = p.isLoopConditionExpr;
p.isLoopConditionExpr = true;
node.getCondition().accept(this, p);
if (p.isConstantTrueCondition) {
// 死循环
this.errMsg.append("Dead loop is not allowed in api expressions. Position: "
+ this.resolveRowAndCol(node)).append('\n');
}
p.isConstantTrueCondition = condTemp;
p.isLoopConditionExpr = isLoopExpTemp;
}
return super.visitForLoop(node, p);
}
public Void visitWhileLoop(WhileLoopTree node, FbdStructContext p) {
boolean condTemp = p.isConstantTrueCondition;
boolean isLoopExpTemp = p.isLoopConditionExpr;
p.isLoopConditionExpr = true;
node.getCondition().accept(this, p);
if (p.isConstantTrueCondition) {
// 死循环
this.errMsg.append("Dead loop is not allowed in api expressions. Position: " + this.resolveRowAndCol(node)).append('\n');
}
p.isConstantTrueCondition = condTemp;
p.isLoopConditionExpr = isLoopExpTemp;
return super.visitWhileLoop(node, p);
}
// 处理循环条件, 需要关心结果为boolean值的表达式
// 二元表达式
public Void visitBinary(BinaryTree node, FbdStructContext p) {
boolean isLoopCondTemp = p.isLoopConditionExpr;
// 求左值
p.isLoopConditionExpr = false;
node.getLeftOperand().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
Object leftVal = p.expValue;
// 求右值
p.isLoopConditionExpr = false;
node.getRightOperand().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
Object rightVal = p.expValue;
// 求整体值
Object val = null;
if (leftVal != null && rightVal != null) switch (node.getKind()) {
case MULTIPLY:
val = ((Number) leftVal).doubleValue() * ((Number) rightVal).doubleValue();
break;
case DIVIDE:
val = ((Number) leftVal).doubleValue() / ((Number) rightVal).doubleValue();
break;
case REMAINDER:
val = ((Number) leftVal).intValue() % ((Number) rightVal).intValue();
break;
case PLUS:
if (leftVal instanceof Number && rightVal instanceof Number) {
val = ((Number) leftVal).doubleValue() + ((Number) rightVal).doubleValue();
} else {
val = String.valueOf(leftVal) + String.valueOf(rightVal);
}
break;
case MINUS:
val = ((Number) leftVal).doubleValue() - ((Number) rightVal).doubleValue();
break;
case LEFT_SHIFT:
val = ((Number) leftVal).longValue() << ((Number) rightVal).intValue();
break;
case RIGHT_SHIFT:
val = ((Number) leftVal).longValue() >> ((Number) rightVal).intValue();
break;
case UNSIGNED_RIGHT_SHIFT:
val = ((Number) leftVal).longValue() >>> ((Number) rightVal).intValue();
break;
case LESS_THAN:
val = ((Number) leftVal).doubleValue() < ((Number) rightVal).doubleValue();
break;
case GREATER_THAN:
val = ((Number) leftVal).doubleValue() > ((Number) rightVal).doubleValue();
break;
case LESS_THAN_EQUAL:
val = ((Number) leftVal).doubleValue() <= ((Number) rightVal).doubleValue();
break;
case GREATER_THAN_EQUAL:
val = ((Number) leftVal).doubleValue() >= ((Number) rightVal).doubleValue();
break;
case EQUAL_TO:
val = leftVal == rightVal;
break;
case NOT_EQUAL_TO:
val = leftVal != rightVal;
break;
case AND:
if (leftVal instanceof Number) {
val = ((Number) leftVal).longValue() & ((Number) rightVal).longValue();
} else {
val = ((Boolean) leftVal) & ((Boolean) rightVal);
}
break;
case XOR:
if (leftVal instanceof Number) {
val = ((Number) leftVal).longValue() ^ ((Number) rightVal).longValue();
} else {
val = ((Boolean) leftVal) ^ ((Boolean) rightVal);
}
break;
case OR:
if (leftVal instanceof Number) {
val = ((Number) leftVal).longValue() | ((Number) rightVal).longValue();
} else {
val = ((Boolean) leftVal) | ((Boolean) rightVal);
}
break;
case CONDITIONAL_AND:
val = ((Boolean) leftVal) && ((Boolean) rightVal);
break;
case CONDITIONAL_OR:
val = ((Boolean) leftVal) || ((Boolean) rightVal);
break;
default:
val = null;
}
if (p.isLoopConditionExpr) {
if (val != null && val instanceof Boolean && (Boolean) val) p.isConstantTrueCondition = true;
} else {
p.expValue = val;
}
return null;
}
// 3元条件表达式
public Void visitConditionalExpression(ConditionalExpressionTree node, FbdStructContext p) {
boolean isLoopCondTemp = p.isLoopConditionExpr;
p.isLoopConditionExpr = false;
node.getCondition().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
Object val = null;
if (p.expValue != null) {
if ((Boolean) p.expValue) {
// 取true expr值
p.isLoopConditionExpr = false;
node.getTrueExpression().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
val = p.expValue;
} else {
// 取false expr值
p.isLoopConditionExpr = false;
node.getFalseExpression().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
val = p.expValue;
}
}
if (p.isLoopConditionExpr) {
p.isConstantTrueCondition = val != null && val instanceof Boolean && (Boolean) val;
} else {
p.expValue = val;
}
return null;
}
// 常量
public Void visitLiteral(LiteralTree node, FbdStructContext p) {
if (p.isLoopConditionExpr) {
p.isConstantTrueCondition = Boolean.TRUE.equals(node.getValue());
} else {
p.expValue = node.getValue();
}
return null;
}
// 括起来的表达式
public Void visitParenthesized(ParenthesizedTree node, FbdStructContext p) {
boolean isLoopCondTemp = p.isLoopConditionExpr;
if (p.isLoopConditionExpr) {
// 求值子表达式
p.isLoopConditionExpr = false;
node.getExpression().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
if (p.expValue != null && Boolean.TRUE.equals(p.expValue)) p.isConstantTrueCondition = true;
} else {
// 直接以子表达式的结果作为括号表达式的结果
p.isLoopConditionExpr = false;
node.getExpression().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
}
return null;
}
// 类型转换表达式
public Void visitTypeCast(TypeCastTree node, FbdStructContext p) {
boolean isLoopCondTemp = p.isLoopConditionExpr;
if (p.isLoopConditionExpr) {
p.isLoopConditionExpr = false;
node.getExpression().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
if (p.expValue != null
&& ("Boolean".equals(node.getType().toString()) || "boolean".equals(node.getType().toString()))) p.isConstantTrueCondition = true;
} else {
p.isLoopConditionExpr = false;
node.getExpression().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
}
return null;
}
// 一元表达式
public Void visitUnary(UnaryTree node, FbdStructContext p) {
boolean isLoopCondTemp = p.isLoopConditionExpr;
// 求子表达式值
p.isLoopConditionExpr = false;
node.getExpression().accept(this, p);
p.isLoopConditionExpr = isLoopCondTemp;
Object val = null;
if (p.expValue != null) {
switch (node.getKind()) {
case POSTFIX_INCREMENT:
case POSTFIX_DECREMENT:
case PREFIX_INCREMENT:
case PREFIX_DECREMENT:
val = null;
break;
case UNARY_PLUS:
val = p.expValue;
break;
case UNARY_MINUS:
val = -((Number) p.expValue).doubleValue();
break;
case BITWISE_COMPLEMENT:
val = ~((Number) p.expValue).longValue();
break;
case LOGICAL_COMPLEMENT:
val = !((Boolean) p.expValue);
break;
default:
val = null;
}
}
if (p.isLoopConditionExpr) {
if (val != null && val instanceof Boolean && (Boolean) val) p.isConstantTrueCondition = true;
} else {
p.expValue = val;
}
return null;
}
// *************禁止定义死循环end*************
}
visitClass
的处理,对“定义嵌套类”这种行为进行了报错visitAssert
的处理,凡是遇到代码中出现assert
语句的,均给出错误信息visitBreak
和visitContinue
的处理禁止了goto语句(即带label的break和continue语句)visitDoWhileLoop
等循环语法结构的访问,以及部分表达式结构的访问(如visitBinary
、visitLiteral
),禁止了如while(true)
、for(;;)
或while(1<2)
等明显死循环接下来的问题就是:我们有了处理AST的visitor,那么到底要在什么时候运行它呢?
答案就是使用jdk1.6的PluggableAnnotationProcessor机制, 在创建compilerTask时设置对应的Processor, 然后在该Processor中调用我们的visitor
下面是我们的processor实现:
@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes("*")
public class ExprCodeCheckProcessor extends AbstractProcessor {
// 工具实例类,用于将CompilerAPI, CompilerTreeAPI和AnnotationProcessing框架粘合起来
private Trees trees;
// 分析过程中可用的日志、信息打印工具
private Messager messager;
// 所有的CodeChecker
private List<ExprCodeChecker<?, ?>> codeCheckers = new ArrayList<ExprCodeChecker<?, ?>>();
// 搜集错误信息
private StringBuilder errMsg = new StringBuilder();
// 代码检查是否成功, 若false, 则'errMsg'里应该有具体错误信息
private boolean success = true;
/**
* ==============在这里列出所有的checker实例==============
*/
public ExprCodeCheckProcessor(){
// 检查 —— 禁止定义一些不必要的结构,如内部类
this.codeCheckers.add(new ForbiddenStructuresChecker());
}
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.trees = Trees.instance(processingEnv);
this.messager = processingEnv.getMessager();
// 为所有checker置入工具实例
for (ExprCodeChecker<?, ?> c : this.codeCheckers) {
c.setTrees(trees);
c.setMessager(messager);
}
}
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
if (!env.processingOver()) for (Element e : env.getRootElements()) {
for (ExprCodeChecker<?, ?> c : this.codeCheckers) {
c.check(this.trees.getPath(e));
if (!c.isSuccess()) {
this.success = false;
this.errMsg.append(c.getErrorMsg()).append('\n');
}
}
}
/*
* 这里若return true将阻止任何后续可能存在的Processor的运行,因此这里可以固定返回false
*/
return false;
}
/**
* 获取代码检查的错误信息
*
* @return
*/
public String getErrMsg() {
return errMsg.toString();
}
/**
* 指示代码检查过程是否成功,若为false,则可调用getErrMsg取得具体错误信息
*
* @return
*/
public boolean isSuccess() {
return success;
}
}
这里面关键的就是实现init
方法和process
方法
init
方法初始化了Trees
工具并将其设置到了所有的ExprCodeChecker
(其实就是visitor)中; Trees是一个很重要的工具类实例,它能帮助我们获取AST结构对应的行号、列号等重要信息
process
方法则是真正对源码进行处理,在这里,真正调用了所有的ExprCodeChecker
(也就是visitor)
然后在使用Compiler API的时候,将processor设置到CompilationTask
中即可:
CompilationTask task = compiler.getTask(out, fileManager, diagnostics, options, null, ss);
if (processors != null) task.setProcessors(processors);
顺便贴下ExprCodeChecker
的代码如下,它就是一个对TreePathScanner
的简单继承,封装了一些代码分析过程中的基本属性和常用方法, 所有的visitor只要继承自它就可以了:
public abstract class ExprCodeChecker<R, P> extends TreePathScanner<R, P> {
// 当前被扫描代码对应的节点转换工具类, 运行时由Processor负责置入
protected Trees trees;
// 错误信息打印、处理流程控制工具, 运行时由Processor负责置入
protected Messager messager;
/**
* 取得代码检查的错误信息, 返回结果为null或空字符串串则表示无错误, 否则认为有错误发生
*
* @return
*/
public abstract String getErrorMsg();
/**
* 取得初始参数
*
* @return 用于遍历代码树的初始参数
*/
protected abstract P getInitParam();
/**
* 代码检查是否成功
*
* @return true - 成功,无问题; false - 失败,调用getErrorMsg可获取错误信息
*/
final boolean isSuccess() {
String err = this.getErrorMsg();
return err == null || err.length() == 0;
}
/**
* package访问权限,专门用于由Processor置入Trees工具实例
*
* @param trees
*/
final void setTrees(Trees trees) {
this.trees = trees;
}
/**
* package访问权限,专门用于由Processor置入Messager工具实例
*
* @param messager
*/
final void setMessager(Messager messager) {
this.messager = messager;
}
/**
* 开始遍历处理传入的代码树节点
*
* @param path
*/
final void check(TreePath path) {
this.scan(path, getInitParam());
}
/**
* 获取指定语法节点缩在源文件中的行号和列号信息, 用于错误信息输出
*
* @param node
* @return
*/
protected final String resolveRowAndCol(Tree node) {
CompilationUnitTree unit = this.getCurrentPath().getCompilationUnit();
long pos = this.trees.getSourcePositions().getStartPosition(unit, node);
LineMap m = unit.getLineMap();
return "row: " + m.getLineNumber(pos) + ", col: " + m.getColumnNumber(pos);
}
}
其中check
方法是遍历分析的起点,由processor调用
resolveRowAndCol
则是获取AST节点对应的行号、列号的方法,用于输出错误信息
在processor的init方法中被置入的Trees工具实例,最大的用处就是获取对应AST节点的行号、列号,具体代码参见上述resolveRowAndCol
方法
有必要讨论下什么样的控制,适合用Tree API来做?
总结起来应该是:静态的、简单的
比如“不能定义内部类”、“不能写annotation”、“不能写assert语句”等
随着需求的复杂度增高,使用Tree API的编码成本也会增高,毕竟使用visitor来分析复杂的AST模式并非十分容易的事情
比如上面的例子,“限制死循环”这种需求;如果说非常简单的死循环,比如while(true)
,这种是非常好做的
但如果稍微复杂一点, 比如while(1<2)
,那么这里势必会牵涉到一个”计算”过程,我们需要在分析过程中对1<2
这个表达式做出计算,从而知晓该循环语句是否死循环;虽然人眼对1<2
的结果一目了然,但这里靠程序来做的话,增加的复杂度还是相当可观的
如果在继续复杂下去,可以想象,其开发成本会越来越高,且这个分析过程本身的“运行”成本也会越来越接近真正运行这段被分析的代码的成本,这个时候使用Tree API来做分析就不划算了
所以说,考虑到成本因素,Tree API并不适合做太复杂的分析
其次就是”静态的”代码,才能在编译期做分析,如果是这样的代码:while(x<1)
,而x
又是从方法参数中传入,那么x
的值就完全在运行期才能确定,那么Tree API就无法判断该循环是否是死循环
还有就是Tree API很容易让人联想到一个问题:可否在遍历AST的过程中改变AST的结构?
这是个激动人心的话题,运行期改变源码的AST是一个想象空间很大的想法,就像在groovy和ruby中能办到的那样,这能成为一种强大的元编程机制
不过,从java的Tree API规范上讲,是不能在遍历AST过程中修改AST的结构的,但目前有一个bug可以做到:Erni08b.pdf
并且目前的Project Lombok就是基于此bug实现; 就未来的版本发展来讲,利用此bug来实现功能是不可靠的, Project Lombok的开发者对此也表示担心
不过这个bug也不失为一种必要时的选择,毕竟通过它能实现的功能很酷
2013-04-18 16:52:10,308 [AvatarRuleChargeService.java:74] [com.alibaba.itbu.billing.biz.adaptor.avatar.AvatarRuleChargeService] ERROR com.alibaba.itbu.billing.biz.adaptor.crm.ChargeProxy :: avatar charge sys error
com.alibaba.dubbo.rpc.RpcException: Failed to invoke the method match in the service com.alibaba.china.ruleservice.RuleService. Tried 3 times of the providers [172.22.6.83:20980, 172.22.6.80:20980, 172.22.9.76:20980] (3/3) from the registry dubbo-reg1.hst.xyi.cn.alidc.net:9090 on the consumer 172.30.118.26 using the dubbo version 2.4.9. Last error is: Failed to invoke remote method: match, provider: dubbo://172.22.6.83:20980/com.alibaba.china.ruleservice.RuleService?anyhost=true&application=billing&check=false&default.reference.filter=dragoon&dubbo=2.4.9&interface=com.alibaba.china.ruleservice.RuleService&methods=match&pid=18616&revision=1.0-SNAPSHOT&side=consumer&timeout=5000×tamp=1366275108588&version=1.0.0, cause: com.alibaba.com.caucho.hessian.io.HessianProtocolException: 'com.alibaba.china.ruleservice.RuleServiceImpl$DynamicPluginInvocationMatchedResult' could not be instantiated
com.alibaba.com.caucho.hessian.io.HessianProtocolException: 'com.alibaba.china.ruleservice.RuleServiceImpl$DynamicPluginInvocationMatchedResult' could not be instantiated
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:275)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:155)
at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:396)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2070)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2005)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1990)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1538)
at com.alibaba.dubbo.common.serialize.support.hessian.Hessian2ObjectInput.readObject(Hessian2ObjectInput.java:94)
at com.alibaba.dubbo.common.serialize.support.hessian.Hessian2ObjectInput.readObject(Hessian2ObjectInput.java:99)
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:83)
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:109)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:97)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:128)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:87)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:49)
at com.alibaba.dubbo.remoting.transport.netty.NettyCodecAdapter$InternalDecoder.messageReceived(NettyCodecAdapter.java:135)
at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream(SimpleChannelUpstreamHandler.java:80)
at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:559)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:274)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:261)
at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:349)
at org.jboss.netty.channel.socket.nio.NioWorker.processSelectedKeys(NioWorker.java:280)
at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:200)
at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:44)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:271)
... 28 more
Caused by: java.lang.NullPointerException
at com.alibaba.china.ruleservice.RuleServiceImpl$DynamicPluginInvocationMatchedResult.<init>(RuleServiceImpl.java:163)
... 33 more
at com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:101)
at com.alibaba.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:226)
at com.alibaba.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker.invoke(MockClusterInvoker.java:72)
at com.alibaba.dubbo.rpc.proxy.InvokerInvocationHandler.invoke(InvokerInvocationHandler.java:52)
at com.alibaba.dubbo.common.bytecode.proxy1.match(proxy1.java)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:307)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:182)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:149)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
at com.alibaba.itbu.billing.framework.aop.OpenApiLogAspect.logExecuteTime(OpenApiLogAspect.java:38)
at sun.reflect.GeneratedMethodAccessor199.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:627)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:616)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:64)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:89)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
at $Proxy107.match(Unknown Source)
at com.alibaba.itbu.billing.biz.adaptor.avatar.AvatarRuleChargeService.chargeByFactor(AvatarRuleChargeService.java:72)
at com.alibaba.itbu.billing.biz.charge.times.RuleChargeByTimesProcessor.getChargeResult(RuleChargeByTimesProcessor.java:62)
at com.alibaba.itbu.billing.biz.charge.times.ChargeByTimesProcessor.charge(ChargeByTimesProcessor.java:117)
at com.alibaba.itbu.billing.biz.task.ChargeByIncInstantTimesTask.charge(ChargeByIncInstantTimesTask.java:174)
at com.alibaba.itbu.billing.biz.task.ChargeByIncInstantTimesTask$1.run(ChargeByIncInstantTimesTask.java:109)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Caused by: com.alibaba.dubbo.remoting.RemotingException: com.alibaba.com.caucho.hessian.io.HessianProtocolException: 'com.alibaba.china.ruleservice.RuleServiceImpl$DynamicPluginInvocationMatchedResult' could not be instantiated
com.alibaba.com.caucho.hessian.io.HessianProtocolException: 'com.alibaba.china.ruleservice.RuleServiceImpl$DynamicPluginInvocationMatchedResult' could not be instantiated
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:275)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:155)
at com.alibaba.com.caucho.hessian.io.SerializerFactory.readObject(SerializerFactory.java:396)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2070)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2005)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1990)
at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:1538)
at com.alibaba.dubbo.common.serialize.support.hessian.Hessian2ObjectInput.readObject(Hessian2ObjectInput.java:94)
at com.alibaba.dubbo.common.serialize.support.hessian.Hessian2ObjectInput.readObject(Hessian2ObjectInput.java:99)
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:83)
at com.alibaba.dubbo.rpc.protocol.dubbo.DecodeableRpcResult.decode(DecodeableRpcResult.java:109)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCodec.decodeBody(DubboCodec.java:97)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:128)
at com.alibaba.dubbo.remoting.exchange.codec.ExchangeCodec.decode(ExchangeCodec.java:87)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboCountCodec.decode(DubboCountCodec.java:49)
at com.alibaba.dubbo.remoting.transport.netty.NettyCodecAdapter$InternalDecoder.messageReceived(NettyCodecAdapter.java:135)
at org.jboss.netty.channel.SimpleChannelUpstreamHandler.handleUpstream(SimpleChannelUpstreamHandler.java:80)
at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:564)
at org.jboss.netty.channel.DefaultChannelPipeline.sendUpstream(DefaultChannelPipeline.java:559)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:274)
at org.jboss.netty.channel.Channels.fireMessageReceived(Channels.java:261)
at org.jboss.netty.channel.socket.nio.NioWorker.read(NioWorker.java:349)
at org.jboss.netty.channel.socket.nio.NioWorker.processSelectedKeys(NioWorker.java:280)
at org.jboss.netty.channel.socket.nio.NioWorker.run(NioWorker.java:200)
at org.jboss.netty.util.ThreadRenamingRunnable.run(ThreadRenamingRunnable.java:108)
at org.jboss.netty.util.internal.DeadLockProofWorker$1.run(DeadLockProofWorker.java:44)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)
at java.lang.Thread.run(Thread.java:662)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:271)
... 28 more
Caused by: java.lang.NullPointerException
at com.alibaba.china.ruleservice.RuleServiceImpl$DynamicPluginInvocationMatchedResult.<init>(RuleServiceImpl.java:163)
... 33 more
at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.returnFromResponse(DefaultFuture.java:190)
at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:110)
at com.alibaba.dubbo.remoting.exchange.support.DefaultFuture.get(DefaultFuture.java:84)
at com.alibaba.dubbo.rpc.protocol.dubbo.DubboInvoker.doInvoke(DubboInvoker.java:96)
at com.alibaba.dubbo.rpc.protocol.AbstractInvoker.invoke(AbstractInvoker.java:144)
at com.alibaba.dubbo.rpc.listener.ListenerInvokerWrapper.invoke(ListenerInvokerWrapper.java:74)
at com.alibaba.dubbo.monitor.dragoon.filter.DragoonFilter.invoke0(DragoonFilter.java:82)
at com.alibaba.dubbo.monitor.dragoon.filter.DragoonFilter.invoke(DragoonFilter.java:34)
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
at com.alibaba.dubbo.monitor.support.MonitorFilter.invoke(MonitorFilter.java:75)
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
at com.alibaba.dubbo.rpc.protocol.dubbo.filter.FutureFilter.invoke(FutureFilter.java:53)
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
at com.alibaba.dubbo.rpc.filter.ConsumerContextFilter.invoke(ConsumerContextFilter.java:48)
at com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper$1.invoke(ProtocolFilterWrapper.java:91)
at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:53)
at com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:77)
... 32 more
看到这个日志第一反映是觉得RuleServiceImpl.java:163
数据错误,抛空指针,但检查那块代码发现那个地方根本不可能抛空指针 —— 所有用到的变量都是new
出来的;
而且,依据调用方提供的错误日志的抛出时间,我在被调用方系统的所有机器的日志里查找了一遍,没有发现对应的服务端日志;照理说服务端抛空指针,服务端也会有对应日志,但没有任何线索…
因此只好翻开了JavaDeserializer.java:275
作检查,发现有这么一段:
public JavaDeserializer(Class cl)
{
_type = cl;
_fieldMap = getFieldMap(cl);
_readResolve = getReadResolve(cl);
if (_readResolve != null) {
_readResolve.setAccessible(true);
}
Constructor []constructors = cl.getDeclaredConstructors();
long bestCost = Long.MAX_VALUE;
for (int i = 0; i < constructors.length; i++) {
Class []param = constructors[i].getParameterTypes();
long cost = 0;
for (int j = 0; j < param.length; j++) {
cost = 4 * cost;
if (Object.class.equals(param[j]))
cost += 1;
else if (String.class.equals(param[j]))
cost += 2;
else if (int.class.equals(param[j]))
cost += 3;
else if (long.class.equals(param[j]))
cost += 4;
else if (param[j].isPrimitive())
cost += 5;
else
cost += 6;
}
if (cost < 0 || cost > (1 << 48))
cost = 1 << 48;
cost += param.length << 48;
if (cost < bestCost) {
_constructor = constructors[i];
bestCost = cost;
}
}
if (_constructor != null) {
_constructor.setAccessible(true);
Class []params = _constructor.getParameterTypes();
_constructorArgs = new Object[params.length];
for (int i = 0; i < params.length; i++) {
_constructorArgs[i] = getParamArg(params[i]);
}
}
}
看完这段后,再结合远程调用的返回结果类后恍然大悟:
上面这段代码,是hessian在反序列化的时候,用于在被反序列化的类里面找一个“得分最低”的构造函数,反序列化时会加以调用;
构造函数的“得分”规则大致是:参数越少得分越低;参数个数相同时,参数类型越接近JDK内置类得分越低
而我们的调用返回的类只有一个构造函数,当然只有这个构造函数会被选中
但是,hessian反序列化调用被选中的构造函数时,是这样来创造该构造函数需要的参数的:
protected static Object getParamArg(Class cl)
{
if (! cl.isPrimitive())
return null;
else if (boolean.class.equals(cl))
return Boolean.FALSE;
else if (byte.class.equals(cl))
return new Byte((byte) 0);
else if (short.class.equals(cl))
return new Short((short) 0);
else if (char.class.equals(cl))
return new Character((char) 0);
else if (int.class.equals(cl))
return new Integer(0);
else if (long.class.equals(cl))
return new Long(0);
else if (float.class.equals(cl))
return new Float(0);
else if (double.class.equals(cl))
return new Double(0);
else
throw new UnsupportedOperationException();
}
可以看到,如果参数不是primitive
类型,会被null
代替;但不巧的是我们的构造函数内部对该参数调用了一个方法,因此抛出了空指针…
也就是说这个空指针是抛在客户端反序列化的时候而不是服务端内部,因此服务端当然找不到对应的错误日志了;
解决的办法也很简单:可以新增一个无参构造函数(无参构造函数肯定“得分”最低一定会被选中);或者修改代码保证任意参数为null
的时候都不会出问题
下面这个问题更有意思,说的是ThreadPoolExecutor
的RejectedExecutionHandler
的使用:
threadPool = new ThreadPoolExecutor(5, maxThreadNum, 5, TimeUnit.MINUTES, new SynchronousQueue<Runnable>(), tf,
new RejectedExecutionHandler() {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
logger.error("Ep thread pool exhausted, epId: " + id + "!!");
r.run();
}
});
这个代码是说,当我这个ThreadPool不够用,又不能再新增线程数的时候,由调用方线程自己来执行这个Runnable
任务…
本来这看上去没什么问题,问题出在这个Runnable
本身的实现上 —— 它内部将执行它的线程block到了一个blocking queue上面,当调用方主线程亲自来执行它时,使得主线程再也回不去做它自己该做的事情了,因此会出大问题…
所以这么看来,“当线程池不够用就让调用方线程自己来干”的这个策略在实际使用时要非常谨慎
这个问题是通过一个方便的thread dump分析工具: tda来查找的,因为出问题的这个应用是个多线程程序,有1600多个常驻线程,thread dump非常之大,肉眼直接看很不方便,但tda能将thread dump变得更友好易读,方便排查问题,在此推荐一下
]]>以目前的android街机三星Galaxy Note2为例(Android 4.1):
常见FAQ:
……
有任何问题可发邮件至: , 不定期可能会回复(当我心血来潮查看垃圾箱时)
若有与服务器列表页面相关的任何问题,可到此页面提交问题记录,会有人处理…
StringTemplate
这个东西,发现它不仅包含velocity几乎全部的语法,还有模板继承机制!这岂不是更复杂?虽然标榜为“为了代码生成”的模板引擎…但鉴于velocity
的复杂印象,我并没有使用它. java.text.MessageFormat
来做代码生成;这也确实可行!不过,缺点也是很快就能遇到的:
{
和}
在MessageFormat
中是关键字!所以输出时必须转义;这两个东西在目标代码中(比如java)很常见,遇到一个就要转义一个是很不爽的事情;MessageFormat
的java代码里;虽然很多时候确实可以这么做并且很合适,但是还是有一些情况,如果能写在模板里会明显比写在java代码里好;MessageFormat
的placeholder是数字,渲染时是根据传入参数的位置来做值替换,这很不直观,更好的用户体验应该是依据名字来替换,就像所有的、普遍意义上人们所见过的模板引擎那样;总结性地思考一下:我在做代码生成的时候,需要的模板引擎大概应该是:
而重点就在于第一点:简单,这需要我好好总结一份“用作代码生成的模板引擎所必须的语法特性”列表, 并且除此以外,不能有更多其它的东西;
至于高效,只要确保最终被反复执行的逻辑是一个编译好的java类就行了,而非遍历AST,这很容易办到;
而且..正好之前开发的dropincc.java需要为用户提供一个模板引擎,因为语法解析与代码生成在很多任务中是一对好基友,常常是伴随着出现;那么在dropincc.java中内建一个针对代码生成的模板引擎也就是非常有必要的了;它之于dropincc.java,就好比StringTemplate之于antlr,都是为了方便用户做代码生成创造的;
并且,这个模板引擎应该随着dropincc.java一起发布,不需要引入新的jar包依赖;
计划中,这个模板引擎的基本特性:
if-else
语句和foreach
语句, foreach
语句支持数组和Collection
的遍历, 也支持对”range常量”的遍历if-else
和foreach
语法,一方面这是为了利用已有的、强大的velocity语法高亮插件)String.valueOf(obj)
的结果一致==
、!
, 支持&&
或||
和括号优先级,因为我认为,用于代码生成的模板引擎的语法应该全是“开关”形式的,即“已经是判断的结果了”,非true
即false
(顶多再加上==
比较),不应该有过多的动态的复杂逻辑判断,这些判断其实应该放到调用这个模板引擎的java代码里, 而在模板里,只应该存在对这些bool值的与、或、非的各种组合判断==
操作符的判断结果与java中==
+ equals
函数的判断逻辑一致.
操作读取bean或map中的属性值,但不支持方法调用public static String merge(String filePath, Object context, Class<?> cls)
public static byte[] mergeAsBytes(String filePath, Object context, Class<?> cls)
public static String merge(String filePath, Object context, Class<?> cls, String encoding)
public static byte[] mergeAsBytes(String filePath, Object context, Class<?> cls, String encoding)
filePath
是模板路径; context
可是javabean或map,为模板上下文,包含要输出的元素和控制条件; cls
是用于加载模板资源的class类,模板资源查找规律与Class.getResourceAsStream
方法一致; encoding
是指定模板所使用的编码格式; 返回String
的方法即返回渲染后的字符串结果;返回byte[]
的接口则是直接返回带编码格式的、byte
的数组,这个结果可直接用作写入外部存储或流,而无需再应用编码格式;而基本上就是这些,不需要太多东西,可能第一眼会觉得“功能是不是太少”?但是,我已经见过了许多过重的逻辑被放在模板语法里的情况,与其增加这种不必要的、很可能是错误的使用方式(因为这样做确实不好维护),还不如一开始就不提供了;
并且,代码生成所需要的模板引擎,相比起做web页面所需要的模板引擎来讲,一定要简单许多;因为大部分的逻辑是应该放在java代码中(比如负责翻译代码的visitor)而不是模板里。
ok, 下面需要给出这个模板引擎的EBNF,这是最重要的准绳,几乎框定了绝大部分东西:
template ::= content $
content ::= renderable*
renderable ::= PLAIN_TEXT
| '#if' '(' boolExpr ')' renderable ('#elseif' '(' boolExpr ')' renderable)* ('#else' renderable)? '#end'
| '#foreach' '(' REF 'in' (REF|RANGE) ')' renderable '#end'
| REF
boolExpr ::= andExpr ('||' andExpr)*
andExpr ::= term ('&&' term)*
term ::= '!'* REF
| value '==' value
| '!'* '(' boolExpr ')'
value ::= '-'?[0-9]+('.'[0-9]+)?
| '\'' .* '\''
语法就是这样,应该已经足够;上述规则中,全大写字母的元素是terminal,一部分terminal的定义未在上面给出,补充在下面:
REF ::= '${' ID('.'ID)* '}'
ID ::= [a-zA-Z_][a-zA-Z0-9_]*
RANGE ::= '[' [0-9]+ '..' [0-9]+ ']'
总结起来讲,这个模板语法支持if
和foreach
语句,以及引用值的输出,其余的元素一律被看作PLAIN_TEXT
直接输出;
在if
语句中,逻辑表达式支持==
比较,以及“非”逻辑!
;支持添加圆括号调整运算优先级;
foreach
语句中,可以支持对引用(必须是Collection
或Array
)或RANGE
表达式的遍历; RANGE
表达式即生成一个数字序列的表达式,步长为1,如[1..10]
表示一个由1到10的数字序列;
至于具体的实现方式,仍然采用与dropincc.java一致的jdk1.6 compiler API的动态编译的形式,编译为字节码运行。
这个模板引擎将作为dropincc.java内部自己做一些代码生成,也同时提供给用户做代码生成工作,这样既能避免过多jar包的引入,又能施行一些最佳实践(“最佳”的意思是我这里定义的,在我看来是在“什么逻辑放在模板中”和“什么逻辑放在java代码中”找到一个平衡点,当然不同人可以有不同理解)。
资料: 用作测试,素数个数对照表: http://primes.utm.edu/howmany.shtml
扩展: 也许下一篇文章介绍一下“素数测试”? http://en.wikipedia.org/wiki/Primality_tests
TODO 后面再完成本文,反正有版本管理~
]]>Calculator.java
.calc ::= expr $
expr ::= addend (('+'|'-') addend)*
We specified that the overall expression
consists one or more addend
sub-expressions with '+'
or '-'
symbol delimited.
And for every single addend
expression:
addend ::= factor (('\*'|'/') factor)*
consists one or more factor
separated by '*'
or '/'
.
And finally, for any single factor
, is either a single digit or a quoted expression:
factor ::= '\d+(\.\d+)?'
| '(' expr ')'
Ok, we’ve done defining the EBNF of our calculator. This is what it look like all over:
calc ::= expr $
expr ::= addend (('+'|'-') addend)*
addend ::= factor (('\*'|'/') factor)*
factor ::= '\d+(\.\d+)?'
| '(' expr ')'
It’s quite short because it utilized the kleene notation to represent ‘zero or more’. And a shorter EBNF ends up a shorter dropincc.java program!
Now let’s ‘translate’ the EBNF into dropincc.java program. I say ‘translate’ here because it’s a really straightforward step:
// some elements' pre-definitions in order to call the methods
Lang calc = new Lang("Calc");
TokenDef a = calc.newToken("\\+");
TokenDef m = calc.newToken("\\*");
Grule expr = calc.newGrule();
Grule addend = calc.newGrule();
Grule factor = calc.newGrule();
// grammar definition section start
calc.defineGrule(expr, CC.EOF);
expr.define(addend, CC.ks(a.or("\\-"), addend));
addend.define(factor, CC.ks(m.or("/"), factor));
factor.define("\\d+(\\.\\d+)?")
.alt("\\(", expr, "\\)");
// grammar definition end
The ‘grammar definition’ part is as many lines as the EBNF. In fact it’s a line-by-line translation of the EBNF(please look at the ‘grammar definition’ section carefully).
Just a little more explanation:
TokenDef
s and Grule
s.(In fact Grule
means ‘grammar rule’) TokenDef
s are defined using java built-in regular expression currently.expr, CC.EOF
means expr $
, addend, CC.ks(a.or("\\-"), addend)
means addend (('+'|'-') addend)*
.CC.EOF
constant, it’s a predefined constant provided by dropincc.java to represent EOF
token definition.CC.ks
method call means all elements passed in as parameters are enclosed in a kleene star node(kleene star node means zero or more multiplication of its enclosing elements, the same as you learnt from using a regular expression). And ks
in fact means ‘kleene star’.CC.kc
method and CC.op
method which means kleene cross
(one or more) and optional
(zero or one) respectively but not used in the above example, you can browse them from source code.alt
method call. Look at the factor
rule definition, the factor
rule has two productions. And, respectively, in java code, it has an additional alt
call besides define
.‘Comma separated match sequence’, ‘CC.EOF’, ‘CC.ks/kc/op’ and ‘alt’, those are almost all you need to know to define a non-trivial DSL grammar in dropincc.java.
Ok, after we have defined the grammar, it’s time to add Action
s to process the elements the parser matched while parsing. It’s also achieved by a very intuitive way:
alternative production
(note: not one rule) could have only one action. That’s, the calc
rule could have only one action because it has only one alternative production. While factor
rule could have two actions because it has two alternative productions.factor
rule first:For the first alternative production:
factor.define("\\d+(\\.\\d+)?").action(new Action<String>(){
public Double act(String matched) {
return Double.parseDouble(matched);
}})
The first alt of factor
rule only matches a single string which have a ‘digit’ format. So the act
method of the Action
interface should be a single String
object holding the matched digit string. And, also, the generic type argument of Action
interface should be String
, to provide a strong type safety for the actually matched element.
We just parse the matched digit string to a number and returned it. So the return type of this action is Double
.
And the Action
for the second alt:
.alt("\\(", expr, "\\)").action(new Action<Object[]>(){
public Double act(Object[] ms) {
return (Double) ms[1];
}
});
The second alt is an expression enclosed by parentheses. There are three element in the match sequence: (
, enclosing expr
, and )
.
The type argument of Action
interface should be Object[]
since the matched element will be passed in as an object array of length 3. With first element (
, second element the returned value of the enclosing expr
rule’s action, and third element )
.
All we care about is the second element: the returned value of the enclosing expr
’s action. So we just return ms[1]
here, ignoring (
and )
. Note that we confidently converted the ms[1]
to a Double
object, that’s because we knew the return value of expr
rule’s action is definitely a Double
value.
addend
rule:It has only one production, so it could have only one corresponding action:
addend.define(factor, CC.ks(m.or("/"), factor)).action(new Action<Object[]>() {
public Double act(Object[] ms) {
Double f0 = (Double) ms[0];
Object[] others = (Object[]) ms[1];
for (Object other : others) {
Object[] opAndFactor = (Object[]) other;
if ("*".equals(opAndFactor[0])) {
f0 *= (Double) opAndFactor[1];
} else {
f0 /= (Double) opAndFactor[1];
}
}
return f0;
}
});
The argument of act
method is an object array of length 2. Its first element is the returned value from the matched factor
rule’s action,
and the second are other operator & factor pairs.
If there’s only one factor matched here, the ‘operator & factor’ pairs would be an array of length 0, otherwise it contains all other matched operators and factors.
You can see that the argument structure of method act
is highly consistent to the defined match sequence of the corresponding alternative production.
We wrote some logic to compute the total multiplication or division of all matched factors, and finally, returned the result as the result of this action.
expr
rule. It’s almost the same as the action of addend
rule, but in an add
or subtract
manner when computing the matched elements:The expr
rule also could have only one action:
expr.define(addend, CC.ks(a.or("\\-"), addend)).action(new Action<Object[]>() {
public Double act(Object[] ms) {
Double a0 = (Double) ms[0];
Object[] others = (Object[]) ms[1];
for (Object other : others) {
Object[] opAndFactor = (Object[]) other;
if ("+".equals(opAndFactor[0])) {
a0 += (Double) opAndFactor[1];
} else {
a0 -= (Double) opAndFactor[1];
}
}
return a0;
}
});
Finally, we add action for the calc
rule. It’s the starter rule. All computations should have been done at this point. So all we need to do in its action is return the final result, ignoring the EOF
:
calc.defineGrule(expr, CC.EOF).action(new Action<Object[]>() {
public Double act(Object[] ms) {
return (Double) ms[0];
}
});
Now, we have done defining a calculator and added proper actions for it, we could compile
it now:
Exe exe = calc.compile();
Then, enjoy with our new calculator:
// this should print the result: 3389.0
System.out.println(exe.eval("1 +2+3+(4 +5*6*7*(64/8/2/(2/1 )/1)*8 +9 )+ 10"));
It’s quite simple isn’t it? If you are already familiar with the API, 15 min is too much for defining such a calculator!
And the follow is all the code in a gist, you could copy and try it in your own IDE:
DSLs are rarely discussed in java community. Or, at least comparing to other dynamic languages’ world, DSLs in java is much harder to create.
Why does this happen?
According to Martin Fowler’s comments on DSLs, they are separated mainly to two kinds: internal and external DSLs.
I believe that, the ability of how a specific general purposed programming language could be customized to an internal DSL relies on the language’s meta-programming features.
Some languages, especially dynamic typed ones, have ability to customize its syntactical appearance at runtime. By intercepting their class defining life-cycles, executing fail-over strategies when method or field missings, or doing presentation substitutions via sophisticated macro systems or even, directly change their parse tree at runtime. All those incredible features are the magic behind internal DSLs.
But, we sadly found that java has none of those magic stuff to help building internal DSLs. There’s little meta-programming features in java. Annotations? Perhaps one rare creature, but its not enough.
Maybe the only way to create internal DSLs in java is to design a set of Fluent Interface API. It’s great, but you won’t have much space on customizing the syntax of your DSL.
Then, seems that the possible ways to create real-world, sophisticated DSLs in java are always turns out to be external-DSLs. But, this won’t be easy, always, since the creation of computer world.
Creating external DSLs means doing lexical & syntactical analyzing all by ourselves. There is a whole bunch of open sourced tools, say, parser generators, to help doing so, but there’s too much unnecessary efforts must be made before you get your grammar run.
Those traditional parser generators are great but in fact not suitable for DSL creating. Like lex&bison, javacc, jcup and antlr etc. They usually have their own grammar you must learn to define grammar rules, this is some what big learning cost.
After you have done writing your grammar, they usually requires you to run a specific compiling tool in command-line to ‘compile’ the rules you just wrote. And finally gives you a couple of source files in your host language and telling you to place them into somewhere of your project directory. Then you could run your program. If errors occur, you would re-do those steps by frequently switching between command-line and your IDE.
These steps are troublesome in practice. Especially for those first using a parser generator. I’ve seen one of my colleague struggled against antlr for one month, in order to build a SQL-like DSL. And much of his time was spent on getting familiar with antlr’s grammar, its ‘antlrWorks’ IDE and some command-line tools. It was painful when recall on this month.
So I think one must not have to carry so much burden when he just want to create a relatively simple DSL. And dropincc.java is something I create to ease the building process of external DSL in java.
And, all in all, in summary… dropincc.java ends up such a tool with those missions. I hope this small lib could help a lot in building external DSLs.
For a starter dummy example, please refer to this post.
]]>Dropincc.java is a dynamic parser generator which allows you crafting your DSLs in pure java, using your favorite editor.
Now it reached its initial release version 0.1.0. You can download it from the project page and play around with it.
There’s a detailed Quick Start guide on the project page, it could guide you through to have a overall feeling on using dropincc.java.
Requires JDK 1.6 or above. Firstly, place the dropincc jar file in your class path.
And then, crafting your language’s EBNF definition:
Let’s define a language which accepts arbitrary numbers of quoted ‘a’ or ‘bc’. For example: ‘(a)(bc)’, ‘(bc)((a))’ or ‘(((bc)))(bc)’, the numbers of pairs of parentheses is unlimited.
The EBNF of this dummy language:
S ::= L $
L ::= Q*
Q ::= 'a'
| 'bc'
| '(' Q ')'
Ok, after defining the EBNF representation of our language, it’s time to translate the EBNF into dropincc.java representation. I say ‘translate’ here because this is a line-by-line straightforward step:
public static void main(String... args) throws Throwable {
Lang lang = new Lang("Dummy");
TokenDef a = lang.newToken("a");
TokenDef bc =lang.newToken("bc");
TokenDef lp = lang.newToken("\\(");
TokenDef rp = lang.newToken("\\)");
Grule L = lang.newGrule();
Grule Q = lang.newGrule();
lang.defineGrule(L, CC.EOF); // S ::= L $
L.define(CC.ks(Q));// L ::= Q*
/*
* Q ::= 'a'
* | 'bc'
* | '(' Q ')'
*/
Q.define(a)
.alt(bc)
.alt(lp, Q, rp);
}
Ok, we have done defining the grammar rules of our dummy language, so easy:
L, CC.EOF
means L $
. CC.EOF is a predefined TokenDef provided by dropincc.java.alt
method call on a grule element. For example, the Q
rule has three alternative productions.CC.ks
and CC.kc
utility functions. See the line: L.define(CC.ks(Q));// L ::= Q*
.That’s almost all you need to learn about how to define a grammar using dropincc.java.
After that, you can compile
your grammar:
Exe exe = lang.compile();
This step would make you a callable object named ‘exe’. You can use this object to eval
and test your newly defined language:
System.out.println(exe.eval("a(a)((bc))"));
This would print the output: [Ljava.lang.Object;@67a5a19
. It’s an object array which holds the whole parse tree:
But it’s useless just getting the parse tree. We should do something to manipulate the parse tree and produce some ‘results’ of this parsing.
Ok, suppose we have a dummy task: to remove all parentheses from the input string, leaving only the 'a'
and 'bc'
s there. Then at last, return the resulting string as the return value of eval
method call on exe
.
This involves adding actions
to the grammar. The action
s are some kind of code block, it should execute when its’ corresponding grammar rule production matches.
In dropincc.java, actions
are implemented by closure
s, using the Action
interface. We try to add an action to the Q
rule:
Q.define(a).action(new Action() {
public String act(Object matched) {
return (String) matched;// string 'a'
}
}).alt(bc).action(new Action() {
public String act(Object matched) {
return (String) matched;// string 'bc'
}
}).alt(lp, Q, rp).action(new Action() {
public String act(Object matched) {
// returns the middle element matched, that's the returned value of the enclosing 'Q' rule
return (String) ((Object[]) matched)[1];
}
});
Above is the Q
rule with three actions, each action for one alternative production. In fact since the first two actions does noting but just returning the matched string, so they are not necessary. The Q
rule with action could be defined as follows:
Q.define(a)
.alt(bc)
.alt(lp, Q, rp).action(new Action() {
public String act(Object matched) {
// returns the middle element matched, that's the returned value of the enclosing 'Q' rule
return (String) ((Object[]) matched)[1];
}
});
Leaving only the third action there. The third action, returned the middle element matched of its production(ignored left and right parentheses). The middle element matched is the return value of the recursively enclosing Q
rule.
The action for L
rule is quite simple and significant: it collects all strings matched and concatenates them into a whole large string, then return the resulting string:
L.define(CC.ks(Q)).action(new Action() {
public String act(Object matched) {
Object[] ms = (Object[]) matched;
StringBuilder sb = new StringBuilder();
for (Object m : ms)
sb.append(m);
return sb.toString();
}
});// L ::= Q*
Then the last top-level rule’s action: just return the value of L
rule, just ignoring EOF:
lang.defineGrule(L, CC.EOF).action(new Action() {
public String act(Object matched) {
return (String) ((Object[]) matched)[0];
}
}); // S ::= L $
Ok, we can now run the program again and could see it prints out the output: aabc
, with all parentheses removed.
The whole program in a gist:
You can download and play around with it on your own.
Dropincc.java is currently on its very initial stage. It has some major improvements to be done:
There is a more practical Hello World example which implements a full-featured calculator in the quick start guide of dropincc.java. On its project home page. Go and check out there if you like.
]]>…真相是…它只是停止生长的一棵小树而已…
因为..
..营养不足…所以“只能成长到这地步”..
原来我们熟知的“这个世界”,只是无比巨大的世界中的…
“一小部分啊”…
LL(*)是由Terrence Parr教授创造的,为使用LL分析的语法解析器做超前查看(look ahead)的一种算法。按照Parr教授的说法,这个算法从最初的想法至今的完善和调整已经历了15年。目前该算法的一个著名的实现在antlr3中。
A ::= a b c | a b e
, 显然这是一个LL(3)语法;A ::= a b* c | a b* e
是无法被LL(k)识别的,因为中间的b*
代表“0个或多个b”(kleene闭包),这并不是一个固定的重复次数,因此LL(k)无法识别,for any k…A ::= a b* c | a b* e
语法使用了“kleene闭包”表示法来表示“0个或多个”,这种表示法在正则表达式中很常见;事实上,基于LL(*)算法实现的语法解析器生成器(比如antlr3)对Kleene闭包表示法特别友好,可以鼓励使用,还能顺便解决一些LL分析法所不允许的“左递归”。Parr教授在2011年发表的paper”LL(*): The Foundation of the ANTLR Parser Generator”中详细描述了这个算法,下面就是其主要过程,具体的代码实现(python)在工程llstar中
为了简单起见,约定大写字母用来命名non-terminal, 小写字母命名terminal或Semantic Predicate,$
表示EOF,假设有如下语法规则:
S ::= A $
A ::= a c*
| b c*
atn_creation.py
的rule.merge_to_atn
方法中。closure
函数经过non-terminal边时对其所对应的ATN的调用和返回。S
规则最后的DFA为:, well, 就一个终结态,没任何状态转换 —— 这是当然的,S规则只有一个alternative production… A
规则的DFA为:, 意思就是,如果第一个token为a
,就选择第一个分支,若是b
,就选择第二个分支,这是一个LL(1)的语法。OK,主要的算法原理了解之后,展示了2个玩具例子,似乎还不能看出LL(*)的特点来。
下面我将逐步展示一些真正non-trivial的例子,以表明它真的不是过家家…
LL(3)(更正:或许这该是LL(4)可能我数错了)语法(对应llstar工程中的LL_3_many_alts.py
):
S ::= A $
A ::= B a*
| C a+
| D a?
B ::= a b c C
| a b c D
| d
C ::= e f g D
| e f g h
D ::= i j k l
| i j k m
这个语法是固定的LL(3),没有什么特别,不过分支比较多, 它的ATN:
规则A
的DFA:
规则B
的DFA:
规则C
的DFA:
规则D
的DFA:
上面这个语法,LL(4)分析也能搞定,而真正体现出LL(*)特点的是下面这样的语法(对应llstar工程中over_kleene.py
):
S ::= A $
A ::= a* b
| a+ c
其中,规则A
的2条分支拥有共同的kleene闭包前缀,LL(k)是无法识别的,它的ATN:
而LL(*)能够为A
生成这样的DFA:
这个DFA能够越过terminala
的任意重复,从而考虑到后面的terminalb
和c
来做判断
而下面这个更加”恶劣”的列子则更能说明LL(*)的这个特征(对应llstar工程中over_non_recurse_rule.py
):
S ::= A $
A ::= B* b
| C+ c
B ::= a d
| e
C ::= a d
| e
这个例子中,B
和C
完全相同,并分别被包含在规则A
的2条分支的kleene闭包中,
最终生成的A
的DFA:
上面提到过,LL(*)也有不能解决的情况,比如2条分支拥有”共同的、递归的规则作为前缀”,比如这样:
S ::= A eof
A ::= E a
| E b
E ::= c E d
| e
上面的A
规则的两条分支拥有共同的前缀E
,而E
本身是一条递归规则,这样的规则是LL(*)无法分析的,在llstar工程中对应experiments/common_recurse_prefix_fail.py
, 运行的时候会抛出错误提示。这种情况在antlr3中会将parsing策略退化为LL(1) + 回溯的形式。
Traceback (most recent call last):
File "/home/pf-miles/myWorkspace/llstar/experiments/common_recurse_prefix_fail.py", line 31, in <module>
d_a = create_dfa(ra.get_start_state(globals_holder.a_net))
File "/home/pf-miles/myWorkspace/llstar/algos.py", line 136, in create_dfa
d_state_new.add_all_confs(closure(d_state, conf))
File "/home/pf-miles/myWorkspace/llstar/algos.py", line 99, in closure
raise Exception("Likely non-LL regular, recursive alts: " + str(d_state.recursive_alts) + ", rule: " + str(globals_holder.a_net.get_rule_of_state(p)))
Exception: Likely non-LL regular, recursive alts: set([0, 1]), rule: E
还有一些特殊情况,比如规则完全就是冲突的,那么这个时候就是Semantic Predicate发挥作用的时候(对应llstar工程中conflicting_with_preds.py
):
S ::= A $
A ::= {pred1}? B
| {pred2}? b
B ::= b
| b
最终A
的DFA:
实际开发中,写出这样的规则是坑到爷的…不过LL(*)还算能很好解决…其实这里就算没有Semantic Predicate, LL(*)也能按照convention,“选择较早定义的语法分支”来解决。
还有就是在分析的过程中会导致分析栈溢出(不是程序语言的callstack的overflow, 而是LL(*)的closure操作维护的一个规则调用栈溢出)的时候,LL(*)也会使用Semantic Predicate来辅助判断, 比如规则(对应llstar中overflow_with_preds.py
):
S ::= A $
A ::= {pred1?} B a
| {pred2?} b* c
| {pred3?} b* c
B ::= b+ B
规则A
的三条分支全部存在严重冲突(都拥有能识别无穷个’b’的前缀)并且其中一个是另一条递归规则B
,这种形式会导致LL(*)的分析栈溢出,不过仍然没关系,这样的情况能在溢出发生时,调用pred1
、pred2
和pred3
来解决(这三个pred就是Semantic Precicate,是能包含任何逻辑但最终返回bool值的表达式)
生成的规则A
的DFA:
这里设定的最大分析递归层数为10
,因此,DFA在接受了第10个’b’之后达到了溢出状态d14
,这时调用了各个alternative附带的几个predicate来解决冲突…
(2012-09-03 注: 实际上上述规则是不正确的,
B ::= b+ B
是有问题的规则,因为b+
会贪婪地match掉所有的连续的'b'
,导致后面的B
只可能抛错; 所以这个例子也只能用来演示分析栈溢出的情况,实际应用中是不可能写出这样的规则的)
OK, 大概如是了;上述算法的实现、例子的代码均在llstar工程中能找到
llstar工程是我花了不少时间实现并调试好的一块LL(*)算法的“实验台”,可以在里面慢慢把玩各种变态的、五花八门的语法规则,以验证LL(*)的分析能力
在工程的experiments
文件夹下已经有许多具有代表性的例子…都是python代码
另外,llstar工程是在eclipse + pydev环境下开发的,因此如果要在命令行里运行,可能要对例子里面的import路径稍作修改,当然,最好是直接在eclipse + pydev环境下运行它们了
在linux环境下,llstar的所有例子都能自动生成上文中看到的各种DFA/NFA图像,不过前提是环境中要装有graphviz
现在的所有语法解析器生成器中,似乎都是LR分析的天下,LL分析大多是手写解析器的首选…
不过在现有的使用LL分析方式的产品中,也就只有LL(*)和PEG(Packrat)parsing比较常见也比较实用了,能与LR分析一较高下。
而配上回溯策略的LL(*)是严格强于Packrat parsing的,这样看来大概走LL分析路子的自动化工具也只有LL(*)容易一枝独秀了(想必这也是antlr相对较为流行的原因)。
对于这个式子,我个人的、不严格的、直觉的理解就是:
不过其实这个是有严格的证明的:
设$T(n) = aT(\frac{n}{b}) + cn^d$
对于第j层递归 ,运算量为:$a^j \times c \times (\frac{n}{b^j})^d$
即:$cn^d(\frac{a}{b^d})^j$,所以a和$b^d$的大小关系是关键!
进而总计算时间: $total work \leqslant cn^d \centerdot \sum\limits_{j=0}^{\log_bn}(\frac{a}{b^d})^j$, 然后通过这个式子,由a和$b^d$的大小关系可以较容易想通为什么master method会是上述这种三段分段函数的形式。
Week2作业是快排…写个快排倒是简单,可是作业要求要count快排过程中的比较次数…在作业论坛里面看到哀嚎声一片…毕竟实现只要稍微有点偏差,虽然同样是快排,确实很容易比较次数不一样。
值得一提的是讲解中给出的in-place的快排partition方法,就是不用建立额外的数组来进行partition,python示意代码如下:
def partition(pivotIndex, arr):
swap(arr, 0, pivotIndex)
pivot = arr[0]
i = 1 # index 0...i < pivot
j = 1 # index i...j > pivot
for k in range(1, len(arr)):
if arr[k] <= pivot :
swap(arr, i, k)
i += 1
j += 1
else:
j += 1
return (arr[1:i], pivot, arr[i:j])
另外,从快排的例子可以看出,若divide and conquer算法是基于某种比较结果,将问题规模初步divide and conquer的话,每一次比较“能否将问题划分为规模差不多均等的子问题”将很大地影响算法实际的执行效率。
]]>addition ::= addition '+' factor
| factor
这个式子其实是想要表达一个“一连串+号组成的和”的表达式,例如1+2+3+4
;
假设目前的输入就是1+2+3+4
, 那么超前查看带来的选择应该是addition '+' factor
这个产生式;于是在parser的代码中立刻就又直接调用了addition()
方法,但注意,整个递归过程没有消耗掉任何token…也就是说,当流程再次递归到addition
函数里时,输入1+2+3+4
并没有任何改变,这又会进入下一轮递归…很快就会overflow。
这个问题,除了机械性地按照公式来消除左递归外,还可以改写语法规则,使其成为右递归语法:
addition ::= factor '+' addition
| factor
这样就不用stackOverFlow了,但是带来另一个问题:这使得我们的’+’加法成为了一个’右结合’的运算…
也就是说,1+2+3+4
的计算次序是这样的:1+(2+(3+4))
还好这里是“加法”,整数集合和加法运算构成“阿贝尔群(可交换群)”,满足交换律,所以就算实现成右结合的也能蒙混过关…但减法呢?除法呢?
难道要先生成一个AST,再写个visitor,把节点顺序给rewrite一下,然后再写个visitor求值?这么麻烦就为了实现一个简单的四则运算功能也太坑爹了。
其实,我们在这里使用递归,只是为了表达元素的“重复”这种意思;的确,递归能表示“重复”的概念,但这很容易让人联想到“循环”,因为平时的编程中,我们都通常使用循环来表达“重复”这种东西的。
更重要的是,循环与尾递归在逻辑上是等价的,这使得我们可以将上述左递归改写成某种表示“循环”的形式,在这里,Kleene Closure(克林闭包)就正是一种表达“循环”的标记形式。
熟悉正则表达式的我们都知道,符号*
(kleene star)和符号+
(kleene cross)分别用来表示前面临接元素的“0个或多个”和“一个或多个”,如果这种标记用在我们的语法规则中那就酷毙了:
addition ::= (factor '+')* factor
表达的意义和”左递归版本”的语法规则是一样的,但是,这种记法的重大意义就是:在生成的parser程序中可以直接以一个while
语句来解析重复的(factor '+')
而不用再递归回addition
中!这样就避免了parser中的左递归实现带来的问题!
由于kleene闭包所能表达的所有语法规则形式都能被递归所表达,所以引入kleene闭包并没有从形式上增加语法解析器生成器的表达能力,所以说kleene闭包只是一个“语法糖”。
但它的实现意义却是重大的:解决了“为了表达‘元素重复’”的这一类左递归问题。
当然,dropincc.java也会支持kleene闭包的表达形式,大致的api形式如下:
addition.fillGrammarRule(addend, CC.ks(ADD.or(SUB), addend));
CC.ks
方法就是kleene star的意思,当然还有CC.kc
,顾名思义。
而”piggyback”的意思就是:记住一些经典算法的实现,遇到类似问题的时候,可以将要解决的问题“搭载”到那些经典算法上面,一般都具有和那些经典算法相同的平均时间复杂度;相当于是把那些经典算法当作一个个“模式”或者“骨架”了(所以说平时经常听到的经典算法还是要熟悉熟悉的,很多地方能派上用场)。
这一周围绕merge sort来讲divide and conquer真是再合适不过了;而作业“Count Inversions”就是一个将场景piggyback到merge sort上的一个典型例子; 作业给出了一个包含10W行数据的文本文件,每一行一个随机数字,要求找出这10W个数字中的所有Inversions。我用python实现如下:
def countInversions(arr):
length = len(arr)
if length == 0 or length == 1:
return (0, arr)
mid = int(length / 2)
(leftCount, leftSortedArr) = countInversions(arr[0:mid])
(rightCount, rightSortedArr) = countInversions(arr[mid:length])
(mergedCount, mergedSortedArr) = mergeCount(leftSortedArr, rightSortedArr)
return (leftCount + rightCount + mergedCount, mergedSortedArr)
def mergeCount(lArr, rArr):
count = 0
mergedArr = []
i = 0
j = 0
lenl = len(lArr)
lenr = len(rArr)
while i < lenl and j < lenr:
if lArr[i] > rArr[j]:
count += lenl - i # key line
mergedArr.append(rArr[j])
j += 1
else:
mergedArr.append(lArr[i])
i += 1
if i < lenl:
mergedArr.extend(lArr[i:lenl])
if j < lenr:
mergedArr.extend(rArr[j:lenr])
return (count, mergedArr)
import sys
if len(sys.argv) < 2:
print 'usage: python countInversionInAFile path_to_file'
exit(1)
numbers = [int(line) for line in open(sys.argv[1], 'r')]
print countInversions(numbers)[0]
程序或许想起来简单但实际上要正确还是要费点功夫的;有些地方没想到的还非得debug才能看清楚为什么,比如程序中标注为”key line”那里,本来我写的是count += 1
的但怎么都不对,后来debug才看出来应该写count += lenl - i
。
另外,本来打算跟2门课程的,但现在看来我这忙碌的程序员完全没有时间…只好放弃“机器人汽车”那门课目前专门跟这一门了,毕竟“算法”的可操作性要强一些…
]]>这种形式能够使得这一套API看起来更具有“业务语义”,而不需要像antlr等其它工具一样强迫用户去学习全新的另外一种含有业务语义的DSL。
新语言的词法、语法规则被用户直接在java语句里书写、执行,其实得到的是dropincc.java这个CC工具的内部概念的AST。这个AST其实与“让用户学习一套DSL,然后编写,然后编译这些DSL得到的AST”是一样的。
也就是说 —— dropincc.java里面很重要的一个理念就是:像书写字面量一样书写CC工具的AST(AST literal???)。
注意:强调一下这里说的AST并非用户想要实现的新语言的AST,我指的是dropincc.java这个工具其内部表达词法、语法规则的内部形式。
经过一些初步的尝试,我认为在java中定义出这样一套API是完全可行的,其中一些值得列出的、具有代表性的点如下:
term ::= LEFTPAREN expr RIGHTPAREN
, 则右边的部分在java中表达为:func(LEFTPAREN, expr, RIGHTPAREN)
;即:“以方法传参的自然按顺序排列表达‘连接’的意思”;而其中expr
是一个non-terminal,也就是说这里要引用expr
这个规则,这也就表达了“递归” —— 要递归地引用expr
的话那么直接写在这里就好了;term ::= DIGIT multail | LEFTPAREN expr RIGHTPAREN | DIGIT;
, 则java中表达为:func(DIGIT, mulTail).alt(LEFTPAREN, expr, RIGHTPAREN).alt(DIGIT);
。MUL_DIV ::= MUL | DIV;
mulTail ::= MUL_DIV term;
mulTail ::= (MUL | DIV) term;
, 少了MUL_DIV
这个没太多意义的产生式定义;func(MUL.or(DIV), term)
; 若子规则中想表达的不是“选择”关系,而是“连接”关系的话,也就是mulTail ::= (MUL DIV) term;
的话,在java中表达为:func(MUL.and(DIV), term)
; 其中and
这个方法就是表达“连接”关系,只不过加了括号,成为了一个子规则而已。OK, dropincc.java的核心FluentInterface形式的API应该就这些了,我相信这是一种很自然的方式,用户需要掌握的东西很少。
对于这个接口设计,我的经验就是:
几乎所有的CC工具都提供一种“在match到特定的节点或树模式时允许执行一段用户自定义代码”的功能;这种自定义代码我把它叫做“动作代码”, “action”。无论是lex & yacc或是antlr都是这样。
这个功能几乎是必须的,这提供了一种自由度相当高的形式让用户可以自定义AST节点,或是直接在parse的过程当中做一些自定义的计算,然后当整个parse结束时直接输出的就是整个的计算结果。
动作代码一般来讲,都是用该CC工具所要生成的程序所使用的语言来写的;比如lex要生成C代码,他们的动作代码就是C来写的;antlr要生成java代码,那么它的动作代码自然就是java写的(当然antlr也能生成其它语言代码,相对应的,就使用那种语言来编写动作代码)。
dropincc.java既然设计目标是要嵌入到java语言中去使用,自然就是要用java语言来写动作代码了;
但是,跟lex & yacc或者antlr不同的是,我并不打算让用户写一些残缺不全的动作代码放在某个约定的位置然后在生成目标程序的过程中将它们“编织”到目标程序中,这样的话就跟传统的CC工具没有任何区别了,没有什么实质上的改进;
既然dropincc.java是“嵌入”到普通的java应用程序代码中间工作的,那么它完全有理由使用“闭包(closure)”的形式来组织“动作代码”!为什么用“闭包”?因为dropincc.java是“嵌入”到业务代码中使用的,用闭包来写动作代码可以capture一些业务代码上下文信息,这对构建DSL来讲是非常大的便利!
比如,你的业务代码上下文里面有个OrderService
对象,它负责一些”订单”业务逻辑,而当你编写一个闭包作为动作代码的时候,你完全可以”capture”上下文中的这个OrderService
对象,从而使得你创造的语言在执行逻辑上与订单业务紧密、无缝地结合 —— 这是一种非常平滑的构建DSL的方式。
注:此段代码只代表示意的风格、API情况,虽然能编译通过、运行,但不一定是行为完全正确四则运算表达式计算器
// 3.define lexical rules
Lang calculator = new Lang();
Token DIGIT = calculator.addToken("\\d+");
Token ADD = calculator.addToken("\\+");
Token SUB = calculator.addToken("\\-");
Token MUL = calculator.addToken("\\*");
Token DIV = calculator.addToken("/");
Token LEFTPAREN = calculator.addToken("\\(");
Token RIGHTPAREN = calculator.addToken("\\)");
// 2.define grammar rules and corresponding actions
Grule expr = new Grule();
Grule term = new Grule();
Element mulTail = calculator.addGrammarRule(MUL.or(DIV), term).action(
new Action() {
public Object act(Object... params) {
return params;
}
});
term.fillGrammarRule(DIGIT, mulTail).action(new Action() {
public Object act(Object... params) {
int factor = Integer.parseInt((String) params[0]);
Object[] mulTailReturn = (Object[]) params[1];
String op = (String) mulTailReturn[0];
int factor2 = (Integer) mulTailReturn[1];
if ("*".equals(op)) {
return factor * factor2;
} else if ("/".equals(op)) {
return factor / factor2;
} else {
throw new RuntimeException("Unsupported operator: " + op);
}
}
}).alt(LEFTPAREN, expr, RIGHTPAREN).action(new Action() {
public Object act(Object... params) {
return params[1];
}
}).alt(DIGIT).action(new Action() {
public Object act(Object... params) {
return Integer.parseInt((String) params[0]);
}
});
Element addendTail = calculator.addGrammarRule(ADD.or(SUB), term)
.action(new Action() {
public Object act(Object... params) {
return params;
}
});
expr.fillGrammarRule(term, addendTail, CC.EOF).action(new Action() {
public Object act(Object... params) {
int addend = (Integer) params[0];
Object[] addendTailReturn = (Object[]) params[1];
String op = (String) addendTailReturn[0];
int addend2 = (Integer) addendTailReturn[1];
if ("+".equals(op)) {
return addend + addend2;
} else if ("-".equals(op)) {
return addend - addend2;
} else {
throw new RuntimeException("Unsupported operator: " + op);
}
}
});
// 1.compile it!
calculator.compile();
// 0.FIRE!!!
System.out.println(calculator.exe("1+2+3+(4+5*6*7*(64/8/2/(2/1)/1)*8+9)+10"));
上面约60行java代码实现了一个相当复杂的多项式计算器语言:支持加减乘除四则运算以及括号调整优先级。而以往我们使用antlr来实现这个功能的话,最终生成、放到项目中工作的java代码会以千行计。
Action
接口实现的闭包(匿名内部类),里面的代码显然可以随意引用当前上下文中的所有变量、对象,这样实现了对上下文的capture,跟业务系统的无缝结合。Action
闭包带有一个方法,参数是Object... params
,这个参数里的值以位置顺序的方式对应于该action代码所附着的产生式里面的语法元素在parsing的时候所match到的值,听起来比较绕口,但实际上很简单,比如上面的 :带括号的表达式的action:
.alt(LEFTPAREN, expr, RIGHTPAREN).action(new Action() {
public Object act(Object... params) {
return params[1];
}
它的params
的长度为3,分别是["(", expr子规则匹配所返回的值, ")"]
,而这里我们的“业务(四则运算)”只需要关心expr
的返回值,所以动作代码直接将params[1]
(expr的返回值)返回给上一级匹配。
就是这样,实际上你可以在动作代码里面做任何事情,只要符合java语法的、你能想到的。
第一个版本的API设计基本就是这样了,往后可能还会加入一些语法糖,比如“Kleen闭包”来进一步方便语法规则的定义,不过会很慎重,因为dropincc.java是以小巧、易用为设计目标的,语法糖太多很可能会帮倒忙。
]]>其实由于java语言先天缺陷的元编程能力,在java的圈子里很少有人提到“DSL”的概念,就算有也是说说而已。与之相比,一些其它语言ruby, python, lisp等等,得益于其强大的元编程能力,直接利用自身语法特性,取一块语法子集,利用一些对象、类生命周期切入函数或者宏系统两三下就DSL ‘on-the-fly’了,这种便利性可能由java的设计目标来看,恐怕永远也无法达到了。
那么,在java里面想实现一个DSL,恐怕就得走“外部DSL(详见Fowler的文章: DomainSpecificLanguage)”的路子。
而“外部DSL”这条路并不好走,说白了也就是自己做词法、语法、语义分析,自己生成目标语言代码去执行真正的逻辑;也就是做一个编译器前端外加一段解释执行逻辑;编译器前端,虽然业界看上去有很多很多工具可以直接采用,但是这些工具,目前看来,我认为还是太“General purpose”了,它们往往要求使用者学习它们自己规定的一套用作表达词法、语法规则的DSL,然后提供一个特殊的编译程序,用作编译你用它的DSL写的这些词法、语法文件,最终生成你想要的、基本不可读的编译器前端代码 —— 放到你的项目中去使用;
这种形式的工具在目前的编译器前端生成器中占大多数,使用上绝对称不上是“便利”。除了它用作描述规则的DSL需要使用者学习之外,其使用过程涉及好几个步骤与环境,是很麻烦的一件事情。
曾经有一次,一个同学试图使用antlr来做一个类似SQL子集的DSL,他花很多时间与antlr本身的语法文件、IDE “antlrworks”作斗争…频繁地在eclipse, 命令行, antlrWorks之间切换;我就想,做一个这样的DSL,是否真的用得上这么多工具?是否真的用得上号称能parse C++的antlr?这些事情能否全部在一个工作环境内搞定?比如eclipse?
而来自rsec的启发让我觉得java似乎更应该有这样一个东西:
而Dropincc.java正是以上述几点为设计目标的一个小巧(但足够强大)的编译器生成器。
以上第一点,基本上的想法就是,设计一套“串接与组合(cascading & composition)”风格的API,模拟出一套parser combinator,让用户使用这套“貌似带有CC的语义”的java方法API来做词法、语法规则的添加(CC即:”compiler-compiler, 就是’编译器生成器’”)。
比如我想创建一套支持加减乘除的表达式计算器,还能使用括号调整运算优先级,我大概能用下面这样的形式直接在java代码中定义其词法、语法规则:
注:下面的代码只是示意说明整体API风格,不一定是最终能运行的代码。
Lang lang = new Lang();
Token DIGIT = lang.addToken("\\d+");
Token ADD = lang.addToken("\\+");
Token SUB = lang.addToken("\\-");
Token MUL = lang.addToken("\\*");
Token DIV = lang.addToken("/");
Token LEFTPAREN = lang.addToken("\\(");
Token RIGHTPAREN = lang.addToken("\\)");
Grule expr = new Grule(); // grammar root
Grule term = new Grule();
Element mulTail = lang.addGrammarRule(MUL.or(DIV), term);
term.addGrammarRule(DIGIT, mulTail)
.alt(LEFTPAREN, expr, RIGHTPAREN)
.alt(DIGIT);
Element addendTail = lang.addGrammarRule(ADD.or(SUB), term);
expr.addGrammarRule(term , addendTail, CC.EOF);
其实想要表达的规则写成类似”BNF”的形式就是:
// token rules
DIGIT ::= '\d+';
ADD ::= '+';
SUB ::= '-';
MUL ::= '*';
DIV ::= '/';
LEFTPAREN ::= '(';
RIGHTPAREN ::= ')';
// grammar rules
mulTail ::= (MUL | DIV) term;
term ::= DIGIT multail
| LEFTPAREN expr RIGHTPAREN
| DIGIT
;
addendTail ::= (ADD | SUB) term;
expr ::= term addendTail EOF;
这简直就是“line to line”的直译,而Dropincc.java的目标也就是要达到这样的效果;
这么做的目的并不是实现一套标准意义上的BNF;它使用了正则表达式来描述词法规则(token),也就是上面提到的“目标”的第5点:“限制词法规则必须为正规文法”,这么做的意思也就是建议用户在“若词法规则中出现正规文法无法解决的意义冲突时”,将冲突“推后”到后面的语法、甚至语义分析阶段去解决;这种推荐的“较好实践”能够保持词法规则足够简单,使用正则表达式就能清晰地描述;如果用户不理解“保持词法规则足够简单”的重要性,只需要让他思考一下“为什么java的标识符不能由数字开头”应该就能想通了。
事实上就是:如果你不想自找麻烦,那么就应该保持词法规则足够简单 —— 你不应该在词法解析阶段耗费太多的实现精力 —— 这里用正则表达式就好了 —— 后面还有更麻烦、更有意义的活儿等着你去干呢…你不应该卡在词法分析这里太久…
好了,上面的这种约束一方面推行了一种“较好的实践”,另一方面也使得我们的这个工具更加直观、好用、简单,并且并不降低其实现语言的能力。
第二、三点,打算使用JDK1.6的新compiler API来实现;目前业界中的大多数CC工具都是“生成一个源文件”,让用户直接在项目中使用这个源文件;其实我认为这个东西以“源文件”的形式出现在项目中意义并不大,因为它几乎不可读(不可读的源代码有什么意义呢?)。只要有一个可执行的内存表示即可,“源文件”其实就不必了…这样也似乎使得项目代码看上去更“干净”。
至于第四点,也是很重要的,实现过程中一直保持“不依赖JDK以外的库”即可。
当所有的规则都定义完毕,只要在代码中触发”compile”,立刻就能基于定义的规则在内存中生成一个可执行的编译器前端,没有“源代码”,因为那没有意义,用户需要做的就只是拿它去执行新出生的语言的代码逻辑:
lang.compile();
lang.exe("1+2+3*4/(5*6*7+8)");
当然,还有“action代码”的具体组织形式示例,上面没有给出,在dropincc.java API Design里面有详尽的阐述。
基本上对于这个工具的最终输出形式,计划中应该有两种:
输出AST其实是灵活度最大的一种形式,有了AST,那么后面是想解释执行?还是做翻译?那就交由用户去考虑了,dropincc.java的工作到这里也就结束了;
而“直接执行action”的方式适合像上面这种简单的诸如“四则运算”这样的简单表达式语言,在创建解析树的过程中就把对应的动作代码给执行了,解析结束后直接就能得到结果。
根据dropincc.java的后续设想,上述2种方式在dropincc.java中实现应该“几乎没有差别”,这两种输出形式虽然目的可能不一样,但在dropincc.java中实现起来应该是高度统一、一致的,这也是为了实现“足够简单”的目标,尽量不要让用户过多地去学习一些概念,增加上手成本。
至于dropincc.java能够用来识别哪种级别的语法?我的设想中dropincc.java是一个LL解析器生成器,最终要能识别LL(*)的语法,也就是跟antlr具有同等能力;不过初期的实现可能还识别不了那种程度的不确定性,这个可以一步一步随着版本推进慢慢来改进。其它一些诸如解决左递归的一些算法改进都可以排在计划中…
OK,前期的设想也就暂时介绍到这里,希望dropincc.java的努力能够将java圈子里的DSL应用甚至LOP编程往前推进一步,这是相当令人振奋的事情。
]]>Liquid error: incompatible encoding regexp match (ascii-8bit regexp with utf-8 string)
其实是由于octopress-tagcloud的插件文件:plugins/tag_cloud.rb
文件本身是ascii编码所致:
$ chardet tag_cloud.rb
tag_cloud.rb: ascii (confidence: 1.00)
tag_cloud.rb
中很多地方用到了ruby的正则表达式,而ruby的正则表达式在匹配的时候,默认是按照“代码源文件”的编码格式(在这里是ascii)进行匹配的,而若我的blog是utf-8编码的话就会出现上述错误。
解决办法有二:
tag_cloud.rb
转成utf-8…tag_cloud.rb
中所有的正则表达式声明,加上u
选项(根据这里的说明,u
的意思就是以utf-8编码格式来进行匹配),即,若原正则式是:/regexp/
, 则改成:/regexp/u
这里是我改好的tag_cloud.rb
:
久了不攒机真是out了啊out,殊不知现在流行显示核心嵌到CPU里面…而且也没想到硬盘由于工厂发大水涨价涨得厉害…哎
不过电脑这东西,平民级的用途的话,啥时候想用啥时候就买就行了,没有必要等,而且买了之后就没有什么好后悔的,因为这个行业的新产品出得太快了…当然也有人等着硬盘降价、等着显卡降价的,人家那不一样,人家那是发烧友,那是高玩,人家一个显卡就几千块的…能不等等么。
首先,无环的情况;无环是《编程之美》原书里的题目,很多人都反应说这个题相对书中其它题来讲太过于简单了。也确实,只要在纸上把“所有单向链表相交的情况”画出来很容易就能想通解法了(只要正确理解题意,那么“两个无环单向链表”画出来只可能是2条不相干的链表或一个”Y”字形) —— 所以,判断两个不带环的链表是否相交,只要将两个链表的头指针都移到链表尾,然后比较尾指针地址是否相等就可以了。 如果带环,个人总结,要明白以下几点:
#include <stdio.h>
// define the node struct of links
typedef struct Node {
struct Node* next;
} Node;
int is_intersected(Node* p1, Node* p2);
Node* has_circle(Node* head);
int main(int args, char** argv) {
Node end1 = { NULL };
Node end2 = { NULL };
// 定义几种链表情况
// two links not intersect with each other, no circle
Node link_1_n =
{
&(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&end1}}}}}}}}};
Node link_2_n =
{
&(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&end2}}}}}}}}};
// two links intersect with each other, no circle
Node common_n = { &(Node) {&(Node) {&end1}}};
Node link_1_y = { &(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&common_n}}}}}};
Node link_2_y = { &(Node) {&(Node) {&(Node) {&(Node) {&(Node) {&common_n}}}}}};
// two links, has circle, not intersected.
Node circle1 = { &(Node) {&(Node) {&(Node) {&(Node) {&circle1}}}}};
Node link_c1_n = { &(Node) {&(Node) {&(Node) {&(Node) {&circle1}}}}};
Node circle2 = { &(Node) {&(Node) {&(Node) {&(Node) {&circle2}}}}};
Node link_c2_n = { &(Node) {&(Node) {&(Node) {&(Node) {&circle2}}}}};
// two links, has circle, intersected at a non-circle position
Node common_c = { &(Node) {&(Node) {&(Node) {&(Node) {&common_c}}}}};
Node common_part = { &(Node) {&common_c}};
Node link_c1_y = { &(Node) {&(Node) {&common_part}}};
Node link_c2_y = { &(Node) {&(Node) {&common_part}}};
// two links, has common circle, but different 'joint-points'.
Node jp1 = { NULL };
Node jp2 = { NULL };
// 'weave' the joint-points into a circle:
jp1.next = &(Node) {&(Node) {&jp2}};
jp2.next = &(Node) {&jp1};
Node link_c1_y2 = { &(Node) {&(Node) {&(Node) {&(Node) {&jp1}}}}};
Node link_c2_y2 = { &(Node) {&(Node) {&(Node) {&(Node) {&jp2}}}}};
if (is_intersected(&link_1_n, &link_2_n)) {
printf("link_1_n and link_2_n Intersected!\n");
}
if (is_intersected(&link_1_y, &link_2_y)) {
printf("link_1_y and link_2_y Intersected!\n");
}
if (is_intersected(&link_c1_n, &link_c2_n)) {
printf("link_c1_n and link_c2_n Intersected!\n");
}
if (is_intersected(&link_c1_y, &link_c2_y)) {
printf("link_c1_y and link_c2_y Intersected!\n");
}
if (is_intersected(&link_c1_y2, &link_c2_y2)) {
printf("link_c1_y2 and link_c2_y2 Intersected!\n");
}
return 0;
}
int is_intersected(Node* p1, Node* p2) {
Node* has_circle_1 = has_circle(p1);
Node* has_circle_2 = has_circle(p2);
if (has_circle_1) {
if (has_circle_2) {
Node* pp1 = has_circle_1;
Node* pp2 = has_circle_2;
if (pp1 == pp2 || pp1->next == pp2)
return 1;
while (pp1->next != has_circle_1) {
pp1 = pp1->next;
pp2 = pp2->next->next;
if (pp1 == pp2)
return 1;
}
return 0;
} else {
return 0;
}
} else {
if (has_circle_2) {
return 0;
} else {
while (p1->next)
p1 = p1->next;
while (p2->next)
p2 = p2->next;
return p1 == p2;
}
}
return 0;
}
Node* has_circle(Node* head) {
Node* p1;
Node* p2;
p1 = p2 = head;
if (p2->next != NULL) {
p2 = p2->next;
} else {
return NULL;
}
while (p2->next != NULL && p2->next->next != NULL) {
p1 = p1->next;
p2 = p2->next->next;
if (p1 == p2)
return p1;
}
return NULL;
}
其中,has_circle
方法是判断一个单向链表是否带环的,基本原理就是设置2个“速度”不同的链表,快的去追慢的,追上就是带环,直到较快指针遇到null还没追上就是没有环;假设环包含n个节点,指针p2
的”速度”是2,p1
的速度是1,相对速度就是1,从相同一点出发的话,p2
追上p1
至少要n步;若再假设该链表除了环的部分外还带有一个长度为k的“尾巴”,那么追上的步数最多是n+k;也就是线性时间复杂度内就能完成这个判断。
这提供了一种很好的判断是否”环状”的思路;以前我只写过“用一个栈来记录”的方式,弱爆了…(时间复杂度为O(n2))
在has_circle_1
和has_circle_2
都满足的时候,也就是说2个链表都带环的时候,要分别取2个环上的一点来玩“追逐游戏”来判断是否相交;在这段程序里是pp1
和pp2
;然后一个速度为2一个速度为1开始玩“追逐游戏”,当慢的那个走完环上所有节点时快的那个还没追上它的话,说明不相交(此时耗费时间n——即环节点数;因为快慢指针的相对速度为1,快指针理应在时间n以内追上慢链表,否则不相交)。
单向链表的问题…着实不简单,可以相当复杂…对于这种关乎“形状”的问题,在纸上画一画会很有帮助。
]]>