URLClassLoader使用方法
1. java类加载器
程序若想执行,必须加载到内存当中才能成功执行。java程序并不是可执行文件,由许多独立的类文件来完成。所以java中加载程序是以类为单外来完成的。这也就需要我们来简单了解一下java的class loader加载机制。
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
引导类加载器(bootstrap class loader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。
扩展类加载器(extensions class loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。
系统类加载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。
除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求。
我们平时程序执行的时候在类加载器中寻找类的结构的顺序是:引导类加载器-》 扩展类加载器-》系统类加载器-》我们自定义的一些类加载器,每个类加载器都有自己的空间,同一个加载器里面的类的二进制名字必须是唯一的,当然同一个类也可以存在不同的加载器内存区域里面,不过我们寻找类的时候是按顺序找的,一但找的也就不会继续往下找了,最终也没找到就会报类不存在异常。
我们如果想动态加载类的话就要仿照我们用的服务器如tomcat和weblogic之类的,他们的开发模式也就是把所有的类都加载到自身的类加载器中,当文件被替换的时候他们就重新加载新的class到内存里面去,从而实现了类的动态加载。
2. 其他
2.1 Java虚拟机的第一个类加载器是Bootstrap,这个加载器很特殊,它不是Java类,因此它不需要被别人加载,它嵌套在Java虚拟机内核里面,也就是JVM启动的时候Bootstrap就已经启动,它是用C++写的二进制代码(不是字节码),它可以去加载别的类。
这也是我们在测试时为什么发现System.class.getClassLoader()
结果为null的原因,这并不表示System这个类没有类加载器,而是它的加载器比较特殊,是BootstrapClassLoader
,由于它不是Java类,因此获得它的引用肯定返回n
2.2 委托机制具体含义
- 首先当前线程的类加载器去加载线程中的第一个类(假设为类A)。
注:当前线程的类加载器可以通过Thread类的getContextClassLoader()获得,也可以通过setContextClassLoader()自己设置类加载器。 - 如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器去加载类B。
- 还可以直接调用
ClassLoader.loadClass()
方法来指定某个类加载器去加载某个类。
当Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?
2.3 委托机制的意义 — 防止内存中出现多份同样的字节码
比如两个类A和类B都要加载System类:
- 如果不用委托而是自己加载自己的,那么类A就会加载一份System字节码,然后类B又会加载一份System字节码,这样内存中就出现了两份System字节码。
- 如果使用委托机制,会递归的向父类查找,也就是首选用Bootstrap尝试加载,如果找不到再向下。这里的System就能在Bootstrap中找到然后加载,如果此时类B也要加载System,也从Bootstrap开始,此时Bootstrap发现已经加载过了System那么直接返回内存中的System即可而不需要重新加载,这样内存中就只有一份System的字节码了。
3. 反射使用的一种场景
某个方法或类A中, 依赖的第三方jar可以动态替换, 或者不能打包到发布版本中, 这时就需要动态获取.
使用方式: 该方法或类A使用URLClassLoader类加载器单独加载, 并使用反射方式调用该类中的方法或实例化; 该类加载器需要满足两个条件:第一是url路径中需要包括三方jar和该类A的jar, 第二是需要重写loadclass方法使其破坏双亲委托机制
4. URLClassLoader使用样例
在使用URLClassLoader加载类时,如果自定义的方法不想违背双亲委派模型,则只需要重写findclass方法即可,如果想违背双亲委派模型,则还需要重写loadclass方法。
同一个Class = ClassName + PackageName + ClassLoaderId(instance)
只要是classPath下的类都是被AppClassLoader加载的,其他以classLoader.load("com.xx.xx") 形式的都是自定义ClassLoader 经过 处理的。
同一个类下的引用类使用这个类的加载器, 和目前线程的classloader无关
如下, Animal类使用默认加载器加载, clazz使用自定义加载器加载
Animal animal = (Animal) clazz.newInstance(); 是不能正常引用的,这行代码用了两个不同的ClassLoader实例,所以Animal animal = (Animal) 这里的引用和clazz.newInstance()不是一个类型的,所以不能互相转换
3.1 自己定义URLClassLoader对象加载外部jar包,针对jar包里面不再出现别的jar包的情况,即只解析.class文件:
private static void test1() {
String path = "D:\\test.jar";//外部jar包的路径
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();//所有的Class对象
Map<Class<?>, Annotation[]> classAnnotationMap = new HashMap<Class<?>, Annotation[]>();//每个Class对象上的注释对象
Map<Class<?>, Map<Method, Annotation[]>> classMethodAnnoMap = new HashMap<Class<?>, Map<Method,Annotation[]>>();//每个Class对象中每个方法上的注释对象
try {
JarFile jarFile = new JarFile(new File(path));
URL url = new URL("file:" + path);
ClassLoader loader = new URLClassLoader(new URL[]{url});//自己定义的classLoader类,把外部路径也加到load路径里,使系统去该路经load对象
Enumeration<JarEntry> es = jarFile.entries();
while (es.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) es.nextElement();
String name = jarEntry.getName();
if(name != null && name.endsWith(".class")){//只解析了.class文件,没有解析里面的jar包
//默认去系统已经定义的路径查找对象,针对外部jar包不能用
//Class<?> c = Thread.currentThread().getContextClassLoader().loadClass(name.replace("/", ".").substring(0,name.length() - 6));
Class<?> c = loader.loadClass(name.replace("/", ".").substring(0,name.length() - 6));//自己定义的loader路径可以找到
System.out.println(c);
classes.add(c);
Annotation[] classAnnos = c.getDeclaredAnnotations();
classAnnotationMap.put(c, classAnnos);
Method[] classMethods = c.getDeclaredMethods();
Map<Method, Annotation[]> methodAnnoMap = new HashMap<Method, Annotation[]>();
for(int i = 0;i<classMethods.length;i++){
Annotation[] a = classMethods[i].getDeclaredAnnotations();
methodAnnoMap.put(classMethods[i], a);
}
classMethodAnnoMap.put(c, methodAnnoMap);
}
}
System.out.println(classes.size());
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
以上的这种情况可以在别的project项目里写test方法,是平时最常用的,如果当.class文件里有依赖别的jar包里的对象的时候,就要把该jar包拷贝到写此测试方法的project并buildPath,不然的话运行的时候会报找不到Class对象的异常。
3.2 第二种情况是针对加载jar包里面的jar包的Class对象,还有读取某一个properties文件的方法。
private static void test2() {
String path = "D:\\test.jar";//此jar包里还有别的jar包
try {
JarFile jarfile = new JarFile(new File(path));
Enumeration<JarEntry> es = jarfile.entries();
while (es.hasMoreElements()) {
JarEntry je = es.nextElement();
String name = je.getName();
if(name.endsWith(".jar")){//读取jar包里的jar包
File f = new File(name);
JarFile j = new JarFile(f);
Enumeration<JarEntry> e = j.entries();
while (e.hasMoreElements()) {
JarEntry jarEntry = (JarEntry) e.nextElement();
System.out.println(jarEntry.getName());
//.........接下去和上面的方法类似
}
}
// System.out.println(je.getName());
if(je.getName().equals("entity_pk.properties")){
InputStream inputStream = jarfile.getInputStream(je);
Properties properties = new Properties();
properties.load(inputStream);
Iterator<Object> ite = properties.keySet().iterator();
while (ite.hasNext()) {
Object key = ite.next();
System.out.println(key + " : " +properties.get(key));
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
3.3. 第三种情况是在该项目下获取某个包的Class对象,当然了,测试方法是在该项目下写的(这样classLoader就直接可以知道对象了,不需要再自定义URLClassLoader了,用Thread.currentThread().getContextClassLoader().loadClass(.....)就可以直接获得Class对象了,回去ClassPath下找,System.out.print(System.getProperty("java.class.path"))就可以找到classPath路径)。
private static Set<Class<?>> getclass() {
Set<Class<?>> classes = new LinkedHashSet<Class<?>>();
boolean flag = true;//是否循环迭代
String packName = "com.yk.framework.db";
// String packName = "org.jdom";
String packDir = packName.replace(".", "/");
Enumeration<URL> dir;
try {
dir = Thread.currentThread().getContextClassLoader().getResources(packDir);
while(dir.hasMoreElements()){
URL url = dir.nextElement();
System.out.println("url:***" + url);
String protocol = url.getProtocol();//获得协议号
if("file".equals(protocol)){
System.err.println("file类型的扫描");
String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
System.out.println("filePath :" + filePath);
findAndAddClassesInPackageByFile(packName, filePath,flag,classes);
}else if("jar".equals(protocol)){
System.err.println("jar类型扫描");
JarFile jar;
jar = ((JarURLConnection)url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
while(entries.hasMoreElements()){
JarEntry entry = entries.nextElement();
String name = entry.getName();
System.out.println(">>>>:" + name);
//......
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
System.out.println(classes.size());
return classes;
}
4.Java查看当前类执行过程中加载的所有类
public class ClassLoadTest {
public static void main(String[] args) throws Exception {
Persion p = new Persion("zhansan", 20);
Field f=ClassLoader.class.getDeclaredField("classes");
f.setAccessible(true);
Vector classes=(Vector)f.get(ClassLoader.getSystemClassLoader());
System.out.println(classes);
}
}
输出: [class test.ClassLoadTest, class test.Persion]
需要说明的是载入的jdk中的类并不会列出来。