From 949fac157d006e4da2ad681df230f0048a14d543 Mon Sep 17 00:00:00 2001 From: Bojun Kim Date: Wed, 13 Sep 2023 22:51:36 +0900 Subject: [PATCH] =?UTF-8?q?[MVC=20=EB=AF=B8=EC=85=98=201=EB=8B=A8=EA=B3=84?= =?UTF-8?q?]=20=ED=8F=AC=EC=9D=B4(=EA=B9=80=EB=B3=B4=EC=A4=80)=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EC=A0=9C=EC=B6=9C=ED=95=A9=EB=8B=88=EB=8B=A4.=20?= =?UTF-8?q?=20(#361)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: 리플렉션 테스트 완료 * test: 서블릿, 필터 학습 테스트 완료 * feat: 어노테이션 기반으로 HandlerMapping을 초기화해주는 기능 추가 --- .../web/bind/annotation/RequestMethod.java | 10 +- .../mvc/tobe/AnnotationHandlerMapping.java | 57 ++++++++++- .../servlet/mvc/tobe/HandlerExecution.java | 11 ++- .../com/example/CharacterEncodingFilter.java | 2 + .../java/reflection/Junit3TestRunner.java | 15 ++- .../java/reflection/Junit4TestRunner.java | 13 ++- .../test/java/reflection/ReflectionTest.java | 99 ++++++++++++------- .../test/java/reflection/ReflectionsTest.java | 11 ++- .../java/servlet/com/example/ServletTest.java | 4 +- 9 files changed, 174 insertions(+), 48 deletions(-) diff --git a/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java index 1dd958bd23..eb37aed637 100644 --- a/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java @@ -1,5 +1,13 @@ package web.org.springframework.web.bind.annotation; public enum RequestMethod { - GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE + GET, + HEAD, + POST, + PUT, + PATCH, + DELETE, + OPTIONS, + TRACE + ; } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java index a355218efa..90860d07a1 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -1,11 +1,20 @@ package webmvc.org.springframework.web.servlet.mvc.tobe; +import context.org.springframework.stereotype.Controller; import jakarta.servlet.http.HttpServletRequest; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.reflections.Reflections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; public class AnnotationHandlerMapping { @@ -21,9 +30,49 @@ public AnnotationHandlerMapping(final Object... basePackage) { public void initialize() { log.info("Initialized AnnotationHandlerMapping!"); + final var reflections = new Reflections(basePackage); + final var methodsPerClass = groupingMethodsByClass(reflections); + + for (var classAndMethods : methodsPerClass.entrySet()) { + final var clazz = classAndMethods.getKey(); + final var methods = classAndMethods.getValue(); + try { + initializeEachClassExecutions(clazz, methods); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } } + private Map, List> groupingMethodsByClass(final Reflections reflections) { + return reflections.getTypesAnnotatedWith(Controller.class).stream() + .map(Class::getDeclaredMethods) + .flatMap(Stream::of) + .filter(method -> method.isAnnotationPresent(RequestMapping.class)) + .collect(Collectors.groupingBy(Method::getDeclaringClass)); + } + + private void initializeEachClassExecutions( + final Class clazz, + final List methods + ) throws InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException { + final var parentPath = clazz.getAnnotation(Controller.class).path(); + final var target = clazz.getConstructor().newInstance(); + for (var method : methods) { + initializeEachMethodExecution(method, parentPath, target); + } + } + + private void initializeEachMethodExecution(final Method method, final String parentPath, final Object target) { + final var requestMapping = method.getAnnotation(RequestMapping.class); + final var path = parentPath + requestMapping.value(); + Arrays.stream(requestMapping.method()) + .map(requestMethod -> new HandlerKey(path, requestMethod)) + .forEach(key -> handlerExecutions.put(key, new HandlerExecution(target, method))); + } + + public Object getHandler(final HttpServletRequest request) { - return null; + return handlerExecutions.get(new HandlerKey(request.getRequestURI(), RequestMethod.valueOf(request.getMethod().toUpperCase()))); } } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java index 37c583fbdf..a49fc49c62 100644 --- a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java @@ -2,11 +2,20 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; import webmvc.org.springframework.web.servlet.ModelAndView; public class HandlerExecution { + private final Object target; + private final Method method; + + public HandlerExecution(Object target, final Method method) { + this.target = target; + this.method = method; + } + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - return null; + return (ModelAndView) method.invoke(target, request, response); } } diff --git a/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java b/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java index cf4d886974..9ff52eb1fb 100644 --- a/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java +++ b/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java @@ -4,6 +4,7 @@ import jakarta.servlet.annotation.WebFilter; import java.io.IOException; +import java.nio.charset.StandardCharsets; @WebFilter("/*") public class CharacterEncodingFilter implements Filter { @@ -11,6 +12,7 @@ public class CharacterEncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.getServletContext().log("doFilter() 호출"); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); chain.doFilter(request, response); } } diff --git a/study/src/test/java/reflection/Junit3TestRunner.java b/study/src/test/java/reflection/Junit3TestRunner.java index b4e465240c..83e0d83e3c 100644 --- a/study/src/test/java/reflection/Junit3TestRunner.java +++ b/study/src/test/java/reflection/Junit3TestRunner.java @@ -1,13 +1,24 @@ package reflection; +import java.util.Arrays; import org.junit.jupiter.api.Test; class Junit3TestRunner { @Test void run() throws Exception { - Class clazz = Junit3Test.class; + final var clazz = Junit3Test.class; - // TODO Junit3Test에서 test로 시작하는 메소드 실행 + // Junit3Test에서 test로 시작하는 메소드 실행 + Arrays.stream(clazz.getDeclaredMethods()) + .filter(method -> method.getName().startsWith("test")) + .forEach(method -> { + try { + method.invoke(clazz.getDeclaredConstructor().newInstance()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + ); } } diff --git a/study/src/test/java/reflection/Junit4TestRunner.java b/study/src/test/java/reflection/Junit4TestRunner.java index 8a6916bc24..eb21dbe118 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.util.Arrays; import org.junit.jupiter.api.Test; class Junit4TestRunner { @@ -8,6 +9,16 @@ class Junit4TestRunner { void run() throws Exception { Class clazz = Junit4Test.class; - // TODO Junit4Test에서 @MyTest 애노테이션이 있는 메소드 실행 + // Junit4Test에서 @MyTest 애노테이션이 있는 메소드 실행 + Arrays.stream(clazz.getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(MyTest.class)) + .forEach(method -> { + try { + method.invoke(clazz.getDeclaredConstructor().newInstance()); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + ); } } diff --git a/study/src/test/java/reflection/ReflectionTest.java b/study/src/test/java/reflection/ReflectionTest.java index 370f0932b9..877bddd5ce 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.List; - -import static org.assertj.core.api.Assertions.assertThat; +import java.util.Arrays; +import java.util.Date; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; class ReflectionTest { @@ -19,44 +20,47 @@ 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 var student = new Student(); + final var fields = student.getClass().getDeclaredFields(); + final var actualFieldNames = Arrays.stream(fields) + .map(Field::getName) + .collect(Collectors.toList()); assertThat(actualFieldNames).contains("name", "age"); } @Test void givenClass_whenGetsMethods_thenCorrect() { - final Class animalClass = Student.class; - final Method[] methods = null; - final List actualMethods = null; - - assertThat(actualMethods) - .hasSize(3) - .contains("getAge", "toString", "getName"); + final var animalClass = Student.class; + final var methods = animalClass.getDeclaredMethods(); + final var actualMethods = Arrays.stream(methods) + .map(Method::getName) + .collect(Collectors.toList()); + + assertThat(actualMethods).hasSize(3) + .contains("getAge", "toString", "getName"); } @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 +69,34 @@ void givenClass_whenGetsAllConstructors_thenCorrect() { void givenClass_whenInstantiatesObjectsAtRuntime_thenCorrect() throws Exception { final Class questionClass = Question.class; - final Constructor firstConstructor = null; - final Constructor secondConstructor = null; - final Question firstQuestion = null; - final Question secondQuestion = 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 = (Question) firstConstructor.newInstance( + "gugu", + "제목1", + "내용1" + ); + final Question secondQuestion = (Question) secondConstructor.newInstance( + 1L, + "gugu", + "제목2", + "내용2", + new Date(), + 0 + ); assertThat(firstQuestion.getWriter()).isEqualTo("gugu"); assertThat(firstQuestion.getTitle()).isEqualTo("제목1"); @@ -82,7 +109,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 +117,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 +126,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 +134,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 +142,17 @@ 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.getConstructor().newInstance(); + final Field field = studentClass.getDeclaredField("age"); + + field.setAccessible(true); - // todo field에 접근 할 수 있도록 만든다. + // field에 접근 할 수 있도록 만든다. 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..fdc73a317b 100644 --- a/study/src/test/java/reflection/ReflectionsTest.java +++ b/study/src/test/java/reflection/ReflectionsTest.java @@ -1,18 +1,25 @@ package reflection; +import static org.reflections.scanners.Scanners.TypesAnnotated; + 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 { private static final Logger log = LoggerFactory.getLogger(ReflectionsTest.class); @Test - void showAnnotationClass() throws Exception { + void showAnnotationClass() { Reflections reflections = new Reflections("reflection.examples"); - // TODO 클래스 레벨에 @Controller, @Service, @Repository 애노테이션이 설정되어 모든 클래스 찾아 로그로 출력한다. + // 클래스 레벨에 @Controller, @Service, @Repository 애노테이션이 설정되어있는 모든 클래스 찾아 로그로 출력한다. + reflections.get(TypesAnnotated.of(Controller.class, Service.class, Repository.class)) + .forEach(clazz -> log.info("{}", clazz)); } } diff --git a/study/src/test/java/servlet/com/example/ServletTest.java b/study/src/test/java/servlet/com/example/ServletTest.java index 75fbb10dd5..e335f8190a 100644 --- a/study/src/test/java/servlet/com/example/ServletTest.java +++ b/study/src/test/java/servlet/com/example/ServletTest.java @@ -28,7 +28,7 @@ void testSharedCounter() { // expected를 0이 아닌 올바른 값으로 바꿔보자. // 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? - assertThat(Integer.parseInt(response.body())).isEqualTo(0); + assertThat(Integer.parseInt(response.body())).isEqualTo(3); } @Test @@ -50,6 +50,6 @@ void testLocalCounter() { // expected를 0이 아닌 올바른 값으로 바꿔보자. // 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? - assertThat(Integer.parseInt(response.body())).isEqualTo(0); + assertThat(Integer.parseInt(response.body())).isEqualTo(1); } }