読者です 読者をやめる 読者になる 読者になる

Spring BootでSpringFox(Swagger)を試す

Spring BootでSpringFox(Swagger)を試したメモです。

目次

SpringFoxでAPIドキュメントを生成してみました。

SpringFoxのドキュメントのサンプルが分かりやすいですのでこちらを参考にしました。
Springfox Reference Documentation

SpringFox

dependency

Maven dependencyにはspringfox-swagger2とspringfox-swagger-uiを追加。

<dependencies>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.6.1</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.6.1</version>
    </dependency>
</dependencies>

API生成の対象クラス

以前使ったコントローラを再使用。
GET, POST, UPDATE, DELETEのそれぞれのメソッドを用意しました。

PeopleController.java

@Controller
public class PeopleController {
    @ResponseBody
    @RequestMapping(value = "/api/people/{country}", method = RequestMethod.GET)
    public People getPeople(@PathVariable String country) {
        People people = new People();
        people.setCountry("Japan");
        people.setYear(2001);
        people.setPopulation(1_000_000);
        return people;
    }

    @ResponseBody
    @RequestMapping(value = "/api/people", method = RequestMethod.GET)
    public List<People> getPeopleList() {
        People japan = new People();
        japan.setCountry("Japan");
        japan.setYear(2001);
        japan.setPopulation(1_000_000);

        People america = new People();
        america.setCountry("America");
        america.setYear(2001);
        america.setPopulation(2_000_000);

        return Arrays.asList(japan, america);
    }

    @ResponseBody
    @RequestMapping(value = "/api/people", method = RequestMethod.POST)
    public int postPeople(@RequestBody People people) {
        // 登録処理
        // 登録件数を返す
        return 1;
    }

    @ResponseBody
    @RequestMapping(value = "/api/people", method = RequestMethod.PUT)
    public int putPeople(@RequestBody People people) {
        // 更新処理
        // 更新件数を返す
        return 2;
    }

    @ResponseBody
    @RequestMapping(value = "/api/people/{country}", method = RequestMethod.DELETE)
    public int deletePeople(@PathVariable String country) throws Exception {
        // 削除処理
        // 削除件数を返す
        return 3;
    }

    @Data
    @NoArgsConstructor
    @FieldDefaults(level = AccessLevel.PRIVATE)
    public static class People {
        String country;
        int year;
        int population;
    }
}

SpringFox Configuration

SpringFoxの最小の設定でやってみます。

(1) SpringFoxを有効にするアノテーションです。
(2) DocketがSwaggerの設定を行うインターフェースになります。
(3) DocumentationTypeにSwagger2を指定します。
(4) select()でApiSelectorBuilderを生成
(5) apis()で対象とするRequestHandlerを選択します。RequestHandlerSelectors.any()ですべてのRequestHandlerを対象にしています。
(6) paths()で対象とするパスを選択します。PathSelectors.any()ですべてのパスを対象にしています。
(7) build()でDocketを生成します。

SpringFoxConfigMinimum.java

@Configuration
@EnableSwagger2 // (1)
public class SpringFoxConfigMinimum {
    @Bean
    public Docket springFoxExampleDoc() { // (2)
        return new Docket(DocumentationType.SWAGGER_2) // (3)
                .select() // (4)
                    .apis(RequestHandlerSelectors.any()) // (5)
                    .paths(PathSelectors.any()) // (6)
                    .build(); // (7)
    }
}

実行

SpringBootアプリケーションを起動して、
localhost:8080/swagger-ui.htmlがデフォルトのURLになっているのでアクセスしてみます。
するとAPIドキュメントが表示されました。
あれだけのConfigurationを設定するだけで出来てしまうのですごいです!!

f:id:pppurple:20161206035904p:plain

見てみると、SpringBootのErrorControllerもAPIドキュメント対象になっています。
これは、対象をPathSelectors.any()ですべてを対象にしたためです。
ErrorControllerを外すのは後ほど設定してみます。

確認

PeopleControllerのそれぞれのメソッドを確認してみます。

GETメソッド

パスにcountryを取り、Peopleオブジェクトを返すGETメソッド。

    @ResponseBody
    @RequestMapping(value = "/api/people/{country}", method = RequestMethod.GET)
    public People getPeople(@PathVariable String country) {
        People people = new People();
        people.setCountry("Japan");
        people.setYear(2001);
        people.setPopulation(1_000_000);
        return people;
    }

生成されたAPIを確認すると、
Parametersにparameter typeがpathでcountryになっています。
ResponseClassにはPeopleオブジェクトがjsonシリアライズされて返っています。
f:id:pppurple:20161215004927p:plain

リストでPeopleオブジェクトを返すGETメソッド。

    @ResponseBody
    @RequestMapping(value = "/api/people", method = RequestMethod.GET)
    public List<People> getPeopleList() {
        People japan = new People();
        japan.setCountry("Japan");
        japan.setYear(2001);
        japan.setPopulation(1_000_000);

        People america = new People();
        america.setCountry("America");
        america.setYear(2001);
        america.setPopulation(2_000_000);

        return Arrays.asList(japan, america);
    }

生成されたAPIを確認すると、
ResponseClassにはPeopleオブジェクトがjsonの配列でシリアライズされて返っています。
f:id:pppurple:20161215004936p:plain

POSTメソッド

リクエストボティにPeopleオブジェクトを取り、登録件数をintで返すPOSTメソッド。

    @ResponseBody
    @RequestMapping(value = "/api/people", method = RequestMethod.POST)
    public int postPeople(@RequestBody People people) {
        // 登録処理
        // 登録件数を返す
        return 1;
    }

生成されたAPIを確認すると、
Parametersにparameter typeがbodyでPeopleのjsonになっています。
ResponseClassにはint32が返っているのが分かります。
f:id:pppurple:20161215010214p:plain

PUTメソッド

リクエストボティにPeopleオブジェクトを取り、更新件数をintで返すPUTメソッド。

    @ResponseBody
    @RequestMapping(value = "/api/people", method = RequestMethod.PUT)
    public int putPeople(@RequestBody People people) {
        // 更新処理
        // 更新件数を返す
        return 2;
    }

生成されたAPIを確認すると、
Parametersにparameter typeがbodyでPeopleのjsonになっています。
ResponseClassにはint32が返っているのが分かります。
f:id:pppurple:20161215010221p:plain

DELETEメソッド

パスにcountryを取り、削除件数をintで返すDELETEメソッド。

    @ResponseBody
    @RequestMapping(value = "/api/people/{country}", method = RequestMethod.DELETE)
    public int deletePeople(@PathVariable String country) throws Exception {
        // 削除処理
        // 削除件数を返す
        return 3;
    }

生成されたAPIを確認すると、
Parametersにparameter typeがpathでcountryになっています。
ResponseClassにはint32が返っているのが分かります。
f:id:pppurple:20161215010227p:plain

他の設定

他の設定を確認するために下記のクラスを追加。

CountryController.java

@Controller
public class CountryController {
    @ResponseBody
    @RequestMapping(value = "/api/country/now", method = RequestMethod.GET)
    public Date getDate() {
        return new Date();
    }

    @ResponseBody
    @RequestMapping(value = "/api/country/{countryName}/{cityName}", method = RequestMethod.GET)
    public MyResponseEntityWithStatus<City> getCity(@PathVariable String country, @PathVariable String city) {
        City yokohama = new City("yokohama", 100);
        HttpStatus status = HttpStatus.OK;
        return new MyResponseEntityWithStatus<>(yokohama, status);
    }

    @ResponseBody
    @RequestMapping(value = "/api/country/{countryName}", method = RequestMethod.GET)
    public MyResponseEntity<List<Country>> getCountry(@PathVariable String country) {
        Country america = new Country("America", "English", 1_000_000_000);
        Country japan = new Country("Japan", "japanese", 1_000_000);
        List<Country> countries = Arrays.asList(america, japan);
        return new MyResponseEntity<>(countries);
    }

    @ApiIgnore(value = "this is dummy")
    @ResponseBody
    @RequestMapping(value = "/api/country/dummy", method = RequestMethod.GET)
    public String dummy() {
        return "dummy country";
    }

    @Data
    @AllArgsConstructor
    public static class Country {
        String name;
        String language;
        int population;
    }

    @Data
    @AllArgsConstructor
    public static class City {
        String name;
        int population;
    }

    @Data
    @AllArgsConstructor
    public static class MyResponseEntity<T> {
        T res;
    }

    @Data
    @AllArgsConstructor
    public static class MyResponseEntityWithStatus<T> {
        T res;
        HttpStatus httpStatus;
    }
}
paths()

paths()で対象とするRequestHandlerを選択できます。
peopleControllerとcountryControllerだけ対象にする場合、こんな感じで設定できます。

@Configuration
@EnableSwagger2
public class SpringFoxConfigPaths {
    @Bean
    public Docket springFoxExampleDoc() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(paths())
                    .build();
    }

    private Predicate<String> paths() {
        return or(
                regex("/api/people.*"),
                regex("/api/country.*")
        );
    }
}

確認すると、peopleControllerとcountryControllerだけ表示されており、
paths(PathSelectors.any())を指定した場合に表示されていたbasic error controllerが対象外になっています。

f:id:pppurple:20161215015306p:plain

basic error controllerを対象外にしたいだけの場合は、下記のようにも設定できます。

    private Predicate<String> paths() {
        return not(
                regex("/error")
        );
    }
pathMapping()

pathMapping()でservletのパスマッピングがある場合に、パスマッピングを指定できます。
pathMapping()の引数で指定した文字列がパスのprefixとして付与されます。

@Configuration
@EnableSwagger2
public class SpringFoxConfigPathMapping {
    @Bean
    public Docket springFoxExampleDoc() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(PathSelectors.any())
                    .build()
                .pathMapping("/prefix_path");
    }
}

見てみるとパスのprefixとして指定した文字列が付与されています。

f:id:pppurple:20161215015815p:plain

apiInfo()

apiInfo()でAPIのメタ情報を設定できます。
(1) APIのタイトル
(2) APIのdescription
(3) APIのバージョン
(4) APIのライセンス
(5) APIのライセンス情報のURL

@Configuration
@EnableSwagger2
public class SpringFoxConfigApiInfo {
    @Bean
    public Docket springFoxExampleDoc() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(PathSelectors.any())
                    .build()
                .apiInfo(apiInfo());
    }
    
    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("My SpringFox Example Api") // (1)
                .description("My SpringFox description xxxxxx.") // (2)
                .version("1.0") // (3)
                .license("Apache License v2.0") // (4)
                .licenseUrl("https://www.apache.org/licenses/LICENSE-2.0") // (5)
                .build();
    }
}

確認してみると、ApiInfo()で設定した情報が表示されているのがわかります。

f:id:pppurple:20161215021238p:plain

@ApiIgnore

@ApiIgnoreを指定すると、ドキュメント対象外にします。

下記のdummy()メソッドに@ApiIgnoreを指定。

    @ApiIgnore(value = "this is dummy")
    @ResponseBody
    @RequestMapping(value = "/api/country/dummy", method = RequestMethod.GET)
    public String dummy() {
        return "dummy country";
    }

確認すると、dummy()メソッドが表示されないことがわかります。

f:id:pppurple:20161218192823p:plain

directModelSubstitute()

directModelSubstitute()で第1引数で指定したクラスを、第2引数で指定したクラスに置き換えます。

Date型を返す下記のようなメソッドの場合。

    @ResponseBody
    @RequestMapping(value = "/api/country/now", method = RequestMethod.GET)
    public Date getDate() {
        return new Date();
    }

directModelSubstitute()でDateをStringに置き換えるように指定。

@Configuration
@EnableSwagger2
public class SpringFoxConfigDirectModelSubstitute {
    @Bean
    public Docket springFoxExampleDoc() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(PathSelectors.any())
                    .build()
                .directModelSubstitute(Date.class, String.class);
    }
}

確認してみると、Response ClassがStringに置き換わっています。

f:id:pppurple:20161218184059p:plain

genericModelSubstitutes()

genericModelSubstitutes()は一つの型変数を持つクラスClazz<T>をTへ置き換えます。

下記のCityクラスを型変数に持つ、MyResponseEntityWithStatus<City>を返すメソッドの場合。

    @ResponseBody
    @RequestMapping(value = "/api/country/{countryName}/{cityName}", method = RequestMethod.GET)
    public MyResponseEntityWithStatus<City> getCity(@PathVariable String country, @PathVariable String city) {
        City yokohama = new City("yokohama", 100);
        HttpStatus status = HttpStatus.OK;
        return new MyResponseEntityWithStatus<>(yokohama, status);
    }

    @Data
    @AllArgsConstructor
    public static class City {
        String name;
        int population;
    }

    @Data
    @AllArgsConstructor
    public static class MyResponseEntityWithStatus<T> {
        T res;
        HttpStatus httpStatus;
    }

下記の様にgenericModelSubstitutes(MyResponseEntityWithStatus.class)を指定すると、Cityで置き換えます。

@Configuration
@EnableSwagger2
public class SpringFoxConfigGenericModelSubstitutes {
    @Bean
    public Docket springFoxExampleDoc() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(PathSelectors.any())
                    .build()
                .genericModelSubstitutes(MyResponseEntityWithStatus.class);
    }
}

何も指定しないと、下記の様にMyResponseEntityWithStatus<City>を返しますが、
f:id:pppurple:20161218191011p:plain

確認してみると、Response ClassでCityが返っているのがわかります。
f:id:pppurple:20161218191005p:plain

alternateTypeRules()

alternateTypeRules()は複雑な変換ルールを指定できます。

下記のようにネストした型変数MyResponseEntity<List<Country>>を返すメソッドの場合。

    @ResponseBody
    @RequestMapping(value = "/api/country/{countryName}", method = RequestMethod.GET)
    public MyResponseEntity<List<Country>> getCountry(@PathVariable String country) {
        Country america = new Country("America", "English", 1_000_000_000);
        Country japan = new Country("Japan", "japanese", 1_000_000);
        List<Country> countries = Arrays.asList(america, japan);
        return new MyResponseEntity<>(countries);
    }

    @Data
    @AllArgsConstructor
    public static class Country {
        String name;
        String language;
        int population;
    }

    @Data
    @AllArgsConstructor
    public static class MyResponseEntity<T> {
        T res;
    }

下記の様にalternateTypeRulesを指定すると、Countryで置き換えます。

@Configuration
@EnableSwagger2
public class SpringFoxConfigAlternateTypeRules {
    @Autowired
    private TypeResolver typeResolver;

    @Bean
    public Docket springFoxExampleDoc() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(PathSelectors.any())
                    .build()
                .alternateTypeRules(
                        newRule(typeResolver.resolve(MyResponseEntity.class,
                                    typeResolver.resolve(List.class, Country.class)),
                                typeResolver.resolve(Country.class))
                );
    }
}

何も指定しないと、下記の様にMyResponseEntity<List<Country>>を返しますが、
f:id:pppurple:20161218192106p:plain

確認してみると、Response ClassでCountryが返っているのがわかります。
f:id:pppurple:20161218192111p:plain

configクラス全体

configの全体です。

package com.example.springfox.config;

import com.fasterxml.classmate.TypeResolver;
import com.google.common.base.Predicate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.Date;
import java.util.List;

import static com.google.common.base.Predicates.or;
import static springfox.documentation.builders.PathSelectors.regex;
import static springfox.documentation.schema.AlternateTypeRules.newRule;

import com.example.springfox.controller.CountryController.MyResponseEntity;
import com.example.springfox.controller.CountryController.MyResponseEntityWithStatus;
import com.example.springfox.controller.CountryController.Country;

@Configuration
@EnableSwagger2
public class SpringFoxConfig {
    @Autowired
    private TypeResolver typeResolver;

    @Bean
    public Docket springFoxExampleDoc() {
        return new Docket(DocumentationType.SWAGGER_2)
                .select()
                    .apis(RequestHandlerSelectors.any())
                    .paths(paths())
                    .build()
                .pathMapping("/")
                .directModelSubstitute(Date.class, String.class)
                .genericModelSubstitutes(MyResponseEntityWithStatus.class)
                .alternateTypeRules(
                        newRule(typeResolver.resolve(MyResponseEntity.class,
                                    typeResolver.resolve(List.class, Country.class)),
                                typeResolver.resolve(Country.class))
                )
                .apiInfo(apiInfo());
    }

    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("My SpringFox Example Api")
                .description("My SpringFox description xxxxxx.")
                .version("1.0")
                .license("Apache License v2.0")
                .licenseUrl("https://www.apache.org/licenses/LICENSE-2.0")
                .build();
    }

    private Predicate<String> paths() {
        return or(
                regex("/api/people.*"),
                regex("/api/country.*")
        );
    }
}

試してみたのはこんなとこです。

ソースは一応あげときました。

github.com

終わり。