java - starter - Listado de todos los puntos finales de descanso desplegados(spring-boot, jersey)
spring boot mybatis mapper xml (5)
¿Es posible enumerar todos mis puntos finales de descanso configurados con el arranque de primavera? El actuador enumera todas las rutas existentes en el inicio, quiero algo similar para mis servicios personalizados, por lo que puedo verificar en el inicio si todas las rutas están configuradas correctamente y usar esta información para las llamadas de los clientes.
¿Cómo hago esto?
Uso anotaciones
@Path
/
@GET
en mis beans de servicio y las registro a través de
ResourceConfig#registerClasses
.
¿Hay alguna manera de consultar la configuración para todas las rutas?
Actualización: registro los controladores REST a través de
@Bean
public ResourceConfig resourceConfig() {
return new ResourceConfig() {
{
register(MyRestController.class);
}
};
}
Actualización2: quiero tener algo como
GET /rest/mycontroller/info
POST /res/mycontroller/update
...
Motivación: cuando se inició la aplicación Spring-boot, quiero imprimir todos los controladores registrados y sus rutas, para que pueda dejar de adivinar qué puntos finales utilizar.
¿Puede usar
ResourceConfig#getResources
en su objeto
ResourceConfig
luego obtener la información que necesita iterando a través de
Set<Resource>
que devuelve?
Disculpas, lo intentaría, pero no tengo los recursos para hacerlo ahora. :-pag
¿Qué pasa con el uso de
RequestMappingHandlerMapping
que contiene toda la información de los puntos finales?
Vea mi respuesta en ¿Cómo acceder a todas las rutas disponibles de una API REST desde un controlador? .
Después de que la aplicación se haya iniciado por completo, puede preguntarle a
ServerConfig
:
ResourceConfig instance;
ServerConfig scfg = instance.getConfiguration();
Set<Class<?>> classes = scfg.getClasses();
classes
contiene todas las clases de punto final en caché.
De
los documentos de API
para
javax.ws.rs.core.Configuration
:
Obtenga el conjunto inmutable de clases de componente JAX-RS registradas (como proveedor o característica) para instanciar, inyectar y utilizar en el ámbito de la instancia configurable.
Sin embargo, no puede hacer esto en el código de inicio de su aplicación, es posible que las clases aún no estén completamente cargadas.
Con las clases, puede escanearlos en busca de los recursos:
public Map<String, List<InfoLine>> scan(Class baseClass) {
Builder builder = Resource.builder(baseClass);
if (null == builder)
return null;
Resource resource = builder.build();
String uriPrefix = "";
Map<String, List<InfoLine>> info = new TreeMap<>();
return process(uriPrefix, resource, info);
}
private Map<String, List<InfoLine>> process(String uriPrefix, Resource resource, Map<String, List<InfoLine>> info) {
String pathPrefix = uriPrefix;
List<Resource> resources = new ArrayList<>();
resources.addAll(resource.getChildResources());
if (resource.getPath() != null) {
pathPrefix = pathPrefix + resource.getPath();
}
for (ResourceMethod method : resource.getAllMethods()) {
if (method.getType().equals(ResourceMethod.JaxrsType.SUB_RESOURCE_LOCATOR)) {
resources.add(
Resource.from(
resource.getResourceLocator()
.getInvocable()
.getDefinitionMethod()
.getReturnType()
)
);
}
else {
List<InfoLine> paths = info.get(pathPrefix);
if (null == paths) {
paths = new ArrayList<>();
info.put(pathPrefix, paths);
}
InfoLine line = new InfoLine();
line.pathPrefix = pathPrefix;
line.httpMethod = method.getHttpMethod();
paths.add(line);
System.out.println(method.getHttpMethod() + "/t" + pathPrefix);
}
}
for (Resource childResource : resources) {
process(pathPrefix, childResource, info);
}
return info;
}
private class InfoLine {
public String pathPrefix;
public String httpMethod;
}
Probablemente la mejor manera de hacerlo es usar un
ApplicationEventListener
.
Desde allí puede escuchar el evento "la aplicación terminó de inicializarse" y obtener el
ResourceModel
del
ApplicationEvent
.
ResourceModel
tendrá todos los
Resource
inicializados.
Entonces puede atravesar el
Resource
como otros han mencionado.
A continuación se muestra una implementación.
Parte de la implementación se ha tomado de la implementación
DropwizardResourceConfig
.
import com.fasterxml.classmate.ResolvedType;
import com.fasterxml.classmate.TypeResolver;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import org.glassfish.jersey.server.model.Resource;
import org.glassfish.jersey.server.model.ResourceMethod;
import org.glassfish.jersey.server.model.ResourceModel;
import org.glassfish.jersey.server.monitoring.ApplicationEvent;
import org.glassfish.jersey.server.monitoring.ApplicationEventListener;
import org.glassfish.jersey.server.monitoring.RequestEvent;
import org.glassfish.jersey.server.monitoring.RequestEventListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class EndpointLoggingListener implements ApplicationEventListener {
private static final TypeResolver TYPE_RESOLVER = new TypeResolver();
private final String applicationPath;
private boolean withOptions = false;
private boolean withWadl = false;
public EndpointLoggingListener(String applicationPath) {
this.applicationPath = applicationPath;
}
@Override
public void onEvent(ApplicationEvent event) {
if (event.getType() == ApplicationEvent.Type.INITIALIZATION_APP_FINISHED) {
final ResourceModel resourceModel = event.getResourceModel();
final ResourceLogDetails logDetails = new ResourceLogDetails();
resourceModel.getResources().stream().forEach((resource) -> {
logDetails.addEndpointLogLines(getLinesFromResource(resource));
});
logDetails.log();
}
}
@Override
public RequestEventListener onRequest(RequestEvent requestEvent) {
return null;
}
public EndpointLoggingListener withOptions() {
this.withOptions = true;
return this;
}
public EndpointLoggingListener withWadl() {
this.withWadl = true;
return this;
}
private Set<EndpointLogLine> getLinesFromResource(Resource resource) {
Set<EndpointLogLine> logLines = new HashSet<>();
populate(this.applicationPath, false, resource, logLines);
return logLines;
}
private void populate(String basePath, Class<?> klass, boolean isLocator,
Set<EndpointLogLine> endpointLogLines) {
populate(basePath, isLocator, Resource.from(klass), endpointLogLines);
}
private void populate(String basePath, boolean isLocator, Resource resource,
Set<EndpointLogLine> endpointLogLines) {
if (!isLocator) {
basePath = normalizePath(basePath, resource.getPath());
}
for (ResourceMethod method : resource.getResourceMethods()) {
if (!withOptions && method.getHttpMethod().equalsIgnoreCase("OPTIONS")) {
continue;
}
if (!withWadl && basePath.contains(".wadl")) {
continue;
}
endpointLogLines.add(new EndpointLogLine(method.getHttpMethod(), basePath, null));
}
for (Resource childResource : resource.getChildResources()) {
for (ResourceMethod method : childResource.getAllMethods()) {
if (method.getType() == ResourceMethod.JaxrsType.RESOURCE_METHOD) {
final String path = normalizePath(basePath, childResource.getPath());
if (!withOptions && method.getHttpMethod().equalsIgnoreCase("OPTIONS")) {
continue;
}
if (!withWadl && path.contains(".wadl")) {
continue;
}
endpointLogLines.add(new EndpointLogLine(method.getHttpMethod(), path, null));
} else if (method.getType() == ResourceMethod.JaxrsType.SUB_RESOURCE_LOCATOR) {
final String path = normalizePath(basePath, childResource.getPath());
final ResolvedType responseType = TYPE_RESOLVER
.resolve(method.getInvocable().getResponseType());
final Class<?> erasedType = !responseType.getTypeBindings().isEmpty()
? responseType.getTypeBindings().getBoundType(0).getErasedType()
: responseType.getErasedType();
populate(path, erasedType, true, endpointLogLines);
}
}
}
}
private static String normalizePath(String basePath, String path) {
if (path == null) {
return basePath;
}
if (basePath.endsWith("/")) {
return path.startsWith("/") ? basePath + path.substring(1) : basePath + path;
}
return path.startsWith("/") ? basePath + path : basePath + "/" + path;
}
private static class ResourceLogDetails {
private static final Logger logger = LoggerFactory.getLogger(ResourceLogDetails.class);
private static final Comparator<EndpointLogLine> COMPARATOR
= Comparator.comparing((EndpointLogLine e) -> e.path)
.thenComparing((EndpointLogLine e) -> e.httpMethod);
private final Set<EndpointLogLine> logLines = new TreeSet<>(COMPARATOR);
private void log() {
StringBuilder sb = new StringBuilder("/nAll endpoints for Jersey application/n");
logLines.stream().forEach((line) -> {
sb.append(line).append("/n");
});
logger.info(sb.toString());
}
private void addEndpointLogLines(Set<EndpointLogLine> logLines) {
this.logLines.addAll(logLines);
}
}
private static class EndpointLogLine {
private static final String DEFAULT_FORMAT = " %-7s %s";
final String httpMethod;
final String path;
final String format;
private EndpointLogLine(String httpMethod, String path, String format) {
this.httpMethod = httpMethod;
this.path = path;
this.format = format == null ? DEFAULT_FORMAT : format;
}
@Override
public String toString() {
return String.format(format, httpMethod, path);
}
}
}
Entonces solo necesita registrar al oyente con Jersey.
Puede obtener la ruta de la aplicación de
JerseyProperties
.
Deberá haberlo configurado en la
application.properties
Spring Boot
application.properties
bajo la propiedad
spring.jersey.applicationPath
.
Esta será la ruta raíz, como si fuera a usar
@ApplicationPath
en su subclase
ResourceConfig
@Bean
public ResourceConfig getResourceConfig(JerseyProperties jerseyProperties) {
return new JerseyConfig(jerseyProperties);
}
...
public class JerseyConfig extends ResourceConfig {
public JerseyConfig(JerseyProperties jerseyProperties) {
register(HelloResource.class);
register(new EndpointLoggingListener(jerseyProperties.getApplicationPath()));
}
}
Una cosa a tener en cuenta es que la carga al inicio no está configurada por defecto en el servlet Jersey. Lo que esto significa es que Jersey no se cargará en el inicio hasta la primera solicitud. Por lo tanto, no verá el escucha activado hasta la primera solicitud. Abrí un problema para obtener una propiedad de configuración, pero mientras tanto, tienes un par de opciones:
-
Configure Jersey como filtro, en lugar de un servlet. El filtro se cargará al inicio. Usar Jersey como filtro, para la mayoría de las publicaciones, realmente no se comporta de manera diferente. Para configurar esto, solo necesita agregar una propiedad Spring Boot en la
application.properties
spring.jersey.type=filter
-
La otra opción es anular Jersey
ServletRegistrationBean
y establecer su propiedadloadOnStartup
. Aquí hay un ejemplo de configuración. Parte de la implementación se ha tomado directamente deJerseyAutoConfiguration
@SpringBootApplication public class JerseyApplication { public static void main(String[] args) { SpringApplication.run(JerseyApplication.class, args); } @Bean public ResourceConfig getResourceConfig(JerseyProperties jerseyProperties) { return new JerseyConfig(jerseyProperties); } @Bean public ServletRegistrationBean jerseyServletRegistration( JerseyProperties jerseyProperties, ResourceConfig config) { ServletRegistrationBean registration = new ServletRegistrationBean( new ServletContainer(config), parseApplicationPath(jerseyProperties.getApplicationPath()) ); addInitParameters(registration, jerseyProperties); registration.setName(JerseyConfig.class.getName()); registration.setLoadOnStartup(1); return registration; } private static String parseApplicationPath(String applicationPath) { if (!applicationPath.startsWith("/")) { applicationPath = "/" + applicationPath; } return applicationPath.equals("/") ? "/*" : applicationPath + "/*"; } private void addInitParameters(RegistrationBean registration, JerseyProperties jersey) { for (Entry<String, String> entry : jersey.getInit().entrySet()) { registration.addInitParameter(entry.getKey(), entry.getValue()); } } }
ACTUALIZAR
Por lo tanto, parece que Spring Boot
agregará la propiedad de
load-on-startup
, por lo que no tenemos que anular el
ServletRegistrationBean
Jersey.
Se agregará en el arranque 1.4.0
Todos los puntos finales REST se enumeran en
/actuator/mappings
endpoint.
Active el punto final de las asignaciones con la propiedad
management.endpoints.web.exposure.include
Por ejemplo:
management.endpoints.web.exposure.include=env,info,health,httptrace,logfile,metrics,mappings