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