diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d672c0be0a..32019de097 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: branches: - main pull_request: - types: [opened, synchronize, reopened] + types: [ opened, synchronize, reopened ] jobs: build: name: Build and analyze @@ -34,4 +34,4 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./gradlew build sonar --info -x :study:build + run: ./gradlew clean build codeCoverageReport sonar --info -x :study:build diff --git a/.gitignore b/.gitignore index 672d1adecf..1bcba66f9d 100644 --- a/.gitignore +++ b/.gitignore @@ -171,3 +171,5 @@ Temporary Items tomcat.* tomcat.*/** + +**/WEB-INF/classes/** diff --git a/README.md b/README.md index 35eff71457..6aca44038e 100644 --- a/README.md +++ b/README.md @@ -1 +1,12 @@ # @MVC 구현하기 + +### 1 단계 - @MVC 프레임워크 구현하기 + +- [x] AnnotationHandlerMappingTest 클래스의 실패하는 테스트를 통과시키기 +- [x] Controller 애너테이션이 적용된 클래스를 전부 반환하는 기능 추가 +- [x] 클래스 레벨의 RequestMapping 적용 +- [x] GET, POST, PATCH, PUT, DELETE Mapping 추가 + +### 2단계 - 점진적인 리팩터링 + + diff --git a/app/src/main/java/com/techcourse/AppWebApplicationInitializer.java b/app/src/main/java/com/techcourse/AppWebApplicationInitializer.java deleted file mode 100644 index 53e45857fd..0000000000 --- a/app/src/main/java/com/techcourse/AppWebApplicationInitializer.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.techcourse; - -import jakarta.servlet.ServletContext; -import nextstep.mvc.DispatcherServlet; -import nextstep.web.WebApplicationInitializer; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class AppWebApplicationInitializer implements WebApplicationInitializer { - - private static final Logger log = LoggerFactory.getLogger(AppWebApplicationInitializer.class); - - @Override - public void onStartup(final ServletContext servletContext) { - final var dispatcherServlet = new DispatcherServlet(); - dispatcherServlet.addHandlerMapping(new ManualHandlerMapping()); - - final var dispatcher = servletContext.addServlet("dispatcher", dispatcherServlet); - dispatcher.setLoadOnStartup(1); - dispatcher.addMapping("/"); - - log.info("Start AppWebApplication Initializer"); - } -} diff --git a/app/src/main/java/com/techcourse/Application.java b/app/src/main/java/com/techcourse/Application.java index f5c8248a3e..500d3f2d8b 100644 --- a/app/src/main/java/com/techcourse/Application.java +++ b/app/src/main/java/com/techcourse/Application.java @@ -1,11 +1,9 @@ package com.techcourse; -import org.apache.catalina.connector.Connector; -import org.apache.catalina.startup.Tomcat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; +import java.io.IOException; import java.util.stream.Stream; public class Application { @@ -16,22 +14,11 @@ public class Application { public static void main(final String[] args) throws Exception { final int port = defaultPortIfNull(args); - - final var tomcat = new Tomcat(); - tomcat.setConnector(createConnector(port)); - final var docBase = new File("app/src/main/webapp/").getAbsolutePath(); - tomcat.addWebapp("", docBase); - log.info("configuring app with basedir: {}", docBase); + final var tomcat = new TomcatStarter(port); + log.info("configuring app with basedir: {}", TomcatStarter.WEBAPP_DIR_LOCATION); tomcat.start(); - tomcat.getServer().await(); - } - - private static Connector createConnector(final int port) { - final var connector = new Connector(); - connector.setPort(port); - connector.setProperty("bindOnInit", "false"); - return connector; + stop(tomcat); } private static int defaultPortIfNull(final String[] args) { @@ -40,4 +27,16 @@ private static int defaultPortIfNull(final String[] args) { .map(Integer::parseInt) .orElse(DEFAULT_PORT); } + + private static void stop(final TomcatStarter tomcat) { + try { + // make the application wait until we press any key. + System.in.read(); + } catch (IOException e) { + log.error(e.getMessage(), e); + } finally { + log.info("web server stop."); + tomcat.stop(); + } + } } diff --git a/mvc/src/main/java/nextstep/mvc/DispatcherServlet.java b/app/src/main/java/com/techcourse/DispatcherServlet.java similarity index 76% rename from mvc/src/main/java/nextstep/mvc/DispatcherServlet.java rename to app/src/main/java/com/techcourse/DispatcherServlet.java index 9e6951a259..277d8eed9a 100644 --- a/mvc/src/main/java/nextstep/mvc/DispatcherServlet.java +++ b/app/src/main/java/com/techcourse/DispatcherServlet.java @@ -1,36 +1,36 @@ -package nextstep.mvc; +package com.techcourse; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.controller.asis.Controller; -import nextstep.mvc.view.JspView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import webmvc.org.springframework.web.servlet.view.JspView; public class DispatcherServlet extends HttpServlet { private static final long serialVersionUID = 1L; private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); - private HandlerMapping handlerMappings; + private ManualHandlerMapping manualHandlerMapping; - public void addHandlerMapping(final HandlerMapping handlerMapping) { - this.handlerMappings = handlerMapping; + public DispatcherServlet() { } @Override public void init() { - this.handlerMappings.initialize(); + manualHandlerMapping = new ManualHandlerMapping(); + manualHandlerMapping.initialize(); } @Override protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { - log.debug("Method : {}, Request URI : {}", request.getMethod(), request.getRequestURI()); + final String requestURI = request.getRequestURI(); + log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI); try { - final var controller = (Controller) handlerMappings.getHandler(request); + final var controller = manualHandlerMapping.getHandler(requestURI); final var viewName = controller.execute(request, response); move(viewName, request, response); } catch (Throwable e) { diff --git a/app/src/main/java/com/techcourse/DispatcherServletInitializer.java b/app/src/main/java/com/techcourse/DispatcherServletInitializer.java new file mode 100644 index 0000000000..6e814cdd25 --- /dev/null +++ b/app/src/main/java/com/techcourse/DispatcherServletInitializer.java @@ -0,0 +1,33 @@ +package com.techcourse; + +import jakarta.servlet.ServletContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import web.org.springframework.web.WebApplicationInitializer; + +/** + * Base class for {@link WebApplicationInitializer} + * implementations that register a {@link DispatcherServlet} in the servlet context. + */ +public class DispatcherServletInitializer implements WebApplicationInitializer { + + private static final Logger log = LoggerFactory.getLogger(DispatcherServletInitializer.class); + + private static final String DEFAULT_SERVLET_NAME = "dispatcher"; + + @Override + public void onStartup(final ServletContext servletContext) { + final var dispatcherServlet = new DispatcherServlet(); + + final var registration = servletContext.addServlet(DEFAULT_SERVLET_NAME, dispatcherServlet); + if (registration == null) { + throw new IllegalStateException("Failed to register servlet with name '" + DEFAULT_SERVLET_NAME + "'. " + + "Check if there is another servlet registered under the same name."); + } + + registration.setLoadOnStartup(1); + registration.addMapping("/"); + + log.info("Start AppWebApplication Initializer"); + } +} diff --git a/app/src/main/java/com/techcourse/ManualHandlerMapping.java b/app/src/main/java/com/techcourse/ManualHandlerMapping.java index 8a6fa9080f..a54863caf8 100644 --- a/app/src/main/java/com/techcourse/ManualHandlerMapping.java +++ b/app/src/main/java/com/techcourse/ManualHandlerMapping.java @@ -1,23 +1,20 @@ package com.techcourse; import com.techcourse.controller.*; -import jakarta.servlet.http.HttpServletRequest; -import nextstep.mvc.HandlerMapping; -import nextstep.mvc.controller.asis.Controller; -import nextstep.mvc.controller.asis.ForwardController; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; +import webmvc.org.springframework.web.servlet.mvc.asis.ForwardController; import java.util.HashMap; import java.util.Map; -public class ManualHandlerMapping implements HandlerMapping { +public class ManualHandlerMapping { private static final Logger log = LoggerFactory.getLogger(ManualHandlerMapping.class); private static final Map controllers = new HashMap<>(); - @Override public void initialize() { controllers.put("/", new ForwardController("/index.jsp")); controllers.put("/login", new LoginController()); @@ -31,9 +28,7 @@ public void initialize() { .forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass())); } - @Override - public Controller getHandler(HttpServletRequest request) { - final String requestURI = request.getRequestURI(); + public Controller getHandler(final String requestURI) { log.debug("Request Mapping Uri : {}", requestURI); return controllers.get(requestURI); } diff --git a/app/src/main/java/com/techcourse/TomcatStarter.java b/app/src/main/java/com/techcourse/TomcatStarter.java new file mode 100644 index 0000000000..4f26f228e3 --- /dev/null +++ b/app/src/main/java/com/techcourse/TomcatStarter.java @@ -0,0 +1,83 @@ +package com.techcourse; + +import org.apache.catalina.Context; +import org.apache.catalina.LifecycleException; +import org.apache.catalina.connector.Connector; +import org.apache.catalina.core.StandardContext; +import org.apache.catalina.startup.Tomcat; +import org.apache.tomcat.util.scan.StandardJarScanner; + +import java.io.File; + +public class TomcatStarter { + + public static final String WEBAPP_DIR_LOCATION = "app/src/main/webapp/"; + + private final Tomcat tomcat; + + public TomcatStarter(final int port) { + this(WEBAPP_DIR_LOCATION, port); + } + + public TomcatStarter(final String webappDirLocation, final int port) { + this.tomcat = new Tomcat(); + tomcat.setConnector(createConnector(port)); + + final var docBase = new File(webappDirLocation).getAbsolutePath(); + final var context = (StandardContext) tomcat.addWebapp("", docBase); + skipJarScan(context); + skipClearReferences(context); + } + + public void start() { + try { + tomcat.start(); + } catch (LifecycleException e) { + throw new UncheckedServletException(e); + } + } + + public void stop() { + try { + tomcat.stop(); + tomcat.destroy(); + } catch (LifecycleException e) { + throw new UncheckedServletException(e); + } + } + + private Connector createConnector(final int port) { + final var connector = new Connector(); + connector.setPort(port); + return connector; + } + + private void skipJarScan(final Context context) { + final var jarScanner = (StandardJarScanner) context.getJarScanner(); + jarScanner.setScanClassPath(false); + } + + private void skipClearReferences(final StandardContext context) { + /** + * https://tomcat.apache.org/tomcat-10.1-doc/config/context.html + * + * setClearReferencesObjectStreamClassCaches 번역 + * true인 경우 웹 응용 프로그램이 중지되면 Tomcat은 직렬화에 사용되는 + * ObjectStreamClass 클래스에서 웹 응용 프로그램에 의해 로드된 + * 클래스에 대한 SoftReference를 찾고 찾은 모든 SoftReference를 지웁니다. + * 이 기능은 리플렉션을 사용하여 SoftReference를 식별하므로 Java 9 이상에서 + * 실행할 때 명령줄 옵션 -XaddExports:java.base/java.io=ALL-UNNAMED를 설정해야 합니다. + * 지정하지 않으면 기본값인 true가 사용됩니다. + * + * ObjectStreamClass와 관련된 메모리 누수는 Java 19 이상, Java 17.0.4 이상 및 + * Java 11.0.16 이상에서 수정되었습니다. + * 수정 사항이 포함된 Java 버전에서 실행할 때 확인이 비활성화됩니다. + * + * Amazon Corretto-17.0.6은 경고 메시지가 나옴. + * 학습과 관련 없는 메시지가 나오지 않도록 관련 설정을 끈다. + */ + context.setClearReferencesObjectStreamClassCaches(false); + context.setClearReferencesRmiTargets(false); + context.setClearReferencesThreadLocals(false); + } +} diff --git a/app/src/main/java/com/techcourse/UncheckedServletException.java b/app/src/main/java/com/techcourse/UncheckedServletException.java new file mode 100644 index 0000000000..26acea7605 --- /dev/null +++ b/app/src/main/java/com/techcourse/UncheckedServletException.java @@ -0,0 +1,8 @@ +package com.techcourse; + +public class UncheckedServletException extends RuntimeException { + + public UncheckedServletException(Exception e) { + super(e); + } +} diff --git a/app/src/main/java/com/techcourse/controller/LoginController.java b/app/src/main/java/com/techcourse/controller/LoginController.java index a07292e25b..0428fe109e 100644 --- a/app/src/main/java/com/techcourse/controller/LoginController.java +++ b/app/src/main/java/com/techcourse/controller/LoginController.java @@ -4,7 +4,7 @@ import com.techcourse.repository.InMemoryUserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.controller.asis.Controller; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,8 +31,7 @@ private String login(final HttpServletRequest request, final User user) { final var session = request.getSession(); session.setAttribute(UserSession.SESSION_KEY, user); return "redirect:/index.jsp"; - } else { - return "redirect:/401.jsp"; } + return "redirect:/401.jsp"; } } diff --git a/app/src/main/java/com/techcourse/controller/LoginViewController.java b/app/src/main/java/com/techcourse/controller/LoginViewController.java index 85d64a73f8..86ec26cdce 100644 --- a/app/src/main/java/com/techcourse/controller/LoginViewController.java +++ b/app/src/main/java/com/techcourse/controller/LoginViewController.java @@ -2,9 +2,9 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.controller.asis.Controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; public class LoginViewController implements Controller { diff --git a/app/src/main/java/com/techcourse/controller/LogoutController.java b/app/src/main/java/com/techcourse/controller/LogoutController.java index 9d1f099a98..4642fd9450 100644 --- a/app/src/main/java/com/techcourse/controller/LogoutController.java +++ b/app/src/main/java/com/techcourse/controller/LogoutController.java @@ -2,7 +2,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.controller.asis.Controller; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; public class LogoutController implements Controller { diff --git a/app/src/main/java/com/techcourse/controller/RegisterController.java b/app/src/main/java/com/techcourse/controller/RegisterController.java index 56bb436f6e..da62e5e8e9 100644 --- a/app/src/main/java/com/techcourse/controller/RegisterController.java +++ b/app/src/main/java/com/techcourse/controller/RegisterController.java @@ -4,7 +4,7 @@ import com.techcourse.repository.InMemoryUserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.controller.asis.Controller; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; public class RegisterController implements Controller { diff --git a/app/src/main/java/com/techcourse/controller/RegisterViewController.java b/app/src/main/java/com/techcourse/controller/RegisterViewController.java index 052639134b..136962136d 100644 --- a/app/src/main/java/com/techcourse/controller/RegisterViewController.java +++ b/app/src/main/java/com/techcourse/controller/RegisterViewController.java @@ -2,7 +2,7 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.controller.asis.Controller; +import webmvc.org.springframework.web.servlet.mvc.asis.Controller; public class RegisterViewController implements Controller { diff --git a/app/src/test/java/com/techcourse/domain/UserTest.java b/app/src/test/java/com/techcourse/domain/UserTest.java new file mode 100644 index 0000000000..0a3205e406 --- /dev/null +++ b/app/src/test/java/com/techcourse/domain/UserTest.java @@ -0,0 +1,21 @@ +package com.techcourse.domain; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class UserTest { + + @Test + void 비밀번호를_확인한다() { + // given + final User user = new User(1L, "hello", "pw", "my@google.com"); + + // expect + assertThat(user.checkPassword("pw")).isTrue(); + } +} diff --git a/build.gradle b/build.gradle index fd90bc151f..79cba97a1b 100644 --- a/build.gradle +++ b/build.gradle @@ -4,6 +4,12 @@ plugins { id "org.sonarqube" version "4.2.1.3168" } +allprojects { + repositories { + mavenCentral() + } +} + subprojects { apply plugin: 'org.sonarqube' sonar { diff --git a/mvc/build.gradle b/mvc/build.gradle index 8cdd011952..001ba455f9 100644 --- a/mvc/build.gradle +++ b/mvc/build.gradle @@ -26,6 +26,7 @@ dependencies { testImplementation "org.assertj:assertj-core:3.24.2" testImplementation "org.junit.jupiter:junit-jupiter-api:5.7.2" + testImplementation "org.junit.jupiter:junit-jupiter-params:5.7.2" testImplementation "org.mockito:mockito-core:5.4.0" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.7.2" } diff --git a/mvc/src/main/java/nextstep/web/annotation/Controller.java b/mvc/src/main/java/context/org/springframework/stereotype/Controller.java similarity index 87% rename from mvc/src/main/java/nextstep/web/annotation/Controller.java rename to mvc/src/main/java/context/org/springframework/stereotype/Controller.java index cb264235b4..ef7379b54a 100644 --- a/mvc/src/main/java/nextstep/web/annotation/Controller.java +++ b/mvc/src/main/java/context/org/springframework/stereotype/Controller.java @@ -1,4 +1,4 @@ -package nextstep.web.annotation; +package context.org.springframework.stereotype; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java b/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java new file mode 100644 index 0000000000..e89c1743f3 --- /dev/null +++ b/mvc/src/main/java/core/org/springframework/util/ReflectionUtils.java @@ -0,0 +1,38 @@ +package core.org.springframework.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Modifier; + +public abstract class ReflectionUtils { + + /** + * Obtain an accessible constructor for the given class and parameters. + * @param clazz the clazz to check + * @param parameterTypes the parameter types of the desired constructor + * @return the constructor reference + * @throws NoSuchMethodException if no such constructor exists + * @since 5.0 + */ + public static Constructor accessibleConstructor(Class clazz, Class... parameterTypes) + throws NoSuchMethodException { + + Constructor ctor = clazz.getDeclaredConstructor(parameterTypes); + makeAccessible(ctor); + return ctor; + } + + /** + * Make the given constructor accessible, explicitly setting it accessible + * if necessary. The {@code setAccessible(true)} method is only called + * when actually necessary, to avoid unnecessary conflicts. + * @param ctor the constructor to make accessible + * @see Constructor#setAccessible + */ + @SuppressWarnings("deprecation") + public static void makeAccessible(Constructor ctor) { + if ((!Modifier.isPublic(ctor.getModifiers()) || + !Modifier.isPublic(ctor.getDeclaringClass().getModifiers())) && !ctor.isAccessible()) { + ctor.setAccessible(true); + } + } +} diff --git a/mvc/src/main/java/nextstep/mvc/HandlerAdapter.java b/mvc/src/main/java/nextstep/mvc/HandlerAdapter.java deleted file mode 100644 index b1666cb54e..0000000000 --- a/mvc/src/main/java/nextstep/mvc/HandlerAdapter.java +++ /dev/null @@ -1,12 +0,0 @@ -package nextstep.mvc; - -import nextstep.mvc.view.ModelAndView; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -public interface HandlerAdapter { - boolean supports(Object handler); - - ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; -} diff --git a/mvc/src/main/java/nextstep/mvc/HandlerMapping.java b/mvc/src/main/java/nextstep/mvc/HandlerMapping.java deleted file mode 100644 index 7da7184209..0000000000 --- a/mvc/src/main/java/nextstep/mvc/HandlerMapping.java +++ /dev/null @@ -1,10 +0,0 @@ -package nextstep.mvc; - -import jakarta.servlet.http.HttpServletRequest; - -public interface HandlerMapping { - - void initialize(); - - Object getHandler(HttpServletRequest request); -} diff --git a/mvc/src/main/java/nextstep/mvc/controller/tobe/AnnotationHandlerMapping.java b/mvc/src/main/java/nextstep/mvc/controller/tobe/AnnotationHandlerMapping.java deleted file mode 100644 index cc1331de49..0000000000 --- a/mvc/src/main/java/nextstep/mvc/controller/tobe/AnnotationHandlerMapping.java +++ /dev/null @@ -1,30 +0,0 @@ -package nextstep.mvc.controller.tobe; - -import jakarta.servlet.http.HttpServletRequest; -import nextstep.mvc.HandlerMapping; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.HashMap; -import java.util.Map; - -public class AnnotationHandlerMapping implements HandlerMapping { - - private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); - - private final Object[] basePackage; - private final Map handlerExecutions; - - public AnnotationHandlerMapping(final Object... basePackage) { - this.basePackage = basePackage; - this.handlerExecutions = new HashMap<>(); - } - - public void initialize() { - log.info("Initialized AnnotationHandlerMapping!"); - } - - public Object getHandler(final HttpServletRequest request) { - return null; - } -} diff --git a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerExecution.java b/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerExecution.java deleted file mode 100644 index ebdc3d9b10..0000000000 --- a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerExecution.java +++ /dev/null @@ -1,12 +0,0 @@ -package nextstep.mvc.controller.tobe; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.view.ModelAndView; - -public class HandlerExecution { - - public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { - return null; - } -} diff --git a/mvc/src/main/java/nextstep/web/support/MediaType.java b/mvc/src/main/java/web/org/springframework/http/MediaType.java similarity index 76% rename from mvc/src/main/java/nextstep/web/support/MediaType.java rename to mvc/src/main/java/web/org/springframework/http/MediaType.java index f881e02174..3a31f51d33 100644 --- a/mvc/src/main/java/nextstep/web/support/MediaType.java +++ b/mvc/src/main/java/web/org/springframework/http/MediaType.java @@ -1,4 +1,4 @@ -package nextstep.web.support; +package web.org.springframework.http; public class MediaType { public static final String APPLICATION_JSON_UTF8_VALUE = "application/json;charset=UTF-8"; diff --git a/mvc/src/main/java/nextstep/web/NextstepServletContainerInitializer.java b/mvc/src/main/java/web/org/springframework/web/SpringServletContainerInitializer.java similarity index 73% rename from mvc/src/main/java/nextstep/web/NextstepServletContainerInitializer.java rename to mvc/src/main/java/web/org/springframework/web/SpringServletContainerInitializer.java index 3e79b34b6a..f1c13fcd53 100644 --- a/mvc/src/main/java/nextstep/web/NextstepServletContainerInitializer.java +++ b/mvc/src/main/java/web/org/springframework/web/SpringServletContainerInitializer.java @@ -1,28 +1,30 @@ -package nextstep.web; +package web.org.springframework.web; +import core.org.springframework.util.ReflectionUtils; import jakarta.servlet.ServletContainerInitializer; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.HandlesTypes; -import java.util.LinkedList; +import java.util.ArrayList; import java.util.List; import java.util.Set; @HandlesTypes(WebApplicationInitializer.class) -public class NextstepServletContainerInitializer implements ServletContainerInitializer { +public class SpringServletContainerInitializer implements ServletContainerInitializer { @Override public void onStartup(Set> webAppInitializerClasses, ServletContext servletContext) throws ServletException { - final List initializers = new LinkedList<>(); + final List initializers = new ArrayList<>(); if (webAppInitializerClasses != null) { for (Class waiClass : webAppInitializerClasses) { try { - initializers.add((WebApplicationInitializer) waiClass.getDeclaredConstructor().newInstance()); - } catch (Throwable e) { - throw new ServletException("Failed to instantiate WebApplicationInitializer class", e); + initializers.add((WebApplicationInitializer) + ReflectionUtils.accessibleConstructor(waiClass).newInstance()); + } catch (Throwable ex) { + throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex); } } } diff --git a/mvc/src/main/java/nextstep/web/WebApplicationInitializer.java b/mvc/src/main/java/web/org/springframework/web/WebApplicationInitializer.java similarity index 84% rename from mvc/src/main/java/nextstep/web/WebApplicationInitializer.java rename to mvc/src/main/java/web/org/springframework/web/WebApplicationInitializer.java index 60e6a89543..4b9b7d893d 100644 --- a/mvc/src/main/java/nextstep/web/WebApplicationInitializer.java +++ b/mvc/src/main/java/web/org/springframework/web/WebApplicationInitializer.java @@ -1,4 +1,4 @@ -package nextstep.web; +package web.org.springframework.web; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; diff --git a/mvc/src/main/java/web/org/springframework/web/bind/annotation/DeleteMapping.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/DeleteMapping.java new file mode 100644 index 0000000000..d82659bed0 --- /dev/null +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/DeleteMapping.java @@ -0,0 +1,14 @@ +package web.org.springframework.web.bind.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@RequestMapping(method = RequestMethod.DELETE) +public @interface DeleteMapping { + + String value() default ""; +} diff --git a/mvc/src/main/java/web/org/springframework/web/bind/annotation/GetMapping.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/GetMapping.java new file mode 100644 index 0000000000..ca4846798e --- /dev/null +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/GetMapping.java @@ -0,0 +1,14 @@ +package web.org.springframework.web.bind.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@RequestMapping(method = RequestMethod.GET) +public @interface GetMapping { + + String value() default ""; +} diff --git a/mvc/src/main/java/web/org/springframework/web/bind/annotation/HttpMappings.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/HttpMappings.java new file mode 100644 index 0000000000..2cad659d7c --- /dev/null +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/HttpMappings.java @@ -0,0 +1,24 @@ +package web.org.springframework.web.bind.annotation; + +import java.lang.annotation.Annotation; +import java.util.Arrays; + +public enum HttpMappings { + GET(GetMapping.class), + POST(PostMapping.class), + PUT(PutMapping.class), + PATCH(PatchMapping.class), + DELETE(DeleteMapping.class), + DEFAULT(RequestMapping.class); + + private final Class annotationType; + + HttpMappings(final Class annotationType) { + this.annotationType = annotationType; + } + + public static boolean isAnyMatch(final Annotation annotation) { + return Arrays.stream(values()) + .anyMatch(value -> value.annotationType.equals(annotation.annotationType())); + } +} diff --git a/mvc/src/main/java/web/org/springframework/web/bind/annotation/PatchMapping.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PatchMapping.java new file mode 100644 index 0000000000..671ee82c62 --- /dev/null +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PatchMapping.java @@ -0,0 +1,14 @@ +package web.org.springframework.web.bind.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@RequestMapping(method = RequestMethod.PATCH) +public @interface PatchMapping { + + String value() default ""; +} diff --git a/mvc/src/main/java/nextstep/web/annotation/PathVariable.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PathVariable.java similarity index 82% rename from mvc/src/main/java/nextstep/web/annotation/PathVariable.java rename to mvc/src/main/java/web/org/springframework/web/bind/annotation/PathVariable.java index 4f2a9b50a5..ceefd548e1 100644 --- a/mvc/src/main/java/nextstep/web/annotation/PathVariable.java +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PathVariable.java @@ -1,4 +1,4 @@ -package nextstep.web.annotation; +package web.org.springframework.web.bind.annotation; import java.lang.annotation.*; diff --git a/mvc/src/main/java/web/org/springframework/web/bind/annotation/PostMapping.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PostMapping.java new file mode 100644 index 0000000000..8135c4407d --- /dev/null +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PostMapping.java @@ -0,0 +1,14 @@ +package web.org.springframework.web.bind.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@RequestMapping(method = RequestMethod.POST) +public @interface PostMapping { + + String value() default ""; +} diff --git a/mvc/src/main/java/web/org/springframework/web/bind/annotation/PutMapping.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PutMapping.java new file mode 100644 index 0000000000..b9b589166e --- /dev/null +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/PutMapping.java @@ -0,0 +1,14 @@ +package web.org.springframework.web.bind.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@RequestMapping(method = RequestMethod.PUT) +public @interface PutMapping { + + String value() default ""; +} diff --git a/mvc/src/main/java/nextstep/web/annotation/RequestMapping.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMapping.java similarity index 82% rename from mvc/src/main/java/nextstep/web/annotation/RequestMapping.java rename to mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMapping.java index bb8c9e6e7b..6a09dfa0c4 100644 --- a/mvc/src/main/java/nextstep/web/annotation/RequestMapping.java +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMapping.java @@ -1,6 +1,4 @@ -package nextstep.web.annotation; - -import nextstep.web.support.RequestMethod; +package web.org.springframework.web.bind.annotation; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/mvc/src/main/java/nextstep/web/support/RequestMethod.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java similarity index 62% rename from mvc/src/main/java/nextstep/web/support/RequestMethod.java rename to mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java index 1f37f21d5a..1dd958bd23 100644 --- a/mvc/src/main/java/nextstep/web/support/RequestMethod.java +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestMethod.java @@ -1,4 +1,4 @@ -package nextstep.web.support; +package web.org.springframework.web.bind.annotation; public enum RequestMethod { GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE diff --git a/mvc/src/main/java/nextstep/web/annotation/RequestParam.java b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestParam.java similarity index 82% rename from mvc/src/main/java/nextstep/web/annotation/RequestParam.java rename to mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestParam.java index 2813247c4a..5b3be4cedc 100644 --- a/mvc/src/main/java/nextstep/web/annotation/RequestParam.java +++ b/mvc/src/main/java/web/org/springframework/web/bind/annotation/RequestParam.java @@ -1,4 +1,4 @@ -package nextstep.web.annotation; +package web.org.springframework.web.bind.annotation; import java.lang.annotation.*; diff --git a/mvc/src/main/java/nextstep/mvc/view/ModelAndView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java similarity index 93% rename from mvc/src/main/java/nextstep/mvc/view/ModelAndView.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java index cb172084b3..ff8e24553f 100644 --- a/mvc/src/main/java/nextstep/mvc/view/ModelAndView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/ModelAndView.java @@ -1,4 +1,4 @@ -package nextstep.mvc.view; +package webmvc.org.springframework.web.servlet; import java.util.Collections; import java.util.HashMap; diff --git a/mvc/src/main/java/nextstep/mvc/view/View.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java similarity index 84% rename from mvc/src/main/java/nextstep/mvc/view/View.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java index 493ed75c64..4499f36866 100644 --- a/mvc/src/main/java/nextstep/mvc/view/View.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/View.java @@ -1,4 +1,4 @@ -package nextstep.mvc.view; +package webmvc.org.springframework.web.servlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/mvc/src/main/java/nextstep/mvc/controller/asis/Controller.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/Controller.java similarity index 80% rename from mvc/src/main/java/nextstep/mvc/controller/asis/Controller.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/Controller.java index b0edabf3fb..bdd1fde780 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/asis/Controller.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/Controller.java @@ -1,4 +1,4 @@ -package nextstep.mvc.controller.asis; +package webmvc.org.springframework.web.servlet.mvc.asis; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; diff --git a/mvc/src/main/java/nextstep/mvc/controller/asis/ForwardController.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ForwardController.java similarity index 89% rename from mvc/src/main/java/nextstep/mvc/controller/asis/ForwardController.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ForwardController.java index ed0f08d940..cd8f1ef371 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/asis/ForwardController.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/asis/ForwardController.java @@ -1,4 +1,4 @@ -package nextstep.mvc.controller.asis; +package webmvc.org.springframework.web.servlet.mvc.asis; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; 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 new file mode 100644 index 0000000000..cfe89b1fa1 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMapping.java @@ -0,0 +1,66 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import static java.util.stream.Collectors.toSet; + +import jakarta.servlet.http.HttpServletRequest; +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.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 { + + private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); + + private final Map handlerExecutions; + private final ControllerScanner controllerScanner; + private final HandlerKeyGenerator handlerKeyGenerator; + + public AnnotationHandlerMapping(final Object... basePackage) { + this.handlerExecutions = new HashMap<>(); + this.controllerScanner = new ControllerScanner(basePackage); + this.handlerKeyGenerator = new HandlerKeyGenerator(new HttpMappingExtractor()); + } + + public void initialize() { + final Map, Object> controllers = controllerScanner.getControllers(); + final Map, String> uriPrefixes = controllerScanner.getUriPrefixes(); + final Set methods = toHttpMappingMethods(controllers.keySet()); + + for (final Method method : methods) { + final String prefix = uriPrefixes.get(method.getDeclaringClass()); + final Object instance = controllers.get(method.getDeclaringClass()); + final List handlerKeys = handlerKeyGenerator.generate(prefix, method); + final HandlerExecution handlerExecution = new HandlerExecution(instance, method); + handlerKeys.forEach(handlerKey -> handlerExecutions.put(handlerKey, handlerExecution)); + } + + log.info("Initialized AnnotationHandlerMapping!"); + } + + public Set toHttpMappingMethods(final Set> types) { + return types.stream() + .flatMap(type -> Arrays.stream(type.getDeclaredMethods())) + .filter(this::isHttpMappingAnnotationPresent) + .collect(toSet()); + } + + private boolean isHttpMappingAnnotationPresent(final Method method) { + final boolean isHttpMappingAnnotationPresent = Arrays.stream(method.getDeclaredAnnotations()) + .anyMatch(annotation -> annotation.annotationType().isAnnotationPresent(RequestMapping.class)); + return method.isAnnotationPresent(RequestMapping.class) || isHttpMappingAnnotationPresent; + } + + public Object getHandler(final HttpServletRequest request) { + final RequestMethod requestMethod = RequestMethod.valueOf(request.getMethod()); + final HandlerKey handlerKey = new HandlerKey(request.getRequestURI(), requestMethod); + + return handlerExecutions.get(handlerKey); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScanner.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScanner.java new file mode 100644 index 0000000000..9d78690628 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScanner.java @@ -0,0 +1,55 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.toMap; + +import context.org.springframework.stereotype.Controller; +import java.lang.reflect.Constructor; +import java.util.Map; +import java.util.Set; +import org.reflections.Reflections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import web.org.springframework.web.bind.annotation.RequestMapping; + +public class ControllerScanner { + + private static final Logger log = LoggerFactory.getLogger(ControllerScanner.class); + private static final String EMPTY = ""; + + private final Reflections reflections; + + public ControllerScanner(final Object... basePackage) { + this.reflections = new Reflections(basePackage); + } + + public Map, Object> getControllers() { + final Set> types = reflections.getTypesAnnotatedWith(Controller.class); + return types.stream() + .collect(toMap(identity(), this::instantiate)); + } + + private Object instantiate(final Class type) { + try { + final Constructor declaredConstructor = type.getDeclaredConstructor(); + return declaredConstructor.newInstance(); + } catch (final Exception e) { + log.error("Instantiate Failed!", e); + throw new IllegalArgumentException("Instantiate Failed"); + } + } + + public Map, String> getUriPrefixes() { + final Set> types = reflections.getTypesAnnotatedWith(Controller.class); + return types.stream() + .collect(toMap(identity(), this::parseUri)); + } + + private String parseUri(Class type) { + if (!type.isAnnotationPresent(RequestMapping.class)) { + return EMPTY; + } + final RequestMapping requestMapping = type.getDeclaredAnnotation(RequestMapping.class); + return requestMapping.value(); + } +} 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 new file mode 100644 index 0000000000..41a9df7710 --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecution.java @@ -0,0 +1,21 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +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 controller; + private final Method method; + + public HandlerExecution(final Object controller, final Method method) { + this.controller = controller; + this.method = method; + } + + public ModelAndView handle(final HttpServletRequest request, final HttpServletResponse response) throws Exception { + return (ModelAndView) method.invoke(controller, request, response); + } +} diff --git a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerKey.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKey.java similarity index 74% rename from mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerKey.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKey.java index b6934407a1..97e9479e03 100644 --- a/mvc/src/main/java/nextstep/mvc/controller/tobe/HandlerKey.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKey.java @@ -1,8 +1,7 @@ -package nextstep.mvc.controller.tobe; - -import nextstep.web.support.RequestMethod; +package webmvc.org.springframework.web.servlet.mvc.tobe; import java.util.Objects; +import web.org.springframework.web.bind.annotation.RequestMethod; public class HandlerKey { @@ -24,8 +23,12 @@ public String toString() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof HandlerKey)) return false; + if (this == o) { + return true; + } + if (!(o instanceof HandlerKey)) { + return false; + } HandlerKey that = (HandlerKey) o; return Objects.equals(url, that.url) && requestMethod == that.requestMethod; } diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKeyGenerator.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKeyGenerator.java new file mode 100644 index 0000000000..f8d5b3c29b --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKeyGenerator.java @@ -0,0 +1,35 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import static java.util.stream.Collectors.toList; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; +import web.org.springframework.web.bind.annotation.HttpMappings; +import web.org.springframework.web.bind.annotation.RequestMethod; + +public class HandlerKeyGenerator { + + private final HttpMappingExtractor httpMappingExtractor; + + public HandlerKeyGenerator(final HttpMappingExtractor httpMappingExtractor) { + this.httpMappingExtractor = httpMappingExtractor; + } + + public List generate(final String prefix, final Method method) { + final Annotation annotation = getHttpMappingAnnotation(method); + final String uri = httpMappingExtractor.extractRequestUri(annotation); + final RequestMethod[] requestMethods = httpMappingExtractor.extractRequestMethod(annotation); + return Arrays.stream(requestMethods) + .map(requestMethod -> new HandlerKey(prefix + uri, requestMethod)) + .collect(toList()); + } + + private Annotation getHttpMappingAnnotation(final Method method) { + return Arrays.stream(method.getDeclaredAnnotations()) + .filter(HttpMappings::isAnyMatch) + .findFirst() + .orElseThrow(); + } +} diff --git a/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HttpMappingExtractor.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HttpMappingExtractor.java new file mode 100644 index 0000000000..50b119b5fe --- /dev/null +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/mvc/tobe/HttpMappingExtractor.java @@ -0,0 +1,30 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; + +public class HttpMappingExtractor { + + private static final String URI_FIELD_NAME = "value"; + + public String extractRequestUri(final Annotation annotation) { + final Class annotationType = annotation.annotationType(); + try { + final Method method = annotationType.getDeclaredMethod(URI_FIELD_NAME); + return (String) method.invoke(annotation); + } catch (final Exception e) { + throw new IllegalArgumentException("Invoke Failed!"); + } + } + + public RequestMethod[] extractRequestMethod(final Annotation annotation) { + if (annotation instanceof RequestMapping) { + return ((RequestMapping) annotation).method(); + } + return annotation.annotationType() + .getDeclaredAnnotation(RequestMapping.class) + .method(); + } +} diff --git a/mvc/src/main/java/nextstep/mvc/view/JsonView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java similarity index 75% rename from mvc/src/main/java/nextstep/mvc/view/JsonView.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java index b7c4c06a71..b42c3466f0 100644 --- a/mvc/src/main/java/nextstep/mvc/view/JsonView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JsonView.java @@ -1,7 +1,8 @@ -package nextstep.mvc.view; +package webmvc.org.springframework.web.servlet.view; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import webmvc.org.springframework.web.servlet.View; import java.util.Map; diff --git a/mvc/src/main/java/nextstep/mvc/view/JspView.java b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java similarity index 88% rename from mvc/src/main/java/nextstep/mvc/view/JspView.java rename to mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java index 858c3ccece..3f4cc906ff 100644 --- a/mvc/src/main/java/nextstep/mvc/view/JspView.java +++ b/mvc/src/main/java/webmvc/org/springframework/web/servlet/view/JspView.java @@ -1,9 +1,10 @@ -package nextstep.mvc.view; +package webmvc.org.springframework.web.servlet.view; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import webmvc.org.springframework.web.servlet.View; import java.util.Map; diff --git a/mvc/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/mvc/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer index b9002355ee..d98fc63525 100644 --- a/mvc/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer +++ b/mvc/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer @@ -1 +1 @@ -nextstep.web.NextstepServletContainerInitializer \ No newline at end of file +web.org.springframework.web.SpringServletContainerInitializer diff --git a/mvc/src/test/java/samples/TestController.java b/mvc/src/test/java/samples/TestController.java index 49d81be351..de49ce74f5 100644 --- a/mvc/src/test/java/samples/TestController.java +++ b/mvc/src/test/java/samples/TestController.java @@ -1,21 +1,28 @@ package samples; +import static web.org.springframework.web.bind.annotation.RequestMethod.GET; + +import context.org.springframework.stereotype.Controller; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import nextstep.mvc.view.JspView; -import nextstep.mvc.view.ModelAndView; -import nextstep.web.annotation.Controller; -import nextstep.web.annotation.RequestMapping; -import nextstep.web.support.RequestMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import web.org.springframework.web.bind.annotation.DeleteMapping; +import web.org.springframework.web.bind.annotation.GetMapping; +import web.org.springframework.web.bind.annotation.PatchMapping; +import web.org.springframework.web.bind.annotation.PostMapping; +import web.org.springframework.web.bind.annotation.PutMapping; +import web.org.springframework.web.bind.annotation.RequestMapping; +import webmvc.org.springframework.web.servlet.ModelAndView; +import webmvc.org.springframework.web.servlet.view.JspView; +@RequestMapping("/test") @Controller public class TestController { private static final Logger log = LoggerFactory.getLogger(TestController.class); - @RequestMapping(value = "/get-test", method = RequestMethod.GET) + @RequestMapping(value = "/request", method = GET) public ModelAndView findUserId(final HttpServletRequest request, final HttpServletResponse response) { log.info("test controller get method"); final var modelAndView = new ModelAndView(new JspView("")); @@ -23,11 +30,43 @@ public ModelAndView findUserId(final HttpServletRequest request, final HttpServl return modelAndView; } - @RequestMapping(value = "/post-test", method = RequestMethod.POST) + @GetMapping("/get") + public ModelAndView find(final HttpServletRequest request, final HttpServletResponse response) { + log.info("test controller get method"); + final var modelAndView = new ModelAndView(new JspView("")); + modelAndView.addObject("id", request.getAttribute("id")); + return modelAndView; + } + + @PostMapping("/post") public ModelAndView save(final HttpServletRequest request, final HttpServletResponse response) { log.info("test controller post method"); final var modelAndView = new ModelAndView(new JspView("")); modelAndView.addObject("id", request.getAttribute("id")); return modelAndView; } + + @PatchMapping("/patch") + public ModelAndView patch(final HttpServletRequest request, final HttpServletResponse response) { + log.info("test controller post method"); + final var modelAndView = new ModelAndView(new JspView("")); + modelAndView.addObject("id", request.getAttribute("id")); + return modelAndView; + } + + @PutMapping("/put") + public ModelAndView put(final HttpServletRequest request, final HttpServletResponse response) { + log.info("test controller post method"); + final var modelAndView = new ModelAndView(new JspView("")); + modelAndView.addObject("id", request.getAttribute("id")); + return modelAndView; + } + + @DeleteMapping("/delete") + public ModelAndView delete(final HttpServletRequest request, final HttpServletResponse response) { + log.info("test controller post method"); + final var modelAndView = new ModelAndView(new JspView("")); + modelAndView.addObject("id", request.getAttribute("id")); + return modelAndView; + } } diff --git a/mvc/src/test/java/web/org/springframework/web/bind/annotation/HttpMappingsTest.java b/mvc/src/test/java/web/org/springframework/web/bind/annotation/HttpMappingsTest.java new file mode 100644 index 0000000000..e0f8f75327 --- /dev/null +++ b/mvc/src/test/java/web/org/springframework/web/bind/annotation/HttpMappingsTest.java @@ -0,0 +1,33 @@ +package web.org.springframework.web.bind.annotation; + +import static org.assertj.core.api.Assertions.assertThat; +import static web.org.springframework.web.bind.annotation.HttpMappings.isAnyMatch; + +import context.org.springframework.stereotype.Controller; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import samples.TestController; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpMappingsTest { + + @Test + void 입력받은_애너테이션이_매핑_애너테이션이라면_true를_반환한다() { + // given + final RequestMapping annotation = TestController.class.getDeclaredAnnotation(RequestMapping.class); + + // expect + assertThat(isAnyMatch(annotation)).isTrue(); + } + + @Test + void 입력받은_애너테이션이_매핑_애너테이션이_아니라면_false를_반환한다() { + // given + final Controller annotation = TestController.class.getDeclaredAnnotation(Controller.class); + + // expect + assertThat(isAnyMatch(annotation)).isFalse(); + } +} diff --git a/mvc/src/test/java/nextstep/mvc/controller/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java similarity index 52% rename from mvc/src/test/java/nextstep/mvc/controller/tobe/AnnotationHandlerMappingTest.java rename to mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java index 3236c5c433..4210a3c858 100644 --- a/mvc/src/test/java/nextstep/mvc/controller/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/AnnotationHandlerMappingTest.java @@ -1,47 +1,34 @@ -package nextstep.mvc.controller.tobe; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +package webmvc.org.springframework.web.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.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + class AnnotationHandlerMappingTest { private AnnotationHandlerMapping handlerMapping; @BeforeEach - void setUp() { + void setUp() throws Exception { handlerMapping = new AnnotationHandlerMapping("samples"); handlerMapping.initialize(); } - @Test - void get() throws Exception { - final var request = mock(HttpServletRequest.class); - final var 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); - - assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); - } - - @Test - void post() throws Exception { + @CsvSource({"/request, GET", "/get, GET", "/post, POST", "/patch, PATCH", "/put, PUT", "/delete, DELETE"}) + @ParameterizedTest + void handlerMapping이_정상적으로_동작한다(final String uri, final String 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("/post-test"); - when(request.getMethod()).thenReturn("POST"); + when(request.getRequestURI()).thenReturn("/test" + uri); + when(request.getMethod()).thenReturn(method); final var handlerExecution = (HandlerExecution) handlerMapping.getHandler(request); final var modelAndView = handlerExecution.handle(request, response); diff --git a/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScannerTest.java b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScannerTest.java new file mode 100644 index 0000000000..db38868b3a --- /dev/null +++ b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/ControllerScannerTest.java @@ -0,0 +1,40 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Map; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import samples.TestController; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class ControllerScannerTest { + + @Test + void 패키지_내부의_컨트롤러_애너테이션이_적용된_클래스를_전부_반환한다() { + // given + final String basePackage = "samples"; + final ControllerScanner controllerScanner = new ControllerScanner(basePackage); + + // when + final Map, Object> result = controllerScanner.getControllers(); + + // then + assertThat(result).hasSize(1); + } + + @Test + void 패키지_내부의_컨트롤러_애너테이션이_적용된_클래스의_타입과_RequestMapping_Uri로_구성된_맵을_반환한다() { + // given + final String basePackage = "samples"; + final ControllerScanner controllerScanner = new ControllerScanner(basePackage); + + // when + final Map, String> uriPrefixes = controllerScanner.getUriPrefixes(); + + // then + assertThat(uriPrefixes).containsEntry(TestController.class, "/test"); + } +} diff --git a/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecutionTest.java b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecutionTest.java new file mode 100644 index 0000000000..be34f21366 --- /dev/null +++ b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerExecutionTest.java @@ -0,0 +1,44 @@ +package webmvc.org.springframework.web.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 java.lang.reflect.Method; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import samples.TestController; +import webmvc.org.springframework.web.servlet.ModelAndView; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HandlerExecutionTest { + + @Test + void handle_메서드_실행() throws Exception { + // given + final var request = mock(HttpServletRequest.class); + final var response = mock(HttpServletResponse.class); + + when(request.getAttribute("id")).thenReturn("gugu"); + when(request.getRequestURI()).thenReturn("/test/request"); + when(request.getMethod()).thenReturn("GET"); + + final Method method = TestController.class.getDeclaredMethod( + "find", + HttpServletRequest.class, + HttpServletResponse.class + ); + + final HandlerExecution handlerExecution = new HandlerExecution(new TestController(), method); + + // when + final ModelAndView modelAndView = handlerExecution.handle(request, response); + + // then + assertThat(modelAndView.getObject("id")).isEqualTo("gugu"); + } +} diff --git a/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKeyGeneratorTest.java b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKeyGeneratorTest.java new file mode 100644 index 0000000000..51c33576e4 --- /dev/null +++ b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/HandlerKeyGeneratorTest.java @@ -0,0 +1,36 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.List; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import samples.TestController; +import web.org.springframework.web.bind.annotation.RequestMethod; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HandlerKeyGeneratorTest { + + private final HandlerKeyGenerator handlerKeyGenerator = new HandlerKeyGenerator(new HttpMappingExtractor()); + + @Test + void prefix와_메서드를_입력받아_HandlerKey의_리스트를_반환한다() throws NoSuchMethodException { + // given + final Method method = TestController.class.getDeclaredMethod( + "find", + HttpServletRequest.class, + HttpServletResponse.class + ); + + // when + final List handlerKeys = handlerKeyGenerator.generate("/hello", method); + + // then + assertThat(handlerKeys).containsExactly(new HandlerKey("/hello/get", RequestMethod.GET)); + } +} diff --git a/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/HttpMappingExtractorTest.java b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/HttpMappingExtractorTest.java new file mode 100644 index 0000000000..8c492e62d0 --- /dev/null +++ b/mvc/src/test/java/webmvc/org/springframework/web/servlet/mvc/tobe/HttpMappingExtractorTest.java @@ -0,0 +1,70 @@ +package webmvc.org.springframework.web.servlet.mvc.tobe; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import org.junit.jupiter.api.DisplayNameGeneration; +import org.junit.jupiter.api.DisplayNameGenerator; +import org.junit.jupiter.api.Test; +import samples.TestController; +import web.org.springframework.web.bind.annotation.PostMapping; +import web.org.springframework.web.bind.annotation.RequestMapping; +import web.org.springframework.web.bind.annotation.RequestMethod; + +@SuppressWarnings("NonAsciiCharacters") +@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) +class HttpMappingExtractorTest { + + private final HttpMappingExtractor httpMappingExtractor = new HttpMappingExtractor(); + + @Test + void Annotation을_입력받아_RequestUri을_추출한다() throws NoSuchMethodException { + // given + final Method method = TestController.class.getDeclaredMethod("findUserId", HttpServletRequest.class, + HttpServletResponse.class); + final RequestMapping requestMapping = method.getDeclaredAnnotation(RequestMapping.class); + + // when + final String uri = httpMappingExtractor.extractRequestUri(requestMapping); + + // then + assertThat(uri).isEqualTo("/request"); + } + + @Test + void RequestMappingAnnotation을_입력받아_RequestUri을_추출한다() throws NoSuchMethodException { + // given + final Method method = TestController.class.getDeclaredMethod( + "findUserId", + HttpServletRequest.class, + HttpServletResponse.class + ); + final RequestMapping requestMapping = method.getDeclaredAnnotation(RequestMapping.class); + + // when + final RequestMethod[] requestMethods = httpMappingExtractor.extractRequestMethod(requestMapping); + + // then + assertThat(requestMethods).containsExactly(RequestMethod.GET); + } + + @Test + void RequestMapping이_아닌_다른_HttpMappingAnnotation을_입력받는_경우_MetaAnnotation에_있는_RequestMapping의_정보를_추출한다() + throws NoSuchMethodException { + // given + final Method method = TestController.class.getDeclaredMethod( + "save", + HttpServletRequest.class, + HttpServletResponse.class + ); + final PostMapping postMapping = method.getDeclaredAnnotation(PostMapping.class); + + // when + final RequestMethod[] requestMethods = httpMappingExtractor.extractRequestMethod(postMapping); + + // then + assertThat(requestMethods).containsExactly(RequestMethod.POST); + } +} diff --git a/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java b/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java index cf4d886974..d638844302 100644 --- a/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java +++ b/study/src/main/java/servlet/com/example/CharacterEncodingFilter.java @@ -1,16 +1,21 @@ 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; @WebFilter("/*") public class CharacterEncodingFilter implements Filter { @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() 호출"); + response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); } } diff --git a/study/src/main/java/servlet/com/example/TomcatStarter.java b/study/src/main/java/servlet/com/example/TomcatStarter.java index 109b9d5297..b91214e9c3 100644 --- a/study/src/main/java/servlet/com/example/TomcatStarter.java +++ b/study/src/main/java/servlet/com/example/TomcatStarter.java @@ -44,6 +44,7 @@ public void await() { public void stop() { try { tomcat.stop(); + tomcat.destroy(); } catch (LifecycleException e) { throw new RuntimeException(e); } diff --git a/study/src/test/java/reflection/Junit3TestRunner.java b/study/src/test/java/reflection/Junit3TestRunner.java index b4e465240c..13f5613584 100644 --- a/study/src/test/java/reflection/Junit3TestRunner.java +++ b/study/src/test/java/reflection/Junit3TestRunner.java @@ -1,13 +1,23 @@ package reflection; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import org.junit.jupiter.api.Test; class Junit3TestRunner { @Test void run() throws Exception { - Class clazz = Junit3Test.class; + final Class clazz = Junit3Test.class; + final Constructor constructor = clazz.getDeclaredConstructor(); + final Junit3Test junit3Test = constructor.newInstance(); // TODO Junit3Test에서 test로 시작하는 메소드 실행 + for (Method method : clazz.getMethods()) { + final String name = method.getName(); + if (name.startsWith("test")) { + method.invoke(junit3Test); + } + } } } diff --git a/study/src/test/java/reflection/Junit4TestRunner.java b/study/src/test/java/reflection/Junit4TestRunner.java index 8a6916bc24..13e0542f8b 100644 --- a/study/src/test/java/reflection/Junit4TestRunner.java +++ b/study/src/test/java/reflection/Junit4TestRunner.java @@ -1,13 +1,23 @@ package reflection; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; import org.junit.jupiter.api.Test; class Junit4TestRunner { @Test void run() throws Exception { - Class clazz = Junit4Test.class; + final Class clazz = Junit4Test.class; + final Constructor constructor = clazz.getConstructor(); + final Junit4Test junit4Test = constructor.newInstance(); // TODO Junit4Test에서 @MyTest 애노테이션이 있는 메소드 실행 + for (Method method : clazz.getMethods()) { + if (method.getDeclaredAnnotation(MyTest.class) != null) { + method.invoke(junit4Test); + } + } } } diff --git a/study/src/test/java/reflection/ReflectionTest.java b/study/src/test/java/reflection/ReflectionTest.java index 370f0932b9..cc4cc4f572 100644 --- a/study/src/test/java/reflection/ReflectionTest.java +++ b/study/src/test/java/reflection/ReflectionTest.java @@ -1,5 +1,10 @@ package reflection; +import java.sql.Date; +import java.time.Instant; +import java.util.Arrays; +import java.util.stream.Collectors; +import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -9,6 +14,7 @@ import java.lang.reflect.Method; import java.util.List; +import static java.util.stream.Collectors.*; import static org.assertj.core.api.Assertions.assertThat; class ReflectionTest { @@ -19,25 +25,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) + .collect(toList()); assertThat(actualFieldNames).contains("name", "age"); } @@ -45,8 +53,10 @@ 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) + .collect(toList()); assertThat(actualMethods) .hasSize(3) @@ -56,7 +66,7 @@ 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); } @@ -65,11 +75,13 @@ 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.getConstructors()[0]; + final Constructor secondConstructor = questionClass.getConstructors()[1]; - final Question firstQuestion = null; - final Question secondQuestion = null; + final Question firstQuestion = (Question) firstConstructor.newInstance("gugu", "제목1", "내용1"); + final Question secondQuestion = (Question) secondConstructor.newInstance( + 1L, "gugu", "제목2", "내용2", Date.from(Instant.now()), 0 + ); assertThat(firstQuestion.getWriter()).isEqualTo("gugu"); assertThat(firstQuestion.getTitle()).isEqualTo("제목1"); @@ -82,7 +94,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 +102,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 +111,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 +119,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 +127,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.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..0c8d73a4dc 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.stream.Stream; 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,9 @@ void showAnnotationClass() throws Exception { Reflections reflections = new Reflections("reflection.examples"); // TODO 클래스 레벨에 @Controller, @Service, @Repository 애노테이션이 설정되어 모든 클래스 찾아 로그로 출력한다. + Stream.of(Controller.class, Service.class, Repository.class) + .flatMap(clazz -> reflections.getTypesAnnotatedWith(clazz).stream()) + .map(Class::getSimpleName) + .forEach(log::info); } } diff --git a/study/src/test/java/servlet/com/example/FilterTest.java b/study/src/test/java/servlet/com/example/FilterTest.java index 7c958af764..53d6236dff 100644 --- a/study/src/test/java/servlet/com/example/FilterTest.java +++ b/study/src/test/java/servlet/com/example/FilterTest.java @@ -1,6 +1,7 @@ package servlet.com.example; import org.junit.jupiter.api.Test; +import support.HttpUtils; import static org.assertj.core.api.Assertions.assertThat; import static servlet.com.example.KoreanServlet.인코딩; @@ -8,12 +9,12 @@ class FilterTest { @Test - void testFilter() throws Exception { + void testFilter() { // 톰캣 서버 시작 - final var tomcatStarter = TestHttpUtils.createTomcatStarter(); + final var tomcatStarter = new TomcatStarter("src/main/webapp/"); tomcatStarter.start(); - final var response = TestHttpUtils.send("/korean"); + final var response = HttpUtils.send("/korean"); // 톰캣 서버 종료 tomcatStarter.stop(); diff --git a/study/src/test/java/servlet/com/example/ServletTest.java b/study/src/test/java/servlet/com/example/ServletTest.java index 0cb371f4d0..c5453f6949 100644 --- a/study/src/test/java/servlet/com/example/ServletTest.java +++ b/study/src/test/java/servlet/com/example/ServletTest.java @@ -1,22 +1,25 @@ package servlet.com.example; -import org.junit.jupiter.api.Test; - import static org.assertj.core.api.Assertions.assertThat; +import org.junit.jupiter.api.Test; +import support.HttpUtils; + class ServletTest { + private final String WEBAPP_DIR_LOCATION = "src/main/webapp/"; + @Test - void testSharedCounter() throws Exception { + void testSharedCounter() { // 톰캣 서버 시작 - final var tomcatStarter = TestHttpUtils.createTomcatStarter(); + final var tomcatStarter = new TomcatStarter(WEBAPP_DIR_LOCATION); tomcatStarter.start(); // shared-counter 페이지를 3번 호출한다. final var PATH = "/shared-counter"; - TestHttpUtils.send(PATH); - TestHttpUtils.send(PATH); - final var response = TestHttpUtils.send(PATH); + HttpUtils.send(PATH); + HttpUtils.send(PATH); + final var response = HttpUtils.send(PATH); // 톰캣 서버 종료 tomcatStarter.stop(); @@ -25,20 +28,20 @@ void testSharedCounter() throws Exception { // expected를 0이 아닌 올바른 값으로 바꿔보자. // 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? - assertThat(Integer.parseInt(response.body())).isEqualTo(0); + assertThat(Integer.parseInt(response.body())).isEqualTo(3); } @Test - void testLocalCounter() throws Exception { + void testLocalCounter() { // 톰캣 서버 시작 - final var tomcatStarter = TestHttpUtils.createTomcatStarter(); + final var tomcatStarter = new TomcatStarter(WEBAPP_DIR_LOCATION); tomcatStarter.start(); // local-counter 페이지를 3번 호출한다. final var PATH = "/local-counter"; - TestHttpUtils.send(PATH); - TestHttpUtils.send(PATH); - final var response = TestHttpUtils.send(PATH); + HttpUtils.send(PATH); + HttpUtils.send(PATH); + final var response = HttpUtils.send(PATH); // 톰캣 서버 종료 tomcatStarter.stop(); @@ -47,6 +50,6 @@ void testLocalCounter() throws Exception { // expected를 0이 아닌 올바른 값으로 바꿔보자. // 예상한 결과가 나왔는가? 왜 이런 결과가 나왔을까? - assertThat(Integer.parseInt(response.body())).isEqualTo(0); + assertThat(Integer.parseInt(response.body())).isEqualTo(1); } } diff --git a/study/src/test/java/servlet/com/example/TestHttpUtils.java b/study/src/test/java/servlet/com/example/TestHttpUtils.java deleted file mode 100644 index dde039228f..0000000000 --- a/study/src/test/java/servlet/com/example/TestHttpUtils.java +++ /dev/null @@ -1,28 +0,0 @@ -package servlet.com.example; - -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; -import java.net.http.HttpResponse; -import java.time.Duration; - -public class TestHttpUtils { - - private static final HttpClient httpClient = HttpClient.newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .connectTimeout(Duration.ofSeconds(3)) - .build(); - - public static TomcatStarter createTomcatStarter() { - return new TomcatStarter("../servlet/src/main/webapp/"); - } - - public static HttpResponse send(final String path) throws Exception { - final var request = HttpRequest.newBuilder() - .uri(URI.create("http://localhost:8080" + path)) - .timeout(Duration.ofSeconds(3)) - .build(); - - return httpClient.send(request, HttpResponse.BodyHandlers.ofString()); - } -}