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..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 @@ -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,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); + } } 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..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 @@ -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(); + } + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - return null; + List preparedArgs = Stream.of(request, response) + .filter(Objects::nonNull) + .toList(); + List> requiredParameterClasses = Stream.of(method.getParameters()) + .map(Parameter::getType) + .toList(); + + List passingArgs = requiredParameterClasses.stream() + .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); } } 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/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index a19ea6e102..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 @@ -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; @@ -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"); + } } 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/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/hided/PrivateConstructorController.java b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/hided/PrivateConstructorController.java new file mode 100644 index 0000000000..c641a3d865 --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/samples/hided/PrivateConstructorController.java @@ -0,0 +1,16 @@ +package com.interface21.webmvc.servlet.samples.hided; + +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() { + } +} 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); + } +} 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; + } +} 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; + } +} 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; + } + } + } } } 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); 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()); } }