Browse Source
This commit introduces Micrometer as an API dependency to the spring-webflux module. Micrometer is used here to instrument `WebClient` and record `Observation` for HTTP client exchanges.observability
Brian Clozel
2 years ago
10 changed files with 624 additions and 2 deletions
@ -0,0 +1,134 @@
@@ -0,0 +1,134 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.web.reactive.function.client; |
||||
|
||||
import io.micrometer.common.docs.KeyName; |
||||
import io.micrometer.observation.Observation; |
||||
import io.micrometer.observation.docs.DocumentedObservation; |
||||
|
||||
/** |
||||
* Documented {@link io.micrometer.common.KeyValue KeyValues} for the HTTP client observations. |
||||
* <p>This class is used by automated tools to document KeyValues attached to the HTTP client observations. |
||||
* @author Brian Clozel |
||||
* @since 6.0 |
||||
*/ |
||||
public enum ClientObservation implements DocumentedObservation { |
||||
|
||||
/** |
||||
* Observation created for an client HTTP exchange. |
||||
*/ |
||||
HTTP_REQUEST { |
||||
@Override |
||||
public Class<? extends Observation.ObservationConvention<? extends Observation.Context>> getDefaultConvention() { |
||||
return DefaultClientObservationConvention.class; |
||||
} |
||||
|
||||
@Override |
||||
public KeyName[] getLowCardinalityKeyNames() { |
||||
return ClientObservation.LowCardinalityKeyNames.values(); |
||||
} |
||||
|
||||
@Override |
||||
public KeyName[] getHighCardinalityKeyNames() { |
||||
return ClientObservation.HighCardinalityKeyNames.values(); |
||||
} |
||||
|
||||
}; |
||||
|
||||
public enum LowCardinalityKeyNames implements KeyName { |
||||
|
||||
/** |
||||
* Name of HTTP request method or {@code "None"} if the request could not be created. |
||||
*/ |
||||
METHOD { |
||||
@Override |
||||
public String asString() { |
||||
return "method"; |
||||
} |
||||
|
||||
}, |
||||
|
||||
/** |
||||
* URI template used for HTTP request, or {@code ""} if none was provided. |
||||
*/ |
||||
URI { |
||||
@Override |
||||
public String asString() { |
||||
return "uri"; |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* HTTP response raw status code, or {@code "IO_ERROR"} in case of {@code IOException}, |
||||
* or {@code "CLIENT_ERROR"} if no response was received. |
||||
*/ |
||||
STATUS { |
||||
@Override |
||||
public String asString() { |
||||
return "status"; |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* Name of the exception thrown during the exchange, or {@code "None"} if no exception happened. |
||||
*/ |
||||
EXCEPTION { |
||||
@Override |
||||
public String asString() { |
||||
return "exception"; |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* Outcome of the HTTP client exchange. |
||||
* |
||||
* @see org.springframework.http.HttpStatus.Series |
||||
*/ |
||||
OUTCOME { |
||||
@Override |
||||
public String asString() { |
||||
return "outcome"; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
public enum HighCardinalityKeyNames implements KeyName { |
||||
|
||||
/** |
||||
* HTTP request URI. |
||||
*/ |
||||
URI_EXPANDED { |
||||
@Override |
||||
public String asString() { |
||||
return "uri.expanded"; |
||||
} |
||||
}, |
||||
|
||||
/** |
||||
* Client name derived from the request URI host. |
||||
*/ |
||||
CLIENT_NAME { |
||||
@Override |
||||
public String asString() { |
||||
return "client.name"; |
||||
} |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.web.reactive.function.client; |
||||
|
||||
import io.micrometer.observation.transport.RequestReplySenderContext; |
||||
|
||||
import org.springframework.lang.Nullable; |
||||
|
||||
/** |
||||
* Context that holds information for metadata collection |
||||
* during the HTTP client observations. |
||||
* |
||||
* @author Brian Clozel |
||||
* @since 6.0 |
||||
*/ |
||||
public class ClientObservationContext extends RequestReplySenderContext<ClientRequest, ClientResponse> { |
||||
|
||||
@Nullable |
||||
private String uriTemplate; |
||||
|
||||
|
||||
public ClientObservationContext() { |
||||
super(ClientObservationContext::setRequestHeader); |
||||
} |
||||
|
||||
private static void setRequestHeader(@Nullable ClientRequest request, String name, String value) { |
||||
if (request != null) { |
||||
request.headers().set(name, value); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Return the URI template used for the current client exchange, {@code null} if none was used. |
||||
*/ |
||||
@Nullable |
||||
public String getUriTemplate() { |
||||
return this.uriTemplate; |
||||
} |
||||
|
||||
/** |
||||
* Set the URI template used for the current client exchange. |
||||
*/ |
||||
public void setUriTemplate(@Nullable String uriTemplate) { |
||||
this.uriTemplate = uriTemplate; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,33 @@
@@ -0,0 +1,33 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.web.reactive.function.client; |
||||
|
||||
import io.micrometer.observation.Observation; |
||||
|
||||
/** |
||||
* Interface for an {@link Observation.ObservationConvention} related to client HTTP exchanges. |
||||
* @author Brian Clozel |
||||
* @since 6.0 |
||||
*/ |
||||
public interface ClientObservationConvention extends Observation.ObservationConvention<ClientObservationContext> { |
||||
|
||||
@Override |
||||
default boolean supportsContext(Observation.Context context) { |
||||
return context instanceof ClientObservationContext; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,123 @@
@@ -0,0 +1,123 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.web.reactive.function.client; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import io.micrometer.common.KeyValue; |
||||
import io.micrometer.common.KeyValues; |
||||
import io.micrometer.observation.Observation; |
||||
|
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.util.StringUtils; |
||||
|
||||
/** |
||||
* Default implementation for a {@code WebClient} {@link Observation.ObservationConvention}, |
||||
* extracting information from the {@link ClientObservationContext}. |
||||
* |
||||
* @author Brian Clozel |
||||
* @since 6.0 |
||||
*/ |
||||
public class DefaultClientObservationConvention implements ClientObservationConvention { |
||||
|
||||
private static final KeyValue URI_NONE = KeyValue.of(ClientObservation.LowCardinalityKeyNames.URI, "none"); |
||||
|
||||
private static final KeyValue METHOD_NONE = KeyValue.of(ClientObservation.LowCardinalityKeyNames.METHOD, "none"); |
||||
|
||||
private static final KeyValue EXCEPTION_NONE = KeyValue.of(ClientObservation.LowCardinalityKeyNames.EXCEPTION, "none"); |
||||
|
||||
private static final KeyValue OUTCOME_UNKNOWN = KeyValue.of(ClientObservation.LowCardinalityKeyNames.OUTCOME, "UNKNOWN"); |
||||
|
||||
@Override |
||||
public String getName() { |
||||
return "http.client.requests"; |
||||
} |
||||
|
||||
@Override |
||||
public KeyValues getLowCardinalityKeyValues(ClientObservationContext context) { |
||||
return KeyValues.of(uri(context), method(context), status(context), exception(context), outcome(context)); |
||||
} |
||||
|
||||
protected KeyValue uri(ClientObservationContext context) { |
||||
if (context.getUriTemplate() != null) { |
||||
return KeyValue.of(ClientObservation.LowCardinalityKeyNames.URI, context.getUriTemplate()); |
||||
} |
||||
return URI_NONE; |
||||
} |
||||
|
||||
protected KeyValue method(ClientObservationContext context) { |
||||
if (context.getCarrier() != null) { |
||||
return KeyValue.of(ClientObservation.LowCardinalityKeyNames.METHOD, context.getCarrier().method().name()); |
||||
} |
||||
else { |
||||
return METHOD_NONE; |
||||
} |
||||
} |
||||
|
||||
protected KeyValue status(ClientObservationContext context) { |
||||
return KeyValue.of(ClientObservation.LowCardinalityKeyNames.STATUS, getStatusMessage(context)); |
||||
} |
||||
|
||||
private String getStatusMessage(ClientObservationContext context) { |
||||
if (context.getResponse() != null) { |
||||
return String.valueOf(context.getResponse().statusCode().value()); |
||||
} |
||||
if (context.getError().isPresent()) { |
||||
return (context.getError().get() instanceof IOException) ? "IO_ERROR" : "CLIENT_ERROR"; |
||||
} |
||||
return "CLIENT_ERROR"; |
||||
} |
||||
|
||||
protected KeyValue exception(ClientObservationContext context) { |
||||
return context.getError().map(exception -> { |
||||
String simpleName = exception.getClass().getSimpleName(); |
||||
return KeyValue.of(ClientObservation.LowCardinalityKeyNames.EXCEPTION, |
||||
StringUtils.hasText(simpleName) ? simpleName : exception.getClass().getName()); |
||||
}).orElse(EXCEPTION_NONE); |
||||
} |
||||
|
||||
protected static KeyValue outcome(ClientObservationContext context) { |
||||
if (context.getResponse() != null) { |
||||
HttpStatus status = HttpStatus.resolve(context.getResponse().statusCode().value()); |
||||
if (status != null) { |
||||
return KeyValue.of(ClientObservation.LowCardinalityKeyNames.OUTCOME, status.series().name()); |
||||
} |
||||
} |
||||
return OUTCOME_UNKNOWN; |
||||
} |
||||
|
||||
@Override |
||||
public KeyValues getHighCardinalityKeyValues(ClientObservationContext context) { |
||||
return KeyValues.of(uriExpanded(context), clientName(context)); |
||||
} |
||||
|
||||
protected KeyValue uriExpanded(ClientObservationContext context) { |
||||
if (context.getCarrier() != null) { |
||||
return KeyValue.of(ClientObservation.HighCardinalityKeyNames.URI_EXPANDED, context.getCarrier().url().toASCIIString()); |
||||
} |
||||
return KeyValue.of(ClientObservation.HighCardinalityKeyNames.URI_EXPANDED, "none"); |
||||
} |
||||
|
||||
protected KeyValue clientName(ClientObservationContext context) { |
||||
String host = "none"; |
||||
if (context.getCarrier() != null && context.getCarrier().url().getHost() != null) { |
||||
host = context.getCarrier().url().getHost(); |
||||
} |
||||
return KeyValue.of(ClientObservation.HighCardinalityKeyNames.CLIENT_NAME, host); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.web.reactive.function.client; |
||||
|
||||
import java.net.URI; |
||||
|
||||
import io.micrometer.common.KeyValue; |
||||
import io.micrometer.observation.Observation; |
||||
import org.junit.jupiter.api.Test; |
||||
|
||||
import org.springframework.http.HttpMethod; |
||||
import org.springframework.http.HttpStatus; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
/** |
||||
* Tests for {@link DefaultClientObservationConvention}. |
||||
* |
||||
* @author Brian Clozel |
||||
*/ |
||||
class DefaultClientObservationConventionTests { |
||||
|
||||
private DefaultClientObservationConvention observationConvention = new DefaultClientObservationConvention(); |
||||
|
||||
@Test |
||||
void shouldOnlySupportWebClientObservationContext() { |
||||
assertThat(this.observationConvention.supportsContext(new ClientObservationContext())).isTrue(); |
||||
assertThat(this.observationConvention.supportsContext(new Observation.Context())).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void shouldAddKeyValuesForNullExchange() { |
||||
ClientObservationContext context = new ClientObservationContext(); |
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(5) |
||||
.contains(KeyValue.of("method", "none"), KeyValue.of("uri", "none"), KeyValue.of("status", "CLIENT_ERROR"), |
||||
KeyValue.of("exception", "none"), KeyValue.of("outcome", "UNKNOWN")); |
||||
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(2) |
||||
.contains(KeyValue.of("client.name", "none"), KeyValue.of("uri.expanded", "none")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldAddKeyValuesForExchangeWithException() { |
||||
ClientObservationContext context = new ClientObservationContext(); |
||||
context.setError(new IllegalStateException("Could not create client request")); |
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)).hasSize(5) |
||||
.contains(KeyValue.of("method", "none"), KeyValue.of("uri", "none"), KeyValue.of("status", "CLIENT_ERROR"), |
||||
KeyValue.of("exception", "IllegalStateException"), KeyValue.of("outcome", "UNKNOWN")); |
||||
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(2) |
||||
.contains(KeyValue.of("client.name", "none"), KeyValue.of("uri.expanded", "none")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldAddKeyValuesForRequestWithUriTemplate() { |
||||
ClientRequest request = ClientRequest.create(HttpMethod.GET, URI.create("/resource/42")) |
||||
.attribute(WebClient.class.getName() + ".uriTemplate", "/resource/{id}").build(); |
||||
ClientObservationContext context = createContext(request); |
||||
context.setUriTemplate("/resource/{id}"); |
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)) |
||||
.contains(KeyValue.of("exception", "none"), KeyValue.of("method", "GET"), KeyValue.of("uri", "/resource/{id}"), |
||||
KeyValue.of("status", "200"), KeyValue.of("outcome", "SUCCESSFUL")); |
||||
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(2) |
||||
.contains(KeyValue.of("client.name", "none"), KeyValue.of("uri.expanded", "/resource/42")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldAddKeyValuesForRequestWithoutUriTemplate() { |
||||
ClientObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("/resource/42")).build()); |
||||
assertThat(this.observationConvention.getLowCardinalityKeyValues(context)) |
||||
.contains(KeyValue.of("method", "GET"), KeyValue.of("uri", "none")); |
||||
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).hasSize(2).contains(KeyValue.of("uri.expanded", "/resource/42")); |
||||
} |
||||
|
||||
@Test |
||||
void shouldAddClientNameKeyValueForRequestWithHost() { |
||||
ClientObservationContext context = createContext(ClientRequest.create(HttpMethod.GET, URI.create("https://localhost:8080/resource/42")).build()); |
||||
assertThat(this.observationConvention.getHighCardinalityKeyValues(context)).contains(KeyValue.of("client.name", "localhost")); |
||||
} |
||||
|
||||
private ClientObservationContext createContext(ClientRequest request) { |
||||
ClientObservationContext context = new ClientObservationContext(); |
||||
context.setCarrier(request); |
||||
context.setResponse(ClientResponse.create(HttpStatus.OK).build()); |
||||
return context; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,105 @@
@@ -0,0 +1,105 @@
|
||||
/* |
||||
* Copyright 2002-2022 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
* |
||||
* Unless required by applicable law or agreed to in writing, software |
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
* See the License for the specific language governing permissions and |
||||
* limitations under the License. |
||||
*/ |
||||
|
||||
package org.springframework.web.reactive.function.client; |
||||
|
||||
import java.time.Duration; |
||||
|
||||
import io.micrometer.observation.tck.TestObservationRegistry; |
||||
import io.micrometer.observation.tck.TestObservationRegistryAssert; |
||||
import org.junit.jupiter.api.BeforeEach; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.mockito.ArgumentCaptor; |
||||
import reactor.core.publisher.Mono; |
||||
import reactor.test.StepVerifier; |
||||
|
||||
import org.springframework.http.HttpStatus; |
||||
|
||||
import static org.mockito.ArgumentMatchers.any; |
||||
import static org.mockito.BDDMockito.given; |
||||
import static org.mockito.BDDMockito.when; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.verify; |
||||
import static org.mockito.Mockito.verifyNoMoreInteractions; |
||||
|
||||
/** |
||||
* Tests for the {@link WebClient} {@link io.micrometer.observation.Observation observations}. |
||||
* @author Brian Clozel |
||||
*/ |
||||
public class DefaultClientObservationTests { |
||||
|
||||
|
||||
private final TestObservationRegistry observationRegistry = TestObservationRegistry.create(); |
||||
|
||||
private ExchangeFunction exchangeFunction = mock(ExchangeFunction.class); |
||||
|
||||
private ArgumentCaptor<ClientRequest> request = ArgumentCaptor.forClass(ClientRequest.class); |
||||
|
||||
private WebClient.Builder builder; |
||||
|
||||
@BeforeEach |
||||
public void setup() { |
||||
ClientResponse mockResponse = mock(ClientResponse.class); |
||||
when(mockResponse.statusCode()).thenReturn(HttpStatus.OK); |
||||
when(mockResponse.bodyToMono(Void.class)).thenReturn(Mono.empty()); |
||||
given(this.exchangeFunction.exchange(this.request.capture())).willReturn(Mono.just(mockResponse)); |
||||
this.builder = WebClient.builder().baseUrl("/base").exchangeFunction(this.exchangeFunction).observationRegistry(this.observationRegistry); |
||||
} |
||||
|
||||
|
||||
@Test |
||||
void recordsObservationForSuccessfulExchange() { |
||||
this.builder.build().get().uri("/resource/{id}", 42) |
||||
.retrieve().bodyToMono(Void.class).block(Duration.ofSeconds(10)); |
||||
verifyAndGetRequest(); |
||||
|
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESSFUL") |
||||
.hasLowCardinalityKeyValue("uri", "/resource/{id}"); |
||||
} |
||||
|
||||
@Test |
||||
void recordsObservationForErrorExchange() { |
||||
ExchangeFunction exchangeFunction = mock(ExchangeFunction.class); |
||||
given(exchangeFunction.exchange(any())).willReturn(Mono.error(new IllegalStateException())); |
||||
WebClient client = WebClient.builder().observationRegistry(observationRegistry).exchangeFunction(exchangeFunction).build(); |
||||
StepVerifier.create(client.get().uri("/path").retrieve().bodyToMono(Void.class)) |
||||
.expectError(IllegalStateException.class) |
||||
.verify(Duration.ofSeconds(5)); |
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("exception", "IllegalStateException") |
||||
.hasLowCardinalityKeyValue("status", "CLIENT_ERROR"); |
||||
} |
||||
|
||||
@Test |
||||
void recordsObservationForCancelledExchange() { |
||||
StepVerifier.create(this.builder.build().get().uri("/path").retrieve().bodyToMono(Void.class)) |
||||
.thenCancel() |
||||
.verify(Duration.ofSeconds(5)); |
||||
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "UNKNOWN") |
||||
.hasLowCardinalityKeyValue("status", "CLIENT_ERROR"); |
||||
} |
||||
|
||||
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() { |
||||
return TestObservationRegistryAssert.assertThat(this.observationRegistry) |
||||
.hasObservationWithNameEqualTo("http.client.requests").that(); |
||||
} |
||||
|
||||
private ClientRequest verifyAndGetRequest() { |
||||
verify(exchangeFunction).exchange(request.getValue()); |
||||
verifyNoMoreInteractions(exchangeFunction); |
||||
return request.getValue(); |
||||
} |
||||
|
||||
} |
Loading…
Reference in new issue