以前经常偷懒用反射实现一些方便的功能(有点儿动态语言的赶脚),殊不知前几天在阅读Tomcat源码的时候又新学到了一个知识点。且看下面的例子:
package com.ben.jni;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class TestReflection {
private int aInt;
protected float aFloat;
public String aString;
public void print(){
System.out.println("hello world");
}
static class Inner{
private void print(){
System.out.println("hello inner world!");
}
}
public static void main(String[] args){
try {
Class> clazz = Class.forName("com.ben.jni.TestReflection$Inner");
Object obj = clazz.newInstance();
Method method = clazz.getDeclaredMethod("print", new Class[]{});
//method.setAccessible(true);
method.invoke(obj);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (SecurityException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
我们知道,如果只是反射到外部类,那么forName("xx.xx.xx")就可以,内部类要注意与外部类直接用“$”链接,否则会报 ClassNotFound。
看到代码当中有一句 method.setAccessible(true),我记得在去年接触Hibernate时,有个有趣的现象:Hibernate要求如果映射出来的pojo对象的setId方法为私有,我当时想,这要是私有的话,那该怎么设置id呢。
如果不调用这一句,并传入参数true,那么报错如下:
java.lang.IllegalAccessException: Class com.ben.jni.TestReflection can not access a member of class com.ben.jni.TestReflection$Inner with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:95)
at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:261)
at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:253)
at java.lang.reflect.Method.invoke(Method.java:594)
at com.ben.jni.TestReflection.main(TestReflection.java:27)
我前天看Tomcat源码,看到catalinaDaemon对象反射构造出来之后,还是用反射调用其中的方法就很奇怪(其实是很无知),为什么不把对象强转为Catalina对象呢?看官可能觉得奇怪,既然用反射,那很有可能就是不知道什么类,谈何强转?不过Tomcat这里用反射目的主要是用自己的类加载器加载类,在对应的那几行代码,其实程序完全有理由知道那个对象实例是个什么东西。
那么强转究竟行不行呢?
Class> clazz = Class.forName("com.ben.jni.TestReflection$Inner");
Inner inner = (Inner) clazz.newInstance();
inner.print();
hello inner world!
我擦?发生了什么?居然可以运行,那么为什么Tomcat的源码还那么笨拙的用反射调用?强转一下不就完了?等等,我们好像忽视了一个问题:类加载器。Tomcat为了将类的使用范围隔离起来,采用了三个不同的类加载器加载类,其中Cataline类用的就是catalina加载器加载的。如果我们明着把Inner写到代码里面,那么应该是被jdk自带的app加载器加载,而对于不同的加载器加载的Inner类,其实根本不是同一个类(尽管行为是一致的,这就像同一个人在平行世界的映射一样),那么强转肯定不行啊,是不是这个原因呢?
为了测试,我们自己找一个类加载器模拟一下。
类加载器的代码是网上随便抄的,能够实现加载的功能就行,细节的话暂时不必追究。
package com.ben.jni;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class CompileClassLoader extends ClassLoader {
// 读入源文件转换为字节数组
private byte[] getSource(String filename) {
File file = new File(filename);
int length = (int) file.length();
byte[] contents = new byte[length];
FileInputStream fis = null;
try {
fis = new FileInputStream(file);
int r = fis.read(contents);
if (r != length) {
throw new IOException("IOException:无法读取" + filename);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return contents;
}
// 编译文件
public boolean compile(String javaFile) {
System.out.println("正在编译" + javaFile);
int ret = 0;
try {
// 调用系统命令编译文件
Process process = Runtime.getRuntime().exec("javac " + javaFile);
process.waitFor();
ret = process.exitValue();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
return ret == 0;
}
// 重写findclass
@Override
protected Class> findClass(String name) throws ClassNotFoundException {
Class> clazz = null;
// 将文件的.替换为/,例如com.lyl.reflect.Reflect被替换为com/lyl/reflect/Reflect
String fileStub = name.replace(".", "/");
// java源文件名
String javaFileName = fileStub + ".java";
// 编译后的class文件名
String classFileName = "bin/"+fileStub + ".class";
File javaFile = new File(javaFileName);
File classFile = new File(classFileName);
// 当指定JAVA源文件存在,且class文件不存在,
// 或者java源文件的修改时间比class文件修改时间晚则重新编译
if (javaFile.exists()
&& (!classFile.exists() || javaFile.lastModified() > classFile
.lastModified())) {
// 如果编译失败,或者class文件不存在
if (!compile(javaFileName) || !classFile.exists()) {
throw new ClassNotFoundException("ClassNotFoundException:"
+ javaFileName);
}
}
// 如果CLASS文件按存在,系统负责将该文件转换成Class对象
if (classFile.exists()) {
byte[] raw = getSource(classFileName);
// 将ClassLoader的defineClass方法将二进制数据转换成Class对象
int divindex = name.indexOf("\\");
String javafilename = null;
// 如果是某个盘里面的文件,要去掉文件的盘符
if (divindex != -1) {
javafilename = name.substring(divindex + 1, name.length());
}
// 将字节数组转换为class实例
clazz = defineClass(javafilename, raw, 0, raw.length);
}
// 如果clazz为null,表明加载失败,则抛出异常
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
// 定义主方法
public static void main(String[] args) throws ClassNotFoundException,
SecurityException, NoSuchMethodException, IllegalArgumentException,
IllegalAccessException, InvocationTargetException {
// 如果运行该程序没有参数,则没有目标类
if (args.length < 1) {
System.out.println("缺少运行的目标类,请按如下格式运行源文件");
System.out.println("java CompileClassLoader ClassName");
System.exit(0);
}
// 第一个参数为需要运行的类
String proClass = args[0];
// 剩下的参数将作为目标类得参数
String[] proArgs = new String[args.length - 1];
System.arraycopy(args, 1, proArgs, 0, proArgs.length);
CompileClassLoader ccl = new CompileClassLoader();
// 加载需要运行的类
Class> clazz = ccl.loadClass(proClass);
Method main = clazz.getMethod("main", (new String[0]).getClass());
Object[] argsArray = { proArgs };
main.invoke(null, argsArray);
}
}
好的,我们这时再来运行下面的代码:
CompileClassLoader ccl = new CompileClassLoader();
Class> clazz = ccl.findClass("com.ben.jni.TestReflection$Inner");
Inner inner = (Inner) clazz.newInstance();
inner.print();
结果正如我们所料:
Exception in thread "main" java.lang.ClassCastException: com.ben.jni.TestReflection$Inner cannot be cast to com.ben.jni.TestReflection$Inner
at com.ben.jni.TestReflection.main(TestReflection.java:25)
无法强转!所以Tomcat的开发者没有办法,只好每次使用catalina对象时,都只能通过反射来调用了。