Java平台的脚本机制
Java平台的脚本API可以使之运行诸如JavaScript和Groovy这样的脚本语言代码。
脚本语言是一种在运行时解释程序文本,从而避免了编译、链接等步骤。其具有以下优势:
- 便于快速变更,鼓励不断实验。
- 可以修改运行着的程序的行为。
- 支持程序用户的定制化。
获取脚本引擎
脚本引擎是一个可以执行特定语言编写的脚本的类库。当虚拟机启动的时候,它会发现可以用的脚本引擎。为了枚举这些引擎,需要构造一个ScriptEngineManager
,并调用getEngineFactories
方法。可以向每个引擎工厂询问它们所支持的引擎名、MIME类型和文件扩展名。
下表显示对应的典型值的表格。
引擎 | 名字 | MIME类型 | 文件扩展 |
---|---|---|---|
Nashorm (包含在JDK中,从jdk6开始,jdk11开始标记将要移除,jdk17已移除) |
narsh,Nashorn,js,JS,JavaScript,javascript,ECMAScript,ecmascript |
application/javascript, application/ecmascript, text/javascript, text/ecmascript |
js |
Groovy |
groovy |
无 | groovy |
Renjin |
Renjin |
text/x-R |
R, r, S, s |
通常,你可以所需要的引擎,因此可以直接通过名字、MIME
类型或文件扩展来请求它。
1 | ScriptEngineManager manager = new ScriptEngineManager(); |
Java8引入了Nashorn
,这是由Oracle
开发的一个JavaScript
解释器。可以通过在类路径中提供必要的JAR文件来添加更多语言的支持。
脚本计算和绑定
执行脚本
可以分别从字符串或者文件中执行脚本。
从字符串执行脚本
1 | Object result = engine.eval(scriptString); |
从文件执行脚本
首先要从文件中获得一个Reader
。
1 | Object result = engine.eval(reader); |
上下文
值得的注意的是,大部分引擎的上下文都是同一个。即多次执行是在同一个上下文中进行的,所以其变量也会被共享。
例如:
1 | engine.eval("n = 1728"); |
与上下文变量交换
添加变量
不光可以使用脚本语法申明变量,也可以通过Java的API来为其添加变量。
如:
1 | engine.pus("k", 1728); |
取变量
可以从脚本上下文取变量
如:
1 | Object result = engine.get("k"); |
注意:如果需要自定义作用域,可以自定义一个实现了ScriptContext
接口,并管理一个作用域集合。每个作用域都是由一个整数标识的,而且数字越小应该越先被搜索到。(标准库提供了SimpleScriptConext
类,但是它只能持有全局作用域和引擎作用域)
重定向输入和输出
可以通过调用脚本上下文的setReader
和setWriter
方法来重定向脚本的标准输入和输出。
例如:
1 | var writer = new StringWriter(); |
在上例中,JavaScript的console.log
产生的输出都将被发送到writer
。
Nashorn
引擎没有标准输入源的概念,因此调用setReader
没有任何效果。
调用脚本的函数和方法
有两个方法用于触发脚本语言的方法。
invokeFunction
:面向函数式编程。invokeMethod
:面向对象编程,可以调用某个对象上的方法。
如:
例1:invokeFcuntion
1 | engine.eval("function greet(how, whom){return how + ', ' + whom + '!'}"); |
例2:invokeMethod
1 | engine.eval("function Greeter(how){this.how = how}"); |
并且还可以用脚本引擎来实现Java的接口,然后调用其方法。
如下接口:
1 | public interface Greeter{ |
实现:
1 | engine.eval("function welcome(whom){return 'Hello, ' + whom + '!'}"); |
编译脚本
某些脚本引擎处于对执行效率的考虑,可以将脚本代码编译为某种中间格式。这些引擎实现了Compilable
接口。
如:
1 | var reder = new FileReader("script.js"); |
一旦该脚本被编译,就可以执行它。例如下面的代码展示了如果可以编译就执行编译后的结果,否则执行原始脚本:
1 | if(script != null){ |
编译器API
可以在Java代码中编译Java代码。
调用编译器
调用编译器API非常简单,例如:
1 | JavaScompiler compiler = ToolProvider.getSystemJavaCompiler(); |
返回值为0则表示成功。
注解
Java 注解(Annotation)又称 Java 标注,是 JDK5.0 引入的一种注释机制。
Java 语言中的类、方法、变量、参数和包等都可以被标注。和 Javadoc 不同,Java 标注可以通过反射获取标注内容。在编译器生成类文件时,标注可以被嵌入到字节码中。Java 虚拟机可以保留标注内容,在运行时可以获取到标注内容 。 当然它也支持自定义 Java 标注。
使用注解
在Java中,注解是当作修饰符来使用的。他被置于被备注项之前,中间没有分号(修饰符就是诸如public
和static
之类的关键词)。每个注解的名称前面都加上了@符号。这有点类似于Javadoc注释出现在/**...*/
之间定界符的内部,而注解是代码的一部分。
注解可以定位包含元素的形式。
1 |
|
这些元素可以被读取这些注解的工具去处理。
注解语法
注解接口-注解定义
注解是由注解接口来定义的:
1 | modifiers AnnotationName{ |
每个元素申明都具有下面的形式:
1 | type elementName(); |
或者附带默认值:
1 | type elementName() default value; |
例如,下面的注解具有两个元素:assignedTo
和severity
。
1 | public BugReport{ |
所有的注解接口都隐式的扩展自java.lang.annotation.Annotation
接口。这个接口是一个常规接口,不是一个注解接口。
不必要为注解接口提供实现类。
注解元素的类型为下列之一“
- 基本类型(
int
、short
、long
、byte
、char
、double
、float
或者boolean
)。 String
。Class
(具有一个可选的类型参数,如Class<? extends MyClass>
)。enum
类型。- 由前面提到的类型构成的数组。
如:
1 | public BugReport{ |
定义注解位置
注解可以出现在很多地方,这些地方可以分为两类:声明和类型用法声明注解可以出现在下列声明处:
- 包
- 类(包括
enum
) - 接口(包括注解接口)
- 方法
- 构造器
- 实例域(包含num常量)
- 局部变量
- 参数变量
- 类型参数
对于类和接口,需要将注解放置在class和interface关键词的前面:
1 | public class User{} |
对于变量,需要将其放置在类型的前面:
1 | List<User> user = ...; |
泛型类或方法中的类型参数可以像下面这样被注解:
1 | public class Cache< V>{...} |
包是在文件packge-info.java
中注解的,该文件中只包含注解先导的包语句,
1 | /** |
注解使用
每个注解都具有下面的格式:
1 |
例如:
1 |
参数元素的顺序无关紧要。
如果某个值没有指定,就是用默认值。如@BugReported(severity="10")
中assignedTo
元素默认值为"[none]"
。
注意:默认值并不是和注解存储在一起的。相反地,它们是动态计算而来得到。例如,如果将assignedTo
的默认值改为[]
,那么不仅后面声明的为[]
,之前申明的也为[]
了。
注解有两种特殊形式:
@annotation
:(标记注解)表示所有元素都用默认参数。@annotation(value)
:(单值注解)如果一个元素名字为value
,那么可以直接在括号中进行赋值。
还有两点值得注意:
数组的赋值:对于数组的赋值可以采用以下格式:
1
2
3//多值数组
//单值数组注解的嵌套
由于一个注解元素可以是另一个注解,那么可以创建出任意复杂的注解,例如:
1
标准注解
Java SE在java.lang
、java.lang.annotation
和javax.annotation
包中定义了大量的注解接口。其中四个是元注解,用于描述注解接口的行为属性,其他的三个是规则接口,可以用它们来注解你的源代码中的项。
注 解 接 口 | 应 用 场 景 | 目 的 |
Deprecated | 全部 | 将项标记为过时的 |
SuppressWarnings | 除了包和注解之外的所有情况 | 阻止某个给定类型的警告信息 |
Override | 方法 | 检查该方法是否覆盖了某一个超类方法 |
PostConstruct | 方法 | 被标记的方法应该在构造之后立即被调用 |
PreDestroy | 被标记的方法应该在移除之前立即被调用 | |
Resource | 类、接口、方法、域 |
在类或者接口上:标记为在其他地方要用到的资源 在方法或者域上 :为 “注入” 而标记 |
Resources | 类、接口 | 一个资源组 |
Grenerated | 全部 | |
Target | 注解 | 指明可以应用这个注解的那些项 |
Retention | 注解 | 指明这个注解可以保留多久 |
Documented | 注解 | 指明这个注解应该包含在注解项的文档中 |
Inherited | 注解 | 指明当这个注解应用于一个类的时候,能够在被他的子类继承 |
用于编译的注解
@Deprecated
注解可以被添加到任何不再鼓励使用的项上。这个注解与JavaDoc
的@deprecated
效果相同。@SuppressWarning
注解会告知编译器阻止特定类型的警告信息。@Override
用于方法上,编译器会检查具有这种注解的方式是否真的覆盖了一个来自超类的方法。@Generated
注解的目的是共提供代码生成的工具来使用。任何生成的源代码都可以被注解,从而与程序员提供的代码区分开。例如,代码编辑器可以隐藏生成的代码,或者代码生成器可以移除生成代码的旧版本。
用于管理资源注解
@PostConstruct
和@PreDestroy
注解用于控制对象生命周期的环境中。@Resource
注解用于资源的注入。
元注解
@Target
表示该注解用于什么地方,可能的值在枚举类 ElemenetType
中,包括:
ElemenetType.CONSTRUCTOR
—————————–构造器声明ElemenetType.FIELD
———————————-域声明(包括 enum 实例)ElemenetType.LOCAL_VARIABLE
————————- 局部变量声明ElemenetType.METHOD
———————————方法声明ElemenetType.PACKAGE
——————————–包声明ElemenetType.PARAMETER
——————————参数声明ElemenetType.TYPE
———————————– 类,接口(包括注解类型)或enum声明
@Retention
表示在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy
中,包括:
RetentionPolicy.SOURCE
————-注解将被编译器丢弃RetentionPolicy.CLASS
————-注解在class文件中可用,但会被VM丢弃RetentionPolicy.RUNTIME
———VM将在运行期也保留注释,因此可以通过反射机制读取注解的信息。
@Documented
将此注解包含在 javadoc
中 ,它代表着此注解会被javadoc
工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@see,@param 等。
@Inherited
允许子类继承父类中的注解。即如果一个类具有继承注解,那么它的子类都自动具有同样的注解。
获取注解信息
在定义和使用注解后,我们需要考虑如何去读取传入注解的信息。
一般来说,我们使用反射来获取注解列表和注解中的信息。
但是要注意,要使用反射来获取注解,则必须将注解标记为RetentionPolicy.RUNTIME
,这样其生命周期才会到达虚拟机可到达位置,即:
1 | package mw; |
然后我们可以通过以下API获取注解:
Annotation[] getAnnotations()
:返回该元素上的所有注解(包括继承的注解)Annotation getAnnotation(Class<A> annotationClass)
:返回该元素上的对应类别的注解(包括继承的注解)Annotation getDeclaredAnnotation(Class<A> annotationClass)
:返回该元素上的所有注解(不包括继承的注解)Annotation getDeclaredAnnotationsByType(Class<A>)
:返回该元素上的对应类别的第一个注解(不包括继承的注解)Annotation[] getDeclaredAnnotations()
:返回该元素上的对应类别的所有注解(不包括继承的注解)
实际上上面的方法还要更复杂一些:
对于上面的Kind of Presence
的解释如下:
- 直接出现(directly present):直接加在该元素的上面得注解。
- 间接出现(indirectly present):加载该元素上的注解中,其中一个注解的的参数是注解A,则该注解被称为间接出现。
- 出现(present):
- 直接出现。
- 父类继承的注解。
- 关联(associated):
- 直接或者间接出现
- 父类继承的注解。
通过这些API我们可以获得Annotation
对象。注意这里赋值就直接赋值给我们自定义的注解类型了。在此之前,我们可以通过以下API来判断是否存在对应的注解:
public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass)
如果存在,则使用上面的2来获取对应的注解,然后通过注解定义的域来获取传入的值。
例如:
Annotation1.java
1 | package mw; |
Dog.java
-使用注解的类:
1 | package mw; |
测试:
1 | Class dog = Class.forName("mw.Dog"); |