From b2cf05e87a7b886bb70372e5352b2ca1d1c4a623 Mon Sep 17 00:00:00 2001
From: Musachy Barroso
Date: Sun, 12 Jul 2009 18:17:55 +0000
Subject: [PATCH] WW-3183 Add class reloading to the Spring plugin
git-svn-id: https://svn.apache.org/repos/asf/struts/struts2/trunk@793388 13f79535-47bb-0310-9956-ffa450edef68
---
plugins/spring/pom.xml | 7 +
.../spring/ClassReloadingBeanFactory.java | 96 ++++++++++
.../ClassReloadingInstantiationStrategy.java | 56 ++++++
...lassReloadingXMLWebApplicationContext.java | 181 ++++++++++++++++++
.../spring/StrutsSpringObjectFactory.java | 16 +-
.../spring/StrutsSpringObjectFactoryTest.java | 4 +-
6 files changed, 357 insertions(+), 3 deletions(-)
create mode 100644 plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingBeanFactory.java
create mode 100644 plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingInstantiationStrategy.java
create mode 100644 plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingXMLWebApplicationContext.java
diff --git a/plugins/spring/pom.xml b/plugins/spring/pom.xml
index a8d904a15..4c61088a8 100644
--- a/plugins/spring/pom.xml
+++ b/plugins/spring/pom.xml
@@ -71,6 +71,13 @@
test
+
+ org.apache.commons
+ commons-jci-fam
+ 1.0
+ true
+
+
junit
junit
diff --git a/plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingBeanFactory.java b/plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingBeanFactory.java
new file mode 100644
index 000000000..eb0fd0b6d
--- /dev/null
+++ b/plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingBeanFactory.java
@@ -0,0 +1,96 @@
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.spring;
+
+import org.springframework.beans.BeanWrapper;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.CannotLoadBeanClassException;
+import org.springframework.util.ObjectUtils;
+import org.springframework.util.ClassUtils;
+import org.springframework.core.DecoratingClassLoader;
+
+import java.lang.reflect.Constructor;
+
+
+/**
+ * Same as DefaultListableBeanFactory, but it doesn't use the constructor and class cached in RootBeanDefinition
+ */
+public class ClassReloadingBeanFactory extends DefaultListableBeanFactory {
+ @Override
+ protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, Object[] args) {
+ Class beanClass = resolveBeanClass(mbd, beanName);
+
+ if (mbd.getFactoryMethodName() != null) {
+ return instantiateUsingFactoryMethod(beanName, mbd, args);
+ }
+
+ //commented to cached constructor is not used
+ /* // Shortcut when re-creating the same bean...
+ if (mbd.resolvedConstructorOrFactoryMethod != null) {
+ if (mbd.constructorArgumentsResolved) {
+ return autowireConstructor(beanName, mbd, null, args);
+ } else {
+ return instantiateBean(beanName, mbd);
+ }
+ }*/
+
+ // Need to determine the constructor...
+ Constructor[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
+ if (ctors != null ||
+ mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR ||
+ mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
+ return autowireConstructor(beanName, mbd, ctors, args);
+ }
+
+ // No special handling: simply use no-arg constructor.
+ return instantiateBean(beanName, mbd);
+ }
+
+ protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Class[] typesToMatch) {
+ try {
+ //commented to cached class is not used
+ /* if (mbd.hasBeanClass()) {
+ return mbd.getBeanClass();
+ }*/
+ if (typesToMatch != null) {
+ ClassLoader tempClassLoader = getTempClassLoader();
+ if (tempClassLoader != null) {
+ if (tempClassLoader instanceof DecoratingClassLoader) {
+ DecoratingClassLoader dcl = (DecoratingClassLoader) tempClassLoader;
+ for (int i = 0; i < typesToMatch.length; i++) {
+ dcl.excludeClass(typesToMatch[i].getName());
+ }
+ }
+ String className = mbd.getBeanClassName();
+ return (className != null ? ClassUtils.forName(className, tempClassLoader) : null);
+ }
+ }
+ return mbd.resolveBeanClass(getBeanClassLoader());
+ }
+ catch (ClassNotFoundException ex) {
+ throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), ex);
+ }
+ catch (LinkageError err) {
+ throw new CannotLoadBeanClassException(mbd.getResourceDescription(), beanName, mbd.getBeanClassName(), err);
+ }
+ }
+}
diff --git a/plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingInstantiationStrategy.java b/plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingInstantiationStrategy.java
new file mode 100644
index 000000000..7e060572d
--- /dev/null
+++ b/plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingInstantiationStrategy.java
@@ -0,0 +1,56 @@
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.spring;
+
+import org.springframework.beans.factory.support.SimpleInstantiationStrategy;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.BeanInstantiationException;
+import org.springframework.beans.BeanUtils;
+
+import java.lang.reflect.Constructor;
+
+/**
+ * Same as SimpleInstantiationStrategy, but constructor is not cached
+ */
+public class ClassReloadingInstantiationStrategy extends SimpleInstantiationStrategy {
+ public Object instantiate(
+ RootBeanDefinition beanDefinition, String beanName, BeanFactory owner) {
+
+ // Don't override the class with CGLIB if no overrides.
+ if (beanDefinition.getMethodOverrides().isEmpty()) {
+ Class clazz = beanDefinition.getBeanClass();
+ if (clazz.isInterface()) {
+ throw new BeanInstantiationException(clazz, "Specified class is an interface");
+ }
+ try {
+ Constructor constructor = clazz.getDeclaredConstructor((Class[]) null);
+ return BeanUtils.instantiateClass(constructor, null);
+ }
+ catch (Exception ex) {
+ throw new BeanInstantiationException(clazz, "No default constructor found", ex);
+ }
+ } else {
+ // Must generate CGLIB subclass.
+ return instantiateWithMethodInjection(beanDefinition, beanName, owner);
+ }
+ }
+}
diff --git a/plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingXMLWebApplicationContext.java b/plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingXMLWebApplicationContext.java
new file mode 100644
index 000000000..978d0c3eb
--- /dev/null
+++ b/plugins/spring/src/main/java/org/apache/struts2/spring/ClassReloadingXMLWebApplicationContext.java
@@ -0,0 +1,181 @@
+/*
+ * $Id$
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.struts2.spring;
+
+import com.opensymphony.xwork2.util.classloader.FileResourceStore;
+import com.opensymphony.xwork2.util.classloader.JarResourceStore;
+import com.opensymphony.xwork2.util.classloader.ReloadingClassLoader;
+import com.opensymphony.xwork2.util.logging.Logger;
+import com.opensymphony.xwork2.util.logging.LoggerFactory;
+import org.apache.commons.jci.monitor.FilesystemAlterationListener;
+import org.apache.commons.jci.monitor.FilesystemAlterationMonitor;
+import org.apache.commons.jci.monitor.FilesystemAlterationObserver;
+import org.springframework.web.context.support.XmlWebApplicationContext;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.BeansException;
+
+import java.io.File;
+import java.util.List;
+import java.util.ArrayList;
+
+/**
+ * This class can be used instead of XmlWebApplicationContext, and it will watch jar files and directories for changes
+ * and reload then changed classes.
+ *
+ * To use this class:
+ *
+ * - Set "struts.devMode" to "true"
+ * - Set "struts.class.reloading.watchList" to a comma separated list of directories, or jar files (absolute paths)
+ * Add this to web.xml:
+ *
+ * <context-param>
+ * <param-name>contextClass</param-name>
+ * <param-value>org.apache.struts2.spring.ClassReloadingXMLWebApplicationContext</param-value>
+ * </context-param>
+ *
+ * Add Apache Commons JCI FAM to the classpath. If you are using maven, add this to pom.xml:
+ *
+ * <dependency>
+ * <groupId>org.apache.commons</groupId>
+ * <artifactId>commons-jci-fam</artifactId>
+ * <version>1.0</version>
+ * <optional>true</optional>
+ * </dependency>
+ *
+ *
+ *
+ */
+public class ClassReloadingXMLWebApplicationContext extends XmlWebApplicationContext implements FilesystemAlterationListener {
+ private static final Logger LOG = LoggerFactory.getLogger(ClassReloadingXMLWebApplicationContext.class);
+
+ private ReloadingClassLoader classLoader;
+ private FilesystemAlterationMonitor fam;
+
+ private ClassReloadingBeanFactory beanFactory;
+
+ public void setupReloading(String[] watchList) {
+ classLoader = new ReloadingClassLoader(ClassReloadingXMLWebApplicationContext.class.getClassLoader());
+ fam = new FilesystemAlterationMonitor();
+
+ //setup stores
+ for (String watch : watchList) {
+ File file = new File(watch);
+ if (watch.endsWith(".jar")) {
+ classLoader.addResourceStore(new JarResourceStore(file));
+ //register with the fam
+ fam.addListener(file, this);
+ LOG.debug("Watching [#0] for changes", file.getAbsolutePath());
+ } else {
+ //get all subdirs
+ List dirs = new ArrayList();
+ getAllPaths(file, dirs);
+
+ for (File dir : dirs) {
+ classLoader.addResourceStore(new FileResourceStore(dir));
+ //register with the fam
+ fam.addListener(dir, this);
+ LOG.debug("Watching [#0] for changes", dir.getAbsolutePath());
+ }
+ }
+ }
+ //setup the bean factory
+ beanFactory = new ClassReloadingBeanFactory();
+ beanFactory.setInstantiationStrategy(new ClassReloadingInstantiationStrategy());
+ beanFactory.setBeanClassLoader(classLoader);
+
+ //start watch thread
+ fam.start();
+ }
+
+ /**
+ * If root is a dir, find al the subdir paths
+ */
+ private void getAllPaths(File root, List dirs) {
+ dirs.add(root);
+
+ if (root.isDirectory()) {
+ File[] files = root.listFiles();
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ getAllPaths(file, dirs);
+ }
+ }
+ }
+ }
+ }
+
+ public void close() {
+ super.close();
+
+ if (fam != null) {
+ fam.removeListener(this);
+ fam.stop();
+ }
+ }
+
+ public void refresh() throws BeansException, IllegalStateException {
+ if (classLoader != null) {
+ classLoader.reload();
+ }
+
+ super.refresh();
+ }
+
+ protected DefaultListableBeanFactory createBeanFactory() {
+ return beanFactory != null ? beanFactory : super.createBeanFactory();
+ }
+
+ protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) {
+ super.prepareBeanFactory(beanFactory);
+
+ //overwrite the class loader in the bean factory
+ if (classLoader != null)
+ beanFactory.setBeanClassLoader(classLoader);
+ }
+
+ public void onDirectoryChange(File file) {
+ }
+
+ public void onDirectoryCreate(File file) {
+ }
+
+ public void onDirectoryDelete(File file) {
+ }
+
+ public void onFileChange(File file) {
+ if (classLoader != null)
+ classLoader.reload();
+ }
+
+ public void onFileCreate(File file) {
+ }
+
+ public void onFileDelete(File file) {
+ }
+
+ public void onStart(FilesystemAlterationObserver filesystemAlterationObserver) {
+ }
+
+ public void onStop(FilesystemAlterationObserver filesystemAlterationObserver) {
+ }
+}
diff --git a/plugins/spring/src/main/java/org/apache/struts2/spring/StrutsSpringObjectFactory.java b/plugins/spring/src/main/java/org/apache/struts2/spring/StrutsSpringObjectFactory.java
index 87019a220..5805338d9 100644
--- a/plugins/spring/src/main/java/org/apache/struts2/spring/StrutsSpringObjectFactory.java
+++ b/plugins/spring/src/main/java/org/apache/struts2/spring/StrutsSpringObjectFactory.java
@@ -26,6 +26,7 @@ import com.opensymphony.xwork2.spring.SpringObjectFactory;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;
import org.apache.struts2.StrutsConstants;
+import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.WebApplicationContext;
@@ -65,7 +66,9 @@ public class StrutsSpringObjectFactory extends SpringObjectFactory {
@Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE,required=false) String autoWire,
@Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_AUTOWIRE_ALWAYS_RESPECT,required=false) String alwaysAutoWire,
@Inject(value=StrutsConstants.STRUTS_OBJECTFACTORY_SPRING_USE_CLASS_CACHE,required=false) String useClassCacheStr,
- @Inject ServletContext servletContext) {
+ @Inject ServletContext servletContext,
+ @Inject(StrutsConstants.STRUTS_DEVMODE) String devMode,
+ @Inject(value = "struts.class.reloading.watchList", required = false) String watchList) {
super();
boolean useClassCache = "true".equals(useClassCacheStr);
@@ -87,6 +90,17 @@ public class StrutsSpringObjectFactory extends SpringObjectFactory {
this.setApplicationContext(appContext);
+ if ("true".equals(devMode)
+ && StringUtils.isNotBlank(watchList)
+ && appContext instanceof ClassReloadingXMLWebApplicationContext) {
+ ClassReloadingXMLWebApplicationContext reloadingContext = (ClassReloadingXMLWebApplicationContext) appContext;
+ reloadingContext.setupReloading(watchList.split(","));
+ LOG.info("Class reloading is enabled. Make sure this is not used on a production environment!", watchList);
+
+ //we need to reload the context, so our isntance of the factory is picked up
+ reloadingContext.refresh();
+ }
+
int type = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME; // default
if ("name".equals(autoWire)) {
type = AutowireCapableBeanFactory.AUTOWIRE_BY_NAME;
diff --git a/plugins/spring/src/test/java/org/apache/struts2/spring/StrutsSpringObjectFactoryTest.java b/plugins/spring/src/test/java/org/apache/struts2/spring/StrutsSpringObjectFactoryTest.java
index 1dda3be96..57daed6d7 100644
--- a/plugins/spring/src/test/java/org/apache/struts2/spring/StrutsSpringObjectFactoryTest.java
+++ b/plugins/spring/src/test/java/org/apache/struts2/spring/StrutsSpringObjectFactoryTest.java
@@ -39,7 +39,7 @@ public class StrutsSpringObjectFactoryTest extends TestCase {
public void testNoSpringContext() throws Exception {
// to cover situations where there will be logged an error
- StrutsSpringObjectFactory fac = new StrutsSpringObjectFactory(null, null, null, new MockServletContext());
+ StrutsSpringObjectFactory fac = new StrutsSpringObjectFactory(null, null, null, new MockServletContext(), null, "false");
assertEquals(AutowireCapableBeanFactory.AUTOWIRE_BY_NAME, fac.getAutowireStrategy());
}
@@ -53,7 +53,7 @@ public class StrutsSpringObjectFactoryTest extends TestCase {
ac.setServletContext(msc);
ac.setConfigLocations(new String[] {"org/apache/struts2/spring/StrutsSpringObjectFactoryTest-applicationContext.xml"});
ac.refresh();
- StrutsSpringObjectFactory fac = new StrutsSpringObjectFactory("constructor", null, null, msc);
+ StrutsSpringObjectFactory fac = new StrutsSpringObjectFactory("constructor", null, null, msc, null, "true");
assertEquals(AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR, fac.getAutowireStrategy());
}