第15次文章:反射+动态编译+脚本引擎
- 2019 年 10 月 8 日
- 笔记
各位小伙伴儿大家好啊!又和各位见面了,风里雨里,小白在这里等你哟!
在前面的文章中,我们简单的介绍过一点反射的内容,没有深入,这次的反射内容会比上一次更加深刻一点!
1、动态语言
程序运行的时候,可以改变程序的结构或变量类型。典型的语言如:Python、ruby、JavaScript、c、c++,但是java不是动态语言,java可以称之为“准动态语言”,java具有一定的动态性,我们可以利用反射机制、字节码操作获得类似动态语言的特性。
JAVA反射机制是在运行状态中,对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制.
2、基本概念
在java中,对象就是用来表示或者封装一些数据。一个类被加载之后,JVM会创建一个对应类的Class对象,类的整个结构信息会放到对应的Class对象中,这个Class对象就像一面镜子,通过这面镜子,我们可以看到这个类的全部信息。包括其中的属性,方法等等信息。我们来测试一下反射机制的内容。
第一步:我们首先自己创建一个Javabean类,作为我们测试的对象
package com.peng.test.bean; public class User { private int age; private int id; private String uname; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getUname() { return uname; } public void setUname(String uname) { this.uname = uname; } public User(int age, int id, String uname) { super(); this.age = age; this.id = id; this.uname = uname; } //Javabean必须要有无参构造器 public User() { // TODO Auto-generated constructor stub } }
tips:在Javabean类中,一定要有无参构造器,在后续的反射机制中,一些创建对象的方法就是根据Javabean类的无参构造进行创建对象的。
第二步:利用Javabean类,创建对象,测试反射机制
public class Demo01 { public static void main(String[] args) { try { String path = "com.peng.test.bean.User";//需要使用的类的路径 //Class对象的获取方式1 Class clazz1 = Class.forName(path); System.out.println(clazz1); System.out.println(clazz1.hashCode()); Class clazz2 = Class.forName(path); System.out.println(clazz2.hashCode()); //Class对象的获取方式2 Class clazz3 = String.class; //Class对象的获取方式3 Class clazz4 = path.getClass(); System.out.println(clazz3 == clazz4); //不同数组对象的Class对象获取 int[] arry01 = new int[10]; int[] arry02 = new int[30]; int[][] arry03 = new int[10][30]; double[] arry04 = new double[20]; System.out.println(arry01.getClass().hashCode()); System.out.println(arry02.getClass().hashCode()); System.out.println(arry03.getClass().hashCode()); System.out.println(arry04.getClass().hashCode()); } catch (ClassNotFoundException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
代码运行后的结果为:

tips:
(1)Class对象的3种获取方式,分别是Class.forName(),类.Class,对象.getClass(),在上面的代码中,分别对应代码“Class clazz1 = Class.forName(path);”、“Class clazz3 = String.class;”、“Class clazz4 = path.getClass();”。
(2)一个类的Class对象只会在JVM中加载一次,同一个类不论加载了多少次,也不论使用什么方式加载,所得到的Class对象都是相同的。比如在测试中,我们对同一个类User加载了两次,分别得到Class对象clazz1和clazz2对象,然后分别获取其hashcode,结果显示两者的hashcode是完全相同的,这就代表了clazz1和clazz2属于同一个对象。与此同时,clazz3是引用类String的Class对象,clazz4是字符串变量path的Class对象,但是最后打印结果也是clazz3=clazz4,这是因为变量path是String的一个实现类。以上结果均表明,在JVM中,加载得到的类的Class对象只会根据该类本身进行产生,而不会因为其加载方式和加载次数而改变。
(3)紧接着,我们又分别创建了四个数组,其长度和维度以及类型有所不同。我们通过最后的打印结果可以看出,arry01与arry02加载得到的Class对象是相同的,arry03和arry04的Class对象是不同的,也就代表着,数组对象的Class对象之间的区分,不在于其数组长度,而在于其维度以及数组的基本类型,一维数组和二维数组的反射对象不同,int和double数组的反射对象不同。
3、利用反射机制进行相应的操作
在上面的代码中,我们基本了解了反射的一些概念,下面我们使用反射API动态的操作构造器、方法、属性。
public class Demo03 { @SuppressWarnings("all") public static void main(String[] args) { String path = "com.peng.test.bean.User"; try { Class clazz = Class.forName(path); //通过反射API调用构造方法,构造对象 User u = (User) clazz.newInstance(); System.out.println(u); Constructor<User> c = clazz.getDeclaredConstructor(int.class,int.class,String.class);//调用带参构造 User u2 = c.newInstance(10,1001,"peng"); System.out.println(u2.getUname()); //通过反射API调用普通方法 User u3 = (User) clazz.newInstance(); Method method = clazz.getDeclaredMethod("setUname", String.class); method.invoke(u3, "peng03"); System.out.println(u3.getUname()); //通过反射API操作属性 User u4 = (User) clazz.newInstance(); Field f = clazz.getDeclaredField("uname"); f.setAccessible(true); //不需要通过安全检查,直接设置属性值 f.set(u4, "peng04"); //通过反射来操作属性值 System.out.println(u4.getUname()); //利用对象的方法来获取属性值 System.out.println(f.get(u4)); //利用反射来获取属性值 } catch (Exception e) { // TODO Auto-generated catch block e.printStackTrace(); } } }
查看结果:

tips:
(1)利用反射机制的前提,依旧是需要先获取一个User类的Class对象,在获取对象u的时候,我们使用的方法是“clazz.newInstance();”其实这个方法的实现原理就是调用User对象的无参构造器,所以在我们创建Javabean的时候,一定要给其加上无参构造器。
(2)我们在创建对象的时候,也可以使用带参构造器。此时我们就需要先获取User类的带参构造器,然后利用带参构造器进行创建对象u2。
(3)利用反射的API调用方法的时候效率较为低下。在给u3设置属性值“Uname”的时候,我们利用反射机制的实现为:“Method method = clazz.getDeclaredMethod("setUname", String.class);method.invoke(u3, "peng03");”,如果不使用反射,实现为:“u3.setUname(peng03);”。所以我们使用反射的时候要尽量慎重。
(4)在User类的定义中,我们会经常定义private的属性或者方法,此时,被private修饰的属性或者方法,都只能在该类的内部使用。当我们使用反射的时候,也是无法调用private修饰的方法或属性的。所以我们通过反射中的setAccessible(true)的途径,实现外部对私有内容的访问与修改,其代表的含义为:不需要通过安全检查,直接设置属性值。
(5)使用反射机制和不使用反射机制进行运行时间的对比:通过安全检查(即:setAccessible(false))的运行时间是不使用反射机制的12倍左右,不通过安全检查(即:setAccessible(true))的效率大概是不使用该语句的4倍。所以慎重使用反射,如果不得不用反射,那么尽可能将其设置为不通过安全检查(即:setAccessible(true)),这样可以在很大程度上提高运行效率。
二、动态编译
1、为什么我们需要使用动态编译
java 6.0引入的动态编译机制
静态编译:一次性编译。在编译的时候把你所有的模块都编译进去。
动态编译:按需编译。程序在运行的时候,用到那个模块就编译哪个模块。
现实中的实例:
比如开发一个阅读器,支持txt,pdf,doc三种格式。我们把读txt,读pdf,读doc定义为三个功能模块。
静态编译:我想看个txt,点击应用程序图标以后,三个功能都加载进来了。在这里,另外两个模块的作用就是占用系统资源。
动态编译:我想看个txt,点击应用程序,判断格式,只加载读txt模块,使用读txt模块。显然,动态编译速度快,节省了系统资源,利于今后拓展。
2、结合实际代码进行讲解
public class Demo01 { public static void main(String[] args) throws IOException { /** * 动态编译 */ JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); int result = compiler.run(null, null, null, "G:\java学习\test\HelloWorld.java"); System.out.println(result == 0?"编译成功":"编译失败"); //通过Runtime调用执行类 Runtime run = Runtime.getRuntime(); Process process = run.exec("java -cp G:/java学习/test HelloWorld"); //输出程序中的打印内容 InputStream in = process.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(in)); String info = ""; while((info = reader.readLine()) != null) { System.out.println(info); } } }
tips:在动态编译的时候,我们首先获取java编译器对象,然后对需要动态编译的java类进行编译操作,主要使用run方法。
int javax.tools.run(InputStream in,OutputStream out,OutputStream err,String… arguments) 方法参数介绍:
第一个参数:为java编译器提供参数
第二个参数:得到java编译器的输出信息
第三个参数:接收编译器的错误信息
第四个参数:可变参数(是一个String数组)能传入一个或多个java源文件
返回值:0表示编译成功,非0表示编译失败。
在动态编译源文件之后,就可以执行源文件了。我们利用IO流输出源文件“HelloWorld”中的内容。
三、脚本引擎执行JavaScript代码:
java脚本引擎是从jdk6.0之后添加的新功能。
脚本引擎介绍:
(1)使得java应用程序可以通过一套固定的接口与各种脚本引擎交互,从而达到在java平台上调用各种脚本语言的目的。
(2)java脚本API是连通java平台和脚本语言的桥梁。
(3)可以把一些复杂异变的业务逻辑交给脚本语言处理,这又大大提高了开发效率。
其实简单的说,也就是通过java中的各个接口,使得JavaScript语句,可以在java的平台上得以实现。结合下面的实例进行解析:
public class Demo01 { public static void main(String[] args) throws ScriptException, NoSuchMethodException, IOException { //获得脚本引擎对象 ScriptEngineManager sem = new ScriptEngineManager(); ScriptEngine engine = sem.getEngineByName("javascript"); //定义变量,存储到引擎上下文中 engine.put("msg", "peng is a good man!"); String str = "var user = {name : 'peng' ,age:18,schools:['CTGU','UESTC']};"; str += "println(user.name);"; //执行脚本 engine.eval(str); engine.eval("msg = 'UESTC is a good school';");//msg的值,可以被动态改变 System.out.println(engine.get("msg")); System.out.println("#####################"); //定义函数 engine.eval("function add(a,b){var sum = a + b; return sum;}"); //取得调用接口 Invocable jsInvoke = (Invocable) engine; //执行脚本中定义的方法 Object result1 = jsInvoke.invokeFunction("add", new Object[] {13,20}); System.out.println(result1); //导入其他java包,使用其他包中的java类 String jscode = "importPackage(java.util);var list = Arrays.asList(["CTGU","UESTC"]);"; engine.eval(jscode);//执行js语句 //从JavaScript中取出list变量,并将其转换为java中的list变量 List<String> list2 = (List<String>)engine.get("list"); for(String temp:list2) { System.out.println(temp); } //执行一个js文件(我们将a.js置于项目的src下即可) URL url = Demo01.class.getClassLoader().getResource("a.js"); FileReader fr = new FileReader(url.getPath()); engine.eval(fr); fr.close(); } }
tips:上面的代码就是使用脚本语言执行javascript代码。
(1)首先就是获取脚本引擎对象,我们利用引擎对象,选择JavaScript语言。在脚本引擎中,我们可以按照JavaScript的语法,将JavaScript语句写在字符串中,再使用脚本语言中的“eval”方法,执行字符串中的JavaScript语句。与此同时,我们也可以在eval方法中改变已经被定义的msg的值。
(2)在上面的代码中,我们主要是将js中的不同功能,使用其语法格式写入字符串中,使用脚本引擎进行执行。同时我们还可以将js中的变量(比如List)转换到java中进行使用。
在最后一段代码中,我们不再仅仅将js语言写在java中的字符串中进行执行,而是在src文件下直接编写一个a.js文件,文件内容如下所示,然后在java中直接执行此js文件。
//定义一个test方法 function test(){ var a = 3; var b = 4; println("invoke js file:"+(a+b)); } //执行test方法 test();