[横書きアイテム]そんなREST APIで大丈夫ですか?本格的なREST APIの実装を試みる-Swagerを使用してProfileリンクを追加

44791 ワード

Swagger UI


Swager UIは、複数の言語、フレームワークでAPIドキュメントを作成およびテストできるライブラリです.
SpringにはSpring Rest Docsというライブラリが存在しますが、個人的にはSwagger UIが好きなので、講義とは異なりSwager UIを使用してProfileリンクを作成します.

Swager依存項目の追加


SpringFox Boot Staterを検索し、ライブラリを検索してプロジェクトに追加します.
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

Swagger設定


SwagerConfigファイルを作成し、Swagerプリファレンスを設定します.
package com.carrykim.restapi.config;

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.builders.RequestHandlerSelectors;
import springfox.documentation.oas.annotations.EnableOpenApi;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.HashSet;
import java.util.Set;

@Configuration
@EnableOpenApi
public class SwaggerConfig {
    @Bean
    public Docket api() {
        return new Docket(DocumentationType.OAS_30)
                .consumes(getConsumeContentTypes())
                .produces(getProduceContentTypes())
                .apiInfo(getApiInfo())
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.carrykim.restapi"))
                .paths(PathSelectors.ant("/**"))
                .build();
    }

    private Set<String> getConsumeContentTypes() {
        Set<String> consumes = new HashSet<>();
        consumes.add("application/json;charset=UTF-8");
        consumes.add("application/x-www-form-urlencoded");
        return consumes;
    }

    private Set<String> getProduceContentTypes() {
        Set<String> produces = new HashSet<>();
        produces.add("application/json;charset=UTF-8");
        return produces;
    }

    private ApiInfo getApiInfo() {
        return new ApiInfoBuilder()
                .title("REST API")
                .description("Event REST API")
                .contact(new Contact("Carrykim", "https://github.com/gimseonjin", "[email protected]"))
                .version("1.0")
                .build();
    }
}
そしてEventControllerに行って下のラベルを貼ります.
@Api(tags = {"Event Controller"})
@RestController()
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
public class EventController {

    private final EventService eventService;

    public EventController(EventService eventService) {
        this.eventService = eventService;
    }

    @ApiOperation(value = "Event 객체를 추가하는 메소드")
    @PostMapping("")
    public ResponseEntity create(@RequestBody @Valid EventDto eventDto) {
        ...
    }

    @ApiOperation(value = "모든 Event 객체를 읽어오는 메소드")
    @GetMapping("")
    public ResponseEntity readAll(Pageable pageable, PagedResourcesAssembler pagedResourcesAssembler) {
   	...
   	}

    @ApiOperation(value = "Event 단일 객체를 읽어오는 메소드")
    @GetMapping("/{id}")
    public ResponseEntity read(@PathVariable Integer id){
    ...
    }

    @ApiOperation(value = "이벤트 객체를 수정하는 메소드")
    @PutMapping("/{id}")
    public ResponseEntity update(@RequestBody @Valid EventDto eventDto, @PathVariable Integer id){
    ...
    }

}

プロファイルリンクの追加


各レスポンス値にプロファイルリンクを追加します.
メソッドOnにインポートできません.Swagerのリンクをインポートするために別のクラスが作成されていないためです.
そこでnew Link()でリンクオブジェクトを作成して入れます.
BaseURL getBaseURLメソッドを作成して、サーブレットUriComponentBuilderからインポートします.
@Api(tags = {"Event Controller"})
@RestController()
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
public class EventController {

    private final EventService eventService;

    public EventController(EventService eventService) {
        this.eventService = eventService;
    }

    @ApiOperation(value = "Event 객체를 추가하는 메소드")
    @PostMapping("")
    public ResponseEntity create(@RequestBody @Valid EventDto eventDto) {
        Event event = this.eventService.create(eventDto);
        EventResource eventResource = new EventResource(event);
        addLinks(eventResource, "/swagger-ui/index.html#/Event%20Controller/createUsingPOST");
        URI uri = linkTo(methodOn(EventController.class)
                .create(new EventDto()))
                .slash(eventResource.getEvent().getId()).toUri();
        return ResponseEntity.created(uri).body(eventResource);
    }

    @ApiOperation(value = "모든 Event 객체를 읽어오는 메소드")
    @GetMapping("")
    public ResponseEntity readAll(Pageable pageable, PagedResourcesAssembler pagedResourcesAssembler) {
        var result =  pagedResourcesAssembler
                .toModel(this.eventService.readWithPage(pageable).map(event -> {
                    EventResource eventResource =  new EventResource(event);
                    addLinks(eventResource, "/swagger-ui/index.html#/Event%20Controller/readUsingGET");
                    return eventResource;
                }));
        result.add(new Link(getBaseURL() + "/swagger-ui/index.html#/Event%20Controller/readAllUsingGET","profile"));
        return ResponseEntity.ok(result);
    }

    @ApiOperation(value = "Event 단일 객체를 읽어오는 메소드")
    @GetMapping("/{id}")
    public ResponseEntity read(@PathVariable Integer id){
        Event event = this.eventService.read(id);
        EventResource eventResource = new EventResource(event);
        addLinks(eventResource, "/swagger-ui/index.html#/Event%20Controller/readUsingGET");
        return ResponseEntity.ok(eventResource);
    }

    @ApiOperation(value = "이벤트 객체를 수정하는 메소드")
    @PutMapping("/{id}")
    public ResponseEntity update(@RequestBody @Valid EventDto eventDto, @PathVariable Integer id){
        Event event = this.eventService.update(id, eventDto);
        EventResource eventResource = new EventResource(event);
        addLinks(eventResource, "/swagger-ui/index.html#/Event%20Controller/updateUsingPUT");
        return ResponseEntity.ok(eventResource);
    }

    private void addLinks(EventResource eventResource, String profileLink){
        WebMvcLinkBuilder selfAndUpdateLink =  linkTo(methodOn(EventController.class)
                .create(new EventDto()))
                .slash(eventResource.getEvent().getId());
        WebMvcLinkBuilder queryLink =  linkTo(methodOn(EventController.class));
        eventResource.add(queryLink.withRel("query-events"));
        eventResource.add(selfAndUpdateLink.withRel("update-event"));
        eventResource.add(selfAndUpdateLink.withSelfRel());
        eventResource.add(new Link(getBaseURL() + profileLink,"profile"));
    }

    private String getBaseURL(){
        return ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
    }
}

コントローラテストの変更


テストコードを変更して、各レスポンスにProfileが含まれていることを確認します.
		mockMvc.perform(put("/api/events/{id}", event.getId())
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(eventDto)))
                .andDo(print())
                .andExpect(jsonPath("event.name").value(event.getName()))
                .andExpect(jsonPath("event.description").value(newDescription))
                .andExpect(jsonPath("_links").exists())
                // Profile 관련 테스트 추가
                .andExpect(jsonPath("_links.profile").exists());