|
From: <jue...@we...> - 2003-10-10 14:32:34
|
Everybody,
Related to the discussion about hierarchical loading, I've just =
committed support for assembling an application context from more than =
one XML file, just available via XmlWebApplicationContext for the =
moment. It allows to specify config locations with multiple paths in it, =
like in the web case:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
WEB-INF/applicationContext1.xml
WEB-INF/applicationContext2.xml
</param-value>
</context-param>
Any number of the characters ";, " between paths are ignored, to allow =
for lenient parsing. The result is a *single* root web application =
context that has been loaded from multiple XML files, each conforming to =
the spring-beans DTD. Bean references between the individual files need =
to be <ref external=3D"..."> instead of <ref bean=3D"..."> that =
references XML entities in the same file.
This allows to split a large definition of a middle tier application =
context into multiple XML files, either separating by system modules or =
by sub layers. Obviously this doesn't address the EJB/multiple web app =
case, but it should be well suited for typical large web applications.
BTW, I've also added support for multiple resource bundle basenames in =
ResourceBundleMessageSource, via the "basenames" property (in addition =
to the existing "basename" one). Those resource bundles get checked =
sequentially within a *single* MessageSource, allowing to split large =
message bundles into multiple files.
Furthermore, I've revised the notation of name aliases like <bean ... =
name=3D"myalias1,myalias2"> to match the rules above: Any number of the =
characters ";, " between bean names are ignored. This particularly =
allows BeanNameUrlHandlerMapping to interpret multiple aliases as =
multiple mappings without custom parsing, no matter if separated by a =
comma or space (the latter looks cleaner with URLs).
The drawback of the latter is that XML bean aliases are not allowed to =
contains spaces or commas anymore -- for the benefit of being able to =
specify multiple aliases. Does anyone object to that tradeoff?
-----
I've also added a new post processor type: BeanPostProcessor, in =
addition to the existing BeanFactoryPostProcessor. Note that the former =
is in the beans.factory.support package, with AbstractBeanFactory =
offering support for it -- while the latter is in context.config, as it =
allows application contexts to override property values *after* their =
bean factory (a ListableBeanFactoryImpl, to be exact) has loaded.
A BeanFactoryPostProcessor gets invoked once per bean factory loading; =
BeanPostProcessor once per bean creation. Both types of post processor =
can be defined as normal beans in an application context, getting =
automatically detected at startup, and applied before the rest of the =
beans gets created. With a plain bean factory, only BeanPostProcessor is =
applicable, but not defined as a normal bean but rather set via =
AbstractBeanFactory's setBeanPostProcessors method.
I've applied the principle that bean factories are not supposed to =
understand special beans in the bean definitions that influence the =
behavior of the factory itself -- just application contexts are, like =
with the message source, options beans, and post processors. Bean =
factories can be customized through their implementation class, though - =
e.g. setEntityResolver, setValidating, and setBeanPostProcessors on =
XmlBeanFactory.
A special implementation of BeanPostProcessor is applied under the hood =
by AbstractApplicationContext: ApplicationContextAwareProcessor is =
implictly registered with the underlying bean factory to automatically =
pass the application context to ApplicationContextAware beans. This =
allowed me to get rid of the rather ugly manual check for =
ApplicationContextAware with a cache of already managed instances to =
avoid double setting of the application context -- the current solution =
is much cleaner.
Note that a custom BeanPostProcessor definition in an application =
context can not only be used to check for marker interfaces but also to =
wrap certain bean instances with proxies, as it can return a different =
bean instance than the one that came in. Look at the respective test =
case in StaticApplicationContextTestSuite that implicitly wraps each =
bean instance with a CGLIB proxy.
Now imagine a BeanPostProcessor implementation that checks for certain =
attributes in the bean class file and applies respective interceptors -- =
like transaction attributes that trigger a CGLIB proxy and an implicit =
TransactionInterceptor! I consider this as the perfect hook for such =
implicit wrapping at bean creation time.
Juergen
-----Original Message-----
From: Colin Sampaleanu [mailto:col...@ex...]
Sent: Thursday, September 18, 2003 5:15 PM
To: spr...@li...
Subject: [Springframework-user] Hierarchical loading of Application
Contexts
I wanted to spur some discussion of best practices for hierachical=20
loading of ApplicationContexts. ApplicationContext implementations can=20
of course be created with a parent context specified. Currently, there=20
is some code support for actually managing this loading in the web ui=20
layer (where it's easy to have a setup with a root=20
WebApplicationContext, and multiple child contexts, per servlet.
Most Spring usage examples assume (unless I'm missing something) that=20
the root WebApplicationContext will contain the entire layer stack, that =
is, data-access beans and service layer beans will be defined in there.=20
This does work great when there there is only one web-app.
Now in my case, I need something a little bit different. I have a J2EE=20
application as an EAR file, which contains 3 web-apps (as WARs) at the=20
top layer. Among other layers, below the web ui layer there is a=20
services layer, and a data-access layer. Now I could get by with three=20
different, parallel, application contexts defined, one for each of the=20
web-apps, where all the bean definitions for the data-access and=20
services layer was duplicated in each application context. I do not want =
to do this however, since I am using Hibernate, and do not want to force =
the Hibernate meta-data to be read in 3 times (it's a slow process). I=20
much prefer to have a hierachical setup for the whole j2ee app, where I =
have an application context definition for the data-access layer, which=20
needs to be the parent of an application context definition for the=20
services layer, which needs to be the parent of three separate web=20
application context definitions, one in each web-app.
I have been able to achieve this as follows (and want to get feedback if =
somebody can think of a better way): I have an application context=20
defintion for my data access layer
data-access-applicationContext.xml
I have an application context definition for my services layer, which is =
a child of the data-access context, and refers to beans in it.:
core-servies-applicationContext.xml
I then have the three application contexts for the web apps, which are=20
children of the services context, and refer to beans within it.
Now what I do, is (in a somewhat lazy fashion) trigger loading (once) of =
the services and data-access contexts, when I load the first web=20
application context. The 2nd and 3rd web application context end up=20
using the previously loaded service context instance. I have an=20
interface, ContextFactory, as follows:
/**
* Defines interface for an ApplicationContext factory.
*
* @version $Revision: 1.40 $
* @author colin
*/
public interface ContextFactory {
=20
/**
* Use the ApplicationContext specified by the key parameter. The=20
context is possibly
* loaded/created as needed.
*
* @param key a value specifying which context to use
* @return the ApplicationContext instance
* @throws ApplicationContextException if there is an error loading=20
one or more contexts
*/
ApplicationContext useContext(String key) throws=20
ApplicationContextException;
=20
/**
* Indicate that the specified ApplicationContext instance is not=20
needed by a user of it,
* who has previsouly obtained it via {@link useContext}. It is an=20
error for releaseContext
* to be called a greater number of times than useContext. Calling=20
this release method may
* cause close() to be called on the specified context, if this is the =
last user of it.
*=20
* @param ac the ApplicationContext instance
* @throws ApplicationContextException
*/
void releaseContext(ApplicationContext ac) throws=20
ApplicationContextException;
}
Now I have an actual implementation, which can be used to get access to=20
a reference counted context, triggering loading as needed, and loading=20
parents of that context, if needed:
/**
* Implementation of ContextFactory.<br />
* In this implementation, the key is actually the name of a properties=20
file accessed as
* a resource, each line of which specifies an ApplicationContext to=20
load. Each line in
* the file is the parent of the subsequent line. The last context is=20
the one returned.
*=20
* @version $Revision: $
* @author colin
*/
public class ContextFactoryImpl implements ContextFactory {
=20
// --- statics
public static final Logger _log =3D=20
Logger.getLogger(ContextFactoryImpl.class);
// we map ContextInfo objects by String keys, and by Contexts
private static HashMap instancesByKey =3D new HashMap();
private static HashMap instancesByObj =3D new HashMap();
// --- methods
=20
/* (non-Javadoc)
* @see=20
com.tira.coreserv.webutil.ContextFactory#useContext(java.lang.String)
*/
public ApplicationContext useContext(String key) throws=20
ApplicationContextException {
synchronized(instancesByKey) {
ContextInfo ci =3D (ContextInfo) instancesByKey.get(key);
if (ci !=3D null) {
_log.debug("Context with key '" + key + "' requested. Returning=20
existing instances");
ci.refcount++;
return ci.context;
}
_log.debug("Context with key '" + key + "' requested. Creating new =
instance");
// this context doesn't exist, we need to try to load it
InputStream is =3D getClass().getResourceAsStream(key);
if (is =3D=3D null)
throw new ApplicationContextException("Unable to load=20
context(s). Key does not point to a valid resource: " + key);
Properties props =3D new Properties();
try {
props.load(is);
is.close();
}
catch (IOException e) {
throw new ApplicationContextException("Error reading application =
context pointer definition.", e);
}
=20
ArrayList contexts =3D new ArrayList();
for (int i =3D 0; ; ++i) {
String context =3D props.getProperty(Integer.toString(i));
if (context =3D=3D null)
break;
contexts.add(context);
}
=20
if (contexts.size() =3D=3D 0)
throw new ApplicationContextException("No application context=20
definitions specified in definition file: " + key);
ClassPathXmlApplicationContext ac;
try {
ac =3D new=20
ClassPathXmlApplicationContext((String[])contexts.toArray(new =
String[0]));
}
catch (IOException e) {
throw new ApplicationContextException("Error reading application =
context definition", e);
}
=20
ci =3D new ContextInfo();
ci.context =3D ac;
ci.key =3D key;
ci.refcount =3D 1;
instancesByKey.put(key, ci);
instancesByObj.put(ac, ci);
=20
return ac;
}
}
/**
* Releases the specified ApplicationContext instance. If there are no =
more users of this
* context, close() will be called on it. Note that close will be=20
called on its parents as
* well, recursively.
* @see=20
com.tira.coreserv.webutil.ContextFactory#ReleaseContext(org.springframewo=
rk.context.ApplicationContext)
*/
public void releaseContext(ApplicationContext ac) throws=20
ApplicationContextException {
synchronized(instancesByKey) {
ContextInfo ci =3D (ContextInfo) instancesByObj.get(ac);
if (ci !=3D null) {
ci.refcount--;
if (ci.refcount < 0)
throw new ApplicationContextException("Illegal state: context=20
released more times than acquired");
if (ci.refcount =3D=3D 0) {
_log.debug("Release requested on Context with key '" + ci.key=20
+ "'. Last reference, so closing along with parents");
instancesByObj.remove(ac);
instancesByKey.remove(ci.key);
ApplicationContext child =3D ci.context;
ApplicationContext parent;
while (child !=3D null) {
parent =3D child.getParent();
child.close();
child =3D parent;
}
}
else
_log.debug("Release requested on Context with key '" + ci.key=20
+ "'. References remain, so not closing.");
}
else
throw new ApplicationContextException("Attempted to release=20
context reference for context not known to this factory");
}
}
// we track contexts with this class
private class ContextInfo {
public ApplicationContext context;
public String key;
public int refcount =3D 0;
}
}
So my ContextFactory impl can load the services application context, and =
first its parent, the data-access context, on demand, and subsequent=20
request will just return a reference to the same context. The 'key'=20
specified to the context factory is just the name of a file specifying=20
which contexts to load:
File core-services-applicationContexts.properties:
# defines one or more hierarchical xml ApplicationContext instances=20
which are considered
# to make up the application context stack for core-services. Each line=20
is a parent of the
# subsequent line.
0=3D/data-access-applicationContext.xml
1=3D/core-services-applicationContext.xml
Now I just need to have a modified version of ContextLoader from Spring=20
which along with loading a specified WebApplicationContext instance,=20
will optioinally use the ContextFactory to load a parent context first.=20
Here's the code:
/**
* Class used to load a web application context, including possibly=20
triggering loading of a parent
* context through a ContextFactory instance
*
* @version $Revision: $
*/
public class ContextLoader {
// --- statics
/**?
* Config param for the root WebApplicationContext implementation=20
class to use.
*/
public static final String CONTEXT_CLASS_PARAM =3D "contextClass";
public static final Class DEFAULT_CONTEXT_CLASS =3D=20
XmlWebApplicationContext.class;
public static final String PARENT_CONTEXT_FACTORY_CLASS_PARAM =3D=20
"parentContextFactoryClass";
public static final String PARENT_CONTEXT_FACTORY_PARAM1_PARAM =3D=20
"parentContextFactoryParam";
=20
public static final String PARENT_CONTEXT_KEY_PARAM =3D =
"parentContextKey";
public static final Logger _log =3D =
Logger.getLogger(ContextLoader.class);
/**
* Initialize Spring's web application context for the given servlet=20
context,
* regarding the "contextClass" servlet context init parameter.
* @param servletContext current servlet context
* @return the new WebApplicationContext
*/
public static WebApplicationContext initContext(ServletContext=20
servletContext)
throws ApplicationContextException {
servletContext.log("Loading root WebApplicationContext");
String contextClass =3D=20
servletContext.getInitParameter(CONTEXT_CLASS_PARAM);
=20
String className =3D null;
try {
className =3D=20
servletContext.getInitParameter(PARENT_CONTEXT_FACTORY_CLASS_PARAM);
ContextFactory parentContextFactory =3D=20
getParentContextFactory(servletContext);
ApplicationContext parentContext =3D null;
if (parentContextFactory !=3D null) {
String parentContextKey =3D=20
servletContext.getInitParameter(PARENT_CONTEXT_KEY_PARAM);
_log.info(
"Getting parent context: using context factory class '" +=20
className + "', key '" +
parentContextKey + "'");
parentContext =3D =
parentContextFactory.useContext(parentContextKey);
}
// Now we must load the WebApplicationContext.
// It configures itself: all we need to do is construct the class=20
with the proper
// constructor, and invoke setServletContext.
Class clazz =3D (contextClass !=3D null ? =
Class.forName(contextClass)=20
: DEFAULT_CONTEXT_CLASS);
_log.info(
"Loading root WebApplicationContext: using context class '" +=20
clazz.getName() + "'");
if (!WebApplicationContext.class.isAssignableFrom(clazz)) {
throw new ApplicationContextException(
"Context class is no WebApplicationContext: " + contextClass);
}
Class[] parameterTypes;
Object[] params;
if (parentContext =3D=3D null) {
parameterTypes =3D new Class[0];
params =3D new Object[0];
}
else {
parameterTypes =3D new Class[] {ApplicationContext.class,=20
String.class};
params =3D new Object[] {parentContext, null};
}
Constructor constructor =3D clazz.getConstructor(parameterTypes);
WebApplicationContext webApplicationContext =3D=20
(WebApplicationContext) constructor.newInstance(params);
webApplicationContext.setServletContext(servletContext);
return webApplicationContext;
}
catch (ApplicationContextException ex) {
handleException("Failed to initialize application context", ex);
}
catch (BeansException ex) {
handleException("Failed to initialize beans in application=20
context", ex);
}
catch (ClassNotFoundException ex) {
handleException("Failed to load config class '" + className + "'", =
ex);
}
catch (InstantiationException ex) {
handleException(
"Failed to instantiate config class '"
+ className
+ "': does it have the proper constructor?",
ex);
}
catch (IllegalAccessException ex) {
handleException(
"Illegal access while finding or instantiating config class '"
+ className
+ "': does it have the proper constructor?",
ex);
}
catch (Throwable ex) {
handleException("Unexpected error loading context configuration", =
ex);
}
return null;
}
/**
* Log and throw an appropriate exception.
*/
private static void handleException(String msg, Throwable ex)
throws ApplicationContextException {
String thrownMsg =3D msg + ": " + ex.getMessage();
_log.error(thrownMsg, ex);
if (ex instanceof Error) {
throw (Error) ex;
}
else if (ex instanceof ApplicationContextException) {
throw (ApplicationContextException) ex;
}
else {
throw new ApplicationContextException(thrownMsg, ex);
}
}
/**
* Close Spring's web application context for the given servlet =
context.
* @param servletContext current servlet context
*/
public static void closeContext(ServletContext servletContext) {
servletContext.log("Closing root WebApplicationContext");
ApplicationContext ac =3D=20
WebApplicationContextUtils.getWebApplicationContext(servletContext);
ApplicationContext parent =3D ac.getParent();
try {
ac.close();
}
finally {
if (parent !=3D null) {
ContextFactory cf =3D null;
String className =3D=20
servletContext.getInitParameter(PARENT_CONTEXT_FACTORY_CLASS_PARAM);
// should we check this for null. for now, let's not, as it has=20
to be there if there is a parent
try {
cf =3D getParentContextFactory(servletContext);
}
catch (Exception ex) {
handleException("Unable to obtain context factory to release=20
parent context. Context factory className '" + className + "'", ex);
}
cf.releaseContext(parent);
}
}
}
/**
* Returns a ContextFactory, if any, to be used to obtain and release=20
a parent context
*/
private static ContextFactory getParentContextFactory(ServletContext=20
servletContext) throws ClassNotFoundException, SecurityException,=20
NoSuchMethodException, IllegalArgumentException, InstantiationException, =
IllegalAccessException, InvocationTargetException {
String parentContextFactoryClassName =3D=20
servletContext.getInitParameter(PARENT_CONTEXT_FACTORY_CLASS_PARAM);
String parentContextFactoryParam1 =3D=20
servletContext.getInitParameter(PARENT_CONTEXT_FACTORY_PARAM1_PARAM);
=20
String className =3D null;
Class[] parameterTypes;
Object[] params;
ContextFactory parentContextFactory =3D null;
if (parentContextFactoryClassName !=3D null) {
className =3D parentContextFactoryClassName;
Class clazz =3D Class.forName(className);
parameterTypes =3D new Class[0];
params =3D new Object[0];
if (parentContextFactoryParam1 !=3D null) {
parameterTypes =3D new Class[] {String.class};
params =3D new Object[] {parentContextFactoryParam1};
}
Constructor constructor =3D clazz.getConstructor(parameterTypes);
parentContextFactory =3D (ContextFactory)=20
constructor.newInstance(params);
}
=20
return parentContextFactory;
}
}
So this mechanism works fine. It allows me to layer application contexts =
all the way down the layer stack, not just in the web-ui layer. Can=20
anybody think of a better or simpler way to handle this need?
Is it worth adding some of this code into Spring itself? I don't think=20
I'm the only person who will need to do something like this. Admittedly, =
in the absence of the large start-up time for Hibernate, it would have=20
been acceptible to handle this via just three application contexts at=20
the web level, with duplicate definitions for the content from the=20
data-access and services layer brought in via xml includes.
Regards,
Colin
-------------------------------------------------------
This sf.net email is sponsored by:ThinkGeek
Welcome to geek heaven.
http://thinkgeek.com/sf
_______________________________________________
Springframework-user mailing list
Spr...@li...
https://lists.sourceforge.net/lists/listinfo/springframework-user
|