面向对象
类加载器
Java虚拟机中可以安装多个类加载器,系统默认有三个主要的类加载器,每个类加载器负责加载特定位置的类:
BootStrap,ExtClassLoader,AppClassLoader。
类加载器也是Java类,因此Java类的类加载器本身也要被其他的类加载器加载,显然必须有第一个类加载器不是Java类,它就是BootStrap类加载器。
代码示例:
package com.itheima.day02;
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(ClassLoaderTest.class.getClassLoader().
getClass().getName());
System.out.println(System.class.getClassLoader());
}}
由上面的示例可以看到ClassLoaderTest类是由AppClassLoader类加载器加载的。
System类是由BootStrap类加载器加载的。
注意:
JVM内核启动的时候,BootStrap就已经被加载了,它是内嵌在JVM内核中的,是用C++语言编写的二进制代
码,因此不需要其他类加载器加载。
Java虚拟机中的所有类装载器采用了具有父子关系的树形结构进行组织。
代码示例:
package com.itheima.day02;
public class ClassLoaderTest {
public static void main(String[] args) {
ClassLoader loader = ClassLoaderTest.class.getClassLoader();
while(loader !=null){
}System.out.println(loader.getClass().getName());
loader = loader.getParent();
}
System.out.println(loader);
由上面的示例可以看到AppClassLoader类加载器的父级别类加载器是ExtClassLoader类加载器,}
ExtClassLoader类加载器的父级别类加载器是BootStrap类加载器。
在实例化每个类加载器对象时,需要为其指定一个父级类加载器对象或者默认采用系统类加载器为其父级类加载。
构造方法摘要 protected
ClassLoader()
使用方法getSystemClassLoader()
返回的ClassLoader 创建一个新的类加载器,将该加载器作为父类加载器。protected
ClassLoader(ClassLoader parent)
使用指定的、用于委托操作的父类加载器创建新的类加载器。
类加载器之间的父子关系和管辖范围图
小实验:将ClassLoaderTest类的class文件,打成jar包,放置在JRE/lib/ext/*.jar,它会由哪个类加载器加载呢?
右击ClassLoaderTest.java-->点击Export。
弹出如下窗口,点击Java/JAR file-->点击Next。
点击Browse...。
存放在JDK目录下的JRE/lib/ext目录下,并且jar包取名为heima.jar。
然后再执行ClassLoaderTest类就会出现如下结果:
也就是说ClassLoaderTest类现在由ExtClassLoader类加载器加载。
类加载器的委托机制
当Java虚拟机要加载一个类时,到底派出哪个类加载器去加载呢?
首先当前线程的类加载器去加载线程中的第一个类。
如果类A中引用了类B,Java虚拟机将使用加载类A的类加载器来加载类B。
还可以直接调用ClassLoader.loadClass()方法来指定某个类加载器去加载某个类。
每个类加载器加载类时,又先委托给其上级类加载器。
当所有祖宗类加载器没有加载到类,回到发起者类加载器,还加载不了,则抛ClassNotFoundException,
不是再去找发起者类加载器的儿子,因为没有getChild方法,即使有,那有多个儿子,找哪一个呢?
注意:
每个ClassLoader本身只能分别加载特定位置和目录中的类,但它们可以委托其他的类装载器去加载类,这就是类加载器的
委托模式。类加载器一级级委托到BootStrap类加载器,当BootStrap无法加载当前所要加载的类时,然后才一级级回退到子
孙类装载器去进行真正的加载。当回退到最初的类装载器时,如果它自己也不能完成类的装加载,
那就会告ClassNotFoundException异常。
有一道面试题,能不能自己写个类叫java.lang.System?
答案是不能,即使写了也不会被类加载器加载。为了不让我们写System类,类加载机制采用委托机制,这样
可以保证父级类加载器优先,也就是总是使用父级类加载器能找到的类,结果就是总是使用Java系统自身提
供的System类,而不会使用我们自己所写的System类。
自定义类加载器的编写原理分析
类加载器中的loadClass方法内部实现了父类委托机制,因此我们没有必要自己覆盖loadClass,否则需要自己去实
现父类委托机制。我们只需要覆盖findClass方法。loadClass方法中调用了findClass方法,使用的是模板设计模
式。我们得到了Class文件后,就可以通过defineClass方法将二进制数据转换成字节码。这就是自定义类加载器的
编写原理。
API文档中的例子:
需求:
编写一个对文件内容进行简单加密的程序。
步骤:
1.对生成的字节码文件进行一个字节一个字节读取,然后对读取到的字节进行加密运算。
2.把加密后的字节读取到另外一个字节码文件中。
对字节码文件进行加密,代码示例:
package com.itheima.day02;
public class ClassLoaderAttachment{
public String toString(){
return "程序员";
}
}
package com.itheima.day02;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader{
public static void main(String[] args) throws Exception{
String srcPath = args[0];
String destPath = args[1]+"\\"+srcPath.substring(srcPath.lastIndexOf("\\")+1,srcPath.length());
InputStream ips =new FileInputStream(srcPath);
OutputStream ops =new FileOutputStream(destPath);
encrypt(ips,ops);
ips.close();
ops.close();
}
//对输入流数据进行加密
public static void encrypt(InputStream ips,OutputStream ops) throws Exception{
int by = 0;
while((by=ips.read())!=-1){
ops.write(by ^ 0xff);
}
}
}
package com.itheima.day02;
public class ClassLoaderTest {
public static void main(String[] args) {
System.out.println(new ClassLoaderAttachment().toString());
}
}
执行MyClassLoader类中的main方法,首先先设置传入的参数:
注意:在运行的时候一定要注意运行的Main方法所在的类是否是MyClassLoader!
执行MyClassLoader类中的main方法后,按F5刷新目录,则可以看到编码后的ClassLoaderTest.class文件。
编写和测试自己编写的解密类加载器
将此class文件,覆盖项目中bin\com\itheima\day02目录下的ClassLoaderAttachment.class文件,重新执行
ClassLoaderTest类中的main方法,就会报错。这是因为AppClassLoader已经无法正确加载编码后的class文件了,
只有我们自己写一个带有解码功能的类加载器才可以。
Exception in thread "main" java.lang.ClassFormatError: Incompatible magic value 889275713 in class file com/itheima/day2/ClassLoaderAttachment
at java.lang.ClassLoader.defineClass1( Native Method)at java.lang.ClassLoader.defineClass( ClassLoader.java:620)at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:124)at java.net.URLClassLoader.defineClass( URLClassLoader.java:260)at java.net.URLClassLoader.access$000( URLClassLoader.java:56)at java.net.URLClassLoader$1.run( URLClassLoader.java:195)at java.security.AccessController.doPrivileged( Native Method)at java.net.URLClassLoader.findClass( URLClassLoader.java:188)at java.lang.ClassLoader.loadClass( ClassLoader.java:306)at sun.misc.Launcher$AppClassLoader.loadClass( Launcher.java:276)at java.lang.ClassLoader.loadClass( ClassLoader.java:251)at java.lang.ClassLoader.loadClassInternal( ClassLoader.java:319)at com.itheima.day2.ClassLoaderTest.main( ClassLoaderTest.java:6)
需求:
编写一个自己的类加载器,可实现对加密过的类进行加载和解密。
步骤:
1、自定义的类加载器必须继承ClassLoader
2、覆盖findClass方法。
3、在findClass方法中对字节码文件进行解密。
4、调用自定义的类加载器对字节码文件进行解密。
自定义类加载器解密,代码示例:
package com.itheima.day02;
import java.util.Date;
public class ClassLoaderAttachment extends Date{
public String toString(){
}return "程序员";
}
package com.itheima.day02;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
public class MyClassLoader extends ClassLoader{
private String classDir;
public MyClassLoader(){}
public MyClassLoader(String classDir){
this.classDir = classDir;
}
@SuppressWarnings("deprecation")
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String classFileName = classDir +"\\"+name.substring(name.lastIndexOf(".")+1)+".class";
try {
InputStream ips =new FileInputStream(classFileName);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
encrypt(ips,bos);
ips.close();
byte[] bytes = bos.toByteArray();
return defineClass(bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.findClass(name);
}
public static void main(String[] args) throws Exception{
String srcPath = args[0];
String destPath = args[1]+"\\"+srcPath.substring(srcPath.lastIndexOf("\\")+1,srcPath.length());
InputStream ips =new FileInputStream(srcPath);
OutputStream ops =new FileOutputStream(destPath);
encrypt(ips,ops);
ips.close();
ops.close();
}
public static void encrypt(InputStream ips,OutputStream ops) throws Exception{
int by = 0;
while((by=ips.read())!=-1){
ops.write(by ^ 0xff);
}
}
}
package com.itheima.day02;
import java.util.Date;
public class ClassLoaderTest {
public static void main(String[] args) throws Exception {
Class clazz = new MyClassLoader("ClassLoaderLib").loadClass("ClassLoaderAttachment");
Date d = (Date)clazz.newInstance();
System. out.println(d);
}
}
注意:
1、之所以让ClassLoaderAttachment类继承Date是因为,如果直接写
ClassLoaderAttachment d = (ClassLoaderAttachment)clazz.newInstance();这条语句,编译的时候就会报错,因为此时的
ClassLoaderAttachment在AppClassLoader加载进内存后就无法识别。所以需要通过借助一个父类对象绕过编译器。也就
是:Date d1 = (Date)clazz.newInstance();。
2、如果想让父类加载器AppClassLoader加载ClassLoaderAttachment类,则需要执行下面的语句:
Class clazz = new MyClassLoader("ClassLoaderLib").loadClass("com.itheima.day2.ClassLoaderAttachment");
但是父类加载可能会出错。
类加载器的一个高级问题的实验分析
编写一个能打印出自己的类加载器名称和当前类加载器的父子结构关系链的MyServlet,正常发布后,看到打印结果为
WebAppClassloader。把MyServlet.class文件打成jar包,放到ext目录中,重启tomcat,发现找不到HttpServlet的错误。
把servlet.jar也放到ext目录中,问题解决了,打印的结果是ExtclassLoader 。
具体步骤:新建一个Web工程。
项目名称命名为itheimaweb-->点击Finish。
右击src-->New-->Servlet,创建一个Servlet。
命名包名和类名,并且只创建doGet方法。点击Next。
点击Finish。
Web代码示例:
package com.itheima.itheimaweb.web.servlets;
import java.io.IOException;
importjava.io.PrintWriter;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
importjavax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public classMyServletextendsHttpServlet {
public voiddoGet(HttpServletRequest request, HttpServletResponse response)
throwsServletException, IOException {
response.setContentType("text/html");
PrintWriter out = response.getWriter();
ClassLoader loader =this.getClass().getClassLoader();
while(loader!=null){
out.println(loader.getClass().getName()+"<br/>");
loader = loader.getParent();
}
out.close();
}
}
选择要运行的文件,右击,Run As ,MyEclipse Server Application
跳出对话框,选择一个Tomcat服务器,点击OK
运行结果如下:
附:由于关于tomcat和Servlet部分的知识点后面的课程都会涉及,因此这里不详述。
在浏览器中输入访问MyServlet的地址,可以看到MyServlet是由WebappClassLoader类加载器加载的。
将MyServlet以jar包的方式导出到jdk\jre\lib\ext目录下,jar包名称为itheima.jar。
重启Tomcat服务器,并且通过浏览器访问,可以看到错误信息提示无法加载HttpServlet。
父级类加载器加载的类无法引用只能被子级类加载器加载的类,原理如下图:
错误原因:
在没有将MyServlet导出为jar包到jdk\jre\lib\ext目录以前,MyServlet以及其父类都是由WebAppClassLoader加
载的。但是导出到jdk\jre\lib\ext目录以后,就由ExtClassLoader类加载器加载MyServlet,但是HttpServlet却不
在ext目录下,所以无法加载到。而且MyServlet和HttpServlet是同一线程的,这也就意味着他们两个是由同一
个类加载器加载的。ExtClassLoader类加载器加载不了,就会直接报错,而不会再交给WebAppClassLoader
类加载器再加载。
解决方法:将HttpServlet所在的jar包拷贝到jdk\jre\lib\ext目录下,重新启动tomcat服务器,重新在浏览器中访问
MyServlet。
结果:可以看到此时加载MyServlet的类加载器是ExtClassLoader类加载器。