https://start.spring.io/ からwebのみを選択してパパッと作成
Swaggerはなかったので、Maven Repositoryから持ってきた
versionなどは build.gradle
参照してもらいたい
サンプルコード
https://github.com/ririkku/swagger-demo
試せる最小構成
コード全量
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()
}
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);
}
}
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();
}
}
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分割)
雑感
UIいいじゃん!!!
コードもそんなに書いてないし、実用的かも
でも色々お節介な感じはする、ちょっとカスタマイズしてみよう
Headerっぽいところをカスタマイズ
コード(変更ファイル)
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());
}
}
確認
雑感
Customise
のサフィックスをつけた場所が書き換えてみた箇所
Base URL
とhttp//localhost:8080/v2/api-docs
の部分は変えられないのかな?って感想
あと、ApiInfoBuilder
クラスにはextensions
が設定できるよう
独自に何かプラグインを作成したい時とか使用するのかな?(https://swagger.io/docs/specification/2-0/swagger-extensions/)
そういやHttpメソッドってどんな感じに出るんだろう
HTTPメソッドのUI確認
コード(変更ファイル)
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() {
}
}
確認
雑感
カラフルで見やすい(PATCHとか使わんやろ)
APIの詳細は色々勝手に考慮してくれる模様(ステータスコードとか)
次は詳細部分をカスタマイズしてみよう
エンドポイントのカスタマイズ
心機一転、新しいクラスを作成した
@PathVariable
はクラスにバインドしたり、@RequestParam
はStringにバインドしたりはわざと!
コード
public class Identifier {
private String value;
public Identifier(String value) {
this.value = value;
}
public String value() {
if (value == null) return "";
return value;
}
}
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
が付くようになった、便利だね
ちなみに、 Try it out
ボタンを押すと以下の表示なって値を入れて検証できるようになった!
Execute
を押すと、 curl
、Request URL
、ResponseCode
、ResponseBody
、ResponseHeaders
が返ってくる!
雑感
だいぶ便利に見える
次は勝手に生成されるレスポンスステータスを自分が欲しいものだけにしてみよう
レスポンスステータスの指定
useDefaultResponseMessages
を設定することで、デフォルトが200
のみ設定されるよう
コード(変更ファイル)
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());
}
}
確認
雑感
お節介が消えてくれた
次は実際に使いそうなケースのAPIを全部盛りで定義してみよう
実用的なエンドポイント定義
Swaggerでみたいだけだから、処理内容は適当of適当
コード
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;
}
}
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;
}
}
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;
}
}
public class LessonIdentifierResponse {
private int value;
LessonIdentifierResponse(LessonIdentifier lessonIdentifier) {
this.value = lessonIdentifier.value();
}
public int getValue() {
return value;
}
}
public class ErrorResponse {
private String message;
public ErrorResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
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
が指定したもののみ!
雑感
いい感じに使えそうなところまできた
APIの説明とか書きたくね?となったので、書いてみる
エンドポイントの説明を詳細化
@ApiOperation
を追加してみた
コード(変更ファイル)
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");
}
}
確認
詳細が出るようになった!
雑感
けど、 @ApiOperation
とか@ApiResponse
とかに書いてるメッセージとか長くなってしまうの嫌すぎるな。。。
別ファイルにできないだろうかってことで試してみる
別ファイルにメッセージを定義する
コード
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=編集後のレッスンを返します!
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");
}
}
確認
問題なく出てそう!
雑感
メッセージを1ファイルにまとめたいときはすごく便利だな
ただ、やりすぎないようにしないとよくある複雑なメッセージファイルになりがちだから、気をつけて使おう
まとめ
とりあえず便利そうだというのはわかった
けど、Build後とか、本番環境にswagger-uiへアクセスして欲しくない場合とか微妙に調べきれてない箇所があるので後々調べてみる