Move Actuator Jersey infrastructure to spring-boot-jersey

This commit is contained in:
Stéphane Nicoll
2025-05-22 10:20:34 +02:00
committed by Phillip Webb
parent 92547def9d
commit bf38d1176d
34 changed files with 367 additions and 194 deletions

View File

@@ -32,8 +32,6 @@ dependencies {
optional("org.apache.tomcat.embed:tomcat-embed-core")
optional("org.aspectj:aspectjweaver")
optional("org.eclipse.angus:angus-mail")
optional("org.glassfish.jersey.core:jersey-server")
optional("org.glassfish.jersey.containers:jersey-container-servlet-core")
optional("org.hibernate.validator:hibernate-validator")
optional("org.influxdb:influxdb-java")
optional("org.springframework:spring-context-support")
@@ -57,13 +55,11 @@ dependencies {
testImplementation("io.projectreactor:reactor-test")
testImplementation("net.minidev:json-smart")
testImplementation("org.apache.logging.log4j:log4j-to-slf4j")
testImplementation("org.glassfish.jersey.media:jersey-media-json-jackson")
testRuntimeOnly(project(":spring-boot-project:spring-boot-http-codec"))
testRuntimeOnly("ch.qos.logback:logback-classic")
testRuntimeOnly("io.projectreactor.netty:reactor-netty-http")
testRuntimeOnly("jakarta.xml.bind:jakarta.xml.bind-api")
testRuntimeOnly("org.apache.tomcat.embed:tomcat-embed-el")
testRuntimeOnly("org.glassfish.jersey.ext:jersey-spring6")
testRuntimeOnly("org.hsqldb:hsqldb")
}

View File

@@ -1,360 +0,0 @@
/*
* Copyright 2012-2024 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.boot.actuate.endpoint.web.jersey;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.glassfish.jersey.process.Inflector;
import org.glassfish.jersey.server.ContainerRequest;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.Resource.Builder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* A factory for creating Jersey {@link Resource Resources} for {@link WebOperation web
* endpoint operations}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
public class JerseyEndpointResourceFactory {
/**
* Creates {@link Resource Resources} for the operations of the given
* {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param endpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param linksResolver resolver for determining links to available endpoints
* @param shouldRegisterLinks should register links
* @return the resources for the operations
*/
public Collection<Resource> createEndpointResources(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
EndpointLinksResolver linksResolver, boolean shouldRegisterLinks) {
List<Resource> resources = new ArrayList<>();
endpoints.stream()
.flatMap((endpoint) -> endpoint.getOperations().stream())
.map((operation) -> createResource(endpointMapping, operation))
.forEach(resources::add);
if (shouldRegisterLinks) {
Resource resource = createEndpointLinksResource(endpointMapping.getPath(), endpointMediaTypes,
linksResolver);
resources.add(resource);
}
return resources;
}
protected Resource createResource(EndpointMapping endpointMapping, WebOperation operation) {
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
String path = requestPredicate.getPath();
String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}",
"{" + matchAllRemainingPathSegmentsVariable + ": .*}");
}
return getResource(endpointMapping, operation, requestPredicate, path, null, null);
}
protected Resource getResource(EndpointMapping endpointMapping, WebOperation operation,
WebOperationRequestPredicate requestPredicate, String path, WebServerNamespace serverNamespace,
JerseyRemainingPathSegmentProvider remainingPathSegmentProvider) {
Builder resourceBuilder = Resource.builder()
.path(endpointMapping.getPath())
.path(endpointMapping.createSubPath(path));
resourceBuilder.addMethod(requestPredicate.getHttpMethod().name())
.consumes(StringUtils.toStringArray(requestPredicate.getConsumes()))
.produces(StringUtils.toStringArray(requestPredicate.getProduces()))
.handledBy(new OperationInflector(operation, !requestPredicate.getConsumes().isEmpty(), serverNamespace,
remainingPathSegmentProvider));
return resourceBuilder.build();
}
private Resource createEndpointLinksResource(String endpointPath, EndpointMediaTypes endpointMediaTypes,
EndpointLinksResolver linksResolver) {
Builder resourceBuilder = Resource.builder().path(endpointPath);
resourceBuilder.addMethod("GET")
.produces(StringUtils.toStringArray(endpointMediaTypes.getProduced()))
.handledBy(new EndpointLinksInflector(linksResolver));
return resourceBuilder.build();
}
/**
* {@link Inflector} to invoke the {@link WebOperation}.
*/
private static final class OperationInflector implements Inflector<ContainerRequestContext, Object> {
private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR;
private static final List<Function<Object, Object>> BODY_CONVERTERS;
static {
List<Function<Object, Object>> converters = new ArrayList<>();
converters.add(new ResourceBodyConverter());
if (ClassUtils.isPresent("reactor.core.publisher.Mono", OperationInflector.class.getClassLoader())) {
converters.add(new FluxBodyConverter());
converters.add(new MonoBodyConverter());
}
BODY_CONVERTERS = Collections.unmodifiableList(converters);
}
private final WebOperation operation;
private final boolean readBody;
private final WebServerNamespace serverNamespace;
private final JerseyRemainingPathSegmentProvider remainingPathSegmentProvider;
private OperationInflector(WebOperation operation, boolean readBody, WebServerNamespace serverNamespace,
JerseyRemainingPathSegmentProvider remainingPathSegments) {
this.operation = operation;
this.readBody = readBody;
this.serverNamespace = serverNamespace;
this.remainingPathSegmentProvider = remainingPathSegments;
}
@Override
public Response apply(ContainerRequestContext data) {
Map<String, Object> arguments = new HashMap<>();
if (this.readBody) {
arguments.putAll(extractBodyArguments(data));
}
arguments.putAll(extractPathParameters(data));
arguments.putAll(extractQueryParameters(data));
try {
JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext());
OperationArgumentResolver serverNamespaceArgumentResolver = OperationArgumentResolver
.of(WebServerNamespace.class, () -> this.serverNamespace);
InvocationContext invocationContext = new InvocationContext(securityContext, arguments,
serverNamespaceArgumentResolver,
new ProducibleOperationArgumentResolver(() -> data.getHeaders().get("Accept")));
Object response = this.operation.invoke(invocationContext);
return convertToJaxRsResponse(response, data.getRequest().getMethod());
}
catch (InvalidEndpointRequestException ex) {
return Response.status(Status.BAD_REQUEST).build();
}
}
@SuppressWarnings("unchecked")
private Map<String, String> extractBodyArguments(ContainerRequestContext data) {
Map<String, String> entity = ((ContainerRequest) data).readEntity(Map.class,
new ParameterizedTypeReference<Map<String, String>>() {
}.getType());
return (entity != null) ? entity : Collections.emptyMap();
}
private Map<String, Object> extractPathParameters(ContainerRequestContext requestContext) {
Map<String, Object> pathParameters = extract(requestContext.getUriInfo().getPathParameters());
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
String remainingPathSegments = getRemainingPathSegments(requestContext, pathParameters,
matchAllRemainingPathSegmentsVariable);
pathParameters.put(matchAllRemainingPathSegmentsVariable, tokenizePathSegments(remainingPathSegments));
}
return pathParameters;
}
private String getRemainingPathSegments(ContainerRequestContext requestContext,
Map<String, Object> pathParameters, String matchAllRemainingPathSegmentsVariable) {
if (this.remainingPathSegmentProvider != null) {
return this.remainingPathSegmentProvider.get(requestContext, matchAllRemainingPathSegmentsVariable);
}
return (String) pathParameters.get(matchAllRemainingPathSegmentsVariable);
}
private String[] tokenizePathSegments(String path) {
String[] segments = StringUtils.tokenizeToStringArray(path, PATH_SEPARATOR, false, true);
for (int i = 0; i < segments.length; i++) {
if (segments[i].contains("%")) {
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
}
}
return segments;
}
private Map<String, Object> extractQueryParameters(ContainerRequestContext requestContext) {
return extract(requestContext.getUriInfo().getQueryParameters());
}
private Map<String, Object> extract(MultivaluedMap<String, String> multivaluedMap) {
Map<String, Object> result = new HashMap<>();
multivaluedMap.forEach((name, values) -> {
if (!CollectionUtils.isEmpty(values)) {
result.put(name, (values.size() != 1) ? values : values.get(0));
}
});
return result;
}
private Response convertToJaxRsResponse(Object response, String httpMethod) {
if (response == null) {
boolean isGet = HttpMethod.GET.equals(httpMethod);
Status status = isGet ? Status.NOT_FOUND : Status.NO_CONTENT;
return Response.status(status).build();
}
if (!(response instanceof WebEndpointResponse<?> webEndpointResponse)) {
return Response.status(Status.OK).entity(convertIfNecessary(response)).build();
}
return Response.status(webEndpointResponse.getStatus())
.header("Content-Type", webEndpointResponse.getContentType())
.entity(convertIfNecessary(webEndpointResponse.getBody()))
.build();
}
private Object convertIfNecessary(Object body) {
for (Function<Object, Object> converter : BODY_CONVERTERS) {
body = converter.apply(body);
}
return body;
}
}
/**
* Body converter from {@link org.springframework.core.io.Resource} to
* {@link InputStream}.
*/
private static final class ResourceBodyConverter implements Function<Object, Object> {
@Override
public Object apply(Object body) {
if (body instanceof org.springframework.core.io.Resource) {
try {
return ((org.springframework.core.io.Resource) body).getInputStream();
}
catch (IOException ex) {
throw new IllegalStateException();
}
}
return body;
}
}
/**
* Body converter from {@link Mono} to {@link Mono#block()}.
*/
private static final class MonoBodyConverter implements Function<Object, Object> {
@Override
public Object apply(Object body) {
if (body instanceof Mono) {
return ((Mono<?>) body).block();
}
return body;
}
}
/**
* Body converter from {@link Flux} to {@link Flux#collectList Mono&lt;List&gt;}.
*/
private static final class FluxBodyConverter implements Function<Object, Object> {
@Override
public Object apply(Object body) {
if (body instanceof Flux) {
return ((Flux<?>) body).collectList();
}
return body;
}
}
/**
* {@link Inflector} to for endpoint links.
*/
private static final class EndpointLinksInflector implements Inflector<ContainerRequestContext, Response> {
private final EndpointLinksResolver linksResolver;
private EndpointLinksInflector(EndpointLinksResolver linksResolver) {
this.linksResolver = linksResolver;
}
@Override
public Response apply(ContainerRequestContext request) {
Map<String, Link> links = this.linksResolver
.resolveLinks(request.getUriInfo().getAbsolutePath().toString());
Map<String, Map<String, Link>> entity = OperationResponseBody.of(Collections.singletonMap("_links", links));
return Response.ok(entity).build();
}
}
private static final class JerseySecurityContext implements SecurityContext {
private final jakarta.ws.rs.core.SecurityContext securityContext;
private JerseySecurityContext(jakarta.ws.rs.core.SecurityContext securityContext) {
this.securityContext = securityContext;
}
@Override
public Principal getPrincipal() {
return this.securityContext.getUserPrincipal();
}
@Override
public boolean isUserInRole(String role) {
return this.securityContext.isUserInRole(role);
}
}
}

View File

@@ -1,83 +0,0 @@
/*
* Copyright 2012-2023 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.boot.actuate.endpoint.web.jersey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import org.glassfish.jersey.server.model.Resource;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
/**
* A factory for creating Jersey {@link Resource Resources} for health groups with
* additional path.
*
* @author Madhura Bhave
* @since 2.6.0
*/
public final class JerseyHealthEndpointAdditionalPathResourceFactory {
private final JerseyEndpointResourceFactory delegate = new JerseyEndpointResourceFactory();
private final Set<HealthEndpointGroup> groups;
private final WebServerNamespace serverNamespace;
public JerseyHealthEndpointAdditionalPathResourceFactory(WebServerNamespace serverNamespace,
HealthEndpointGroups groups) {
this.serverNamespace = serverNamespace;
this.groups = groups.getAllWithAdditionalPath(serverNamespace);
}
public Collection<Resource> createEndpointResources(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints) {
return endpoints.stream()
.flatMap((endpoint) -> endpoint.getOperations().stream())
.flatMap((operation) -> createResources(endpointMapping, operation))
.toList();
}
private Stream<Resource> createResources(EndpointMapping endpointMapping, WebOperation operation) {
WebOperationRequestPredicate requestPredicate = operation.getRequestPredicate();
String matchAllRemainingPathSegmentsVariable = requestPredicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
List<Resource> resources = new ArrayList<>();
for (HealthEndpointGroup group : this.groups) {
AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath();
if (additionalPath != null) {
resources.add(this.delegate.getResource(endpointMapping, operation, requestPredicate,
additionalPath.getValue(), this.serverNamespace,
(data, pathSegmentsVariable) -> data.getUriInfo().getPath()));
}
}
return resources.stream();
}
return Stream.empty();
}
}

View File

@@ -1,31 +0,0 @@
/*
* Copyright 2012-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.boot.actuate.endpoint.web.jersey;
import jakarta.ws.rs.container.ContainerRequestContext;
/**
* Strategy interface used to provide the remaining path segments for a Jersey actuator
* endpoint.
*
* @author Madhura Bhave
*/
interface JerseyRemainingPathSegmentProvider {
String get(ContainerRequestContext requestContext, String matchAllRemainingPathSegmentsVariable);
}

View File

@@ -1,20 +0,0 @@
/*
* Copyright 2012-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.
*/
/**
* Jersey support for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.web.jersey;