java 动态加载修改类 java 动态更新 class
[b] Java 动态重新加载 Class [/b]
项目中使用到了动态重新加载 Class 的机制,作用是让一些代码上线之前可以在线上环境测试一下,当然,这是非常不好的测试机制,我刚来的时候也为这种机制感到惊讶 — 怎么可以在线上环境运行测试代码!后来经过了解,这么做的原因有以下两个:
[list]
[*] 有些代码没有办法在本地进行测试,本地没有线上的环境
[*] 我们弱到连测试机都没有(这是重点)
[/list]
既然我们连测试机都没有,那么我就觉得我们的项目其实也没有想象中的重要,这么测就这么测吧~~
之前对 ClassLoader 没啥概念,google 到一篇文章,翻译了一下并且做了一些补充,加深记忆
原文地址:
[quote]http://tutorials.jenkov.com/java-reflection/dynamic-class-loading-reloading.html#classloader[/quote]
---------------------------------------------------
[b]ClassLoader[/b]
顾名思义,ClassLoader 就是用来 Load Class 的,当一个 Class 被加载的时候,这个 Class 所引用到的所有 Class 也会被加载,而且这种加载是递归的,也就是说,如果 A 引用到 B,B 引用到 C,那么当 A 被加载的时候,B 也会被加载,而 B 被加载的时候,C 也会加载。如此递归直到所有需要的 Class 都加载好。
常见的 ClassLoader:
[quote]* Bootstrap class loader:虚拟机运行时必须要用到的类的加载器,比如 java.*。它通常是在虚拟机种用本地代码(如 C)实现,在系统中用 null 表示。
* Extension class loader:负责加载 ext 目录下的 Class。
* Application class loader:负责加载 CLASSPATH 上的类。[/quote]
[b] ClassLoader 的代理层次关系 [/b]
ClassLoader 是以层次关系组织起来的,当你创建一个标准的 Java ClassLoader 的时候,你必须提供一个父 ClassLoader。当一个 ClassLoader 需要加载一个 Class 的时候,它首先会让父 ClassLoader 去加载这个 Class,如果父 ClassLoader 不能加载这个 Class,那么当前的 ClassLoader 才会自己去加载。
ClassLoader 加载 Class 的步骤:
[list]
[*] 检查这个 Class 是否已经被加载过了
[*] 如果没有被加载过,那么让父 ClassLoader 尝试去加载
[*] 如果父 ClassLoader 无法加载,那么尝试使用当前 ClassLoader 加载
[/list]
从 ClassLoader 加载 Class 的步骤可以得知,如果你需要动态重新加载一个 Class,那么你的 ClassLoader 必须跟上述标准流程有所区别,需要动态加载的 Class 不能交给父 ClassLoader,否则你自己的 ClassLoader 将没有机会去加载这个 Class(因为正常情况下父 ClassLoader 总是能加载到你所请求的 Class)。
所以,如果你需要 ClassLoader 重新加载一个 Class,重写 findClass 方法是起不到效果的,因为 findClass 在父 ClassLoader 加载失败之后才会执行
必须重写 loadClass 方法才能达到效果。
[b] 动态重新加载 Class [/b]
Java 内置的 ClassLoader 总会在加载一个 Class 之前检查这个 Class 是否已经被加载过,已经被加载过的 Class 不会加载第二次。因此要想重新加载 Class,我们需要实现自己的 ClassLoader。
另外一个问题是,每个被加载的 Class 都需要被链接 (link),这是通过执行 ClassLoader.resolve () 来实现的,这个方法是 final 的,因此无法重写。Resove () 方法不允许一个 ClassLoader 实例 link 一个 Class 两次,因此,当你需要重新加载一个 Class 的时候,你需要重新 New 一个你自己的 ClassLoader 实例。
刚才说到一个 Class 不能被一个 ClassLoader 实例加载两次,但是可以被不同的 ClassLoader 实例加载,这会带来新的问题:
这段代码会导致一个 ClassCastException,因为在一个 Java 应用中,Class 是根据它的全名(包名 + 类名)和加载它的 ClassLoader 来唯一标识的。在上面的代码中 object 对象对应的 Class 和 newInstance 返回的实例对应的 Class 是有区别的:
[table]
| | 全名 | ClassLoader 实例 |
|Object 对象的 Class|com.jenkov.MyObject |AppClassLoader 实例 |
|newInstance 返回对象的 Class|com.jenkov.MyObject | 自定义 ClassLoader 实例 |
[/table]
解决的办法是使用接口或者父类,只重新加载实现类或者子类即可。
在自己实现的 ClassLoader 中,当需要加载 MyObjectInterface 或者 MyObjectSuperclass 的时候,要代理给父 ClassLoader 去加载。
实例代码就不贴了,可以去原作者网站上去看,动态重新加载 Class 可以做成当 Class 文件有修改的时候就重新加载 (比如根据文件大小 + 修改时间或者算个文件 md5 值)。
三、自定义 ClassLoader,自定义 ClassLoader 需要继承 java.lang.ClassLoader 或者继承 URLClassLoader
放两个类型的具体实现代码:
1. 继承自 ClassLoader
public class NetworkClassLoader extends ClassLoader { private String rootUrl; public NetworkClassLoader(String rootUrl) { this.rootUrl = rootUrl; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { Class clazz = null;//this.findLoadedClass(name); // 父类已加载 //if (clazz == null) { //检查该类是否已被加载过 byte[] classData = getClassData(name); //根据类的二进制名称,获得该class文件的字节码数组 if (classData == null) { throw new ClassNotFoundException(); } clazz = defineClass(name, classData, 0, classData.length); //将class的字节码数组转换成Class类的实例 //} return clazz; } private byte[] getClassData(String name) { InputStream is = null; try { String path = classNameToPath(name); URL url = new URL(path); byte[] buff = new byte[1024*4]; int len = -1; is = url.openStream(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); while((len = is.read(buff)) != -1) { baos.write(buff,0,len); } return baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { if (is != null) { try { is.close(); } catch(IOException e) { e.printStackTrace(); } } } return null; } private String classNameToPath(String name) { return rootUrl + "/" + name.replace(".", "/") + ".class"; } }
2. 继承自 URLClassLoader
ublic class SimpleURLClassLoader extends URLClassLoader { //工程class类所在的路径 public static String projectClassPath = "E:/IDE/work_place/ZJob-Note/bin/"; //所有的测试的类都在同一个包下 public static String packagePath = "testjvm/testclassloader/"; public SimpleURLClassLoader() { //设置ClassLoader加载的路径 super(getMyURLs()); } private static URL[] getMyURLs(){ URL url = null; try { url = new File(projectClassPath).toURI().toURL(); } catch (MalformedURLException e) { e.printStackTrace(); } return new URL[] { url }; } public Class load(String name) throws Exception{ return loadClass(name); } public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name,false); } /** * 重写loadClass,不采用双亲委托机制("java."开头的类还是会由系统默认ClassLoader加载) */ @Override public Class<?> loadClass(String name,boolean resolve) throws ClassNotFoundException { Class clazz = null; //查看HotSwapURLClassLoader实例缓存下,是否已经加载过class clazz = findLoadedClass(name); if (clazz != null ) { if (resolve) { resolveClass(clazz); } return (clazz); } //如果类的包名为"java."开始,则有系统默认加载器AppClassLoader加载 if(name.startsWith("java.")) { try { //得到系统默认的加载cl,即AppClassLoader ClassLoader system = ClassLoader.getSystemClassLoader(); clazz = system.loadClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return (clazz); } } catch (ClassNotFoundException e) { // Ignore } } return customLoad(name,this); } /** * 自定义加载 * @param name * @param cl * @return * @throws ClassNotFoundException */ public Class customLoad(String name,ClassLoader cl) throws ClassNotFoundException { return customLoad(name, false,cl); } /** * 自定义加载 * @param name * @param resolve * @return * @throws ClassNotFoundException */ public Class customLoad(String name, boolean resolve,ClassLoader cl) throws ClassNotFoundException { //findClass()调用的是URLClassLoader里面重载了ClassLoader的findClass()方法 Class clazz = ((SimpleURLClassLoader)cl).findClass(name); if (resolve) ((SimpleURLClassLoader)cl).resolveClass(clazz); return clazz; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { return super.findClass(name); } }
四、ClassLoader 卸载 Class
JVM 中的 Class 只有满足以下三个条件,才能被 GC 回收,也就是该 Class 被卸载(unload):
- 该类所有的实例都已经被 GC。
- 加载该类的 ClassLoader 实例已经被 GC。
- 该类的 java.lang.Class 对象没有在任何地方被引用。
GC 的时机我们是不可控的,那么同样的我们对于 Class 的卸载也是不可控的。
package testjvm.testclassloader; public class TestClassUnLoad { public static void main(String[] args) throws Exception { SimpleURLClassLoader loader = new SimpleURLClassLoader(); // 用自定义的加载器加载A Class clazzA = loader.load("testjvm.testclassloader.A"); Object a = clazzA.newInstance(); // 清除相关引用 a = null; //清除该类的实例 clazzA = null; //清除该class对象的引用 loader = null; //清楚该类的ClassLoader引用 // 执行一次gc垃圾回收 System.gc(); System.out.println("GC over"); } }
参考文档:
http://blog.csdn.net/xyang81/article/details/7292380
https://my.oschina.net/xianggao/blog/367822
四、定义自已的 ClassLoader
既然 JVM 已经提供了默认的类加载器,为什么还要定义自已的类加载器呢?
因为 Java 中提供的默认 ClassLoader,只加载指定目录下的 jar 和 class,如果我们想加载其它位置的类或 jar 时,比如:我要加载网络上的一个 class 文件,通过动态加载到内存之后,要调用这个类中的方法实现我的业务逻辑。在这样的情况下,默认的 ClassLoader 就不能满足我们的需求了,所以需要定义自己的 ClassLoader。
定义自已的类加载器分为两步:
1、继承 java.lang.ClassLoader
2、重写父类的 findClass 方法
读者可能在这里有疑问,父类有那么多方法,为什么偏偏只重写 findClass 方法?
因为 JDK 已经在 loadClass 方法中帮我们实现了 ClassLoader 搜索类的算法,当在 loadClass 方法中搜索不到类时,loadClass 方法就会调用 findClass 方法来搜索类,所以我们只需重写该方法即可。如没有特殊的要求,一般不建议重写 loadClass 搜索类的算法。下图是 API 中 ClassLoader 的 loadClass 方法:
示例:自定义一个 NetworkClassLoader,用于加载网络上的 class 文件
-
package classloader;
-
-
import java.io.ByteArrayOutputStream;
-
import java.io.InputStream;
-
import java.net.URL;
-
-
/**
-
* 加载网络class的ClassLoader
-
*/
-
public class NetworkClassLoader extends ClassLoader {
-
-
private String rootUrl;
-
-
public NetworkClassLoader(String rootUrl) {
-
this.rootUrl = rootUrl;
-
}
-
-
-
protected Class<?> findClass(String name) throws ClassNotFoundException {
-
Class clazz = null;//this.findLoadedClass(name); // 父类已加载
-
//if (clazz == null) { //检查该类是否已被加载过
-
byte[] classData = getClassData(name); //根据类的二进制名称,获得该class文件的字节码数组
-
if (classData == null) {
-
throw new ClassNotFoundException();
-
}
-
clazz = defineClass(name, classData, 0, classData.length); //将class的字节码数组转换成Class类的实例
-
//}
-
return clazz;
-
}
-
-
private byte[] getClassData(String name) {
-
InputStream is = null;
-
try {
-
String path = classNameToPath(name);
-
URL url = new URL(path);
-
byte[] buff = new byte[1024*4];
-
int len = -1;
-
is = url.openStream();
-
ByteArrayOutputStream baos = new ByteArrayOutputStream();
-
while((len = is.read(buff)) != -1) {
-
baos.write(buff,0,len);
-
}
-
return baos.toByteArray();
-
} catch (Exception e) {
-
e.printStackTrace();
-
} finally {
-
if (is != null) {
-
try {
-
is.close();
-
} catch(IOException e) {
-
e.printStackTrace();
-
}
-
}
-
}
-
return null;
-
}
-
-
private String classNameToPath(String name) {
-
return rootUrl + "/" + name.replace(".", "/") + ".class";
-
}
-
-
}
测试类:
-
package classloader;
-
-
public class ClassLoaderTest {
-
-
public static void main(String[] args) {
-
try {
-
/*ClassLoader loader = ClassLoaderTest.class.getClassLoader(); //获得ClassLoaderTest这个类的类加载器
-
while(loader != null) {
-
System.out.println(loader);
-
loader = loader.getParent(); //获得父加载器的引用
-
}
-
System.out.println(loader);*/
-
-
-
String rootUrl = "http://localhost:8080/httpweb/classes";
-
NetworkClassLoader networkClassLoader = new NetworkClassLoader(rootUrl);
-
String classname = "org.classloader.simple.NetClassLoaderTest";
-
Class clazz = networkClassLoader.loadClass(classname);
-
System.out.println(clazz.getClassLoader());
-
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}
-
-
}
打印结果:
下图是我机器上 web 服务器的目录结构:
目前常用 web 服务器中都定义了自己的类加载器,用于加载 web 应用指定目录下的类库(jar 或 class),如:Weblogic、Jboss、tomcat 等,下面我以 Tomcat 为例,展示该 web 容器都定义了哪些个类加载器:
1、新建一个 web 工程 httpweb
2、新建一个 ClassLoaderServletTest,用于打印 web 容器中的 ClassLoader 层次结构
-
import java.io.IOException;
-
import java.io.PrintWriter;
-
-
import javax.servlet.ServletException;
-
import javax.servlet.http.HttpServlet;
-
import javax.servlet.http.HttpServletRequest;
-
import javax.servlet.http.HttpServletResponse;
-
-
public class ClassLoaderServletTest extends HttpServlet {
-
-
public void doGet(HttpServletRequest request, HttpServletResponse response)
-
throws ServletException, IOException {
-
-
response.setContentType("text/html");
-
PrintWriter out = response.getWriter();
-
ClassLoader loader = this.getClass().getClassLoader();
-
while(loader != null) {
-
out.write(loader.getClass().getName()+"<br/>");
-
loader = loader.getParent();
-
}
-
out.write(String.valueOf(loader));
-
out.flush();
-
out.close();
-
}
-
-
public void doPost(HttpServletRequest request, HttpServletResponse response)
-
throws ServletException, IOException {
-
this.doGet(request, response);
-
}
-
-
}
3、配置 Servlet,并启动服务
-
-
<web-app version="2.4"
-
xmlns="http://java.sun.com/xml/ns/j2ee"
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
-
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
-
<servlet>
-
<servlet-name>ClassLoaderServletTest</servlet-name>
-
<servlet-class>ClassLoaderServletTest</servlet-class>
-
</servlet>
-
-
<servlet-mapping>
-
<servlet-name>ClassLoaderServletTest</servlet-name>
-
<url-pattern>/servlet/ClassLoaderServletTest</url-pattern>
-
</servlet-mapping>
-
<welcome-file-list>
-
<welcome-file>index.jsp</welcome-file>
-
</welcome-file-list>
-
</web-app>
4、访问 Servlet,获得显示结果