当Java反射遇到了类加载器

以前经常偷懒用反射实现一些方便的功能(有点儿动态语言的赶脚),殊不知前几天在阅读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();
		}
 	}
}

1、反射到内部类

我们知道,如果只是反射到外部类,那么forName("xx.xx.xx")就可以,内部类要注意与外部类直接用“$”链接,否则会报 ClassNotFound。

2、访问私有函数(或者属性)

看到代码当中有一句 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)

3、反射构造的对象

我前天看Tomcat源码,看到catalinaDaemon对象反射构造出来之后,还是用反射调用其中的方法就很奇怪(其实是很无知),为什么不把对象强转为Catalina对象呢?看官可能觉得奇怪,既然用反射,那很有可能就是不知道什么类,谈何强转?不过Tomcat这里用反射目的主要是用自己的类加载器加载类,在对应的那几行代码,其实程序完全有理由知道那个对象实例是个什么东西。

那么强转究竟行不行呢?

			Class<?> clazz = Class.forName("com.ben.jni.TestReflection$Inner");
			Inner inner = (Inner) clazz.newInstance();
			inner.print();

我把代码改成上面的样子,print方法也改成public,这样不会有访问限制的问题。运行:

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对象时,都只能通过反射来调用了。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值