提交 8e272bc5 编写于 作者: S Sebastien Deleuze

Expose ServerCodecConfigurer as a bean

With this commit, ServerCodecConfigurer is now exposed as a bean in
order to be provided to DefaultServerWebExchange via
WebHttpHandlerBuilder and HttpWebHandlerAdapter. This allows
DefaultServerWebExchange to get configured codecs for reading form or
multipart requests.

Issue: SPR-14546
上级 a712c196
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package org.springframework.mock.http.server.reactive; package org.springframework.mock.http.server.reactive;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.server.ServerWebExchangeDecorator; import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.DefaultWebSessionManager;
...@@ -35,7 +36,8 @@ public class MockServerWebExchange extends ServerWebExchangeDecorator { ...@@ -35,7 +36,8 @@ public class MockServerWebExchange extends ServerWebExchangeDecorator {
public MockServerWebExchange(MockServerHttpRequest request) { public MockServerWebExchange(MockServerHttpRequest request) {
super(new DefaultServerWebExchange( super(new DefaultServerWebExchange(
request, new MockServerHttpResponse(), new DefaultWebSessionManager())); request, new MockServerHttpResponse(), new DefaultWebSessionManager(),
ServerCodecConfigurer.create()));
} }
......
...@@ -34,7 +34,8 @@ import org.springframework.http.HttpMethod; ...@@ -34,7 +34,8 @@ import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.InvalidMediaTypeException;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.codec.FormHttpMessageReader; import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert; import org.springframework.util.Assert;
...@@ -46,6 +47,8 @@ import org.springframework.web.server.ServerWebExchange; ...@@ -46,6 +47,8 @@ import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebSession; import org.springframework.web.server.WebSession;
import org.springframework.web.server.session.WebSessionManager; import org.springframework.web.server.session.WebSessionManager;
import static org.springframework.http.MediaType.*;
/** /**
* Default implementation of {@link ServerWebExchange}. * Default implementation of {@link ServerWebExchange}.
* *
...@@ -56,8 +59,6 @@ public class DefaultServerWebExchange implements ServerWebExchange { ...@@ -56,8 +59,6 @@ public class DefaultServerWebExchange implements ServerWebExchange {
private static final List<HttpMethod> SAFE_METHODS = Arrays.asList(HttpMethod.GET, HttpMethod.HEAD); private static final List<HttpMethod> SAFE_METHODS = Arrays.asList(HttpMethod.GET, HttpMethod.HEAD);
private static final FormHttpMessageReader FORM_READER = new FormHttpMessageReader();
private static final ResolvableType FORM_DATA_VALUE_TYPE = private static final ResolvableType FORM_DATA_VALUE_TYPE =
ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class); ResolvableType.forClassWithGenerics(MultiValueMap.class, String.class, String.class);
...@@ -85,26 +86,35 @@ public class DefaultServerWebExchange implements ServerWebExchange { ...@@ -85,26 +86,35 @@ public class DefaultServerWebExchange implements ServerWebExchange {
* Alternate constructor with a WebSessionManager parameter. * Alternate constructor with a WebSessionManager parameter.
*/ */
public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response, public DefaultServerWebExchange(ServerHttpRequest request, ServerHttpResponse response,
WebSessionManager sessionManager) { WebSessionManager sessionManager, ServerCodecConfigurer codecConfigurer) {
Assert.notNull(request, "'request' is required"); Assert.notNull(request, "'request' is required");
Assert.notNull(response, "'response' is required"); Assert.notNull(response, "'response' is required");
Assert.notNull(response, "'sessionManager' is required"); Assert.notNull(sessionManager, "'sessionManager' is required");
Assert.notNull(response, "'formReader' is required"); Assert.notNull(codecConfigurer, "'codecConfigurer' is required");
this.request = request; this.request = request;
this.response = response; this.response = response;
this.sessionMono = sessionManager.getSession(this).cache(); this.sessionMono = sessionManager.getSession(this).cache();
this.formDataMono = initFormData(request); this.formDataMono = initFormData(request, codecConfigurer);
this.requestParamsMono = initRequestParams(request, this.formDataMono); this.requestParamsMono = initRequestParams(request, this.formDataMono);
} }
private static Mono<MultiValueMap<String, String>> initFormData(ServerHttpRequest request) { @SuppressWarnings("unchecked")
private static Mono<MultiValueMap<String, String>> initFormData(
ServerHttpRequest request, ServerCodecConfigurer codecConfigurer) {
MediaType contentType; MediaType contentType;
try { try {
contentType = request.getHeaders().getContentType(); contentType = request.getHeaders().getContentType();
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) { if (APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType)) {
return FORM_READER return ((HttpMessageReader<MultiValueMap<String, String>>)codecConfigurer
.getReaders()
.stream()
.filter(messageReader -> messageReader.canRead(FORM_DATA_VALUE_TYPE, APPLICATION_FORM_URLENCODED))
.findFirst()
.orElseThrow(() -> new IllegalStateException("Could not find HttpMessageReader that supports " + APPLICATION_FORM_URLENCODED)))
.readMono(FORM_DATA_VALUE_TYPE, request, Collections.emptyMap()) .readMono(FORM_DATA_VALUE_TYPE, request, Collections.emptyMap())
.switchIfEmpty(EMPTY_FORM_DATA) .switchIfEmpty(EMPTY_FORM_DATA)
.cache(); .cache();
......
...@@ -21,6 +21,7 @@ import org.apache.commons.logging.LogFactory; ...@@ -21,6 +21,7 @@ import org.apache.commons.logging.LogFactory;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse; import org.springframework.http.server.reactive.ServerHttpResponse;
...@@ -38,6 +39,7 @@ import org.springframework.web.server.session.WebSessionManager; ...@@ -38,6 +39,7 @@ import org.springframework.web.server.session.WebSessionManager;
* then invokes the target {@code WebHandler}. * then invokes the target {@code WebHandler}.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @since 5.0 * @since 5.0
*/ */
public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler { public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHandler {
...@@ -46,6 +48,8 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa ...@@ -46,6 +48,8 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
private WebSessionManager sessionManager = new DefaultWebSessionManager(); private WebSessionManager sessionManager = new DefaultWebSessionManager();
private ServerCodecConfigurer codecConfigurer;
public HttpWebHandlerAdapter(WebHandler delegate) { public HttpWebHandlerAdapter(WebHandler delegate) {
super(delegate); super(delegate);
...@@ -71,6 +75,24 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa ...@@ -71,6 +75,24 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
return this.sessionManager; return this.sessionManager;
} }
/**
* Configure a custom {@link ServerCodecConfigurer}. The provided instance is set on
* each created {@link DefaultServerWebExchange}.
* <p>By default this is set to {@link ServerCodecConfigurer#create()}.
* @param codecConfigurer the codec configurer to use
*/
public void setCodecConfigurer(ServerCodecConfigurer codecConfigurer) {
Assert.notNull(codecConfigurer, "ServerCodecConfigurer must not be null");
this.codecConfigurer = codecConfigurer;
}
/**
* Return the configured {@link ServerCodecConfigurer}.
*/
public ServerCodecConfigurer getCodecConfigurer() {
return this.codecConfigurer != null ? this.codecConfigurer : ServerCodecConfigurer.create();
}
@Override @Override
public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) { public Mono<Void> handle(ServerHttpRequest request, ServerHttpResponse response) {
...@@ -87,7 +109,7 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa ...@@ -87,7 +109,7 @@ public class HttpWebHandlerAdapter extends WebHandlerDecorator implements HttpHa
} }
protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) { protected ServerWebExchange createExchange(ServerHttpRequest request, ServerHttpResponse response) {
return new DefaultServerWebExchange(request, response, this.sessionManager); return new DefaultServerWebExchange(request, response, this.sessionManager, getCodecConfigurer());
} }
} }
...@@ -22,6 +22,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; ...@@ -22,6 +22,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
...@@ -51,6 +52,7 @@ import org.springframework.web.server.session.WebSessionManager; ...@@ -51,6 +52,7 @@ import org.springframework.web.server.session.WebSessionManager;
* {@link #applicationContext(ApplicationContext)}, or a mix of both. * {@link #applicationContext(ApplicationContext)}, or a mix of both.
* *
* @author Rossen Stoyanchev * @author Rossen Stoyanchev
* @author Sebastien Deleuze
* @since 5.0 * @since 5.0
* @see HttpWebHandlerAdapter * @see HttpWebHandlerAdapter
*/ */
...@@ -62,6 +64,9 @@ public class WebHttpHandlerBuilder { ...@@ -62,6 +64,9 @@ public class WebHttpHandlerBuilder {
/** Well-known name for the WebSessionManager in the bean factory. */ /** Well-known name for the WebSessionManager in the bean factory. */
public static final String WEB_SESSION_MANAGER_BEAN_NAME = "webSessionManager"; public static final String WEB_SESSION_MANAGER_BEAN_NAME = "webSessionManager";
/** Well-known name for the ServerCodecConfigurer in the bean factory. */
public static final String SERVER_CODEC_CONFIGURER_BEAN_NAME = "serverCodecConfigurer";
private final WebHandler webHandler; private final WebHandler webHandler;
...@@ -71,6 +76,8 @@ public class WebHttpHandlerBuilder { ...@@ -71,6 +76,8 @@ public class WebHttpHandlerBuilder {
private WebSessionManager sessionManager; private WebSessionManager sessionManager;
private ServerCodecConfigurer codecConfigurer;
/** /**
* Private constructor. * Private constructor.
...@@ -102,6 +109,8 @@ public class WebHttpHandlerBuilder { ...@@ -102,6 +109,8 @@ public class WebHttpHandlerBuilder {
* ordered. * ordered.
* <li>{@link WebSessionManager} [0..1] -- looked up by the name * <li>{@link WebSessionManager} [0..1] -- looked up by the name
* {@link #WEB_SESSION_MANAGER_BEAN_NAME}. * {@link #WEB_SESSION_MANAGER_BEAN_NAME}.
* <li>{@link ServerCodecConfigurer} [0..1] -- looked up by the name
* {@link #SERVER_CODEC_CONFIGURER_BEAN_NAME}.
* </ul> * </ul>
* @param context the application context to use for the lookup * @param context the application context to use for the lookup
* @return the prepared builder * @return the prepared builder
...@@ -126,6 +135,14 @@ public class WebHttpHandlerBuilder { ...@@ -126,6 +135,14 @@ public class WebHttpHandlerBuilder {
// Fall back on default // Fall back on default
} }
try {
builder.codecConfigurer(
context.getBean(SERVER_CODEC_CONFIGURER_BEAN_NAME, ServerCodecConfigurer.class));
}
catch (NoSuchBeanDefinitionException ex) {
// Fall back on default
}
return builder; return builder;
} }
...@@ -184,6 +201,16 @@ public class WebHttpHandlerBuilder { ...@@ -184,6 +201,16 @@ public class WebHttpHandlerBuilder {
return this; return this;
} }
/**
* Configure the {@link ServerCodecConfigurer} to set on the
* {@link ServerWebExchange WebServerExchange}.
* @param codecConfigurer the codec configurer
*/
public WebHttpHandlerBuilder codecConfigurer(ServerCodecConfigurer codecConfigurer) {
this.codecConfigurer = codecConfigurer;
return this;
}
/** /**
* Build the {@link HttpHandler}. * Build the {@link HttpHandler}.
...@@ -199,6 +226,9 @@ public class WebHttpHandlerBuilder { ...@@ -199,6 +226,9 @@ public class WebHttpHandlerBuilder {
if (this.sessionManager != null) { if (this.sessionManager != null) {
adapted.setSessionManager(this.sessionManager); adapted.setSessionManager(this.sessionManager);
} }
if (this.codecConfigurer != null) {
adapted.setCodecConfigurer(this.codecConfigurer);
}
return adapted; return adapted;
} }
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
package org.springframework.mock.http.server.reactive.test; package org.springframework.mock.http.server.reactive.test;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.server.ServerWebExchangeDecorator; import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.adapter.DefaultServerWebExchange; import org.springframework.web.server.adapter.DefaultServerWebExchange;
import org.springframework.web.server.session.DefaultWebSessionManager; import org.springframework.web.server.session.DefaultWebSessionManager;
...@@ -35,7 +36,7 @@ public class MockServerWebExchange extends ServerWebExchangeDecorator { ...@@ -35,7 +36,7 @@ public class MockServerWebExchange extends ServerWebExchangeDecorator {
public MockServerWebExchange(MockServerHttpRequest request) { public MockServerWebExchange(MockServerHttpRequest request) {
super(new DefaultServerWebExchange( super(new DefaultServerWebExchange(
request, new MockServerHttpResponse(), new DefaultWebSessionManager())); request, new MockServerHttpResponse(), new DefaultWebSessionManager(), ServerCodecConfigurer.create()));
} }
......
...@@ -26,6 +26,7 @@ import java.util.List; ...@@ -26,6 +26,7 @@ import java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebExchange;
...@@ -59,7 +60,7 @@ public class DefaultWebSessionManagerTests { ...@@ -59,7 +60,7 @@ public class DefaultWebSessionManagerTests {
MockServerHttpRequest request = MockServerHttpRequest.get("/path").build(); MockServerHttpRequest request = MockServerHttpRequest.get("/path").build();
MockServerHttpResponse response = new MockServerHttpResponse(); MockServerHttpResponse response = new MockServerHttpResponse();
this.exchange = new DefaultServerWebExchange(request, response, this.manager); this.exchange = new DefaultServerWebExchange(request, response, this.manager, ServerCodecConfigurer.create());
} }
......
...@@ -81,8 +81,6 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { ...@@ -81,8 +81,6 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
private PathMatchConfigurer pathMatchConfigurer; private PathMatchConfigurer pathMatchConfigurer;
private ServerCodecConfigurer messageCodecsConfigurer;
private ApplicationContext applicationContext; private ApplicationContext applicationContext;
...@@ -242,7 +240,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { ...@@ -242,7 +240,7 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
@Bean @Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() { public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter(); RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
adapter.setMessageCodecConfigurer(getMessageCodecsConfigurer()); adapter.setMessageCodecConfigurer(serverCodecConfigurer());
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer()); adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
adapter.setReactiveAdapterRegistry(webFluxAdapterRegistry()); adapter.setReactiveAdapterRegistry(webFluxAdapterRegistry());
...@@ -267,16 +265,15 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { ...@@ -267,16 +265,15 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
} }
/** /**
* Main method to access the configurer for HTTP message readers and writers. * Return the configurer for HTTP message readers and writers.
* <p>Use {@link #configureHttpMessageCodecs(ServerCodecConfigurer)} to * <p>Use {@link #configureHttpMessageCodecs(ServerCodecConfigurer)} to
* configure the readers and writers. * configure the readers and writers.
*/ */
protected final ServerCodecConfigurer getMessageCodecsConfigurer() { @Bean
if (this.messageCodecsConfigurer == null) { public ServerCodecConfigurer serverCodecConfigurer() {
this.messageCodecsConfigurer = ServerCodecConfigurer.create(); ServerCodecConfigurer serverCodecConfigurer = ServerCodecConfigurer.create();
configureHttpMessageCodecs(this.getMessageCodecsConfigurer()); configureHttpMessageCodecs(serverCodecConfigurer);
} return serverCodecConfigurer;
return this.messageCodecsConfigurer;
} }
/** /**
...@@ -372,13 +369,13 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware { ...@@ -372,13 +369,13 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
@Bean @Bean
public ResponseEntityResultHandler responseEntityResultHandler() { public ResponseEntityResultHandler responseEntityResultHandler() {
return new ResponseEntityResultHandler(getMessageCodecsConfigurer().getWriters(), return new ResponseEntityResultHandler(serverCodecConfigurer().getWriters(),
webFluxContentTypeResolver(), webFluxAdapterRegistry()); webFluxContentTypeResolver(), webFluxAdapterRegistry());
} }
@Bean @Bean
public ResponseBodyResultHandler responseBodyResultHandler() { public ResponseBodyResultHandler responseBodyResultHandler() {
return new ResponseBodyResultHandler(getMessageCodecsConfigurer().getWriters(), return new ResponseBodyResultHandler(serverCodecConfigurer().getWriters(),
webFluxContentTypeResolver(), webFluxAdapterRegistry()); webFluxContentTypeResolver(), webFluxAdapterRegistry());
} }
......
...@@ -125,13 +125,13 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr ...@@ -125,13 +125,13 @@ public class DispatcherHandlerIntegrationTests extends AbstractHttpHandlerIntegr
new HandlerStrategies() { new HandlerStrategies() {
@Override @Override
public Supplier<Stream<HttpMessageReader<?>>> messageReaders() { public Supplier<Stream<HttpMessageReader<?>>> messageReaders() {
return () -> getMessageCodecsConfigurer().getReaders().stream() return () -> serverCodecConfigurer().getReaders().stream()
.map(reader -> (HttpMessageReader<?>) reader); .map(reader -> (HttpMessageReader<?>) reader);
} }
@Override @Override
public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() { public Supplier<Stream<HttpMessageWriter<?>>> messageWriters() {
return () -> getMessageCodecsConfigurer().getWriters().stream() return () -> serverCodecConfigurer().getWriters().stream()
.map(writer -> (HttpMessageWriter<?>) writer); .map(writer -> (HttpMessageWriter<?>) writer);
} }
......
...@@ -31,6 +31,7 @@ import org.springframework.core.MethodParameter; ...@@ -31,6 +31,7 @@ import org.springframework.core.MethodParameter;
import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest;
import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse; import org.springframework.mock.http.server.reactive.test.MockServerHttpResponse;
...@@ -81,7 +82,7 @@ public class SessionAttributeMethodArgumentResolverTests { ...@@ -81,7 +82,7 @@ public class SessionAttributeMethodArgumentResolverTests {
WebSessionManager sessionManager = new MockWebSessionManager(this.session); WebSessionManager sessionManager = new MockWebSessionManager(this.session);
ServerHttpRequest request = MockServerHttpRequest.get("/").build(); ServerHttpRequest request = MockServerHttpRequest.get("/").build();
this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager); this.exchange = new DefaultServerWebExchange(request, new MockServerHttpResponse(), sessionManager, ServerCodecConfigurer.create());
this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithSessionAttribute", (Class<?>[]) null); this.handleMethod = ReflectionUtils.findMethod(getClass(), "handleWithSessionAttribute", (Class<?>[]) null);
} }
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册