java源代码和字节码操作

Java虚拟机平台代码的编译,运行流程:

1、用户编写、或运行时动态编译 => Java源代码

2、javac、或使用工具动态创建  => Java字节码

3、Java字节码在被虚拟机执行前,修改代码内容来改变程序行为。


一、Java字节代码格式

大部分Java源代码在编译之后生成,保存在class文件中。(当然字节代码也可以通过网络从远程服务器下载,或运行时动态生成)

字节代码准确来说是包含单个Java类或借口定义的字节流,通常用byte[]来标示。

字节代码的一般格式:

1、所有类和借口都用全名表示,并且用”/”代替“.”。 java.lang.String => java/lang/String

2、基本类型用一个字符表示。B(byte)、C(char)、D(double)、F(float)、I(int)、J(long)、S(short)、Z(boolean)

3、数组类型,在元素类型前加上“[”,“[”的个数表明维度。[[D: 一个double类型的二维数组。

4、方法,格式为“(参数类型)返回值类型”(返回值void, 用V表示)。int calculate(String str) => (Ljava/lang/String)I。

注:字节代码是以个连续的字节流,每个部分含义不同,在解析时,需要识别出不同内容的字节数据之间的边界(数据有定长和不定长之分),具体细节可以翻阅周志明的《深入理解Java虚拟机》。


二、动态编译Java源代码

一般源代码是程序员事先写好,然后通过编译器转换为字节代码去执行。

动态编译源代码,可以把编译和运行两个过程同一起来,都在运行时完成,为程序增加了运行时动态修改其行为的能力。(相对于修改字节码,修改源代码更容易理解)

动态编译源代码的场景一般比较简单:

对于一个Java源文件,在运行时使用API编译出字节代码,再通过类加载器加载到虚拟机中,使用Java反射API调用。

源代码可能来自磁盘,或远程服务器。

1、使用javac,最直接最简单(虽然大多时候是作为命令行工作使用,但通过Java标准库提供的创建底层操作系统上进程的能力,可以在运行时动态调用)

[java]  view plain copy
  1. public class JavaCompiler{  
  2.    public void compile(Path src,Pathoutput)throws CompileException{  
  3.       ProcessBuilder pb = new ProcessBuilder("javac.exe", src.toString(), "-d", output.toString());  
  4.       try{  
  5.          pb.start();  
  6.       }catch(IOException e){  
  7.          throw new CompileException(e);  
  8.       }  
  9.    }  
  10. }  
注:使用javac的不足在于输入和输出只能是文件,另外,使用外部进程方式的性能也比较低。

javac工具也提供了编程接口程序直接调用,相对于外部进程,可移植性和性能更好。

[java]  view plain copy
  1. public void compile(Path src, Path output) throws CompileException{  
  2.    String[] args = new String[] (src.toString(), "-d", output.toString());  
  3.    try{  
  4.       PrintWriter out = new PrintWriter(Paths.get("output.txt".toFile());  
  5.       com.sun.tools.javac.Main.compile(args, out);  
  6.    }catch(FileNotFoundException e){  
  7.       throw new CompileException(e);  
  8.    }  
  9. }  

注:com.sun.tools.* 都是Oracles的私有实现,将来可能发生变化,从而造成维护上的问题。

2、Java编译器API
JavaSE 6 开始,Java编译器相关API以JSR 199的形式规范了下来,使用编译器API可以对编译过程进行更加精细的控制。

[java]  view plain copy
  1. public class JavaCompilerAPICompiler{  
  2.    public void compile(Path src, Path outpu) throws IOException{  
  3.       JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();//得到当前Java平台上默认的编译器实现。  
  4.     //JavaFileManager 负责在编译过程中对Java源代码和字节代码文件的管理  
  5.       try(StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null)){  
  6.      //FileObject是编译器API中对所从操作的对象来源进行了抽象,可以表示磁盘文件,内存对象和数据库中的数据等,主要用于获取来源信息和读写操作  
  7.      //JavaFileObject继承自FileObject  
  8.          Iterable<? extends JavaFileObject> compilationUnits = fileManager.getJavaFileObjects(src.toFile());  
  9.          Iterable<String> options = Arrays.asList("-d", output.toString());  
  10.          CompilationTask task = compiler.getTask(null , fileManager, null, options, null, compilationUnits);  
  11.          boolean result = task.call();  
  12.       }  
  13.    }  
  14. }  

注:编译器API相比javac工具,重要优势在于Java源代码的存在不限于文件形式。

JavaFileObject接口的实现对象可以作为源代码的来源。在实现JavaFileObject接口时,比较好的做法是继承已有的javax.tools.SimpleJavaFileObject类。

例子:从字符串创建出JavaFileObject接口的实现对象。

[java]  view plain copy
  1. public class StringSourceJavaFileObject extends SimpleJavaFileObject{  
  2.    private String content;  
  3.    public StringSourceJavaFileObject(String name, String content){  
  4.       super(URI.create("String:///" + name.replace('.','/') + Kind.SOURCE.extension), Kind.SOURCE);  
  5.       this.content = content;  
  6.    }  
  7.    public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException {  
  8.       return content;  
  9.    }  
  10. }  
例子:编写一个Java程序来计算带括号的四则运算表达式的值

(1、解析String,很费力;2、使用脚本语言支持API;3、使用Java编译器API,把表达式作为Java方法的内容)

[java]  view plain copy
  1. public class Calculator extends ClassLoader{  
  2.    public double calculate(String expr) throws Exception{  
  3.       String className = "CalculatorMain";  
  4.       String methodName = "calculate";  
  5.       String source = "public class " + className + " {public static double " + methodName + "(){ return " + expr + ";}}";  
  6.       JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();  
  7.       StandardJavaFileManager fileManager = compiler.getStandardFileManager(nullnullnull);  
  8.       JavaFileObject sourceObject = new StringSourceJavaFileObject(className, source);  
  9.       Iterable<? extends JavaFileObject> fileObjects = Array.asList(sourceObject);  
  10.       Path output = File.createTempDirectory("calculator");  
  11.       Iterable<String> options = Arrays.asList("-d", output.toString());  
  12.       CompilationTask task = compiler.getTask(null, fileManager, null, options, null, fileObjects);  
  13.       boolean result = task.call();  
  14.       if(result){  
  15.          byte[]  classData = Files.readAllBytes(Paths.get(output.toString(), className + ".class"));  
  16.          Class<?> clazz = defineClass(className, classData, 0, classData.length);  
  17.          Method method = clazz.getMethod(methodName);  
  18.          Object value = method.invoke(null);  
  19.          return (Double) value;  
  20.       }else{  
  21.          throw new Exception("无法识别的表达式")  
  22.       }  
  23.    }   
  24. }  
动态生成的Java源代码的内容示例:

[java]  view plain copy
  1. public class CalculatorMain{  
  2.    public static double calculate(){  
  3.       return (3+2) * 5;  
  4.    }  
  5. }  

注:Java编译API为编译源代码提供了规范的API,但能力有限,更强大的有 Eclipse JDT编译器(Java development tools),变成接口更丰富。


三、字节代码增强

在某些情况下,可能无法获得相关的源代码,而只得到二进制的字节代码,如果希望对字节代码进行处理,需要用到字节代码增强技术。

可以选择轻量级的ASM或AspectJ等。


四、注解

注解(annotation)是J2SE5.0引入的对Java所做的重要修改之一。

1、注解声明使用的事“@interface”

2、注解不能添加泛型声明,也不能使用extends来继承自另外的接口,所有注解都隐式继承自java.lang.annotation.Annotation。

3、元注解(java.lang.annotaion.Target)声明注解可以用的范围,

TYPE(类型声明),ANNOTATION_TYPE(注解类型声明),PACKAGE(包声明),CONSTRUCTOR(构造方法),FIELD(域),METHOD(方法),PARAMETER(方法参数),LOCAL_VARIABLE(方法中局部变量)。

4、元注解(java.lang.annotaion.Rentention),表示注解的保留策略, 默认为CLASS。

SOURCE(只存在源文件中),CLASS(字节代码中),RUNTIME(字节代码中,并且运行时可用,可以通过反射API来获取注解相关信息)

注:局部变量的注解在任何情况下都不会保留在字节代码中。

5、元注解(java.lang.annotation.Inherited),表示注解类型对应的注解声明是可以被继承的。

Java标准库提供了几个一般的注解:

a、java.lang.Override

b、java.lang.Deprecated

c、java.lang.SuppressWarnings

6、创建注解类型

[java]  view plain copy
  1. @Target({ElementType.TYPE})  
  2. @Retention(RetentionPolicy.SOURCE)  
  3. public @interface Author{   
  4.    String name();  
  5.    String email();  
  6.    boolean enableEmailNotfication() default true;  
  7. }  
[java]  view plain copy
  1. // 如果注解类型只包含一个元素,使用value作为该元素名称,在使用时可以省略。  
  2. @Target(ElementType.TYPE)  
  3. public @interface Employee{  
  4.    enum EMPLOYEE_TYPE {REGULAR, CONTRACT};  
  5.    EMPLOYEE_TYPE value();  
  6. }  

[java]  view plain copy
  1. @Target(ElementTYPE.TYPE)  
  2. public @interface WithArrayValue{  
  3.    String name();  
  4.    Class<?> [] appliedTo();  
  5. }  

7、使用注解类型

[java]  view plain copy
  1. @Author(name="Alex", email = "alex@example.org")  
  2. public class MyClass{  
  3. }  
[java]  view plain copy
  1. @Employee(EMPLOYEE_TYPE.REGULAR)  
  2. public class AnotherClass{  
  3. }  
[java]  view plain copy
  1. @WithArrayValue(name="Test",appliedTo = {String.class, Integer.class})  
  2. public class ArrayClass{  
  3. }  
8、处理注解
处理直接通常有两种方式:在编译时、或在运行时。

J2SE 5.0使用的apt工具和Oracle私有的Mirror API工具已经在JavaSe 7中被声明为 废弃。

J2SE 6.0引入了Pluggable Annotaion Processing API,对注解处理机制进行了标准化。

a、分析注解,比如统计一个有多少个类加载了某个注解。(编译时)

b、根据注解,创建和修改源代码(运行时)

c、使用反射处理注解(运行时)

典型的场景:把注解、反射API和动态代理结合起来。注解用来设置运行时的行为,反射API用来解析注解,动态带来负责应用的具体行为。

例子:权限管理

[java]  view plain copy
  1. @Target(ElementType.METHOD)  
  2. @Retention(RetentionPolicy.RUNTIME)  
  3. public @interface Role{  
  4.    String[] value();  
  5. }  
[java]  view plain copy
  1. public interface EmployeeInfoManager{  
  2.    @Role("manager")  
  3.    public void updateSalary();  
  4. }  
[java]  view plain copy
  1. public class EmployeeInfoManagerFactory{  
  2.    private static class AccessInvocationHandler<T> implements InvocationHandler{  
  3.       private final T targetObject;  
  4.       public AccessInvocationHandler(T targetObject){  
  5.       this.targetObject = targetObject;  
  6.    }  
  7.    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable{  
  8.       Role annotation = method.getAnnotation(Role.class);  
  9.       if(annotation != null){  
  10.          String[] roles = annotation.value();  
  11.          String currentRole = AccessManager.getCurrentUserRole();  
  12.          if(!Arrays.asList(roles).contains(currentRole)){  
  13.             throw new RuntimeException("没有调用此方法的权限。")  
  14.          }  
  15.       }  
  16.       return method.invoke(targetObject, args);  
  17.    }  
  18.   
  19.    public static EmployeeInfoManager getManager(){  
  20.       EmployeeInfoManager instance = new DefaultEmployeeInfoManager();  
  21.       return (EmployeeInfoManager)Proxy.newProxyInstance(instance.getClass().getClassLoader(), new Class<?>{EmployeeInfoManager.class},new AccessInvocationHandler<EmployeeInfoManager>(instance));  
  22.    }  
  23. }  
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值