Introduce composed annotations for web scopes

This commit introduces the following common composed annotations for
web scopes.

- @RequestScope
- @SessionScope
- @ApplicationScope

Issue: SPR-13993
This commit is contained in:
Sam Brannen 2016-02-27 20:28:29 +01:00
parent df7b24b8e7
commit b423596b2e
5 changed files with 243 additions and 49 deletions

View File

@ -0,0 +1,66 @@
/*
* Copyright 2002-2016 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.web.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.annotation.AliasFor;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.springframework.web.context.WebApplicationContext.SCOPE_APPLICATION;
/**
* {@code @ApplicationScope} is a specialization of {@link Scope @Scope} for a
* component whose lifecycle is bound to the current web application.
*
* <p>Specifically, {@code @ApplicationScope} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @Scope("application")} with the default
* {@link #proxyMode} set to {@link ScopedProxyMode#TARGET_CLASS TARGET_CLASS}.
*
* <p>{@code @ApplicationScope} may be used as a meta-annotation to create custom
* composed annotations.
*
* @author Sam Brannen
* @since 4.3
* @see RequestScope
* @see SessionScope
* @see org.springframework.context.annotation.Scope
* @see org.springframework.web.context.WebApplicationContext#SCOPE_APPLICATION
* @see org.springframework.web.context.support.ServletContextScope
* @see org.springframework.stereotype.Component
* @see org.springframework.context.annotation.Bean
*/
@Scope(SCOPE_APPLICATION)
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@Documented
public @interface ApplicationScope {
/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2002-2016 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.web.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.annotation.AliasFor;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.springframework.web.context.WebApplicationContext.SCOPE_REQUEST;
/**
* {@code @RequestScope} is a specialization of {@link Scope @Scope} for a
* component whose lifecycle is bound to the current web request.
*
* <p>Specifically, {@code @RequestScope} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @Scope("request")} with the default
* {@link #proxyMode} set to {@link ScopedProxyMode#TARGET_CLASS TARGET_CLASS}.
*
* <p>{@code @RequestScope} may be used as a meta-annotation to create custom
* composed annotations.
*
* @author Sam Brannen
* @since 4.3
* @see SessionScope
* @see ApplicationScope
* @see org.springframework.context.annotation.Scope
* @see org.springframework.web.context.WebApplicationContext#SCOPE_REQUEST
* @see org.springframework.web.context.request.RequestScope
* @see org.springframework.stereotype.Component
* @see org.springframework.context.annotation.Bean
*/
@Scope(SCOPE_REQUEST)
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@Documented
public @interface RequestScope {
/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

View File

@ -0,0 +1,66 @@
/*
* Copyright 2002-2016 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.web.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.core.annotation.AliasFor;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.springframework.web.context.WebApplicationContext.SCOPE_SESSION;
/**
* {@code @SessionScope} is a specialization of {@link Scope @Scope} for a
* component whose lifecycle is bound to the current web session.
*
* <p>Specifically, {@code @SessionScope} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @Scope("session")} with the default
* {@link #proxyMode} set to {@link ScopedProxyMode#TARGET_CLASS TARGET_CLASS}.
*
* <p>{@code @SessionScope} may be used as a meta-annotation to create custom
* composed annotations.
*
* @author Sam Brannen
* @since 4.3
* @see RequestScope
* @see ApplicationScope
* @see org.springframework.context.annotation.Scope
* @see org.springframework.web.context.WebApplicationContext#SCOPE_SESSION
* @see org.springframework.web.context.request.SessionScope
* @see org.springframework.stereotype.Component
* @see org.springframework.context.annotation.Bean
*/
@Scope(SCOPE_SESSION)
@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@Documented
public @interface SessionScope {
/**
* Alias for {@link Scope#proxyMode}.
* <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

View File

@ -662,10 +662,11 @@ Spring 4.3 also improves the caching abstraction as follows:
=== Web Improvements === Web Improvements
* Built-in support for <<mvc-ann-requestmapping-head-options,HTTP HEAD and HTTP OPTIONS>>. * Built-in support for <<mvc-ann-requestmapping-head-options,HTTP HEAD and HTTP OPTIONS>>.
* New `@RequestScope`, `@SessionScope`, and `@ApplicationScope` _composed annotations_ for web scopes.
* New `@RestControllerAdvice` annotation with combined `@ControllerAdvice` with `@ResponseBody` semantics. * New `@RestControllerAdvice` annotation with combined `@ControllerAdvice` with `@ResponseBody` semantics.
* `@ResponseStatus` supported on the class level and inherited on all methods. * `@ResponseStatus` is now supported at the class level and inherited by all methods.
* New `@SessionAttribute` annotation for access to session attributes (see <<mvc-ann-sessionattrib-global, example>>). * New `@SessionAttribute` annotation for access to session attributes (see <<mvc-ann-sessionattrib-global, example>>).
* New `@RequestAttribute` annotation for access to session attributes (see <<mvc-ann-requestattrib, example>>). * New `@RequestAttribute` annotation for access to request attributes (see <<mvc-ann-requestattrib, example>>).
* `@ModelAttribute` allows preventing data binding via `binding=false` attribute (see <<mvc-ann-modelattrib-method-args, reference>>). * `@ModelAttribute` allows preventing data binding via `binding=false` attribute (see <<mvc-ann-modelattrib-method-args, reference>>).
* `AsyncRestTemplate` supports request interception. * `AsyncRestTemplate` supports request interception.

View File

@ -1,5 +1,5 @@
/* /*
* Copyright 2002-2012 the original author or authors. * Copyright 2002-2016 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,50 +17,50 @@
package org.springframework.context.annotation.scope; package org.springframework.context.annotation.scope;
import org.junit.After; import org.junit.After;
import static org.junit.Assert.*;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ClassPathBeanDefinitionScanner; import org.springframework.context.annotation.ClassPathBeanDefinitionScanner;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockHttpSession;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.RequestScope;
import org.springframework.web.context.annotation.SessionScope;
import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.context.support.GenericWebApplicationContext;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.springframework.context.annotation.ScopedProxyMode.DEFAULT;
import static org.springframework.context.annotation.ScopedProxyMode.INTERFACES;
import static org.springframework.context.annotation.ScopedProxyMode.NO;
import static org.springframework.context.annotation.ScopedProxyMode.TARGET_CLASS;
/** /**
* @author Mark Fisher * @author Mark Fisher
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Chris Beams * @author Chris Beams
* @author Sam Brannen
*/ */
public class ClassPathBeanDefinitionScannerScopeIntegrationTests { public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
private static final String DEFAULT_NAME = "default"; private static final String DEFAULT_NAME = "default";
private static final String MODIFIED_NAME = "modified"; private static final String MODIFIED_NAME = "modified";
private ServletRequestAttributes oldRequestAttributes; private ServletRequestAttributes oldRequestAttributes = new ServletRequestAttributes(new MockHttpServletRequest());
private ServletRequestAttributes newRequestAttributes = new ServletRequestAttributes(new MockHttpServletRequest());
private ServletRequestAttributes newRequestAttributes;
private ServletRequestAttributes oldRequestAttributesWithSession; private ServletRequestAttributes oldRequestAttributesWithSession;
private ServletRequestAttributes newRequestAttributesWithSession; private ServletRequestAttributes newRequestAttributesWithSession;
@Before @Before
public void setUp() { public void setUp() {
this.oldRequestAttributes = new ServletRequestAttributes(new MockHttpServletRequest());
this.newRequestAttributes = new ServletRequestAttributes(new MockHttpServletRequest());
MockHttpServletRequest oldRequestWithSession = new MockHttpServletRequest(); MockHttpServletRequest oldRequestWithSession = new MockHttpServletRequest();
oldRequestWithSession.setSession(new MockHttpSession()); oldRequestWithSession.setSession(new MockHttpSession());
this.oldRequestAttributesWithSession = new ServletRequestAttributes(oldRequestWithSession); this.oldRequestAttributesWithSession = new ServletRequestAttributes(oldRequestWithSession);
@ -72,14 +72,14 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
@After @After
public void tearDown() throws Exception { public void tearDown() throws Exception {
RequestContextHolder.setRequestAttributes(null); RequestContextHolder.resetRequestAttributes();
} }
@Test @Test
public void testSingletonScopeWithNoProxy() { public void singletonScopeWithNoProxy() {
RequestContextHolder.setRequestAttributes(oldRequestAttributes); RequestContextHolder.setRequestAttributes(oldRequestAttributes);
ApplicationContext context = createContext(ScopedProxyMode.NO); ApplicationContext context = createContext(NO);
ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton");
// should not be a proxy // should not be a proxy
@ -98,9 +98,9 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
@Test @Test
public void testSingletonScopeIgnoresProxyInterfaces() { public void singletonScopeIgnoresProxyInterfaces() {
RequestContextHolder.setRequestAttributes(oldRequestAttributes); RequestContextHolder.setRequestAttributes(oldRequestAttributes);
ApplicationContext context = createContext(ScopedProxyMode.INTERFACES); ApplicationContext context = createContext(INTERFACES);
ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton");
// should not be a proxy // should not be a proxy
@ -119,9 +119,9 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
@Test @Test
public void testSingletonScopeIgnoresProxyTargetClass() { public void singletonScopeIgnoresProxyTargetClass() {
RequestContextHolder.setRequestAttributes(oldRequestAttributes); RequestContextHolder.setRequestAttributes(oldRequestAttributes);
ApplicationContext context = createContext(ScopedProxyMode.TARGET_CLASS); ApplicationContext context = createContext(TARGET_CLASS);
ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton"); ScopedTestBean bean = (ScopedTestBean) context.getBean("singleton");
// should not be a proxy // should not be a proxy
@ -140,9 +140,9 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
@Test @Test
public void testRequestScopeWithNoProxy() { public void requestScopeWithNoProxy() {
RequestContextHolder.setRequestAttributes(oldRequestAttributes); RequestContextHolder.setRequestAttributes(oldRequestAttributes);
ApplicationContext context = createContext(ScopedProxyMode.NO); ApplicationContext context = createContext(NO);
ScopedTestBean bean = (ScopedTestBean) context.getBean("request"); ScopedTestBean bean = (ScopedTestBean) context.getBean("request");
// should not be a proxy // should not be a proxy
@ -161,9 +161,9 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
@Test @Test
public void testRequestScopeWithProxiedInterfaces() { public void requestScopeWithProxiedInterfaces() {
RequestContextHolder.setRequestAttributes(oldRequestAttributes); RequestContextHolder.setRequestAttributes(oldRequestAttributes);
ApplicationContext context = createContext(ScopedProxyMode.INTERFACES); ApplicationContext context = createContext(INTERFACES);
IScopedTestBean bean = (IScopedTestBean) context.getBean("request"); IScopedTestBean bean = (IScopedTestBean) context.getBean("request");
// should be dynamic proxy, implementing both interfaces // should be dynamic proxy, implementing both interfaces
@ -182,9 +182,9 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
@Test @Test
public void testRequestScopeWithProxiedTargetClass() { public void requestScopeWithProxiedTargetClass() {
RequestContextHolder.setRequestAttributes(oldRequestAttributes); RequestContextHolder.setRequestAttributes(oldRequestAttributes);
ApplicationContext context = createContext(ScopedProxyMode.TARGET_CLASS); ApplicationContext context = createContext(TARGET_CLASS);
IScopedTestBean bean = (IScopedTestBean) context.getBean("request"); IScopedTestBean bean = (IScopedTestBean) context.getBean("request");
// should be a class-based proxy // should be a class-based proxy
@ -203,9 +203,9 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
@Test @Test
public void testSessionScopeWithNoProxy() { public void sessionScopeWithNoProxy() {
RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession);
ApplicationContext context = createContext(ScopedProxyMode.NO); ApplicationContext context = createContext(NO);
ScopedTestBean bean = (ScopedTestBean) context.getBean("session"); ScopedTestBean bean = (ScopedTestBean) context.getBean("session");
// should not be a proxy // should not be a proxy
@ -224,9 +224,9 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
@Test @Test
public void testSessionScopeWithProxiedInterfaces() { public void sessionScopeWithProxiedInterfaces() {
RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession);
ApplicationContext context = createContext(ScopedProxyMode.INTERFACES); ApplicationContext context = createContext(INTERFACES);
IScopedTestBean bean = (IScopedTestBean) context.getBean("session"); IScopedTestBean bean = (IScopedTestBean) context.getBean("session");
// should be dynamic proxy, implementing both interfaces // should be dynamic proxy, implementing both interfaces
@ -251,9 +251,9 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
@Test @Test
public void testSessionScopeWithProxiedTargetClass() { public void sessionScopeWithProxiedTargetClass() {
RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession); RequestContextHolder.setRequestAttributes(oldRequestAttributesWithSession);
ApplicationContext context = createContext(ScopedProxyMode.TARGET_CLASS); ApplicationContext context = createContext(TARGET_CLASS);
IScopedTestBean bean = (IScopedTestBean) context.getBean("session"); IScopedTestBean bean = (IScopedTestBean) context.getBean("session");
// should be a class-based proxy // should be a class-based proxy
@ -283,12 +283,7 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
GenericWebApplicationContext context = new GenericWebApplicationContext(); GenericWebApplicationContext context = new GenericWebApplicationContext();
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context); ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context);
scanner.setIncludeAnnotationConfig(false); scanner.setIncludeAnnotationConfig(false);
scanner.setBeanNameGenerator(new BeanNameGenerator() { scanner.setBeanNameGenerator((definition, registry) -> definition.getScope());
@Override
public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry registry) {
return definition.getScope();
}
});
scanner.setScopedProxyMode(scopedProxyMode); scanner.setScopedProxyMode(scopedProxyMode);
// Scan twice in order to find errors in the bean definition compatibility check. // Scan twice in order to find errors in the bean definition compatibility check.
@ -300,7 +295,7 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
public static interface IScopedTestBean { static interface IScopedTestBean {
String getName(); String getName();
@ -308,7 +303,7 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
} }
public static abstract class ScopedTestBean implements IScopedTestBean { static abstract class ScopedTestBean implements IScopedTestBean {
private String name = DEFAULT_NAME; private String name = DEFAULT_NAME;
@ -321,23 +316,23 @@ public class ClassPathBeanDefinitionScannerScopeIntegrationTests {
@Component @Component
public static class SingletonScopedTestBean extends ScopedTestBean { static class SingletonScopedTestBean extends ScopedTestBean {
} }
public static interface AnotherScopeTestInterface { static interface AnotherScopeTestInterface {
} }
@Component @Component
@Scope("request") @RequestScope(proxyMode = DEFAULT)
public static class RequestScopedTestBean extends ScopedTestBean implements AnotherScopeTestInterface { static class RequestScopedTestBean extends ScopedTestBean implements AnotherScopeTestInterface {
} }
@Component @Component
@Scope("session") @SessionScope(proxyMode = DEFAULT)
public static class SessionScopedTestBean extends ScopedTestBean implements AnotherScopeTestInterface { static class SessionScopedTestBean extends ScopedTestBean implements AnotherScopeTestInterface {
} }
} }