Skip to content

Commit

Permalink
[MVC 구현하기 - 1단계] 유콩(김유빈) 미션 제출합니다. (#169)
Browse files Browse the repository at this point in the history
* docs: 1단계 체크리스트 작성

* feat: @controller 읽어와 핸들러 추가

* feat: 핸들러 메서드 실행

* feat: DispatcherServlet 초기화 시 어노테이션 기반 핸들러 매핑 추가

* feat: HandlerAdapter 이용하여 Handler 실제 로직 수행

* feat: ModelAndView rendering
  • Loading branch information
kyukong authored Sep 22, 2022
1 parent 10a03b3 commit 16b867b
Show file tree
Hide file tree
Showing 15 changed files with 296 additions and 22 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
# @MVC 구현하기

## 🚀 1단계 - @MVC 프레임워크 구현하기
- [x] AnnotationHandlerMappingTest가 정상 동작한다.
- [x] DispatcherServlet에서 HandlerMapping 인터페이스를 활용하여 AnnotationHandlerMapping과 ManualHandlerMapping 둘다 처리할 수 있다.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import jakarta.servlet.ServletContext;
import nextstep.mvc.DispatcherServlet;
import nextstep.mvc.controller.tobe.AnnotationHandlerAdapter;
import nextstep.mvc.controller.tobe.AnnotationHandlerMapping;
import nextstep.web.WebApplicationInitializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand All @@ -13,12 +15,20 @@ public class AppWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(final ServletContext servletContext) {
final var dispatcherServlet = new DispatcherServlet();
dispatcherServlet.addHandlerMapping(new ManualHandlerMapping());
addHandlerMappingAndAdapter(dispatcherServlet);

final var dispatcher = servletContext.addServlet("dispatcher", dispatcherServlet);
dispatcher.setLoadOnStartup(1);
dispatcher.addMapping("/");

log.info("Start AppWebApplication Initializer");
}

private void addHandlerMappingAndAdapter(final DispatcherServlet dispatcherServlet) {
dispatcherServlet.addHandlerMapping(new ManualHandlerMapping());
dispatcherServlet.addHandlerMapping(new AnnotationHandlerMapping());

dispatcherServlet.addHandlerAdapter(new ManualHandlerAdapter());
dispatcherServlet.addHandlerAdapter(new AnnotationHandlerAdapter());
}
}
21 changes: 21 additions & 0 deletions app/src/main/java/com/techcourse/ManualHandlerAdapter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.techcourse;

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

public class ManualHandlerAdapter 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 {
final String viewName = ((Controller) handler).execute(request, response);
return new ModelAndView(viewName);
}
}
43 changes: 29 additions & 14 deletions mvc/src/main/java/nextstep/mvc/DispatcherServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
import jakarta.servlet.http.HttpServlet;
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 org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;

public class DispatcherServlet extends HttpServlet {
Expand All @@ -19,9 +20,11 @@ public class DispatcherServlet extends HttpServlet {
private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);

private final List<HandlerMapping> handlerMappings;
private final List<HandlerAdapter> handlerAdapters;

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

@Override
Expand All @@ -33,36 +36,48 @@ public void addHandlerMapping(final HandlerMapping handlerMapping) {
handlerMappings.add(handlerMapping);
}

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

@Override
protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException {
log.debug("Method : {}, Request URI : {}", request.getMethod(), request.getRequestURI());

try {
final var controller = getController(request);
final var viewName = controller.execute(request, response);
move(viewName, request, response);
final Object handler = getHandler(request);
final ModelAndView modelAndView = getModelAndView(handler, request, response);
final Map<String, Object> model = modelAndView.getModel();
modelAndView.getView().render(model, request, response);
} catch (Throwable e) {
log.error("Exception : {}", e.getMessage(), e);
throw new ServletException(e.getMessage());
}
}

private Controller getController(final HttpServletRequest request) {
private Object getHandler(final HttpServletRequest request) {
return handlerMappings.stream()
.map(handlerMapping -> handlerMapping.getHandler(request))
.filter(Objects::nonNull)
.map(Controller.class::cast)
.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;
}
private ModelAndView getModelAndView(final Object handler, final HttpServletRequest request, final HttpServletResponse response) {
return handlerAdapters.stream()
.filter(handlerAdapter -> handlerAdapter.supports(handler))
.map(handlerAdapter -> getModelAndView(handler, request, response, handlerAdapter))
.findFirst()
.orElseThrow();
}

final var requestDispatcher = request.getRequestDispatcher(viewName);
requestDispatcher.forward(request, response);
private ModelAndView getModelAndView(final Object handler,
final HttpServletRequest request, final HttpServletResponse response, final HandlerAdapter handlerAdapter
){
try {
return handlerAdapter.handle(request, response, handler);
} catch (Exception e) {
throw new IllegalArgumentException(String.format("적절하지 않은 handler 입니다. [%s]", handler));
}
}
}
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;
import nextstep.web.annotation.Controller;

public class AnnotationHandlerAdapter implements HandlerAdapter {

@Override
public boolean supports(final Object handler) {
return handler.getClass().isAnnotationPresent(Controller.class);
}

@Override
public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
return ((HandlerExecution) handler).handle(request, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,18 @@

import jakarta.servlet.http.HttpServletRequest;
import nextstep.mvc.HandlerMapping;
import nextstep.web.annotation.Controller;
import nextstep.web.annotation.RequestMapping;
import nextstep.web.support.RequestMethod;

import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class AnnotationHandlerMapping implements HandlerMapping {

Expand All @@ -20,11 +27,55 @@ public AnnotationHandlerMapping(final Object... basePackage) {
this.handlerExecutions = new HashMap<>();
}

@Override
public void initialize() {
final Reflections reflections = new Reflections(basePackage);
final Set<Class<?>> controllers = reflections.getTypesAnnotatedWith(Controller.class);
for (final Class<?> controller : controllers) {
addRequestMappingMethod(controller);
}
log.info("Initialized AnnotationHandlerMapping!");
}

@Override
public Object getHandler(final HttpServletRequest request) {
return null;
final HandlerKey handlerKey = new HandlerKey(request.getRequestURI(), RequestMethod.from(request.getMethod()));
log.info("request handler [{}]", handlerKey);
if (!handlerExecutions.containsKey(handlerKey)) {
throw new IllegalArgumentException(String.format("요청한 핸들러가 존재하지 않습니다. [%s]", handlerKey));
}
final HandlerExecution handlerExecution = handlerExecutions.get(handlerKey);
log.info("search handler [{}]", handlerExecution);
return handlerExecution;
}

private void addRequestMappingMethod(final Class<?> controller) {
final Method[] methods = controller.getMethods();
for (final Method method : methods) {
if (!method.isAnnotationPresent(RequestMapping.class)) {
continue;
}
final RequestMapping requestMapping = method.getAnnotation(RequestMapping.class);
addHandlerExecution(controller, requestMapping, method);
}
}

private void addHandlerExecution(final Class<?> controller, final RequestMapping requestMapping, final Method method) {
for (final RequestMethod requestMethod : requestMapping.method()) {
final HandlerKey handlerKey = new HandlerKey(requestMapping.value(), requestMethod);
if (handlerExecutions.containsKey(handlerKey)) {
throw new IllegalArgumentException(String.format("중복적으로 매핑되었습니다. [%s]", handlerKey));
}
handlerExecutions.put(handlerKey, getHandlerExecution(controller, method));
}
}

private HandlerExecution getHandlerExecution(final Class<?> controller, final Method method) {
try {
final Object newInstance = controller.getConstructor().newInstance();
return new HandlerExecution(newInstance, method);
} catch (Exception e) {
throw new IllegalArgumentException(String.format("기본 생성자가 존재하지 않습니다. [%s]", controller));
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,37 @@
package nextstep.mvc.controller.tobe;

import java.lang.reflect.Method;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

public class HandlerExecution {

private final static Logger log = LoggerFactory.getLogger(HandlerExecution.class);

private final Object clazz;
private final Method method;

public HandlerExecution(final Object clazz, final Method method) {
this.clazz = clazz;
this.method = method;
}

public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
return null;
final ModelAndView modelAndView = (ModelAndView) method.invoke(clazz, request, response);
log.info("execute controller method [{}]", method);
return modelAndView;
}

@Override
public String toString() {
return "HandlerExecution{" +
"clazz=" + clazz +
", method=" + method +
'}';
}
}
6 changes: 6 additions & 0 deletions mvc/src/main/java/nextstep/mvc/view/JsonView.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@

public class JsonView implements View {

private final String body;

public JsonView(final String body) {
this.body = body;
}

@Override
public void render(final Map<String, ?> model, final HttpServletRequest request, HttpServletResponse response) throws Exception {
}
Expand Down
31 changes: 28 additions & 3 deletions mvc/src/main/java/nextstep/mvc/view/JspView.java
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package nextstep.mvc.view;

import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Map;

public class JspView implements View {
Expand All @@ -13,18 +15,41 @@ public class JspView implements View {

public static final String REDIRECT_PREFIX = "redirect:";

private final String viewName;
private final boolean redirect;

public JspView(final String viewName) {
validateJsp(viewName);
if (viewName.startsWith(REDIRECT_PREFIX)) {
this.viewName = viewName.substring(JspView.REDIRECT_PREFIX.length());
this.redirect = true;
return;
}
this.viewName = viewName;
this.redirect = false;
}

private void validateJsp(final String viewName) {
if (!viewName.endsWith(".jsp")) {
throw new IllegalArgumentException(String.format("JSP 형식이 아닙니다. [%s]", viewName));
}
}

@Override
public void render(final Map<String, ?> model, final HttpServletRequest request, final HttpServletResponse response) throws Exception {
// todo

addLocation(response);
model.keySet().forEach(key -> {
log.debug("attribute name : {}, value : {}", key, model.get(key));
request.setAttribute(key, model.get(key));
});

// todo
final RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewName);
requestDispatcher.forward(request, response);
}

private void addLocation(final HttpServletResponse response) throws IOException {
if (redirect) {
response.sendRedirect(viewName);
}
}
}
11 changes: 11 additions & 0 deletions mvc/src/main/java/nextstep/mvc/view/ModelAndView.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ public class ModelAndView {
private final View view;
private final Map<String, Object> model;

public ModelAndView(final String viewName) {
this(getView(viewName));
}

public ModelAndView(final View view) {
this.view = view;
this.model = new HashMap<>();
Expand All @@ -19,6 +23,13 @@ public ModelAndView addObject(final String attributeName, final Object attribute
return this;
}

private static View getView(String viewName) {
if (viewName.endsWith(".jsp")) {
return new JspView(viewName);
}
return new JsonView(viewName);
}

public Object getObject(final String attributeName) {
return model.get(attributeName);
}
Expand Down
11 changes: 11 additions & 0 deletions mvc/src/main/java/nextstep/web/support/RequestMethod.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
package nextstep.web.support;

import java.util.Arrays;

public enum RequestMethod {

GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
;

public static RequestMethod from(final String value) {
return Arrays.stream(values())
.filter(method -> method.name().equals(value.toUpperCase()))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(String.format("정의되지 않은 메서드입니다. [%s]", value)));
}
}
Loading

0 comments on commit 16b867b

Please sign in to comment.