Polish
This commit is contained in:
parent
0f405c06bf
commit
a59b6cb1f3
|
@ -47,21 +47,8 @@ public abstract class AbstractCompositeHealthContributorConfiguration<C, I exten
|
|||
*/
|
||||
@Deprecated(since = "3.0.0", forRemoval = true)
|
||||
protected AbstractCompositeHealthContributorConfiguration() {
|
||||
ResolvableType type = ResolvableType.forClass(AbstractCompositeHealthContributorConfiguration.class,
|
||||
getClass());
|
||||
Class<?> indicatorType = type.resolveGeneric(1);
|
||||
Class<?> beanType = type.resolveGeneric(2);
|
||||
this.indicatorFactory = (bean) -> {
|
||||
try {
|
||||
@SuppressWarnings("unchecked")
|
||||
Constructor<I> constructor = (Constructor<I>) indicatorType.getDeclaredConstructor(beanType);
|
||||
return BeanUtils.instantiateClass(constructor, bean);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException(
|
||||
"Unable to create health indicator " + indicatorType + " for bean type " + beanType, ex);
|
||||
}
|
||||
};
|
||||
this.indicatorFactory = new ReflectionIndicatorFactory(
|
||||
ResolvableType.forClass(AbstractCompositeHealthContributorConfiguration.class, getClass()));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -88,4 +75,34 @@ public abstract class AbstractCompositeHealthContributorConfiguration<C, I exten
|
|||
return this.indicatorFactory.apply(bean);
|
||||
}
|
||||
|
||||
private class ReflectionIndicatorFactory implements Function<B, I> {
|
||||
|
||||
private final Class<?> indicatorType;
|
||||
|
||||
private final Class<?> beanType;
|
||||
|
||||
ReflectionIndicatorFactory(ResolvableType type) {
|
||||
this.indicatorType = type.resolveGeneric(1);
|
||||
this.beanType = type.resolveGeneric(2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public I apply(B bean) {
|
||||
try {
|
||||
return BeanUtils.instantiateClass(getConstructor(), bean);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new IllegalStateException("Unable to create health indicator %s for bean type %s"
|
||||
.formatted(this.indicatorType, this.beanType), ex);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private Constructor<I> getConstructor() throws NoSuchMethodException {
|
||||
return (Constructor<I>) this.indicatorType.getDeclaredConstructor(this.beanType);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -132,8 +132,8 @@ public class ObservationAutoConfiguration {
|
|||
@Bean
|
||||
TracingAwareMeterObservationHandler<Observation.Context> tracingAwareMeterObservationHandler(
|
||||
MeterRegistry meterRegistry, Tracer tracer) {
|
||||
return new TracingAwareMeterObservationHandler<>(new DefaultMeterObservationHandler(meterRegistry),
|
||||
tracer);
|
||||
DefaultMeterObservationHandler delegate = new DefaultMeterObservationHandler(meterRegistry);
|
||||
return new TracingAwareMeterObservationHandler<>(delegate, tracer);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,16 +16,15 @@
|
|||
|
||||
package org.springframework.boot.actuate.autoconfigure.observation;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.micrometer.observation.ObservationHandler;
|
||||
import io.micrometer.observation.ObservationHandler.FirstMatchingCompositeObservationHandler;
|
||||
import io.micrometer.observation.ObservationRegistry.ObservationConfig;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Groups {@link ObservationHandler ObservationHandlers} by type.
|
||||
|
@ -46,11 +45,11 @@ class ObservationHandlerGrouping {
|
|||
}
|
||||
|
||||
void apply(List<ObservationHandler<?>> handlers, ObservationConfig config) {
|
||||
Map<Class<? extends ObservationHandler>, List<ObservationHandler<?>>> groupings = new HashMap<>();
|
||||
MultiValueMap<Class<? extends ObservationHandler>, ObservationHandler<?>> groupings = new LinkedMultiValueMap<>();
|
||||
for (ObservationHandler<?> handler : handlers) {
|
||||
Class<? extends ObservationHandler> category = findCategory(handler);
|
||||
if (category != null) {
|
||||
groupings.computeIfAbsent(category, (c) -> new ArrayList<>()).add(handler);
|
||||
groupings.add(category, handler);
|
||||
}
|
||||
else {
|
||||
config.observationHandler(handler);
|
||||
|
|
|
@ -73,13 +73,11 @@ class ObservationRegistryConfigurer {
|
|||
}
|
||||
|
||||
private void registerObservationPredicates(ObservationRegistry registry) {
|
||||
this.observationPredicates.orderedStream().forEach(
|
||||
(observationPredicate) -> registry.observationConfig().observationPredicate(observationPredicate));
|
||||
this.observationPredicates.orderedStream().forEach(registry.observationConfig()::observationPredicate);
|
||||
}
|
||||
|
||||
private void registerGlobalObservationConventions(ObservationRegistry registry) {
|
||||
this.observationConventions.orderedStream()
|
||||
.forEach((convention) -> registry.observationConfig().observationConvention(convention));
|
||||
this.observationConventions.orderedStream().forEach(registry.observationConfig()::observationConvention);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
|
|
|
@ -44,9 +44,9 @@ class ClientHttpObservationConventionAdapter implements ClientRequestObservation
|
|||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
Iterable<Tag> tags = this.tagsProvider.getTags(context.getUriTemplate(), context.getCarrier(),
|
||||
context.getResponse());
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
for (Tag tag : tags) {
|
||||
keyValues = keyValues.and(tag.getKey(), tag.getValue());
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import io.micrometer.core.instrument.Tag;
|
|||
import io.micrometer.observation.Observation;
|
||||
|
||||
import org.springframework.boot.actuate.metrics.web.reactive.client.WebClientExchangeTagsProvider;
|
||||
import org.springframework.core.Conventions;
|
||||
import org.springframework.web.reactive.function.client.ClientRequest;
|
||||
import org.springframework.web.reactive.function.client.ClientRequestObservationContext;
|
||||
import org.springframework.web.reactive.function.client.ClientRequestObservationConvention;
|
||||
|
@ -32,10 +33,11 @@ import org.springframework.web.reactive.function.client.WebClient;
|
|||
*
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
@SuppressWarnings({ "deprecation", "removal" })
|
||||
@SuppressWarnings("removal")
|
||||
class ClientObservationConventionAdapter implements ClientRequestObservationConvention {
|
||||
|
||||
private static final String URI_TEMPLATE_ATTRIBUTE = WebClient.class.getName() + ".uriTemplate";
|
||||
private static final String URI_TEMPLATE_ATTRIBUTE = Conventions.getQualifiedAttributeName(WebClient.class,
|
||||
"uriTemplate");
|
||||
|
||||
private final String metricName;
|
||||
|
||||
|
@ -53,20 +55,18 @@ class ClientObservationConventionAdapter implements ClientRequestObservationConv
|
|||
|
||||
@Override
|
||||
public KeyValues getLowCardinalityKeyValues(ClientRequestObservationContext context) {
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
mutateClientRequest(context);
|
||||
Iterable<Tag> tags = this.tagsProvider.tags(context.getCarrier(), context.getResponse(), context.getError());
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
for (Tag tag : tags) {
|
||||
keyValues = keyValues.and(tag.getKey(), tag.getValue());
|
||||
}
|
||||
return keyValues;
|
||||
}
|
||||
|
||||
/*
|
||||
* {@link WebClientExchangeTagsProvider} relies on a request attribute to get the URI
|
||||
* template, we need to adapt to that.
|
||||
*/
|
||||
private static void mutateClientRequest(ClientRequestObservationContext context) {
|
||||
private void mutateClientRequest(ClientRequestObservationContext context) {
|
||||
// WebClientExchangeTagsProvider relies on a request attribute to get the URI
|
||||
// template, we need to adapt to that.
|
||||
ClientRequest clientRequest = ClientRequest.from(context.getCarrier())
|
||||
.attribute(URI_TEMPLATE_ATTRIBUTE, context.getUriTemplate()).build();
|
||||
context.setCarrier(clientRequest);
|
||||
|
|
|
@ -23,6 +23,7 @@ import io.micrometer.observation.ObservationRegistry;
|
|||
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Client;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationProperties;
|
||||
|
@ -67,13 +68,14 @@ public class HttpClientObservationsAutoConfiguration {
|
|||
@SuppressWarnings("removal")
|
||||
MeterFilter metricsHttpClientUriTagFilter(ObservationProperties observationProperties,
|
||||
MetricsProperties metricsProperties) {
|
||||
String metricName = metricsProperties.getWeb().getClient().getRequest().getMetricName();
|
||||
Client clientProperties = metricsProperties.getWeb().getClient();
|
||||
String metricName = clientProperties.getRequest().getMetricName();
|
||||
String observationName = observationProperties.getHttp().getClient().getRequests().getName();
|
||||
String name = (observationName != null) ? observationName : metricName;
|
||||
MeterFilter denyFilter = new OnlyOnceLoggingDenyMeterFilter(() -> String
|
||||
.format("Reached the maximum number of URI tags for '%s'. Are you using 'uriVariables'?", name));
|
||||
return MeterFilter.maximumAllowableTags(name, "uri", metricsProperties.getWeb().getClient().getMaxUriTags(),
|
||||
denyFilter);
|
||||
MeterFilter denyFilter = new OnlyOnceLoggingDenyMeterFilter(
|
||||
() -> "Reached the maximum number of URI tags for '%s'. Are you using 'uriVariables'?"
|
||||
.formatted(name));
|
||||
return MeterFilter.maximumAllowableTags(name, "uri", clientProperties.getMaxUriTags(), denyFilter);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -58,8 +58,8 @@ class ServerRequestObservationConventionAdapter implements ServerRequestObservat
|
|||
|
||||
@Override
|
||||
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
Iterable<Tag> tags = this.tagsProvider.httpRequestTags(context.getServerWebExchange(), context.getError());
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
for (Tag tag : tags) {
|
||||
keyValues = keyValues.and(tag.getKey(), tag.getValue());
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ import org.springframework.beans.factory.ObjectProvider;
|
|||
import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.MetricsProperties.Web.Server;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.OnlyOnceLoggingDenyMeterFilter;
|
||||
import org.springframework.boot.actuate.autoconfigure.metrics.export.simple.SimpleMetricsExportAutoConfiguration;
|
||||
import org.springframework.boot.actuate.autoconfigure.observation.ObservationAutoConfiguration;
|
||||
|
@ -80,22 +81,26 @@ public class WebFluxObservationAutoConfiguration {
|
|||
public ServerHttpObservationFilter webfluxObservationFilter(ObservationRegistry registry,
|
||||
ObjectProvider<WebFluxTagsProvider> tagConfigurer,
|
||||
ObjectProvider<WebFluxTagsContributor> contributorsProvider) {
|
||||
|
||||
String observationName = this.observationProperties.getHttp().getServer().getRequests().getName();
|
||||
String metricName = this.metricsProperties.getWeb().getServer().getRequest().getMetricName();
|
||||
String name = (observationName != null) ? observationName : metricName;
|
||||
WebFluxTagsProvider tagsProvider = tagConfigurer.getIfAvailable();
|
||||
List<WebFluxTagsContributor> tagsContributors = contributorsProvider.orderedStream().toList();
|
||||
ServerRequestObservationConvention convention = new DefaultServerRequestObservationConvention(name);
|
||||
if (tagsProvider != null) {
|
||||
convention = new ServerRequestObservationConventionAdapter(name, tagsProvider);
|
||||
}
|
||||
else if (!tagsContributors.isEmpty()) {
|
||||
convention = new ServerRequestObservationConventionAdapter(name, tagsContributors);
|
||||
}
|
||||
ServerRequestObservationConvention convention = extracted(name, tagsProvider, tagsContributors);
|
||||
return new ServerHttpObservationFilter(registry, convention);
|
||||
}
|
||||
|
||||
private ServerRequestObservationConvention extracted(String name, WebFluxTagsProvider tagsProvider,
|
||||
List<WebFluxTagsContributor> tagsContributors) {
|
||||
if (tagsProvider != null) {
|
||||
return new ServerRequestObservationConventionAdapter(name, tagsProvider);
|
||||
}
|
||||
if (!tagsContributors.isEmpty()) {
|
||||
return new ServerRequestObservationConventionAdapter(name, tagsContributors);
|
||||
}
|
||||
return new DefaultServerRequestObservationConvention(name);
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(MeterRegistry.class)
|
||||
@ConditionalOnBean(MeterRegistry.class)
|
||||
|
@ -104,11 +109,11 @@ public class WebFluxObservationAutoConfiguration {
|
|||
@Bean
|
||||
@Order(0)
|
||||
MeterFilter metricsHttpServerUriTagFilter(MetricsProperties properties) {
|
||||
String metricName = properties.getWeb().getServer().getRequest().getMetricName();
|
||||
Server serverProperties = properties.getWeb().getServer();
|
||||
String metricName = serverProperties.getRequest().getMetricName();
|
||||
MeterFilter filter = new OnlyOnceLoggingDenyMeterFilter(
|
||||
() -> String.format("Reached the maximum number of URI tags for '%s'.", metricName));
|
||||
return MeterFilter.maximumAllowableTags(metricName, "uri", properties.getWeb().getServer().getMaxUriTags(),
|
||||
filter);
|
||||
() -> "Reached the maximum number of URI tags for '%s'.".formatted(metricName));
|
||||
return MeterFilter.maximumAllowableTags(metricName, "uri", serverProperties.getMaxUriTags(), filter);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -64,9 +64,9 @@ class ServerRequestObservationConventionAdapter implements ServerRequestObservat
|
|||
|
||||
@Override
|
||||
public KeyValues getLowCardinalityKeyValues(ServerRequestObservationContext context) {
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
Iterable<Tag> tags = this.tagsProvider.getTags(context.getCarrier(), context.getResponse(), getHandler(context),
|
||||
context.getError());
|
||||
KeyValues keyValues = KeyValues.empty();
|
||||
for (Tag tag : tags) {
|
||||
keyValues = keyValues.and(tag.getKey(), tag.getValue());
|
||||
}
|
||||
|
|
|
@ -110,9 +110,7 @@ public class BraveAutoConfiguration {
|
|||
Builder builder = Tracing.newBuilder().currentTraceContext(currentTraceContext).traceId128Bit(true)
|
||||
.supportsJoin(false).propagationFactory(propagationFactory).sampler(sampler)
|
||||
.localServiceName(applicationName);
|
||||
for (SpanHandler spanHandler : spanHandlers) {
|
||||
builder.addSpanHandler(spanHandler);
|
||||
}
|
||||
spanHandlers.forEach(builder::addSpanHandler);
|
||||
for (TracingCustomizer tracingCustomizer : tracingCustomizers) {
|
||||
tracingCustomizer.customize(builder);
|
||||
}
|
||||
|
@ -130,9 +128,7 @@ public class BraveAutoConfiguration {
|
|||
public CurrentTraceContext braveCurrentTraceContext(List<CurrentTraceContext.ScopeDecorator> scopeDecorators,
|
||||
List<CurrentTraceContextCustomizer> currentTraceContextCustomizers) {
|
||||
ThreadLocalCurrentTraceContext.Builder builder = ThreadLocalCurrentTraceContext.newBuilder();
|
||||
for (ScopeDecorator scopeDecorator : scopeDecorators) {
|
||||
builder.addScopeDecorator(scopeDecorator);
|
||||
}
|
||||
scopeDecorators.forEach(builder::addScopeDecorator);
|
||||
for (CurrentTraceContextCustomizer currentTraceContextCustomizer : currentTraceContextCustomizers) {
|
||||
currentTraceContextCustomizer.customize(builder);
|
||||
}
|
||||
|
|
|
@ -66,8 +66,11 @@ class MeterRegistrySpanMetrics implements SpanMetrics {
|
|||
|
||||
@Override
|
||||
public void registerQueueRemainingCapacity(BlockingQueue<?> queue) {
|
||||
this.meterRegistry.gauge("wavefront.reporter.queue.remaining_capacity", queue,
|
||||
(q) -> (double) q.remainingCapacity());
|
||||
this.meterRegistry.gauge("wavefront.reporter.queue.remaining_capacity", queue, this::remainingCapacity);
|
||||
}
|
||||
|
||||
private double remainingCapacity(BlockingQueue<?> queue) {
|
||||
return queue.remainingCapacity();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -60,9 +60,11 @@ class ZipkinConfigurations {
|
|||
@Bean
|
||||
@ConditionalOnMissingBean(Sender.class)
|
||||
URLConnectionSender urlConnectionSender(ZipkinProperties properties) {
|
||||
return URLConnectionSender.newBuilder().connectTimeout((int) properties.getConnectTimeout().toMillis())
|
||||
.readTimeout((int) properties.getReadTimeout().toMillis()).endpoint(properties.getEndpoint())
|
||||
.build();
|
||||
URLConnectionSender.Builder builder = URLConnectionSender.newBuilder();
|
||||
builder.connectTimeout((int) properties.getConnectTimeout().toMillis());
|
||||
builder.readTimeout((int) properties.getReadTimeout().toMillis());
|
||||
builder.endpoint(properties.getEndpoint());
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -78,7 +80,7 @@ class ZipkinConfigurations {
|
|||
ObjectProvider<ZipkinRestTemplateBuilderCustomizer> customizers) {
|
||||
RestTemplateBuilder restTemplateBuilder = new RestTemplateBuilder()
|
||||
.setConnectTimeout(properties.getConnectTimeout()).setReadTimeout(properties.getReadTimeout());
|
||||
customizers.orderedStream().forEach((c) -> c.customize(restTemplateBuilder));
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(restTemplateBuilder));
|
||||
return new ZipkinRestTemplateSender(properties.getEndpoint(), restTemplateBuilder.build());
|
||||
}
|
||||
|
||||
|
@ -94,7 +96,7 @@ class ZipkinConfigurations {
|
|||
ZipkinWebClientSender webClientSender(ZipkinProperties properties,
|
||||
ObjectProvider<ZipkinWebClientBuilderCustomizer> customizers) {
|
||||
WebClient.Builder builder = WebClient.builder();
|
||||
customizers.orderedStream().forEach((c) -> c.customize(builder));
|
||||
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
|
||||
return new ZipkinWebClientSender(properties.getEndpoint(), builder.build());
|
||||
}
|
||||
|
||||
|
|
|
@ -20,6 +20,7 @@ import reactor.core.publisher.Mono;
|
|||
import zipkin2.Call;
|
||||
import zipkin2.Callback;
|
||||
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
|
||||
|
@ -73,8 +74,12 @@ class ZipkinWebClientSender extends HttpSender {
|
|||
}
|
||||
|
||||
private Mono<ResponseEntity<Void>> sendRequest() {
|
||||
return this.webClient.post().uri(this.endpoint).headers((headers) -> headers.addAll(getDefaultHeaders()))
|
||||
.bodyValue(getBody()).retrieve().toBodilessEntity();
|
||||
return this.webClient.post().uri(this.endpoint).headers(this::addDefaultHeaders).bodyValue(getBody())
|
||||
.retrieve().toBodilessEntity();
|
||||
}
|
||||
|
||||
private void addDefaultHeaders(HttpHeaders headers) {
|
||||
headers.addAll(getDefaultHeaders());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -48,12 +48,12 @@ public class WavefrontSenderConfiguration {
|
|||
@ConditionalOnMissingBean
|
||||
public WavefrontSender wavefrontSender(WavefrontProperties properties) {
|
||||
Builder builder = new Builder(properties.getEffectiveUri().toString(), properties.getApiTokenOrThrow());
|
||||
PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
WavefrontProperties.Sender sender = properties.getSender();
|
||||
mapper.from(sender.getMaxQueueSize()).to(builder::maxQueueSize);
|
||||
mapper.from(sender.getFlushInterval()).asInt(Duration::getSeconds).to(builder::flushIntervalSeconds);
|
||||
mapper.from(sender.getMessageSize()).asInt(DataSize::toBytes).to(builder::messageSizeBytes);
|
||||
mapper.from(sender.getBatchSize()).to(builder::batchSize);
|
||||
map.from(sender.getMaxQueueSize()).to(builder::maxQueueSize);
|
||||
map.from(sender.getFlushInterval()).asInt(Duration::getSeconds).to(builder::flushIntervalSeconds);
|
||||
map.from(sender.getMessageSize()).asInt(DataSize::toBytes).to(builder::messageSizeBytes);
|
||||
map.from(sender.getBatchSize()).to(builder::batchSize);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
|
|
|
@ -43,6 +43,7 @@ import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpoint;
|
|||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
|
@ -208,10 +209,7 @@ public class HeapDumpWebEndpoint {
|
|||
|
||||
@Override
|
||||
public File dumpHeap(Boolean live) throws IOException, InterruptedException {
|
||||
if (live != null) {
|
||||
throw new IllegalArgumentException(
|
||||
"OpenJ9DiagnosticsMXBean does not support live parameter when dumping the heap");
|
||||
}
|
||||
Assert.isNull(live, "OpenJ9DiagnosticsMXBean does not support live parameter when dumping the heap");
|
||||
return new File(
|
||||
(String) ReflectionUtils.invokeMethod(this.dumpHeapMethod, this.diagnosticMXBean, "heap", null));
|
||||
}
|
||||
|
|
|
@ -99,11 +99,11 @@ class RabbitStreamConfiguration {
|
|||
static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitProperties properties) {
|
||||
builder.lazyInitialization(true);
|
||||
RabbitProperties.Stream stream = properties.getStream();
|
||||
PropertyMapper mapper = PropertyMapper.get();
|
||||
mapper.from(stream.getHost()).to(builder::host);
|
||||
mapper.from(stream.getPort()).to(builder::port);
|
||||
mapper.from(stream.getUsername()).as(withFallback(properties::getUsername)).whenNonNull().to(builder::username);
|
||||
mapper.from(stream.getPassword()).as(withFallback(properties::getPassword)).whenNonNull().to(builder::password);
|
||||
PropertyMapper map = PropertyMapper.get();
|
||||
map.from(stream.getHost()).to(builder::host);
|
||||
map.from(stream.getPort()).to(builder::port);
|
||||
map.from(stream.getUsername()).as(withFallback(properties::getUsername)).whenNonNull().to(builder::username);
|
||||
map.from(stream.getPassword()).as(withFallback(properties::getPassword)).whenNonNull().to(builder::password);
|
||||
return builder;
|
||||
}
|
||||
|
||||
|
|
|
@ -52,9 +52,9 @@ class MongoDataConfiguration {
|
|||
@ConditionalOnMissingBean
|
||||
MongoMappingContext mongoMappingContext(MongoProperties properties, MongoCustomConversions conversions,
|
||||
MongoManagedTypes managedTypes) {
|
||||
PropertyMapper mapper = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
MongoMappingContext context = new MongoMappingContext();
|
||||
mapper.from(properties.isAutoIndexCreation()).to(context::setAutoIndexCreation);
|
||||
map.from(properties.isAutoIndexCreation()).to(context::setAutoIndexCreation);
|
||||
context.setManagedTypes(managedTypes);
|
||||
Class<?> strategyClass = properties.getFieldNamingStrategy();
|
||||
if (strategyClass != null) {
|
||||
|
|
|
@ -24,6 +24,7 @@ import java.util.Collection;
|
|||
import java.util.List;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.flywaydb.core.api.FlywayException;
|
||||
import org.flywaydb.core.api.Location;
|
||||
|
@ -57,7 +58,7 @@ class NativeImageResourceProvider implements ResourceProvider {
|
|||
|
||||
private final boolean failOnMissingLocations;
|
||||
|
||||
private final List<ResourceWithLocation> resources = new ArrayList<>();
|
||||
private final List<LocatedResource> locatedResources = new ArrayList<>();
|
||||
|
||||
private final Lock lock = new ReentrantLock();
|
||||
|
||||
|
@ -93,14 +94,21 @@ class NativeImageResourceProvider implements ResourceProvider {
|
|||
return this.scanner.getResources(prefix, suffixes);
|
||||
}
|
||||
ensureInitialized();
|
||||
List<LoadableResource> result = new ArrayList<>(this.scanner.getResources(prefix, suffixes));
|
||||
this.resources.stream().filter((r) -> StringUtils.startsAndEndsWith(r.resource.getFilename(), prefix, suffixes))
|
||||
.map((r) -> (LoadableResource) new ClassPathResource(r.location(),
|
||||
r.location().getPath() + "/" + r.resource().getFilename(), this.classLoader, this.encoding))
|
||||
Predicate<LocatedResource> matchesPrefixAndSuffixes = (locatedResource) -> StringUtils
|
||||
.startsAndEndsWith(locatedResource.resource.getFilename(), prefix, suffixes);
|
||||
List<LoadableResource> result = new ArrayList<>();
|
||||
result.addAll(this.scanner.getResources(prefix, suffixes));
|
||||
this.locatedResources.stream().filter(matchesPrefixAndSuffixes).map(this::asClassPathResource)
|
||||
.forEach(result::add);
|
||||
return result;
|
||||
}
|
||||
|
||||
private ClassPathResource asClassPathResource(LocatedResource locatedResource) {
|
||||
Location location = locatedResource.location();
|
||||
String fileNameWithAbsolutePath = location.getPath() + "/" + locatedResource.resource().getFilename();
|
||||
return new ClassPathResource(location, fileNameWithAbsolutePath, this.classLoader, this.encoding);
|
||||
}
|
||||
|
||||
private void ensureInitialized() {
|
||||
this.lock.lock();
|
||||
try {
|
||||
|
@ -127,20 +135,24 @@ class NativeImageResourceProvider implements ResourceProvider {
|
|||
}
|
||||
continue;
|
||||
}
|
||||
Resource[] resources;
|
||||
try {
|
||||
resources = resolver.getResources(root.getURI() + "/*");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException("Failed to list resources for " + location.getDescriptor(), ex);
|
||||
}
|
||||
Resource[] resources = getResources(resolver, location, root);
|
||||
for (Resource resource : resources) {
|
||||
this.resources.add(new ResourceWithLocation(resource, location));
|
||||
this.locatedResources.add(new LocatedResource(resource, location));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record ResourceWithLocation(Resource resource, Location location) {
|
||||
private Resource[] getResources(PathMatchingResourcePatternResolver resolver, Location location, Resource root) {
|
||||
try {
|
||||
return resolver.getResources(root.getURI() + "/*");
|
||||
}
|
||||
catch (IOException ex) {
|
||||
throw new UncheckedIOException("Failed to list resources for " + location.getDescriptor(), ex);
|
||||
}
|
||||
}
|
||||
|
||||
private record LocatedResource(Resource resource, Location location) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -52,8 +52,8 @@ class HazelcastSessionConfiguration {
|
|||
SessionRepositoryCustomizer<HazelcastIndexedSessionRepository> springBootSessionRepositoryCustomizer(
|
||||
SessionProperties sessionProperties, HazelcastSessionProperties hazelcastSessionProperties,
|
||||
ServerProperties serverProperties) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
return (sessionRepository) -> {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(sessionProperties.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()))
|
||||
.to(sessionRepository::setDefaultMaxInactiveInterval);
|
||||
map.from(hazelcastSessionProperties::getMapName).to(sessionRepository::setSessionMapName);
|
||||
|
|
|
@ -67,8 +67,8 @@ class JdbcSessionConfiguration {
|
|||
SessionRepositoryCustomizer<JdbcIndexedSessionRepository> springBootSessionRepositoryCustomizer(
|
||||
SessionProperties sessionProperties, JdbcSessionProperties jdbcSessionProperties,
|
||||
ServerProperties serverProperties) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
return (sessionRepository) -> {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(sessionProperties.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()))
|
||||
.to(sessionRepository::setDefaultMaxInactiveInterval);
|
||||
map.from(jdbcSessionProperties::getTableName).to(sessionRepository::setTableName);
|
||||
|
|
|
@ -50,8 +50,8 @@ class MongoReactiveSessionConfiguration {
|
|||
ReactiveSessionRepositoryCustomizer<ReactiveMongoSessionRepository> springBootSessionRepositoryCustomizer(
|
||||
SessionProperties sessionProperties, MongoSessionProperties mongoSessionProperties,
|
||||
ServerProperties serverProperties) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
return (sessionRepository) -> {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(sessionProperties.determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout()))
|
||||
.to(sessionRepository::setDefaultMaxInactiveInterval);
|
||||
map.from(mongoSessionProperties::getCollectionName).to(sessionRepository::setCollectionName);
|
||||
|
|
|
@ -50,8 +50,8 @@ class RedisReactiveSessionConfiguration {
|
|||
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> springBootSessionRepositoryCustomizer(
|
||||
SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
|
||||
ServerProperties serverProperties) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
return (sessionRepository) -> {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(sessionProperties.determineTimeout(() -> serverProperties.getReactive().getSession().getTimeout()))
|
||||
.to(sessionRepository::setDefaultMaxInactiveInterval);
|
||||
map.from(redisSessionProperties::getNamespace).to(sessionRepository::setRedisKeyNamespace);
|
||||
|
|
|
@ -67,11 +67,11 @@ class RedisSessionConfiguration {
|
|||
String cleanupCron = redisSessionProperties.getCleanupCron();
|
||||
if (cleanupCron != null) {
|
||||
throw new InvalidConfigurationPropertyValueException("spring.session.redis.cleanup-cron", cleanupCron,
|
||||
"Cron-based cleanup is only supported when spring.session.redis.repository-type is set to "
|
||||
+ "indexed.");
|
||||
"Cron-based cleanup is only supported when "
|
||||
+ "spring.session.redis.repository-type is set to indexed.");
|
||||
}
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
return (sessionRepository) -> {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(sessionProperties
|
||||
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()))
|
||||
.to(sessionRepository::setDefaultMaxInactiveInterval);
|
||||
|
@ -101,8 +101,8 @@ class RedisSessionConfiguration {
|
|||
SessionRepositoryCustomizer<RedisIndexedSessionRepository> springBootSessionRepositoryCustomizer(
|
||||
SessionProperties sessionProperties, RedisSessionProperties redisSessionProperties,
|
||||
ServerProperties serverProperties) {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
return (sessionRepository) -> {
|
||||
PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
|
||||
map.from(sessionProperties
|
||||
.determineTimeout(() -> serverProperties.getServlet().getSession().getTimeout()))
|
||||
.to(sessionRepository::setDefaultMaxInactiveInterval);
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
[[actuator.tracing]]
|
||||
== HTTP Tracing
|
||||
|
||||
You can enable HTTP Tracing by providing a bean of type `HttpTraceRepository` in your application's configuration.
|
||||
For convenience, Spring Boot offers `InMemoryHttpTraceRepository`, which stores traces for the last 100 (the default) request-response exchanges.
|
||||
`InMemoryHttpTraceRepository` is limited compared to other tracing solutions, and we recommend using it only for development environments.
|
||||
|
@ -9,8 +8,9 @@ Alternatively, you can create your own `HttpTraceRepository`.
|
|||
|
||||
You can use the `httptrace` endpoint to obtain information about the request-response exchanges that are stored in the `HttpTraceRepository`.
|
||||
|
||||
[[actuator.tracing.custom]]
|
||||
=== Custom HTTP tracing
|
||||
|
||||
|
||||
[[actuator.tracing.custom]]
|
||||
=== Custom HTTP Tracing
|
||||
To customize the items that are included in each trace, use the configprop:management.trace.http.include[] configuration property.
|
||||
For advanced customization, consider registering your own `HttpExchangeTracer` implementation.
|
||||
|
|
|
@ -1,22 +1,23 @@
|
|||
[[actuator.micrometer-tracing]]
|
||||
== Tracing
|
||||
|
||||
Spring Boot Actuator provides dependency management and auto-configuration for https://micrometer.io/docs/tracing[Micrometer Tracing], a facade for popular tracer libraries.
|
||||
Micrometer Tracing hooks into Micrometer's `ObservationHandler`, which means a https://micrometer.io/docs/tracing#_glossary[span] is reported for every completed observation.
|
||||
|
||||
TIP: To learn more about Micrometer Tracing capabilities, see its https://micrometer.io/docs/tracing[reference documentation].
|
||||
|
||||
[[actuator.micrometer-tracing.tracers]]
|
||||
=== Supported tracers
|
||||
|
||||
|
||||
[[actuator.micrometer-tracing.tracers]]
|
||||
=== Supported Tracers
|
||||
Spring Boot ships auto-configuration for the following tracers:
|
||||
|
||||
* https://opentelemetry.io/[OpenTelemetry] with https://zipkin.io/[Zipkin] or https://docs.wavefront.com/[Wavefront]
|
||||
* https://github.com/openzipkin/brave[OpenZipkin Brave] with https://zipkin.io/[Zipkin] or https://docs.wavefront.com/[Wavefront]
|
||||
|
||||
|
||||
|
||||
[[actuator.micrometer-tracing.getting-started]]
|
||||
=== Getting Started
|
||||
|
||||
We need an example application that we can use to getting started with tracing.
|
||||
For our purposes, the simple "`Hello World!`" web application that's covered in the "`<<getting-started#getting-started.first-application>>`" section will suffice.
|
||||
We're going to use the OpenTelemetry tracer with Zipkin as trace backend.
|
||||
|
@ -64,42 +65,52 @@ Press the "Show" button to see the details of that trace.
|
|||
|
||||
TIP: You can include the current trace and span id in the logs by setting the `logging.pattern.level` property to `%5p [${spring.application.name:},%X{traceId:-},%X{spanId:-}]`
|
||||
|
||||
[[actuator.micrometer-tracing.tracer-implementations]]
|
||||
=== Tracer implementations
|
||||
|
||||
|
||||
[[actuator.micrometer-tracing.tracer-implementations]]
|
||||
=== Tracer Implementations
|
||||
As Micrometer Tracer supports multiple tracer implementations, there are multiple dependency combinations possible with Spring Boot.
|
||||
|
||||
All tracer implementations need the `org.springframework.boot:spring-boot-starter-actuator` dependency.
|
||||
|
||||
|
||||
|
||||
[[actuator.micrometer-tracing.tracer-implementations.otel-zipkin]]
|
||||
==== OpenTelemetry with Zipkin
|
||||
==== OpenTelemetry With Zipkin
|
||||
|
||||
* `io.micrometer:micrometer-tracing-bridge-otel` - which is needed to bride the Micrometer Observation API to OpenTelemetry.
|
||||
* `io.opentelemetry:opentelemetry-exporter-zipkin` - which is needed to report traces to Zipkin.
|
||||
|
||||
|
||||
|
||||
[[actuator.micrometer-tracing.tracer-implementations.otel-wavefront]]
|
||||
==== OpenTelemetry with Wavefront
|
||||
==== OpenTelemetry With Wavefront
|
||||
|
||||
* `io.micrometer:micrometer-tracing-bridge-otel` - which is needed to bride the Micrometer Observation API to OpenTelemetry.
|
||||
* `io.micrometer:micrometer-tracing-reporter-wavefront` - which is needed to report traces to Wavefront.
|
||||
|
||||
|
||||
|
||||
[[actuator.micrometer-tracing.tracer-implementations.brave-zipkin]]
|
||||
==== OpenZipkin Brave with Zipkin
|
||||
==== OpenZipkin Brave With Zipkin
|
||||
|
||||
* `io.micrometer:micrometer-tracing-bridge-brave` - which is needed to bridge the Micrometer Observation API to Brave.
|
||||
* `io.zipkin.reporter2:zipkin-reporter-brave` - which is needed to report traces to Zipkin.
|
||||
|
||||
NOTE: If your project doesn't use Spring MVC or Spring WebFlux, the `io.zipkin.reporter2:zipkin-sender-urlconnection` dependency is needed, too.
|
||||
|
||||
|
||||
|
||||
[[actuator.micrometer-tracing.tracer-implementations.brave-wavefront]]
|
||||
==== OpenZipkin Brave with Wavefront
|
||||
==== OpenZipkin Brave With Wavefront
|
||||
|
||||
* `io.micrometer:micrometer-tracing-bridge-brave` - which is needed to bridge the Micrometer Observation API to Brave.
|
||||
* `io.micrometer:micrometer-tracing-reporter-wavefront` - which is needed to report traces to Wavefront.
|
||||
|
||||
[[actuator.micrometer-tracing.creating-spans]]
|
||||
=== Creating custom spans
|
||||
|
||||
|
||||
[[actuator.micrometer-tracing.creating-spans]]
|
||||
=== Creating Custom Spans
|
||||
You can create your own spans by starting an observation.
|
||||
For this, inject `ObservationRegistry` into your component:
|
||||
|
||||
|
|
|
@ -67,11 +67,21 @@ public class MockServerRestTemplateCustomizer implements RestTemplateCustomizer
|
|||
this(SimpleRequestExpectationManager::new);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crate a new {@link MockServerRestTemplateCustomizer} instance.
|
||||
* @param expectationManager the expectation manager class to use
|
||||
*/
|
||||
public MockServerRestTemplateCustomizer(Class<? extends RequestExpectationManager> expectationManager) {
|
||||
this(() -> BeanUtils.instantiateClass(expectationManager));
|
||||
Assert.notNull(expectationManager, "ExpectationManager must not be null");
|
||||
}
|
||||
|
||||
/**
|
||||
* Crate a new {@link MockServerRestTemplateCustomizer} instance.
|
||||
* @param expectationManagerSupplier a supplier that provides the
|
||||
* {@link RequestExpectationManager} to use
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public MockServerRestTemplateCustomizer(Supplier<? extends RequestExpectationManager> expectationManagerSupplier) {
|
||||
Assert.notNull(expectationManagerSupplier, "ExpectationManagerSupplier must not be null");
|
||||
this.expectationManagerSupplier = expectationManagerSupplier;
|
||||
|
|
|
@ -76,11 +76,14 @@ class NativeImagePluginAction implements PluginApplicationAction {
|
|||
}
|
||||
|
||||
private Iterable<Configuration> removeDevelopmentOnly(Set<Configuration> configurations) {
|
||||
return configurations.stream().filter((
|
||||
configuration) -> !SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME.equals(configuration.getName()))
|
||||
return configurations.stream().filter(this::isNotDevelopmentOnly)
|
||||
.collect(Collectors.toCollection(LinkedHashSet::new));
|
||||
}
|
||||
|
||||
private boolean isNotDevelopmentOnly(Configuration configuration) {
|
||||
return !SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME.equals(configuration.getName());
|
||||
}
|
||||
|
||||
private void configureTestNativeBinaryClasspath(SourceSetContainer sourceSets, GraalVMExtension graalVmExtension,
|
||||
String sourceSetName) {
|
||||
SourceSetOutput output = sourceSets.getByName(SpringBootAotPlugin.AOT_TEST_SOURCE_SET_NAME).getOutput();
|
||||
|
|
|
@ -38,6 +38,7 @@ import org.springframework.util.StringUtils;
|
|||
*
|
||||
* @author Phillip Webb
|
||||
* @author Dave Syer
|
||||
* @author Moritz Halbritter
|
||||
*/
|
||||
class StartupInfoLogger {
|
||||
|
||||
|
|
|
@ -222,44 +222,39 @@ class SpringBootJoranConfigurator extends JoranConfigurator {
|
|||
}
|
||||
|
||||
private Class<?> determineType(Model model, Supplier<Object> parentSupplier) {
|
||||
String className = null;
|
||||
if (model instanceof ComponentModel) {
|
||||
className = ((ComponentModel) model).getClassName();
|
||||
}
|
||||
if (className == null) {
|
||||
String tag = model.getTag();
|
||||
if (tag != null) {
|
||||
className = this.modelInterpretationContext.getDefaultNestedComponentRegistry()
|
||||
.findDefaultComponentTypeByTag(tag);
|
||||
if (className == null) {
|
||||
Class<?> type = inferTypeFromParent(parentSupplier, tag);
|
||||
if (type != null) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
String className = (model instanceof ComponentModel componentModel) ? componentModel.getClassName() : null;
|
||||
if (className != null) {
|
||||
className = this.modelInterpretationContext.getImport(className);
|
||||
return loadComponentType(className);
|
||||
return loadImportType(className);
|
||||
}
|
||||
String tag = model.getTag();
|
||||
if (tag != null) {
|
||||
className = this.modelInterpretationContext.getDefaultNestedComponentRegistry()
|
||||
.findDefaultComponentTypeByTag(tag);
|
||||
if (className != null) {
|
||||
return loadImportType(className);
|
||||
}
|
||||
return inferTypeFromParent(parentSupplier, tag);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private Class<?> loadImportType(String className) {
|
||||
return loadComponentType(this.modelInterpretationContext.getImport(className));
|
||||
}
|
||||
|
||||
private Class<?> inferTypeFromParent(Supplier<Object> parentSupplier, String tag) {
|
||||
Object parent = parentSupplier.get();
|
||||
if (parent != null) {
|
||||
try {
|
||||
Class<?> typeFromSetter = new PropertySetter(
|
||||
this.modelInterpretationContext.getBeanDescriptionCache(), parent)
|
||||
.getClassNameViaImplicitRules(tag, AggregationType.AS_COMPLEX_PROPERTY,
|
||||
this.modelInterpretationContext.getDefaultNestedComponentRegistry());
|
||||
if (typeFromSetter != null) {
|
||||
return typeFromSetter;
|
||||
}
|
||||
PropertySetter propertySetter = new PropertySetter(
|
||||
this.modelInterpretationContext.getBeanDescriptionCache(), parent);
|
||||
Class<?> typeFromPropertySetter = propertySetter.getClassNameViaImplicitRules(tag,
|
||||
AggregationType.AS_COMPLEX_PROPERTY,
|
||||
this.modelInterpretationContext.getDefaultNestedComponentRegistry());
|
||||
return typeFromPropertySetter;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
// Continue
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
|
|
@ -234,7 +234,6 @@ class LogbackConfigurationAotContributionTests {
|
|||
public static class Outer {
|
||||
|
||||
public void setImplementation(Implementation implementation) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -243,7 +242,6 @@ class LogbackConfigurationAotContributionTests {
|
|||
|
||||
@DefaultClass(Implementation.class)
|
||||
public void setContract(Contract contract) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue