Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
package com.interface21.webmvc.servlet.mvc.tobe;

import com.interface21.context.stereotype.Controller;
import com.interface21.web.bind.annotation.RequestMapping;
import com.interface21.web.bind.annotation.RequestMethod;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class AnnotationHandlerMapping {

private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class);
private static final String DELIMITER_PATH = "/";
private static final String REGEX_MANY_PATH_DELIMITER = "/{2,}";
private static final String DEFAULT_PATH = "";

private final Object[] basePackage;
private final Map<HandlerKey, HandlerExecution> handlerExecutions;
Expand All @@ -21,9 +29,71 @@ public AnnotationHandlerMapping(final Object... basePackage) {

public void initialize() {
log.info("Initialized AnnotationHandlerMapping!");
Reflections reflections = new Reflections(basePackage);
reflections.getTypesAnnotatedWith(Controller.class).forEach(this::assignHandlerByClass);
}

public Object getHandler(final HttpServletRequest request) {
private void assignHandlerByClass(Class<?> clazz) {
RequestMapping annotation = clazz.getAnnotation(RequestMapping.class);
String path = extractPath(annotation);

for (Method method : clazz.getMethods()) {
assignHandlerByMethod(clazz, method, path);
}
}

private String extractPath(RequestMapping annotation) {
return Optional.ofNullable(annotation)
.map(RequestMapping::value)
.orElse(DEFAULT_PATH);
}

private void assignHandlerByMethod(Class<?> clazz, Method method, String basePath) {
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
if (annotation == null) {
return;
}
String path = generateEndpoint(basePath, extractPath(annotation));

for (RequestMethod requestMethod : annotation.method()) {
HandlerKey handlerKey = new HandlerKey(path, requestMethod);
validateDuplicated(handlerKey);
HandlerExecution handlerExecution = findHandlerExecution(clazz, method);
handlerExecutions.put(handlerKey, handlerExecution);
}
}

private String generateEndpoint(String basePath, String subPath) {
return String.join(DELIMITER_PATH, DELIMITER_PATH, basePath, subPath)
.replaceAll(REGEX_MANY_PATH_DELIMITER, DELIMITER_PATH);
}

private void validateDuplicated(HandlerKey handlerKey) {
if (handlerExecutions.containsKey(handlerKey)) {
throw new IllegalArgumentException("HandlerKey exists: " + handlerKey.toString());
}
}

private HandlerExecution findHandlerExecution(Class<?> clazz, Method method) {
try {
return new HandlerExecution(method, clazz);
} catch (NoSuchMethodException e) {
throw new IllegalArgumentException("Public constructor not found: {}" + clazz);
} catch (Exception e) {
log.error("Failed to find HandlerExecution for class {}", clazz, e);
}
return null;
}

public Object getHandler(final HttpServletRequest request) {
RequestMethod requestMethod;
try {
requestMethod = RequestMethod.valueOf(request.getMethod());
} catch (IllegalArgumentException e) {
return null;
}

HandlerKey handlerKey = new HandlerKey(request.getRequestURI(), requestMethod);
return handlerExecutions.get(handlerKey);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,45 @@
package com.interface21.webmvc.servlet.mvc.tobe;

import com.interface21.webmvc.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import com.interface21.webmvc.servlet.ModelAndView;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;

public class HandlerExecution {

private final Method method;
private final Object controllerInstance;

public HandlerExecution(Method method, Class<?> controllerClass)
throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
this.method = method;
this.controllerInstance = controllerClass.getConstructor().newInstance();
Copy link

Choose a reason for hiding this comment

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

HandlerExecution이 reflection으로 컨트롤러 인스턴스를 만드는 책임을 가지는게 적절할까요? 새양의 의견이 궁금합니다 👀

Copy link
Member Author

@geoje geoje Sep 21, 2024

Choose a reason for hiding this comment

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

적절하지 않습니다! 컴포넌트 스캐닝을 하여 @Controller 어노테이션을 붙은 녀석들을 인스턴스로 생성하여 어떠한 공간속에 저장해둔 뒤 이 HandlerExecution 은 단순히 requestresponse 등으로 핸들러를 실행시켜주어야 한다고 생각합니다!

하지만 이 미션은 스프링을 구현하는게 아니다 보니 그런 객체 관리까지 신경쓰지 않았습니다. 😅

}

public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
return null;
List<Object> preparedArgs = Stream.of(request, response)
.filter(Objects::nonNull)
.toList();
List<? extends Class<?>> requiredParameterClasses = Stream.of(method.getParameters())
.map(Parameter::getType)
.toList();

List<Object> passingArgs = requiredParameterClasses.stream()
.map(param -> decideArgumentByParameter(param, preparedArgs))
.toList();

return (ModelAndView) method.invoke(controllerInstance, passingArgs.toArray());
}

private Object decideArgumentByParameter(Class<?> param, List<Object> preparedArgs) {
return preparedArgs.stream()
.filter(arg -> param.isAssignableFrom(arg.getClass()))
.findAny()
.orElse(null);
}
}
20 changes: 13 additions & 7 deletions mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,35 @@
import com.interface21.webmvc.servlet.View;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Map;

public class JspView implements View {

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

public static final String REDIRECT_PREFIX = "redirect:";

private static final Logger log = LoggerFactory.getLogger(JspView.class);
private final String viewName;

public JspView(final String viewName) {
this.viewName = viewName;
}

@Override
public void render(final Map<String, ?> model, final HttpServletRequest request, final HttpServletResponse response) throws Exception {
// todo
public void render(final Map<String, ?> model, final HttpServletRequest request, final HttpServletResponse response)
throws Exception {
if (viewName.startsWith(REDIRECT_PREFIX)) {
response.sendRedirect(viewName.substring(REDIRECT_PREFIX.length()));
return;
}

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

// todo
request.getRequestDispatcher(viewName)
.forward(request, response);
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package com.interface21.webmvc.servlet.mvc.tobe;

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

import com.interface21.webmvc.servlet.ModelAndView;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

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

class AnnotationHandlerMappingTest {

private AnnotationHandlerMapping handlerMapping;
Expand All @@ -20,32 +23,118 @@ void setUp() {
}

@Test
@DisplayName("핸들러를 통해 GET 요청을 하고 모델에 속성 id 를 조회한다.")
void get() throws Exception {
final var request = mock(HttpServletRequest.class);
final var response = mock(HttpServletResponse.class);
// given
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);

when(request.getAttribute("id")).thenReturn("gugu");
when(request.getRequestURI()).thenReturn("/get-test");
when(request.getMethod()).thenReturn("GET");

final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request);
final var modelAndView = handlerExecution.handle(request, response);
// when
HandlerExecution handlerExecution = (HandlerExecution) handlerMapping.getHandler(request);
ModelAndView modelAndView = handlerExecution.handle(request, response);

// then
assertThat(modelAndView.getObject("id")).isEqualTo("gugu");
}

@Test
@DisplayName("핸들러를 통해 POST 요청을 하고 모델에 속성 id 를 조회한다.")
void post() throws Exception {
final var request = mock(HttpServletRequest.class);
final var response = mock(HttpServletResponse.class);
// given
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);

when(request.getAttribute("id")).thenReturn("gugu");
when(request.getRequestURI()).thenReturn("/post-test");
when(request.getMethod()).thenReturn("POST");

final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request);
final var modelAndView = handlerExecution.handle(request, response);
// when
HandlerExecution handlerExecution = (HandlerExecution) handlerMapping.getHandler(request);
ModelAndView modelAndView = handlerExecution.handle(request, response);

// then
assertThat(modelAndView.getObject("id")).isEqualTo("gugu");
}

@Test
@DisplayName("동일한 요청을 처리하는 2개의 핸들러 등록 시 예외가 발생한다.")
void assignHandlerDuplicated() {
// given
String basePackage = "com.interface21.webmvc.servlet.samples.duplicated";
AnnotationHandlerMapping mapping = new AnnotationHandlerMapping(basePackage);

// when & then
assertThatCode(mapping::initialize)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("HandlerKey exists");
}

@Test
@DisplayName("생성자가 가려진 컨트롤러 등록 시 예외가 발생한다.")
void assignHandlerPrivate() {
// given
String basePackage = "com.interface21.webmvc.servlet.samples.hided";
AnnotationHandlerMapping mapping = new AnnotationHandlerMapping(basePackage);

// when & then
assertThatCode(mapping::initialize)
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("Public constructor not found");
}

@Test
@DisplayName("정의되지 않은 HTTP 메서드로 요청 시 핸들러를 찾을 수 없다.")
void requestWithInvalidMethod() throws Exception {
// given
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);

when(request.getRequestURI()).thenReturn("/get-test");
when(request.getMethod()).thenReturn("CONNECT");

// when
Object handlerExecution = handlerMapping.getHandler(request);

// then
assertThat(handlerExecution).isNull();
}

@Test
@DisplayName("등록되지 않은 엔드포인트로 요청 시 핸들러를 찾을 수 없다.")
void requestWithUnassignedEndpoint() throws Exception {
// given
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);

when(request.getRequestURI()).thenReturn("/unknown-endpoint");
when(request.getMethod()).thenReturn("GET");

// when
Object handlerExecution = handlerMapping.getHandler(request);

// then
assertThat(handlerExecution).isNull();
}

@Test
@DisplayName("컨트롤러와 메서드의 경로를 합쳐서 엔드포인트로 요청 경로를 결정한다.")
void joinPathOfControllerAndMethod() throws Exception {
// given
HttpServletRequest request = mock(HttpServletRequest.class);
HttpServletResponse response = mock(HttpServletResponse.class);

when(request.getRequestURI()).thenReturn("/api/join-paths");
when(request.getMethod()).thenReturn("GET");

// when
HandlerExecution handlerExecution = (HandlerExecution) handlerMapping.getHandler(request);
ModelAndView modelAndView = handlerExecution.handle(request, response);

// then
assertThat(modelAndView.getObject("message")).isEqualTo("Paths joined");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.interface21.webmvc.servlet.mvc.tobe;

import static org.assertj.core.api.Assertions.assertThatCode;

import java.lang.reflect.Method;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import samples.NoArgsController;

class HandlerExecutionTest {

@Test
@DisplayName("컨트롤러의 메서드가 요구하는 파라미터를 동적으로 대응하여 의존성을 주입한다.")
void passArgumentsWithRequiredParameters() throws Exception {
// given
Method method = NoArgsController.class.getMethod("noArgsMethod");
HandlerExecution handlerExecution = new HandlerExecution(method, NoArgsController.class);

// when & then
assertThatCode(() -> handlerExecution.handle(null, null))
.doesNotThrowAnyException();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.interface21.webmvc.servlet.mvc.tobe;

import static org.assertj.core.api.Assertions.assertThat;

import com.interface21.web.bind.annotation.RequestMethod;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

class HandlerKeyTest {

@Test
@DisplayName("동등성에 대해 검증한다.")
void methodName() {
// given
HandlerKey key1 = new HandlerKey("/api/test", RequestMethod.GET);
HandlerKey key2 = new HandlerKey("/api/test", RequestMethod.GET);

// when & then
assertThat(key1).isEqualTo(key2);
}
}
Loading