From d7388ee126374515346dea8139e85f853588f7ad Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Mon, 16 Sep 2024 18:29:25 +0900 Subject: [PATCH 01/17] test: do basic reflection tests junit 3 and 4 --- .../src/test/java/reflection/Junit3TestRunner.java | 9 +++++++++ .../src/test/java/reflection/Junit4TestRunner.java | 13 +++++++++++++ 2 files changed, 22 insertions(+) diff --git a/study/src/test/java/reflection/Junit3TestRunner.java b/study/src/test/java/reflection/Junit3TestRunner.java index b4e465240c..83b3234372 100644 --- a/study/src/test/java/reflection/Junit3TestRunner.java +++ b/study/src/test/java/reflection/Junit3TestRunner.java @@ -1,5 +1,6 @@ package reflection; +import java.lang.reflect.Method; import org.junit.jupiter.api.Test; class Junit3TestRunner { @@ -9,5 +10,13 @@ void run() throws Exception { Class clazz = Junit3Test.class; // TODO Junit3Test에서 test로 시작하는 메소드 실행 + Method[] methods = clazz.getDeclaredMethods(); + Junit3Test instance = clazz.getDeclaredConstructor().newInstance(); + String prefix = "test"; + for (Method method : methods) { + if (method.getName().startsWith(prefix)) { + method.invoke(instance); + } + } } } diff --git a/study/src/test/java/reflection/Junit4TestRunner.java b/study/src/test/java/reflection/Junit4TestRunner.java index 8a6916bc24..55b8e4b3fc 100644 --- a/study/src/test/java/reflection/Junit4TestRunner.java +++ b/study/src/test/java/reflection/Junit4TestRunner.java @@ -1,5 +1,7 @@ package reflection; +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; import org.junit.jupiter.api.Test; class Junit4TestRunner { @@ -9,5 +11,16 @@ void run() throws Exception { Class clazz = Junit4Test.class; // TODO Junit4Test에서 @MyTest 애노테이션이 있는 메소드 실행 + Method[] methods = clazz.getDeclaredMethods(); + Junit4Test instance = clazz.getDeclaredConstructor().newInstance(); + for (Method method : methods) { + Annotation[] annotations = method.getDeclaredAnnotations(); + for (Annotation annotation : annotations) { + if (annotation.annotationType() == MyTest.class) { + method.invoke(instance); + break; + } + } + } } } From 5802ba775839583fdfa70898e0567e33a57d6414 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Mon, 16 Sep 2024 19:37:39 +0900 Subject: [PATCH 02/17] test: do ReflectionTest --- .../test/java/reflection/ReflectionTest.java | 59 ++++++++++--------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/study/src/test/java/reflection/ReflectionTest.java b/study/src/test/java/reflection/ReflectionTest.java index 370f0932b9..0a5c6eee2c 100644 --- a/study/src/test/java/reflection/ReflectionTest.java +++ b/study/src/test/java/reflection/ReflectionTest.java @@ -1,15 +1,16 @@ package reflection; -import org.junit.jupiter.api.Test; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import static org.assertj.core.api.Assertions.assertThat; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Date; import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class ReflectionTest { @@ -19,25 +20,27 @@ class ReflectionTest { void givenObject_whenGetsClassName_thenCorrect() { final Class clazz = Question.class; - assertThat(clazz.getSimpleName()).isEqualTo(""); - assertThat(clazz.getName()).isEqualTo(""); - assertThat(clazz.getCanonicalName()).isEqualTo(""); + assertThat(clazz.getSimpleName()).isEqualTo("Question"); + assertThat(clazz.getName()).isEqualTo("reflection.Question"); + assertThat(clazz.getCanonicalName()).isEqualTo("reflection.Question"); } @Test void givenClassName_whenCreatesObject_thenCorrect() throws ClassNotFoundException { final Class clazz = Class.forName("reflection.Question"); - assertThat(clazz.getSimpleName()).isEqualTo(""); - assertThat(clazz.getName()).isEqualTo(""); - assertThat(clazz.getCanonicalName()).isEqualTo(""); + assertThat(clazz.getSimpleName()).isEqualTo("Question"); + assertThat(clazz.getName()).isEqualTo("reflection.Question"); + assertThat(clazz.getCanonicalName()).isEqualTo("reflection.Question"); } @Test void givenObject_whenGetsFieldNamesAtRuntime_thenCorrect() { final Object student = new Student(); - final Field[] fields = null; - final List actualFieldNames = null; + final Field[] fields = student.getClass().getDeclaredFields(); + final List actualFieldNames = Arrays.stream(fields) + .map(Field::getName) + .toList(); assertThat(actualFieldNames).contains("name", "age"); } @@ -45,8 +48,8 @@ void givenObject_whenGetsFieldNamesAtRuntime_thenCorrect() { @Test void givenClass_whenGetsMethods_thenCorrect() { final Class animalClass = Student.class; - final Method[] methods = null; - final List actualMethods = null; + final Method[] methods = animalClass.getDeclaredMethods(); + final List actualMethods = Arrays.stream(methods).map(Method::getName).toList(); assertThat(actualMethods) .hasSize(3) @@ -56,7 +59,7 @@ void givenClass_whenGetsMethods_thenCorrect() { @Test void givenClass_whenGetsAllConstructors_thenCorrect() { final Class questionClass = Question.class; - final Constructor[] constructors = null; + final Constructor[] constructors = questionClass.getDeclaredConstructors(); assertThat(constructors).hasSize(2); } @@ -65,11 +68,12 @@ void givenClass_whenGetsAllConstructors_thenCorrect() { void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception { final Class questionClass = Question.class; - final Constructor firstConstructor = null; - final Constructor secondConstructor = null; + final Constructor firstConstructor = questionClass.getDeclaredConstructors()[0]; + final Constructor secondConstructor = questionClass.getDeclaredConstructors()[1]; - final Question firstQuestion = null; - final Question secondQuestion = null; + final Question firstQuestion = (Question) firstConstructor.newInstance("gugu", "제목1", "내용1"); + final Question secondQuestion = (Question) secondConstructor.newInstance(1, "gugu", "제목2", "내용2", new Date(), + 0); assertThat(firstQuestion.getWriter()).isEqualTo("gugu"); assertThat(firstQuestion.getTitle()).isEqualTo("제목1"); @@ -82,7 +86,7 @@ void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception @Test void givenClass_whenGetsPublicFields_thenCorrect() { final Class questionClass = Question.class; - final Field[] fields = null; + final Field[] fields = questionClass.getFields(); assertThat(fields).hasSize(0); } @@ -90,7 +94,7 @@ void givenClass_whenGetsPublicFields_thenCorrect() { @Test void givenClass_whenGetsDeclaredFields_thenCorrect() { final Class questionClass = Question.class; - final Field[] fields = null; + final Field[] fields = questionClass.getDeclaredFields(); assertThat(fields).hasSize(6); assertThat(fields[0].getName()).isEqualTo("questionId"); @@ -99,7 +103,7 @@ void givenClass_whenGetsDeclaredFields_thenCorrect() { @Test void givenClass_whenGetsFieldsByName_thenCorrect() throws Exception { final Class questionClass = Question.class; - final Field field = null; + final Field field = questionClass.getDeclaredField("questionId"); assertThat(field.getName()).isEqualTo("questionId"); } @@ -107,7 +111,7 @@ void givenClass_whenGetsFieldsByName_thenCorrect() throws Exception { @Test void givenClassField_whenGetsType_thenCorrect() throws Exception { final Field field = Question.class.getDeclaredField("questionId"); - final Class fieldClass = null; + final Class fieldClass = field.getType(); assertThat(fieldClass.getSimpleName()).isEqualTo("long"); } @@ -115,15 +119,16 @@ void givenClassField_whenGetsType_thenCorrect() throws Exception { @Test void givenClassField_whenSetsAndGetsValue_thenCorrect() throws Exception { final Class studentClass = Student.class; - final Student student = null; - final Field field = null; + final Student student = (Student) studentClass.getDeclaredConstructor().newInstance(); + final Field field = studentClass.getDeclaredField("age"); // todo field에 접근 할 수 있도록 만든다. + field.setAccessible(true); assertThat(field.getInt(student)).isZero(); assertThat(student.getAge()).isZero(); - field.set(null, null); + field.set(student, 99); assertThat(field.getInt(student)).isEqualTo(99); assertThat(student.getAge()).isEqualTo(99); From 3575a4c164f97eec2dfc0019b8e2a969d1695b4a Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 12:33:13 +0900 Subject: [PATCH 03/17] test: solve logging all mvc classes by reflections --- study/src/test/java/reflection/ReflectionsTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/study/src/test/java/reflection/ReflectionsTest.java b/study/src/test/java/reflection/ReflectionsTest.java index 5040c2ffa2..b276b6efae 100644 --- a/study/src/test/java/reflection/ReflectionsTest.java +++ b/study/src/test/java/reflection/ReflectionsTest.java @@ -1,9 +1,13 @@ package reflection; +import java.util.Set; import org.junit.jupiter.api.Test; import org.reflections.Reflections; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reflection.annotation.Controller; +import reflection.annotation.Repository; +import reflection.annotation.Service; class ReflectionsTest { @@ -14,5 +18,13 @@ void showAnnotationClass() throws Exception { Reflections reflections = new Reflections("reflection.examples"); // TODO 클래스 레벨에 @Controller, @Service, @Repository 애노테이션이 설정되어 모든 클래스 찾아 로그로 출력한다. + Set> controllers = reflections.getTypesAnnotatedWith(Controller.class); + log.info(controllers.toString()); + + Set> services = reflections.getTypesAnnotatedWith(Service.class); + log.info(services.toString()); + + Set> repositories = reflections.getTypesAnnotatedWith(Repository.class); + log.info(repositories.toString()); } } From 98bb5187aa4655bb7ffcc091e1b0a543294dcb08 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 15:34:09 +0900 Subject: [PATCH 04/17] feat: inject handler execution with reflection --- .../mvc/tobe/AnnotationHandlerMapping.java | 63 +++++++++++++++++-- .../servlet/mvc/tobe/HandlerExecution.java | 15 ++++- .../tobe/AnnotationHandlerMappingTest.java | 8 +-- 3 files changed, 76 insertions(+), 10 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index ccae5ea3ad..fcce1d7fcd 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -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 handlerExecutions; @@ -21,9 +29,56 @@ public AnnotationHandlerMapping(final Object... basePackage) { public void initialize() { log.info("Initialized AnnotationHandlerMapping!"); + Reflections reflections = new Reflections(basePackage); + reflections.getTypesAnnotatedWith(Controller.class).forEach(this::assignHandlerByClass); + } + + private void assignHandlerByClass(Class clazz) { + RequestMapping annotation = clazz.getAnnotation(RequestMapping.class); + String path = Optional.ofNullable(annotation) + .map(RequestMapping::value) + .orElse(DEFAULT_PATH); + + for (Method method : clazz.getMethods()) { + assignHandlerByMethod(clazz, method, path); + } + } + + private void assignHandlerByMethod(Class clazz, Method method, String basePath) { + RequestMapping annotation = method.getAnnotation(RequestMapping.class); + if (annotation == null) { + return; + } + String subPath = Optional.of(annotation.value()).orElse(DEFAULT_PATH); + String path = String.join(DELIMITER_PATH, DELIMITER_PATH, basePath, subPath) + .replaceAll(REGEX_MANY_PATH_DELIMITER, DELIMITER_PATH); + + for (RequestMethod requestMethod : annotation.method()) { + HandlerKey handlerKey = new HandlerKey(path, requestMethod); + HandlerExecution handlerExecution = findHandlerExecution(clazz, method); + handlerExecutions.put(handlerKey, handlerExecution); + } + } + + private HandlerExecution findHandlerExecution(Class clazz, Method method) { + try { + return new HandlerExecution(method, clazz); + } catch (Exception e) { + log.error("Failed to find HandlerExecution for class {}", clazz, e); + return null; + } } public Object getHandler(final HttpServletRequest request) { - return null; + RequestMethod requestMethod; + try { + requestMethod = RequestMethod.valueOf(request.getMethod()); + } catch (IllegalArgumentException e) { + log.error("Failed to get HandlerExecution"); + return null; + } + + HandlerKey handlerKey = new HandlerKey(request.getRequestURI(), requestMethod); + return handlerExecutions.get(handlerKey); } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java index 79a22355a9..9824ee93ae 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java @@ -1,12 +1,23 @@ 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; 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.getDeclaredConstructor().newInstance(); + } + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - return null; + return (ModelAndView) method.invoke(controllerInstance, request, response); } } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index a19ea6e102..dfa10cb76f 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -1,14 +1,14 @@ package com.interface21.webmvc.servlet.mvc.tobe; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.junit.jupiter.api.BeforeEach; 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; From aaf05c81c9f5b0f35a91e6f743b2ddf464b635ed Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 16:29:50 +0900 Subject: [PATCH 05/17] feat: validate handler key duplication --- .../webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index fcce1d7fcd..ddc0df7501 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -55,11 +55,18 @@ private void assignHandlerByMethod(Class clazz, Method method, String basePat for (RequestMethod requestMethod : annotation.method()) { HandlerKey handlerKey = new HandlerKey(path, requestMethod); + validateDuplicated(handlerKey); HandlerExecution handlerExecution = findHandlerExecution(clazz, method); handlerExecutions.put(handlerKey, handlerExecution); } } + 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); From 1e41df4028144e6c4fc95280d970213632d5a5f7 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 16:36:00 +0900 Subject: [PATCH 06/17] test: validate duplication for handler key --- .../tobe/AnnotationHandlerMappingTest.java | 39 +++++++++++++++---- .../servlet/mvc/tobe/HandlerKeyTest.java | 21 ++++++++++ .../servlet/samples/DuplicateController.java | 17 ++++++++ 3 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java create mode 100644 mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicateController.java diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index dfa10cb76f..2be3aa73b3 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -1,12 +1,15 @@ 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; class AnnotationHandlerMappingTest { @@ -20,32 +23,52 @@ 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 + AnnotationHandlerMapping mapping = new AnnotationHandlerMapping("com.interface21.webmvc.servlet.samples"); + + // when & then + assertThatCode(mapping::initialize) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("HandlerKey exists"); + } } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java new file mode 100644 index 0000000000..8f226b747d --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java @@ -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); + } +} diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicateController.java b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicateController.java new file mode 100644 index 0000000000..1f8e581ee9 --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicateController.java @@ -0,0 +1,17 @@ +package com.interface21.webmvc.servlet.samples; + +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; + +@Controller +public class DuplicateController { + + @RequestMapping(value = "/api/test", method = RequestMethod.GET) + public void test1() { + } + + @RequestMapping(value = "/api/test", method = RequestMethod.GET) + public void test2() { + } +} From 430a6ecd72c6d7a40ba5e364301d2b9c56f681f4 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 16:38:52 +0900 Subject: [PATCH 07/17] test: rename for specifying --- ...uplicateController.java => DuplicatedRequestController.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename mvc/src/test/java/com/interface21/webmvc/servlet/samples/{DuplicateController.java => DuplicatedRequestController.java} (91%) diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicateController.java b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java similarity index 91% rename from mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicateController.java rename to mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java index 1f8e581ee9..ed4ccab1a7 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicateController.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java @@ -5,7 +5,7 @@ import com.interface21.web.bind.annotation.RequestMethod; @Controller -public class DuplicateController { +public class DuplicatedRequestController { @RequestMapping(value = "/api/test", method = RequestMethod.GET) public void test1() { From 1a980a29d9b722e6daf2cc7ee0d4786a45a67458 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 16:48:14 +0900 Subject: [PATCH 08/17] refactor: extract method for readability --- .../mvc/tobe/AnnotationHandlerMapping.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index ddc0df7501..8e144637e0 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -35,23 +35,25 @@ public void initialize() { private void assignHandlerByClass(Class clazz) { RequestMapping annotation = clazz.getAnnotation(RequestMapping.class); - String path = Optional.ofNullable(annotation) - .map(RequestMapping::value) - .orElse(DEFAULT_PATH); + 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 subPath = Optional.of(annotation.value()).orElse(DEFAULT_PATH); - String path = String.join(DELIMITER_PATH, DELIMITER_PATH, basePath, subPath) - .replaceAll(REGEX_MANY_PATH_DELIMITER, DELIMITER_PATH); + String path = generateEndpoint(basePath, extractPath(annotation)); for (RequestMethod requestMethod : annotation.method()) { HandlerKey handlerKey = new HandlerKey(path, requestMethod); @@ -61,6 +63,11 @@ private void assignHandlerByMethod(Class clazz, Method method, String basePat } } + 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()); From c435c7b277597ec27b99d0b6d3ae81a9519d14c3 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 17:42:06 +0900 Subject: [PATCH 09/17] feat: fit for dynamic arguments --- .../servlet/mvc/tobe/HandlerExecution.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java index 9824ee93ae..e10f5d1485 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java @@ -5,6 +5,9 @@ import jakarta.servlet.http.HttpServletResponse; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.List; +import java.util.stream.Stream; public class HandlerExecution { @@ -18,6 +21,18 @@ public HandlerExecution(Method method, Class controllerClass) } public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - return (ModelAndView) method.invoke(controllerInstance, request, response); + List preparedArgs = List.of(request, response); + List> requiredParameterClasses = Stream.of(method.getParameters()) + .map(Parameter::getType) + .toList(); + + List passingArgs = requiredParameterClasses.stream() + .map(param -> preparedArgs.stream() + .filter(arg -> param.isAssignableFrom(arg.getClass())) + .findAny() + .orElse(null)) + .toList(); + + return (ModelAndView) method.invoke(controllerInstance, passingArgs.toArray()); } } From 0649d478dacf2453ed40920f968c355c6373d5c6 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 17:42:42 +0900 Subject: [PATCH 10/17] test: join paths of controller and method --- .../tobe/AnnotationHandlerMappingTest.java | 18 ++++++++++++++++++ .../java/samples/JoiningPathsController.java | 19 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 mvc/src/test/java/samples/JoiningPathsController.java diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index 2be3aa73b3..d72dd55a8f 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -71,4 +71,22 @@ void assignHandlerDuplicated() { .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("HandlerKey exists"); } + + @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"); + } } diff --git a/mvc/src/test/java/samples/JoiningPathsController.java b/mvc/src/test/java/samples/JoiningPathsController.java new file mode 100644 index 0000000000..e7dd340319 --- /dev/null +++ b/mvc/src/test/java/samples/JoiningPathsController.java @@ -0,0 +1,19 @@ +package samples; + +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; + +@Controller +@RequestMapping("/api") +public class JoiningPathsController { + + @RequestMapping(value = "/join-paths", method = RequestMethod.GET) + public ModelAndView expectJoiningPaths() { + ModelAndView modelAndView = new ModelAndView((model, request, response) -> { + }); + modelAndView.addObject("message", "Paths joined"); + return modelAndView; + } +} From 6979728c8bdc6d167b33579092c359ebeec99109 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 17:46:14 +0900 Subject: [PATCH 11/17] test: pass argument with required parameters --- .../servlet/mvc/tobe/AnnotationHandlerMappingTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index d72dd55a8f..7fed25dc26 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -72,6 +72,12 @@ void assignHandlerDuplicated() { .hasMessageContaining("HandlerKey exists"); } + @Test + @DisplayName("컨트롤러의 메서드가 요구하는 파라미터를 동적으로 대응하여 의존성을 주입한다.") + void passArgumentsWithRequiredParameters() throws Exception { + joinPathOfControllerAndMethod(); + } + @Test @DisplayName("컨트롤러와 메서드의 경로를 합쳐서 엔드포인트로 요청 경로를 결정한다.") void joinPathOfControllerAndMethod() throws Exception { From 827a98bace968807daef39c63bed5ebfa212c7dd Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 21:03:18 +0900 Subject: [PATCH 12/17] refactor: extract deciding argument method --- .../webmvc/servlet/mvc/tobe/HandlerExecution.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java index e10f5d1485..8b2859dc17 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java @@ -27,12 +27,16 @@ public ModelAndView handle(final HttpServletRequest request, final HttpServletRe .toList(); List passingArgs = requiredParameterClasses.stream() - .map(param -> preparedArgs.stream() - .filter(arg -> param.isAssignableFrom(arg.getClass())) - .findAny() - .orElse(null)) + .map(param -> decideArgumentByParameter(param, preparedArgs)) .toList(); return (ModelAndView) method.invoke(controllerInstance, passingArgs.toArray()); } + + private Object decideArgumentByParameter(Class param, List preparedArgs) { + return preparedArgs.stream() + .filter(arg -> param.isAssignableFrom(arg.getClass())) + .findAny() + .orElse(null); + } } From d029c34cfc1a62d3789bdde0b61e753753cceb7b Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 21:04:53 +0900 Subject: [PATCH 13/17] test: extract base package path --- .../webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index 7fed25dc26..e467f2038e 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -64,7 +64,8 @@ void post() throws Exception { @DisplayName("동일한 요청을 처리하는 2개의 핸들러 등록 시 예외가 발생한다.") void assignHandlerDuplicated() { // given - AnnotationHandlerMapping mapping = new AnnotationHandlerMapping("com.interface21.webmvc.servlet.samples"); + String basePackage = "com.interface21.webmvc.servlet.samples"; + AnnotationHandlerMapping mapping = new AnnotationHandlerMapping(basePackage); // when & then assertThatCode(mapping::initialize) From ba909d723c0bf4dcba991d72a78b3c2af182df08 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Thu, 19 Sep 2024 21:20:45 +0900 Subject: [PATCH 14/17] test: check invalidation --- .../mvc/tobe/AnnotationHandlerMapping.java | 1 - .../servlet/mvc/tobe/HandlerExecution.java | 2 +- .../tobe/AnnotationHandlerMappingTest.java | 34 +++++++++++++++++++ .../samples/DuplicatedRequestController.java | 4 +-- .../samples/PrivateConstructorController.java | 16 +++++++++ 5 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 mvc/src/test/java/com/interface21/webmvc/servlet/samples/PrivateConstructorController.java diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index 8e144637e0..469824b174 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -88,7 +88,6 @@ public Object getHandler(final HttpServletRequest request) { try { requestMethod = RequestMethod.valueOf(request.getMethod()); } catch (IllegalArgumentException e) { - log.error("Failed to get HandlerExecution"); return null; } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java index 8b2859dc17..cea191760b 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java @@ -17,7 +17,7 @@ public class HandlerExecution { public HandlerExecution(Method method, Class controllerClass) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { this.method = method; - this.controllerInstance = controllerClass.getDeclaredConstructor().newInstance(); + this.controllerInstance = controllerClass.getConstructor().newInstance(); } public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index e467f2038e..60afe2c6ab 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -73,6 +73,40 @@ void assignHandlerDuplicated() { .hasMessageContaining("HandlerKey exists"); } + @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 passArgumentsWithRequiredParameters() throws Exception { diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java index ed4ccab1a7..062a104234 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java @@ -7,11 +7,11 @@ @Controller public class DuplicatedRequestController { - @RequestMapping(value = "/api/test", method = RequestMethod.GET) + @RequestMapping(value = "/api/duplicated", method = RequestMethod.GET) public void test1() { } - @RequestMapping(value = "/api/test", method = RequestMethod.GET) + @RequestMapping(value = "/api/duplicated", method = RequestMethod.GET) public void test2() { } } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/PrivateConstructorController.java b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/PrivateConstructorController.java new file mode 100644 index 0000000000..76a743ace6 --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/PrivateConstructorController.java @@ -0,0 +1,16 @@ +package com.interface21.webmvc.servlet.samples; + +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; + +@Controller +public class PrivateConstructorController { + + private PrivateConstructorController() { + } + + @RequestMapping(value = "/api/private", method = RequestMethod.GET) + public void test() { + } +} From 0f726084886c319fbcb41374abfaf1fb545aea7f Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Fri, 20 Sep 2024 10:04:48 +0900 Subject: [PATCH 15/17] feat: implement JspView by DispatcherServlet.service --- .../webmvc/servlet/view/JspView.java | 20 ++++--- .../webmvc/servlet/view/JspViewTest.java | 56 +++++++++++++++++++ 2 files changed, 69 insertions(+), 7 deletions(-) create mode 100644 mvc/src/test/java/com/interface21/webmvc/servlet/view/JspViewTest.java diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java index 443fe4b4dd..554b50a0d6 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java @@ -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 model, final HttpServletRequest request, final HttpServletResponse response) throws Exception { - // todo + public void render(final Map 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); } } diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/view/JspViewTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/view/JspViewTest.java new file mode 100644 index 0000000000..cc1e7afffb --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/view/JspViewTest.java @@ -0,0 +1,56 @@ +package com.interface21.webmvc.servlet.view; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import jakarta.servlet.RequestDispatcher; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class JspViewTest { + + @Test + @DisplayName("요청 URI 가 redirect: 로 시작할 경우 응답을 Redirect 한다.") + void redirect() throws Exception { + // given + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + String endpoint = "/test.jsp"; + JspView jspView = new JspView(JspView.REDIRECT_PREFIX + endpoint); + + // when + jspView.render(null, request, response); + + // then + verify(response).sendRedirect(endpoint); + verify(request, never()).getRequestDispatcher(anyString()); + } + + @Test + @DisplayName("일반 요청에 대해 JSP 로 요청을 전달한다.") + void forward() throws Exception { + // given + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + String viewName = "/test.jsp"; + JspView jspView = new JspView(viewName); + RequestDispatcher requestDispatcher = mock(RequestDispatcher.class); + Map model = Map.of("k1", "v1", "k2", "v2"); + + when(request.getRequestDispatcher(viewName)).thenReturn(requestDispatcher); + + // when + jspView.render(model, request, response); + + // then + verify(response, never()).sendRedirect(anyString()); + model.forEach((k, v) -> verify(request).setAttribute(k, v)); + verify(requestDispatcher).forward(request, response); + } +} From bf3c880ee62fcf05111840cdb775f26285cb89c7 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sat, 21 Sep 2024 21:00:05 +0900 Subject: [PATCH 16/17] test: validate private constructor --- .../mvc/tobe/AnnotationHandlerMapping.java | 4 +++- .../mvc/tobe/AnnotationHandlerMappingTest.java | 15 ++++++++++++++- .../samples/DuplicatedRequestController.java | 17 ----------------- .../PrivateConstructorController.java | 2 +- 4 files changed, 18 insertions(+), 20 deletions(-) delete mode 100644 mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java rename mvc/src/test/java/com/interface21/webmvc/servlet/samples/{ => hided}/PrivateConstructorController.java (87%) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java index 469824b174..77720fae40 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -77,10 +77,12 @@ private void validateDuplicated(HandlerKey handlerKey) { 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; } + return null; } public Object getHandler(final HttpServletRequest request) { diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index 60afe2c6ab..578d06a478 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -64,7 +64,7 @@ void post() throws Exception { @DisplayName("동일한 요청을 처리하는 2개의 핸들러 등록 시 예외가 발생한다.") void assignHandlerDuplicated() { // given - String basePackage = "com.interface21.webmvc.servlet.samples"; + String basePackage = "com.interface21.webmvc.servlet.samples.duplicated"; AnnotationHandlerMapping mapping = new AnnotationHandlerMapping(basePackage); // when & then @@ -73,6 +73,19 @@ void assignHandlerDuplicated() { .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 { diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java deleted file mode 100644 index 062a104234..0000000000 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/DuplicatedRequestController.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.interface21.webmvc.servlet.samples; - -import com.interface21.context.stereotype.Controller; -import com.interface21.web.bind.annotation.RequestMapping; -import com.interface21.web.bind.annotation.RequestMethod; - -@Controller -public class DuplicatedRequestController { - - @RequestMapping(value = "/api/duplicated", method = RequestMethod.GET) - public void test1() { - } - - @RequestMapping(value = "/api/duplicated", method = RequestMethod.GET) - public void test2() { - } -} diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/PrivateConstructorController.java b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/hided/PrivateConstructorController.java similarity index 87% rename from mvc/src/test/java/com/interface21/webmvc/servlet/samples/PrivateConstructorController.java rename to mvc/src/test/java/com/interface21/webmvc/servlet/samples/hided/PrivateConstructorController.java index 76a743ace6..c641a3d865 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/samples/PrivateConstructorController.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/hided/PrivateConstructorController.java @@ -1,4 +1,4 @@ -package com.interface21.webmvc.servlet.samples; +package com.interface21.webmvc.servlet.samples.hided; import com.interface21.context.stereotype.Controller; import com.interface21.web.bind.annotation.RequestMapping; From 23c02be3711933144ca739426ca23cd091a857d3 Mon Sep 17 00:00:00 2001 From: Gyeongho Yang Date: Sat, 21 Sep 2024 21:13:23 +0900 Subject: [PATCH 17/17] test: fix meaningless method to clarify --- .../servlet/mvc/tobe/HandlerExecution.java | 5 +++- .../tobe/AnnotationHandlerMappingTest.java | 6 ----- .../mvc/tobe/HandlerExecutionTest.java | 23 +++++++++++++++++++ .../test/java/samples/NoArgsController.java | 19 +++++++++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) create mode 100644 mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecutionTest.java create mode 100644 mvc/src/test/java/samples/NoArgsController.java diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java index cea191760b..57edfaa1a5 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java @@ -7,6 +7,7 @@ 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 { @@ -21,7 +22,9 @@ public HandlerExecution(Method method, Class controllerClass) } public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - List preparedArgs = List.of(request, response); + List preparedArgs = Stream.of(request, response) + .filter(Objects::nonNull) + .toList(); List> requiredParameterClasses = Stream.of(method.getParameters()) .map(Parameter::getType) .toList(); diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index 578d06a478..293330d1c1 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -120,12 +120,6 @@ void requestWithUnassignedEndpoint() throws Exception { assertThat(handlerExecution).isNull(); } - @Test - @DisplayName("컨트롤러의 메서드가 요구하는 파라미터를 동적으로 대응하여 의존성을 주입한다.") - void passArgumentsWithRequiredParameters() throws Exception { - joinPathOfControllerAndMethod(); - } - @Test @DisplayName("컨트롤러와 메서드의 경로를 합쳐서 엔드포인트로 요청 경로를 결정한다.") void joinPathOfControllerAndMethod() throws Exception { diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecutionTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecutionTest.java new file mode 100644 index 0000000000..190cb5975c --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecutionTest.java @@ -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(); + } +} diff --git a/mvc/src/test/java/samples/NoArgsController.java b/mvc/src/test/java/samples/NoArgsController.java new file mode 100644 index 0000000000..bdcae94d5a --- /dev/null +++ b/mvc/src/test/java/samples/NoArgsController.java @@ -0,0 +1,19 @@ +package samples; + +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; + +@Controller +@RequestMapping("/api") +public class NoArgsController { + + @RequestMapping(value = "/no-args", method = RequestMethod.GET) + public ModelAndView noArgsMethod() { + ModelAndView modelAndView = new ModelAndView((model, request, response) -> { + }); + modelAndView.addObject("message", "no args"); + return modelAndView; + } +}