From 88af1aad58cb3e404a721d53c0c3beea807c6488 Mon Sep 17 00:00:00 2001 From: youngh0 Date: Sun, 24 Sep 2023 14:59:41 +0900 Subject: [PATCH] =?UTF-8?q?[MVC=20=EA=B5=AC=ED=98=84=ED=95=98=EA=B8=B0=202?= =?UTF-8?q?=EB=8B=A8=EA=B3=84]=20=EB=A0=88=EC=98=A4(=EC=B0=A8=EC=98=81?= =?UTF-8?q?=ED=98=B8)=20=EB=AF=B8=EC=85=98=20=EC=A0=9C=EC=B6=9C=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4.=20(#468)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: annotation 기반 핸들러 어댑터 작성 * feat: 핸들러 매핑 구현 및 DispatcherServlet 수정 * refactor: 메서드 추출 * feat: HandlerExecution 에서 instance 의존하도록 수정 * refactor: css 최신화 * refactor: 저장 타입 Object -> Controller 로 변경 * refactor: try-catch 예외 명시 * refactor: 사용하지 않는 클래스 제거 * refactor: 메서드 순서 변경 --- .../com/techcourse/DispatcherServlet.java | 54 ++++++++++++++++--- .../com/techcourse/ManualHandlerAdapter.java | 22 ++++++++ .../com/techcourse/ManualHandlerMapping.java | 15 ++++-- .../controller/RegisterController.java | 23 ++++++-- .../controller/RegisterViewController.java | 13 ----- app/src/main/webapp/css/styles.css | 2 +- .../web/servlet/ModelAndView.java | 4 ++ .../org/springframework/web/servlet/View.java | 2 + .../mvc/tobe/AnnotationHandlerAdapter.java | 18 +++++++ .../mvc/tobe/AnnotationHandlerMapping.java | 29 +++++++--- .../web/servlet/mvc/tobe/HandlerAdapter.java | 12 +++++ .../servlet/mvc/tobe/HandlerExecution.java | 9 ++-- .../web/servlet/mvc/tobe/HandlerMapping.java | 10 ++++ .../web/servlet/view/JsonView.java | 5 ++ .../web/servlet/view/JspView.java | 7 +++ .../tobe/AnnotationHandlerAdapterTest.java | 23 ++++++++ 16 files changed, 205 insertions(+), 43 deletions(-) create mode 100644 app/src/main/java/com/techcourse/ManualHandlerAdapter.java delete mode 100644 app/src/main/java/com/techcourse/controller/RegisterViewController.java create mode 100644 mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapter.java create mode 100644 mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerAdapter.java create mode 100644 mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerMapping.java create mode 100644 mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapterTest.java diff --git a/app/src/main/java/com/techcourse/DispatcherServlet.java b/app/src/main/java/com/techcourse/DispatcherServlet.java index 277d8eed9a..da1dd8f5d9 100644 --- a/app/src/main/java/com/techcourse/DispatcherServlet.java +++ b/app/src/main/java/com/techcourse/DispatcherServlet.java @@ -6,39 +6,81 @@ import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerMapping; +import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerAdapter; +import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerMapping; import webmvc.org.springframework.web.servlet.view.JspView; +import java.util.ArrayList; +import java.util.List; + public class DispatcherServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); - private ManualHandlerMapping manualHandlerMapping; + private List handlerAdapters; + private List handlerMappings; public DispatcherServlet() { } @Override public void init() { - manualHandlerMapping = new ManualHandlerMapping(); + initHandlerMappings(); + initHandlerAdapters(); + } + + private void initHandlerMappings() { + this.handlerMappings = new ArrayList<>(); + ManualHandlerMapping manualHandlerMapping = new ManualHandlerMapping(); manualHandlerMapping.initialize(); + handlerMappings.add(manualHandlerMapping); + + AnnotationHandlerMapping annotationHandlerMapping = new AnnotationHandlerMapping(); + annotationHandlerMapping.initialize(); + handlerMappings.add(annotationHandlerMapping); + } + + private void initHandlerAdapters() { + this.handlerAdapters = new ArrayList<>(); + handlerAdapters.add(new ManualHandlerAdapter()); + handlerAdapters.add(new AnnotationHandlerAdapter()); } @Override protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { final String requestURI = request.getRequestURI(); log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI); - try { - final var controller = manualHandlerMapping.getHandler(requestURI); - final var viewName = controller.execute(request, response); - move(viewName, request, response); + Object handler = findHandler(request); + HandlerAdapter handlerAdapter = findHandlerAdapter(handler); + + ModelAndView modelAndView = handlerAdapter.handle(request, response, handler); + move(modelAndView.getViewName(), request, response); } catch (Throwable e) { log.error("Exception : {}", e.getMessage(), e); throw new ServletException(e.getMessage()); } } + private Object findHandler(HttpServletRequest request) { + return handlerMappings.stream() + .filter(handlerMapping -> handlerMapping.getHandler(request) != null) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("처리할 수 없는 요청입니다.")) + .getHandler(request); + } + + private HandlerAdapter findHandlerAdapter(Object handler) { + return handlerAdapters.stream() + .filter(adapter -> adapter.supports(handler)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException("요청을 처리할 수 있는 핸들러 어댑터가 없습니다.")); + } + private void move(final String viewName, final HttpServletRequest request, final HttpServletResponse response) throws Exception { if (viewName.startsWith(JspView.REDIRECT_PREFIX)) { response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length())); diff --git a/app/src/main/java/com/techcourse/ManualHandlerAdapter.java b/app/src/main/java/com/techcourse/ManualHandlerAdapter.java new file mode 100644 index 0000000000..26aa63027a --- /dev/null +++ b/app/src/main/java/com/techcourse/ManualHandlerAdapter.java @@ -0,0 +1,22 @@ +package com.techcourse; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; +import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerAdapter; +import webmvc.org.springframework.web.servlet.view.JspView; + +public class ManualHandlerAdapter implements HandlerAdapter { + @Override + public boolean supports(Object handler) { + return handler instanceof Controller; + } + + @Override + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Controller controller = (Controller) handler; + String viewName = controller.execute(request, response); + return new ModelAndView(new JspView(viewName)); + } +} diff --git a/app/src/main/java/com/techcourse/ManualHandlerMapping.java b/app/src/main/java/com/techcourse/ManualHandlerMapping.java index a54863caf8..68a5e65c8b 100644 --- a/app/src/main/java/com/techcourse/ManualHandlerMapping.java +++ b/app/src/main/java/com/techcourse/ManualHandlerMapping.java @@ -1,34 +1,39 @@ package com.techcourse; -import com.techcourse.controller.*; +import com.techcourse.controller.LoginController; +import com.techcourse.controller.LoginViewController; +import com.techcourse.controller.LogoutController; +import jakarta.servlet.http.HttpServletRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import webmvc.org.springframework.web.servlet.mvc.asis.Controller; import webmvc.org.springframework.web.servlet.mvc.asis.ForwardController; +import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerMapping; import java.util.HashMap; import java.util.Map; -public class ManualHandlerMapping { +public class ManualHandlerMapping implements HandlerMapping { private static final Logger log = LoggerFactory.getLogger(ManualHandlerMapping.class); private static final Map controllers = new HashMap<>(); + @Override public void initialize() { controllers.put("/", new ForwardController("/index.jsp")); controllers.put("/login", new LoginController()); controllers.put("/login/view", new LoginViewController()); controllers.put("/logout", new LogoutController()); - controllers.put("/register/view", new RegisterViewController()); - controllers.put("/register", new RegisterController()); log.info("Initialized Handler Mapping!"); controllers.keySet() .forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass())); } - public Controller getHandler(final String requestURI) { + @Override + public Object getHandler(final HttpServletRequest request) { + String requestURI = request.getRequestURI(); log.debug("Request Mapping Uri : {}", requestURI); return controllers.get(requestURI); } diff --git a/app/src/main/java/com/techcourse/controller/RegisterController.java b/app/src/main/java/com/techcourse/controller/RegisterController.java index da62e5e8e9..2790851041 100644 --- a/app/src/main/java/com/techcourse/controller/RegisterController.java +++ b/app/src/main/java/com/techcourse/controller/RegisterController.java @@ -2,20 +2,33 @@ import com.techcourse.domain.User; import com.techcourse.repository.InMemoryUserRepository; +import context.org.springframework.stereotype.Controller; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import webmvc.org.springframework.web.servlet.mvc.asis.Controller; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.view.JspView; -public class RegisterController implements Controller { +@Controller +public class RegisterController { - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + private static final Logger log = LoggerFactory.getLogger(RegisterController.class); + + @RequestMapping(value = "/register", method = RequestMethod.POST) + public ModelAndView save(HttpServletRequest req, HttpServletResponse res) { final var user = new User(2, req.getParameter("account"), req.getParameter("password"), req.getParameter("email")); InMemoryUserRepository.save(user); + return new ModelAndView(new JspView("/index.jsp")); + } - return "redirect:/index.jsp"; + @RequestMapping(value = "/register", method = RequestMethod.GET) + public ModelAndView show(HttpServletRequest req, HttpServletResponse res) { + return new ModelAndView(new JspView("/register.jsp")); } } diff --git a/app/src/main/java/com/techcourse/controller/RegisterViewController.java b/app/src/main/java/com/techcourse/controller/RegisterViewController.java deleted file mode 100644 index 136962136d..0000000000 --- a/app/src/main/java/com/techcourse/controller/RegisterViewController.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.techcourse.controller; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import webmvc.org.springframework.web.servlet.mvc.asis.Controller; - -public class RegisterViewController implements Controller { - - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { - return "/register.jsp"; - } -} diff --git a/app/src/main/webapp/css/styles.css b/app/src/main/webapp/css/styles.css index 92a2dc177e..c0f45249b1 100644 --- a/app/src/main/webapp/css/styles.css +++ b/app/src/main/webapp/css/styles.css @@ -11262,4 +11262,4 @@ body { width: 20px !important; font-size: 0.75rem; border-radius: 0.25rem !important; -} \ No newline at end of file +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java index ff8e24553f..fdf4e585d3 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java @@ -30,4 +30,8 @@ public Map getModel() { public View getView() { return view; } + + public String getViewName() { + return view.getName(); + } } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java index 4499f36866..27833b3cd1 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java @@ -7,4 +7,6 @@ public interface View { void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception; + + String getName(); } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapter.java new file mode 100644 index 0000000000..906b3d9b4e --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapter.java @@ -0,0 +1,18 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; + +public class AnnotationHandlerAdapter implements HandlerAdapter { + @Override + public boolean supports(Object handler) { + return handler instanceof HandlerExecution; + } + + @Override + public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + HandlerExecution handlerExecution = (HandlerExecution) handler; + return handlerExecution.handle(request, response); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java index 3bc9548e61..a3a4fa0d6e 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -9,6 +9,7 @@ import web.org.springframework.web.bind.annotation.RequestMethod; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; @@ -17,7 +18,7 @@ import java.util.Set; import java.util.stream.Collectors; -public class AnnotationHandlerMapping { +public class AnnotationHandlerMapping implements HandlerMapping { private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); @@ -29,6 +30,7 @@ public AnnotationHandlerMapping(final Object... basePackage) { this.handlerExecutions = new HashMap<>(); } + @Override public void initialize() { Reflections reflections = new Reflections(basePackage); @@ -36,7 +38,8 @@ public void initialize() { for (Class controller : controllers) { List requestMappingMethods = getRequestMappingMethods(controller); Constructor constructor = getConstructor(controller); - addHandlerExecutions(requestMappingMethods, constructor); + Object instance = makeInstance(constructor); + addHandlerExecutions(requestMappingMethods, instance); } } @@ -55,18 +58,27 @@ private Constructor getConstructor(Class controller) { } } - private void addHandlerExecutions(List requestMappingMethods, Constructor constructor) { + private void addHandlerExecutions(List requestMappingMethods, Object instance) { for (Method requestMappingMethod : requestMappingMethods) { RequestMapping requestMapping = requestMappingMethod.getAnnotation(RequestMapping.class); - RequestMethod[] controllergMethods = requestMapping.method(); - List handlerKeys = makeHandlerKeys(requestMapping, controllergMethods); - putHandlerExecutions(constructor, requestMappingMethod, handlerKeys); + RequestMethod[] controllerMethods = requestMapping.method(); + List handlerKeys = makeHandlerKeys(requestMapping, controllerMethods); + putHandlerExecutions(instance, requestMappingMethod, handlerKeys); } } - private void putHandlerExecutions(Constructor constructor, Method requestMappingMethod, List handlerKeys) { + private Object makeInstance(Constructor constructor) { + try { + return constructor.newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + log.debug("fail make to instance"); + throw new RuntimeException(e); + } + } + + private void putHandlerExecutions(Object instance, Method requestMappingMethod, List handlerKeys) { for (HandlerKey handlerKey : handlerKeys) { - handlerExecutions.put(handlerKey, new HandlerExecution(requestMappingMethod, constructor)); + handlerExecutions.put(handlerKey, new HandlerExecution(requestMappingMethod, instance)); } } @@ -76,6 +88,7 @@ private List makeHandlerKeys(RequestMapping requestMapping, RequestM .collect(Collectors.toList()); } + @Override public Object getHandler(final HttpServletRequest request) { RequestMethod requestMethod = RequestMethod.from(request.getMethod()); String requestURI = request.getRequestURI(); diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerAdapter.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerAdapter.java new file mode 100644 index 0000000000..13351a1575 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerAdapter.java @@ -0,0 +1,12 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; + +public interface HandlerAdapter { + + boolean supports(Object handler); + + ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java index cc70f3d8f6..97b2eb0129 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java @@ -4,20 +4,19 @@ import jakarta.servlet.http.HttpServletResponse; import webmvc.org.springframework.web.servlet.ModelAndView; -import java.lang.reflect.Constructor; import java.lang.reflect.Method; public class HandlerExecution { private final Method method; - private final Constructor constructor; + private final Object instance; - public HandlerExecution(Method method, Constructor constructor) { + public HandlerExecution(Method method, Object instance) { this.method = method; - this.constructor = constructor; + this.instance = instance; } public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - return (ModelAndView) method.invoke(constructor.newInstance(), request, response); + return (ModelAndView) method.invoke(instance, request, response); } } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerMapping.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerMapping.java new file mode 100644 index 0000000000..d900bb88f5 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerMapping.java @@ -0,0 +1,10 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import jakarta.servlet.http.HttpServletRequest; + +public interface HandlerMapping { + + void initialize(); + + Object getHandler(final HttpServletRequest request); +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java index b42c3466f0..c00d6a2b81 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java @@ -11,4 +11,9 @@ public class JsonView implements View { @Override public void render(final Map model, final HttpServletRequest request, HttpServletResponse response) throws Exception { } + + @Override + public String getName() { + return ""; + } } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java index 3f4cc906ff..aaa69da116 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java @@ -13,8 +13,10 @@ public class JspView implements View { private static final Logger log = LoggerFactory.getLogger(JspView.class); public static final String REDIRECT_PREFIX = "redirect:"; + private final String viewName; public JspView(final String viewName) { + this.viewName = viewName; } @Override @@ -28,4 +30,9 @@ public void render(final Map model, final HttpServletRequest request, // todo } + + @Override + public String getName() { + return viewName; + } } diff --git a/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapterTest.java b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapterTest.java new file mode 100644 index 0000000000..58d2238ea2 --- /dev/null +++ b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerAdapterTest.java @@ -0,0 +1,23 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class AnnotationHandlerAdapterTest { + + @Test + void Annotation기반_핸들러면_true_반환() { + AnnotationHandlerAdapter annotationHandlerAdapter = new AnnotationHandlerAdapter(); + HandlerExecution handlerExecution = new HandlerExecution(null, null); + + assertThat(annotationHandlerAdapter.supports(handlerExecution)).isTrue(); + } + + @Test + void Annotation기반_핸들러가_아니면_false_반환() { + AnnotationHandlerAdapter annotationHandlerAdapter = new AnnotationHandlerAdapter(); + + assertThat(annotationHandlerAdapter.supports(new Object())).isFalse(); + } +}