编写高质量代码:改善Java程序的151个建议(5)

  建议16:易变业务使用脚本语言编写

  Java世界一直在遭受着异种语言的入侵,比如PHP、Ruby、Groovy、JavaScript等,这些“入侵者”都有一个共同特征:全是同一类语言—脚本语言,它们都是在运行期解释执行的。为什么Java这种强编译型语言会需要这些脚本语言呢?那是因为脚本语言的三大特征,如下所示:

  灵活。脚本语言一般都是动态类型,可以不用声明变量类型而直接使用,也可以在运行期改变类型。

  便捷。脚本语言是一种解释型语言,不需要编译成二进制代码,也不需要像Java一样生成字节码。它的执行是依靠解释器解释的,因此在运行期变更代码非常容易,而且不用停止应用。

  简单。只能说部分脚本语言简单,比如Groovy,Java程序员若转到Groovy程序语言上,只需要两个小时,看完语法说明,看完Demo即可使用了,没有太多的技术门槛。

  脚本语言的这些特性是Java所缺少的,引入脚本语言可以使Java更强大,于是Java6开始正式支持脚本语言。但是因为脚本语言比较多,Java的开发者也很难确定该支持哪种语言,于是JCP(JavaCommunityProcess)很聪明地提出了JSR223规范,只要符合该规范的语言都可以在Java平台上运行(它对JavaScript是默认支持的),诸位读者有兴趣的话可以自己写个脚本语言,然后再实现ScriptEngine,即可在Java平台上运行。

  我们来分析一个案例,展现一下脚本语言是如何实现“拥抱变化”的。咱们编写一套模型计算公式,预测下一个工作日的股票走势(如果真有,那巴菲特就羞愧死了),即把国家政策、汇率、利率、地域系数等参数输入到公式中,然后计算出明天这支股票是涨还是跌,该公式是依靠历史数据推断而来的,会根据市场环境逐渐优化调整,也就是逐渐趋向“真理”的过程,在此过程中,公式经常需要修改(这里的修改不仅仅是参数修改,还涉及公式的算法修改),如果把这个公式写到一个类中(或者几个类中),就需要经常发布重启等操作(比如业务中断,需要冒烟测试(SmokeTesting)等),使用脚本语言则可以很好地简化这一过程,我们写一个简单公式来模拟一下,代码如下:

  

    functionformula(var1,var2){ returnvar1+var2*factor; }

  这就是一个简单的脚本语言函数,可能你会很疑惑:factor(因子)这个变量是从哪儿来的?它是从上下文来的,类似于一个运行的环境变量。该JavaScript保存在C:/model.js中。下一步Java需要调用JavaScript公式,代码如下:

  

    publicstaticvoidmain(String[]args)throwsException{ //获得一个JavaScript的执行引擎 ScriptEngineengine=newScriptEngineManager().getEngineByName(“javascript”); //建立上下文变量 Bindingsbind=engine.createBindings(); bind.put(“factor”,1); //绑定上下文,作用域是当前引擎范围 engine.setBindings(bind,ScriptContext.ENGINE_SCOPE); Scannerinput=newScanner(System.in); while(input.hasNextInt()){ intfirst=input.nextInt(); intsec=input.nextInt(); System.out.println(“输入参数是:”+first+”,”+sec); //执行js代码 engine.eval(newFileReader(“c:/model.js”)); //是否可调用方法 if(engineinstanceofInvocable){ Invocablein=(Invocable)engine; //执行js中的函数 Doubleresult=(Double)in.invokeFunction(“formula”,first,sec); System.out.println(“运算结果:”+result.intValue()); } } }

  上段代码使用Scanner类接受键盘输入的两个数字,然后调用JavaScript脚本的formula函数计算其结果,注意,除非输入了一个非int数字,否则当前JVM会一直运行,这也是模拟生产系统的在线变更状况。运行结果如下:

  

    输入参数是:1,2 运算结果:3

  此时,保持JVM的运行状态,我们修改一下formula函数,代码如下:

  

    functionformula(var1,var2){ returnvar1+var2-factor; }

  其中,乘号变成了减号,计算公式发生了重大改变。回到JVM中继续输入,运行结果如下。

  

    输入参数是:1,2 运算结果:2

  修改Java代码,JVM没有重启,输入参数也没有任何改变,仅仅改变脚本函数即可产生不同的结果。这就是脚本语言对系统设计最有利的地方:可以随时发布而不用重新部署;这也是我们Javaer最喜爱它的地方—即使进行变更,也能提供不间断的业务服务。

  Java 6不仅仅提供了代码级的脚本内置,还提供了一个jrunscript命令工具,它可以在批处理中发挥最大效能,而且不需要通过JVM解释脚本语言,可以直接通过该工具运行脚本。想想看,这是多么大的诱惑力呀!而且这个工具是可以跨操作系统的,脚本移植就更容易了。但是有一点需要注意:该工具是实验性的,在以后的JDK中会不会继续提供就很难说了。

  建议17:慎用动态编译

  动态编译一直是Java的梦想,从Java6版本它开始支持动态编译了,可以在运行期直接编译.java文件,执行.class,并且能够获得相关的输入输出,甚至还能监听相关的事件。不过,我们最期望的还是给定一段代码,直接编译,然后运行,也就是空中编译执行(on-the-fly),来看如下代码:

  

    publicclassClient{ publicstaticvoidmain(String[]args)throwsException{ //Java源代码 StringsourceStr=”publicclassHello{publicStringsayHello(Stringname){return\”Hello,\”+name+\”!\”;}}”; //类名及文件名 StringclsName=”Hello”; //方法名 StringmethodName=”sayHello”; //当前编译器 JavaCompilercmp=ToolProvider.getSystemJavaCompiler(); //Java标准文件管理器 StandardJavaFileManagerfm=cmp.getStandardFileManager(null,null,null); //Java文件对象 JavaFileObjectjfo=newStringJavaObject(clsName,sourceStr); //编译参数,类似于javac<options>中的options List<String>optionsList=newArrayList<String>(); //编译文件的存放地方,注意:此处是为Eclipse工具特设的 optionsList.addAll(Arrays.asList(“-d”,”./bin”)); //要编译的单元 List<JavaFileObject>jfos=Arrays.asList(jfo); //设置编译环境 JavaCompiler.CompilationTasktask=cmp.getTask(null,fm,null,optionsList,null,jfos); //编译成功 if(task.call()){ //生成对象 Objectobj=Class.forName(clsName).newInstance(); Class<?extendsObject>cls=obj.getClass(); //调用sayHello方法 Methodm=cls.getMethod(methodName,String.class); Stringstr=(String)m.invoke(obj,”DynamicCompilation”); System.out.println(str); } } } //文本中的Java对象 classStringJavaObjectextendsSimpleJavaFileObject{ //源代码 privateStringcontent=””; //遵循Java规范的类名及文件 publicStringJavaObject(String_javaFileName,String_content){ super(_createStringJavaObjectUri(_javaFileName),Kind.SOURCE); content=_content; } //产生一个URL资源路径 privatestaticURI_createStringJavaObjectUri(Stringname){ //注意此处没有设置包名 returnURI.create(“String:///”+name+Kind.SOURCE.extension); } //文本文件代码 @Override publicCharSequencegetCharContent(booleanignoreEncodingErrors) throwsIOException{ returncontent; } }

  上面的代码较多,这是一个动态编译的模板程序,读者可以拷贝到项目中使用,代码中的中文注释也较多,相信读者看得懂,不多解释,读者只要明白一件事:只要是在本地静态编译能够实现的任务,比如编译参数、输入输出、错误监控等,动态编译就都能实现。

  Java的动态编译对源提供了多个渠道。比如,可以是字符串(例子中就是字符串),可以是文本文件,也可以是编译过的字节码文件(.class文件),甚至可以是存放在数据库中的明文代码或是字节码。汇总成一句话,只要是符合Java规范的就都可以在运行期动态加载,其实现方式就是实现JavaFileObject接口,重写getCharContent、openInputStream、openOutputStream,或者实现JDK已经提供的两个SimpleJavaFileObject、ForwardingJavaFileObject,具体代码可以参考上个例子。

  动态编译虽然是很好的工具,让我们可以更加自如地控制编译过程,但是在我目前所接触的项目中还是使用得较少。原因很简单,静态编译已经能够帮我们处理大部分的工作,甚至是全部的工作,即使真的需要动态编译,也有很好的替代方案,比如JRuby、Groovy等无缝的脚本语言。

  另外,我们在使用动态编译时,需要注意以下几点:

  (1)在框架中谨慎使用

  比如要在Struts中使用动态编译,动态实现一个类,它若继承自ActionSupport就希望它成为一个Action。能做到,但是debug很困难;再比如在Spring中,写一个动态类,要让它动态注入到Spring容器中,这是需要花费老大功夫的。

  (2)不要在要求高性能的项目使用

  动态编译毕竟需要一个编译过程,与静态编译相比多了一个执行环节,因此在高性能项目中不要使用动态编译。不过,如果是在工具类项目中它则可以很好地发挥其优越性,比如在Eclipse工具中写一个插件,就可以很好地使用动态编译,不用重启即可实现运行、调试功能,非常方便。

  (3)动态编译要考虑安全问题

  如果你在Web界面上提供了一个功能,允许上传一个Java文件然后运行,那就等于说:“我的机器没有密码,大家都来看我的隐私吧”,这是非常典型的注入漏洞,只要上传一个恶意Java程序就可以让你所有的安全工作毁于一旦。

  (4)记录动态编译过程

  建议记录源文件、目标文件、编译过程、执行过程等日志,不仅仅是为了诊断,还是为了安全和审计,对Java项目来说,空中编译和运行是很不让人放心的,留下这些依据可以更好地优化程序。

  建议18:避免instanceof非预期结果

  instanceof是一个简单的二元操作符,它是用来判断一个对象是否是一个类实例的,其操作类似于>=、==,非常简单,我们来看段程序,代码如下:

  

    publicclassClient{ publicstaticvoidmain(String[]args){ //String对象是否是Object的实例 booleanb1=”Sting”instanceofObject; //String对象是否是String的实例 booleanb2=newString()instanceofString; //Object对象是否是String的实例 booleanb3=newObject()instanceofString; //拆箱类型是否是装箱类型的实例 booleanb4=’A’instanceofCharacter; //空对象是否是String的实例 booleanb5=nullinstanceofString; //类型转换后的空对象是否是String的实例 booleanb6=(String)nullinstanceofString; //Date对象是否是String的实例 booleanb7=newDate()instanceofString; //在泛型类中判断String对象是否是Date的实例 booleanb8=newGenericClass<String>().isDateInstance(“”); } } classGenericClass<T>{ //判断是否是Date类型 publicbooleanisDateInstance(Tt){ returntinstanceofDate; } }

  就这么一段程序,instanceof的所有应用场景都出现了,同时问题也产生了:这段程序中哪些语句会编译通不过?我们一个一个地来解说。

  ”Sting” instanceof Object

  返回值是true,这很正常,“String”是一个字符串,字符串又继承了Object,那当然是返回true了。

  new String() instanceof String

  返回值是true,没有任何问题,一个类的对象当然是它的实例了。

  new Object() instanceof String

  返回值是false,Object是父类,其对象当然不是String类的实例了。要注意的是,这句话其实完全可以编译通过,只要instanceof关键字的左右两个操作数有继承或实现关系,就可以编译通过。

  ’A’ instanceof Character

  这句话可能有读者会猜错,事实上它编译不通过,为什么呢?因为’A’是一个char类型,也就是一个基本类型,不是一个对象,instanceof只能用于对象的判断,不能用于基本类型的判断。

  null instanceof String

  返回值是false,这是instanceof特有的规则:若左操作数是null,结果就直接返回false,不再运算右操作数是什么类。这对我们的程序非常有利,在使用instanceof操作符时,不用关心被判断的类(也就是左操作数)是否为null,这与我们经常用到的equals、toString方法不同。

  (String)null instanceof String

  返回值是false,不要看这里有个强制类型转换就认为结果是true,不是的,null是一个万用类型,也可以说它没类型,即使做类型转换还是个null。

  new Date() instanceof String

  编译通不过,因为Date类和String没有继承或实现关系,所以在编译时直接就报错了,instanceof操作符的左右操作数必须有继承或实现关系,否则编译会失败。

  new GenericClass<String>().isDateInstance(“”)

  编译通不过?非也,编译通过了,返回值是false,T是个String类型,与Date之间没有继承或实现关系,为什么”t instanceof Date”会编译通过呢?那是因为Java的泛型是为编码服务的,在编译成字节码时,T已经是Object类型了,传递的实参是String类型,也就是说T的表面类型是Object,实际类型是String,那”t instanceof Date”这句话就等价于 ”Object instance of Date”了,所以返回false就很正常了。

  就这么一个简单的instanceof,你答对几个?

  相关链接:

  编写高质量代码:改善Java程序的151个建议(1)

  编写高质量代码:改善Java程序的151个建议(2)

  编写高质量代码:改善Java程序的151个建议(3)

  编写高质量代码:改善Java程序的151个建议(4)

获得幸福的二法门是珍惜你所拥有的、遗忘你所没有的

编写高质量代码:改善Java程序的151个建议(5)

相关文章:

你感兴趣的文章:

标签云: