Skip to content

Commit

Permalink
[MVC 구현하기 - 1단계] 푸우(백승준) 미션 제출합니다. (#344)
Browse files Browse the repository at this point in the history
* chore: reflection 학습 테스트 작성

* chore: Servlet 학습 테스트 작성

* feat: 핸들러 매핑 구현

* test: 테스트 케이스 추가

* feat: HandlerExecution 재정의

* refactor: AnnotationHandlerMapping 메서드 리팩터링

* test: 테스트 케이스 추가

* refactor: AnnotationHandlerMapping 메서드 리팩터링

* refactor: Method.isAnnotationPresent() 사용

* refactor: 메서드 네이밍 변경
  • Loading branch information
BGuga authored Sep 14, 2023
1 parent e7c21ab commit 8097ab6
Show file tree
Hide file tree
Showing 14 changed files with 369 additions and 36 deletions.
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 으로 입력해야 합니다");
}
}
}

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;
};
}

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

@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

0 comments on commit 8097ab6

Please sign in to comment.