Resolving servletContext.contextPath Expression in Spring 3.2

I ran into a painful thing with Spring 3.2 trying to reference the deployed servlet’s contextPath from within the web dispatch context.  I thought it would be good to write up my notes in case anybody else runs into a similar problem.

I have a case where I want the same web application to execute side-by-side on the same server.  I deploy the app into differently named contexts depending on the focus of that app.  For example, myapp-trees, myapp-bushes, and myapp-plants.  I need different configurations for each of the contexts in order for them to work properly.

So, my thought was to use the contextPath to find the correct properties file in the spring web dispatch context file.  Something like this:

<context:property-placeholder location="classpath:${servletContext.contextPath}.properties"/>

That doesn’t work for a whole slew of reasons.  First and foremost, in Spring 3.2 there is no longer a variable servletContext namespace.  Instead, it’s been replaced with servletContextInitParams.  That seems like a silly and arbitrary access constraint to me, but I haven’t heard the pros and cons around this decision.

My initial solution attempt was to extend the ServletContextEnvironment to push the servletContext into the resolution chain. 

Here’s the ServletContextEnvironment:

public class ServletContextEnvironment
  extends StandardServletEnvironment
  implements ServletContextAware
{
  /** Servlet context property source name: {@value} */
  static public final String SERVLET_CONTEXT_SOURCE_NAME = "servletContext";
  @Override
  public void setServletContext (ServletContext servletContext)
  {
    _log.debug("Setting servletContext");
    _servletContext = servletContext;
    initServletContextSource();
  }
  @Override
  protected void customizePropertySources (MutablePropertySources propertySources)
  {
    // set a stub now, replace it later when we have the servletContext
    propertySources.addLast(new StubPropertySource(SERVLET_CONTEXT_SOURCE_NAME));
    super.customizePropertySources(propertySources);
  }
  @Override
  public String getProperty (String key)
  {
    _log.debug("getProperty " + key);
    return super.getProperty(key);
  }
  protected void initServletContextSource ()
  {
    MutablePropertySources propertySources = getPropertySources();
    if (_servletContext != null &&
        propertySources.contains(SERVLET_CONTEXT_SOURCE_NAME) &&
        propertySources.get(SERVLET_CONTEXT_SOURCE_NAME) instanceof StubPropertySource)
    {
      propertySources.replace(SERVLET_CONTEXT_SOURCE_NAME,
          new ServletContextSource(SERVLET_CONTEXT_SOURCE_NAME,_servletContext));
    }
  }
  private ServletContext _servletContext;
  private Logger _log = LogManager.getLogger(getClass());
}

And the ServletContextSource that it inserts into the environment:

public class ServletContextSource
  extends PropertySource<ServletContext>
{
  public ServletContextSource (String name, ServletContext source)
  {
    super(name, source);
  }
  @Override
  public Object getProperty (String name)
  {
    if (name.startsWith(getName()))
    {
      name = name.substring(getName().length() + 1);  // + 1 for the "."
      _log.debug("Resolving " + name);
      if (name != null)
      {
        String caseName =
            (name.length() > 0 ? name.substring(0,1).toUpperCase() : "") +
            (name.length() > 1 ? name.substring(1) : "");
        String[] prefixes = { "get", "is", "has" };
        for (String prefix: prefixes)
        {
          try
          {
            Method getter = getSource().getClass().getMethod(prefix + caseName);
            return getter.invoke(getSource());
          }
          catch (NoSuchMethodException ignore)
          {}
          catch (InvocationTargetException | IllegalAccessException e)
          {
            _log.warn("Could not get property " + name + ": " + e.toString());
          }
        }
      }
    }
    return null;
  }
  private Logger _log = LogManager.getLogger(getClass());
}

However, this still doesn’t allow me to do the above because the resolution of the ${} variable in the location attribute happens before my custom ServletContextEnvironment is hooked into the placeholder resolution chain.  “Pow!” right to the face.

So, I adjusted my attack vector a bit.  I put all my properties in a single properties file, prefixing the ones for each environment with the context path for that environment.  For example:

/myapp-trees.update-schedule=0 5 23 * * MON
/myapp-shrubs.update-schedule=0 5 23 * * TUE
/myapp-plants.update-schedule=0 5 23 * * WED

Notice that slash in the front?  Yeah, that annoys the piss outta me.  The context path returned by the ServletContext object includes that, so it has to be part of the property name.  It’s entirely possible to hack something in that will remove that, of course.

So then, in my bean definitions, I referenced the properties with a nested reference to the servletContext.contextPath, like so:

<property name="cronExpression" value="${${servletContext.contextPath}.update-schedule}" />

Great!  Since I already have the code to inject the servletContext resolutions, that solves everything, right?  Not so fast there, Skippy.

It seems that Spring 3.2 has a feature by which any environment set in the PropertySourcesPlaceholderConfigurer will be overwritten with their own StandardServletEnvironment.  “Bam!” right to the face.

To solve this, I extended the PropertySourcePlaceholderConfigurer and overrode the setEnvironment method with a setter that would only set it if it hadn’t already been set.  Seems straightforward, right?  Well, there’s a problem with that: there is no getEnvironment method, and the environment member defined by the parent class is scoped as private.  So there’s no way to tell if the environment has been set.

So, I set the subclass to track it’s own environment member.  That meant I had to override every method that used the environment in the parent class so that it would use my environment instead.

On the one hand it would have been easier to just patch the parent class.  It would have been a 1 line change.  On the other hand, then I’d be stuck in the world of building my own version of the spring jars.  That’s not a fun world.

Here’s my subclassed Configurer:

public class EnvironmentPropertySourcesPlaceholderConfigurer
  extends PropertySourcesPlaceholderConfigurer
{
  @Override
  public void setEnvironment (Environment environment)
  {
    if (_environment == null)
    {
      _log.debug("Setting environment " + environment.toString());
      _environment = environment;
    }
    else
    {
      _log.debug("Ignoring environment override request " + environment.toString());
    }
  }
  @Override
  public void postProcessBeanFactory (ConfigurableListableBeanFactory beanFactory)
      throws BeansException
  {
    MutablePropertySources sources = new MutablePropertySources();
    if (_environment instanceof AbstractEnvironment)
    {
      MutablePropertySources envSources =
          ((AbstractEnvironment) _environment).getPropertySources();
      for (PropertySource envSource: envSources)
      {
        _log.debug("Wiring source " + envSource.getName());
        sources.addLast(envSource);
      }
    }
    try
    {
      PropertySource<?> localPropertySource =
          new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties());
      _log.debug("Wiring source " + localPropertySource.getName());
      sources.addLast(localPropertySource);
    }
    catch (IOException e)
    {
      throw new BeanInitializationException("Could not load properties", e);
    }
    processProperties(beanFactory, new PropertySourcesPropertyResolver(sources));
  }
  private Environment _environment;
  private Logger _log = LogManager.getLogger(getClass());
}

And the Spring bean definitions for this, which replace the <context:property-placeholder…/> tag:

  <bean class="com.xephyrus.tools.spring.EnvironmentPropertySourcesPlaceholderConfigurer">
    <property name="location" value="classpath:myapp.properties"/>
    <property name="environment">
      <bean class="com.xephyrus.buddy3.tools.spring.ServletContextEnvironment"/>
    </property>
  </bean>

Now it works.

Leave a Reply