Skip to content

Commit fe7da9a

Browse files
committed
fixes 35343
1 parent 5d325ca commit fe7da9a

File tree

2 files changed

+190
-2
lines changed

2 files changed

+190
-2
lines changed

spring-webmvc/src/main/java/org/springframework/web/servlet/mvc/method/annotation/JsonViewResponseBodyAdvice.java

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,15 @@
3333
/**
3434
* A {@link ResponseBodyAdvice} implementation that adds support for Jackson's
3535
* {@code @JsonView} annotation declared on a Spring MVC {@code @RequestMapping}
36-
* or {@code @ExceptionHandler} method.
36+
* or {@code @ExceptionHandler} method, or at the controller class level.
3737
*
3838
* <p>The serialization view specified in the annotation will be passed in to the
3939
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}
4040
* which will then use it to serialize the response body.
4141
*
42+
* <p>When both method-level and class-level {@code @JsonView} annotations are present,
43+
* the method-level annotation takes precedence.
44+
*
4245
* <p>Note that despite {@code @JsonView} allowing for more than one class to
4346
* be specified, the use for a response body advice is only supported with
4447
* exactly one class argument. Consider the use of a composite interface.
@@ -53,7 +56,9 @@ public class JsonViewResponseBodyAdvice extends AbstractMappingJacksonResponseBo
5356

5457
@Override
5558
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
56-
return super.supports(returnType, converterType) && returnType.hasMethodAnnotation(JsonView.class);
59+
return super.supports(returnType, converterType) &&
60+
(returnType.hasMethodAnnotation(JsonView.class) ||
61+
returnType.getDeclaringClass().isAnnotationPresent(JsonView.class));
5762
}
5863

5964
@Override
@@ -70,6 +75,12 @@ protected void beforeBodyWriteInternal(MappingJacksonValue bodyContainer, MediaT
7075

7176
private static Class<?> getJsonView(MethodParameter returnType) {
7277
JsonView ann = returnType.getMethodAnnotation(JsonView.class);
78+
79+
// If no method-level annotation, check for class-level annotation
80+
if (ann == null) {
81+
ann = returnType.getDeclaringClass().getAnnotation(JsonView.class);
82+
}
83+
7384
Assert.state(ann != null, "No JsonView annotation");
7485

7586
Class<?>[] classes = ann.value();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright 2002-present 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.web.servlet.mvc.method.annotation;
18+
19+
import java.lang.reflect.Method;
20+
import java.util.List;
21+
22+
import com.fasterxml.jackson.annotation.JsonView;
23+
import org.junit.jupiter.api.BeforeEach;
24+
import org.junit.jupiter.api.Test;
25+
26+
import org.springframework.core.MethodParameter;
27+
import org.springframework.http.MediaType;
28+
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter;
29+
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
30+
import org.springframework.http.converter.json.MappingJacksonValue;
31+
import org.springframework.http.server.ServerHttpRequest;
32+
import org.springframework.http.server.ServerHttpResponse;
33+
import org.springframework.http.server.ServletServerHttpRequest;
34+
import org.springframework.http.server.ServletServerHttpResponse;
35+
import org.springframework.web.bind.annotation.RequestMapping;
36+
import org.springframework.web.bind.annotation.ResponseBody;
37+
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
38+
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
39+
40+
import static org.assertj.core.api.Assertions.assertThat;
41+
42+
/**
43+
* Tests for class-level {@code @JsonView} support in {@link JsonViewResponseBodyAdvice}.
44+
*
45+
* @author Asif Ebrahim
46+
* @since 7.0
47+
*/
48+
class JsonViewResponseBodyAdviceClassLevelTests {
49+
50+
private JsonViewResponseBodyAdvice advice;
51+
52+
private ServerHttpRequest request;
53+
54+
private ServerHttpResponse response;
55+
56+
57+
@BeforeEach
58+
void setup() {
59+
this.advice = new JsonViewResponseBodyAdvice();
60+
this.request = new ServletServerHttpRequest(new MockHttpServletRequest());
61+
this.response = new ServletServerHttpResponse(new MockHttpServletResponse());
62+
}
63+
64+
65+
@Test
66+
void supportsWithClassLevelJsonView() throws Exception {
67+
Method method = ClassLevelJsonViewController.class.getDeclaredMethod("methodWithoutAnnotation");
68+
MethodParameter returnType = new MethodParameter(method, -1);
69+
70+
assertThat(this.advice.supports(returnType, MappingJackson2HttpMessageConverter.class)).isTrue();
71+
assertThat(this.advice.supports(returnType, JacksonJsonHttpMessageConverter.class)).isTrue();
72+
}
73+
74+
@Test
75+
void supportsWithMethodLevelJsonView() throws Exception {
76+
Method method = RegularController.class.getDeclaredMethod("methodWithJsonView");
77+
MethodParameter returnType = new MethodParameter(method, -1);
78+
79+
assertThat(this.advice.supports(returnType, MappingJackson2HttpMessageConverter.class)).isTrue();
80+
assertThat(this.advice.supports(returnType, JacksonJsonHttpMessageConverter.class)).isTrue();
81+
}
82+
83+
@Test
84+
void doesNotSupportWithoutJsonView() throws Exception {
85+
Method method = RegularController.class.getDeclaredMethod("methodWithoutAnnotation");
86+
MethodParameter returnType = new MethodParameter(method, -1);
87+
88+
assertThat(this.advice.supports(returnType, MappingJackson2HttpMessageConverter.class)).isFalse();
89+
assertThat(this.advice.supports(returnType, JacksonJsonHttpMessageConverter.class)).isFalse();
90+
}
91+
92+
@Test
93+
void beforeBodyWriteWithClassLevelJsonView() throws Exception {
94+
Method method = ClassLevelJsonViewController.class.getDeclaredMethod("methodWithoutAnnotation");
95+
MethodParameter returnType = new MethodParameter(method, -1);
96+
97+
MappingJacksonValue container = new MappingJacksonValue(new Object());
98+
this.advice.beforeBodyWriteInternal(container, MediaType.APPLICATION_JSON, returnType, this.request, this.response);
99+
100+
assertThat(container.getSerializationView()).isEqualTo(MyJsonView.class);
101+
}
102+
103+
@Test
104+
void beforeBodyWriteWithMethodLevelJsonView() throws Exception {
105+
Method method = RegularController.class.getDeclaredMethod("methodWithJsonView");
106+
MethodParameter returnType = new MethodParameter(method, -1);
107+
108+
MappingJacksonValue container = new MappingJacksonValue(new Object());
109+
this.advice.beforeBodyWriteInternal(container, MediaType.APPLICATION_JSON, returnType, this.request, this.response);
110+
111+
assertThat(container.getSerializationView()).isEqualTo(MyJsonView.class);
112+
}
113+
114+
@Test
115+
void methodLevelAnnotationTakesPrecedenceOverClassLevel() throws Exception {
116+
Method method = ClassLevelJsonViewController.class.getDeclaredMethod("methodWithDifferentJsonView");
117+
MethodParameter returnType = new MethodParameter(method, -1);
118+
119+
MappingJacksonValue container = new MappingJacksonValue(new Object());
120+
this.advice.beforeBodyWriteInternal(container, MediaType.APPLICATION_JSON, returnType, this.request, this.response);
121+
122+
// Method-level annotation should take precedence
123+
assertThat(container.getSerializationView()).isEqualTo(AnotherJsonView.class);
124+
}
125+
126+
@Test
127+
void determineWriteHintsWithClassLevelJsonView() throws Exception {
128+
Method method = ClassLevelJsonViewController.class.getDeclaredMethod("methodWithoutAnnotation");
129+
MethodParameter returnType = new MethodParameter(method, -1);
130+
131+
var hints = this.advice.determineWriteHints(new Object(), returnType, MediaType.APPLICATION_JSON, MappingJackson2HttpMessageConverter.class);
132+
133+
assertThat(hints).containsEntry(JsonView.class.getName(), MyJsonView.class);
134+
}
135+
136+
137+
// Test interfaces for JsonView
138+
private interface MyJsonView {}
139+
140+
private interface AnotherJsonView {}
141+
142+
// Test controller with class-level @JsonView
143+
@JsonView(MyJsonView.class)
144+
private static class ClassLevelJsonViewController {
145+
146+
@RequestMapping
147+
@ResponseBody
148+
public String methodWithoutAnnotation() {
149+
return "test";
150+
}
151+
152+
@RequestMapping
153+
@ResponseBody
154+
@JsonView(AnotherJsonView.class)
155+
public String methodWithDifferentJsonView() {
156+
return "test";
157+
}
158+
}
159+
160+
// Test controller without class-level @JsonView
161+
private static class RegularController {
162+
163+
@RequestMapping
164+
@ResponseBody
165+
@JsonView(MyJsonView.class)
166+
public String methodWithJsonView() {
167+
return "test";
168+
}
169+
170+
@RequestMapping
171+
@ResponseBody
172+
public String methodWithoutAnnotation() {
173+
return "test";
174+
}
175+
}
176+
177+
}

0 commit comments

Comments
 (0)