Skip to content

Commit

Permalink
[MVC 구현하기 - 2단계] 베로(김은솔) 미션 제출합니다. (#455)
Browse files Browse the repository at this point in the history
* 서블릿 학습 테스트 코드 개선

* refactor: 학습 테스트 구현 완료

* feat: 핸들러를 찾을 수 없는 경우 예외를 반환하도록 변경

* feat: view 이름 getter 추가

* feat: 어노테이션 기반 컨트롤러 지원

* feat: ManualHandlerMapping 지원하는 어댑터 추가

* refactor: 개행 추가

* refactor: 사용하지 않는 파일 삭제

---------

Co-authored-by: kang-hyungu <[email protected]>
  • Loading branch information
Cyma-s and kang-hyungu authored Sep 21, 2023
1 parent 62d063e commit de6c578
Show file tree
Hide file tree
Showing 20 changed files with 378 additions and 34 deletions.
74 changes: 62 additions & 12 deletions app/src/main/java/com/techcourse/DispatcherServlet.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,92 @@
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.NoSuchElementException;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import webmvc.org.springframework.web.servlet.ModelAndView;
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;

public class DispatcherServlet extends HttpServlet {

private static final long serialVersionUID = 1L;
private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class);
private static final String BASE_PACKAGE_PATH = "com.techcourse";

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

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

@Override
public void init() {
manualHandlerMapping = new ManualHandlerMapping();
manualHandlerMapping.initialize();
initHandlerMappings();
initHandlerAdapters();
}

private void initHandlerMappings() {
final List<HandlerMapping> handlerMappingInstances = HandlerMappingFactory.getHandlerMappings(BASE_PACKAGE_PATH)
.stream()
.peek(HandlerMapping::initialize)
.collect(Collectors.toList());
handlerMappings.addAll(handlerMappingInstances);
}

private void initHandlerAdapters() {
final List<HandlerAdapter> handlerAdapterInstances = HandlerAdapterFactory.getHandlerAdapters();
handlerAdapters.addAll(handlerAdapterInstances);
}

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

@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 = manualHandlerMapping.getHandler(requestURI);
final var viewName = controller.execute(request, response);
move(viewName, request, response);
} catch (Throwable e) {
process(request, response);
} catch (Exception e) {
log.error("Exception : {}", e.getMessage(), e);
throw new ServletException(e.getMessage());
}
}

private void move(final String viewName, final HttpServletRequest request, final HttpServletResponse response) throws Exception {
private void process(final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
final Object handler = getHandler(request);
final HandlerAdapter handlerAdapter = getHandlerAdapter(handler);
final ModelAndView modelAndView = handlerAdapter.handle(request, response, handler);
move(modelAndView, request, response);
}

private Object getHandler(final HttpServletRequest request) {
return handlerMappings.stream()
.filter(mapping -> mapping.getHandler(request) != null)
.findFirst()
.orElseThrow(() -> new NoSuchElementException("해당하는 HandlerMapping이 없습니다."))
.getHandler(request);
}

private HandlerAdapter getHandlerAdapter(final Object handler) {
return handlerAdapters.stream()
.filter(adapter -> adapter.supports(handler))
.findFirst()
.orElseThrow(() -> new NoSuchElementException("해당하는 HandlerAdapter가 없습니다."));
}

private void move(
final ModelAndView modelAndView,
final HttpServletRequest request,
final HttpServletResponse response
) throws Exception {
final String viewName = modelAndView.getViewName();
if (viewName.startsWith(JspView.REDIRECT_PREFIX)) {
response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length()));
return;
Expand Down
16 changes: 16 additions & 0 deletions app/src/main/java/com/techcourse/HandlerAdapterFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.techcourse;

import java.util.List;
import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerAdapter;
import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerAdapter;
import webmvc.org.springframework.web.servlet.mvc.tobe.ManualHandlerAdapter;

public class HandlerAdapterFactory {

public static List<HandlerAdapter> getHandlerAdapters() {
return List.of(
new AnnotationHandlerAdapter(),
new ManualHandlerAdapter()
);
}
}
15 changes: 15 additions & 0 deletions app/src/main/java/com/techcourse/HandlerMappingFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.techcourse;

import java.util.List;
import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerMapping;
import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerMapping;

public class HandlerMappingFactory {

public static List<HandlerMapping> getHandlerMappings(final Object... basePackagePath) {
return List.of(
new AnnotationHandlerMapping(basePackagePath),
new ManualHandlerMapping()
);
}
}
23 changes: 15 additions & 8 deletions app/src/main/java/com/techcourse/ManualHandlerMapping.java
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
package com.techcourse;

import com.techcourse.controller.*;
import com.techcourse.controller.LoginController;
import com.techcourse.controller.LoginViewController;
import com.techcourse.controller.LogoutController;
import com.techcourse.controller.RegisterController;
import com.techcourse.controller.RegisterViewController;
import jakarta.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
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<String, Controller> controllers = new HashMap<>();

@Override
public void initialize() {
controllers.put("/", new ForwardController("/index.jsp"));
controllers.put("/login", new LoginController());
Expand All @@ -25,10 +30,12 @@ public void initialize() {

log.info("Initialized Handler Mapping!");
controllers.keySet()
.forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass()));
.forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass()));
}

public Controller getHandler(final String requestURI) {
@Override
public Object getHandler(final HttpServletRequest request) {
final String requestURI = request.getRequestURI();
log.debug("Request Mapping Uri : {}", requestURI);
return controllers.get(requestURI);
}
Expand Down
31 changes: 31 additions & 0 deletions app/src/test/java/com/techcourse/DispatcherServletTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.techcourse;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class DispatcherServletTest {

@DisplayName("해당하는 HandlerMapping 이 존재하지 않으면 예외를 반환한다.")
@Test
void notExistHandlerMappingThrowsException() {
// given
final DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.init();
final HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getRequestURI()).thenReturn("/not-exist");
when(request.getMethod()).thenReturn("GET");

// when
// then
assertThatThrownBy(() -> dispatcherServlet.service(request, mock(HttpServletResponse.class)))
.isInstanceOf(ServletException.class)
.hasMessage("해당하는 HandlerMapping이 없습니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,8 @@ public Map<String, Object> getModel() {
public View getView() {
return view;
}

public String getViewName() {
return view.getName();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.util.Map;

public interface View {

void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response) throws Exception;

String getName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
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(final Object handler) {
return handler instanceof HandlerExecution;
}

@Override
public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response,
final Object handler)
throws Exception {
final HandlerExecution handlerExecution = (HandlerExecution) handler;
return handlerExecution.handle(request, response);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import web.org.springframework.web.bind.annotation.RequestMapping;
import web.org.springframework.web.bind.annotation.RequestMethod;

public class AnnotationHandlerMapping {
public class AnnotationHandlerMapping implements HandlerMapping {

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

Expand All @@ -28,6 +28,7 @@ public AnnotationHandlerMapping(final Object... basePackages) {
this.handlerExecutions = new HashMap<>();
}

@Override
public void initialize() {
log.info("Initialized AnnotationHandlerMapping!");
makeHandlerExecutions(basePackages);
Expand Down Expand Up @@ -95,6 +96,7 @@ private Map<HandlerKey, HandlerExecution> convertHandlerExecutions(final Object
));
}

@Override
public Object getHandler(final HttpServletRequest request) {
final HandlerKey handlerKey = makeHandlerKey(request);
return handlerExecutions.get(handlerKey);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package webmvc.org.springframework.web.servlet.mvc.tobe;

import jakarta.servlet.http.HttpServletRequest;

public interface HandlerMapping {

Object getHandler(final HttpServletRequest request);

void initialize();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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.asis.Controller;
import webmvc.org.springframework.web.servlet.view.JspView;

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 Controller controller = (Controller) handler;
final String path = controller.execute(request, response);
return new ModelAndView(new JspView(path));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,9 @@ public class JsonView implements View {
@Override
public void render(final Map<String, ?> model, final HttpServletRequest request, HttpServletResponse response) throws Exception {
}

@Override
public String getName() {
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -28,4 +30,9 @@ public void render(final Map<String, ?> model, final HttpServletRequest request,

// todo
}

@Override
public String getName() {
return viewName;
}
}
4 changes: 2 additions & 2 deletions mvc/src/test/java/samples/TestController.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,15 @@ public class TestController {
@RequestMapping(value = "/get-test", method = RequestMethod.GET)
public ModelAndView findUserId(final HttpServletRequest request, final HttpServletResponse response) {
log.info("test controller get method");
final var modelAndView = new ModelAndView(new JspView(""));
final var modelAndView = new ModelAndView(new JspView("test"));
modelAndView.addObject("id", request.getAttribute("id"));
return modelAndView;
}

@RequestMapping(value = "/post-test", method = RequestMethod.POST)
public ModelAndView save(final HttpServletRequest request, final HttpServletResponse response) {
log.info("test controller post method");
final var modelAndView = new ModelAndView(new JspView(""));
final var modelAndView = new ModelAndView(new JspView("test"));
modelAndView.addObject("id", request.getAttribute("id"));
return modelAndView;
}
Expand Down
Loading

0 comments on commit de6c578

Please sign in to comment.