Skip to content

Commit ffcd83e

Browse files
committed
Ignore scoped proxy targets for @ControllerAdvice beans
Prior to this commit, methods in a @ControllerAdvice bean were registered and invoked twice if the advice was a scoped bean (e.g., request or session scoped). In other words, both the proxy bean and the target bean were wrapped in ControllerAdviceBean instances. This commit fixes this bug by modifying the findAnnotatedBeans() method in ControllerAdviceBean so that it filters out targets of scoped proxies. Closes gh-24017
1 parent 9a52294 commit ffcd83e

File tree

3 files changed

+143
-10
lines changed

3 files changed

+143
-10
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*
2+
* Copyright 2002-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.test.web.servlet.samples.spr;
18+
19+
import java.util.concurrent.atomic.AtomicInteger;
20+
21+
import org.junit.Before;
22+
import org.junit.Test;
23+
import org.junit.runner.RunWith;
24+
25+
import org.springframework.beans.factory.annotation.Autowired;
26+
import org.springframework.context.annotation.Bean;
27+
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.stereotype.Controller;
29+
import org.springframework.test.context.junit4.SpringRunner;
30+
import org.springframework.test.context.web.WebAppConfiguration;
31+
import org.springframework.test.web.servlet.MockMvc;
32+
import org.springframework.ui.Model;
33+
import org.springframework.web.bind.annotation.ControllerAdvice;
34+
import org.springframework.web.bind.annotation.GetMapping;
35+
import org.springframework.web.bind.annotation.ModelAttribute;
36+
import org.springframework.web.context.WebApplicationContext;
37+
import org.springframework.web.context.annotation.RequestScope;
38+
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
39+
40+
import static org.junit.Assert.assertEquals;
41+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
42+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.forwardedUrl;
43+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
44+
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup;
45+
46+
/**
47+
* Integration tests for {@link ControllerAdvice @ControllerAdvice}.
48+
*
49+
* <p>Introduced in conjunction with
50+
* <a href="https://github.com/spring-projects/spring-framework/issues/24017">gh-24017</a>.
51+
*
52+
* @author Sam Brannen
53+
* @since 5.1.12
54+
*/
55+
@RunWith(SpringRunner.class)
56+
@WebAppConfiguration
57+
public class ControllerAdviceIntegrationTests {
58+
59+
@Autowired
60+
WebApplicationContext wac;
61+
62+
MockMvc mockMvc;
63+
64+
@Before
65+
public void setUpMockMvc() {
66+
this.mockMvc = webAppContextSetup(wac).build();
67+
}
68+
69+
@Test
70+
public void controllerAdviceIsAppliedOnlyOnce() throws Exception {
71+
assertEquals(0, SingletonControllerAdvice.counter.get());
72+
assertEquals(0, RequestScopedControllerAdvice.counter.get());
73+
74+
this.mockMvc.perform(get("/test"))//
75+
.andExpect(status().isOk())//
76+
.andExpect(forwardedUrl("singleton:1;request-scoped:1"));
77+
78+
assertEquals(1, SingletonControllerAdvice.counter.get());
79+
assertEquals(1, RequestScopedControllerAdvice.counter.get());
80+
}
81+
82+
@Configuration
83+
@EnableWebMvc
84+
static class Config {
85+
86+
@Bean
87+
TestController testController() {
88+
return new TestController();
89+
}
90+
91+
@Bean
92+
SingletonControllerAdvice singletonControllerAdvice() {
93+
return new SingletonControllerAdvice();
94+
}
95+
96+
@Bean
97+
@RequestScope
98+
RequestScopedControllerAdvice requestScopedControllerAdvice() {
99+
return new RequestScopedControllerAdvice();
100+
}
101+
}
102+
103+
@ControllerAdvice
104+
static class SingletonControllerAdvice {
105+
106+
static final AtomicInteger counter = new AtomicInteger();
107+
108+
@ModelAttribute
109+
void initModel(Model model) {
110+
model.addAttribute("singleton", counter.incrementAndGet());
111+
}
112+
}
113+
114+
@ControllerAdvice
115+
static class RequestScopedControllerAdvice {
116+
117+
static final AtomicInteger counter = new AtomicInteger();
118+
119+
@ModelAttribute
120+
void initModel(Model model) {
121+
model.addAttribute("request-scoped", counter.incrementAndGet());
122+
}
123+
}
124+
125+
@Controller
126+
static class TestController {
127+
128+
@GetMapping("/test")
129+
String get(Model model) {
130+
return "singleton:" + model.asMap().get("singleton") + ";request-scoped:"
131+
+ model.asMap().get("request-scoped");
132+
}
133+
}
134+
135+
}

spring-web/src/main/java/org/springframework/web/method/ControllerAdviceBean.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2019 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020
import java.util.List;
2121
import java.util.stream.Collectors;
2222

23+
import org.springframework.aop.scope.ScopedProxyUtils;
2324
import org.springframework.beans.factory.BeanFactory;
2425
import org.springframework.beans.factory.BeanFactoryUtils;
2526
import org.springframework.context.ApplicationContext;
@@ -42,6 +43,7 @@
4243
* @author Rossen Stoyanchev
4344
* @author Brian Clozel
4445
* @author Juergen Hoeller
46+
* @author Sam Brannen
4547
* @since 3.2
4648
*/
4749
public class ControllerAdviceBean implements Ordered {
@@ -187,6 +189,7 @@ public String toString() {
187189
*/
188190
public static List<ControllerAdviceBean> findAnnotatedBeans(ApplicationContext context) {
189191
return Arrays.stream(BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context, Object.class))
192+
.filter(name -> !ScopedProxyUtils.isScopedTarget(name))
190193
.filter(name -> context.findAnnotationOnBean(name, ControllerAdvice.class) != null)
191194
.map(name -> new ControllerAdviceBean(name, context))
192195
.collect(Collectors.toList());

spring-webmvc/src/test/java/org/springframework/web/servlet/mvc/annotation/RequestScopedControllerAdviceIntegrationTests.java

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,17 +48,12 @@ public void loadContextWithRequestScopedControllerAdvice() {
4848
context.register(Config.class);
4949
context.refresh();
5050

51-
// Until gh-24017 is fixed, we expect the RequestScopedControllerAdvice to show up twice.
5251
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(context);
53-
assertEquals(2, adviceBeans.size());
52+
assertEquals(1, adviceBeans.size());
5453

55-
ControllerAdviceBean adviceBean1 = adviceBeans.get(0);
56-
assertEquals(RequestScopedControllerAdvice.class, adviceBean1.getBeanType());
57-
assertEquals(42, adviceBean1.getOrder());
58-
59-
ControllerAdviceBean adviceBean2 = adviceBeans.get(1);
60-
assertEquals(RequestScopedControllerAdvice.class, adviceBean2.getBeanType());
61-
assertEquals(42, adviceBean2.getOrder());
54+
ControllerAdviceBean adviceBean = adviceBeans.get(0);
55+
assertEquals(RequestScopedControllerAdvice.class, adviceBean.getBeanType());
56+
assertEquals(42, adviceBean.getOrder());
6257

6358
context.close();
6459
}

0 commit comments

Comments
 (0)