论坛首页 Java企业应用论坛

自定义的类装载器-从DB装载class(附上对类装载器的分析)

浏览 5223 次
精华帖 (0) :: 良好帖 (2) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2011-11-25   最后修改:2011-11-30
代码才是最实在的,先从代码开始,然后再一步一步分析:
第一步:写个类用来装载
public class MyClassLoaderTest implements MyClassLoaderTestInterface {

    public MyClassLoaderTest() {
        System.out.println("MyClassLoaderTest构造函数被调用了...");
    }
    
    public void sayHello(String name) {
        System.out.println("Hello " + name + " 我是sayHello函数,我被调用了...");
    }
    
    public class InnerClassTest {
        public InnerClassTest() {}
        public void print() {
            System.out.println("内部类");
        }
    }
}

package com.jvm.one.loadclassfromdb;

public interface MyClassLoaderTestInterface {
    void sayHello(String name);
}


第二步:把编译后的class文件插入DB(MySql)
1.单独写个连接DB的类
package com.jvm.one.loadclassfromdb;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class ConnectDB {
    
    /**
     * 驱动程序名
     */
    private static final String driver = "com.mysql.jdbc.Driver";
    
    /**
     * URL指向要访问的数据库名
     */
    private static final String url = "jdbc:mysql://localhost:3306/classloader_test?useUnicode=true&characterEncoding=UTF-8";
    
    /**
     * 用户名
     */
    private static final String user = "root";
    
    /**
     * 密码
     */
    private static final String password = "141421";

    public static Connection getConnnection() {
        
        Connection conn = null;
        try {
            Class.forName(driver);
            //连接数据库
            conn = DriverManager.getConnection(url, user, password);
            if (!conn.isClosed())
                System.out.println("数据库连接成功...");
            
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            try {
                if (null != conn) conn.close();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
        return conn;
    }
    
    public static void closeConnection(Connection conn) {
        try {
            if (null != conn) {
                conn.close();
                System.out.println("数据库连接关闭...");
            } else {
                System.out.println("数据库连接不存在...");
            }
        } catch (SQLException e1) {
            e1.printStackTrace();
        }
    }
}

这里插一句,由于Class.forName函数也跟ClassLoader有关,这里有点问题也解释下
平常我们都习惯使用Class.forName(driver)这种方式装载JDBC驱动,但是Class.forName只是返回Class对象,并没有返回driver的实例,也就是说driver并没有被实例化,为什么不需要实例化呢,很多人有这样的困惑,所以他们这么写代码:
Class.forName(driver).newInstance()

其实这样写是没有必要的,代码是最真实的,我们看看MySql的jdbc驱动源码片段:
代码来自com.mysql.jdbc.Driver
	static {
		try {
			java.sql.DriverManager.registerDriver(new Driver());
		} catch (SQLException E) {
			throw new RuntimeException("Can't register driver!");
		}
	}

下面代码来自java.lang.Class
public static Class<?> forName(String className) 
                throws ClassNotFoundException {
        return forName0(className, true, ClassLoader.getCallerClassLoader());
    }

    private static native Class forName0(String name, boolean initialize,
					    ClassLoader loader)
	throws ClassNotFoundException;

boolean initialize参数为true的时候,在装载类的同时会初始化(注意不是调用构造函数),类被初始化时类中的静态语句块会被执行,所以Class.forName(driver);执行的时候已经有一个驱动的实例被注册到DriverManager中了,好了,这个问题告一段落,接着贴代码

2.建表
CREATE TABLE `classfile` (
  `classname` varchar(200) NOT NULL,
  `classcode` blob NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=gbk;

3.将class文件的内容插入DB
package com.jvm.one.loadclassfromdb;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

public class InsertClassToDB {

    /**
     * 将class文件内容存入DB
     * @param args
     * @return void
     */
    public static void main(String[] args) {

        FileInputStream fis = null;
        PreparedStatement ps = null;
        Connection conn = ConnectDB.getConnnection();;
        try {
            //编译好的class文件
            fis = new FileInputStream(
                    "D:\\Workspace\\MyEclipse\\workspace\\jvm-study\\bin\\com\\jvm\\one\\loadclassfromdb\\MyClassLoaderTest.class");
            //类的全限定名称
            String className = "com.jvm.one.loadclassfromdb.MyClassLoaderTest";
            String sql = "insert into classfile(classname, classcode) values (?, ?)";
            ps = conn.prepareStatement(sql);
            ps.setString(1, className);
            ps.setBinaryStream(2, fis, fis.available());
            ps.executeUpdate();
            //处理内部类
            fis = new FileInputStream(
            "D:\\Workspace\\MyEclipse\\workspace\\jvm-study\\bin\\com\\jvm\\one\\loadclassfromdb\\MyClassLoaderTest$InnerClassTest.class");
            //类的全限定名称
            className = "com.jvm.one.loadclassfromdb.MyClassLoaderTest$InnerClassTest";
            sql = "insert into classfile(classname, classcode) values (?, ?)";
            ps = conn.prepareStatement(sql);
            ps.setString(1, className);
            ps.setBinaryStream(2, fis, fis.available());
            ps.executeUpdate();
    
            System.out.println("插入二进制文件到DB成功");
            ps.clearParameters();
            ps.close();
            
            fis.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            try {
                if (null != fis) fis.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } catch (SQLException e) {
            try {
                if (null != ps) ps.close();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        } finally {
            ConnectDB.closeConnection(conn);
        }
    }

}

上面这段简单的代码不多说了,不是今天的重点

第三步:装载
1.负责从DB载入class文件的类
package com.jvm.one.loadclassfromdb;

import java.sql.Blob;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class GetClassFromDB {

    public static Blob getBinary(String className) {
        Blob blob = null;
        Connection conn = ConnectDB.getConnnection();
        PreparedStatement ps = null;
        try {
            ps = conn.prepareStatement("select classcode from classfile where classname=?");
            ps.setString(1, className);
            
            ResultSet rs = ps.executeQuery();
            if (rs.next())
                blob = rs.getBlob("classcode");
            else
                System.out.println("数据不存在...");
            
        } catch (SQLException e) {
            try {
                if (null != ps)  ps.close();
            } catch (SQLException e1) {
                e1.printStackTrace();
            }
            e.printStackTrace();
        }
        ConnectDB.closeConnection(conn);
        return blob;
    }
}

上面这段代码也不需要解释,不是重点

2.自定义的类装载器
package com.jvm.one.loadclassfromdb;

import java.lang.reflect.Method;
import java.sql.Blob;
import java.sql.SQLException;

/**
 * 从DB载入class文件
 * @author fengjc
 * @version 1.00 Nov 29, 2011
 */
public class LoadClassFromDB extends ClassLoader {
    
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("LoadClassFromDB.findClass is called");
        Blob blob = GetClassFromDB.getBinary(name);
        try {
            byte[] bytes;
            if (null != blob) {
                bytes = blob.getBytes(1, (int) blob.length());
                //通过调用defineClass(String name, byte[] b, int off, int len)方法来定义一个类:
                return defineClass(name, bytes, 0, bytes.length);
            }
            else {
                return null;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return null;
    }
    
    public static void main(String[] args) {
        try {
            LoadClassFromDB loadClassFromDB = new LoadClassFromDB();
            
            /**
             输出:
                com.jvm.one.loadclassfromdb.LoadClassFromDB@c17164
                sun.misc.Launcher$AppClassLoader@19821f
                sun.misc.Launcher$ExtClassLoader@addbf1
                null
                
                第一个是当前的自定义类加载器LoadClassFromDB
                第二个是AppClassLoader,它是LoadClassFromDB的双亲,主要装载ClassPath下的字节码
                第三个是ExtClassLoader,它是AppClassLoader的双亲,主要装载Jdk安装路径/jre/lib/ext下的字节码
                第四个为null,代表bootstrap启动类装载器(由C++语言编写,固化在jvm上),它是ExtClassLoader的双亲,主要装载rt.jar
             */
            ClassLoader classLoader = loadClassFromDB;
            System.out.println(classLoader);
            do {
                classLoader = classLoader.getParent();
                //如果打印出null代表为启动类装载器
                System.out.println(classLoader);
            } while (null != classLoader);
            
            //没有经过loadClass()方法中的双亲委派模型来装载类,跳出了委派链
            Class<?> outerC = loadClassFromDB.findClass("com.jvm.one.loadclassfromdb.MyClassLoaderTest");
            
            /*下面两种方法更好,这两种方法没有跳出双亲委派模式
             *但是比如现在我的bin目录下存在com.jvm.one.loadclassfromdb.MyClassLoaderTest的class文件,
             *那么AppClassLoader会装载过它,所以LoadClassFromDB再去装载的时候会委派给它的双亲AppClassLoader
             *AppClassLoader发现这个类自己已经装载了,就会直接返回给LoadClassFromDB,这样LoadClassFromDB就没有机会从DB中装载了
             *如果想从DB中装载class文件,删除bin目录下的对应class文件即可
             */
//            Class<?> outerC = loadClassFromDB.loadClass("com.jvm.one.loadclassfromdb.MyClassLoaderTest");
//            Class<?> outerC = Class.forName("com.jvm.one.loadclassfromdb.MyClassLoaderTest", false, loadClassFromDB);
            System.out.println("装载MyClassLoaderTest类的装载器为: " + outerC.getClassLoader());
            
            Object outer = null;
            Object inner = null;
            if (null != outerC) {
                /*注意这里不能用类型转换
                 *java怎么比较两个类型是否相同呢?首先第一点就是看这两个类是不是同一个类装载器装载的
                 *所以下面代码是一定会抛出java.lang.ClassCastException的
                 *MyClassLoaderTest o = (MyClassLoaderTest) c.newInstance();
                 */
                outer = outerC.newInstance();
                
                Method outerM = outerC.getMethod("sayHello", new Class<?>[] {String.class});
                outerM.invoke(outer, "童鞋们");
                
                //可以定义一个接口在客户端,这样就可以不用反射了
                MyClassLoaderTestInterface oInterface = (MyClassLoaderTestInterface) outerC.newInstance();
                oInterface.sayHello("接口");
                
                Class<?> innerC = loadClassFromDB.findClass("com.jvm.one.loadclassfromdb.MyClassLoaderTest$InnerClassTest");
                if (null != innerC) {
                    /*
                        这里为什么取得内部类的构造函数时有一个参数(就是它的外部类)呢?
                    javap -verbose MyClassLoaderTest$InnerClassTest看看javac生成的字节码指令
                        我们会看到如下内容片段:
                    public com.jvm.one.loadclassfromdb.MyClassLoaderTest$InnerClassTest(com.jvm.one.loadclassfromdb.MyClassLoaderTest);
                        这就是javac为内部类生成的构造函数,带上了一个参数是它的外部类MyClassLoaderTest(java代码中内部类的构造函数是无参的)
                     */
                    inner = innerC.getConstructor(outer.getClass()).newInstance(outer);
                    
                    Method innerM = innerC.getMethod("print");
                    innerM.invoke(inner);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


输出:
com.jvm.one.loadclassfromdb.LoadClassFromDB@c17164
sun.misc.Launcher$AppClassLoader@19821f
sun.misc.Launcher$ExtClassLoader@addbf1
null
LoadClassFromDB.findClass is called
数据库连接成功...
数据库连接关闭...
装载MyClassLoaderTest类的装载器为: com.jvm.one.loadclassfromdb.LoadClassFromDB@c17164
MyClassLoaderTest构造函数被调用了...
Hello 童鞋们 我是sayHello函数,我被调用了...
MyClassLoaderTest构造函数被调用了...
Hello 接口 我是sayHello函数,我被调用了...
LoadClassFromDB.findClass is called
数据库连接成功...
数据库连接关闭...
内部类


最后程序的输出我们可以看到 类装载器双亲委派模式的委派链为:
null(启动类装载器)->ExtClassLoader->AppClassLoader->自定义类装载器
(链子中前面的是后面的双亲(注意这不是继承))

下面介绍下类装载器的一些概念:
类装载器负责装载类。在JVM中是通过类装载器调用loadClass方法来装载类对象。
双亲委派模型就在ClassLoader.loadClass方法中体现,下面会详细解释

类装载器层次体系:




类装载器结构:
1. 引导类装载器(bootstrap class loader):它用来装载 Java 的核心库,是用本地代码来实现的,java中看不到它[null]
2. 扩展类装载器(extensions class loader):它用来装载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类装载器在此目录里面查找并装载 Java 类[ExtClassLoader]
3. 系统类装载器(system class loader):它根据 Java 应用的类路径(CLASSPATH)来装载 Java 类。一般来说,Java 应用的类都是由它来完成装载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它[AppClassLoader]

类装载器结构图:




双亲委派模型(装载class的过程):
1.检查此Class是否载入过(即在cache中是否有此Class),如果有到8
2.如果parent classloader不存在(没有parent,那parent一定是bootstrap classloader了),到4
3.请求parent classloader载入,如果成功到8,不成功到5
4.请求jvm从bootstrap classloader中载入,如果成功到8
5.寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.
6.从文件中载入Class,到8.
7.抛出ClassNotFoundException.
8.返回Class.

实现这个双亲委派模型的代码:
代码来自java.lang.ClassLoader,AppClassLoader、ExtClassLoader均继承自URLClassLoader,URLClassLoader继承自SecureClassLoader,SecureClassLoader继承自ClassLoader,所以AppClassLoader、ExtClassLoader都是ClassLoader的子类,都继承了loadClass方法
代码不必太详细解释了吧,就是实现了上面描述的8步操作
    protected synchronized Class<?> loadClass(String name, boolean resolve)
	throws ClassNotFoundException
    {
	// First, check if the class has already been loaded
    //检查此Class是否载入过
	Class c = findLoadedClass(name);
	if (c == null) {
	    try {
		if (parent != null) {
            //请求parent classloader载入
		    c = parent.loadClass(name, false);
		} else {
            //如果parent classloader不存在请求jvm从bootstrap classloader中载入
		    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.
            //一直向上委托,如果自己的双亲(层层向上)找不到该类,自己寻找Class文件(从与此classloader相关的类路径中寻找)
	        c = findClass(name);
	    }
	}
	if (resolve) {
	    resolveClass(c);
	}
	return c;
    }


下面我们来反编译下rt.jar看看sun.misc.Launcher中的部分代码,ExtClassLoader和AppClassLoader均是Launcher的内部类
  public Launcher()
  {
    ExtClassLoader localExtClassLoader;
    try
    {
      localExtClassLoader = ExtClassLoader.getExtClassLoader();
    } catch (IOException localIOException1) {
      throw new InternalError("Could not create extension class loader");
    }

    try
    {
      this.loader = AppClassLoader.getAppClassLoader(localExtClassLoader);
    } catch (IOException localIOException2) {
      throw new InternalError("Could not create application class loader");
    }
......

启动类装载器启动后的一个重要任务就是装载sun.misc.Launcher来找到ExtClassLoader和AppClassLoader两个类装载器

再来看看sun.misc.Launcher内部类ExtClassLoader中的部分代码:
    public static ExtClassLoader getExtClassLoader()
      throws IOException
    {
      File[] arrayOfFile = getExtDirs();
      try
      {
        return (ExtClassLoader)AccessController.doPrivileged(new PrivilegedExceptionAction(arrayOfFile)
        {
          public Object run() throws IOException {
            int i = this.val$dirs.length;
            for (int j = 0; j < i; j++) {
              MetaIndex.registerDirectory(this.val$dirs[j]);
            }
            return new Launcher.ExtClassLoader(this.val$dirs);
          } } );
      } catch (PrivilegedActionException localPrivilegedActionException) {
      }
      throw ((IOException)localPrivilegedActionException.getException());
    }
......
    private static File[] getExtDirs() {
      String str = System.getProperty("java.ext.dirs");
      File[] arrayOfFile;
......

我们注意到System.getProperty("java.ext.dirs"),这就是ExtClassLoader要装载class的所有路径
System.out.println(System.getProperty("java.ext.dirs"));

我电脑上的输出:
C:\jdk1.6.0_21\jre\lib\ext;C:\Windows\Sun\Java\lib\ext


继续看看sun.misc.Launcher内部类AppClassLoader中的部分代码:
  static class AppClassLoader extends URLClassLoader
  {
    public static ClassLoader getAppClassLoader(ClassLoader paramClassLoader)
      throws IOException
    {
      String str = System.getProperty("java.class.path");
      File[] arrayOfFile = str == null ? new File[0] : Launcher.access$200(str);

      return (AppClassLoader)AccessController.doPrivileged(new PrivilegedAction(str, arrayOfFile, paramClassLoader)
      {
        public Object run() {
          URL[] arrayOfURL = this.val$s == null ? new URL[0] : Launcher.access$300(this.val$path);

          return new Launcher.AppClassLoader(arrayOfURL, this.val$extcl);
        }
      });
    }

同理System.getProperty("java.class.path")的值也是AppClassLoader要装载class的所有路径
System.out.println(System.getProperty("java.class.path"));

我电脑上的输出:
D:\Workspace\MyEclipse\workspace\jvm-study\bin;E:\软件备份\开发工具\Java工具\JDBC-Driver\mysql-connector-java-3.1.13-bin.jar


注意到这个函数了吗?
public static synchronized URLClassPath getBootstrapClassPath()
  {
    if (bootstrapClassPath == null) {
      String str = (String)AccessController.doPrivileged(new GetPropertyAction("sun.boot.class.path"));
......

这个函数能取得Bootstrap ClassLoader要装载的类路径
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL url : urls)
            System.out.println(url.toExternalForm());
        
        //这么做也可以,参照getBootstrapClassPath函数代码就知道
        System.out.println(System.getProperty("sun.boot.class.path"));

我电脑上的输出:
file:/C:/jdk1.6.0_21/jre/lib/resources.jar
file:/C:/jdk1.6.0_21/jre/lib/rt.jar
file:/C:/jdk1.6.0_21/jre/lib/jsse.jar
file:/C:/jdk1.6.0_21/jre/lib/jce.jar
file:/C:/jdk1.6.0_21/jre/lib/charsets.jar
C:\jdk1.6.0_21\jre\lib\resources.jar;C:\jdk1.6.0_21\jre\lib\rt.jar;C:\jdk1.6.0_21\jre\lib\jsse.jar;C:\jdk1.6.0_21\jre\lib\jce.jar;C:\jdk1.6.0_21\jre\lib\charsets.jar
   发表时间:2011-11-25   最后修改:2011-11-29
这层删掉吧,二楼要说的已经编辑在帖子里了
0 请登录后投票
   发表时间:2011-11-29  
又仔仔细细认真了解了一番ClassLoader,重新编辑了下内容,希望能帮到一些人,如果理解有误,也欢迎大牛们指点
0 请登录后投票
   发表时间:2011-11-29  
内部类怎么处理啊
0 请登录后投票
   发表时间:2011-11-29  
java_user 写道
内部类怎么处理啊

没明白您的意思,您要问...?
0 请登录后投票
   发表时间:2011-11-29  
你是想说明自定义类加载器还是想说明原来字节码可以存在数据库?

这个弄明白以后再写文章可能会更清晰..

其实你连字节码都可以用jdk自带的类来动态编译,然后再load进db..这样的话你这篇文章就更乱了.哈哈.

不过东西说的倒都不错.挺详细的,有图有代码.
0 请登录后投票
   发表时间:2011-11-29  
budairenqin 写道
java_user 写道
内部类怎么处理啊

没明白您的意思,您要问...?

含有内部类的类编译后会生成多个class文件,这个你加载时是怎么处理的
0 请登录后投票
   发表时间:2011-11-29  
java_user 写道
budairenqin 写道
java_user 写道
内部类怎么处理啊

没明白您的意思,您要问...?

含有内部类的类编译后会生成多个class文件,这个你加载时是怎么处理的

刚刚重新编辑了下帖子,加入了内部类的装载,你看看现在符合你的要求不?可能我使用的方法比较笨,临时想的时间太紧
0 请登录后投票
   发表时间:2011-11-29  
chenjingbo 写道
你是想说明自定义类加载器还是想说明原来字节码可以存在数据库?

这个弄明白以后再写文章可能会更清晰..

其实你连字节码都可以用jdk自带的类来动态编译,然后再load进db..这样的话你这篇文章就更乱了.哈哈.

不过东西说的倒都不错.挺详细的,有图有代码.

呵呵 谢谢您的意见,其实我只是想利用这个例子说明一点ClassLoad的东西,思路是有点乱,我本身也是个菜鸟,呵呵,就是把学习到的东西和大家分享下,再一个当是给自己记一个笔记
至于为什么没选择在我的代码里编译是因为这样的话感觉代码会更多更乱,而且编译那个环节不是我想说明的东西
0 请登录后投票
   发表时间:2011-11-29  
budairenqin 写道
chenjingbo 写道
你是想说明自定义类加载器还是想说明原来字节码可以存在数据库?

这个弄明白以后再写文章可能会更清晰..

其实你连字节码都可以用jdk自带的类来动态编译,然后再load进db..这样的话你这篇文章就更乱了.哈哈.

不过东西说的倒都不错.挺详细的,有图有代码.

呵呵 谢谢您的意见,其实我只是想利用这个例子说明一点ClassLoad的东西,思路是有点乱,我本身也是个菜鸟,呵呵,就是把学习到的东西和大家分享下,再一个当是给自己记一个笔记
至于为什么没选择在我的代码里编译是因为这样的话感觉代码会更多更乱,而且编译那个环节不是我想说明的东西

动态编译说不定会出现各种各样的依赖问题呢
0 请登录后投票
论坛首页 Java企业应用版

跳转论坛:
Global site tag (gtag.js) - Google Analytics