[横書きアイテム]そんなREST APIで大丈夫ですか?本格的なREST APIの実装:イベントコントローラの分解中のトラブルシューティング(MethodOn)
95023 ワード
EventControlの再設計
説明に沿ってコードを作成したが、実装に重点を置いたため、コードが不潔になった.このため、まず講義を中止し、排気コントローラを再設計した.
再包装の過程は以下の通りです.
プロジェクトの再構築
既存のコード
@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:
したがって、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();
}
}
Reference
この問題について([横書きアイテム]そんなREST APIで大丈夫ですか?本格的なREST APIの実装:イベントコントローラの分解中のトラブルシューティング(MethodOn)), 我々は、より多くの情報をここで見つけました https://velog.io/@carrykim/사이드프로젝트-그저-그런-REST-API로-괜찮은가-진정한-REST-API-구현해보기-EventController-Refactoring-중에-생긴-트러블-슈팅methodOnテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol