8.5 使用案例
下面通过一个具体的案例把字节代码操作、源代码生成和注解处理等技术结合起来,形成完整的解决方案。这个案例涉及Java程序的国际化问题,有关Java程序国际化的知识在第4章中已经做了介绍。国际化实现最主要的问题是需要在代码中将ResourceBundle类的使用与属性文件内容保持同步。如果添加了新的字符串,那么需要同时更新Java代码和属性文件。如果对属性文件中字符串的键做了修改,也需要对Java代码进行相应的修改。一个改动会涉及程序中的多个部分。通过本章介绍的技术,可以把这些变化统一到一个地方,即Java源代码本身中。基本的实现思路是在Java源代码中通过注解的形式声明所有需要国际化的字符串在属性文件中的键和值,由注解处理器负责收集所有的字符串,动态生成属性文件和相应的使用ResourceBundle类的Java代码,再通过字节代码修改,在程序中添加调用ResourceBundle类的字节代码。开发人员只需要使用注解在源代码中声明需要国际化的字符串即可,其他的工作都是自动完成的。
代码清单8-35给出了一个包含需要国际化的字符串的Java类的示例。由于注解无法添加在源代码中方法内部的字符串常量上,需要把这些字符串提取出来,包装在一个方法中,然后在方法中添加注解。方法getTestMessage的作用只是为了封装字符串,它的返回值并不重要,但是该方法必须包含一个可变长度的Object类型的参数,表示构建字符串时的参数。注解类型Message的作用是声明方法所封装的字符串在属性文件中的键和值,注解类型MessageBundle的作用是声明类使用的属性文件对应的资源包的名称。
代码清单8-35 使用注解进行国际化字符串声明的代码示例
@MessageBundle("Messages")
public class DemoClass{
public void output(){
System.out.println(getTestMessage("Hello"));
}
@Message(key="TEST_MESSAGE",value="This is a test message.%1$s")
public String getTestMessage(Object……args){
return"";
}
}
在相应的注解处理器实现中,扫描所有的MessageBundle和Message注解,把相关的信息收集起来。接着生成属性文件和使用ResourceBundle类的Java文件。创建属性文件使用的是Filer接口中的createResource方法。与代码清单8-30中创建Java文件的做法相似,得到FileObject接口的实现对象之后,再向对应的输出流中写入数据。使用ResourceBundle类的Java文件也通过自动的方式生成,所生成的Java文件如代码清单8-36所示。在Messages类中,加载了对应的属性文件,并提供getMessage方法从属性文件中获取字符串。
代码清单8-36 使用ResourceBundle类的Java源代码
public class Messages{
private static ResourceBundle bundle;
static{
bundle=ResourceBundle.getBundle("com.java7book.chapter8.annotation.i18n.Messages");
}
public static String getMessage(String key, Object……args){
String message=bundle.getString(key);
return String.format(message, args);
}
}
最后一步是对包含Message注解的方法的字节代码进行修改,使这些方法不再简单地返回空字符串,而是调用代码清单8-36中Messages类的getMessage方法来返回实际的字符串。这一步操作在编译之后由ASM工具来完成。代码清单8-37给出了修改方法的字节代码的实现。这里使用的是ASM的树形API。先把当前方法对应的MethodNode类的对象中包含的指令全部删除,再添加调用Messages类中方法的指令。
代码清单8-37 修改返回国际化字符串的方法的字节代码
private static void updateMethodNode(MethodNode mn, String key){
InsnList instructions=mn.instructions;
Iterator iterator=instructions.iterator();
while(iterator.hasNext()){
iterator.next();
iterator.remove();
}
instructions.add(new LdcInsnNode(key));
instructions.add(new VarInsnNode(ALOAD,1));
instructions.add(new MethodInsnNode(INVOKESTATIC,"com/java7book/chapter8/annotation/i18n/Messages","getMessage","(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String;"));
instructions.add(new InsnNode(ARETURN));
mn.maxStack=2;
mn.maxLocals=2;
}
在经过这些处理之后,当代码清单8-35中的getTestMessage方法被调用时,所执行的代码指令是调用代码清单8-36中的Messages类的getMessage方法。而getMessage方法会根据国际化字符串在属性文件中的键的名称来查找对应的值,并作为getTestMessage方法的返回值。