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 }