[横書きアイテム]そんなREST APIで大丈夫ですか?本格的なREST APIの実装:イベントコントローラの分解中のトラブルシューティング(MethodOn)

95023 ワード

EventControlの再設計


説明に沿ってコードを作成したが、実装に重点を置いたため、コードが不潔になった.このため、まず講義を中止し、排気コントローラを再設計した.
再包装の過程は以下の通りです.
プロジェクトの再構築
  • 共通のSelf-AndUpdate Linkブレーク
  • を作成
  • リンクを追加した部分を解除します(query-event&self-Link.)
  • プロファイルリンクを分割した後、継承によってEventResource&PageModel
  • を同時に有効にします.
  • 汎用イベント->EventResource&addLinks論理分離
  • PageModel内部イベントモデルをEventResourceの論理分離にマッピングする:
  • 既存のコード

    @Api(tags = {"Event Controller"})
    @RestController()
    @RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
    public class EventController {
    
        @Autowired
        private EventService eventService;
    
        @ApiOperation(value = "Event 객체를 추가하는 메소드")
        @PostMapping("")
        public ResponseEntity create(@RequestBody @Valid EventDto eventDto) {
            Event event = this.eventService.create(eventDto);
            EventResource eventResource = createEventResource(event,
                    "/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 = createEventResource(event,
                                "/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 = createEventResource(event,
                    "/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 = createEventResource(event,
                    "/swagger-ui/index.html#/Event%20Controller/updateUsingPUT");
            return ResponseEntity.ok(eventResource);
        }
    
        private EventResource createEventResource(Event event, String profileLink){
            EventResource eventResource = new EventResource(event);
            addLinks(eventResource, profileLink);
            return 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();
        }
    }

    共通のselfAndUpdateLinkを作成する部分を解除


    selfAndUpdateLinkは、イベントを作成および変更できるリンクです.このリンクを使用する部分は、URI、SelfLink、UpdateLinkの作成の3つです.したがって,リンク実装を部分的に分離し,タイプで論理を安全に実現する.
        private WebMvcLinkBuilder getSelfAndUpdateLink(EventResource eventResource){
            return linkTo(methodOn(EventController.class)
                    .create(new EventDto()))
                    .slash(eventResource.getEvent().getId());
        }

    リンクの追加を解除


    リンクを追加する部分はaddLinksで行います.
    このとき,各実装リンクの部分はaddLinksで実装される.
    したがって,リンク実装部を分離し,addLinksはリンクを接続する論理のみを担当する.
    	private void addLinks(EventResource eventResource, String profileLink){
            addQueryLink(eventResource);
            addUpdateLink(eventResource);
            addSelfLink(eventResource);
            addProfileLink(eventResource, profileLink);
        }
    
        private void addUpdateLink(EventResource eventResource){
            WebMvcLinkBuilder selfAndUpdateLink = getCreateAndUpdateLink(eventResource);
            eventResource.add(selfAndUpdateLink.withRel("update-event"));
        }
    
        private void addSelfLink(EventResource eventResource){
            WebMvcLinkBuilder selfAndUpdateLink = getCreateAndUpdateLink(eventResource);
            eventResource.add(selfAndUpdateLink.withSelfRel());
        }
    
        private void addQueryLink(EventResource eventResource){
            WebMvcLinkBuilder queryLink = linkTo(methodOn(EventController.class));
            eventResource.add(queryLink.withRel("query-events"));
        }
    
        private void addProfileLink(EventResource eventResource, String profileLink){
            eventResource.add(new Link(getBaseURL() + profileLink,"profile"));
        }

    Profile Linkセクションを切断した後、継承を使用してEventResource&PageModelを使用可能にします。


    現在profileリンクを使用しているエンティティは、主にEventResourceとPageModelです.この2つのオブジェクトは、Representation Modelを継承します.
    したがって,profileリンクを追加する論理を分離し,Representationモデルを用いて両者を共通に使用することができる.
        private void addProfileLink(RepresentationModel eventResource, String profileLink){
            eventResource.add(new Link(getBaseURL() + profileLink,"profile"));
        }

    汎用イベントの分離->EventResource&addLinksロジック


    イベントモデルをEventResourceに変換する操作とaddLinksロジックは複数の場所で共通に使用されます.コードの重複を防止するために,それを分離することにした.
        private EventResource createEventResource(Event event, String profileLink){
            EventResource eventResource = new EventResource(event);
            addLinks(eventResource, profileLink);
            return eventResource;
        }

    PageModel内部イベントモデルをEventResourceにマッピングする論理を分離


    PageModel内部にはEventService ReadAllメソッドによってEvent Modelが含まれています.ただし、Self descritiveとHATEOASを満たすためには、イベントモデルをEventResourceに変換する必要があります.
    現在、readAll()メソッドではPageModel Event->EventResourceロジックとaddProfileロジックが同時に実現されています.addProfileロジックは分離されているので、Event->EventResource変換ロジックも分離することにします.
        private PagedModel createPagingModel(Pageable pageable, PagedResourcesAssembler pagedResourcesAssembler){
            return pagedResourcesAssembler
                    .toModel(this.eventService.readWithPage(pageable).map(event -> {
                        EventResource eventResource = createEventResource(event,
                                "/swagger-ui/index.html#/Event%20Controller/readUsingGET");
                        return eventResource;
                    }));
        }

    しゅさいせいコード

    @Api(tags = {"Event Controller"})
    @RestController()
    @RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
    public class EventController {
    
        @Autowired
        private EventService eventService;
    
        @ApiOperation(value = "Event 객체를 추가하는 메소드")
        @PostMapping("")
        public ResponseEntity create(@RequestBody @Valid EventDto eventDto) {
            Event event = this.eventService.create(eventDto);
            EventResource eventResource = createEventResource(event,
                    "/swagger-ui/index.html#/Event%20Controller/createUsingPOST");
            URI uri = getSelfAndUpdateLink(eventResource).toUri();
            return ResponseEntity.created(uri).body(eventResource);
        }
    
        @ApiOperation(value = "모든 Event 객체를 읽어오는 메소드")
        @GetMapping("")
        public ResponseEntity readAll(Pageable pageable, PagedResourcesAssembler pagedResourcesAssembler) {
            var result =  createPagingModel(pageable, pagedResourcesAssembler);
            addProfileLink(result,
                    "/swagger-ui/index.html#/Event%20Controller/readAllUsingGET");
            return ResponseEntity.ok(result);
        }
    
        @ApiOperation(value = "Event 단일 객체를 읽어오는 메소드")
        @GetMapping("/{id}")
        public ResponseEntity read(@PathVariable Integer id){
            Event event = this.eventService.read(id);
            EventResource eventResource = createEventResource(event,
                    "/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 = createEventResource(event,
                    "/swagger-ui/index.html#/Event%20Controller/updateUsingPUT");
            return ResponseEntity.ok(eventResource);
        }
    
        private EventResource createEventResource(Event event, String profileLink){
            EventResource eventResource = new EventResource(event);
            addLinks(eventResource, profileLink);
            return eventResource;
        }
    
        private PagedModel createPagingModel(Pageable pageable, PagedResourcesAssembler pagedResourcesAssembler){
            return pagedResourcesAssembler
                    .toModel(this.eventService.readWithPage(pageable).map(event -> {
                        EventResource eventResource = createEventResource(event,
                                "/swagger-ui/index.html#/Event%20Controller/readUsingGET");
                        return eventResource;
                    }));
        }
    
        private void addLinks(EventResource eventResource, String profileLink){
            addQueryLink(eventResource);
            addUpdateLink(eventResource);
            addSelfLink(eventResource);
            addProfileLink(eventResource, profileLink);
        }
    
        private void addUpdateLink(EventResource eventResource){
            WebMvcLinkBuilder selfAndUpdateLink = getSelfAndUpdateLink(eventResource);
            eventResource.add(selfAndUpdateLink.withRel("update-event"));
        }
    
        private void addSelfLink(EventResource eventResource){
            WebMvcLinkBuilder selfAndUpdateLink =  getSelfAndUpdateLink(eventResource);
            eventResource.add(selfAndUpdateLink.withSelfRel());
        }
    
        private void addQueryLink(EventResource eventResource){
            WebMvcLinkBuilder queryLink =  linkTo(methodOn(EventController.class));
            eventResource.add(queryLink.withRel("query-events"));
        }
    
        private void addProfileLink(RepresentationModel eventResource, String profileLink){
            eventResource.add(new Link(getBaseURL() + profileLink,"profile"));
        }
    
        private WebMvcLinkBuilder getSelfAndUpdateLink(EventResource eventResource){
            return linkTo(methodOn(EventController.class)
                    .create(new EventDto()))
                    .slash(eventResource.getEvent().getId());
        }
    
        private String getBaseURL(){
            return ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
        }
    }
    

    エラー発生:NullPointerException


    再パッケージのコードをテストした結果、次のようになりました.

    エラーをチェックすると、次のようにNullPointerExceptionが表示されます.

    発生したコードをチェックすると、query-linkを貼り付ける論理的に発生していることがわかります.
        private void addQueryLink(EventResource eventResource){
            WebMvcLinkBuilder queryLink =  linkTo(methodOn(EventController.class));
            eventResource.add(queryLink.withRel("query-events"));
        }
    正確には、このコードのLinkToメソッドにはnull値が含まれ、methodOn()メソッドはnullを返します.
    MethodOnMethodでWebMcLinkBuilderクラスの内部を確認し、返される値を確認します.
    	//WebMvcLinkBuilder.class
        public static <T> T methodOn(Class<T> controller, Object... parameters) {
            return DummyInvocationUtils.methodOn(controller, parameters);
        }
    methodOn関数はT,すなわちクラスタイプ,正確にはDummyInvocatioinである.Utilsで作成したクラスタイプが返されます.
    再びDummyInvocationUtilsに乗って行きました
    public class DummyInvocationUtils {
        private static final ThreadLocal<Map<DummyInvocationUtils.CacheKey<?>, Object>> CACHE = ThreadLocal.withInitial(HashMap::new);
    
        public DummyInvocationUtils() {
        }
    
        public static <T> T methodOn(Class<T> type, Object... parameters) {
            Assert.notNull(type, "Given type must not be null!");
            return ((Map)CACHE.get()).computeIfAbsent(DummyInvocationUtils.CacheKey.of(type, parameters), (it) -> {
                DummyInvocationUtils.InvocationRecordingMethodInterceptor interceptor = new DummyInvocationUtils.InvocationRecordingMethodInterceptor(it.type, it.arguments);
                return getProxyWithInterceptor(it.type, interceptor, type.getClassLoader());
            });
        }
        ...
    }
    DummyInvocationUtilsで使用したmethodOnはgetProxyWithInterceptorを返します.ここではproxyオブジェクトを使った感じがします.
    確実にするために、私はもう一度乗った.
        private static <T> T getProxyWithInterceptor(Class<?> type, DummyInvocationUtils.InvocationRecordingMethodInterceptor interceptor, ClassLoader classLoader) {
            if (type.equals(Object.class)) {
                return interceptor;
            } else {
                ProxyFactory factory = new ProxyFactory();
                factory.addAdvice(interceptor);
                factory.addInterface(LastInvocationAware.class);
                if (type.isInterface()) {
                    factory.addInterface(type);
                } else {
                    factory.setOptimize(true);
                    factory.setTargetClass(type);
                    factory.setProxyTargetClass(true);
                }
    
                return factory.getProxy(classLoader);
            }
        }
    同じくProxyFactoryでエージェントとして返されます.
    ProxyFactoryはSpringでAOPを実現するためのエージェントを作成するオブジェクトであり,methodOn()メソッドで作成するオブジェクトがエージェントオブジェクトである.
    したがって、NullPointerExceptionが発生したのは、プロキシオブジェクトがLinkToメソッドにパラメータ化されており、実際のオブジェクトを作成するにはinvokeが必要であるためです.invokeは、プロキシオブジェクト上でメソッドを実行する必要があります.
    Spring Docsでも次の注意点が見つかりました.
    methodOn(…) creates a proxy of the controller class that records the method invocation and exposes it in a proxy created for the return type of the method. This allows the fluent expression of the method for which we want to obtain the mapping. However, there are a few constraints on the methods that can be obtained by using this technique:
  • The return type has to be capable of proxying, as we need to expose the method invocation on it.
  • The parameters handed into the methods are generally neglected (except the ones referred to through @PathVariable, because they make up the URI).
  • 簡単に説明する(正確ではないかもしれない)MethodOnでは、プロキシオブジェクトからメソッドマッピング情報を簡単に取得できます.しかし、いくつかの制約要素もある.
  • は、タイプがエージェント可能である必要があるため、メソッドを呼び出す必要がある.
  • パラメータを超えるオブジェクト値は無視されます.
  • 最初の制約から見ると、プロキシとして使用するためにメソッドを呼び出す必要があるため、NullPointerceptionが発生する可能性があります.
    したがって、methodOn()を使用してロードされたオブジェクトからメソッドを呼び出すか、methodOn()メソッドを削除する方向にコードを変更する必要があります.
        private void addQueryLink(EventResource eventResource){
            WebMvcLinkBuilder queryLink =  linkTo(methodOn(EventController.class).getClass());
            eventResource.add(queryLink.withRel("query-events"));
        }
    または
        private void addQueryLink(EventResource eventResource){
            WebMvcLinkBuilder queryLink =  linkTo(EventController.class);
            eventResource.add(queryLink.withRel("query-events"));
        }
    個人的にはここにエージェントオブジェクトを書く必要はないと思いますので、以下の方法でコードを修正、テストし、合格しました.

    最終Event Controlコード

    @Api(tags = {"Event Controller"})
    @RestController()
    @RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_VALUE)
    public class EventController {
    
        @Autowired
        private EventService eventService;
    
        @ApiOperation(value = "Event 객체를 추가하는 메소드")
        @PostMapping("")
        public ResponseEntity create(@RequestBody @Valid EventDto eventDto) {
            Event event = this.eventService.create(eventDto);
            EventResource eventResource = createEventResource(event,
                    "/swagger-ui/index.html#/Event%20Controller/createUsingPOST");
            URI uri = getCreateAndUpdateLink(eventResource).toUri();
            return ResponseEntity.created(uri).body(eventResource);
        }
    
        @ApiOperation(value = "모든 Event 객체를 읽어오는 메소드")
        @GetMapping("")
        public ResponseEntity readAll(Pageable pageable, PagedResourcesAssembler pagedResourcesAssembler) {
            var result =  createPagingModel(pageable, pagedResourcesAssembler);
            addProfileLink(result,
                    "/swagger-ui/index.html#/Event%20Controller/readAllUsingGET");
            return ResponseEntity.ok(result);
        }
    
        @ApiOperation(value = "Event 단일 객체를 읽어오는 메소드")
        @GetMapping("/{id}")
        public ResponseEntity read(@PathVariable Integer id){
            Event event = this.eventService.read(id);
            EventResource eventResource = createEventResource(event,
                    "/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 = createEventResource(event,
                    "/swagger-ui/index.html#/Event%20Controller/updateUsingPUT");
            return ResponseEntity.ok(eventResource);
        }
    
        private EventResource createEventResource(Event event, String profileLink){
            EventResource eventResource = new EventResource(event);
            addLinks(eventResource, profileLink);
            return eventResource;
        }
    
        private PagedModel createPagingModel(Pageable pageable, PagedResourcesAssembler pagedResourcesAssembler){
            return pagedResourcesAssembler
                    .toModel(this.eventService.readWithPage(pageable).map(event -> {
                        EventResource eventResource = createEventResource(event,
                                "/swagger-ui/index.html#/Event%20Controller/readUsingGET");
                        return eventResource;
                    }));
        }
    
        private void addLinks(EventResource eventResource, String profileLink){
            addQueryLink(eventResource);
            addUpdateLink(eventResource);
            addSelfLink(eventResource);
            addProfileLink(eventResource, profileLink);
        }
    
        private void addUpdateLink(EventResource eventResource){
            WebMvcLinkBuilder selfAndUpdateLink = getCreateAndUpdateLink(eventResource);
            eventResource.add(selfAndUpdateLink.withRel("update-event"));
        }
    
        private void addSelfLink(EventResource eventResource){
            WebMvcLinkBuilder selfAndUpdateLink =  getCreateAndUpdateLink(eventResource);
            eventResource.add(selfAndUpdateLink.withSelfRel());
        }
    
        private void addQueryLink(EventResource eventResource){
            WebMvcLinkBuilder queryLink =  linkTo(EventController.class);
            eventResource.add(queryLink.withRel("query-events"));
        }
    
        private void addProfileLink(RepresentationModel eventResource, String profileLink){
            eventResource.add(new Link(getBaseURL() + profileLink,"profile"));
        }
    
        private WebMvcLinkBuilder getCreateAndUpdateLink(EventResource eventResource){
            return linkTo(methodOn(EventController.class)
                    .create(new EventDto()))
                    .slash(eventResource.getEvent().getId());
        }
    
        private String getBaseURL(){
            return ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();
        }
    }