From d026dd682126e14b1f65c46da9d9eaa4587ba09f Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Tue, 30 Sep 2014 14:13:26 -0500 Subject: [PATCH] Add AbstractHttpSessionApplicationInitializer This reduces the boilerplate code required to register Spring Session with the servlet container. Fixes gh-43 --- README.adoc | 19 +- samples/web/src/main/java/Initializer.java | 25 +- spring-session/build.gradle | 3 +- ...ractHttpSessionApplicationInitializer.java | 283 ++++++++++++++++++ 4 files changed, 294 insertions(+), 36 deletions(-) create mode 100644 spring-session/src/main/java/org/springframework/session/web/context/AbstractHttpSessionApplicationInitializer.java diff --git a/README.adoc b/README.adoc index c9ffc35..fa2dafd 100644 --- a/README.adoc +++ b/README.adoc @@ -181,24 +181,15 @@ In our example, we are connecting to the default port (6379). For more informati We next need to be sure our Servlet Container (i.e. Tomcat) is properly configured. -. First we need ensure that our `Config` class from above was loaded. In the example below we do this by extending `AbstractContextLoaderInitializer` and implementing `createRootApplicationContext`. -. Next we need to be sure the `SessionRepositoryFilter` is regsitered with the Servlet Container. We can do this by mapping a `DelegatingFilterProxy` to every request with the same name as the bean name of our `SessionRepositoryFilter`. In our instance, the bean name is the method name we used to create our `SessionRepositoryFilter`. +. First we need ensure that our `Config` class from above was loaded. In the example below we do this by extending `AbstractHttpSessionApplicationInitializer` and passing our `Config` class to the superclass. +. Next we need to be sure the `SessionRepositoryFilter` is regsitered with the Servlet Container. We can do this by mapping a `DelegatingFilterProxy` to every request with the same name as the bean name of our `SessionRepositoryFilter`. Fortunately, this is performed automatically by the `AbstractHttpSessionApplicationInitializer`. [source,java] ---- -public class Initializer extends AbstractContextLoaderInitializer { - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - super.onStartup(servletContext); - servletContext.addFilter("springSessionFilter", DelegatingFilterProxy.class) - .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*"); - } +public class Initializer extends AbstractHttpSessionApplicationInitializer { - @Override - protected WebApplicationContext createRootApplicationContext() { - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.register(Config.class); - return context; + public Initializer() { + super(Config.class); } } ---- diff --git a/samples/web/src/main/java/Initializer.java b/samples/web/src/main/java/Initializer.java index d47b471..5756cbb 100644 --- a/samples/web/src/main/java/Initializer.java +++ b/samples/web/src/main/java/Initializer.java @@ -15,31 +15,14 @@ */ -import org.springframework.web.context.AbstractContextLoaderInitializer; -import org.springframework.web.context.WebApplicationContext; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.filter.DelegatingFilterProxy; - -import javax.servlet.DispatcherType; -import javax.servlet.ServletContext; -import javax.servlet.ServletException; -import java.util.EnumSet; +import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer; /** * @author Rob Winch */ -public class Initializer extends AbstractContextLoaderInitializer { - @Override - public void onStartup(ServletContext servletContext) throws ServletException { - super.onStartup(servletContext); - servletContext.addFilter("springSessionRepositoryFilter", DelegatingFilterProxy.class) - .addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/*"); - } +public class Initializer extends AbstractHttpSessionApplicationInitializer { - @Override - protected WebApplicationContext createRootApplicationContext() { - AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); - context.register(Config.class); - return context; + public Initializer() { + super(Config.class); } } diff --git a/spring-session/build.gradle b/spring-session/build.gradle index 96dc719..2fbb3d8 100644 --- a/spring-session/build.gradle +++ b/spring-session/build.gradle @@ -11,7 +11,8 @@ configurations { dependencies { optional "org.springframework.data:spring-data-redis:$springDataRedisVersion", - "org.springframework:spring-context:$springVersion" + "org.springframework:spring-context:$springVersion", + "org.springframework:spring-web:$springVersion" provided "javax.servlet:javax.servlet-api:$servletApiVersion" integrationTestCompile "redis.clients:jedis:2.4.1", "org.apache.commons:commons-pool2:2.2", diff --git a/spring-session/src/main/java/org/springframework/session/web/context/AbstractHttpSessionApplicationInitializer.java b/spring-session/src/main/java/org/springframework/session/web/context/AbstractHttpSessionApplicationInitializer.java new file mode 100644 index 0000000..a16e056 --- /dev/null +++ b/spring-session/src/main/java/org/springframework/session/web/context/AbstractHttpSessionApplicationInitializer.java @@ -0,0 +1,283 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.session.web.context; + +import java.util.Arrays; +import java.util.EnumSet; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; +import javax.servlet.FilterRegistration.Dynamic; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; + +import org.springframework.context.ApplicationContext; +import org.springframework.core.Conventions; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.util.Assert; +import org.springframework.web.WebApplicationInitializer; +import org.springframework.web.context.AbstractContextLoaderInitializer; +import org.springframework.web.context.ContextLoaderListener; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; +import org.springframework.web.filter.DelegatingFilterProxy; + +/** + * Registers the {@link DelegatingFilterProxy} to use the + * springSessionRepositoryFilter before any other registered {@link Filter}. When + * used with {@link #AbstractHttpSessionApplicationInitializer(Class...)}, it + * will also register a {@link ContextLoaderListener}. When used with + * {@link #AbstractHttpSessionApplicationInitializer()}, this class is typically + * used in addition to a subclass of {@link AbstractContextLoaderInitializer}. + * + *

+ * By default the {@link DelegatingFilterProxy} is registered with support for + * asynchronous requests, but can be enabled by overriding {@link #isAsyncSecuritySupported()} and + * {@link #getSecurityDispatcherTypes()}. + *

+ * + *

+ * Additional configuration before and after the springSecurityFilterChain can + * be added by overriding + * {@link #afterSpringSecurityFilterChain(ServletContext)}. + *

+ * + * + *

Caveats

+ *

+ * Subclasses of AbstractDispatcherServletInitializer will register their + * filters before any other {@link Filter}. This means that you will typically + * want to ensure subclasses of AbstractDispatcherServletInitializer are invoked + * first. This can be done by ensuring the {@link Order} or {@link Ordered} of + * AbstractDispatcherServletInitializer are sooner than subclasses of + * {@link AbstractSecurityWebApplicationInitializer}. + *

+ * + * @author Rob Winch + * + */ +@Order(100) +public abstract class AbstractHttpSessionApplicationInitializer implements WebApplicationInitializer { + + private static final String SERVLET_CONTEXT_PREFIX = "org.springframework.web.servlet.FrameworkServlet.CONTEXT."; + + public static final String DEFAULT_FILTER_NAME = "springSessionRepositoryFilter"; + + private final Class[] configurationClasses; + + + /** + * Creates a new instance that assumes the Spring Session configuration is + * loaded by some other means than this class. For example, a user might + * create a {@link ContextLoaderListener} using a subclass of + * {@link AbstractContextLoaderInitializer}. + * + * @see ContextLoaderListener + */ + protected AbstractHttpSessionApplicationInitializer() { + this.configurationClasses = null; + } + + /** + * Creates a new instance that will instantiate the + * {@link ContextLoaderListener} with the specified classes. + * + * @param configurationClasses + */ + protected AbstractHttpSessionApplicationInitializer(Class... configurationClasses) { + this.configurationClasses = configurationClasses; + } + + @Override + public void onStartup(ServletContext servletContext) + throws ServletException { + beforeSessionRepositoryFilter(servletContext); + if(configurationClasses != null) { + AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext(); + rootAppContext.register(configurationClasses); + servletContext.addListener(new ContextLoaderListener(rootAppContext)); + } + insertSessionRepositoryFilter(servletContext); + afterSessionRepositoryFilter(servletContext); + } + + /** + * Registers the springSessionRepositoryFilter + * @param servletContext the {@link ServletContext} + */ + private void insertSessionRepositoryFilter(ServletContext servletContext) { + String filterName = DEFAULT_FILTER_NAME; + DelegatingFilterProxy springSessionRepositoryFilter = new DelegatingFilterProxy(filterName); + String contextAttribute = getWebApplicationContextAttribute(); + if(contextAttribute != null) { + springSessionRepositoryFilter.setContextAttribute(contextAttribute); + } + registerFilter(servletContext, true, filterName, springSessionRepositoryFilter); + } + + /** + * Inserts the provided {@link Filter}s before existing {@link Filter}s + * using default generated names, {@link #getSessionDispatcherTypes()}, and + * {@link #isAsyncSessionSupported()}. + * + * @param servletContext + * the {@link ServletContext} to use + * @param filters + * the {@link Filter}s to register + */ + protected final void insertFilters(ServletContext servletContext,Filter... filters) { + registerFilters(servletContext, true, filters); + } + + /** + * Inserts the provided {@link Filter}s after existing {@link Filter}s + * using default generated names, {@link #getSessionDispatcherTypes()}, and + * {@link #isAsyncSessionSupported()}. + * + * @param servletContext + * the {@link ServletContext} to use + * @param filters + * the {@link Filter}s to register + */ + protected final void appendFilters(ServletContext servletContext,Filter... filters) { + registerFilters(servletContext, false, filters); + } + + /** + * Registers the provided {@link Filter}s using default generated names, + * {@link #getSessionDispatcherTypes()}, and + * {@link #isAsyncSessionSupported()}. + * + * @param servletContext + * the {@link ServletContext} to use + * @param insertBeforeOtherFilters + * if true, will insert the provided {@link Filter}s before other + * {@link Filter}s. Otherwise, will insert the {@link Filter}s + * after other {@link Filter}s. + * @param filters + * the {@link Filter}s to register + */ + private void registerFilters(ServletContext servletContext, boolean insertBeforeOtherFilters, Filter... filters) { + Assert.notEmpty(filters, "filters cannot be null or empty"); + + for(Filter filter : filters) { + if(filter == null) { + throw new IllegalArgumentException("filters cannot contain null values. Got " + Arrays.asList(filters)); + } + String filterName = Conventions.getVariableName(filter); + registerFilter(servletContext, insertBeforeOtherFilters, filterName, filter); + } + } + + /** + * Registers the provided filter using the {@link #isAsyncSessionSupported()} and {@link #getSessionDispatcherTypes()}. + * + * @param servletContext + * @param insertBeforeOtherFilters should this Filter be inserted before or after other {@link Filter} + * @param filterName + * @param filter + */ + private final void registerFilter(ServletContext servletContext, boolean insertBeforeOtherFilters, String filterName, Filter filter) { + Dynamic registration = servletContext.addFilter(filterName, filter); + if(registration == null) { + throw new IllegalStateException("Duplicate Filter registration for '" + filterName +"'. Check to ensure the Filter is only configured once."); + } + registration.setAsyncSupported(isAsyncSessionSupported()); + EnumSet dispatcherTypes = getSessionDispatcherTypes(); + registration.addMappingForUrlPatterns(dispatcherTypes, !insertBeforeOtherFilters, "/*"); + } + + /** + * Returns the {@link DelegatingFilterProxy#getContextAttribute()} or null + * if the parent {@link ApplicationContext} should be used. The default + * behavior is to use the parent {@link ApplicationContext}. + * + *

+ * If {@link #getDispatcherWebApplicationContextSuffix()} is non-null the + * {@link WebApplicationContext} for the Dispatcher will be used. This means + * the child {@link ApplicationContext} is used to look up the + * springSessionRepositoryFilter bean. + *

+ * + * @return the {@link DelegatingFilterProxy#getContextAttribute()} or null + * if the parent {@link ApplicationContext} should be used + */ + private String getWebApplicationContextAttribute() { + String dispatcherServletName = getDispatcherWebApplicationContextSuffix(); + if(dispatcherServletName == null) { + return null; + } + return SERVLET_CONTEXT_PREFIX + dispatcherServletName; + } + + + + /** + * Return the to use the DispatcherServlet's + * {@link WebApplicationContext} to find the {@link DelegatingFilterProxy} + * or null to use the parent {@link ApplicationContext}. + * + *

+ * For example, if you are using AbstractDispatcherServletInitializer or + * AbstractAnnotationConfigDispatcherServletInitializer and using the + * provided Servlet name, you can return "dispatcher" from this method to + * use the DispatcherServlet's {@link WebApplicationContext}. + *

+ * + * @return the of the DispatcherServlet to use its + * {@link WebApplicationContext} or null (default) to use the parent + * {@link ApplicationContext}. + */ + protected String getDispatcherWebApplicationContextSuffix() { + return null; + } + + /** + * Invoked before the springSessionRepositoryFilter is added. + * @param servletContext the {@link ServletContext} + */ + protected void beforeSessionRepositoryFilter(ServletContext servletContext) { + + } + + /** + * Invoked after the springSessionRepositoryFilter is added. + * @param servletContext the {@link ServletContext} + */ + protected void afterSessionRepositoryFilter(ServletContext servletContext) { + + } + + /** + * Get the {@link DispatcherType} for the springSessionRepositoryFilter. + * @return + */ + protected EnumSet getSessionDispatcherTypes() { + return EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR, DispatcherType.ASYNC); + } + + /** + * Determine if the springSessionRepositoryFilter should be marked as supporting + * asynch. Default is true. + * + * @return true if springSessionRepositoryFilter should be marked as supporting + * asynch + */ + protected boolean isAsyncSessionSupported() { + return true; + } +}