这个问题是在做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进程的。对于这一点在使用时必须要多注意。