Java之类的加载机制与反射

类的加载机制与反射

类的加载连接与初始化

JVM和类

类的加载(产生一个Class的对象)

当程序主动使用一个类的时候,如果该类还没有被加载到内存中,则系统会通过加载、连接、初始化三个步骤来初始化这个类。这三个步骤通常被称为类的初始化。

类的加载主要是指将class文件读入内存,并且创建一个java.lang.Class对象。也就是说程序使用任何类时都会为之建立一个java.lang.Class的对象。

站在JVM的角度上什么都是对象,我们常说万物皆对象。一个类它表示了一类的事物,但是类本身也是一种事物所以一个类也就是一个对象,既类对象。

类的加载是由类的加载器完成的,类加载器是由JVM提供。这些类加载器通常被称之为系统的类加载器,当然我们也可以继承ClassLoader来创建自己的自定义类。

我们无需等到首次使用类时才加载这个类。Java虚拟机规范允许系统预先加载某个类。

类的连接(将生成的二进制数据合并到JRE中)

当我们加载好一个类的时候我们就得到了java.lang.Class的一个对象,这个时候我们就进入了连接状态。我们需要将类的二进制数据合并到虚拟机中。

类的初始化(主要是对类变量的初始化)

类的初始化主要就是对类变量的初始化。无论是静态代码块中的还是直接声明的类变量JVM都会按照他们在程序中的排列先后次序来初始化。类的初始化一般分为以下几个步骤:

①  没有加载连接的类先去加载连接。

②  加入该类的父类还没有被加载那么就先去加载父类。

③  类中有初始化语句那么按照顺序依次初始化。

类初始化的时机

①  创建类的实例时,new一个、通过反射、通过反序列化。

②  调用某个类的静态方法时。

③  访问静态变量的时候。

④  强制创建某个类的java.lang.Class对象的时候。

⑤  初始化某个类的子类时。

⑥  Java.exe运行某个主类时。

以下几种情况特别指出:

对于一个final修饰的类变量,如果这个变量在编译时就确定了下来那么这个变量就相当于“宏变量”。在编译的时候就把这个变量出现过的地方替换成它的值。这个时候不需要初始化类。

如果一个final修饰的类变量在编译的时候没有确定下来那么必须等到运行时才能够确定的话。则会导致类的初始化。

当使用ClassLoader的load方法加载某个类时,该方法只是加载某个类并不会执行该类的初始化。当使用Class.forName()方法是就会初始化这个类。

类加载器

类加载器简介

一旦有一个类加载到JVM中那么这个类就不允许再次载入了。在Java中一个类的唯一标识就是他的全限定类名。而在JVM中标识一个加载好的Class对象则还与它的类加载器有关。假如说一个class A被两个不同的加载器B、C加载那么它们也属于不同的加载类分别为(A B)和(A C)。

当JVM启动时,会形成由三个类加载器组成的初始类加载器层次结构。

Bootstrap ClassLoader:类加载器。

Extension ClassLoader:类加载器。

System ClassLoader:类加载器。

①  其中BootstrapClassLoader比较特殊它不继承ClassLoader它直接隶属于JVM不是由Java编写。它负责加载Java的核心类。比如我们常用到的rt.jar文件中的类。

②  Extension ClassLoader为扩展类加载器,它负责加载JRE的扩展目录这样就可以扩展Java核心类之外的新功能。只要把自己开发的jar文件放到JAVA_HOME/jre/lib/ext中就可直接使用。

③  System ClassLoader系统的类加载器,负责加载java命令中classpath相关的选项。

最后指出他们的继承关系:

ObjectàClassLoaderàURLClassLoaderà……Extendsion ClassLoader||System ClassLoader。

类的加载机制

JVM的类加载机制主要有以下三种。

全盘负责:一个加载器加载当前类与其引用的所有类。

父类委托:首先说这里的父类不是Java中的有继承关系的父类,而是在现实意义上的父类。当一个加载器遇到一个类首先交给父类去加载它,当父加载器无法胜任的时候再交给子类加载器去加载当前类。

缓存机制:缓存机制会保证所有加载过的Class都会被缓存。当程序需要用到某个Class的时候会首先在缓存区中寻找如果没有的话再进行加载,有的话直接拿来使用。

加载类的几个步骤:

①  检测该Class有没有被加载过,若加载过则直接返回java.lang.Class对象。

②  在正式加载一个Class的时候秉承着有父类加载器先用父类加载器,没有则使用根类加载器。

③  在加载的过程中如果没有找到Class文件的话那么就会在与此ClassLoader中相关的路径中寻找找不到则抛出ClassNotFoundException。

④  加载的最终目的是生成一个java.lang.Class对象。

使用自定义加载类

在JVM中所有的类加载器除了根加载器其余都是ClassLoader的加载器,我们可以继承ClassLoader的子类实例来自定义类加载器。

在ClassLoader中有两个关键的方法

loadClass(String name,booleanresolve)根据类名来加载指定的Class。

findClass(String name)根据指定名称来查找类。

其中loadClass方法比较复杂,他会经过首先检测缓存中是否有已经加载过的当前对象,然后会首先调用父类的构造器,最后使用findClass方法来查找类。我们注意到他最后调用的方法是findClass所以我们只要手动的重写findClass方法就能够强制规避前两个步骤所带来的影响。也避免了复杂的逻辑。下面是一个自定义类加载器的实例,我们通过自定义的类加载器来加载一个简单的Class文件并且执行该简单类的main方法(注意我们是通过反射来执行这个方法的):

自定义类:

package customclassloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;

// 我们使用重写findClass来自定义一个类加载器 
public class CompileClassLoader extends ClassLoader{
    // 读取一个文件的内容
	private byte[] getBytes(String filename) throws Exception, IOException {
		File file = new File(filename);
		long len = file.length();
		byte[] raw = new byte[(int)len];
		try (FileInputStream fin = new FileInputStream(file)){
			// 一次性读取全部的Class文件的全部二进制数据
			int r = fin.read(raw);
			if (r != len) {
				throw new IOException("无法读取全部文件!");
			}
			return raw;
		} 
	}
	
	// 编译指定的Java文件
	private boolean compile(String javaFile) throws IOException {
		System.out.println("CompileClassLoader正在编译" + javaFile + "...");
		System.out.println("文件名:" + javaFile);
		javaFile = javaFile.substring(javaFile.lastIndexOf("/") + 1, javaFile.length());
		// 调用系统的javac命令
		Process p = Runtime.getRuntime().exec("javac " + javaFile);
		
		// 让其它线程都等待这个线程完成
		try {
			p.waitFor();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		int ret = p.exitValue();
		return ret == 0;
	}
	
	// 重写ClassLoader的findClass()方法
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		Class clazz = null;
		String fileSub = name.replace(".", "/");
		String javaFilename = fileSub + ".java";
		String classFilename = fileSub + ".class";
		// 均创建对应的文件对象
		File javaFile = new File(javaFilename);
		File classFile = new File(classFilename);
		/*
		 * 当指定的class文件不存在或者是java文件的最后修改时间比class的
		 * 时间晚就需要重新生成或者修改class文件
		 * */
		if (javaFile.exists() && (!classFile.exists() 
				|| javaFile.lastModified() > classFile.lastModified())) {
			try {
				boolean b = !compile(javaFilename);
				System.out.println("==============================" + b);
				if ( b || !classFile.exists()) {
			        throw new ClassNotFoundException("ClassNotFoundExecption:" + javaFilename);		
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
			// 如果文件存在那么就把这个class文件转换成一个class对象
			if (classFile.exists()) {
				// 将class的二进制文件读入数组
				try {
					byte[] raw = getBytes(classFilename);
					
					clazz = defineClass(name , raw , 0 , raw.length);
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
		if (clazz == null) {
			throw new ClassNotFoundException(name);
		}
		return clazz;
	}
	
	public static void main(String[] args) throws Exception {
		// 我们是从主函数数组来传入对象的所以运行程序时没有参数就没有目标类
		System.out.println("正在运行:CompileClassLoader类");
		if (args.length < 1) {
			System.out.println("乌拉拉拉拉缺少目标类,请按照如下的格式运行Java文件");
            System.out.println("java CompileClassLoader ClassName");
		}
            // 第一个参数是需要运行的类
            String progClass = args[0];
            // 剩下的参数将作为运行目标类时的参数
            // 将这些参数复制到一个新数组中
            String[] progArgs = new String[args.length - 1];
            System.arraycopy(args, 1, progArgs, 0, progArgs.length);
            CompileClassLoader ccl = new CompileClassLoader();
            Class<?> clazz = ccl.loadClass(progClass);
            // 获取需要运行的类的主方法
            Method main = clazz.getMethod("main", new String[0].getClass());
            Object argsArray[] = {progArgs};
            main.invoke(null, argsArray);
            System.out.println("运行完毕!");
	}
}

被加载类:

package customclassloader;

public class Hello {
    public static void main(String[] args) {
		System.out.println("12312");
        for (String arg : args) {
        	System.out.println("运行Hello的参数:" + arg);
        }
    }
}

URLClassLoader类

URLClassLoader既可以从本地加载二进制数据来加载类也可以从互联网上来加载数据来加载类。

使用反射查看信息

反射也就是剖析,发现运行时对象的真实信息。有时候我们在编译的阶段无法获得一个对象的运行时信息。为了调用运行时的对象的方法我们可以通过instance of关键字来判断进行强转,也可以通过反射来获取。

获取Class对象

通过反射来获得泛型通常有三种方法:

①  通过Class.forName(StringclazzName)类名必须是全限定类名。

②  通过某个类的class属性来获得。

③  getClass()方法,所有的Java对象都可以调用的方法它属于Object类。

推荐使用②无需调用方法性能比较高,而且代码更安全在编译的阶段就能够知道Class对象是否存在。

获取Class中的信息

一个实例说明这些问题:

package reflect;

import java.lang.annotation.Annotation;
import java.lang.annotation.Repeatable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

// 定义可重复注解
@Repeatable(Annos.class)
@interface Anno {}
@interface Annos {
  Anno[] value();	
}
// 使用四个注解类修饰该类
@SuppressWarnings(value = { "unchecked" })
@Deprecated
@Anno
@Anno
public class ClassTest {
    private ClassTest() {
    	
    }
    public ClassTest(String name) {
    	System.out.println("有参数的构造器!");
    }
    public void info() {
    	System.out.println("无参数的info方法。。。");
    }
    public void info(String name) {
    	System.out.println("有参数的info方法。。");
    }
    class Inner {
    	
    }
    public static void main(String[] args) throws Exception {
		Class<ClassTest> clazz = ClassTest.class;
		Constructor[] ctors = clazz.getDeclaredConstructors();
		System.out.println("ClassTest对应的所有构造器如下:");
		for (Constructor c : ctors) {
			System.out.println(c);
		}
		// 获取全部的public构造器
		Constructor[] publicCtors = clazz.getConstructors();
		System.out.println("ClassTest的全部public构造器如下");
		for (Constructor c : publicCtors) {
			System.out.println(c);
		}
		// 获取全部的public的方法
		Method[] methods = clazz.getMethods();
		System.out.println("ClassTest的全部public方法如下");
		for (Method m : methods) {
			System.out.println(m);
		}
		// 获取指定的方法
		System.out.println("带参数的info方法如下:"+ clazz.getMethod("info", String.class));
		// 获取class的全部注解
		Annotation[] anns = clazz.getAnnotations();
		System.out.println("全部的Annotation如下:");
		for (Annotation an : anns) {
			System.out.println(an);
		}
		System.out.println("该类的全部内部类如下:");
		Class<?>[] inners = clazz.getDeclaredClasses();
		for (Class c : inners) {
			System.out.println(c);
		}
		Class innerclass = clazz.forName("reflect.ClassTest$Inner"); // 此处需要写全限定类名才ok
		System.out.println("Inner对应的外部类是:" + innerclass.getDeclaringClass());
		// 也可以获取包名和父类
		System.out.println("package: " + clazz.getPackage());
		System.out.println("superclass" + clazz.getSuperclass());
	}
}

有的注解只停留在源码级别(使用了@Retention(value=SOURCE))所以无法获取。

获得参数

在Java8的java.lang.reflect包下面有一个Executable抽象基类它包含了可执行的类成员包括方法(Method)和构造器(Constructor)。这些类下面有两个方法来获得他们方法的参数分别是:

①  int getParameterCount():获得方法的参数的个数。

②  Parameter[] getParameters():获取该方法或者是该构造器的所有参数。

而parameter本身也提供了一些方法来获取参数的泛型类型。值的指出的是parameter在没有指定-parameters的时候是无法获取形参名的。所以在编译的时候需要javac –parameters –d . Xxx.java来保留形参信息。以下为示例代码:

package reflect;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;

// 使用反射得到方法参数
public class ParmaeterReflect {
    public static void main(String[] args) throws Exception {
    	Class clazz = Test.class;
    	// 获取Test类的带有两个参数的方法
    	Method replace = clazz.getMethod("replace", String.class, List.class);
    	System.out.println("replace方法参数个数 :" + replace.getParameterCount());
    	Parameter[] params = replace.getParameters();
    	int index = 1;
    	for (Parameter p : params) {
    		System.out.println("第" + index++ + "个参数的消息");
    		System.out.println("参数名:" + p.getName());
    		System.out.println("参数类型" + p.getType());
    		System.out.println("泛型类型" + p.getParameterizedType());
    	}
    	replace.invoke(clazz.newInstance(), "zhuliu7", null); // 方法调用
	}
}

class Test {
	public void replace (String str, List<String> list) {
		System.out.println(str);
	}
}

使用反射操作对象

Class可以获得被加载类中的属性、方法、构造器并且用Field、Method、Constructor三个对象来表示。通过这三个对象我们可以访问修改字段值、调用方法、创建对象。

创建对象调用方法

当我们利用反射创建一个对象时有两种方法:

①  通过默认的构造器来创建。直接clazz.newInstance();

②  通过制定的构造器来创建。首先获得指定的构造器然后再创建。

下面模仿一下Spring的Bean工厂,这也是Ioc(控制反转指的是对象的创建靠的是反射机制的newInstance())的基础。基本步骤就是从配置文件中读取到我们传进去的字符串,然后再找到字符串对应的value。最后我们拿着value来创建一个新的对象。

主类信息

package reflect;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

public class ObjectNewInstance {
	// 读取配置文件得到对象  这是使用了默认的构造器来产生对象的还可以使用指定的构造器来产生对象
	private Map<String, Object> pool = new HashMap<String, Object>();
    public static void main(String[] args) {
    	try {
    		ObjectNewInstance oni = new ObjectNewInstance();
			oni.initObjectPool("src/reflect/zhuliu7.properties");
			System.out.println(oni.getObject("b"));
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
    private Object createObject(String clazzName) throws Exception {
    	Class<?> clazz = Class.forName(clazzName);
    	return clazz.newInstance();
    }
    public void initObjectPool(String fileName) throws Exception {
    	FileInputStream fis = new FileInputStream(fileName);
    	Properties p = new Properties();
    	p.load(fis);
    	for (String name : p.stringPropertyNames()) {
    		pool.put(name, createObject(p.getProperty(name)));
    	}
    	fis.close();
    }
    
    public Object getObject(String objectName) {
    	return pool.get(objectName);
    }
}

配置文件信息

a=javax.swing.JFrame

b=java.util.Date

访问成员变量值

Class对象的getField()和getFields()两个方法可以获得指定的或全部的成员变量的值。而他们都只是可以获得public修饰的成员变量而getDeclaredField(“name”)可以获的所有修饰符的变量。在访问成员变量之前首先将获得的Filed的值setAccessible(true);这样的话就可以强制访问private的成员变量值了。以下为示例代码:


package reflect;

import java.lang.reflect.Field;
// 访问private的字段
public class FieldTest {
    public static void main(String[] args) throws Exception {
		Person p = new Person();
		Class<Person> clazz = Person.class;
		Field f = clazz.getDeclaredField("name");
		f.setAccessible(true);
		f.set(p, "zhulu7");
		System.out.println(p);
		
		
	}
}
class Person{
	private String name;
	public String toString() {
		return this.name;
	}
	
}

动态创建数组

在java.lang.reflect包下有一个Array类这个类可以动态的创建一个任意类型的数组并且可以操作。以下为示例代码:

package reflect;

import java.lang.reflect.Array;
// 动态的创建一个数组
public class DynamicCreateArray {
    public static void main(String[] args) {
		Object arr = Array.newInstance(String.class, 10);
		Array.set(arr, 0, "zhuliu7");
		Array.set(arr, 1, "天脊乾坤剑");
		Object str1 = Array.get(arr, 0);
		Object str2 = Array.get(arr, 1);
		System.out.println(str1);
		System.out.println(str2);
	}
}

使用反射生成动态代理

动态代理在以前专门学过借此机会回顾一把:

目标就是在不改变源代码的情况下在方法的前后加上一些操作。

首先说静态代理,它包括继承代理和接口代理这些代理都是提前编好的。继承代理的缺点就是会出现无限继承的局面和无法调整处理顺序非常的不灵活。接口代理要比继承代理灵活一些但是也不可避免的预先写好很多的代理类。他们的原理类似就是我们在调用原来的方法时再实现代理之后转而去调用子类的增强方法,而子类的增强方法内部会有前置后置的操作,中间夹着父类的目标方法,这样也就满足了我们的代理要求。

而动态代理不要就我们提前写好静态的代理类,而是传入一些参数利用类加载器动态生成代理类。我们传入的参数有

①  我们所代理的类的类加载器

②  我们所代理类的所有接口(因为我们还是要模拟静态的代理)

③  一个处理器类,我们要如何处理目标方法。

Ok这样我们就可以动态的生成代理类了。以下为一个示例代码:

目标类的接口类

package proxytest;

public interface Dog {
    void info();
    void run();
}

拦截器方法类(这个类可以不写直接在操作类中写上如何处理但是为了可扩展性还是写了)

package proxytest;

public class DogUtil {// 拦截器方法
    public void method1() {
    	System.out.println("this is 1--------------");
    }
    public void method2() {
    	System.out.println("this is 2--------------");
    }
}

目标类(不改变它代码的情况下要在它的代码的前后加上操作)

package proxytest;

public class GunDog implements Dog{

	@Override
	public void info() {
        System.out.println("我是一条Dog");		
	}

	@Override
	public void run() {
       System.out.println("I am run fast!");		
	}
    
}

代理工厂类(返回一个个代理类)

package proxytest;

import java.lang.reflect.Proxy;

public class MyProxyFactory {
    public static <T> T getProxy(T target) {
    	MyInvocationHandler mi = new MyInvocationHandler();
    	mi.setTarget(target);
    	return (T) Proxy.newProxyInstance(target.getClass().getClassLoader()
    			, target.getClass().getInterfaces(),mi);
    }
}

处理类(如何处理目标方法)

package proxytest;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class MyInvocationHandler implements InvocationHandler{
    private Object target;
    public void setTarget(Object target) {
    	this.target = target;
    }
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		DogUtil du = new DogUtil();
		du.method1();
		Object result = method.invoke(target, args);
		du.method2();
		return result;
	}

}

测试类

package proxytest;

public class Test {
    public static void main(String[] args) {
		GunDog dog = new GunDog();
		Dog d = MyProxyFactory.getProxy(dog);// 这就是要传入接口的原因
		d.info();
		d.run();
	}
}


再次附上一个深入理解的图:


使用反射获取泛型信息

这部分主要是引出一个概念参数化类型ParameterizedType这个类提供了这样的两个方法:

①  get RawType():返回没有泛型的原始类型。

②  get ActualTypeArguments():返回有泛型参数的类型。

以下为一个示例代码:

package generic;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.Map;
import java.lang.reflect.Type;
public class GenericTest {
	private Map<String, Integer> map;
    public static void main(String[] args) throws Exception {
    	// 使用反射获得泛型的信息
		Class<GenericTest> clazz = GenericTest.class;
		Field field = clazz.getDeclaredField("map");
		field.setAccessible(true);
		Class<?> c = field.getType();
		System.out.println("字段的类型是" + c);
		Type t = field.getGenericType();
		if (t instanceof ParameterizedType) {
			ParameterizedType pType = (ParameterizedType)t;
			Type rType = pType.getRawType();
			System.out.println("原始类型是" + rType);
			Type[] tArgs = pType.getActualTypeArguments();
			for (int i = 0; i < tArgs.length; i++) {
				System.out.println("第" + i + "个泛型类型是" + tArgs[i]);
			}
		}else {
			System.out.println("泛型类型出错!");
		}
	}
}













猜你喜欢

转载自blog.csdn.net/qq_34993631/article/details/80808007