@EnableWebFlux setup supports WebSocketHandler
Closes gh-22587
This commit is contained in:
parent
8f369ffed5
commit
591ab8a00a
|
|
@ -27,6 +27,7 @@ import org.springframework.validation.MessageCodesResolver;
|
||||||
import org.springframework.validation.Validator;
|
import org.springframework.validation.Validator;
|
||||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
||||||
|
import org.springframework.web.reactive.socket.server.WebSocketService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A subclass of {@code WebFluxConfigurationSupport} that detects and delegates
|
* A subclass of {@code WebFluxConfigurationSupport} that detects and delegates
|
||||||
|
|
@ -98,6 +99,12 @@ public class DelegatingWebFluxConfiguration extends WebFluxConfigurationSupport
|
||||||
return (messageCodesResolver != null ? messageCodesResolver : super.getMessageCodesResolver());
|
return (messageCodesResolver != null ? messageCodesResolver : super.getMessageCodesResolver());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected WebSocketService getWebSocketService() {
|
||||||
|
WebSocketService service = this.configurers.getWebSocketService();
|
||||||
|
return (service != null ? service : super.getWebSocketService());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configureViewResolvers(ViewResolverRegistry registry) {
|
protected void configureViewResolvers(ViewResolverRegistry registry) {
|
||||||
this.configurers.configureViewResolvers(registry);
|
this.configurers.configureViewResolvers(registry);
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,8 @@ import org.springframework.web.reactive.result.method.annotation.ResponseBodyRes
|
||||||
import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler;
|
import org.springframework.web.reactive.result.method.annotation.ResponseEntityResultHandler;
|
||||||
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
|
import org.springframework.web.reactive.result.view.ViewResolutionResultHandler;
|
||||||
import org.springframework.web.reactive.result.view.ViewResolver;
|
import org.springframework.web.reactive.result.view.ViewResolver;
|
||||||
|
import org.springframework.web.reactive.socket.server.WebSocketService;
|
||||||
|
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
import org.springframework.web.server.WebExceptionHandler;
|
import org.springframework.web.server.WebExceptionHandler;
|
||||||
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
|
import org.springframework.web.server.i18n.AcceptHeaderLocaleContextResolver;
|
||||||
|
|
@ -431,6 +433,24 @@ public class WebFluxConfigurationSupport implements ApplicationContextAware {
|
||||||
return new SimpleHandlerAdapter();
|
return new SimpleHandlerAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebSocketHandlerAdapter webFluxWebSocketHandlerAdapter() {
|
||||||
|
WebSocketService service = getWebSocketService();
|
||||||
|
WebSocketHandlerAdapter adapter = (service != null ?
|
||||||
|
new WebSocketHandlerAdapter(service) : new WebSocketHandlerAdapter());
|
||||||
|
|
||||||
|
// For backwards compatibility, lower the (default) priority
|
||||||
|
int defaultOrder = adapter.getOrder();
|
||||||
|
adapter.setOrder(defaultOrder + 1);
|
||||||
|
|
||||||
|
return adapter;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
protected WebSocketService getWebSocketService() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public ResponseEntityResultHandler responseEntityResultHandler(
|
public ResponseEntityResultHandler responseEntityResultHandler(
|
||||||
@Qualifier("webFluxAdapterRegistry") ReactiveAdapterRegistry reactiveAdapterRegistry,
|
@Qualifier("webFluxAdapterRegistry") ReactiveAdapterRegistry reactiveAdapterRegistry,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2020 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.
|
||||||
|
|
@ -25,6 +25,7 @@ import org.springframework.validation.MessageCodesResolver;
|
||||||
import org.springframework.validation.Validator;
|
import org.springframework.validation.Validator;
|
||||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
||||||
|
import org.springframework.web.reactive.socket.server.WebSocketService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines callback methods to customize the configuration for WebFlux
|
* Defines callback methods to customize the configuration for WebFlux
|
||||||
|
|
@ -124,6 +125,18 @@ public interface WebFluxConfigurer {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provide the {@link WebSocketService} to create
|
||||||
|
* {@link org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter}
|
||||||
|
* with. This can be used to configure server-specific properties through the
|
||||||
|
* {@link org.springframework.web.reactive.socket.server.RequestUpgradeStrategy}.
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
default WebSocketService getWebSocketService() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configure view resolution for rendering responses with a view and a model,
|
* Configure view resolution for rendering responses with a view and a model,
|
||||||
* where the view is typically an HTML template but could also be based on
|
* where the view is typically an HTML template but could also be based on
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2017 the original author or authors.
|
* Copyright 2002-2020 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.
|
||||||
|
|
@ -30,6 +30,7 @@ import org.springframework.validation.MessageCodesResolver;
|
||||||
import org.springframework.validation.Validator;
|
import org.springframework.validation.Validator;
|
||||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||||
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
import org.springframework.web.reactive.result.method.annotation.ArgumentResolverConfigurer;
|
||||||
|
import org.springframework.web.reactive.socket.server.WebSocketService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link WebFluxConfigurer} that delegates to one or more others.
|
* A {@link WebFluxConfigurer} that delegates to one or more others.
|
||||||
|
|
@ -70,6 +71,12 @@ public class WebFluxConfigurerComposite implements WebFluxConfigurer {
|
||||||
this.delegates.forEach(delegate -> delegate.addResourceHandlers(registry));
|
this.delegates.forEach(delegate -> delegate.addResourceHandlers(registry));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public WebSocketService getWebSocketService() {
|
||||||
|
return createSingleBean(WebFluxConfigurer::getWebSocketService, WebSocketService.class);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
|
public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) {
|
||||||
this.delegates.forEach(delegate -> delegate.configureArgumentResolvers(configurer));
|
this.delegates.forEach(delegate -> delegate.configureArgumentResolvers(configurer));
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2018 the original author or authors.
|
* Copyright 2002-2020 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,8 +17,8 @@ package org.springframework.web.reactive.socket.server.support;
|
||||||
|
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import org.springframework.core.Ordered;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.web.reactive.DispatcherHandler;
|
|
||||||
import org.springframework.web.reactive.HandlerAdapter;
|
import org.springframework.web.reactive.HandlerAdapter;
|
||||||
import org.springframework.web.reactive.HandlerResult;
|
import org.springframework.web.reactive.HandlerResult;
|
||||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||||
|
|
@ -26,18 +26,30 @@ import org.springframework.web.reactive.socket.server.WebSocketService;
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
import org.springframework.web.server.ServerWebExchange;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@link HandlerAdapter} that allows using a {@link WebSocketHandler} with the
|
* {@code HandlerAdapter} that allows
|
||||||
* generic {@link DispatcherHandler} mapping URLs directly to such handlers.
|
* {@link org.springframework.web.reactive.DispatcherHandler} to support
|
||||||
* Requests are handled by delegating to the configured {@link WebSocketService}
|
* handlers of type {@link WebSocketHandler} with such handlers mapped to
|
||||||
* which by default is {@link HandshakeWebSocketService}.
|
* URL patterns via
|
||||||
|
* {@link org.springframework.web.reactive.handler.SimpleUrlHandlerMapping}.
|
||||||
|
*
|
||||||
|
* <p>Requests are handled by delegating to a
|
||||||
|
* {@link WebSocketService}, by default {@link HandshakeWebSocketService},
|
||||||
|
* which checks the WebSocket handshake request parameters, upgrades to a
|
||||||
|
* WebSocket interaction, and uses the {@link WebSocketHandler} to handle it.
|
||||||
|
*
|
||||||
|
* <p>As of 5.3 the WebFlux Java configuration, imported via
|
||||||
|
* {@code @EnableWebFlux}, includes a declaration of this adapter and therefore
|
||||||
|
* it no longer needs to be present in application configuration.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
* @since 5.0
|
* @since 5.0
|
||||||
*/
|
*/
|
||||||
public class WebSocketHandlerAdapter implements HandlerAdapter {
|
public class WebSocketHandlerAdapter implements HandlerAdapter, Ordered {
|
||||||
|
|
||||||
private final WebSocketService webSocketService;
|
private final WebSocketService webSocketService;
|
||||||
|
|
||||||
|
private int order = 2;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default constructor that creates and uses a
|
* Default constructor that creates and uses a
|
||||||
|
|
@ -56,6 +68,25 @@ public class WebSocketHandlerAdapter implements HandlerAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the order value for this adapter.
|
||||||
|
* <p>By default this is set to 2.
|
||||||
|
* @param order the value to set to
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
public void setOrder(int order) {
|
||||||
|
this.order = order;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link #setOrder(int) configured} order for this instance.
|
||||||
|
* @since 5.3
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public int getOrder() {
|
||||||
|
return this.order;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the configured {@code WebSocketService} to handle requests.
|
* Return the configured {@code WebSocketService} to handle requests.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,14 @@ import org.springframework.validation.Validator;
|
||||||
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
|
||||||
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
import org.springframework.web.bind.support.ConfigurableWebBindingInitializer;
|
||||||
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder;
|
||||||
|
import org.springframework.web.reactive.socket.server.WebSocketService;
|
||||||
|
import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.BDDMockito.given;
|
import static org.mockito.BDDMockito.given;
|
||||||
import static org.mockito.BDDMockito.willAnswer;
|
import static org.mockito.BDDMockito.willAnswer;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
import static org.mockito.Mockito.verify;
|
import static org.mockito.Mockito.verify;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,6 +76,7 @@ public class DelegatingWebFluxConfigurationTests {
|
||||||
delegatingConfig.setApplicationContext(new StaticApplicationContext());
|
delegatingConfig.setApplicationContext(new StaticApplicationContext());
|
||||||
given(webFluxConfigurer.getValidator()).willReturn(null);
|
given(webFluxConfigurer.getValidator()).willReturn(null);
|
||||||
given(webFluxConfigurer.getMessageCodesResolver()).willReturn(null);
|
given(webFluxConfigurer.getMessageCodesResolver()).willReturn(null);
|
||||||
|
given(webFluxConfigurer.getWebSocketService()).willReturn(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -125,6 +129,17 @@ public class DelegatingWebFluxConfigurationTests {
|
||||||
verify(webFluxConfigurer).configurePathMatching(any(PathMatchConfigurer.class));
|
verify(webFluxConfigurer).configurePathMatching(any(PathMatchConfigurer.class));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void webSocketService() {
|
||||||
|
WebSocketService service = mock(WebSocketService.class);
|
||||||
|
given(webFluxConfigurer.getWebSocketService()).willReturn(service);
|
||||||
|
|
||||||
|
delegatingConfig.setConfigurers(Collections.singletonList(webFluxConfigurer));
|
||||||
|
WebSocketHandlerAdapter adapter = delegatingConfig.webFluxWebSocketHandlerAdapter();
|
||||||
|
|
||||||
|
assertThat(adapter.getWebSocketService()).isSameAs(service);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void responseBodyResultHandler() {
|
public void responseBodyResultHandler() {
|
||||||
delegatingConfig.setConfigurers(Collections.singletonList(webFluxConfigurer));
|
delegatingConfig.setConfigurers(Collections.singletonList(webFluxConfigurer));
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ The following example shows how to do so:
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
Then you can map it to a URL and add a `WebSocketHandlerAdapter`, as the following example shows:
|
Then you can map it to a URL:
|
||||||
|
|
||||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||||
.Java
|
.Java
|
||||||
|
|
@ -70,11 +70,6 @@ Then you can map it to a URL and add a `WebSocketHandlerAdapter`, as the followi
|
||||||
|
|
||||||
return new SimpleUrlHandlerMapping(map, order);
|
return new SimpleUrlHandlerMapping(map, order);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
public WebSocketHandlerAdapter handlerAdapter() {
|
|
||||||
return new WebSocketHandlerAdapter();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||||
|
|
@ -90,6 +85,34 @@ Then you can map it to a URL and add a `WebSocketHandlerAdapter`, as the followi
|
||||||
|
|
||||||
return SimpleUrlHandlerMapping(map, order)
|
return SimpleUrlHandlerMapping(map, order)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
If using the <<web-reactive.adoc#webflux-config, WebFlux Config>> there is nothing
|
||||||
|
further to do, or otherwise if not using the WebFlux config you'll need to declare a
|
||||||
|
`WebSocketHandlerAdapter` as shown below:
|
||||||
|
|
||||||
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||||
|
.Java
|
||||||
|
----
|
||||||
|
@Configuration
|
||||||
|
class WebConfig {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public WebSocketHandlerAdapter handlerAdapter() {
|
||||||
|
return new WebSocketHandlerAdapter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||||
|
.Kotlin
|
||||||
|
----
|
||||||
|
@Configuration
|
||||||
|
class WebConfig {
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
fun handlerAdapter() = WebSocketHandlerAdapter()
|
fun handlerAdapter() = WebSocketHandlerAdapter()
|
||||||
|
|
@ -333,9 +356,11 @@ into the attributes of the `WebSocketSession`.
|
||||||
=== Server Configation
|
=== Server Configation
|
||||||
[.small]#<<web.adoc#websocket-server-runtime-configuration, Same as in the Servlet stack>>#
|
[.small]#<<web.adoc#websocket-server-runtime-configuration, Same as in the Servlet stack>>#
|
||||||
|
|
||||||
The `RequestUpgradeStrategy` for each server exposes WebSocket-related configuration
|
The `RequestUpgradeStrategy` for each server exposes configuration specific to the
|
||||||
options available for the underlying WebSocket engine. The following example sets
|
underlying WebSocket server engine. When using the WebFlux Java config you can customize
|
||||||
WebSocket options when running on Tomcat:
|
such properties as shown in the corresponding section of the
|
||||||
|
<<web-reactive.adoc#webflux-config-websocket-service, WebFlux Config>>, or otherwise if
|
||||||
|
not using the WebFlux config, use the below:
|
||||||
|
|
||||||
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||||
.Java
|
.Java
|
||||||
|
|
|
||||||
|
|
@ -4289,6 +4289,53 @@ reliance on it.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[[webflux-config-websocket-service]]
|
||||||
|
=== WebSocketService
|
||||||
|
|
||||||
|
The WebFlux Java config declares of a `WebSocketHandlerAdapter` bean which provides
|
||||||
|
support for the invocation of WebSocket handlers. That means all that remains to do in
|
||||||
|
order to handle a WebSocket handshake request is to map a `WebSocketHandler` to a URL
|
||||||
|
via `SimpleUrlHandlerMapping`.
|
||||||
|
|
||||||
|
In some cases it may be necessary to create the `WebSocketHandlerAdapter` bean with a
|
||||||
|
provided `WebSocketService` service which allows configuring WebSocket server properties.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
|
||||||
|
.Java
|
||||||
|
----
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFlux
|
||||||
|
public class WebConfig implements WebFluxConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public WebSocketService getWebSocketService() {
|
||||||
|
TomcatRequestUpgradeStrategy strategy = new TomcatRequestUpgradeStrategy();
|
||||||
|
strategy.setMaxSessionIdleTimeout(0L);
|
||||||
|
return new HandshakeWebSocketService(strategy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
|
||||||
|
.Kotlin
|
||||||
|
----
|
||||||
|
@Configuration
|
||||||
|
@EnableWebFlux
|
||||||
|
class WebConfig : WebFluxConfigurer {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
fun webSocketService(): WebSocketService {
|
||||||
|
val strategy = TomcatRequestUpgradeStrategy().apply {
|
||||||
|
setMaxSessionIdleTimeout(0L)
|
||||||
|
}
|
||||||
|
return HandshakeWebSocketService(strategy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[[webflux-config-advanced-java]]
|
[[webflux-config-advanced-java]]
|
||||||
=== Advanced Configuration Mode
|
=== Advanced Configuration Mode
|
||||||
[.small]#<<web.adoc#mvc-config-advanced-java, Web MVC>>#
|
[.small]#<<web.adoc#mvc-config-advanced-java, Web MVC>>#
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue