wake-up-neo.com

Auflistung aller eingesetzten Rest-Endpunkte (Spring-Boot, Jersey)

Kann ich alle meine konfigurierten Rest-Endpunkte mit Federschuh auflisten? Der Aktor listet alle vorhandenen Pfade beim Start auf. Ich möchte etwas Ähnliches für meine benutzerdefinierten Dienste. Ich kann beim Start überprüfen, ob alle Pfade richtig konfiguriert sind, und diese Informationen für Clientaufrufe verwenden.

Wie mache ich das? Ich benutze @Path/@GET-Anmerkungen in meinen Service-Beans und registriere sie über ResourceConfig#registerClasses.

Gibt es eine Möglichkeit, die Konfiguration für alle Pfade abzufragen?

Update: Ich registriere die REST -Controller über

@Bean
public ResourceConfig resourceConfig() {
   return new ResourceConfig() {
    {  
      register(MyRestController.class);
    }
   };
}

Update2: Ich möchte sowas haben

GET /rest/mycontroller/info
POST /res/mycontroller/update
...

Motivation: Wenn die Spring-Boot-App gestartet wurde, möchte ich alle registrierten Controller und ihre Pfade ausdrucken, sodass ich nicht mehr raten kann, welche Endpunkte verwendet werden sollen.

17
Jan Galinski

Der wahrscheinlich beste Weg, dies zu tun, ist die Verwendung eines ApplicationEventListener . Von dort aus können Sie auf das Ereignis "Initialisierung der Anwendung abgeschlossen" warten und ResourceModel aus ApplicationEvent abrufen. Das ResourceModel enthält alle initialisierten Resources. Dann können Sie den Resource durchlaufen, wie andere bereits erwähnt haben. Nachfolgend finden Sie eine Implementierung. Ein Teil der Implementierung stammt aus der Implementierung 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);
        }
    }
}

Dann müssen Sie nur noch den Hörer bei Jersey registrieren. Sie können den Anwendungspfad aus dem JerseyProperties abrufen. Sie müssen es im Spring Boot application.properties Unter der Eigenschaft spring.jersey.applicationPath Einstellen. Dies ist der Root-Pfad, als ob Sie @ApplicationPath Für Ihre ResourceConfig -Unterklasse verwenden würden

@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()));
    }
}

Zu beachten ist, dass beim Jersey-Servlet das Laden beim Start nicht standardmäßig eingestellt ist. Dies bedeutet, dass Jersey erst bei der ersten Anforderung beim Start geladen wird. Der Listener wird also erst bei der ersten Anfrage ausgelöst. Ich habe ein Problem geöffnet , um möglicherweise eine Konfigurationseigenschaft abzurufen, aber in der Zwischenzeit haben Sie einige Optionen:

  1. Richten Sie Jersey anstelle eines Servlets als Filter ein. Der Filter wird beim Start geladen. Wenn Sie Jersey als Filter für die meisten Posts verwenden, verhält es sich eigentlich nicht anders. Um dies zu konfigurieren, müssen Sie lediglich eine Spring Boot-Eigenschaft in das Feld application.properties Einfügen.

    spring.jersey.type=filter
    
  2. Die andere Möglichkeit besteht darin, das Jersey ServletRegistrationBean zu überschreiben und seine loadOnStartup -Eigenschaft festzulegen. Hier ist eine Beispielkonfiguration. Ein Teil der Implementierung wurde direkt aus dem JerseyAutoConfiguration übernommen.

    @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());
            }
        }
    }
    

AKTUALISIEREN

Es sieht also so aus, als würde Spring Boot die Eigenschaft load-on-startup hinzufügen, damit wir das Jersey ServletRegistrationBean. Wird in Boot 1.4.0 hinzugefügt

14
Paul Samsotha

Alle REST Endpunkte werden im /actuator/mappings Endpunkt aufgelistet.

Aktivieren Sie den Mappings-Endpunkt mit der Eigenschaft management.endpoints.web.exposure.include

Zum Beispiel: management.endpoints.web.exposure.include=env,info,health,httptrace,logfile,metrics,mappings

1
Frischling

Nachdem die Anwendung vollständig gestartet wurde, können Sie ServerConfig fragen:

ResourceConfig instance; 
ServerConfig scfg = instance.getConfiguration();
Set<Class<?>> classes = scfg.getClasses();

classes enthält alle zwischengespeicherten Endpunktklassen.

Von den API-Dokumenten für javax.ws.rs.core.Configuration:

Rufen Sie den unveränderlichen Satz registrierter JAX-RS-Komponentenklassen (z. B. Anbieter- oder Feature-Klassen) ab, die im Rahmen der konfigurierbaren Instanz instanziiert, eingefügt und verwendet werden sollen. 

Im Init-Code Ihrer Anwendung ist dies jedoch nicht möglich. Die Klassen sind möglicherweise noch nicht vollständig geladen.

Mit den Klassen können Sie sie nach Ressourcen durchsuchen:

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;
}
1
Johannes Jander

Können Sie ResourceConfig#getResources für Ihr ResourceConfig-Objekt verwenden und dann die erforderlichen Informationen abrufen, indem Sie das Set<Resource> durchlaufen, das zurückgegeben wird?

Entschuldigung, würde es versuchen, aber ich habe nicht die Resources, um es jetzt zu tun. :-p

1

Was ist mit RequestMappingHandlerMapping, die alle Endpunktinformationen enthalten?.

Siehe meine Antwort unter Wie kann ich von einem Controller aus auf alle verfügbaren Routen einer REST - API zugreifen? .

0
Thomas Decaux