系列文章目录
提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加
文章目录
- 系列文章目录
- 15.脚本、编译、注解
- 15.1 Java的脚本机制
- 15.1.1 获取脚本引擎
- 15.1.2 脚本计算与绑定
- 15.1.3 重定向输入和输出
- 15.1.4 调用脚本的函数和方法
- 15.1.5 编译脚本
- 15.2 编译器`API`
- 15.2.1 调用编译器
- 15.2.2 发起编译任务
- 15.2.3 捕获诊断信息
- 15.2.4 从内存中读取源文件
- 15.2.5 将字节码写出到内存中
- 15.3 注解简介
- 15.4 注解语法
- 15.4.1 注解接口
- 15.4.2 注解
- 15.4.3 注解各类声明
- 15.4.4 注解类型用法
- 15.5 标准注解
- 15.5.1 用于编译的注解
- 15.5.2 用于管理资源的注解
- 15.5.3 元注解
15.脚本、编译、注解
15.1 Java的脚本机制
脚本语言是一种在通过在运行时解释程序文本,从而避免使用通常的编辑/编译/链接/运行循环的语言。
优点:
- 便于快速变更、鼓励不断试验
- 可以修改运行着的程序的行为
- 支持程序用户的定制化
另外,大部分脚本语言都缺乏可以使编写复杂应用受益的特性。
所以将脚本语言与传统语言结合,脚本API
使得可以在Java上实现这个目标。可以在Java平台上使用JavaScript,Groovvy,Ruby
等。
15.1.1 获取脚本引擎
脚本引擎是一个可以执行某种特定语言编写的脚本的类库。虚拟机启动时,就会发现可用的脚本引擎,为了枚举他们,需要构造一个ScriptEngineManager
并调用getEngineFactories
方法。可以向每个引擎工厂询问他们的引擎名,MIME类型和文件扩展名。
同时可以使用引擎名去请求他。
15.1.2 脚本计算与绑定
一旦有了引擎,就可以调用脚本。
Object result=engine.eval(scriptString)
如果脚本存储在文件中,就需要先打开一个Reader,然后在进行调用。
Object result=engine.eval(reader)
同一个引擎上保存的变量、函数或类,大多数引擎都会保留这些定义。如:
engine.put("n = 1");
Object result=engine.eval("n+1") // =2
可以为新增的变量进行绑定:engine.eval("k",1)
变量k的值为1。
或者绑定对象:engine.eval("obj",new Date())
。反过来还可以通过变量名获取对象。
除了引擎作用域外,还可以使用全局作用域,添加到ScriptEngineManager
的对象对所有引擎都是可视的。
还可以将所有绑定收集到一个类型为Bindings
的对象中,然后再传递给eval
Bindings scope = engine.createBindings();
scope.put("b",new Date());
engine.eval(scriptString,scopr);
15.1.3 重定向输入和输出
可以通过调用脚本上下文的setReader
和setWriter
来重定向输入和输出。
这两个 方法只会影响脚本的标准输入源和输出源
15.1.4 调用脚本的函数和方法
对于诸多脚本引擎而言,可以调用脚本语言的函数,而不必对具体代码进行计算。因此可以是用提供了这种功能的接口Invocable
。
如果想从Java中调用脚本代码,又不想因为这种脚本语言的语法而受到困扰,那么Invocable
接口便很实用。
要调用一个函数,就用函数名使用invokeFunction
方法:
engine.eval("function greet(how,whom)...");//定义的脚本函数
result=((Invocable)engine).invokeFuntion("grret","hello","world");//调用函数
如果脚本语言是面向对象的,则可以用invokeMethod
方法:
engine.eval("function Greeter(how)...");//定义类
engine.eval("Greeter.prototype.welcom="+"function(whom)...");//定义对象的方法
Object obj=engine.eval("new Greeter('You')");
result=((Invocable)engine).invokeMethod(obj,"welcome","world");
如果在Java中定义了接口,同时在脚本中有同名的函数,就使用getInterFace
方法来调用它:
engine.eval("function welcom(whom)...");//定义的脚本函数
//Java接口
interface Greeter{
String welcome(String whom);
}
//直接使用Java调用
Greeter g=((Invocable)engine).getInterFace(Greeter.class);
result=g.welcome("world");
15.1.5 编译脚本
出于对执行效率的考虑,可以将脚本代码编译为某种中间格式。这些引擎实现了Compilable
接口。
var reader = new FIleReader("myscript.js");
CompiledScript script=null;
if(engine implements Compliable)
script=((Compilable)engine).compile(reader);
一旦脚本被编译,就可以直接执行它。如果引擎不支持编译,则会执行原始的脚本。
15.2 编译器API
15.2.1 调用编译器
调用编译器很简单,直接使用JavaCompiler
创建对象即可。
JavaCompiler compiler=ToolProvider.getSystemJavaCompiler();
OutputStream out=...;
OutputStream err=...;
int result = compiler.run(null,out,err,"-sourcepath","src","Test.java");
返回值为0则编译成功。
编译器会向提供的流发送输出信息和错误信息。如果将参数置为null,编译器就会使用
System.out和System.err
。
15.2.2 发起编译任务
可以通过使用CompilationTask
对编译过程进行更多的操作。应用场景:在字符串中提供源码、在内存中捕获类文件、处理错误和警告信息等。
15.2.3 捕获诊断信息
为了监听错误信息,需要安装一个DiagnosticListener
。这个监视器在编译期报告警告或错误信息时就会收到一个Diagnostic
对象。DiagnosticCollector
类实现了这个接口,它将收集所有的诊断信息,使得你可以在编译完成之后遍历这些信息。
Diagnostic
对象包含有关问题位置的信息(包括文件名、行号和列号)以及人类可阅读的描述。
还可以在标准的文件管理器上安装一个 DiagnosticListener
对象,这样就可以捕获到有关文件缺失的消息:
StandardJavaFileManager fileManager= compiler.getStandardFileManager(diagnostics, null, null);
15.2.4 从内存中读取源文件
如果动态地生成了源代码,那么就可以从内存中获取它来进行编译,而无须在磁盘上保存文件。可以使用下面的类来持有代码:
public class StringSource extends SimpleJavafile0biect{
private String code;
StringSource(String name, String code){
super(URl.create("string:///"+ name.replace('.','/')+ ".java"),Kind.SOURCE);this.code = code;
}
public CharSequence getCharContent(boolean ignoreEncodingErrors){
return code;
}
}
然后,生成类的代码,并提交给编译器一个StringSource对象的列表:
List<StringSource> sources = List.of(new StringSource(classNamel,classlCodeString),...);
task = compiler.getTask(null,fileManager, diagnostics, null, null, sources);
15.2.5 将字节码写出到内存中
如果动态地编译类,那么就无须将类文件写出到硬盘上。可以将它们存储在内存中,并立即加载它们。
首先,要有一个类来持有这些字节:
public class ByteArrayClass extends SimpleJavaFile0bject{
private ByteArrayOutputStream out;
ByteArrayClass(String name){
super(URI.create("bytes:///"+ name.replace(',','/')+".class"),Kind.CLASS);
}
public byte[] getcode(){
return out.toByteArray();
}
public OutputStream openOutputStream()throws IOException{
out = new ByteArrayOutputStream();
return out;
}
}
接下来,需要将文件管理器配置为使用这些类作为输出:
List<ByteArrayClass>classes = new ArrayList<>();
StandardJavaFileManager stdFileManager=compiler.getStandardFileManager(null, null, null);
JavaFileManager fileManager=new ForwardingJavaFileManager<JavaFileManager>(stdFileManager){
public JavaFile0bject getJavaFileFor0utput(Location location,String className,Kind kind, File0bject sibling)throws IOException{
if(kind == Kind.CLASS){
ByteArrayClass outfile = new ByteArrayClass(className);
classes.add(outfile);
return outfile;
}
else
return super.getJavaFileForOutput(location, className, kind, sibling);
}
}
为了加载这些类需要使用类加载器:
public class ByteArrayClassLoader extends ClassLoader{
private Iterable<ByteArrayClass> classes;
public ByteArrayClassLoader(Iterable<ByteArrayClass> classes){
this.classes = classes;
}
public Class<?>findClass(String name)throws ClassNotFoundException{
for(ByteArrayClass cl :classes){
if (cl.getName().equals("/" + name.replace('.','/')+ ".class")){
byte[]bytes = cl.getCode();
return defineClass(name,bytes,0,bytes.length);
}
}
throw new ClassNotFoundException(name);
}
}
编译完成后,用上面的类加载器调用Class.forName
方法:
ByteArrayClassLoader loader = new ByteArrayClassLoader(classes);
Class<?>cl=Class.forName(className, true, loader);
15.3 注解简介
注解是插入到源代码中使其他工具可以对其进行处理的标签。这些工具可以在源码层次上进行操作,或者可以处理编译器在其中放置了注解的类文件。
注解不会改变程序的编译方式。为了能够受益于注解,需要选择一个处理工具,通过处理工具来理解代码中插入的注解,再处理代码。
注解应用场景:
- 附属文件的自动生成。
- 测试、日志、事务语义等代码的自动生成。
注解举例:
public class AppTest{
@Test//@Test用于注解start方法
public void start(){}
}
在Java中注解是当作修饰符使用,一般位于被修饰项之前,没有分号间隔,且每个注解前都有@
符号。
@Test
注解本身不会做任何事情,他需要借助处理工具才能有作用。(比如需要JUnit4
测试工具,才会识别这个注解)
注解可以注解方法、类、成员以及局部变量,可以存放在任何可以放置修饰符的地方。
每一个注解必须通过一个注解接口进行定义,这些接口中的方法与注解中的元素相对应。例如:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test{
long timeout() default 0L;
}
@interface
声明创建了一个真正的Java接口。处理工具将接受实现了注解接口的对象,调用方法来获取特定注解的目标元素。注解
Target和Retention
是元注解,注解了Test
注解,将其标识为只能用在方法上的注解并加载到虚拟机上时,仍旧可以保留。
15.4 注解语法
15.4.1 注解接口
注解是由注解接口定义的:
modifiers @interface AnnotationName{
elementDeclaration;
}
每个元素声明都具有一下形式:
// 1
type elementName();
// 2
type elementName() default value;
所有注解接口都隐式扩展java.lang.annotation.Annotation
接口。
注解的元素类型为下列之一:
- 基本类型:
int,short,long,byte,char,double,float,boolean
String
Class
:具有一个可选的类型参数,例如Class<? extends MyClass>
enum
类型- 注解类型
- 数组
15.4.2 注解
每个注解都具有这种格式:@AnnotationName(elementName1=value1,elementName2=value2...)
。(元素的顺序无关)
特殊的注解使用方法:
-
标记注解:如果使用的注解没有指定元素,或者所有元素都有默认值,就可以不适用括号
@Test public void start(){};
-
单值注解:如果一个元素具有特殊的名字,并且没有指定其他元素,就可以忽略掉这个元素名以及等号
@GetMapper("/hello")//url="" public String welcome(){};
一个项可以有多个注解
15.4.3 注解各类声明
注解可以出现在很多地方,大体分为两类:声明注解和类型用法生声明注解。
声明注解可出现在下处:
- 包、类、接口、方法、构造器、实例域、局部变量、参数变量、类型参数
对于类和接口,需将注解放在class和interface
前面
@Entity public class User{}
对于变量需要将注解放置在类型前面
@SupperessWarnings("unchecked") List<User> users=...;
泛型类或方法可以将注解放在类型变量中
public class Cache<@Immutable V>{}
15.4.4 注解类型用法
声明注解提供了正在被声明的项的相关信息。例如:
public User getUser(@NonNull String uId)
就断言了uId
不为空。
类型用法注解可以出现在以下位置:
- 与泛化类型参数一起使用:
List <@NonNull String>
- 数组中任意位置:
@NonNull String[][] | String @NonNull [][] | String[] @NonNull []
- 与超类和实现接口一起使用:
class Warning extends @Localized Message
- 与强制类型转换和
instanceof
检查一起使用:(@Localized String) text
- 与异常规约一起使用:
public String read() throws @Localized IOException
- 与通配符和类型边界一起使用:
List<@Localized ? extends Message>
- 与方法和构造器引用一起使用:
@Localized Message::getText
15.5 标准注解
在Java中定义了大量的注解接口,四种是元注解,用于描述注解接口的行为属性,其他的是规则接口,可以用他们注解源代码中的项。
注解接口 | 应用场合 | 目的 |
---|---|---|
Deprecated | 全部 | 将项标记为过时的 |
SuppressWarnings | 除了包和注解之外的所有情况 | 阻止某个给定类型的警告信息 |
SafeVarargs | 方法和构造器 | 断言 varargs 参数可安全使用 |
0verride | 方法 | 检查该方法是否覆盖了某一个超类方法 |
FunctionalInterface | 接口 | 将接口标记为只有一个抽象方法的函数式接口 |
PostConstruct PreDestroy | 方法 | 被标记的方法应该在构造之后或移除之前立即被调用 |
Resource | 类、接口、方法、域 | 在类或接口上:标记为在其他地方要用到的资源。在方法或域上:为“注入”而标记 |
Resources | 类、接口 | 一个资源数组 |
Generated | 全部 | 用于标识生成的代码或类是由哪个工具或程序自动生成的,一般是由代码生成器或自动化构建工具生成的代码所使用的。 |
Target | 注解 | 指明可以应用这个注解的那些项 |
Retention | 注解 | 指明这个注解可以保留多久 |
Documented | 注解 | 指明这个注解应该包含在注解项的文档中 |
Inherited | 注解 | 指明当这个注解应用于一个类的时候,能够自动被它的子类继承 |
Repeatable | 注解 | 指明这个注解可以在同一个项上应用多次 |
15.5.1 用于编译的注解
@Deprecated
注解可以被添加到任意不再鼓励使用的项上。所以,当使用一个已过时的项时,编译器将会发出警告。这个注解与Javadoc
的标签@deprecated
具有同等功效,但这个注解会持久化到运行时。
@SuppressWarnings
注解会告知编译器组织特定类型的警告信息。如:@SuppressWarnings("unchecked")
@Override
注解只能用在方法上,编译器会检查具有这种注解的方法是否真正覆盖了一个来自于超类的方法。如果声明了
class MyClass{
@Override public boolean equals(...)
}
就会报错,因为equals
方法没有覆盖Object
类的equals
方法。
@Generated
注解的目的是供代码生成工具来使用。任何生成的源代码都可以被注解,从而与程序员提供的代码区分开。比如,代码编辑器可以隐藏生成的代码,或者代码生成器可以移除生成的代码的旧版本。每个注解都必须要包含一个表示代码生成器的唯一标识符,而日期字符串(ISO8601
格式)和注释字符串是可选的。
如:
@Generated("com.horstmann.beanproperty","2008-01-04T12:08:56.235-0700");
15.5.2 用于管理资源的注解
@PostConstruct
和@PreDestroy
注解用于控制对象生命周期环境中,如Web
容器和应用服务器。标记了这些注解的方法应在对象被构建之后,或者在对象被移除之前,紧接着调用。
@Resource
注解用于资源注入,当一个被注解的项的对象被构造时,容器就会注入一个对该数据源的引用。
@Resource
属于 JDK 提供的注解,默认注入方式为byName
。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType
。
@Resource
有两个比较重要且日常开发常用的属性:name
(名称)、type
(类型)。
如果仅指定name
属性则注入方式为byName
,如果仅指定type
属性则注入方式为byType
,如果同时指定name
和type
属性(不建议这么做)则注入方式为byType+byName
。
如:// 报错,byName 和 byType 都无法匹配到 bean @Resource private SmsService smsService; // 正确注入 SmsServiceImpl1 对象对应的 bean @Resource private SmsService smsServiceImpl1; // 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式) @Resource(name = "smsServiceImpl1") private SmsService smsService;
总结:
@Autowired
是Spring
提供的注解,@Resource
是JDK
提供的注解。Autowired
默认的注入方式为byType
(根据类型进行匹配),@Resource
默认注入方式为byName
(根据名称进行匹配)。- 当一个接口存在多个实现类的情况下,
@Autowired
和@Resource
都需要通过名称才能正确匹配到对应的Bean
。Autowired
可以通过@Qualifier
注解来显式指定名称,@Resource
可以通过name
属性来显式指定名称。@Autowired
支持在构造函数、方法、字段和参数上使用。@Resource
主要用于字段和方法上的注入,不支持在构造函数或参数上使用。
15.5.3 元注解
@Target
注解可以应用于一个注解,以限制该注解可以应用到哪些项上。如:
@Target(ElementType.xxx)
@Target
注解枚举类型ElementType
的取值范围:
元素类型 | 使用场合 | 元素类型 | 使用场合 |
---|---|---|---|
ANNOTATION_TYPE | 注解类型声明 | FIELD | 成员域 |
PACKAGE | 包 | PARAMETER | 方法或构造器参数 |
TYPE | 类和接口 | LOCAL_VARIABLE | 局部变量 |
METHOD | 方法 | TYPE_PARAMETER | 类型参数 |
CONSTRUCTOR | 构造器 | TYPE_USE | 类型用法 |
一条没有Target
限制的注解可以用在任意位置的项上。编译器将检查你是否将一条注解只应用到某个允许的项上,否则会导致编译器错误。
@Retention
注解用于指定一条注解应该保留多长时间。并且只能将其指定为下表中的值。
默认为RetentionPolicy.Class
保留规则 | 描述 |
---|---|
SOURCE | 不包括在类文件中的注解 |
CLASS | 包括在类文件中的注解,但虚拟机不需要将他们载入 |
RUNTIME | 包括在类文件中的注解,并由虚拟机载入,通过反射API 得到他们 |
@Documented
注解为向Javadoc
这种归档工具提供提示。应像处理其他修饰符一样处理归档注解。
如果某个注解是暂时性的,就不要使用这个注解进行归档
@Inherited
注解只能应用于对类的注解。如果一个类具有继承注解,那他的所有子类都自动具有同样的注解。比如:
// 注解
@Inherited
@interface Persistent{}
// 使用注解的类
@Persistent
class Employee{}
//子类
class Manager extends Employee
如果这个注解指明一个类的对象可以存储到数据库中,那么它的子类就会自动被注解为持久性的。
持久化机制去查找存储在数据库中的对象时,会自动去寻找Employee对象和Manager对象。