diff --git a/app/src/main/java/com/techcourse/DispatcherServlet.java b/app/src/main/java/com/techcourse/DispatcherServlet.java index 2943e9ae55..930562235b 100644 --- a/app/src/main/java/com/techcourse/DispatcherServlet.java +++ b/app/src/main/java/com/techcourse/DispatcherServlet.java @@ -1,12 +1,14 @@ package com.techcourse; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.View; +import com.interface21.webmvc.servlet.view.JspView; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.interface21.webmvc.servlet.view.JspView; public class DispatcherServlet extends HttpServlet { @@ -25,27 +27,20 @@ public void init() { } @Override - protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { + protected void service(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException { final String requestURI = request.getRequestURI(); log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI); try { final var controller = manualHandlerMapping.getHandler(requestURI); final var viewName = controller.execute(request, response); - move(viewName, request, response); + ModelAndView modelAndView = new ModelAndView(new JspView(viewName)); + View view = modelAndView.getView(); + view.render(modelAndView.getModel(), request, response); } catch (Throwable e) { log.error("Exception : {}", e.getMessage(), e); throw new ServletException(e.getMessage()); } } - - private void move(final String viewName, final HttpServletRequest request, final HttpServletResponse response) throws Exception { - if (viewName.startsWith(JspView.REDIRECT_PREFIX)) { - response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length())); - return; - } - - final var requestDispatcher = request.getRequestDispatcher(viewName); - requestDispatcher.forward(request, response); - } } diff --git a/mvc/src/main/java/com/interface21/web/bind/annotation/RequestMethod.java b/mvc/src/main/java/com/interface21/web/bind/annotation/RequestMethod.java index 3ffee4a52f..59aeea3ddd 100644 --- a/mvc/src/main/java/com/interface21/web/bind/annotation/RequestMethod.java +++ b/mvc/src/main/java/com/interface21/web/bind/annotation/RequestMethod.java @@ -1,5 +1,14 @@ package com.interface21.web.bind.annotation; +import java.util.Arrays; + public enum RequestMethod { - GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE + GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE; + + public static RequestMethod find(String raw) { + return Arrays.stream(values()) + .filter(value -> value.name().equals(raw)) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(String.format("유효하지 않은 요청 메서드입니다: %s", raw))); + } } 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..32ea39058f 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,16 +1,25 @@ 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.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.Set; +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 int EMPTY_REQUEST_METHODS = 0; + private final Object[] basePackage; private final Map handlerExecutions; @@ -21,9 +30,57 @@ public AnnotationHandlerMapping(final Object... basePackage) { public void initialize() { log.info("Initialized AnnotationHandlerMapping!"); + Reflections reflections = new Reflections(basePackage); + Set> controllerTypes = reflections.getTypesAnnotatedWith(Controller.class); + controllerTypes.forEach(this::mapControllerHandlers); + } + + private void mapControllerHandlers(Class controllerType) { + try { + Object controller = controllerType.getConstructor().newInstance(); + Method[] handlers = controllerType.getDeclaredMethods(); + mapHandlerToExecution(controller, handlers); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void mapHandlerToExecution(Object controller, Method[] handlers) { + Arrays.stream(handlers) + .filter(handler -> handler.isAnnotationPresent(RequestMapping.class)) + .forEach(handler -> addMapper(handler, new HandlerExecution(controller, handler))); + } + + private void addMapper(Method handler, HandlerExecution handlerExecution) { + RequestMapping requestMapping = handler.getAnnotation(RequestMapping.class); + List handlerKeys = createHandlerKeys(requestMapping); + handlerKeys.forEach(handlerKey -> handlerExecutions.put(handlerKey, handlerExecution)); + } + + private List createHandlerKeys(RequestMapping requestMapping) { + String uri = requestMapping.value(); + RequestMethod[] requestMethods = requestMapping.method(); + if (requestMethods.length == EMPTY_REQUEST_METHODS) { + requestMethods = RequestMethod.values(); + } + return Arrays.stream(requestMethods) + .map(requestMethod -> HandlerKey.from(uri, requestMethod)) + .toList(); } public Object getHandler(final HttpServletRequest request) { - return null; + HandlerKey handlerKey = createHandlerKey(request); + HandlerExecution handlerExecution = handlerExecutions.get(handlerKey); + if (handlerExecution == null) { + throw new IllegalArgumentException( + String.format("해당 요청에 대응하는 핸들러가 없습니다: %s %s", request.getMethod(), request.getRequestURI())); + } + return handlerExecution; + } + + private HandlerKey createHandlerKey(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + RequestMethod requestMethod = RequestMethod.find(request.getMethod()); + return HandlerKey.from(requestURI, requestMethod); } } 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..14ab0d60a7 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,26 @@ 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.Method; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class HandlerExecution { + private static final Logger log = LoggerFactory.getLogger(HandlerExecution.class); + + private final Object controller; + private final Method handler; + + public HandlerExecution(Object controller, Method handler) { + this.controller = controller; + this.handler = handler; + } + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - return null; + log.info("handler execution handle"); + return (ModelAndView) handler.invoke(controller, request, response); } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java index 55f8e593a1..6ff6b6c3f2 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java @@ -1,37 +1,53 @@ package com.interface21.webmvc.servlet.mvc.tobe; import com.interface21.web.bind.annotation.RequestMethod; - +import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; public class HandlerKey { - private final String url; - private final RequestMethod requestMethod; + private static final Map CACHE = new ConcurrentHashMap<>(); - public HandlerKey(final String url, final RequestMethod requestMethod) { - this.url = url; - this.requestMethod = requestMethod; + public final String url; + public final RequestMethod requestMethod; + + public static HandlerKey from(String url, RequestMethod requestMethod) { + String key = url + requestMethod.name(); + if (CACHE.containsKey(key)) { + return CACHE.get(key); + } + return new HandlerKey(url, requestMethod); } - @Override - public String toString() { - return "HandlerKey{" + - "url='" + url + '\'' + - ", requestMethod=" + requestMethod + - '}'; + private HandlerKey(final String url, final RequestMethod requestMethod) { + this.url = url; + this.requestMethod = requestMethod; + CACHE.put(url + requestMethod.name(), this); } @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof HandlerKey)) return false; - HandlerKey that = (HandlerKey) o; - return Objects.equals(url, that.url) && requestMethod == that.requestMethod; + if (this == o) { + return true; + } + if (!(o instanceof HandlerKey)) { + return false; + } + HandlerKey handlerKey = (HandlerKey) o; + return Objects.equals(url, handlerKey.url) && requestMethod == handlerKey.requestMethod; } @Override public int hashCode() { return Objects.hash(url, requestMethod); } + + @Override + public String toString() { + return "HandlerKey{" + + "url='" + url + '\'' + + ", requestMethod=" + requestMethod + + '}'; + } } 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..3ca81cb6dd 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 @@ -1,6 +1,7 @@ package com.interface21.webmvc.servlet.view; import com.interface21.webmvc.servlet.View; +import jakarta.servlet.RequestDispatcher; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; @@ -14,18 +15,27 @@ public class JspView implements View { public static final String REDIRECT_PREFIX = "redirect:"; + 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 + log.info("jsp view render"); model.keySet().forEach(key -> { log.debug("attribute name : {}, value : {}", key, model.get(key)); request.setAttribute(key, model.get(key)); }); - // todo + if (viewName.startsWith(REDIRECT_PREFIX)) { + response.sendRedirect(viewName.substring(REDIRECT_PREFIX.length())); + return; + } + + RequestDispatcher requestDispatcher = request.getRequestDispatcher(viewName); + requestDispatcher.forward(request, response); } } diff --git a/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java b/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java new file mode 100644 index 0000000000..f8e6d640eb --- /dev/null +++ b/mvc/src/test/java/com/interface21/web/bind/annotation/RequestMethodTest.java @@ -0,0 +1,35 @@ +package com.interface21.web.bind.annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class RequestMethodTest { + + @DisplayName("전달받은 문자열과 형태가 같은 요청 메서드를 반환한다.") + @Test + void should_findRequestMethod_when_givenValidString() { + // given + String raw = "GET"; + + // when + RequestMethod requestMethod = RequestMethod.find(raw); + + // then + assertThat(requestMethod).isEqualTo(RequestMethod.GET); + } + + @DisplayName("전달받은 문자열이 유효하지 않은 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenInvalidString() { + // given + String raw = "INVALID"; + + // when & then + assertThatThrownBy(() -> RequestMethod.find(raw)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("유효하지 않은 요청 메서드입니다: " + raw); + } +} 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..7857baf853 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,13 +1,18 @@ package com.interface21.webmvc.servlet.mvc.tobe; +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; + +import com.interface21.web.bind.annotation.RequestMethod; 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; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; class AnnotationHandlerMappingTest { @@ -19,8 +24,9 @@ void setUp() { handlerMapping.initialize(); } + @DisplayName("해당 uri와 method get에 매핑되는 핸들러를 찾아 실행한다.") @Test - void get() throws Exception { + void should_handleHandler_when_getRequest() throws Exception { final var request = mock(HttpServletRequest.class); final var response = mock(HttpServletResponse.class); @@ -34,8 +40,9 @@ void get() throws Exception { assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); } + @DisplayName("해당 uri와 method post에 매핑되는 핸들러를 찾아 실행한다.") @Test - void post() throws Exception { + void should_handleHandler_when_postRequest() throws Exception { final var request = mock(HttpServletRequest.class); final var response = mock(HttpServletResponse.class); @@ -48,4 +55,35 @@ void post() throws Exception { assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); } + + @DisplayName("해당 uri와 method에 매핑되는 핸들러가 없는 경우 예외가 발생한다.") + @Test + void should_throwException_when_givenInvalidRequest() throws Exception { + final var request = mock(HttpServletRequest.class); + + when(request.getAttribute("id")).thenReturn("gugu"); + when(request.getRequestURI()).thenReturn("/get-test"); + when(request.getMethod()).thenReturn("POST"); + + assertThatThrownBy(() -> handlerMapping.getHandler(request)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("해당 요청에 대응하는 핸들러가 없습니다: POST /get-test"); + } + + @DisplayName("핸들러에 http method가 선언되지 않은 경우 모든 method가 매핑되도록 한다.") + @ParameterizedTest + @EnumSource(value = RequestMethod.class) + void should_mapAllMethods_when_declaredMethodEmpty(RequestMethod method) throws Exception { + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + + when(request.getAttribute("id")).thenReturn("gugu"); + when(request.getRequestURI()).thenReturn("/all-test"); + when(request.getMethod()).thenReturn(method.name()); + + final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request); + final var modelAndView = handlerExecution.handle(request, response); + + assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); + } } 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..a5cde74acb --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKeyTest.java @@ -0,0 +1,70 @@ +package com.interface21.webmvc.servlet.mvc.tobe; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.interface21.web.bind.annotation.RequestMethod; +import java.lang.reflect.Field; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class HandlerKeyTest { + + @BeforeEach + void setUp() { + cleanCache(); + } + + @DisplayName("캐시에 존재하지 않는 HandlerKey 생성을 시도하는 경우 해당 인스턴스를 캐시에 추가 후 반환한다.") + @Test + void should_addInCache_when_createNewHandlerKey() { + // given + assertThat(getCacheSize()).isEqualTo(0); + + // when + HandlerKey.from("/new", RequestMethod.GET); + + // then + assertThat(getCacheSize()).isEqualTo(1); + } + + @DisplayName("캐시에 존재하는 HandlerKey 생성을 시도하는 경우 캐시 내 인스턴스를 반환한다.") + @Test + void should_getInCache_when_createExistHandlerKey() { + // given + HandlerKey exist = HandlerKey.from("/url", RequestMethod.GET); + assertThat(getCacheSize()).isEqualTo(1); + + // when + HandlerKey created = HandlerKey.from("/url", RequestMethod.GET); + + // then + assertThat(created).isSameAs(exist); + assertThat(getCacheSize()).isEqualTo(1); + } + + private int getCacheSize() { + try { + Field cacheField = HandlerKey.class.getDeclaredField("CACHE"); + cacheField.setAccessible(true); + Map cache = (Map) cacheField.get(null); + return cache.size(); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + throw new RuntimeException("CACHE 사이즈에 접근할 수 없습니다."); + } + } + + private void cleanCache() { + try { + Field cacheField = HandlerKey.class.getDeclaredField("CACHE"); + cacheField.setAccessible(true); + Map cache = (Map) cacheField.get(null); + cache.clear(); + } catch (NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + throw new RuntimeException("CACHE를 초기화할 수 없습니다."); + } + } +} diff --git a/mvc/src/test/java/samples/TestController.java b/mvc/src/test/java/samples/TestController.java index 6ad9c741dd..bf125171f4 100644 --- a/mvc/src/test/java/samples/TestController.java +++ b/mvc/src/test/java/samples/TestController.java @@ -16,17 +16,25 @@ public class TestController { private static final Logger log = LoggerFactory.getLogger(TestController.class); @RequestMapping(value = "/get-test", method = RequestMethod.GET) - public ModelAndView findUserId(final HttpServletRequest request, final HttpServletResponse response) { + public ModelAndView findUserId(HttpServletRequest request, HttpServletResponse response) { log.info("test controller get method"); - final var modelAndView = new ModelAndView(new JspView("")); + final ModelAndView modelAndView = new ModelAndView(new JspView("/get-test.jsp")); modelAndView.addObject("id", request.getAttribute("id")); return modelAndView; } @RequestMapping(value = "/post-test", method = RequestMethod.POST) - public ModelAndView save(final HttpServletRequest request, final HttpServletResponse response) { + public ModelAndView save(HttpServletRequest request, HttpServletResponse response) { log.info("test controller post method"); - final var modelAndView = new ModelAndView(new JspView("")); + final ModelAndView modelAndView = new ModelAndView(new JspView("/post-test.jsp")); + modelAndView.addObject("id", request.getAttribute("id")); + return modelAndView; + } + + @RequestMapping(value = "/all-test") + public ModelAndView all(HttpServletRequest request, HttpServletResponse response) { + log.info("test controller all method"); + final ModelAndView modelAndView = new ModelAndView(new JspView("")); modelAndView.addObject("id", request.getAttribute("id")); return modelAndView; } diff --git a/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java b/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java index cf4d886974..14a7e6525b 100644 --- a/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java +++ b/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java @@ -1,16 +1,27 @@ package servlet.com.example; -import jakarta.servlet.*; +import jakarta.servlet.Filter; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; import jakarta.servlet.annotation.WebFilter; - import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @WebFilter("/*") public class CharacterEncodingFilter implements Filter { + private static final Logger log = LoggerFactory.getLogger(CharacterEncodingFilter.class); + @Override - public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { request.getServletContext().log("doFilter() 호출"); + log.info("기존 characterEncoding = {}", response.getCharacterEncoding()); + response.setCharacterEncoding("UTF-8"); + log.info("변경 characterEncoding = {}", response.getCharacterEncoding()); chain.doFilter(request, response); } } diff --git a/study/src/test/java/reflection/Junit3Test.java b/study/src/test/java/reflection/Junit3Test.java index 2fdf586e01..de7cad07df 100644 --- a/study/src/test/java/reflection/Junit3Test.java +++ b/study/src/test/java/reflection/Junit3Test.java @@ -9,7 +9,7 @@ public void test2() throws Exception { System.out.println("Running Test2"); } - public void three() throws Exception { + private void privateTest3() throws Exception { System.out.println("Running Test3"); } } diff --git a/study/src/test/java/reflection/Junit3TestRunner.java b/study/src/test/java/reflection/Junit3TestRunner.java index b4e465240c..663a24fd97 100644 --- a/study/src/test/java/reflection/Junit3TestRunner.java +++ b/study/src/test/java/reflection/Junit3TestRunner.java @@ -1,13 +1,72 @@ package reflection; +import static org.assertj.core.api.Assertions.assertThat; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; class Junit3TestRunner { + Class clazz = Junit3Test.class; + @Test void run() throws Exception { - Class clazz = Junit3Test.class; - // TODO Junit3Test에서 test로 시작하는 메소드 실행 + Junit3Test junit3Test = clazz.getConstructor().newInstance(); + List testMethods = getDeclaredTestMethods(clazz); + assertThat(testMethods).hasSize(2); + for (Method method : testMethods) { + method.invoke(junit3Test); + } + } + + private List getDeclaredTestMethods(Class clazz) { + Method[] methods = clazz.getMethods(); + return Arrays.stream(methods) + .filter(method -> method.getName().startsWith("test")) + .toList(); + } + + @DisplayName("getMethods: 상속 메서드를 포함한 모든 public 메서드를 반환한다.") + @Test + void getMethods() { + // given & when + Method[] methods = clazz.getMethods(); + + // then + List actual = Arrays.stream(methods) + .map(Method::getName) + .toList(); + assertThat(actual).containsOnly( + "test1", + "test2", + "equals", + "toString", + "hashCode", + "getClass", + "notify", + "notifyAll", + "wait", + "wait", + "wait"); + } + + @DisplayName("getDeclaredMethods: 상속 메서드를 제외하고 직접 선언된 모든 메서드를 반환한다.") + @Test + void getDeclaredMethods() { + // given & when + Method[] methods = clazz.getDeclaredMethods(); + + // then + List actual = Arrays.stream(methods) + .map(Method::getName) + .toList(); + assertThat(actual).containsOnly( + "test1", + "test2", + "privateTest3"); } } diff --git a/study/src/test/java/reflection/Junit4TestRunner.java b/study/src/test/java/reflection/Junit4TestRunner.java index 8a6916bc24..1c54f34111 100644 --- a/study/src/test/java/reflection/Junit4TestRunner.java +++ b/study/src/test/java/reflection/Junit4TestRunner.java @@ -1,5 +1,6 @@ package reflection; +import java.lang.reflect.Method; import org.junit.jupiter.api.Test; class Junit4TestRunner { @@ -9,5 +10,12 @@ void run() throws Exception { Class clazz = Junit4Test.class; // TODO Junit4Test에서 @MyTest 애노테이션이 있는 메소드 실행 + Junit4Test junit4Test = clazz.getConstructor().newInstance(); + Method[] methods = clazz.getDeclaredMethods(); + for (Method method : methods) { + if (method.isAnnotationPresent(MyTest.class)) { + method.invoke(junit4Test); + } + } } } diff --git a/study/src/test/java/reflection/Question.java b/study/src/test/java/reflection/Question.java index 2b20c67659..452d7727b9 100644 --- a/study/src/test/java/reflection/Question.java +++ b/study/src/test/java/reflection/Question.java @@ -64,6 +64,25 @@ public void update(Question newQuestion) { this.contents = newQuestion.contents; } + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof Question)) { + return false; + } + Question question = (Question) o; + return questionId == question.questionId && countOfComment == question.countOfComment && Objects.equals(writer, + question.writer) && Objects.equals(title, question.title) && Objects.equals(contents, question.contents) + && Objects.equals(createdDate, question.createdDate); + } + + @Override + public int hashCode() { + return Objects.hash(questionId, writer, title, contents, createdDate, countOfComment); + } + @Override public String toString() { return "Question{" + @@ -75,17 +94,4 @@ public String toString() { ", countOfComment=" + countOfComment + '}'; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof Question)) return false; - Question question = (Question) o; - return questionId == question.questionId && countOfComment == question.countOfComment && Objects.equals(writer, question.writer) && Objects.equals(title, question.title) && Objects.equals(contents, question.contents) && Objects.equals(createdDate, question.createdDate); - } - - @Override - public int hashCode() { - return Objects.hash(questionId, writer, title, contents, createdDate, countOfComment); - } } diff --git a/study/src/test/java/reflection/ReflectionTest.java b/study/src/test/java/reflection/ReflectionTest.java index 370f0932b9..56ee26f075 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,36 +20,42 @@ 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; - - assertThat(actualFieldNames).contains("name", "age"); + final Field[] fields = student.getClass().getDeclaredFields(); + final List actualFieldNames = Arrays.stream(fields) + .map(Field::getName) + .toList(); + + assertThat(actualFieldNames) + .hasSize(2) + .contains("name", "age"); } @Test void givenClass_whenGetsMethods_thenCorrect() { - final Class animalClass = Student.class; - final Method[] methods = null; - final List actualMethods = null; + final Class studentClass = Student.class; + final Method[] methods = studentClass.getDeclaredMethods(); + final List actualMethodNames = Arrays.stream(methods) + .map(Method::getName) + .toList(); - assertThat(actualMethods) + assertThat(actualMethodNames) .hasSize(3) .contains("getAge", "toString", "getName"); } @@ -56,20 +63,22 @@ void givenClass_whenGetsMethods_thenCorrect() { @Test void givenClass_whenGetsAllConstructors_thenCorrect() { final Class questionClass = Question.class; - final Constructor[] constructors = null; + final Constructor[] constructors = questionClass.getConstructors(); assertThat(constructors).hasSize(2); } @Test void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception { - final Class questionClass = Question.class; + final Class questionClass = Question.class; - final Constructor firstConstructor = null; - final Constructor secondConstructor = null; + final Constructor firstConstructor = questionClass + .getConstructor(String.class, String.class, String.class); + final Constructor secondConstructor = questionClass + .getConstructor(long.class, String.class, String.class, String.class, Date.class, int.class); - final Question firstQuestion = null; - final Question secondQuestion = null; + final Question firstQuestion = firstConstructor.newInstance("gugu", "제목1", "내용1"); + final Question secondQuestion = secondConstructor.newInstance(1, "gugu", "제목2", "내용2", new Date(), 1); assertThat(firstQuestion.getWriter()).isEqualTo("gugu"); assertThat(firstQuestion.getTitle()).isEqualTo("제목1"); @@ -82,7 +91,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(); // public fields assertThat(fields).hasSize(0); } @@ -90,7 +99,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 +108,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,23 +116,24 @@ 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"); } @Test void givenClassField_whenSetsAndGetsValue_thenCorrect() throws Exception { - final Class studentClass = Student.class; - final Student student = null; - final Field field = null; + final Class studentClass = Student.class; + final Student student = studentClass.getConstructor().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..1ad5b1aee8 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,12 @@ void showAnnotationClass() throws Exception { Reflections reflections = new Reflections("reflection.examples"); // TODO 클래스 레벨에 @Controller, @Service, @Repository 애노테이션이 설정되어 모든 클래스 찾아 로그로 출력한다. + Set> controllers = reflections.getTypesAnnotatedWith(Controller.class); + Set> services = reflections.getTypesAnnotatedWith(Service.class); + Set> repositories = reflections.getTypesAnnotatedWith(Repository.class); + + controllers.forEach(controller -> log.info(controller.getSimpleName())); + services.forEach(service -> log.info(service.getSimpleName())); + repositories.forEach(repository -> log.info(repository.getSimpleName())); } } diff --git a/study/src/test/java/servlet/com/example/ServletTest.java b/study/src/test/java/servlet/com/example/ServletTest.java index 75fbb10dd5..36a262fce4 100644 --- a/study/src/test/java/servlet/com/example/ServletTest.java +++ b/study/src/test/java/servlet/com/example/ServletTest.java @@ -1,10 +1,10 @@ package servlet.com.example; +import static org.assertj.core.api.Assertions.assertThat; + import org.junit.jupiter.api.Test; import support.HttpUtils; -import static org.assertj.core.api.Assertions.assertThat; - class ServletTest { private final String WEBAPP_DIR_LOCATION = "src/main/webapp/"; @@ -27,8 +27,9 @@ void testSharedCounter() { assertThat(response.statusCode()).isEqualTo(200); // expected를 0이 아닌 올바른 값으로 바꿔보자. - // 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? - assertThat(Integer.parseInt(response.body())).isEqualTo(0); + // Q. 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? + // A. SharedCounterServlet의 sharedCounter는 공유되므로 + assertThat(Integer.parseInt(response.body())).isEqualTo(3); } @Test @@ -49,7 +50,9 @@ void testLocalCounter() { assertThat(response.statusCode()).isEqualTo(200); // expected를 0이 아닌 올바른 값으로 바꿔보자. - // 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? - assertThat(Integer.parseInt(response.body())).isEqualTo(0); + // Q. 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? + // A. LocalCounterServlet의 localCounter는 매 요청마다 새로 생성되므로 + assertThat(Integer.parseInt(response.body())).isEqualTo(1); + // init -> (doFilter -> service) * 3 -> destroy } }