From dcaaf11b1e78a0cbb5e636d23a6076d83d217209 Mon Sep 17 00:00:00 2001 From: gonzalad Date: Sun, 6 Aug 2017 20:50:41 +0200 Subject: [PATCH] Spring MVC Async support Support Asynchronous Request Processing for Spring Web MVC. --- pom.xml | 12 +- .../AbstractDatabaseSwitchingDataSource.java | 7 + ...yContextCallableProcessingInterceptor.java | 75 +++++++++ .../tenancy/web/TenancyWebAsyncFilter.java | 71 ++++++++ ...textCallableProcessingInterceptorTest.java | 81 +++++++++ .../web/TenancyWebAsyncFilterTest.java | 154 ++++++++++++++++++ 6 files changed, 397 insertions(+), 3 deletions(-) create mode 100644 src/main/java/org/springframework/tenancy/web/TenancyContextCallableProcessingInterceptor.java create mode 100644 src/main/java/org/springframework/tenancy/web/TenancyWebAsyncFilter.java create mode 100644 src/test/java/org/springframework/tenancy/web/TenancyContextCallableProcessingInterceptorTest.java create mode 100644 src/test/java/org/springframework/tenancy/web/TenancyWebAsyncFilterTest.java diff --git a/pom.xml b/pom.xml index a10fa37..40f9856 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,7 @@ Spring Multi-Tenancy Framework - 3.0.5.RELEASE + 3.2.0.RELEASE @@ -128,6 +128,12 @@ 2.0.0 test + + org.mockito + mockito-core + 1.10.19 + test + http://r.tasktop.com/#projects/spring-tenancy @@ -146,8 +152,8 @@ maven-compiler-plugin 2.0.2 - 1.6 - 1.6 + 1.7 + 1.7 diff --git a/src/main/java/org/springframework/tenancy/datasource/AbstractDatabaseSwitchingDataSource.java b/src/main/java/org/springframework/tenancy/datasource/AbstractDatabaseSwitchingDataSource.java index 931e3d0..e5c61e3 100644 --- a/src/main/java/org/springframework/tenancy/datasource/AbstractDatabaseSwitchingDataSource.java +++ b/src/main/java/org/springframework/tenancy/datasource/AbstractDatabaseSwitchingDataSource.java @@ -21,7 +21,9 @@ import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; +import java.util.logging.Logger; import javax.sql.DataSource; @@ -78,6 +80,11 @@ public void setLogWriter(PrintWriter arg0) throws SQLException { wrappedDataSource.setLogWriter(arg0); } + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return wrappedDataSource.getParentLogger(); + } + @Override public void setLoginTimeout(int arg0) throws SQLException { wrappedDataSource.setLoginTimeout(arg0); diff --git a/src/main/java/org/springframework/tenancy/web/TenancyContextCallableProcessingInterceptor.java b/src/main/java/org/springframework/tenancy/web/TenancyContextCallableProcessingInterceptor.java new file mode 100644 index 0000000..6c98f77 --- /dev/null +++ b/src/main/java/org/springframework/tenancy/web/TenancyContextCallableProcessingInterceptor.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 SpringSource, a division of VMware + * + * 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. + * + * Contributors: + * Tasktop Technologies Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.tenancy.web; + +import java.util.concurrent.Callable; + +import org.springframework.tenancy.context.TenancyContext; +import org.springframework.tenancy.context.TenancyContextHolder; +import org.springframework.util.Assert; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; + +public class TenancyContextCallableProcessingInterceptor extends CallableProcessingInterceptorAdapter { + + private TenancyContext tenancyContext; + + /** + * Create a new {@link TenancyContextCallableProcessingInterceptor} that uses the + * {@link TenancyContext} from the {@link TenancyContextHolder} at the time + * {@link #beforeConcurrentHandling(NativeWebRequest, Callable)} is invoked. + */ + public TenancyContextCallableProcessingInterceptor() { + } + + /** + * Creates a new {@link TenancyContextCallableProcessingInterceptor} with the + * specified {@link TenancyContext}. + * + * @param tenancyContext the {@link TenancyContext} to set on the + * {@link org.springframework.tenancy.context.TenancyContextHolder} in {@link #preProcess(NativeWebRequest, Callable)}. + * Cannot be null. + * @throws IllegalArgumentException if tenancyContext is null. + */ + public TenancyContextCallableProcessingInterceptor(TenancyContext tenancyContext) { + Assert.notNull(tenancyContext, "tenancyContext cannot be null"); + setTenancyContext(tenancyContext); + } + + @Override + public void beforeConcurrentHandling(NativeWebRequest request, Callable task) throws Exception { + if (tenancyContext == null) { + setTenancyContext(TenancyContextHolder.getContext()); + } + } + + @Override + public void preProcess(NativeWebRequest request, Callable task) throws Exception { + TenancyContextHolder.setContext(tenancyContext); + } + + @Override + public void postProcess(NativeWebRequest request, Callable task, Object concurrentResult) throws Exception { + TenancyContextHolder.clearContext(); + } + + private void setTenancyContext(TenancyContext tenancyContext) { + this.tenancyContext = tenancyContext; + } +} diff --git a/src/main/java/org/springframework/tenancy/web/TenancyWebAsyncFilter.java b/src/main/java/org/springframework/tenancy/web/TenancyWebAsyncFilter.java new file mode 100644 index 0000000..6b05f0d --- /dev/null +++ b/src/main/java/org/springframework/tenancy/web/TenancyWebAsyncFilter.java @@ -0,0 +1,71 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 SpringSource, a division of VMware + * + * 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. + * + * Contributors: + * Tasktop Technologies Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.tenancy.web; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * This filter provides Asynchronous Request Processing for multi-tenant + * application. + * + * You need to add this filter to your application to support + * {@link org.springframework.tenancy.context.TenancyContextHolder} usage + * in Spring Web MVC Async applications. + * + * i.e. + *
+ * 
+ @Bean
+ @Conditional(TenantCondition.class)
+ public FilterRegistrationBean tenancyWebAsyncFilter() {
+     FilterRegistrationBean registration = new FilterRegistrationBean();
+     TenancyWebAsyncFilter filter = new TenancyWebAsyncFilter();
+     registration.setFilter(filter);
+     registration.setOrder(5); // whatever order
+     return registration;
+ }
+ * 
+ * 
+ */ +public class TenancyWebAsyncFilter extends OncePerRequestFilter { + + private static final Object CALLABLE_INTERCEPTOR_KEY = new Object(); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + + TenancyWebAsyncFilter tenancyProcessingInterceptor = + (TenancyWebAsyncFilter) asyncManager.getCallableInterceptor(CALLABLE_INTERCEPTOR_KEY); + if (tenancyProcessingInterceptor == null) { + asyncManager.registerCallableInterceptor(CALLABLE_INTERCEPTOR_KEY, new TenancyContextCallableProcessingInterceptor()); + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/test/java/org/springframework/tenancy/web/TenancyContextCallableProcessingInterceptorTest.java b/src/test/java/org/springframework/tenancy/web/TenancyContextCallableProcessingInterceptorTest.java new file mode 100644 index 0000000..b1ef668 --- /dev/null +++ b/src/test/java/org/springframework/tenancy/web/TenancyContextCallableProcessingInterceptorTest.java @@ -0,0 +1,81 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 SpringSource, a division of VMware + * + * 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. + * + * Contributors: + * Tasktop Technologies Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.tenancy.web; + +import java.util.concurrent.Callable; + +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.tenancy.context.TenancyContext; +import org.springframework.tenancy.context.TenancyContextHolder; +import org.springframework.web.context.request.NativeWebRequest; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class TenancyContextCallableProcessingInterceptorTest { + + @Mock + private TenancyContext tenancyContext; + + @Mock + private Callable callable; + + @Mock + private NativeWebRequest webRequest; + + @After + public void clearTenancyContext() { + TenancyContextHolder.clearContext(); + } + + @Test(expected = IllegalArgumentException.class) + public void constructorNull() { + new TenancyContextCallableProcessingInterceptor(null); + } + + @Test + public void currentTenancyContext() throws Exception { + TenancyContextCallableProcessingInterceptor interceptor = new TenancyContextCallableProcessingInterceptor(); + TenancyContextHolder.setContext(tenancyContext); + interceptor.beforeConcurrentHandling(webRequest, callable); + TenancyContextHolder.clearContext(); + + interceptor.preProcess(webRequest, callable); + assertThat(TenancyContextHolder.getContext(), is(tenancyContext)); + + interceptor.postProcess(webRequest, callable, null); + assertThat(TenancyContextHolder.getContext(), is(not(tenancyContext))); + } + + @Test + public void specificTenancyContext() throws Exception { + TenancyContextCallableProcessingInterceptor interceptor = new TenancyContextCallableProcessingInterceptor(tenancyContext); + + interceptor.preProcess(webRequest, callable); + assertThat(TenancyContextHolder.getContext(), is(tenancyContext)); + + interceptor.postProcess(webRequest, callable, null); + assertThat(TenancyContextHolder.getContext(), is(not(tenancyContext))); + } +} diff --git a/src/test/java/org/springframework/tenancy/web/TenancyWebAsyncFilterTest.java b/src/test/java/org/springframework/tenancy/web/TenancyWebAsyncFilterTest.java new file mode 100644 index 0000000..7cb7805 --- /dev/null +++ b/src/test/java/org/springframework/tenancy/web/TenancyWebAsyncFilterTest.java @@ -0,0 +1,154 @@ +/******************************************************************************* + * Copyright (c) 2010, 2011 SpringSource, a division of VMware + * + * 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. + * + * Contributors: + * Tasktop Technologies Inc. - initial API and implementation + *******************************************************************************/ +package org.springframework.tenancy.web; + +import java.util.concurrent.Callable; +import java.util.concurrent.ThreadFactory; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.core.task.SimpleAsyncTaskExecutor; +import org.springframework.mock.web.MockFilterChain; +import org.springframework.tenancy.context.TenancyContext; +import org.springframework.tenancy.context.TenancyContextHolder; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.context.request.async.AsyncWebRequest; +import org.springframework.web.context.request.async.CallableProcessingInterceptorAdapter; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class TenancyWebAsyncFilterTest { + @Mock + private TenancyContext tenancyContext; + + @Mock + private HttpServletRequest request; + + @Mock + private HttpServletResponse response; + + @Mock + private AsyncWebRequest asyncWebRequest; + + private WebAsyncManager asyncManager; + + private JoinableThreadFactory threadFactory; + + private MockFilterChain filterChain; + + private TenancyWebAsyncFilter filter; + + @Before + public void setUp() { + when(asyncWebRequest.getNativeRequest(HttpServletRequest.class)).thenReturn( + request); + when(request.getRequestURI()).thenReturn("/"); + filterChain = new MockFilterChain(); + + threadFactory = new JoinableThreadFactory(); + SimpleAsyncTaskExecutor executor = new SimpleAsyncTaskExecutor(); + executor.setThreadFactory(threadFactory); + + asyncManager = WebAsyncUtils.getAsyncManager(request); + asyncManager.setAsyncWebRequest(asyncWebRequest); + asyncManager.setTaskExecutor(executor); + when(request.getAttribute(WebAsyncUtils.WEB_ASYNC_MANAGER_ATTRIBUTE)).thenReturn( + asyncManager); + + filter = new TenancyWebAsyncFilter(); + } + + @After + public void clearTenancyContext() { + TenancyContextHolder.clearContext(); + } + + @Test + public void doFilterInternalRegistersTenancyContextCallableProcessor() + throws Exception { + TenancyContextHolder.setContext(tenancyContext); + asyncManager + .registerCallableInterceptors(new CallableProcessingInterceptorAdapter() { + @Override + public void postProcess(NativeWebRequest request, + Callable task, Object concurrentResult) throws Exception { + assertThat(TenancyContextHolder.getContext(), is(not(tenancyContext))); + } + }); + filter.doFilterInternal(request, response, filterChain); + + VerifyingCallable verifyingCallable = new VerifyingCallable(); + asyncManager.startCallableProcessing(verifyingCallable); + threadFactory.join(); + assertThat(asyncManager.getConcurrentResult(), is((Object) tenancyContext)); + } + + @Test + public void doFilterInternalRegistersTenancyContextCallableProcessorContextUpdated() + throws Exception { + TenancyContextHolder.setContext(TenancyContextHolder.createEmptyContext()); + asyncManager + .registerCallableInterceptors(new CallableProcessingInterceptorAdapter() { + @Override + public void postProcess(NativeWebRequest request, + Callable task, Object concurrentResult) throws Exception { + assertThat(TenancyContextHolder.getContext(), is(not(tenancyContext))); + } + }); + filter.doFilterInternal(request, response, filterChain); + TenancyContextHolder.setContext(tenancyContext); + + VerifyingCallable verifyingCallable = new VerifyingCallable(); + asyncManager.startCallableProcessing(verifyingCallable); + threadFactory.join(); + assertThat(asyncManager.getConcurrentResult(), is((Object) tenancyContext)); + } + + private static final class JoinableThreadFactory implements ThreadFactory { + private Thread t; + + public Thread newThread(Runnable r) { + t = new Thread(r); + return t; + } + + public void join() throws InterruptedException { + t.join(); + } + } + + private class VerifyingCallable implements Callable { + + public TenancyContext call() throws Exception { + return TenancyContextHolder.getContext(); + } + + } +}