Skip to content

Commit 6cda08e

Browse files
committed
Add Jackson @JSONVIEW support
Issue: SPR-14693
1 parent e74c59b commit 6cda08e

File tree

4 files changed

+327
-3
lines changed

4 files changed

+327
-3
lines changed

spring-web-reactive/src/main/java/org/springframework/web/reactive/config/WebReactiveConfiguration.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
import org.springframework.web.reactive.handler.AbstractHandlerMapping;
6161
import org.springframework.web.reactive.result.SimpleHandlerAdapter;
6262
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolver;
63+
import org.springframework.http.codec.Jackson2ServerHttpMessageReader;
64+
import org.springframework.http.codec.Jackson2ServerHttpMessageWriter;
6365
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter;
6466
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
6567
import org.springframework.web.reactive.result.method.annotation.ResponseBodyResultHandler;
@@ -286,7 +288,7 @@ protected final void addDefaultHttpMessageReaders(List<HttpMessageReader<?>> rea
286288
readers.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
287289
}
288290
if (jackson2Present) {
289-
readers.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
291+
readers.add(new Jackson2ServerHttpMessageReader(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder())));
290292
}
291293
}
292294

@@ -404,10 +406,10 @@ protected final void addDefaultHttpMessageWriters(List<HttpMessageWriter<?>> wri
404406
}
405407
if (jackson2Present) {
406408
Jackson2JsonEncoder jacksonEncoder = new Jackson2JsonEncoder();
407-
writers.add(new EncoderHttpMessageWriter<>(jacksonEncoder));
409+
writers.add(new Jackson2ServerHttpMessageWriter(new EncoderHttpMessageWriter<>(jacksonEncoder)));
408410
sseDataEncoders.add(jacksonEncoder);
409411
}
410-
writers.add(new ServerSentEventHttpMessageWriter(sseDataEncoders));
412+
writers.add(new Jackson2ServerHttpMessageWriter(new ServerSentEventHttpMessageWriter(sseDataEncoders)));
411413
}
412414
/**
413415
* Override this to modify the list of message writers after it has been
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright 2002-2016 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+
* http://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.reactive.result.method.annotation;
18+
19+
import java.util.Arrays;
20+
import java.util.List;
21+
22+
import com.fasterxml.jackson.annotation.JsonView;
23+
import static org.junit.Assert.assertEquals;
24+
import org.junit.Test;
25+
import reactor.core.publisher.Flux;
26+
import reactor.core.publisher.Mono;
27+
28+
import org.springframework.context.ApplicationContext;
29+
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
30+
import org.springframework.context.annotation.ComponentScan;
31+
import org.springframework.context.annotation.Configuration;
32+
import org.springframework.http.MediaType;
33+
import org.springframework.web.bind.annotation.GetMapping;
34+
import org.springframework.web.bind.annotation.PostMapping;
35+
import org.springframework.web.bind.annotation.RequestBody;
36+
import org.springframework.web.bind.annotation.RestController;
37+
import org.springframework.web.reactive.config.WebReactiveConfiguration;
38+
39+
/**
40+
* @author Sebastien Deleuze
41+
*/
42+
public class JacksonHintsIntegrationTests extends AbstractRequestMappingIntegrationTests {
43+
44+
@Override
45+
protected ApplicationContext initApplicationContext() {
46+
AnnotationConfigApplicationContext wac = new AnnotationConfigApplicationContext();
47+
wac.register(WebConfig.class);
48+
wac.refresh();
49+
return wac;
50+
}
51+
52+
@Test
53+
public void jsonViewResponse() throws Exception {
54+
String expected = "{\"withView1\":\"with\"}";
55+
assertEquals(expected, performGet("/response/raw", MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
56+
}
57+
58+
@Test
59+
public void jsonViewWithMonoResponse() throws Exception {
60+
String expected = "{\"withView1\":\"with\"}";
61+
assertEquals(expected, performGet("/response/mono", MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
62+
}
63+
64+
@Test
65+
public void jsonViewWithFluxResponse() throws Exception {
66+
String expected = "[{\"withView1\":\"with\"},{\"withView1\":\"with\"}]";
67+
assertEquals(expected, performGet("/response/flux", MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
68+
}
69+
70+
@Test
71+
public void jsonViewWithRequest() throws Exception {
72+
String expected = "{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}";
73+
assertEquals(expected, performPost("/request/raw", MediaType.APPLICATION_JSON,
74+
new JacksonViewBean("with", "with", "without"), MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
75+
}
76+
77+
@Test
78+
public void jsonViewWithMonoRequest() throws Exception {
79+
String expected = "{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}";
80+
assertEquals(expected, performPost("/request/mono", MediaType.APPLICATION_JSON,
81+
new JacksonViewBean("with", "with", "without"), MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
82+
}
83+
84+
@Test
85+
public void jsonViewWithFluxRequest() throws Exception {
86+
String expected = "[{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}," +
87+
"{\"withView1\":\"with\",\"withView2\":null,\"withoutView\":null}]";
88+
List<JacksonViewBean> beans = Arrays.asList(new JacksonViewBean("with", "with", "without"), new JacksonViewBean("with", "with", "without"));
89+
assertEquals(expected, performPost("/request/flux", MediaType.APPLICATION_JSON, beans,
90+
MediaType.APPLICATION_JSON_UTF8, String.class).getBody());
91+
}
92+
93+
94+
@Configuration
95+
@ComponentScan(resourcePattern = "**/JacksonHintsIntegrationTests*.class")
96+
@SuppressWarnings({"unused", "WeakerAccess"})
97+
static class WebConfig extends WebReactiveConfiguration {
98+
}
99+
100+
@RestController
101+
@SuppressWarnings("unused")
102+
private static class JsonViewRestController {
103+
104+
@GetMapping("/response/raw")
105+
@JsonView(MyJacksonView1.class)
106+
public JacksonViewBean rawResponse() {
107+
return new JacksonViewBean("with", "with", "without");
108+
}
109+
110+
@GetMapping("/response/mono")
111+
@JsonView(MyJacksonView1.class)
112+
public Mono<JacksonViewBean> monoResponse() {
113+
return Mono.just(new JacksonViewBean("with", "with", "without"));
114+
}
115+
116+
@GetMapping("/response/flux")
117+
@JsonView(MyJacksonView1.class)
118+
public Flux<JacksonViewBean> fluxResponse() {
119+
return Flux.just(new JacksonViewBean("with", "with", "without"), new JacksonViewBean("with", "with", "without"));
120+
}
121+
122+
@PostMapping("/request/raw")
123+
public JacksonViewBean rawRequest(@JsonView(MyJacksonView1.class) @RequestBody JacksonViewBean bean) {
124+
return bean;
125+
}
126+
127+
@PostMapping("/request/mono")
128+
public Mono<JacksonViewBean> monoRequest(@JsonView(MyJacksonView1.class) @RequestBody Mono<JacksonViewBean> mono) {
129+
return mono;
130+
}
131+
132+
@PostMapping("/request/flux")
133+
public Flux<JacksonViewBean> fluxRequest(@JsonView(MyJacksonView1.class) @RequestBody Flux<JacksonViewBean> flux) {
134+
return flux;
135+
}
136+
137+
}
138+
139+
private interface MyJacksonView1 {}
140+
141+
private interface MyJacksonView2 {}
142+
143+
144+
@SuppressWarnings("unused")
145+
private static class JacksonViewBean {
146+
147+
@JsonView(MyJacksonView1.class)
148+
private String withView1;
149+
150+
@JsonView(MyJacksonView2.class)
151+
private String withView2;
152+
153+
private String withoutView;
154+
155+
156+
public JacksonViewBean() {
157+
}
158+
159+
public JacksonViewBean(String withView1, String withView2, String withoutView) {
160+
this.withView1 = withView1;
161+
this.withView2 = withView2;
162+
this.withoutView = withoutView;
163+
}
164+
165+
public String getWithView1() {
166+
return withView1;
167+
}
168+
169+
public void setWithView1(String withView1) {
170+
this.withView1 = withView1;
171+
}
172+
173+
public String getWithView2() {
174+
return withView2;
175+
}
176+
177+
public void setWithView2(String withView2) {
178+
this.withView2 = withView2;
179+
}
180+
181+
public String getWithoutView() {
182+
return withoutView;
183+
}
184+
185+
public void setWithoutView(String withoutView) {
186+
this.withoutView = withoutView;
187+
}
188+
}
189+
190+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2002-2016 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+
* http://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.http.codec;
18+
19+
import java.util.Collections;
20+
import java.util.Map;
21+
22+
import com.fasterxml.jackson.annotation.JsonView;
23+
24+
import org.springframework.core.MethodParameter;
25+
import org.springframework.core.ResolvableType;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.http.codec.json.AbstractJackson2Codec;
28+
import org.springframework.http.server.reactive.ServerHttpRequest;
29+
30+
/**
31+
* {@link ServerHttpMessageReader} that resolves those annotation or request based Jackson 2 hints:
32+
* <ul>
33+
* <li>{@code @JsonView} + {@code @RequestBody} annotated handler method parameter</li>
34+
* </ul>
35+
*
36+
* @author Sebastien Deleuze
37+
* @since 5.0
38+
* @see com.fasterxml.jackson.annotation.JsonView
39+
*/
40+
public class Jackson2ServerHttpMessageReader extends AbstractServerHttpMessageReader<Object> {
41+
42+
public Jackson2ServerHttpMessageReader(HttpMessageReader<Object> reader) {
43+
super(reader);
44+
}
45+
46+
@Override
47+
protected Map<String, Object> resolveReadHintsInternal(ResolvableType streamType,
48+
ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) {
49+
50+
Object source = streamType.getSource();
51+
MethodParameter parameter = (source instanceof MethodParameter ? (MethodParameter)source : null);
52+
if (parameter != null) {
53+
JsonView annotation = parameter.getParameterAnnotation(JsonView.class);
54+
if (annotation != null) {
55+
Class<?>[] classes = annotation.value();
56+
if (classes.length != 1) {
57+
throw new IllegalArgumentException(
58+
"@JsonView only supported for read hints with exactly 1 class argument: " + parameter);
59+
}
60+
return Collections.singletonMap(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]);
61+
}
62+
}
63+
return Collections.emptyMap();
64+
}
65+
66+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2002-2016 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+
* http://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.http.codec;
18+
19+
import java.util.Collections;
20+
import java.util.Map;
21+
22+
import com.fasterxml.jackson.annotation.JsonView;
23+
24+
import org.springframework.core.MethodParameter;
25+
import org.springframework.core.ResolvableType;
26+
import org.springframework.http.MediaType;
27+
import org.springframework.http.codec.json.AbstractJackson2Codec;
28+
import org.springframework.http.server.reactive.ServerHttpRequest;
29+
30+
/**
31+
* {@link ServerHttpMessageWriter} that resolves those annotation or request based Jackson 2 hints:
32+
* <ul>
33+
* <li>{@code @JsonView} annotated handler method</li>
34+
* </ul>
35+
*
36+
* @author Sebastien Deleuze
37+
* @since 5.0
38+
* @see com.fasterxml.jackson.annotation.JsonView
39+
*/
40+
public class Jackson2ServerHttpMessageWriter extends AbstractServerHttpMessageWriter<Object> {
41+
42+
public Jackson2ServerHttpMessageWriter(HttpMessageWriter<Object> writer) {
43+
super(writer);
44+
}
45+
46+
@Override
47+
protected Map<String, Object> resolveWriteHintsInternal(ResolvableType streamType,
48+
ResolvableType elementType, MediaType mediaType, ServerHttpRequest request) {
49+
50+
Object source = streamType.getSource();
51+
MethodParameter returnValue = (source instanceof MethodParameter ? (MethodParameter)source : null);
52+
if (returnValue != null) {
53+
JsonView annotation = returnValue.getMethodAnnotation(JsonView.class);
54+
if (annotation != null) {
55+
Class<?>[] classes = annotation.value();
56+
if (classes.length != 1) {
57+
throw new IllegalArgumentException(
58+
"@JsonView only supported for write hints with exactly 1 class argument: " + returnValue);
59+
}
60+
return Collections.singletonMap(AbstractJackson2Codec.JSON_VIEW_HINT, classes[0]);
61+
}
62+
}
63+
return Collections.emptyMap();
64+
}
65+
66+
}

0 commit comments

Comments
 (0)