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 구현하기 - 1단계] 푸우(백승준) 미션 제출합니다. #344

Merged
merged 10 commits into from
Sep 14, 2023
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package webmvc.org.springframework.web.servlet.mvc.tobe;

import context.org.springframework.stereotype.Controller;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BinaryOperator;
import org.reflections.Reflections;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import web.org.springframework.web.bind.annotation.RequestMapping;

public class AnnotationHandlerMapping {

Expand All @@ -15,15 +24,83 @@ public class AnnotationHandlerMapping {
private final Map<HandlerKey, HandlerExecution> handlerExecutions;

public AnnotationHandlerMapping(final Object... basePackage) {
validateBasePackage(basePackage);
this.basePackage = basePackage;
this.handlerExecutions = new HashMap<>();
}

private void validateBasePackage(Object[] basePackage) {
for (Object o : basePackage) {
if (!(o instanceof String)) {
throw new IllegalArgumentException("basePackage 는 String 으로 입력해야 합니다");
}
}
}
Comment on lines +32 to +38
Copy link
Member

Choose a reason for hiding this comment

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

꼼꼼한 검증 좋네요! 👍

Copy link
Author

Choose a reason for hiding this comment

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

감사합니다 ㅎㅎ


public void initialize() {
log.info("Initialized AnnotationHandlerMapping!");
handlerExecutions.putAll(extractHandler());
}

private Map<HandlerKey, HandlerExecution> extractHandler() {
Reflections reflections = new Reflections(basePackage);
return reflections.getTypesAnnotatedWith(Controller.class).stream()
.map(this::extractHandlerFromClass)
.reduce(new HashMap<>(), migrateHandler());
}

private Map<HandlerKey, HandlerExecution> extractHandlerFromClass(Class<?> targetClass) {
Object handler = toInstance(targetClass);
return Arrays.stream(targetClass.getMethods())
.filter(method -> method.isAnnotationPresent(RequestMapping.class))
.map(method -> extractHandlerFromMethod(method, handler))
.reduce(new HashMap<>(), migrateHandler());
}

private Object toInstance(Class<?> targetClass) {
try {
return targetClass.getDeclaredConstructor().newInstance();
} catch (NoSuchMethodException | InvocationTargetException | InstantiationException |
IllegalAccessException e) {
throw new IllegalArgumentException(e);
}
}

private Map<HandlerKey, HandlerExecution> extractHandlerFromMethod(Method method, Object handler) {
HandlerExecution handlerExecution = new HandlerExecution(handler, method);
RequestMapping annotation = method.getAnnotation(RequestMapping.class);
return Arrays.stream(annotation.method())
.map(requestMethod -> {
Map<HandlerKey, HandlerExecution> extractedHandlerMapping = new HashMap<>();
extractedHandlerMapping.put(new HandlerKey(annotation.value(), requestMethod), handlerExecution);
return extractedHandlerMapping;
}).reduce(new HashMap<>(), migrateHandler());
}

private BinaryOperator<Map<HandlerKey, HandlerExecution>> migrateHandler() {
return (originHandler, migrateHandler) -> {
checkDuplication(originHandler, migrateHandler);
originHandler.putAll(migrateHandler);
return originHandler;
};
}
Comment on lines +80 to +86
Copy link
Member

Choose a reason for hiding this comment

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

오.. 생소한 방식이네요!
이렇게 Map을 합쳐가는 방식을 고려해볼 수도 있겠군요 👍


private void checkDuplication(Map<HandlerKey, HandlerExecution> originHandlers,
Map<HandlerKey, HandlerExecution> newHandlers) {
Set<HandlerKey> duplicatedHandlerKeys = new HashSet<>(originHandlers.keySet());
duplicatedHandlerKeys.retainAll(newHandlers.keySet());
if (!duplicatedHandlerKeys.isEmpty()) {
HandlerKey duplicatedHandlerKey = duplicatedHandlerKeys.iterator().next();
log.error("duplication handler : {}", duplicatedHandlerKey);
throw new IllegalArgumentException("Duplicated HandlerKey");
}
}

public Object getHandler(final HttpServletRequest request) {
return null;
Optional<HandlerKey> findHandler = handlerExecutions.keySet().stream()
.filter(handlerKey -> handlerKey.canHandle(request))
.findAny();
return findHandler.map(handlerExecutions::get)
.orElseGet(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,21 @@

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import webmvc.org.springframework.web.servlet.ModelAndView;

public class HandlerExecution {

public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception {
return null;
private final Object handler;
private final Method handlerMethod;

public HandlerExecution(Object handler, Method handlerMethod) {
this.handler = handler;
this.handlerMethod = handlerMethod;
}

public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response) throws Exception {
return (ModelAndView) handlerMethod.invoke(handler, request, response);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package webmvc.org.springframework.web.servlet.mvc.tobe;

import jakarta.servlet.http.HttpServletRequest;
import web.org.springframework.web.bind.annotation.RequestMethod;

import java.util.Objects;
Expand All @@ -14,6 +15,11 @@ public HandlerKey(final String url, final RequestMethod requestMethod) {
this.requestMethod = requestMethod;
}

public boolean canHandle(HttpServletRequest httpServletRequest){
return httpServletRequest.getMethod().equals(requestMethod.name()) &&
httpServletRequest.getRequestURI().equals(url);
}
Comment on lines +18 to +21
Copy link
Member

Choose a reason for hiding this comment

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

equals가 재정의되어있는데 해당 메소드를 다시 정의한 이유가 있을까요?

Copy link
Author

Choose a reason for hiding this comment

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

저는 HandlerKey 이 RequestMapping 가 객체화됨으로써 요청으로 전달된 HttpRequest 가 Mapping 되는지 확인하는 역할로 생각했습니다 ㅎㅎ
그래서 든 생각이 @RequestMapping 에 만약에 consumes 과 같은 column이 추가되어서

@RequestMapping(consumes = { 
    "application/json",
    "text/plain"
})

요 상황에서 만약

HttpRequest
~
Content-Type: application/json

이런게 오면 해당 Request 로 만든 HandlerKey 로 equals 연산을 못하겠다고 생각해서
HandlerKey 가 해당 요청을 처리할 수 있는지 판단하게 canHandle 을 만들자! 하고 생각했습니다 ㅎㅎ
지금은 equal 로 정의할 수 있지만 의존을 미리 만들어두면 나중에 처리하기 힘들 것 같아서 똑같은 메서드라도 재정의 해뒀습니다 ㅎㅎ

Copy link
Member

Choose a reason for hiding this comment

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

오 contentType까지 고려한 로직이였군요!
확장성을 고려한 설계라면 동의합니다 👍


@Override
public String toString() {
return "HandlerKey{" +
Expand Down
22 changes: 22 additions & 0 deletions mvc/src/test/java/duplicate/case1/TestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package duplicate.case1;

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;

@Controller
public class TestController {

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

@RequestMapping(value = "/get-test", method = RequestMethod.GET)
public ModelAndView duplicatedMethod(final HttpServletRequest request,
final HttpServletResponse response) {
return null;
}
}
28 changes: 28 additions & 0 deletions mvc/src/test/java/duplicate/case2/TestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package duplicate.case2;

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;

@Controller
public class TestController {

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

@RequestMapping(value = "/get-test", method = RequestMethod.GET)
public ModelAndView duplicatedMethod1(final HttpServletRequest request,
final HttpServletResponse response) {
return null;
}

@RequestMapping(value = "/get-test", method = RequestMethod.GET)
public ModelAndView duplicatedMethod2(final HttpServletRequest request,
final HttpServletResponse response) {
return null;
}
}
28 changes: 28 additions & 0 deletions mvc/src/test/java/duplicate/case3/TestController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package duplicate.case3;

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;

@Controller
public class TestController {

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

@RequestMapping(value = "/get-test", method = {
RequestMethod.GET,
RequestMethod.GET,
RequestMethod.GET,
RequestMethod.GET,
RequestMethod.GET
})
public ModelAndView duplicatedMethod1(final HttpServletRequest request,
final HttpServletResponse response) {
return null;
}
}
17 changes: 17 additions & 0 deletions mvc/src/test/java/samples/TestController.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,21 @@ public ModelAndView save(final HttpServletRequest request, final HttpServletResp
modelAndView.addObject("id", request.getAttribute("id"));
return modelAndView;
}

@RequestMapping(value = "/multi-method-test", method = {RequestMethod.GET, RequestMethod.POST} )
public ModelAndView multiHandle(final HttpServletRequest request, final HttpServletResponse response) {
log.info("test controller multi-handle method");
String method = request.getMethod();
if("GET".equals(method)){
final var modelAndView = new ModelAndView(new JspView(""));
modelAndView.addObject("id", "getPooh");
return modelAndView;
}
if("POST".equals(method)){
final var modelAndView = new ModelAndView(new JspView(""));
modelAndView.addObject("id", "postPooh");
return modelAndView;
}
throw new IllegalArgumentException("해당 요청을 Handling 할 수 없는 핸들러입니다.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.junit.jupiter.api.Test;

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

Expand All @@ -19,6 +20,57 @@ void setUp() {
handlerMapping.initialize();
}

@Test
void methodDuplicationException_existMappingInOtherClass_case1() {
// given & when & then
AnnotationHandlerMapping duplicatedHandlerMapping = new AnnotationHandlerMapping(
"samples",
"duplicate.case1");
assertThatThrownBy(() -> duplicatedHandlerMapping.initialize())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Duplicated HandlerKey");
}

@Test
void methodDuplicationException_existMappingIntSameClass_case2() {
// given & when & then
AnnotationHandlerMapping duplicatedHandlerMapping = new AnnotationHandlerMapping(
"duplicate.case2");
assertThatThrownBy(() -> duplicatedHandlerMapping.initialize())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Duplicated HandlerKey");
}

@Test
void methodDuplicationException_duplicateMappingInOneMethod_case3() {
// given & when & then
AnnotationHandlerMapping duplicatedHandlerMapping = new AnnotationHandlerMapping(
"duplicate.case3");
assertThatThrownBy(() -> duplicatedHandlerMapping.initialize())
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Duplicated HandlerKey");
}

@Test
void oneMethodCanCreateManyHttpRequestMappings() throws Exception {
final var request = mock(HttpServletRequest.class);
final var response = mock(HttpServletResponse.class);

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

final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request);
final var getModelAndView = handlerExecution.handle(request, response);

when(request.getMethod()).thenReturn("POST");

final var postModelAndView = handlerExecution.handle(request, response);

assertThat(getModelAndView.getObject("id")).isEqualTo("getPooh");
assertThat(postModelAndView.getObject("id")).isEqualTo("postPooh");
}

@Test
void get() throws Exception {
final var request = mock(HttpServletRequest.class);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@
@WebFilter("/*")
public class CharacterEncodingFilter implements Filter {

public static final String DEFAULT_ENCODING = "UTF-8";

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
request.getServletContext().log("doFilter() 호출");
request.setCharacterEncoding(DEFAULT_ENCODING);
response.setCharacterEncoding(DEFAULT_ENCODING);
chain.doFilter(request, response);
}
}
14 changes: 13 additions & 1 deletion study/src/test/java/reflection/Junit3TestRunner.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,25 @@
package reflection;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;

class Junit3TestRunner {

@Test
void run() throws Exception {
Class<Junit3Test> clazz = Junit3Test.class;
List<Method> methods = Arrays.stream(clazz.getMethods())
.filter(method -> method.getName().startsWith("test"))
.collect(Collectors.toList());

// TODO Junit3Test에서 test로 시작하는 메소드 실행
Junit3Test junit3Test = clazz.getConstructor()
.newInstance();

for (Method method : methods) {
method.invoke(junit3Test);
}
}
}
20 changes: 19 additions & 1 deletion study/src/test/java/reflection/Junit4TestRunner.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
package reflection;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import org.junit.jupiter.api.Test;

class Junit4TestRunner {

@Test
void run() throws Exception {
Class<Junit4Test> clazz = Junit4Test.class;
List<Method> methods = Arrays.stream(clazz.getMethods())
.filter(method -> haveTestAnnotation(method))
.collect(Collectors.toList());

// TODO Junit4Test에서 @MyTest 애노테이션이 있는 메소드 실행
Junit4Test junit4Test = clazz.getConstructor()
.newInstance();

for (Method method : methods) {
method.invoke(junit4Test);
}
}

private boolean haveTestAnnotation(Method method) {
return Arrays.stream(method.getDeclaredAnnotations())
.anyMatch(annotation -> annotation instanceof MyTest);
}
}
Loading
Loading