Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MVC 구현하기 - 2단계] 후디(조동현) 미션 제출합니다. #256

Merged
merged 8 commits into from
Sep 26, 2022
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

import jakarta.servlet.ServletContext;
import nextstep.mvc.DispatcherServlet;
import nextstep.mvc.controller.asis.ControllerHandlerAdapter;
import nextstep.mvc.controller.tobe.AnnotationHandlerMapping;
import nextstep.mvc.controller.tobe.HandlerExecutionHandlerAdapter;
import nextstep.web.WebApplicationInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -14,6 +16,9 @@ public class AppWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(final ServletContext servletContext) {
final var dispatcherServlet = new DispatcherServlet();
dispatcherServlet.addHandlerAdapter(new ControllerHandlerAdapter());
dispatcherServlet.addHandlerAdapter(new HandlerExecutionHandlerAdapter());

dispatcherServlet.addHandlerMapping(new ManualHandlerMapping());
dispatcherServlet.addHandlerMapping(new AnnotationHandlerMapping("com.techcourse.controller"));

Expand Down
11 changes: 5 additions & 6 deletions app/src/main/java/com/techcourse/ManualHandlerMapping.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
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 java.util.HashMap;
import java.util.Map;
import nextstep.mvc.HandlerMapping;
import nextstep.mvc.controller.asis.Controller;
import nextstep.mvc.controller.asis.ForwardController;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.HashMap;
import java.util.Map;

public class ManualHandlerMapping implements HandlerMapping {

private static final Logger log = LoggerFactory.getLogger(ManualHandlerMapping.class);
Expand All @@ -23,8 +24,6 @@ public void initialize() {
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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,30 @@
import com.techcourse.repository.InMemoryUserRepository;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import nextstep.mvc.controller.asis.Controller;
import nextstep.mvc.view.JspView;
import nextstep.mvc.view.ModelAndView;
import nextstep.web.annotation.Controller;
import nextstep.web.annotation.RequestMapping;
import nextstep.web.support.RequestMethod;

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 save(HttpServletRequest req, HttpServletResponse res) {
final var user = new User(2,
req.getParameter("account"),
req.getParameter("password"),
req.getParameter("email"));
InMemoryUserRepository.save(user);

return "redirect:/index.jsp";
JspView jspView = new JspView("redirect:/index.jsp");
return new ModelAndView(jspView);
}

@RequestMapping(value = "/register/view", method = RequestMethod.GET)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분도 /register/view가 아닌 /register로 변경하는 것이 어떨까요? ㅎㅎ

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3단계 미션에서 반영했습니다! :)

public ModelAndView show(HttpServletRequest req, HttpServletResponse res) {
JspView jspView = new JspView("/register.jsp");
return new ModelAndView(jspView);
}
}

This file was deleted.

57 changes: 15 additions & 42 deletions mvc/src/main/java/nextstep/mvc/DispatcherServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import nextstep.mvc.controller.asis.Controller;
import nextstep.mvc.controller.tobe.HandlerExecution;
import nextstep.mvc.view.JspView;
import nextstep.mvc.controller.tobe.exception.ControllerNotFoundException;
import nextstep.mvc.view.ModelAndView;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -20,19 +14,24 @@ public class DispatcherServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);

private final List<HandlerMapping> handlerMappings;
private final HandlerAdapterRegistry handlerAdapterRegistry;
private final HandlerMappingRegistry handlerMappingRegistry;

public DispatcherServlet() {
this.handlerMappings = new ArrayList<>();
this.handlerAdapterRegistry = new HandlerAdapterRegistry();
this.handlerMappingRegistry = new HandlerMappingRegistry();
}

@Override
public void init() {
handlerMappings.forEach(HandlerMapping::initialize);
}

public void addHandlerAdapter(final HandlerAdapter handlerAdapter) {
handlerAdapterRegistry.addHandlerAdapter(handlerAdapter);
}

public void addHandlerMapping(final HandlerMapping handlerMapping) {
handlerMappings.add(handlerMapping);
handlerMappingRegistry.addHandlerMapping(handlerMapping);
}

@Override
Expand All @@ -41,41 +40,15 @@ protected void service(final HttpServletRequest request, final HttpServletRespon
log.debug("Method : {}, Request URI : {}", request.getMethod(), request.getRequestURI());

try {
final Object controller = getController(request);

// TODO: 다형성을 통해 해결해야함
if (controller instanceof HandlerExecution) {
HandlerExecution handlerExecution = (HandlerExecution) controller;
ModelAndView modelAndView = handlerExecution.handle(request, response);
Map<String, Object> model = modelAndView.getModel();
modelAndView.getView().render(model, request, response);
return;
}
Object controller = handlerMappingRegistry.getHandler(request)
.orElseThrow(ControllerNotFoundException::new);

final var viewName = ((Controller) controller).execute(request, response);
move(viewName, request, response);
HandlerAdapter handlerAdapter = handlerAdapterRegistry.getHandlerAdapter(controller);
ModelAndView modelAndView = handlerAdapter.handle(request, response, controller);
modelAndView.render(request, response);
} catch (Throwable e) {
log.error("Exception : {}", e.getMessage(), e);
throw new ServletException(e.getMessage());
}
}

private Object getController(final HttpServletRequest request) {
return handlerMappings.stream()
.map(handlerMapping -> handlerMapping.getHandler(request))
.filter(Objects::nonNull)
.findFirst()
.orElseThrow();
}

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;
}
Comment on lines -71 to -76
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

나중에 이부분도 DispatcherServlet이 아닌 View 쪽으로 로직을 넘기면 좋을 것 같아요!

이번 PR에서 로직을 모두 옮겼습니다 😄


final var requestDispatcher = request.getRequestDispatcher(viewName);
requestDispatcher.forward(request, response);
}
}
25 changes: 25 additions & 0 deletions mvc/src/main/java/nextstep/mvc/HandlerAdapterRegistry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package nextstep.mvc;

import java.util.ArrayList;
import java.util.List;

public class HandlerAdapterRegistry {

private final List<HandlerAdapter> handlerAdapters;

public HandlerAdapterRegistry() {
this.handlerAdapters = new ArrayList<>();
}

public void addHandlerAdapter(final HandlerAdapter handlerAdapter) {
handlerAdapters.add(handlerAdapter);
}

public HandlerAdapter getHandlerAdapter(final Object handler) {
return handlerAdapters.stream()
.filter(it -> it.supports(handler))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 핸들러입니다."));
// TODO: 적절한 예외로 변경
}
}
28 changes: 28 additions & 0 deletions mvc/src/main/java/nextstep/mvc/HandlerMappingRegistry.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package nextstep.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<HandlerMapping> handlerMappings;

public HandlerMappingRegistry() {
handlerMappings = new ArrayList<>();
}

public void addHandlerMapping(final HandlerMapping handlerMapping) {
handlerMapping.initialize();
handlerMappings.add(handlerMapping);
}

public Optional<Object> getHandler(final HttpServletRequest request) {
return handlerMappings.stream()
.map(handlerMapping -> handlerMapping.getHandler(request))
.filter(Objects::nonNull)
.findFirst();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package nextstep.mvc.controller.asis;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import nextstep.mvc.HandlerAdapter;
import nextstep.mvc.view.JspView;
import nextstep.mvc.view.ModelAndView;

public class ControllerHandlerAdapter implements HandlerAdapter {

@Override
public boolean supports(final Object handler) {
return handler instanceof Controller;
}

@Override
public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response,
final Object handler) throws Exception {
String viewName = ((Controller) handler).execute(request, response);
JspView jspView = new JspView(viewName);
return new ModelAndView(jspView);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,9 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import nextstep.mvc.HandlerMapping;
import nextstep.mvc.controller.tobe.exception.ControllerNotFoundException;
import nextstep.web.annotation.Controller;
import nextstep.web.annotation.RequestMapping;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -30,8 +25,9 @@ public AnnotationHandlerMapping(final Object... basePackage) {
}

public void initialize() {
Set<Class<?>> controllerClasses = extractClasses();
List<Method> methods = extractMethods(controllerClasses);
ControllerScanner controllerScanner = new ControllerScanner();
Map<Class<?>, Object> controllers = controllerScanner.getControllers(basePackage);
List<Method> methods = extractMethods(controllers);

for (Method method : methods) {
addHandlerExecutions(method);
Expand All @@ -40,13 +36,8 @@ public void initialize() {
log.info("Initialized AnnotationHandlerMapping!");
}

private Set<Class<?>> extractClasses() {
Reflections classReflections = new Reflections(basePackage, Scanners.TypesAnnotated);
return classReflections.getTypesAnnotatedWith(Controller.class);
}

private List<Method> extractMethods(final Set<Class<?>> controllers) {
return controllers.stream()
private List<Method> extractMethods(final Map<Class<?>, Object> controllers) {
return controllers.keySet().stream()
.flatMap(it -> Arrays.stream(it.getMethods()))
.filter(it -> it.isAnnotationPresent(RequestMapping.class))
.collect(Collectors.toList());
Expand All @@ -62,14 +53,8 @@ private void addHandlerExecutions(final Method method) {
}
}

public Object getHandler(final HttpServletRequest request) {
public HandlerExecution getHandler(final HttpServletRequest request) {
HandlerKey handlerKey = new HandlerKey(request);
HandlerExecution handlerExecution = handlerExecutions.get(handlerKey);

if (handlerExecution == null) {
throw new ControllerNotFoundException();
}

return handlerExecution;
return handlerExecutions.get(handlerKey);
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

request가 들어오면 DispatcherServlet에 있는 모든 HandlerMapping을 돌며 handle이 가능한지 여부를 확인할텐데 이렇게 특정 HandlerMapping에서 NotFoundException을 터트려버리면 다른 HandlerMapping에서 해당 request를 처리할 수 있는 여지를 남길 수 없을 것 같아요. 모든 HandlerMapping을 전부 순회했는데도 불구하고 찾지 못한다면 그 때 ControllerNotFoundException을 터트리는 것이 어떨까요?

남겨주신 부분이 맞는 것 같습니다! ControllerHandlerMapping 과 동일하게 존재하지 않으면, null 을 반환하게 하고 예외처리는 HandlerMappingRegistry 에서 처리했습니다!

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package nextstep.mvc.controller.tobe;

import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import nextstep.web.annotation.Controller;
import org.reflections.Reflections;
import org.reflections.scanners.Scanners;

public class ControllerScanner {

public Map<Class<?>, Object> getControllers(final Object[] basePackage) {
Reflections classReflections = new Reflections(basePackage, Scanners.TypesAnnotated);
Set<Class<?>> classes = classReflections.getTypesAnnotatedWith(Controller.class);

return instantiateControllers(classes);
}

private Map<Class<?>, Object> instantiateControllers(final Set<Class<?>> classes) {
return classes.stream()
.collect(Collectors.toMap(clazz -> clazz, this::instantiateClass));
}

private Object instantiateClass(final Class<?> clazz) {
try {
return clazz.getConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new IllegalArgumentException("Controller를 인스턴스화 할 수 없습니다.");
// TODO: 적절한 예외를 던져야함
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package nextstep.mvc.controller.tobe;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import nextstep.mvc.HandlerAdapter;
import nextstep.mvc.view.ModelAndView;

public class HandlerExecutionHandlerAdapter implements HandlerAdapter {

@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);
}
}
Loading