這個問題是在做beetl-spring擴展的時候遇到的一個問題。擴展的思想是盡可能允許Beetl模板用到的所有可配置組件都交給Spring容器管理。
但是遇到問題是Beetl引擎在內部對模板執行進行優化的時候有使用Java對象序列化和反序列化來實現深拷貝,序列化的對象中包括了一個 可能被Spring管理的Bean:SpringBeanTagFactory,通過這個操作該對象會從Spring容器中脫管,更麻煩的該對象是通過 ApplicationContextAware注入的ApplicationContext實例無法序列化。
在這樣的基礎上,想到如下的解決方案:SpringBeanTagFactory序列化時,只序列化當前Bean的名字和Bean所在的 ApplicationContext的標識(id),反序列化時通過ApplicationContext標識找到對應的 ApplicationContext實例,再繼續通過Bean名獲取到對應的實例。
首先的第一個問題,我們應該維護好ApplicationContext id與ApplicationContext實例的關系,這在Spring MVC項目中很重要(因為除了頂層的WebApplicationContext外,每個DispatcherServlet都對應了一個子 ApplicationContext),這個維護工作可以采用Spring ApplicationEvent機制來實現,設計這樣一個類,在應用程序上下文創建時,將他添加到緩存中,在應用程序上下文關閉時,將他從緩存中刪除:
1 package org.fox.beetl.ext.spring.utils; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 import org.apache.commons.logging.Log; 7 import org.apache.commons.logging.LogFactory; 8 import org.springframework.context.ApplicationContext; 9 import org.springframework.context.ApplicationListener; 10 import org.springframework.context.event.ApplicationContextEvent; 11 import org.springframework.context.event.ContextClosedEvent; 12 import org.springframework.context.event.ContextRefreshedEvent; 13 import org.springframework.context.event.ContextStartedEvent; 14 import org.springframework.context.event.ContextStoppedEvent; 15 16 /** 17 * ApplicationContext生命周期事件監聽器 18 * 19 * @author Chen Rui 20 */ 21 public class ApplicationContextLifecycleEventListener implements ApplicationListener<ApplicationContextEvent> { 22 private Log log = LogFactory.getLog(ApplicationContextLifecycleEventListener.class); 23 24 @Override 25 public void onApplicationEvent(ApplicationContextEvent event) { 26 // 獲取應用程序上下文 27 ApplicationContext applicationContext = event.getApplicationContext(); 28 log.info(String.format("ApplicationContext生命周期事件(%s): %s", 29 applicationContext != null ? applicationContext.getId() : "null", event.getClass().getSimpleName())); 30 31 if (applicationContext != null) { 32 if ((event instanceof ContextStoppedEvent) || (event instanceof ContextClosedEvent)) { 33 // 應用程序上下文關閉或停止 34 removeApplicationContext(applicationContext.getId()); 35 } else if ((event instanceof ContextRefreshedEvent) || (event instanceof ContextStartedEvent)) { 36 // 應用程序上下文啟動或刷新 37 setApplicationContext(applicationContext); 38 } 39 } 40 } 41 42 /* ----- ----- ----- ----- ApplicationContext管理 ----- ----- ----- ----- */ 43 /** 44 * application應用程序上下文 45 */ 46 private static Map<String, ApplicationContext> applicationContextPool = new HashMap<String, ApplicationContext>(); 47 48 /** 49 * 添加ApplicationContext對象 50 * 51 * @param applicationContext 52 */ 53 private static synchronized void setApplicationContext(ApplicationContext applicationContext) { 54 applicationContextPool.put(applicationContext.getId(), applicationContext); 55 } 56 57 /** 58 * 刪除ApplicationContext對象 59 * 60 * @param id 61 */ 62 private static synchronized void removeApplicationContext(String id) { 63 applicationContextPool.remove(id); 64 } 65 66 /** 67 * 獲取ID指定的ApplicationContext 68 * 69 * @param id 70 * @return 71 */ 72 public static synchronized ApplicationContext getApplicationContext(String id) { 73 return applicationContextPool.get(id); 74 } 75 }
將這個Bean定義在Spring容器中(Spring MVC項目中,只需要將他添加到頂層的WebApplicationContext中即可,對子上下文也會生效):
<bean class="org.fox.beetl.ext.spring.utils.ApplicationContextLifecycleEventListener"/>
第二個問題,我們要干涉SpringBeanTagFactory類的序列化機制,讓他在序列化的時候只保存 ApplicationContext的標識和Bean名稱,這里我們使用了Java序列化提供的writeReplace() 和 readResolve()方法。
首先定義一個實際用於序列化的類,他持有ApplicationContext的id值和bean名字進行實際的序列化,在反序列化時,通過readResolve()方法找回實際被Spring Bean管理的Bean實例:
package org.fox.beetl.ext.spring.utils; import java.io.Serializable; import org.springframework.context.ApplicationContext; /** * Spring Bean序列化類 * * @author Chen Rui */ public class SpringBeanSerializable implements Serializable { private static final long serialVersionUID = 1L; /** * Bean所屬applicationContextId */ private String applicationContextId = null; /** * bean名稱 */ private String beanName = null; /** * @param applicationContextId * @param beanName */ public SpringBeanSerializable(String applicationContextId, String beanName) { this.applicationContextId = applicationContextId; this.beanName = beanName; } /** * 將序列化內容還原成原Spring Bean的方法 * * @return */ private Object readResolve() { ApplicationContext applicationContext = ApplicationContextLifecycleEventListener .getApplicationContext(applicationContextId); if (applicationContext == null) { throw new IllegalStateException(String.format("id為%s的ApplicationContext不存在", applicationContextId)); } return applicationContext.getBean(beanName); } }
然后改寫SpringBeanTagFactory類,讓他能知道自己的Bean名和所在ApplicationContext的id(這通過 Spring容器感知特性很容易實現,只需要實現相應接口),然后提供writeReplace()方法,在序列化時,將實際執行序列化的對象替換成上面 定義的SpringBeanSerializable對象:
package org.fox.beetl.ext.spring.tag; import org.beetl.core.Tag; import org.beetl.core.TagFactory; import org.fox.beetl.ext.spring.utils.SpringBeanSerializable; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.annotation.Required; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; /** * 使用指定名字的Spring Bean為Beetl的Tag對象 注意這個Tag Bean應該是prototype而非單例的,否則在程序中會有問題 * * @author Chen Rui */ public class SpringBeanTagFactory implements TagFactory, ApplicationContextAware, BeanNameAware { private static final long serialVersionUID = 1L; /* ----- ----- ----- ----- 屬性 ----- ----- ----- ----- */ /** * 目標Bean名 */ private String name = null; /** * Spring 應用程序上下文 */ private ApplicationContext applicationContext = null; /** * Spring Bean名稱 */ private String beanName = null; /** * 目標Bean名 * * @param name */ @Required public void setName(String name) { this.name = name; } /** * Spring 應用程序上下文 * * @param applicationContext */ @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } /** * Spring Bean名稱 * * @param beanName */ @Override public void setBeanName(String beanName) { this.beanName = beanName; } /* ----- ----- ----- ----- 其他方法 ----- ----- ----- ----- */ /** * 返回上下文中對應Tag bean對象 * * @return */ @Override public Tag createTag() { return applicationContext.getBean(name, Tag.class); } /* ----- ----- ----- ----- 序列化方法 ----- ----- ----- ----- */ /** * 生成序列化替代類 * * @return */ private Object writeReplace() { return new SpringBeanSerializable(applicationContext.getId(), beanName); } }
好了,到此大功告成。測試通過。
其實說大功告成還差得遠,上面的解決方案有幾個致命性的限制條件:
1. SpringBeanSerializable通過bean名從applicationContext中獲取bean實例,所以SpringBeanTagFactory這個Bean必須是通過名字可直接獲取的,即bean必須是公開的有明確名字的bean,不能是內部bean或匿名bean,否則反序列化時會拋出異常(NoSuchBean......);
2. 通過如此反序列化得到的SpringBeanTagFactory,並不保證和原對象有相同的狀態, 即他實際是用ApplicationContext的標識和bean的名字來獲取的,如果序列化內容傳遞到其他的JVM進程,實際反序列化時的 ApplicationContext即使標識相同,仍是兩個無關的實例。即使ApplicationContext實例相同,如果bean本身 scope不保證單例的話,也可能造成無法完全還原序列化前bean的所有可變屬性。對於這一點在使用時必須要多注意。
3.Web環境下的WebApplicationContext和DispacherServlet所持有的應用程序上下文的標識與進程無關。但在一般的Application應用程序中,ApplicationContext實例的id實際上是類名加hashcode() (如ClasspathXmlApplicationContext),在這種情況下,序列化數據不能傳遞出當前JVM進程的。對於這一點在使用時必須要多注意。