Help us understand the problem. What is going on with this article?

Spring Boot + Swagger2.0の触り心地を確かめてみる

https://start.spring.io/ からwebのみを選択してパパッと作成
Swaggerはなかったので、Maven Repositoryから持ってきた
versionなどは build.gradle 参照してもらいたい

サンプルコード
https://github.com/ririkku/swagger-demo

試せる最小構成

コード全量

build.gradle
plugins {
    id 'org.springframework.boot' version '2.2.1.RELEASE'
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'

    // Swaggerで必要な最低限
    implementation "io.springfox:springfox-swagger2:2.9.2"
    implementation "io.springfox:springfox-swagger-ui:2.9.2"

    testImplementation('org.springframework.boot:spring-boot-starter-test') {
        exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
    }
}

test {
    useJUnitPlatform()
}
SwaggerDemoApplication.java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SwaggerDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SwaggerDemoApplication.class, args);
    }
}
SwaggerDemoConfiguration.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2 // swagger2.0を使用
public class SwaggerDemoConfiguration {

    @Bean
    public Docket petApi() {
        return new Docket(DocumentationType.SWAGGER_2) // Swagger2.0を使用します宣言
                .select()
                .paths(PathSelectors.ant("/apis/**"))
                .build();
    }
}
SwaggerDemoRestController.java
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("apis")
public class SwaggerDemoRestController {

    @GetMapping
    public String get() {
        return "get";
    }
}

確認

普段はIntellij IDEAから起動しているけど、その環境じゃない人もいるかもだから Gradleで起動する
./gradlew bootRun
ローカルホストでアクセス
http://localhost:8080/swagger-ui.html
(入りきらなかったので2分割)
スクリーンショット 2019-12-03 0.04.52.png
スクリーンショット 2019-12-03 0.05.09.png

雑感

UIいいじゃん!!!
コードもそんなに書いてないし、実用的かも
でも色々お節介な感じはする、ちょっとカスタマイズしてみよう

Headerっぽいところをカスタマイズ

コード(変更ファイル)

SwaggerDemoConfiguration.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2 // swagger2.0を使用
public class SwaggerDemoConfiguration {

    @Bean
    public Docket petApi() {
        return new Docket(DocumentationType.SWAGGER_2) // Swagger2.0を使用します宣言
                .select()
                .paths(PathSelectors.ant("/apis/**"))
                .build()
                .apiInfo(new ApiInfoBuilder()
                        .title("Customise Title Swagger Demo Application")
                        .description("自分好みにカスタマイズしてたよ")
                        .contact(new Contact("customise-name", "http://customise-contact", "customise-email"))
                        .version("1.0")
                        .termsOfServiceUrl("http://customise.com")
                        .license("Customise License").licenseUrl("http://customise-license-url") // licenseのみだとテキスト、licenseUrl設定するとリンクになる
                        .build());
    }
}

確認

スクリーンショット 2019-12-03 0.27.24.png

雑感

Customiseのサフィックスをつけた場所が書き換えてみた箇所
Base URLhttp//localhost:8080/v2/api-docsの部分は変えられないのかな?って感想
あと、ApiInfoBuilderクラスにはextensionsが設定できるよう
独自に何かプラグインを作成したい時とか使用するのかな?(https://swagger.io/docs/specification/2-0/swagger-extensions/)
そういやHttpメソッドってどんな感じに出るんだろう

HTTPメソッドのUI確認

コード(変更ファイル)

SwaggerDemoRestController.java
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("apis")
public class SwaggerDemoRestController {

    @GetMapping
    public String get() {
        return "get";
    }

    @PostMapping
    public void post() {
    }

    @DeleteMapping
    public void delete() {
    }

    @PutMapping
    public void put() {
    }

    @PatchMapping
    public void patch() {
    }
}

確認

さっきはGET見たから次は気分でPOST
スクリーンショット 2019-12-03 0.38.16.png
スクリーンショット 2019-12-03 0.41.35.png

雑感

カラフルで見やすい(PATCHとか使わんやろ)
APIの詳細は色々勝手に考慮してくれる模様(ステータスコードとか)
次は詳細部分をカスタマイズしてみよう

エンドポイントのカスタマイズ

心機一転、新しいクラスを作成した
@PathVariableはクラスにバインドしたり、@RequestParamはStringにバインドしたりはわざと!

コード

Identifier.java
public class Identifier {

    private String value;

    public Identifier(String value) {
        this.value = value;
    }

    public String value() {
        if (value == null) return "";
        return value;
    }
}
SwaggerDemoCustomiseRestController.java
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("apis/customise")
public class SwaggerDemoCustomiseRestController {

    @GetMapping("{identifier}")
    public String detail(@RequestHeader("X-Customise-Header") String customiseHeader,
                         @PathVariable("identifier") Identifier identifier,
                         @RequestParam(value = "name", required = false) String name,
                         @RequestParam("limit") int limit) {
        return identifier.value();
    }
}

確認

@RequestHeader@PathVariable@RequestParamを入れてみた
必須はものには、requiredが付くようになった、便利だね

スクリーンショット 2019-12-03 0.56.52.png

ちなみに、 Try it out ボタンを押すと以下の表示なって値を入れて検証できるようになった!

スクリーンショット 2019-12-03 1.00.22.png

Execute を押すと、 curlRequest URLResponseCodeResponseBodyResponseHeadersが返ってくる!

スクリーンショット 2019-12-03 1.04.11.png

スクリーンショット 2019-12-03 1.05.13.png

雑感

だいぶ便利に見える
次は勝手に生成されるレスポンスステータスを自分が欲しいものだけにしてみよう

レスポンスステータスの指定

useDefaultResponseMessages を設定することで、デフォルトが200のみ設定されるよう

コード(変更ファイル)

SwaggerDemoConfiguration.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2 // swagger2.0を使用
public class SwaggerDemoConfiguration {

    @Bean
    public Docket petApi() {
        return new Docket(DocumentationType.SWAGGER_2) // Swagger2.0を使用します宣言
                .select()
                .paths(PathSelectors.ant("/apis/**"))
                .build()
                .useDefaultResponseMessages(false) // <- 追加
                .apiInfo(new ApiInfoBuilder()
                        .title("Customise Title Swagger Demo Application")
                        .description("自分好みにカスタマイズしてたよ")
                        .contact(new Contact("customise-name", "http://customise-contact", "customise-email"))
                        .version("1.0")
                        .termsOfServiceUrl("http://customise.com")
                        .license("Customise License").licenseUrl("http://customise-license-url") // licenseのみだとテキスト、licenseUrl設定するとリンクになる
                        .build());
    }
}

確認

スクリーンショット 2019-12-03 1.11.07.png

雑感

お節介が消えてくれた
次は実際に使いそうなケースのAPIを全部盛りで定義してみよう

実用的なエンドポイント定義

Swaggerでみたいだけだから、処理内容は適当of適当

コード

LessonIdentifier.java
class LessonIdentifier {

    private Integer value;

    LessonIdentifier(String value) {
        this.value = Integer.valueOf(value);
    }

    LessonIdentifier(int value) {
        this.value = value;
    }

    Integer value() {
        if (value == null) return 0;
        return value;
    }
}
LessonRequest.java
public class LessonRequest {

    private String studentName;
    private String tutorName;

    public LessonRequest(String studentName, String tutorName) {
        this.studentName = studentName;
        this.tutorName = tutorName;
    }

    public String getStudentName() {
        return studentName;
    }

    public String getTutorName() {
        return tutorName;
    }
}
LessonResponse.java
public class LessonResponse {

    private int id;
    private String studentName;
    private String tutorName;

    LessonResponse(int id, String studentName, String tutorName) {
        this.id = id;
        this.studentName = studentName;
        this.tutorName = tutorName;
    }

    public int getId() {
        return id;
    }

    public String getStudentName() {
        return studentName;
    }

    public String getTutorName() {
        return tutorName;
    }
}
LessonIdentifierResponse.java
public class LessonIdentifierResponse {

    private int value;

    LessonIdentifierResponse(LessonIdentifier lessonIdentifier) {
        this.value = lessonIdentifier.value();
    }

    public int getValue() {
        return value;
    }
}
ErrorResponse.java
public class ErrorResponse {

    private String message;

    public ErrorResponse(String message) {
        this.message = message;
    }

    public String getMessage() {
        return message;
    }
}
LessonController.java
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("apis/lessons")
public class LessonController {

    @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class)
    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    public List<LessonResponse> list() {
        // 取得処理
        return Arrays.asList(
                new LessonResponse(1, "studentName1", "tutorName1"),
                new LessonResponse(2, "studentName2", "tutorName2"),
                new LessonResponse(3, "studentName3", "tutorName3"));
    }

    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @GetMapping("{lessonIdentifier}")
    @ResponseStatus(HttpStatus.OK)
    public LessonResponse detail(@PathVariable("lessonIdentifier") LessonIdentifier lessonIdentifier) {
        // 取得処理
        return new LessonResponse(1, "studentName1", "tutorName1");
    }

    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public LessonIdentifierResponse add(@RequestBody LessonRequest lessonRequest) {
        // 追加処理
        return new LessonIdentifierResponse(new LessonIdentifier(4));
    }

    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @DeleteMapping("{lessonIdentifier}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable("lessonIdentifier") LessonIdentifier lessonIdentifier) {
        // 削除処理
    }

    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @PutMapping("{lessonIdentifier}")
    @ResponseStatus(HttpStatus.OK)
    public LessonResponse edit(@PathVariable("lessonIdentifier") LessonIdentifier lessonIdentifier,
                               @RequestBody LessonRequest lessonRequest) {
        // 編集処理
        return new LessonResponse(1, "EditStudentName1", "EditTutorName1");
    }
}

確認

たくさんあるので、一部だけ
Example Value がいい感じに出るようになった
あと ResponseStatus が指定したもののみ!

スクリーンショット 2019-12-03 1.42.28.png

スクリーンショット 2019-12-03 1.43.52.png
スクリーンショット 2019-12-03 1.44.06.png

雑感

いい感じに使えそうなところまできた
APIの説明とか書きたくね?となったので、書いてみる

エンドポイントの説明を詳細化

@ApiOperation を追加してみた

コード(変更ファイル)

LessonController.java
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("apis/lessons")
public class LessonController {

    @ApiOperation(value = "レッスンのリストを取得します", notes = "検索条件なしで全件取ることで、あなたのブラウザの時を止めます。")
    @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class)
    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    public List<LessonResponse> list() {
        // 取得処理
        return Arrays.asList(
                new LessonResponse(1, "studentName1", "tutorName1"),
                new LessonResponse(2, "studentName2", "tutorName2"),
                new LessonResponse(3, "studentName3", "tutorName3"));
    }

    @ApiOperation(value = "レッスンを取得します", notes = "ID指定したレッスンを取得します")
    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @GetMapping("{lessonIdentifier}")
    @ResponseStatus(HttpStatus.OK)
    public LessonResponse detail(@PathVariable("lessonIdentifier") LessonIdentifier lessonIdentifier) {
        // 取得処理
        return new LessonResponse(1, "studentName1", "tutorName1");
    }

    @ApiOperation(value = "レッスンを作成します", notes = "作成した後はIDを返します!")
    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public LessonIdentifierResponse add(@RequestBody LessonRequest lessonRequest) {
        // 追加処理
        return new LessonIdentifierResponse(new LessonIdentifier(4));
    }

    @ApiOperation(value = "レッスンを削除します", notes = "何も返さないよ")
    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @DeleteMapping("{lessonIdentifier}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable("lessonIdentifier") LessonIdentifier lessonIdentifier) {
        // 削除処理
    }

    @ApiOperation(value = "レッスンを編集します", notes = "編集後のレッスンを返します!")
    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @PutMapping("{lessonIdentifier}")
    @ResponseStatus(HttpStatus.OK)
    public LessonResponse edit(@PathVariable("lessonIdentifier") LessonIdentifier lessonIdentifier,
                               @RequestBody LessonRequest lessonRequest) {
        // 編集処理
        return new LessonResponse(1, "EditStudentName1", "EditTutorName1");
    }
}

確認

詳細が出るようになった!

スクリーンショット 2019-12-03 1.52.23.png

スクリーンショット 2019-12-03 1.52.35.png

雑感

けど、 @ApiOperationとか@ApiResponseとかに書いてるメッセージとか長くなってしまうの嫌すぎるな。。。
別ファイルにできないだろうかってことで試してみる

別ファイルにメッセージを定義する

コード

application.properties
LessonController.list.value=レッスンのリストを取得します
LessonController.list.notes=検索条件なしで全件取ることで、あなたのブラウザの時を止めます

LessonController.detail.value=レッスンを取得します
LessonController.detail.notes=ID指定したレッスンを取得します

LessonController.add.value=レッスンを作成します
LessonController.add.notes=作成した後はIDを返します!

LessonController.delete.value=レッスンを削除します
LessonController.delete.notes=何も返さないよ

LessonController.edit.value=レッスンを編集します
LessonController.edit.notes=編集後のレッスンを返します!
LessonController.java
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.Arrays;
import java.util.List;

@RestController
@RequestMapping("apis/lessons")
public class LessonController {

    @ApiOperation(value = "${LessonController.list.value}", notes = "${LessonController.list.notes}")
    @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class)
    @GetMapping
    @ResponseStatus(HttpStatus.OK)
    public List<LessonResponse> list() {
        // 取得処理
        return Arrays.asList(
                new LessonResponse(1, "studentName1", "tutorName1"),
                new LessonResponse(2, "studentName2", "tutorName2"),
                new LessonResponse(3, "studentName3", "tutorName3"));
    }

    @ApiOperation(value = "${LessonController.detail.value}", notes = "${LessonController.detail.notes}")
    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @GetMapping("{lessonIdentifier}")
    @ResponseStatus(HttpStatus.OK)
    public LessonResponse detail(@PathVariable("lessonIdentifier") LessonIdentifier lessonIdentifier) {
        // 取得処理
        return new LessonResponse(1, "studentName1", "tutorName1");
    }

    @ApiOperation(value = "${LessonController.add.value}", notes = "${LessonController.add.notes}")
    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public LessonIdentifierResponse add(@RequestBody LessonRequest lessonRequest) {
        // 追加処理
        return new LessonIdentifierResponse(new LessonIdentifier(4));
    }

    @ApiOperation(value = "${LessonController.delelte.value}", notes = "${LessonController.delete.notes}")
    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @DeleteMapping("{lessonIdentifier}")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void delete(@PathVariable("lessonIdentifier") LessonIdentifier lessonIdentifier) {
        // 削除処理
    }

    @ApiOperation(value = "${LessonController.edit.value}", notes = "${LessonController.edit.notes}")
    @ApiResponses(value = {
            @ApiResponse(code = 400, message = "Bad Request", response = ErrorResponse.class),
            @ApiResponse(code = 404, message = "Not Found", response = ErrorResponse.class)})
    @PutMapping("{lessonIdentifier}")
    @ResponseStatus(HttpStatus.OK)
    public LessonResponse edit(@PathVariable("lessonIdentifier") LessonIdentifier lessonIdentifier,
                               @RequestBody LessonRequest lessonRequest) {
        // 編集処理
        return new LessonResponse(1, "EditStudentName1", "EditTutorName1");
    }
}

確認

問題なく出てそう!

スクリーンショット 2019-12-03 2.16.51.png

雑感

メッセージを1ファイルにまとめたいときはすごく便利だな
ただ、やりすぎないようにしないとよくある複雑なメッセージファイルになりがちだから、気をつけて使おう

まとめ

とりあえず便利そうだというのはわかった
けど、Build後とか、本番環境にswagger-uiへアクセスして欲しくない場合とか微妙に調べきれてない箇所があるので後々調べてみる

参考

https://springfox.github.io/springfox/docs/current/

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした