View Javadoc

1   package com.atlassian.johnson.spring.web.context;
2   
3   import com.atlassian.johnson.Johnson;
4   import com.atlassian.johnson.JohnsonEventContainer;
5   import com.atlassian.johnson.event.Event;
6   import com.atlassian.johnson.event.EventLevel;
7   import com.atlassian.johnson.event.EventType;
8   import com.atlassian.johnson.spring.web.SpringEventType;
9   import org.slf4j.Logger;
10  import org.slf4j.LoggerFactory;
11  import org.springframework.core.Conventions;
12  import org.springframework.web.context.ContextLoaderListener;
13  import org.springframework.web.context.WebApplicationContext;
14  
15  import javax.servlet.ServletContext;
16  import javax.servlet.ServletContextEvent;
17  
18  /**
19   * Extends the standard Spring {@code ContextLoaderListener} to make it Johnson-aware. When using this class, if the
20   * Spring context fails to start an event will added to Johnson rather than propagated to the servlet container.
21   * <p/>
22   * The goal of this class is to prevent the web application from being shutdown if Spring cannot be started. By default,
23   * if the {@code WebApplicationContext} cannot be started for any reason, an exception is thrown which is propagated up
24   * to the container. When this happens, the entire web application is terminated. This precludes the use of Johnson,
25   * which requires that the web application be up so that it can serve its status pages.
26   */
27  public class JohnsonContextLoaderListener extends ContextLoaderListener
28  {
29      public static final String BYPASSED_ATTRIBUTE = Conventions.getQualifiedAttributeName(JohnsonContextLoaderListener.class, "bypassed");
30  
31      private static final Logger LOG = LoggerFactory.getLogger(JohnsonContextLoaderListener.class);
32  
33      /**
34       * Performs standard Spring {@code ContextLoaderListener} teardown and ensures any attributes added to the servlet
35       * context for this dispatcher are removed.
36       *
37       * @param event the context event
38       */
39      @Override
40      public void contextDestroyed(ServletContextEvent event)
41      {
42          try
43          {
44              super.contextDestroyed(event);
45          }
46          finally
47          {
48              event.getServletContext().removeAttribute(BYPASSED_ATTRIBUTE);
49          }
50      }
51  
52      /**
53       * Overrides the standard Spring {@code ContextLoaderListener} initialisation to make it Johnson-aware, allowing it
54       * to be automatically bypassed, when {@link com.atlassian.johnson.event.ApplicationEventCheck application checks}
55       * produce {@link EventLevel#FATAL fatal} events, or add an {@link Event} if initialisation fails.
56       * <p/>
57       * This implementation will never throw an exception. Unlike the base implementation, though, it may return
58       * {@code null} if initialisation is bypassed (due to previous fatal events or failing to initialise) or if
59       * initialisation fails.
60       *
61       * @param servletContext the servlet context
62       * @return the initialised context, or {@code null} if initialisation is bypassed or fails
63       */
64      @Override
65      public WebApplicationContext initWebApplicationContext(ServletContext servletContext)
66      {
67          String eventType = SpringEventType.getContextEventType(servletContext);
68  
69          //Search for previous FATAL errors and, if any are found, add another indicating Spring startup has been
70          //canceled. The presence of other error types in the container (warnings, errors) will not prevent this
71          //implementation from attempting to start Spring.
72          JohnsonEventContainer container = Johnson.getEventContainer(servletContext);
73          if (container.hasEvents())
74          {
75              LOG.debug("Searching Johnson for previous {} errors", EventLevel.FATAL);
76              for (Event event : container.getEvents())
77              {
78                  EventLevel level = event.getLevel();
79                  if (EventLevel.FATAL.equals(level.getLevel()))
80                  {
81                      LOG.error("Bypassing Spring ApplicationContext initialisation; a previous {} error was found: {}",
82                              level.getLevel(), event.getDesc());
83                      servletContext.setAttribute(BYPASSED_ATTRIBUTE, Boolean.TRUE);
84  
85                      if (SpringEventType.addEventOnBypass(servletContext))
86                      {
87                          String message = "The Spring WebApplicationContext will not be started due to a previous " +
88                                  level.getLevel() + " error";
89                          container.addEvent(new Event(EventType.get(eventType), message, level));
90                      }
91  
92                      //The base class actually ignores the return, so null is safe.
93                      return null;
94                  }
95              }
96          }
97          
98          WebApplicationContext context = null;
99          try
100         {
101             LOG.debug("Attempting to initialise the Spring ApplicationContext");
102             context = super.initWebApplicationContext(servletContext);
103         }
104         catch (Throwable t)
105         {
106             String message = "The Spring WebApplicationContext could not be started";
107             LOG.error(message, t);
108 
109             //The Spring ContextLoader class sets the exception that was thrown during initialisation on the servlet
110             //context under this constant. Whenever things attempt to retrieve the WebApplicationContext from the
111             //servlet context, if the property value is an exception, it is rethrown. This makes other parts of the
112             //web application, like DelegatingFilterProxies, fail to start (which, in turn, brings down the entire
113             //web application and prevents access to Johnson).
114             //Because we need the web application to be able to come up even if Spring fails, that behaviour is not
115             //desirable. So before we add the event to Johnson, the first thing we do is clear that attribute back
116             //off of the context. That way, when things attempt to retrieve the context, they'll just get a null back
117             //(which matches what happens if we bypass Spring startup completely, above)
118             servletContext.removeAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE);
119 
120             //After we remove Spring's attribute, we need to set our own. This allows other Johnson-aware constructs
121             //to know we've bypassed Spring initialisation (or, more exactly, that it failed)
122             Event event = createEvent(eventType, message, t);
123             if (event == null)
124             {
125                 //When derived classes misbehave on creating the event, fallback internally on our own implementation
126                 event = createDefaultEvent(eventType, message, t);
127             }
128             servletContext.setAttribute(BYPASSED_ATTRIBUTE, event);
129 
130             //Add the event to Johnson
131             container.addEvent(event);
132         }
133         return context;
134     }
135 
136     /**
137      * May be overridden in derived classes to allow them to override the default event type or message based on
138      * application-specific understanding of the exception that was thrown.
139      * <p/>
140      * For cases where derived classes are not able to offer a more specific event type or message, they are
141      * encouraged to fall back on the behaviour of this superclass method.
142      *
143      * @param defaultEventType the default event type to use if no more specific type is appropriate
144      * @param defaultMessage   the default message to use if no more specific message is available
145      * @param t                the exception thrown while attempting to initialise the WebApplicationContext
146      * @return the event to add to Johnson, which may not be {@code null}
147      */
148     protected Event createEvent(String defaultEventType, String defaultMessage, Throwable t)
149     {
150         return createDefaultEvent(defaultEventType, defaultMessage, t);
151     }
152 
153     /**
154      * A fail-safe event creator which cannot be overridden, allowing
155      * {@link #initWebApplicationContext(ServletContext)}} something with reliable semantics to fall back on when
156      * {@link #createEvent(String, String, Throwable)} overrides return {@code null} against its contract.
157      *
158      * @param eventType the event type to use
159      * @param message   the message to use
160      * @param t         the exception thrown while attempting to initialise the WebApplicationContext
161      * @return the event to add to Johnson, which will never be {@code null}
162      */
163     private Event createDefaultEvent(String eventType, String message, Throwable t)
164     {
165         return new Event(EventType.get(eventType), message,
166             Event.toString(t), EventLevel.get(EventLevel.FATAL));
167     }
168 }