Springfox Swagger mit externem Markdown erweitern

4 Kommentare

Dokumentationen von REST APIs sind für deren Anwender essentiell. Ohne eine ansprechende und verständliche Dokumentation lässt sich eine solche API bestenfalls nach dem „trial and error“-Prinzip anwenden. Mit Swagger wurde eine Spezifikation geschaffen, die API-Dokumentationen im JSON- oder YAML-Format vorgibt. Springfox baut auf dieser Spezifikation auf und bietet, beispielsweise per Annotationen, die Möglichkeit in Spring-Projekten die Dokumentation direkt innerhalb des REST Controllers zu hinterlegen. Über den automatisch erzeugten /api-docs Endpunkt wird eine swagger konforme, generierte API-Dokumentation zur Verfügung gestellt. In diesem Artikel möchte ich eine Möglichkeit zeigen wie man diese Dokumentation durch das Inkludieren externer Markdown-Dateien erweitern kann.

Swagger und Springfox

Swagger ist ein Open-Source-Framework das unter anderem Hilfsmittel zur Dokumentation von REST APIs zur Verfügung stellt. Neben einer Spezifikation für eine JSON-basierte API-Dokumentation werden Tools und Frameworks zum Erzeugen dieser Dokumentation angeboten. Diese JSON Representation kann beispielsweise mit Hilfe von Swagger-UI im Browser visualisiert werden. Springfox ist eine Spring-Implementierung der Swagger-Spezifikation. Springfox bietet anhand von Annotationen die Möglichkeit API-Dokumentationen automatisch zu generieren und anhand des /api-docs Endpoints bereitzustellen.

Motivation

Ein Ziel von Springfox ist die Zentralisierung und Versionierung der Dokumentation, sowie die automatische Bereitstellung. Werden die Beschreibungen der REST-Endpunkte allerdings zu ausführlich, so dass die entsprechenden Java-Klassen zum Großteil aus API-Dokumentation bestehen, sollte man das Konzept der „Dokumentation im Code“ zumindest hinterfragen. Eine Alternative zur Generierung der Dokumentation wäre beispielsweise das Bereitstellen einer statischen JSON-Dokumentation über einen definierten REST-Endpunkt. Eine solche statische Dokumentation lässt sich über den Swagger-Editor erzeugen. Die erzeugte Datei lässt sich ebenfalls versionieren, ist allerdings schwieriger zu lesen und es besteht im Code keine direkte sichtbare Beziehung zu den entsprechenden Endpunkten. Um diese Nachteile zu umgehen zeige ich im folgenden eine Möglichkeit der Verknüpfung von Markdown-Ressourcen mit den jeweiligen API-Dokumentationen. Sowohl Swagger-UI als auch der Swagger-Editor unterstützen Markdown innerhalb der Endpunktbeschreibungen.

Springfox Lifecycle

Springfox parsed die Swagger-Dokumentation nachdem der Spring Context aufgebaut wurde. Intern nutzt Springfox hierfür das Spring-Plugin-Projekt. Mit Spring Plugin lassen sich Interfaces registrieren deren Implementierungen automatisch in PluginRegistries gesammelt werden. Springfox definiert unter anderem die OperationBuilderPluginRegistry mit Implementierungen des OperationBuilderPlugin-Interfaces. Eine Implementierung dieses Interfaces ist der OperationNotesReader der Springfox-Annotationen auswertet und die Beschreibung eines REST-Endpunktes erzeugt. Springfox implementiert für jede Swagger Annotation eine Reader-Klasse, welche die einzelnen Elemente der Swagger-Dokumentation anhand der Annotation Properties zusammenbaut. An dieser Stelle lassen sich eigene Annotationen bzw. eigene Implementierungen des OperationBuilderPlugin Interfaces anlegen, um wie im nächsten Abschnitt beschrieben externe Markdown-Dateien zu inkludieren.

Implementierung

Um eine eigene Annotation für Markdown-Dateien nutzen zu können, muss das OperationBuilderPlugin Interface implementiert werden. Eine Bean dieser Klasse wird durch die @Component Annotation beim Erzeugen des Spring Contexts angelegt. Die apply-Methode wird automatisch für alle Implementierungen dieses Interfaces vom Springfox DocumentationPluginsManager aufgerufen, so dass man sich um das Bootstrappen und Registrieren nicht selber kümmern muss.

@Component
@Order(SwaggerPluginSupport.SWAGGER_PLUGIN_ORDER)
public class OperationNotesResourcesReader implements OperationBuilderPlugin {
    private final DescriptionResolver descriptions;
 
    final static Logger logger = LoggerFactory.getLogger(OperationNotesResourcesReader.class);
 
    @Autowired
    public OperationNotesResourcesReader(DescriptionResolver descriptions) {
        this.descriptions = descriptions;
    }
 
    @Override
    public void apply(OperationContext context) {
 
        Optional<ApiNotes> methodAnnotation = context.findAnnotation(ApiNotes.class);
        if (methodAnnotation.isPresent() && StringUtils.hasText(methodAnnotation.get().value())) {
 
            final String mdFile = methodAnnotation.get().value();
            URL url = Resources.getResource(mdFile);
            String text;
            try {
                text = Resources.toString(url, Charsets.UTF_8);
            } catch (IOException e) {
                logger.error("Error while reading markdown description file {}", mdFile, e);
                text = "Markdown file " + mdFile + " not loaded";
            }
 
            context.operationBuilder().notes(descriptions.resolve(text));
        }
    }
 
    @Override
    public boolean supports(DocumentationType delimiter) {
        return SwaggerPluginSupport.pluginDoesApply(delimiter);
    }
}

Innerhalb der apply-Methode wird hier lediglich im classpath nach der Markdown resource gesucht und diese als String geladen. Die Annotation „ApiNotes“ habe ich selber definiert. Sie erwartet als Argument den Namen der Markdown resource. An dieser Stelle könnte man beispielsweise auch Inhalte aus einem CMS oder einer Datenbank verwenden um die Dokumentation mit Inhalten zu füllen.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ApiNotes {
    String value() default "";
}

Ein Beispiel einer RestController Methode mit der entwickelten Annotation könnte wie folgt aussehen:

    @ApiOperation(
            value = "Add a comment",
            consumes = "application/json" ,
            produces = "application/json" )
    @ApiNotes("addComment.md")
    @ApiResponses(value = {
            @ApiResponse(code = 201, message = "The comment was created")})
    @RequestMapping(value= { "/" }, method = { RequestMethod.POST })
    public void addComment(@RequestBody(required = true) Comment comment) {
        ...
    }

Die hier genutzte addComment.md Markdown-Datei muss in dieser Implementierung im Classpath bzw. dem resource-Verzeichnis der Anwendung vorhanden sein. Die folgende Abbildung zeigt die Swagger Dokumentation für den oben dargestellten REST-Endpunkt addComment mit Markdown-Elementen wie Tabellen und Bildern. Für die Darstellung der Dokumentation wurde Swagger-UI verwendet.
Beispiel einer Swagger Dokumentation mit markdown Elementen

Fazit

Swagger bietet viele Möglichkeiten seine REST-Endpunkte zu dokumentieren und bereitzustellen. In diesem Post habe ich eine Möglichkeit gezeigt, wie die Spring Implementierung von Springfox erweitert werden kann um Dokumentation aus externen Quellen – aber mit Verknüpfung zu den Endpunkten – zu benutzen.

Markus Höfer

Markus arbeitet als Consultant und Entwickler im Bereich Big Data und verteilter Systeme. Er unterstützt seine Kunden bei der Konzeptionierung und Implementierung von Service Architekturen.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • Quirin

    Das sollte doch sicher:

    Optional methodAnnotation = context.findAnnotation(ApiNotes.class);

    sein.
    THX 4 Sharing

    • Markus Höfer

      18. Oktober 2017 von Markus Höfer

      Vielen Dank fürs aufmerksame Lesen, aber ich finde den Fehler nicht.
      Ich nutze findAnnotation exakt wie von dir beschrieben.
      Was übersehe ich?

  • Hayk

    Es fehlt eine TypDef ApiNotes.

    Optional methodAnnotation = context.findAnnotation(ApiNotes.class);

    Danke für die hilfreiche Artikel.

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.