diff --git a/app/src/main/java/com/techcourse/DispatcherServlet.java b/app/src/main/java/com/techcourse/DispatcherServlet.java deleted file mode 100644 index 76f0fb8b0d..0000000000 --- a/app/src/main/java/com/techcourse/DispatcherServlet.java +++ /dev/null @@ -1,98 +0,0 @@ -package com.techcourse; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import webmvc.org.springframework.web.servlet.mvc.asis.Controller; -import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerMapping; -import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerExecution; -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; -import java.util.Objects; -import java.util.Optional; - -public class DispatcherServlet extends HttpServlet { - - public static final String BASE_PACKAGE = "com.techcourse.controller"; - private static final long serialVersionUID = 1L; - private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); - - - private final List handlerMappings = new ArrayList<>(); - - public DispatcherServlet() { - } - - @Override - public void init() { - handlerMappings.add(new ManualHandlerMapping()); - handlerMappings.add(new AnnotationHandlerMapping(BASE_PACKAGE)); - handlerMappings.forEach(HandlerMapping::initialize); - } - - @Override - protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { - final String requestURI = request.getRequestURI(); - log.info("Method : {}, Request URI : {}", request.getMethod(), requestURI); - - try { - final Optional optionalHandler = handlerMappings.stream() - .map(handlerMapping -> handlerMapping.getHandler(request)) - .filter(Objects::nonNull) - .findFirst(); - - optionalHandler.ifPresentOrElse(handler -> handle(request, response, handler), () -> { - throw new IllegalArgumentException("Not found handler for request URI : " + requestURI); - }); - } catch (Throwable e) { - log.error("Exception : {}", e.getMessage(), e); - throw new ServletException(e.getMessage()); - } - } - - private void handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) { - if (handler instanceof Controller) { - handleController(request, response, (Controller) handler); - return; - } - if (handler instanceof HandlerExecution) { - handleException(request, response, (HandlerExecution) handler); - return; - } - throw new IllegalArgumentException("Not supported handler type"); - } - - private void handleException(final HttpServletRequest request, final HttpServletResponse response, final HandlerExecution handler) { - try { - final var modelAndView = handler.handle(request, response); - move(modelAndView.getViewName(), request, response); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - private void handleController(final HttpServletRequest request, final HttpServletResponse response, final Controller handler) { - try { - final String viewName = handler.execute(request, response); - move(viewName, request, response); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - 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())); - return; - } - - final var requestDispatcher = request.getRequestDispatcher(viewName); - requestDispatcher.forward(request, response); - } -} diff --git a/app/src/main/java/com/techcourse/DispatcherServletInitializer.java b/app/src/main/java/com/techcourse/DispatcherServletInitializer.java index 6e814cdd25..53fb9af5c8 100644 --- a/app/src/main/java/com/techcourse/DispatcherServletInitializer.java +++ b/app/src/main/java/com/techcourse/DispatcherServletInitializer.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import web.org.springframework.web.WebApplicationInitializer; +import webmvc.org.springframework.web.servlet.mvc.DispatcherServlet; /** * Base class for {@link WebApplicationInitializer} diff --git a/app/src/main/java/com/techcourse/ManualHandlerMapping.java b/app/src/main/java/com/techcourse/ManualHandlerMapping.java deleted file mode 100644 index bb8b43531f..0000000000 --- a/app/src/main/java/com/techcourse/ManualHandlerMapping.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.techcourse; - -import com.techcourse.controller.LoginController; -import com.techcourse.controller.LoginViewController; -import com.techcourse.controller.LogoutController; -import com.techcourse.controller.RegisterController; -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 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", new RegisterController()); - - log.info("Initialized Handler Mapping!"); - controllers.keySet() - .forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass())); - } - - @Override - public Object getHandler(final HttpServletRequest request) { - final String requestURI = request.getRequestURI(); - return controllers.get(requestURI); - } -} diff --git a/app/src/main/java/com/techcourse/controller/ForwardController.java b/app/src/main/java/com/techcourse/controller/ForwardController.java new file mode 100644 index 0000000000..05481f5590 --- /dev/null +++ b/app/src/main/java/com/techcourse/controller/ForwardController.java @@ -0,0 +1,16 @@ +package com.techcourse.controller; + +import context.org.springframework.stereotype.Controller; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; +import webmvc.org.springframework.web.servlet.ModelAndView; + +@Controller +public class ForwardController { + @RequestMapping(value = "/", method = RequestMethod.GET) + public ModelAndView execute(HttpServletRequest request, HttpServletResponse response) throws Exception { + return ModelAndView.fromJspViewName("redirect:/index.jsp"); + } +} diff --git a/app/src/main/java/com/techcourse/controller/LoginController.java b/app/src/main/java/com/techcourse/controller/LoginController.java index 0428fe109e..8a822c4bcf 100644 --- a/app/src/main/java/com/techcourse/controller/LoginController.java +++ b/app/src/main/java/com/techcourse/controller/LoginController.java @@ -2,20 +2,24 @@ 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; -public class LoginController implements Controller { +@Controller +public class LoginController { private static final Logger log = LoggerFactory.getLogger(LoginController.class); - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + @RequestMapping(value = "/login", method = RequestMethod.POST) + public ModelAndView execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { if (UserSession.isLoggedIn(req.getSession())) { - return "redirect:/index.jsp"; + return ModelAndView.fromJspViewName("redirect:/index.jsp"); } return InMemoryUserRepository.findByAccount(req.getParameter("account")) @@ -23,15 +27,15 @@ public String execute(final HttpServletRequest req, final HttpServletResponse re log.info("User : {}", user); return login(req, user); }) - .orElse("redirect:/401.jsp"); + .orElse(ModelAndView.fromJspViewName("redirect:/401.jsp")); } - private String login(final HttpServletRequest request, final User user) { + private ModelAndView login(final HttpServletRequest request, final User user) { if (user.checkPassword(request.getParameter("password"))) { final var session = request.getSession(); session.setAttribute(UserSession.SESSION_KEY, user); - return "redirect:/index.jsp"; + return ModelAndView.fromJspViewName("redirect:/index.jsp"); } - return "redirect:/401.jsp"; + return ModelAndView.fromJspViewName("redirect:/401.jsp"); } } diff --git a/app/src/main/java/com/techcourse/controller/LoginViewController.java b/app/src/main/java/com/techcourse/controller/LoginViewController.java index 86ec26cdce..6ad762abad 100644 --- a/app/src/main/java/com/techcourse/controller/LoginViewController.java +++ b/app/src/main/java/com/techcourse/controller/LoginViewController.java @@ -1,22 +1,26 @@ package com.techcourse.controller; +import context.org.springframework.stereotype.Controller; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import webmvc.org.springframework.web.servlet.mvc.asis.Controller; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; +import webmvc.org.springframework.web.servlet.ModelAndView; -public class LoginViewController implements Controller { +@Controller +public class LoginViewController { private static final Logger log = LoggerFactory.getLogger(LoginViewController.class); - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + @RequestMapping(value = "/login", method = RequestMethod.GET) + public ModelAndView execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { return UserSession.getUserFrom(req.getSession()) .map(user -> { log.info("logged in {}", user.getAccount()); - return "redirect:/index.jsp"; + return ModelAndView.fromJspViewName("redirect:/index.jsp"); }) - .orElse("/login.jsp"); + .orElse(ModelAndView.fromJspViewName("/login.jsp")); } } diff --git a/app/src/main/java/com/techcourse/controller/LogoutController.java b/app/src/main/java/com/techcourse/controller/LogoutController.java index 4642fd9450..207a322548 100644 --- a/app/src/main/java/com/techcourse/controller/LogoutController.java +++ b/app/src/main/java/com/techcourse/controller/LogoutController.java @@ -1,15 +1,19 @@ package com.techcourse.controller; +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 web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; +import webmvc.org.springframework.web.servlet.ModelAndView; -public class LogoutController implements Controller { +@Controller +public class LogoutController { - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + @RequestMapping(value = "/logout", method = RequestMethod.GET) + public ModelAndView execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { final var session = req.getSession(); session.removeAttribute(UserSession.SESSION_KEY); - return "redirect:/"; + return ModelAndView.fromJspViewName("redirect:/"); } } diff --git a/app/src/main/java/com/techcourse/controller/RegisterController.java b/app/src/main/java/com/techcourse/controller/RegisterController.java index da62e5e8e9..5d77cc298a 100644 --- a/app/src/main/java/com/techcourse/controller/RegisterController.java +++ b/app/src/main/java/com/techcourse/controller/RegisterController.java @@ -2,20 +2,24 @@ 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 web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; +import webmvc.org.springframework.web.servlet.ModelAndView; -public class RegisterController implements Controller { +@Controller +public class RegisterController { - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + @RequestMapping(value = "/register", method = RequestMethod.POST) + public ModelAndView execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { final var user = new User(2, req.getParameter("account"), req.getParameter("password"), req.getParameter("email")); InMemoryUserRepository.save(user); - return "redirect:/index.jsp"; + return ModelAndView.fromJspViewName("redirect:/index.jsp"); } } diff --git a/app/src/main/java/com/techcourse/controller/RegisterViewController.java b/app/src/main/java/com/techcourse/controller/RegisterViewController.java index dd571ba10f..4d1c0a4daa 100644 --- a/app/src/main/java/com/techcourse/controller/RegisterViewController.java +++ b/app/src/main/java/com/techcourse/controller/RegisterViewController.java @@ -6,13 +6,12 @@ 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; @Controller public class RegisterViewController { - @RequestMapping(value = "/register/view", method = RequestMethod.GET) + @RequestMapping(value = "/register", method = RequestMethod.GET) public ModelAndView execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { - return new ModelAndView(new JspView("/register.jsp")); + return ModelAndView.fromJspViewName("/register.jsp"); } } diff --git a/app/src/main/java/com/techcourse/controller/UserController.java b/app/src/main/java/com/techcourse/controller/UserController.java new file mode 100644 index 0000000000..0c57470009 --- /dev/null +++ b/app/src/main/java/com/techcourse/controller/UserController.java @@ -0,0 +1,32 @@ +package com.techcourse.controller; + +import com.techcourse.domain.User; +import com.techcourse.exception.UserNotFoundException; +import com.techcourse.repository.InMemoryUserRepository; +import context.org.springframework.stereotype.Controller; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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.JsonView; + +@Controller +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + + @RequestMapping(value = "/api/user", method = RequestMethod.GET) + public ModelAndView show(HttpServletRequest request, HttpServletResponse response) { + final String account = request.getParameter("account"); + log.debug("user id : {}", account); + + final ModelAndView modelAndView = new ModelAndView(new JsonView()); + final User user = InMemoryUserRepository.findByAccount(account) + .orElseThrow(() -> new UserNotFoundException("존재하지 않는 사용자입니다.")); + modelAndView.addObject("user", user); + return modelAndView; + } +} diff --git a/app/src/main/java/com/techcourse/exception/UserNotFoundException.java b/app/src/main/java/com/techcourse/exception/UserNotFoundException.java new file mode 100644 index 0000000000..0f748251eb --- /dev/null +++ b/app/src/main/java/com/techcourse/exception/UserNotFoundException.java @@ -0,0 +1,9 @@ +package com.techcourse.exception; + +import webmvc.org.springframework.web.servlet.mvc.exception.NotFoundException; + +public class UserNotFoundException extends NotFoundException { + public UserNotFoundException(final String message) { + super(message); + } +} diff --git a/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java b/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java index e89c1743f3..6f8240c49d 100644 --- a/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java +++ b/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java @@ -1,13 +1,25 @@ package core.org.springframework.util; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.List; +import java.util.function.Predicate; +import java.util.stream.Collectors; public abstract class ReflectionUtils { + public static Predicate withAnnotation(final Class annotation) { + return input -> input != null && input.isAnnotationPresent(annotation); + } + /** * Obtain an accessible constructor for the given class and parameters. - * @param clazz the clazz to check + * + * @param clazz the clazz to check * @param parameterTypes the parameter types of the desired constructor * @return the constructor reference * @throws NoSuchMethodException if no such constructor exists @@ -25,6 +37,7 @@ public static Constructor accessibleConstructor(Class clazz, Class. * Make the given constructor accessible, explicitly setting it accessible * if necessary. The {@code setAccessible(true)} method is only called * when actually necessary, to avoid unnecessary conflicts. + * * @param ctor the constructor to make accessible * @see Constructor#setAccessible */ @@ -35,4 +48,10 @@ public static void makeAccessible(Constructor ctor) { ctor.setAccessible(true); } } + + public static List getAllMethods(final Class clazz, final Predicate annotatedElementPredicate) { + return Arrays.stream(clazz.getMethods()) + .filter(annotatedElementPredicate) + .collect(Collectors.toList()); + } } 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 fdf4e585d3..02a9d3b08f 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 @@ -1,5 +1,9 @@ package webmvc.org.springframework.web.servlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.view.JspView; + import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -14,6 +18,10 @@ public ModelAndView(final View view) { this.model = new HashMap<>(); } + public static ModelAndView fromJspViewName(final String viewName) { + return new ModelAndView(new JspView(viewName)); + } + public ModelAndView addObject(final String attributeName, final Object attributeValue) { model.put(attributeName, attributeValue); return this; @@ -23,6 +31,10 @@ public Object getObject(final String attributeName) { return model.get(attributeName); } + public void render(final HttpServletRequest request, final HttpServletResponse response) throws Exception { + view.render(model, request, response); + } + public Map getModel() { return Collections.unmodifiableMap(model); } @@ -30,8 +42,4 @@ 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/mvc/DispatcherServlet.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/DispatcherServlet.java new file mode 100644 index 0000000000..7e6d616d3e --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/DispatcherServlet.java @@ -0,0 +1,63 @@ +package webmvc.org.springframework.web.servlet.mvc; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +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.exception.HandlerNotFoundException; +import webmvc.org.springframework.web.servlet.mvc.exception.NotFoundException; +import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerMapping; +import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerExecutionHandlerAdaptor; + +import java.io.IOException; + +public class DispatcherServlet extends HttpServlet { + private static final long serialVersionUID = 1L; + private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); + + + HandlerMappingRegistry handlerMappingRegistry = new HandlerMappingRegistry(); + HandlerAdaptorRegistry handlerAdaptorRegistry = new HandlerAdaptorRegistry(); + + public DispatcherServlet() { + } + + public void addHandlerAdaptor(final HandlerAdaptor handlerAdaptor) { + handlerAdaptorRegistry.addHandlerAdaptor(handlerAdaptor); + } + + public void addHandlerMapping(final HandlerMapping handlerMapping) { + handlerMappingRegistry.addHandlerMapping(handlerMapping); + } + + @Override + public void init() { + handlerMappingRegistry.addHandlerMapping(new AnnotationHandlerMapping("com.techcourse.controller")); + handlerAdaptorRegistry.addHandlerAdaptor(new HandlerExecutionHandlerAdaptor()); + handlerMappingRegistry.initialize(); + } + + @Override + protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException, IOException { + final String requestURI = request.getRequestURI(); + log.info("Method : {}, Request URI : {}", request.getMethod(), requestURI); + + try { + final Object handler = handlerMappingRegistry.getHandler(request) + .orElseThrow(() -> new HandlerNotFoundException("Handler Not Found")); + final HandlerAdaptor handlerAdaptor = handlerAdaptorRegistry.getHandlerAdaptor(handler); + final ModelAndView modelAndView = handlerAdaptor.handle(request, response, handler); + modelAndView.render(request, response); + } catch (NotFoundException e) { + log.error("Exception : {}", e.getMessage(), e); + response.sendError(HttpServletResponse.SC_NOT_FOUND); + } catch (Throwable e) { + log.error("Exception : {}", e.getMessage(), e); + throw new ServletException(e.getMessage()); + } + } + +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdaptor.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdaptor.java new file mode 100644 index 0000000000..77cfb8b4d4 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdaptor.java @@ -0,0 +1,11 @@ +package webmvc.org.springframework.web.servlet.mvc; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.ModelAndView; + +public interface HandlerAdaptor { + boolean supports(final Object handler); + + ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception; +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdaptorRegistry.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdaptorRegistry.java new file mode 100644 index 0000000000..949eb247a2 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerAdaptorRegistry.java @@ -0,0 +1,19 @@ +package webmvc.org.springframework.web.servlet.mvc; + +import java.util.ArrayList; +import java.util.List; + +public class HandlerAdaptorRegistry { + private final List handlerAdaptors = new ArrayList<>(); + + public void addHandlerAdaptor(final HandlerAdaptor handlerAdaptor) { + handlerAdaptors.add(handlerAdaptor); + } + + public HandlerAdaptor getHandlerAdaptor(final Object handler) { + return handlerAdaptors.stream() + .filter(handlerAdaptor -> handlerAdaptor.supports(handler)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("해당 핸들러를 처리할 수 있는 핸들러 어댑터가 없습니다.")); + } +} 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/HandlerMapping.java similarity index 74% rename from mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerMapping.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMapping.java index d26a232f55..126604eb52 100644 --- 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/HandlerMapping.java @@ -1,4 +1,4 @@ -package webmvc.org.springframework.web.servlet.mvc.tobe; +package webmvc.org.springframework.web.servlet.mvc; import jakarta.servlet.http.HttpServletRequest; diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMappingRegistry.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMappingRegistry.java new file mode 100644 index 0000000000..277c46dd96 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/HandlerMappingRegistry.java @@ -0,0 +1,27 @@ +package webmvc.org.springframework.web.servlet.mvc; + +import jakarta.servlet.http.HttpServletRequest; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class HandlerMappingRegistry { + private final List handlerMappings = new ArrayList<>(); + + public void addHandlerMapping(final HandlerMapping handlerMapping) { + handlerMappings.add(handlerMapping); + } + + public Optional getHandler(final HttpServletRequest request) { + return handlerMappings.stream() + .map(handlerMapping -> handlerMapping.getHandler(request)) + .filter(Objects::nonNull) + .findFirst(); + } + + public void initialize() { + handlerMappings.forEach(HandlerMapping::initialize); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/Controller.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/Controller.java deleted file mode 100644 index bdd1fde780..0000000000 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/Controller.java +++ /dev/null @@ -1,8 +0,0 @@ -package webmvc.org.springframework.web.servlet.mvc.asis; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -public interface Controller { - String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception; -} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ForwardController.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ForwardController.java deleted file mode 100644 index cd8f1ef371..0000000000 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ForwardController.java +++ /dev/null @@ -1,20 +0,0 @@ -package webmvc.org.springframework.web.servlet.mvc.asis; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.util.Objects; - -public class ForwardController implements Controller { - - private final String path; - - public ForwardController(final String path) { - this.path = Objects.requireNonNull(path); - } - - @Override - public String execute(final HttpServletRequest request, final HttpServletResponse response) { - return path; - } -} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/HandlerNotFoundException.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/HandlerNotFoundException.java new file mode 100644 index 0000000000..16f23de378 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/HandlerNotFoundException.java @@ -0,0 +1,7 @@ +package webmvc.org.springframework.web.servlet.mvc.exception; + +public class HandlerNotFoundException extends NotFoundException { + public HandlerNotFoundException(final String message) { + super(message); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/NotFoundException.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/NotFoundException.java new file mode 100644 index 0000000000..c3d7eadaa8 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/exception/NotFoundException.java @@ -0,0 +1,7 @@ +package webmvc.org.springframework.web.servlet.mvc.exception; + +public class NotFoundException extends RuntimeException { + public NotFoundException(final String message) { + super(message); + } +} 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 b23f6a0fa2..f246c52504 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 @@ -1,18 +1,19 @@ package webmvc.org.springframework.web.servlet.mvc.tobe; -import context.org.springframework.stereotype.Controller; import jakarta.servlet.http.HttpServletRequest; -import org.reflections.Reflections; 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.mvc.HandlerMapping; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; public class AnnotationHandlerMapping implements HandlerMapping { @@ -28,25 +29,39 @@ public AnnotationHandlerMapping(final Object... basePackage) { @Override public void initialize() { - final Reflections reflections = new Reflections(basePackage); - final Set> typesAnnotatedWith = reflections.getTypesAnnotatedWith(Controller.class); - typesAnnotatedWith.stream() - .map(Class::getMethods) - .flatMap(methods -> List.of(methods).stream()) - .filter(method -> method.isAnnotationPresent(RequestMapping.class)) - .forEach(this::generateHandlerMapping); - log.info("Initialized AnnotationHandlerMapping!"); + final ControllerScanner controllerScanner = new ControllerScanner(basePackage); + final Map, Object> controllers = controllerScanner.getControllers(); + final Set> controllerClasses = controllers.keySet(); + Set methods = getRequestMappingMethods(controllerClasses); + methods.forEach(method -> { + final RequestMapping requestMapping = method.getAnnotation(RequestMapping.class); + addHandlerExecutions(controllers, method, requestMapping); + }); + } + + private void addHandlerExecutions(Map, Object> controllerMapper, final Method method, final RequestMapping requestMapping) { + final String requestURI = requestMapping.value(); + List handlerKeys = mapHandlerKeys(requestURI, requestMapping.method()); + handlerKeys.forEach(handlerKey -> { + final Object instance = controllerMapper.get(method.getDeclaringClass()); + handlerExecutions.put(handlerKey, new HandlerExecution(instance, method)); + }); } - private void generateHandlerMapping(final Method method) { - final RequestMapping mapping = method.getAnnotation(RequestMapping.class); - for (final RequestMethod requestMethod : mapping.method()) { - final HandlerKey key = new HandlerKey(mapping.value(), requestMethod); - final HandlerExecution handlerExecution = new HandlerExecution(method); - handlerExecutions.put(key, handlerExecution); - } + private List mapHandlerKeys(final String requestURI, final RequestMethod[] methods) { + return Arrays.stream(methods) + .map(method -> new HandlerKey(requestURI, method)) + .collect(Collectors.toList()); } + private Set getRequestMappingMethods(final Set> controllerClasses) { + return controllerClasses.stream() + .flatMap(controllerClass -> Arrays.stream(controllerClass.getMethods())) + .filter(method -> method.isAnnotationPresent(RequestMapping.class)) + .collect(Collectors.toSet()); + } + + @Override public Object getHandler(final HttpServletRequest request) { final String requestURI = request.getRequestURI(); diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScanner.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScanner.java new file mode 100644 index 0000000000..31c4441838 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScanner.java @@ -0,0 +1,34 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import context.org.springframework.stereotype.Controller; +import org.reflections.Reflections; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +public class ControllerScanner { + private final Reflections reflections; + + public ControllerScanner(final Object... basePackage) { + this.reflections = new Reflections(basePackage); + } + + public Map, Object> getControllers() { + final Set> typesAnnotatedWith = this.reflections.getTypesAnnotatedWith(Controller.class); + return instantiateControllers(typesAnnotatedWith); + } + + private Map, Object> instantiateControllers(Set> controllers) { + return controllers.stream() + .collect(Collectors.toMap(controller -> controller, this::instantiateController)); + } + + private Object instantiateController(final Class controller) { + try { + return controller.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} 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 a32cb5c4b9..4705fb49f6 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 @@ -8,17 +8,23 @@ public class HandlerExecution { private final Method method; - private final Object controller; + private final Object declaredObject; + public HandlerExecution(Method method) { this.method = method; try { - this.controller = method.getDeclaringClass().getConstructor().newInstance(); + this.declaredObject = method.getDeclaringClass().getConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } + public HandlerExecution(Object declaredObject, Method method) { + this.declaredObject = declaredObject; + this.method = method; + } + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - return (ModelAndView) method.invoke(controller, request, response); + return (ModelAndView) method.invoke(declaredObject, request, response); } } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecutionHandlerAdaptor.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecutionHandlerAdaptor.java new file mode 100644 index 0000000000..622dd4db5b --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecutionHandlerAdaptor.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; +import webmvc.org.springframework.web.servlet.mvc.HandlerAdaptor; + +public class HandlerExecutionHandlerAdaptor implements HandlerAdaptor { + @Override + public boolean supports(final Object handler) { + return handler instanceof HandlerExecution; + } + + @Override + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception { + return ((HandlerExecution) handler).handle(request, response); + } +} 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 2c0ba3fc3b..b946c0a1bf 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 @@ -1,5 +1,7 @@ package webmvc.org.springframework.web.servlet.view; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import webmvc.org.springframework.web.servlet.View; @@ -10,6 +12,21 @@ public class JsonView implements View { @Override public void render(final Map model, final HttpServletRequest request, HttpServletResponse response) throws Exception { + String body = createJsonBody(model); + response.setContentType("application/json"); + response.setCharacterEncoding("utf-8"); + response.getWriter().write(body); + } + + private String createJsonBody(final Map model) throws JsonProcessingException { + final ObjectMapper objectMapper = new ObjectMapper(); + String json = ""; + if (model.size() == 1) { + json = objectMapper.writeValueAsString(model.get(model.keySet().iterator().next())); + } else { + json = objectMapper.writeValueAsString(model); + } + return json; } @Override 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 41161a581e..a201d9ae85 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 @@ -21,14 +21,18 @@ public JspView(final String viewName) { @Override public void render(final Map model, final HttpServletRequest request, final HttpServletResponse response) throws Exception { - // todo + if (viewName.startsWith(JspView.REDIRECT_PREFIX)) { + response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length())); + return; + } model.keySet().forEach(key -> { log.debug("attribute name : {}, value : {}", key, model.get(key)); request.setAttribute(key, model.get(key)); }); - // todo + final var requestDispatcher = request.getRequestDispatcher(viewName); + requestDispatcher.forward(request, response); } @Override diff --git a/study/src/test/java/di/stage3/context/DIContainer.java b/study/src/test/java/di/stage3/context/DIContainer.java index b62feb1ed3..3084900fcc 100644 --- a/study/src/test/java/di/stage3/context/DIContainer.java +++ b/study/src/test/java/di/stage3/context/DIContainer.java @@ -1,6 +1,14 @@ package di.stage3.context; +import java.lang.reflect.Constructor; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * 스프링의 BeanFactory, ApplicationContext에 해당되는 클래스 @@ -10,11 +18,68 @@ class DIContainer { private final Set beans; public DIContainer(final Set> classes) { - this.beans = Set.of(); + beans = instantiateBeans(classes); + } + + private Set instantiateBeans(final Set> classes) { + final Map, Object> classObjectMap = new HashMap<>(); + final List> sortedClasses = classes.stream() + .sorted(Comparator.comparing(a -> a.getConstructors()[0].getParameterCount())) + .collect(Collectors.toList()); + System.out.println("sortedClasses = " + sortedClasses); + sortedClasses.forEach(aClass -> { + final Constructor[] constructors = aClass.getConstructors(); + Arrays.stream(constructors).forEachOrdered(constructor -> instantiateBean(aClass, constructor, classObjectMap)); + }); + return new HashSet<>(classObjectMap.values()); + } + + private static void instantiateBean(final Class aClass, final Constructor constructor, final Map, Object> classObjectMap) { + if (constructor.getParameterCount() == 0) { + try { + putInterfacesToInstanceEntry(aClass, constructor, classObjectMap); + } catch (Exception e) { + throw new RuntimeException(e); + } + } else { + findDependencyAndInstantiateBean(aClass, constructor, classObjectMap); + } + } + + private static void findDependencyAndInstantiateBean(final Class aClass, final Constructor constructor, final Map, Object> classObjectMap) { + final Class[] parameterTypes = constructor.getParameterTypes(); + final Object[] parameters = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + final Class parameterType = parameterTypes[i]; + final Object parameter = classObjectMap.get(parameterType); + parameters[i] = parameter; + } + try { + putInterfacesToInstanceEntry(aClass, constructor, classObjectMap, parameters); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private static void putInterfacesToInstanceEntry(final Class aClass, final Constructor constructor, final Map, Object> classObjectMap, final Object... parameters) throws Exception { + final Class[] interfaces = aClass.getInterfaces(); + final Object instance = constructor.newInstance(parameters); + if (interfaces.length == 0) { + classObjectMap.put(aClass, instance); + } else { + for (final Class anInterface : interfaces) { + classObjectMap.put(anInterface, instance); + } + } } @SuppressWarnings("unchecked") public T getBean(final Class aClass) { - return null; + System.out.println("beans = " + beans); + return (T) beans.stream() + .peek(System.out::println) + .filter(bean -> aClass.isAssignableFrom(bean.getClass())) + .findFirst() + .orElseThrow(() -> new RuntimeException("해당하는 클래스가 존재하지 않습니다.")); } } diff --git a/study/src/test/java/di/stage4/annotations/ClassPathScanner.java b/study/src/test/java/di/stage4/annotations/ClassPathScanner.java index 9dab1fd9c4..68b9dc94f6 100644 --- a/study/src/test/java/di/stage4/annotations/ClassPathScanner.java +++ b/study/src/test/java/di/stage4/annotations/ClassPathScanner.java @@ -1,10 +1,17 @@ package di.stage4.annotations; +import org.reflections.Reflections; + +import java.util.HashSet; import java.util.Set; public class ClassPathScanner { public static Set> getAllClassesInPackage(final String packageName) { - return null; + final HashSet> classes = new HashSet<>(); + final Reflections reflections = new Reflections(packageName); + classes.addAll(reflections.getTypesAnnotatedWith(Repository.class)); + classes.addAll(reflections.getTypesAnnotatedWith(Service.class)); + return classes; } } diff --git a/study/src/test/java/di/stage4/annotations/DIContainer.java b/study/src/test/java/di/stage4/annotations/DIContainer.java index 9248ecad7e..cd383aa6c2 100644 --- a/study/src/test/java/di/stage4/annotations/DIContainer.java +++ b/study/src/test/java/di/stage4/annotations/DIContainer.java @@ -1,6 +1,15 @@ package di.stage4.annotations; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; /** * 스프링의 BeanFactory, ApplicationContext에 해당되는 클래스 @@ -10,15 +19,100 @@ class DIContainer { private final Set beans; public DIContainer(final Set> classes) { - this.beans = Set.of(); + beans = instantiateBeans(classes); } - public static DIContainer createContainerForPackage(final String rootPackageName) { - return null; + private Set instantiateBeans(final Set> classes) { + final Map, Object> classObjectMap = new HashMap<>(); + final List> sortedClasses = sortClassByLessDependnecies(classes); + for (final Class sortedClass : sortedClasses) { + if (sortedClass.getConstructors().length != 0) { + injectConstructorDependency(sortedClass, classObjectMap); + } + injectFieldDependency(sortedClass, classObjectMap); + } + return new HashSet<>(classObjectMap.values()); + } + + private List> sortClassByLessDependnecies(final Set> classes) { + return classes.stream() + .sorted(Comparator + .comparing(a -> ((Class) a).getDeclaredFields().length) + .reversed() + ) + .collect(Collectors.toList()); + } + + private void injectFieldDependency(final Class sortedClass, final Map, Object> classObjectMap) { + Arrays.stream(sortedClass.getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Inject.class)) + .forEach(field -> { + try { + field.setAccessible(true); + final Object instance = classObjectMap.getOrDefault(sortedClass, instantiatePrivateConstructor(sortedClass)); + field.set(instance, classObjectMap.get(field.getType())); + classObjectMap.put(sortedClass, instance); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + private Object instantiatePrivateConstructor(final Class sortedClass) throws InstantiationException, IllegalAccessException, InvocationTargetException { + final Constructor declaredConstructor = sortedClass.getDeclaredConstructors()[0]; + declaredConstructor.setAccessible(true); + return declaredConstructor.newInstance(); + } + + private void injectConstructorDependency(final Class sortedClass, final Map, Object> classObjectMap) { + final Constructor defaultConstructor = sortedClass.getConstructors()[0]; + if (defaultConstructor.getParameterCount() != 0) { + instantiateBeanWithParameters(sortedClass, classObjectMap, defaultConstructor); + } else { + instantiateBean(sortedClass, classObjectMap, defaultConstructor); + } + } + + private void instantiateBean(final Class sortedClass, final Map, Object> classObjectMap, final Constructor defaultConstructor) { + try { + putInterfaceOrClass(sortedClass, classObjectMap, defaultConstructor); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void instantiateBeanWithParameters(final Class sortedClass, final Map, Object> classObjectMap, final Constructor defaultConstructor) { + final Class[] parameterTypes = defaultConstructor.getParameterTypes(); + final Object[] parameters = new Object[parameterTypes.length]; + for (int i = 0; i < parameterTypes.length; i++) { + parameters[i] = classObjectMap.get(parameterTypes[i]); + } + try { + putInterfaceOrClass(sortedClass, classObjectMap, defaultConstructor, parameters); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void putInterfaceOrClass(final Class sortedClass, final Map, Object> classObjectMap, final Constructor constructor, final Object... parameters) throws InstantiationException, IllegalAccessException, InvocationTargetException { + final Class[] interfaces = sortedClass.getInterfaces(); + for (final Class anInterface : interfaces) { + classObjectMap.put(anInterface, constructor.newInstance(parameters)); + } + classObjectMap.put(sortedClass, constructor.newInstance(parameters)); + } + + public static DIContainer createContainerForPackage(final String packageName) { + return new DIContainer(ClassPathScanner.getAllClassesInPackage(packageName)); } @SuppressWarnings("unchecked") public T getBean(final Class aClass) { - return null; + System.out.println("beans = " + beans); + return (T) beans.stream() + .peek(System.out::println) + .filter(bean -> aClass.isAssignableFrom(bean.getClass())) + .findFirst() + .orElseThrow(() -> new RuntimeException("해당하는 클래스가 존재하지 않습니다.")); } }