URLClassLoader使用方法

    科技2024-05-16  77

    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中的类并不会列出来。

    Processed: 0.021, SQL: 8