转:https://blog.csdn.net/seu_calvin/article/details/52315125
先介绍自定义类加载器的应用场景:
(1)加密:Java代码可以轻易的被反编译,如果你需要把自己的代码进行加密以防止反编译,可以先将编译后的代码用某种加密算法加密,类加密后就不能再用Java的ClassLoader去加载类了,这时就需要自定义ClassLoader在加载类的时候先解密类,然后再加载。
(2)从非标准的来源加载代码:如果你的字节码是放在数据库、甚至是在云端,就可以自定义类加载器,从指定的来源加载类。
(3)以上两种情况在实际中的综合运用:比如你的应用需要通过网络来传输 Java 类的字节码,为了安全性,这些字节码经过了加密处理。这个时候你就需要自定义类加载器来从某个网络地址上读取加密后的字节代码,接着进行解密和验证,最后定义出在Java虚拟机中运行的类。
1、双亲委派模型
双亲委派对于程序的稳定运行很重要,比如这种机制可以防止用户自定义一个Object类来自己加载,然后就会影响程序运行。
双亲委派的实现非常简单,主要集中在java.lang.ClassLoader中的loadClass()方法之中,从下面的代码可以看出:
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) {
long t0 = System.nanoTime();
try {
if (parent != null) {
//如果没有加载则交给父加载器的loadClas()方法去加载 父类加载器同样会执行同样的操作 查找 然后交给父类加载器
c = parent.loadClass(name, false);
} 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.如果最终父类也没有找到或加载此类,则就会调用调用该类的自己的
long t1 = System.nanoTime();
c = findClass(name);
// 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);
}
return c;
}
}
首先检查这个类是否被加载过,若没有被加载则调用父加载器的loadClass()的方法,若父加载器为空则默认使用启动类加载器作为父加载器。如果父类加载失败,抛出异常后,C=null,则调用自己的findClass()方法进行加载。
双亲委派的的具体逻辑实现在在loadClass()方法中,所以不提倡用户覆盖loadClass()方法,而应当把自己的类加载逻辑写到findClass()中,这样先执行loadClass()方法类加载失败后,就会调用自己的findClass()方法来完成加载,这样就可以保证写出来的类加载器符合双亲委派规格。(当然也可以直接使用自己的类加载器来加载,需要把方法暴露出来)
package com.jvm.myclassloader;
public class Animail {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
package com.jvm.myclassloader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MyClassLoader extends ClassLoader{
private String path;
private String fileName;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public MyClassLoader() {
}
public MyClassLoader(ClassLoader parent) {
super(parent);
// TODO Auto-generated constructor stub
}
public Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
byte [] b=this.getClassBytes();
Class<?> clazz=this.defineClass(null, b, 0, b.length);
return clazz;
}
public byte[] getClassBytes() {
if(path.equals("")||fileName.equals("")){
return null;
}
System.out.println(path+fileName);
File file=new File(path+fileName);
if( file.exists()){
System.out.println("exits");
}
try(FileInputStream fis = new FileInputStream(file);ByteArrayOutputStream bos=new ByteArrayOutputStream();) {
byte[] b=new byte[1024*2];
int n;
while((n=fis.read(b))!=-1){
bos.write(b, 0, n);
}
return bos.toByteArray();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader mcl=new MyClassLoader();
//mcl.setPath("E:\\UniWorkspace\\JVM\\bin\\com\\jvm\\classloaderpratice\\");
mcl.setPath("D:/test/");
mcl.setFileName("Animail.class");
Class c=mcl.findClass("Animail");//参数没有用,因为里面没有用
Object obj=c.newInstance();
System.out.println(obj.getClass().toString());
System.out.println(obj.getClass().getClassLoader());
}
}
运行结果:
D:/test/Animail.class
exits
class com.jvm.myclassloader.Animail
com.jvm.myclassloader.MyClassLoader@37f2ae62
我这里是先在myeclipse中写好文件Animail.class放到“D:/test/”目录下,然后执行的结果,如果在findClass()函数中defineClass(null, b, 0, b.length);第一个参数设置为null,否则会报错,因为在myeclipse中写的Animail.class文件是包含包名的,放到“D:/test/”下,这样目录就不对了,会一直提示name不对,它是根据文件中包名定位。从这里可以看出,加载的类已经加载了,并且类加载器就是自定义的类加载器。
如果想使用name那就需要改一下:
//包名去掉
public class Animail {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
//去掉包名
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class MyClassLoader extends ClassLoader{
private String path;
private String fileName;
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public MyClassLoader() {
}
public MyClassLoader(ClassLoader parent) {
super(parent);
// TODO Auto-generated constructor stub
}
public Class<?> findClass(String name) throws ClassNotFoundException {
// TODO Auto-generated method stub
byte [] b=this.getClassBytes();
Class<?> clazz=this.defineClass(name, b, 0, b.length); /使用name
return clazz;
}
public byte[] getClassBytes() {
if(path.equals("")||fileName.equals("")){
return null;
}
System.out.println(path+fileName);
File file=new File(path+fileName);
if( file.exists()){
System.out.println("exits");
}
try(FileInputStream fis = new FileInputStream(file);ByteArrayOutputStream bos=new ByteArrayOutputStream();) {
byte[] b=new byte[1024*2];
int n;
while((n=fis.read(b))!=-1){
bos.write(b, 0, n);
}
return bos.toByteArray();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
MyClassLoader mcl=new MyClassLoader();
mcl.setPath("D:/");
mcl.setFileName("Animail.class");
Class c=mcl.findClass("Animail");
Object obj=c.newInstance();
System.out.println("类名:"+obj.getClass().toString());
System.out.println("类的加载器:"+obj.getClass().getClassLoader());
}
}
运行结果:
这里需要说明一下使用命令窗口来执行:
首先使用javac Animail.java命令行来编译在目录“D:\”下java文件,然后进入javatest目录下编译javac MyClassLoader.java文件,然后在该目录下执行java MyClassLoader 就会输出如上结果。这里的findClass(name)中的name属性就使用到了。
MyClassLoader mcl=new MyClassLoader();
mcl.setPath("E:\\UniWorkspace\\JVM\\bin\\com\\jvm\\myclassloader\\");
mcl.setFileName("Animail.class");
Class c=mcl.findClass("com.jvm.myclassloader.Animail");
Object obj=c.newInstance();
System.out.println(obj.getClass().toString());
System.out.println(obj.getClass().getClassLoader());
如果是在myEclipse中写且包名已经写了,则就这样写name:“com.jvm.myclassloader.Animail” 是可以运行的,这里name报错主要是因为路径问题,这里的name、包名是相对于工程根目录,而在别的路径下的文件则不能这样写了。
mcl.setPath("D:/test/");
mcl.setFileName("Animail.class");
Class c=mcl.findClass("Animail");
Object obj=c.newInstance();
System.out.println(obj.getClass().toString());
System.out.println(obj.getClass().getClassLoader());
如果把Animail.java文件放在src下default包中(这里是没有包名的),编译后,然后放在D:/test/下也是可以的,从这里可以看出findClass(name)中的name是Animail.java开头的报名+文件名 比如:com.jvm.myclassloader.Animail,由于这里的Animail.java文件中开头没有包名,所以就直接是类名Animail。
比如:
Animail文件中加入包名:package com.jvm.myclassloader;
虽然把Animail.class文件放在D:/test2/目录下 和位置无关
则name就应该是如下写
MyClassLoader mcl=new MyClassLoader();
mcl.setPath("D:/test2/");
mcl.setFileName("Animail.class");
Class c=mcl.findClass("com.jvm.myclassloader.Animail");
Object obj=c.newInstance();
System.out.println(obj.getClass().toString());
System.out.println(obj.getClass().getClassLoader());