TWaver - 专注UI技术

http://twaver.servasoft.com/
posts - 171, comments - 191, trackbacks - 0, articles - 2
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

Reload Class的陷阱

Posted on 2010-08-23 13:28 TWaver 阅读(1558) 评论(0)  编辑  收藏

J2EE能在大规模项目中得到广泛应用,我觉得主要有以下几方面原因:

1、JSP、Servlet的方式能够很容易的将模块进行划分,而且模块间很容易做到互不影响,几个页面有问题不会影响其它不相关的业务模块运行,因此做到不同模块不同时间上线的方式

2、容器已经处理好了很多基础工作,一般程序员不用管socket通讯的并发、不用管如果使用线程池、不用管资源的同步死锁问题、不用管数据库连接池的实现、再加上Java的自动GC功能以及现在的硬件条件,即使不懂Java的程序员只要通过短期的项目经历,一般都能很快参与业务代码的堆积(也许说这话有点侮辱程序员,不过不得不承认现在绝大部分公司拉的项目无非就是基于某个行业业务的增、删、改、查+统计报表)

3、web容器的reload功能大大提高了开发效率,修改了页面或者业务类代码不需要重新启动JVM就能通过ClassLoader加载新的修改过得类进行测试,这点即使在上线运行时也起了很大的作用,由于现在我们的项目都是基于XMLHTTP的方式,在提交时用户界面数据保持不变,如果提交出错,打个电话过来改改后台代码,然后用户再次点击提交一切就OK了,这点相对于以前将业务代码和UI界面绑定一块,每次修改代码得重新启动JVM的CS程序是不可思议的方便

回到今天的话题,以上的第3点可是存在陷阱的地方,以下论述是基于WebLogic的测试结果:

当我们修改某个Java类保存编译后你会发现console输出控制台似乎什么都没发生,其实如果你在每个类加上 static{ System.out.println(“loading XXX.class”); },你会发现任何类的修改将会导致所有(甚至与改修改类没有任何瓜葛的类)应用类被重新加载,当然weblogic是用lazy load的方式,你调用到那个类就重新加载哪个类。这样如果系统中缓存的数据以及已经设置为某种状态的static静态属性将会被重新初始化,这样就很可能破坏某些正常的业务逻辑,出现奇怪的让人觉得不可能发生的问题:“我明明初始化了这个实例了…,我明明通过跟踪断点发现某某属性已经被我设置为…,怎么现在又成了….”。

还有一种出错的情况,对于需要定时任务的系统(例如简单的java.util.Timer或者功能强大的开源quartz)也许你会实现为类似如下方式:

 1public class Task extends TimerTask{
 2    public static Timer timer = new Timer();
 3    static{
 4        System.out.println("loading Task.class " + timer);
 5    }

 6    public void run() {
 7        System.out.println("i am doing at " + new Date());
 8    }

 9    public static void start(){
10        Task.timer.schedule(new Task(), 500010000);
11    }

12    public static void stop(){
13        Task.timer.cancel();
14    }

15}
 
16
17public class SchedulerManager
18 implements  ServletContextListener{
19    public void contextInitialized(ServletContextEvent arg0) {
20        Task.start();
21    }

22    public void contextDestroyed(ServletContextEvent arg0) {
23        Task.stop();
24    }

25}

1<listener>
2    <listener-class>test.SchedulerManager</listener-class>
3</listener>

通过上下文监听启动和关闭定时器,正常运行是没有问题的,但是如果你修改了类导致容器重新加载class那么问题出现了,例如你可以试着将 System.out.println(“i am doing at ” + new Date());
简单修改为 System.out.println(“i am new doing at ” + new Date());接着随便找个jsp运行这样的代码:

System.out.println(“=========================================”);
(new Task()).run();

你会发现在输出日志种将出现“新老类共存”的效果:

i am doing at Sat Jun 25 22:45:27 CST 2005
i am doing at Sat Jun 25 22:45:37 CST 2005
i am doing at Sat Jun 25 22:46:01 CST 2005
=========================================
i am new doing at Sat Jun 25 22:46:02 CST 2005
i am doing at Sat Jun 25 22:46:11 CST 2005
i am doing at Sat Jun 25 22:46:21 CST 2005
i am doing at Sat Jun 25 22:46:31 CST 2005

而且这时如果你想通过Task.stop();停止定时器,对不起,那个第一次被启动的Task已经是“另一个空间”的东东了,你是触及不到的了,如果你再调用Task.start()那系统将有两个Timer在跑一个跑着老的类,一个跑着新的类,你可以调用Task.stop();关闭新的Timer但是老的Timer只有redeploy或者重启整个JVM才能关闭了,终其原因是重新加载类和重新部署web工程效果是不一样的,reload class并不调用监听器的contextDestroyed和contextInitialized函数,所以这样情况下用Servlet是个不错的选择:

1public class TestServlet extends HttpServlet {
2    public void init() throws ServletException {
3        Task.start();
4    }

5    public void destroy() {
6        Task.stop();
7    }

8}


1<servlet>
2  <servlet-name>TestServlet</servlet-name>
3  <servlet-class>test.TestServlet</servlet-class>
4  <load-on-startup>1</load-on-startup>
5</servlet>

如果你修改了Task类,而且Task被调用到并且reload了,但是TestServlet还没被调用到,所以还没reload TestServlet那么也是会出现新老类并存的现象,但是一旦TestServlet被触及那么在reload之前destroy将会被调用,接着init将会被调用,这样系统将会恢复正常状态,老类不再运行,只有新类在运行,用Servlet的方式至少不会出现老类永远触及不到无非关闭的情况。

       按照这种说法Servlet似乎可以解决所有问题了,其实非也,如果你只用Servlet的“正统”用法操作不会有任何问题,但是如果你在某些情况下需要不通过Servlet的方式而是用普通类的方式直接操作Servlet的函数那么问题就来了。举个例子:我们利用Servlet在init()时启动quartz任务定制器,但是在系统运行中我们需要添加新的任务和修改任务参数,简单的方式就是全部重新加载所有任务,为此你也许回在servlet中提供public static void reload()的函数,这就是问题的根源了,例如在调用reload之前有其他的类(或者就是这个Servlet本身)被修改过了,当你调用reload函数时这个Servlet需要reload class,但是由于你并非通过“正统”的Servlet访问方式操作,而是用类的普通调用方式操作的,所以weblogic容器(其他容器我还没测试过,也许会有不一样的效果)在reload class时不再调用Servlet的public void destroy()函数,并且在reload class是也不再调用Servlet的public void init()函数,这样的话以前启动的org.quartz.Scheduler实例再也没有机会调用scheduler.shutdown(false)进行关闭了。

以下代码是个不优雅但是管用的方法:

 1<IMG style="display:none" id="SchedulerServlet" BORDER="1" WIDTH=0 HEIGHT=0/>
 2
 3<SCRIPT LANGUAGE="JavaScript">
 4<!--
 5function reload(){
 6    document.all("SchedulerServlet").src = "/servlet/SchedulerServlet";
 7    if(window.confirm("你确信要重新加载作业信息?")){
 8        ×××  通过XMLHTTP远程调用SchedulerServlet.reload() ×××
 9    }

10}

11//-->
12</SCRIPT>

这样在“非正统”的SchedulerServlet.reload()调用之间已经有个src = “/servlet/SchedulerServlet”的“正统”调用被除触发了。不过以上解决方法纯粹脱裤放屁,直接通过HTTP请求servlet的get、post函数进行处理不就得了,所以以下的方案才是正道:

 1function reload(){
 2    if(window.confirm("你确信要重新加载作业信息?")) {
 3        document.all("SchedulerManager").src = "/SchedulerManager?action=reload";
 4    }

 5}

 6
 7function shutdown(){
 8    if(window.confirm("你确信要定制所有作业?")){
 9        document.all("SchedulerManager").src = "/SchedulerManager?action=shutdown";
10    }

11}

最后让我们来研究一下reload class对容器中HTTP会话session的影响,我们在开发时修改类并不需要重新登录就能进行测试,说明reload class时session还保存着用户数据,那么这些数据是完整的没问题的吗?眼见为实先让我们做几个测试:

 1// 不可串行化的普通NoSerial类
 2public class NoSerial {
 3   static{
 4      System.out.println("loading NoSerial.class");
 5   }

 6   public NoSerial(){
 7      System.out.println("NoSerial构造中");
 8   }

 9   public int prop = 1;
10}

11
12// 实现Serializable接口的可串行化Data类,writeObject与readObject可以不实现
13// 对这两个函数的实现只是简单的调用了默认的操作,为了输出日志方便监控过程
14public class Data implements Serializable {
15   static{
16      System.out.println("loading Data.class");
17   }

18   public Data(){
19      System.out.println("Data构造中");
20   }

21   public int prop = 1;
22   private void writeObject(ObjectOutputStream stream) throws IOException {
23      System.out.println("writeObject");
24      stream.defaultWriteObject();
25   }

26    private void readObject(ObjectInputStream stream) throws IOException,  ClassNotFoundException {
27      System.out.println("readObject");
28      stream.defaultReadObject();
29   }

30}

31
32// 实现Externalizable接口的可串行化ExterData类
33public class ExterData implements Externalizable{
34   static{
35      System.out.println("loading ExterData.class");
36   }

37   public ExterData(){
38      System.out.println("ExterData构造中");
39   }

40   public int prop = 1;
41   public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
42      System.out.println("readExternal");
43   prop = in.readInt();
44   }

45   public void writeExternal(ObjectOutput out) throws IOException {
46      System.out.println("writeExternal");
47      out.writeInt(prop);
48   }

49}

50
51////////////////    在session中存入一下三个类的实例   //////////////////
52System.out.println("【##############################】");
53session.setAttribute("NoSerial"new test.NoSerial());
54Object noSerial = session.getAttribute("NoSerial");
55System.out.println((noSerial instanceof NoSerial) + " | " + noSerial);
56System.out.println("【------------------------------】");
57session.setAttribute("data"new test.Data());
58Object data = session.getAttribute("data");
59System.out.println((data instanceof Data) + " | " + data);
60System.out.println("【******************************】");
61session.setAttribute("ExterData"new test.ExterData());
62Object exterData = session.getAttribute("ExterData");
63System.out.println((exterData instanceof ExterData) + " | " + exterData);

输出日志:
【##############################】
NoSerial构造中…
true | test.NoSerial@a2dbe8
【——————————】
Data构造中…
true | test.Data@138ce2
【******************************】
ExterData构造中…
true | test.ExterData@18654c0

 1// 屏蔽掉session.setAttribute的代码,并且随便修改一下其他的类,触发一下容器的reload class //
 2System.out.println("【##############################】");
 3//session.setAttribute("NoSerial", new test.NoSerial());
 4Object noSerial = session.getAttribute("NoSerial");
 5System.out.println((noSerial instanceof NoSerial) + " | " + noSerial);
 6System.out.println("【------------------------------】");
 7//session.setAttribute("data", new test.Data());
 8Object data = session.getAttribute("data");
 9System.out.println((data instanceof Data) + " | " + data);
10System.out.println("【******************************】");
11//session.setAttribute("ExterData", new test.ExterData());
12Object exterData = session.getAttribute("ExterData");
13System.out.println((exterData instanceof ExterData) + " | " + exterData);

输出日志:
【##############################】
false | test.NoSerial@6b3836
【——————————】
writeObject…
loading Data.class…
readObject…
true | test.Data@12a14a6
【******************************】
writeExternal…
loading ExterData.class…
ExterData构造中…
readExternal…
true | test.ExterData@14ea478

通过以上的测试可知reload class对于容器session中的内容的影响:

对于非可串行话的实例NoSerial容器不进行任何操作,这样这个NoSerial实例仍然为以前的classLoad加载进来的class创建的实例,与现在重新加载的class已经没有关系了,所以进行instanceof的判断时结果为false,因此如果你想NoSerial noSerial = (NoSerial)session.getAttribute(“NoSerial”);这样获取该实例的话将会抛出类ClassCastException异常

对于实现了Serializable或者Externalizable接口的串行化类容器在重新加载类前将会先串行化(writeObject和writeExternal)保存实例内容,然后加载类loading XXX.class,最后重新初始化实例(这里有点小细节,对于Serializable没有调用不带参数的构造函数,对于Externalizable进行调用了,所以有loading ExterData.class…日志的输出)调用readObject和readExternal恢复原来的数据,因此进行instanceof的判断时结果为true,可以安全的取出来操作。所以对于需要保存再session中的类最好实现为可串行化的,这不仅有利于容器在内存受限时将会话内容保存到磁盘避免outofmemory的危险,而且在reload class后还可以正常操作session中的对象内容。

对于实现串行化有有几点需要说明一下, 如果你不设置private static final long serialVersionUID = XXXL;属性,那么当你改动类的属性、函数名(函数的实现修改不要紧,JVM计算serialVersionUID的哈西算法并不考虑函数内容,所以修改函数内容并不会影响serialVersionUID值)等信息时会造成JVM计算的serialVersionUID前后不一致,会出现java.io.InvalidClassException: zt.cims.utils.test.Data; local class incompatible: stream classdesc serialVersionUID = -8934056548762896729, local class serialVersionUID = -5363502546919843732之类的异常。如果你设置了serialVersionUID属性那么默认的串行化读取时会运用“最小化损失”原则进行属性匹配进行赋值,也就是说如果以前有i、j属性,现在添加了k属性那么i、j属性将会被填充而k不进行处理,至于函数部分你可以任意改动甚至是函数名和参数。

最后对于序列化方面TWaver Java的TWaverUtil上有几个函数挺方便的,也许你用得上

1public static byte[] toBytes(Object object)
2public static Object toObject(byte[] bytes)
3public static byte[] toByteByGZIP(Object object)
4public static Object toObjectByGZIP(byte[] bytes)

只有注册用户登录后才能发表评论。


网站导航: