diff --git a/app/build.gradle b/app/build.gradle index 6ce583778d..b8b779a482 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,6 +22,7 @@ dependencies { implementation 'org.apache.tomcat.embed:tomcat-embed-jasper:10.1.25' implementation 'ch.qos.logback:logback-classic:1.5.7' implementation 'org.apache.commons:commons-lang3:3.14.0' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1' testImplementation 'org.assertj:assertj-core:3.26.0' testImplementation 'org.mockito:mockito-core:5.12.0' diff --git a/app/src/main/java/com/techcourse/controller/ForwardController.java b/app/src/main/java/com/techcourse/controller/ForwardController.java new file mode 100644 index 0000000000..3c9ba04154 --- /dev/null +++ b/app/src/main/java/com/techcourse/controller/ForwardController.java @@ -0,0 +1,19 @@ +package com.techcourse.controller; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JspView; + +@Controller +public class ForwardController { + + @RequestMapping(value = "/", method = RequestMethod.GET) + public ModelAndView index(HttpServletRequest request, HttpServletResponse response) { + return new ModelAndView(new JspView("/index.jsp")); + } +} diff --git a/app/src/main/java/com/techcourse/controller/LoginController.java b/app/src/main/java/com/techcourse/controller/LoginController.java index a87d996693..177bc2fe25 100644 --- a/app/src/main/java/com/techcourse/controller/LoginController.java +++ b/app/src/main/java/com/techcourse/controller/LoginController.java @@ -1,21 +1,28 @@ package com.techcourse.controller; -import com.techcourse.domain.User; -import com.techcourse.repository.InMemoryUserRepository; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import com.interface21.webmvc.servlet.mvc.asis.Controller; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class LoginController implements Controller { +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JspView; +import com.techcourse.domain.User; +import com.techcourse.repository.InMemoryUserRepository; + +@Controller +public class LoginController { private static final Logger log = LoggerFactory.getLogger(LoginController.class); - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { + @RequestMapping(value = "/login", method = RequestMethod.POST) + public ModelAndView save(HttpServletRequest req, HttpServletResponse res) { if (UserSession.isLoggedIn(req.getSession())) { - return "redirect:/index.jsp"; + return new ModelAndView(new JspView("redirect:/index.jsp")); } return InMemoryUserRepository.findByAccount(req.getParameter("account")) @@ -23,15 +30,26 @@ public String execute(final HttpServletRequest req, final HttpServletResponse re log.info("User : {}", user); return login(req, user); }) - .orElse("redirect:/401.jsp"); + .orElse(new ModelAndView(new JspView("redirect:/401.jsp"))); } - private String login(final HttpServletRequest request, final User user) { + private ModelAndView login(final HttpServletRequest request, final User user) { if (user.checkPassword(request.getParameter("password"))) { final var session = request.getSession(); session.setAttribute(UserSession.SESSION_KEY, user); - return "redirect:/index.jsp"; + return new ModelAndView(new JspView("redirect:/index.jsp")); } - return "redirect:/401.jsp"; + return new ModelAndView(new JspView("redirect:/401.jsp")); + } + + @RequestMapping(value = "/login/view", method = RequestMethod.GET) + public ModelAndView show(HttpServletRequest req, HttpServletResponse res) { + JspView view = UserSession.getUserFrom(req.getSession()) + .map(user -> { + log.info("logged in {}", user.getAccount()); + return new JspView("redirect:/index.jsp"); + }) + .orElse(new JspView("/login.jsp")); + return new ModelAndView(view); } } diff --git a/app/src/main/java/com/techcourse/controller/LoginViewController.java b/app/src/main/java/com/techcourse/controller/LoginViewController.java deleted file mode 100644 index dc71ee8e99..0000000000 --- a/app/src/main/java/com/techcourse/controller/LoginViewController.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.techcourse.controller; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.interface21.webmvc.servlet.mvc.asis.Controller; - -public class LoginViewController implements Controller { - - private static final Logger log = LoggerFactory.getLogger(LoginViewController.class); - - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { - return UserSession.getUserFrom(req.getSession()) - .map(user -> { - log.info("logged in {}", user.getAccount()); - return "redirect:/index.jsp"; - }) - .orElse("/login.jsp"); - } -} diff --git a/app/src/main/java/com/techcourse/controller/LogoutController.java b/app/src/main/java/com/techcourse/controller/LogoutController.java index 729d262de5..e95a4485ca 100644 --- a/app/src/main/java/com/techcourse/controller/LogoutController.java +++ b/app/src/main/java/com/techcourse/controller/LogoutController.java @@ -2,14 +2,20 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import com.interface21.webmvc.servlet.mvc.asis.Controller; -public class LogoutController implements Controller { +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JspView; - @Override - public String execute(final HttpServletRequest req, final HttpServletResponse res) throws Exception { +@Controller +public class LogoutController { + + @RequestMapping(value = "/logout", method = RequestMethod.GET) + public ModelAndView logout(final HttpServletRequest req, final HttpServletResponse res) throws Exception { final var session = req.getSession(); session.removeAttribute(UserSession.SESSION_KEY); - return "redirect:/"; + return new ModelAndView(new JspView("redirect:/")); } } diff --git a/app/src/main/java/com/techcourse/controller/UserController.java b/app/src/main/java/com/techcourse/controller/UserController.java new file mode 100644 index 0000000000..956a249a42 --- /dev/null +++ b/app/src/main/java/com/techcourse/controller/UserController.java @@ -0,0 +1,35 @@ +package com.techcourse.controller; + + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.interface21.context.stereotype.Controller; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.view.JsonView; +import com.techcourse.domain.User; +import com.techcourse.repository.InMemoryUserRepository; + +@Controller +public class UserController { + + private static final Logger log = LoggerFactory.getLogger(UserController.class); + + @RequestMapping(value = "/api/user", method = RequestMethod.GET) + public ModelAndView show(HttpServletRequest request, HttpServletResponse response) { + final String account = request.getParameter("account"); + log.debug("user id : {}", account); + + final ModelAndView modelAndView = new ModelAndView(new JsonView()); + final User user = InMemoryUserRepository.findByAccount(account) + .orElseThrow(); + + modelAndView.addObject("user", user); + return modelAndView; + } +} diff --git a/app/src/main/java/com/techcourse/framework/DispatcherServlet.java b/app/src/main/java/com/techcourse/framework/DispatcherServlet.java deleted file mode 100644 index 29475e4aa3..0000000000 --- a/app/src/main/java/com/techcourse/framework/DispatcherServlet.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.techcourse.framework; - -import java.util.List; -import java.util.Objects; - -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.interface21.webmvc.servlet.ModelAndView; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerAdapter; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerMappingAdapter; -import com.interface21.webmvc.servlet.mvc.tobe.NoMatchedHandlerException; -import com.interface21.webmvc.servlet.mvc.tobe.annotation.AnnotationHandlerMappingAdapter; -import com.techcourse.framework.ManualHandler.ManualHandlerMapping; -import com.techcourse.framework.ManualHandler.ManualHandlerMappingAdapter; - -public class DispatcherServlet extends HttpServlet { - - private static final long serialVersionUID = 1L; - private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); - - private List handlerMappingAdapters; - - public DispatcherServlet() { - } - - @Override - public void init() { - handlerMappingAdapters = List.of( - new AnnotationHandlerMappingAdapter("com.techcourse.controller"), - new ManualHandlerMappingAdapter(new ManualHandlerMapping()) - ); - for (HandlerMappingAdapter handlerMappingAdapter : handlerMappingAdapters) { - handlerMappingAdapter.initialize(); - } - } - - @Override - protected void service(final HttpServletRequest request, final HttpServletResponse response) throws ServletException { - final String requestURI = request.getRequestURI(); - log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI); - - try { - HandlerAdapter handlerAdapter = handlerMappingAdapters.stream() - .map(handlerMappingAdapter -> handlerMappingAdapter.getHandler(request)) - .filter(Objects::nonNull) - .findFirst() - .orElseThrow(() -> new NoMatchedHandlerException(request)); - ModelAndView modelAndView = handlerAdapter.handle(request, response); - modelAndView.getView().render(modelAndView.getModel(), request, response); - } catch (Throwable e) { - log.error("Exception : {}", e.getMessage(), e); - throw new ServletException(e.getMessage(), e); - } - } -} diff --git a/app/src/main/java/com/techcourse/framework/DispatcherServletInitializer.java b/app/src/main/java/com/techcourse/framework/DispatcherServletInitializer.java index e9f8344d4e..316500ef7d 100644 --- a/app/src/main/java/com/techcourse/framework/DispatcherServletInitializer.java +++ b/app/src/main/java/com/techcourse/framework/DispatcherServletInitializer.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.interface21.web.WebApplicationInitializer; +import com.interface21.webmvc.servlet.mvc.framework.DispatcherServlet; /** * Base class for {@link WebApplicationInitializer} @@ -17,7 +18,7 @@ public class DispatcherServletInitializer implements WebApplicationInitializer { @Override public void onStartup(final ServletContext servletContext) { - final var dispatcherServlet = new DispatcherServlet(); + final var dispatcherServlet = new DispatcherServlet("com.techcourse.controller"); final var registration = servletContext.addServlet(DEFAULT_SERVLET_NAME, dispatcherServlet); if (registration == null) { diff --git a/app/src/main/java/com/techcourse/framework/ManualHandler/ManualHandlerAdapter.java b/app/src/main/java/com/techcourse/framework/ManualHandler/ManualHandlerAdapter.java deleted file mode 100644 index 99cd38a559..0000000000 --- a/app/src/main/java/com/techcourse/framework/ManualHandler/ManualHandlerAdapter.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.techcourse.framework.ManualHandler; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import com.interface21.webmvc.servlet.ModelAndView; -import com.interface21.webmvc.servlet.mvc.asis.Controller; -import com.interface21.webmvc.servlet.view.JspView; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerAdapter; - -public class ManualHandlerAdapter implements HandlerAdapter { - - private final Controller controller; - - public ManualHandlerAdapter(Controller controller) { - this.controller = controller; - } - - @Override - public ModelAndView handle(HttpServletRequest request, HttpServletResponse response) throws Exception { - String execute = controller.execute(request, response); - return new ModelAndView(new JspView(execute)); - } -} diff --git a/app/src/main/java/com/techcourse/framework/ManualHandler/ManualHandlerMapping.java b/app/src/main/java/com/techcourse/framework/ManualHandler/ManualHandlerMapping.java deleted file mode 100644 index c1dd5fc223..0000000000 --- a/app/src/main/java/com/techcourse/framework/ManualHandler/ManualHandlerMapping.java +++ /dev/null @@ -1,33 +0,0 @@ -package com.techcourse.framework.ManualHandler; - -import com.techcourse.controller.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import com.interface21.webmvc.servlet.mvc.asis.Controller; -import com.interface21.webmvc.servlet.mvc.asis.ForwardController; - -import java.util.HashMap; -import java.util.Map; - -public class ManualHandlerMapping { - - private static final Logger log = LoggerFactory.getLogger(ManualHandlerMapping.class); - - private static final Map controllers = new HashMap<>(); - - public void initialize() { - controllers.put("/", new ForwardController("/index.jsp")); - controllers.put("/login", new LoginController()); - controllers.put("/login/view", new LoginViewController()); - controllers.put("/logout", new LogoutController()); - - log.info("Initialized Handler Mapping!"); - controllers.keySet() - .forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass())); - } - - 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/framework/ManualHandler/ManualHandlerMappingAdapter.java b/app/src/main/java/com/techcourse/framework/ManualHandler/ManualHandlerMappingAdapter.java deleted file mode 100644 index 149bf42e3a..0000000000 --- a/app/src/main/java/com/techcourse/framework/ManualHandler/ManualHandlerMappingAdapter.java +++ /dev/null @@ -1,31 +0,0 @@ -package com.techcourse.framework.ManualHandler; - -import jakarta.servlet.http.HttpServletRequest; - -import com.interface21.webmvc.servlet.mvc.asis.Controller; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerAdapter; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerMappingAdapter; - -public class ManualHandlerMappingAdapter implements HandlerMappingAdapter { - - private final ManualHandlerMapping manualHandlerMapping; - - public ManualHandlerMappingAdapter(ManualHandlerMapping manualHandlerMapping) { - this.manualHandlerMapping = manualHandlerMapping; - } - - @Override - public void initialize() { - manualHandlerMapping.initialize(); - } - - @Override - //todo Optional???? - public HandlerAdapter getHandler(HttpServletRequest request) { - Controller handler = manualHandlerMapping.getHandler(request.getRequestURI()); - if (handler == null) { - return null; - } - return new ManualHandlerAdapter(handler); - } -} diff --git a/app/src/test/java/com/techcourse/controller/UserControllerTest.java b/app/src/test/java/com/techcourse/controller/UserControllerTest.java new file mode 100644 index 0000000000..5c7d2166fa --- /dev/null +++ b/app/src/test/java/com/techcourse/controller/UserControllerTest.java @@ -0,0 +1,49 @@ +package com.techcourse.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.interface21.webmvc.servlet.mvc.framework.DispatcherServlet; +import com.techcourse.repository.InMemoryUserRepository; + +class UserControllerTest { + + DispatcherServlet dispatcherServlet = new DispatcherServlet("com.techcourse.controller"); + + @BeforeEach + void init() { + dispatcherServlet.init(); + } + + @Test + void test() throws IOException, ServletException { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + StringWriter stringWriter = new StringWriter(); + + when(request.getRequestURI()).thenReturn("/api/user"); + when(request.getMethod()).thenReturn("GET"); + when(request.getParameter("account")).thenReturn("gugu"); + when(response.getWriter()).thenReturn(new PrintWriter(stringWriter)); + + dispatcherServlet.service(request, response); + + assertThat(stringWriter.toString()).isEqualTo( + new ObjectMapper().writeValueAsString( + InMemoryUserRepository.findByAccount("gugu").get() + )); + } +} diff --git a/mvc/src/main/java/com/interface21/core/util/ReflectionUtils.java b/mvc/src/main/java/com/interface21/core/util/ReflectionUtils.java index 986da054fc..df6cac5b2d 100644 --- a/mvc/src/main/java/com/interface21/core/util/ReflectionUtils.java +++ b/mvc/src/main/java/com/interface21/core/util/ReflectionUtils.java @@ -7,7 +7,8 @@ public abstract class ReflectionUtils { /** * Obtain an accessible constructor for the given class and parameters. - * @param clazz the clazz to check + * + * @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 @@ -22,9 +23,9 @@ public static Constructor accessibleConstructor(Class clazz, Class. } /** - * 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. + * 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 */ diff --git a/mvc/src/main/java/com/interface21/web/SpringServletContainerInitializer.java b/mvc/src/main/java/com/interface21/web/SpringServletContainerInitializer.java index da8da4f324..a26decb688 100644 --- a/mvc/src/main/java/com/interface21/web/SpringServletContainerInitializer.java +++ b/mvc/src/main/java/com/interface21/web/SpringServletContainerInitializer.java @@ -5,7 +5,6 @@ import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.HandlesTypes; - import java.util.ArrayList; import java.util.List; import java.util.Set; diff --git a/mvc/src/main/java/com/interface21/web/bind/annotation/PathVariable.java b/mvc/src/main/java/com/interface21/web/bind/annotation/PathVariable.java index 259dc6e880..165c7e453b 100644 --- a/mvc/src/main/java/com/interface21/web/bind/annotation/PathVariable.java +++ b/mvc/src/main/java/com/interface21/web/bind/annotation/PathVariable.java @@ -1,6 +1,10 @@ package com.interface21.web.bind.annotation; -import java.lang.annotation.*; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) diff --git a/mvc/src/main/java/com/interface21/web/bind/annotation/RequestParam.java b/mvc/src/main/java/com/interface21/web/bind/annotation/RequestParam.java index 4db9537996..dfd0fb5190 100644 --- a/mvc/src/main/java/com/interface21/web/bind/annotation/RequestParam.java +++ b/mvc/src/main/java/com/interface21/web/bind/annotation/RequestParam.java @@ -1,6 +1,10 @@ package com.interface21.web.bind.annotation; -import java.lang.annotation.*; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/View.java b/mvc/src/main/java/com/interface21/webmvc/servlet/View.java index 24d7bad00a..c81b1861df 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/View.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/View.java @@ -2,7 +2,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - import java.util.Map; public interface View { diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerAdapter.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerAdapter.java similarity index 85% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerAdapter.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerAdapter.java index 15c1fac4fc..8870724798 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerAdapter.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerAdapter.java @@ -1,10 +1,9 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.mvc; +import com.interface21.webmvc.servlet.ModelAndView; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import com.interface21.webmvc.servlet.ModelAndView; - public interface HandlerAdapter { ModelAndView handle(HttpServletRequest request, HttpServletResponse response) throws Exception; } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerExecution.java similarity index 91% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerExecution.java index 2960379f40..d39b5ac5ae 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerExecution.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerExecution.java @@ -1,14 +1,13 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.mvc; +import com.interface21.webmvc.servlet.ModelAndView; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import com.interface21.webmvc.servlet.ModelAndView; - public class HandlerExecution { private static final Map, Object> controllerInstances = new ConcurrentHashMap<>(); @@ -23,7 +22,8 @@ public HandlerExecution(Class runnerInstanceClazz, Method method) { private Object createNewInstance(Class clazz) { try { return clazz.getConstructor().newInstance(); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | + NoSuchMethodException e) { throw new RuntimeException(String.format( "컨트롤러 %s 를 생성할 수 있는 기본 생성자가 존재하지 않습니다.", clazz.getName()) ); diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerKey.java similarity index 81% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerKey.java index e10abd75eb..cf9c1af490 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerKey.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerKey.java @@ -1,7 +1,6 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.mvc; import com.interface21.web.bind.annotation.RequestMethod; - import java.util.Objects; 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/com/interface21/webmvc/servlet/mvc/tobe/HandlerMappingAdapter.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerMappingAdapter.java similarity index 78% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMappingAdapter.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerMappingAdapter.java index adcd8f86dc..deb4e9cc8a 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/HandlerMappingAdapter.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/HandlerMappingAdapter.java @@ -1,4 +1,4 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.mvc; import jakarta.servlet.http.HttpServletRequest; diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/NoMatchedHandlerException.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/NoMatchedHandlerException.java similarity index 89% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/NoMatchedHandlerException.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/mvc/NoMatchedHandlerException.java index bc8fb12a45..6c30cea138 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/NoMatchedHandlerException.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/NoMatchedHandlerException.java @@ -1,4 +1,4 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.mvc; import jakarta.servlet.http.HttpServletRequest; diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/AnnotationHandlerAdapter.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/AnnotationHandlerAdapter.java similarity index 75% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/AnnotationHandlerAdapter.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/AnnotationHandlerAdapter.java index 5cb99aa907..0c5ca6b513 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/AnnotationHandlerAdapter.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/AnnotationHandlerAdapter.java @@ -1,12 +1,11 @@ -package com.interface21.webmvc.servlet.mvc.tobe.annotation; +package com.interface21.webmvc.servlet.mvc.annotation; +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.mvc.HandlerAdapter; +import com.interface21.webmvc.servlet.mvc.HandlerExecution; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import com.interface21.webmvc.servlet.ModelAndView; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerAdapter; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerExecution; - public class AnnotationHandlerAdapter implements HandlerAdapter { private final HandlerExecution handlerExecution; diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/AnnotationHandlerMapping.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/AnnotationHandlerMapping.java similarity index 91% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/AnnotationHandlerMapping.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/AnnotationHandlerMapping.java index 9fe9dbd2a7..cdf113b0f8 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/AnnotationHandlerMapping.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/AnnotationHandlerMapping.java @@ -1,21 +1,18 @@ -package com.interface21.webmvc.servlet.mvc.tobe.annotation; +package com.interface21.webmvc.servlet.mvc.annotation; +import com.interface21.web.bind.annotation.RequestMapping; +import com.interface21.web.bind.annotation.RequestMethod; +import com.interface21.webmvc.servlet.mvc.HandlerExecution; +import com.interface21.webmvc.servlet.mvc.HandlerKey; +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 jakarta.servlet.http.HttpServletRequest; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.interface21.web.bind.annotation.RequestMapping; -import com.interface21.web.bind.annotation.RequestMethod; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerExecution; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerKey; - public class AnnotationHandlerMapping { private static final Logger log = LoggerFactory.getLogger(AnnotationHandlerMapping.class); @@ -56,7 +53,7 @@ private RequestMethod[] getRequestMethods(RequestMapping requestMappingAnnotatio } private void bindControllerToRequest(Class controllerClass, Method method, String requestUrl, - RequestMethod... requestMethods + RequestMethod... requestMethods ) { for (RequestMethod requestMethod : requestMethods) { handlerExecutions.put(new HandlerKey(requestUrl, requestMethod), diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/AnnotationHandlerMappingAdapter.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/AnnotationHandlerMappingAdapter.java similarity index 70% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/AnnotationHandlerMappingAdapter.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/AnnotationHandlerMappingAdapter.java index 566bd50060..c3dd12ab2f 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/AnnotationHandlerMappingAdapter.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/AnnotationHandlerMappingAdapter.java @@ -1,11 +1,10 @@ -package com.interface21.webmvc.servlet.mvc.tobe.annotation; +package com.interface21.webmvc.servlet.mvc.annotation; +import com.interface21.webmvc.servlet.mvc.HandlerAdapter; +import com.interface21.webmvc.servlet.mvc.HandlerExecution; +import com.interface21.webmvc.servlet.mvc.HandlerMappingAdapter; import jakarta.servlet.http.HttpServletRequest; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerAdapter; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerExecution; -import com.interface21.webmvc.servlet.mvc.tobe.HandlerMappingAdapter; - public class AnnotationHandlerMappingAdapter implements HandlerMappingAdapter { private final AnnotationHandlerMapping annotationHandlerMapping; @@ -20,8 +19,6 @@ public void initialize() { } @Override - //todo Optional?? - //지금은 예외가 터질거임 public HandlerAdapter getHandler(HttpServletRequest request) { HandlerExecution handlerExecution = annotationHandlerMapping.getHandler(request); if (handlerExecution == null) { diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/ControllerScanner.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/ControllerScanner.java similarity index 78% rename from mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/ControllerScanner.java rename to mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/ControllerScanner.java index f4c4ed2d55..efca07ef39 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/tobe/annotation/ControllerScanner.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/annotation/ControllerScanner.java @@ -1,13 +1,12 @@ -package com.interface21.webmvc.servlet.mvc.tobe.annotation; +package com.interface21.webmvc.servlet.mvc.annotation; +import com.interface21.context.stereotype.Controller; import java.util.Set; - import org.reflections.Reflections; -import com.interface21.context.stereotype.Controller; - public class ControllerScanner { - private ControllerScanner() {} + private ControllerScanner() { + } public static Set> getControllerClass(Object... basePackage) { Reflections reflections = new Reflections(basePackage); diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/Controller.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/Controller.java deleted file mode 100644 index e1883d68b8..0000000000 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/Controller.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.interface21.webmvc.servlet.mvc.asis; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -public interface Controller { - String execute(HttpServletRequest req, HttpServletResponse res) throws Exception; -} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/ForwardController.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/ForwardController.java deleted file mode 100644 index 35cb2ab211..0000000000 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/asis/ForwardController.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.interface21.webmvc.servlet.mvc.asis; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import java.util.Objects; - -public class ForwardController implements Controller { - - private final String path; - - public ForwardController(String path) { - this.path = Objects.requireNonNull(path); - } - - @Override - public String execute(HttpServletRequest request, HttpServletResponse response) { - return path; - } -} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/framework/DispatcherServlet.java b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/framework/DispatcherServlet.java new file mode 100644 index 0000000000..31fcf45786 --- /dev/null +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/mvc/framework/DispatcherServlet.java @@ -0,0 +1,63 @@ +package com.interface21.webmvc.servlet.mvc.framework; + +import com.interface21.webmvc.servlet.ModelAndView; +import com.interface21.webmvc.servlet.mvc.HandlerAdapter; +import com.interface21.webmvc.servlet.mvc.HandlerMappingAdapter; +import com.interface21.webmvc.servlet.mvc.NoMatchedHandlerException; +import com.interface21.webmvc.servlet.mvc.annotation.AnnotationHandlerMappingAdapter; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Objects; +import java.util.stream.Stream; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DispatcherServlet extends HttpServlet { + + private static final long serialVersionUID = 1L; + private static final Logger log = LoggerFactory.getLogger(DispatcherServlet.class); + + private final Object[] basePackage; + + private List handlerMappingAdapters; + + public DispatcherServlet(Object... basePackage) { + this.basePackage = basePackage; + } + + @Override + public void init() { + handlerMappingAdapters = Stream.of(new AnnotationHandlerMappingAdapter(basePackage)) + .map(adapter -> { + adapter.initialize(); + return (HandlerMappingAdapter) adapter; + }).toList(); + } + + @Override + protected void service(final HttpServletRequest request, final HttpServletResponse response) + throws ServletException { + final String requestURI = request.getRequestURI(); + log.debug("Method : {}, Request URI : {}", request.getMethod(), requestURI); + + try { + HandlerAdapter handlerAdapter = getHandlerAdapter(request); + ModelAndView modelAndView = handlerAdapter.handle(request, response); + modelAndView.getView().render(modelAndView.getModel(), request, response); + } catch (Throwable e) { + log.error("Exception : {}", e.getMessage(), e); + throw new ServletException(e.getMessage(), e); + } + } + + private HandlerAdapter getHandlerAdapter(HttpServletRequest request) { + return handlerMappingAdapters.stream() + .map(handlerMappingAdapter -> handlerMappingAdapter.getHandler(request)) + .filter(Objects::nonNull) + .findFirst() + .orElseThrow(() -> new NoMatchedHandlerException(request)); + } +} diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JsonView.java b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JsonView.java index 7f5eff44a9..e7a66ef6bb 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JsonView.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JsonView.java @@ -1,14 +1,41 @@ package com.interface21.webmvc.servlet.view; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.interface21.web.http.MediaType; import com.interface21.webmvc.servlet.View; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - +import java.io.PrintWriter; import java.util.Map; public class JsonView implements View { + private static final ObjectMapper objectMapper = new ObjectMapper(); @Override - public void render( Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { + public void render(Map model, HttpServletRequest request, HttpServletResponse response) + throws Exception { + String value = getModelAsString(model); + PrintWriter writer = response.getWriter(); + writer.write(value); + writer.flush(); + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + } + + private String getModelAsString(Map model) throws JsonProcessingException { + if (model.size() == 1) { + return writeSingleObjectValue(model); + } + return objectMapper.writeValueAsString(model); + } + + private String writeSingleObjectValue(Map model) throws JsonProcessingException { + if (model.size() != 1) { + throw new IllegalStateException(); + } + String key = model.keySet().stream() + .findFirst() + .get(); + return objectMapper.writeValueAsString(model.get(key)); } } diff --git a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java index 95ece5343d..6bf6d641fa 100644 --- a/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java +++ b/mvc/src/main/java/com/interface21/webmvc/servlet/view/JspView.java @@ -3,11 +3,10 @@ import com.interface21.webmvc.servlet.View; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import java.util.Map; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.Map; - public class JspView implements View { private static final Logger log = LoggerFactory.getLogger(JspView.class); @@ -21,7 +20,8 @@ public JspView(String viewName) { } @Override - public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { + public void render(Map model, HttpServletRequest request, HttpServletResponse response) + throws Exception { if (viewName.startsWith(JspView.REDIRECT_PREFIX)) { response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length())); return; diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/AnnotationHandlerMappingTest.java similarity index 94% rename from mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java rename to mvc/src/test/java/com/interface21/webmvc/servlet/mvc/AnnotationHandlerMappingTest.java index d9362ccf37..78333b0888 100644 --- a/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/tobe/AnnotationHandlerMappingTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/AnnotationHandlerMappingTest.java @@ -1,18 +1,16 @@ -package com.interface21.webmvc.servlet.mvc.tobe; +package com.interface21.webmvc.servlet.mvc; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.interface21.webmvc.servlet.mvc.annotation.AnnotationHandlerMapping; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import com.interface21.webmvc.servlet.mvc.tobe.annotation.AnnotationHandlerMapping; - class AnnotationHandlerMappingTest { private AnnotationHandlerMapping handlerMapping; diff --git a/app/src/test/java/com/techcourse/framework/DispatcherServletTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/framework/DispatcherServletTest.java similarity index 72% rename from app/src/test/java/com/techcourse/framework/DispatcherServletTest.java rename to mvc/src/test/java/com/interface21/webmvc/servlet/mvc/framework/DispatcherServletTest.java index 23378884fc..fe0eaa826e 100644 --- a/app/src/test/java/com/techcourse/framework/DispatcherServletTest.java +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/mvc/framework/DispatcherServletTest.java @@ -1,4 +1,4 @@ -package com.techcourse.framework; +package com.interface21.webmvc.servlet.mvc.framework; import static org.assertj.core.api.Assertions.assertThatCode; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -6,21 +6,19 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.interface21.webmvc.servlet.mvc.NoMatchedHandlerException; import jakarta.servlet.RequestDispatcher; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; - import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import com.interface21.webmvc.servlet.mvc.tobe.NoMatchedHandlerException; - class DispatcherServletTest { - private final DispatcherServlet dispatcherServlet = new DispatcherServlet(); + private final DispatcherServlet dispatcherServlet = new DispatcherServlet("samples"); @BeforeEach void init() { @@ -38,22 +36,7 @@ void serviceWithAnnotation() { final var response = mock(HttpServletResponse.class); final var requestDispatcher = mock(RequestDispatcher.class); - when(request.getRequestURI()).thenReturn("/register/view"); - when(request.getMethod()).thenReturn("GET"); - when(request.getRequestDispatcher(anyString())).thenReturn(requestDispatcher); - - assertThatCode(() -> dispatcherServlet.service(request, response)) - .doesNotThrowAnyException(); - } - - @Test - @DisplayName("인터페이스 기반 컨트롤러 요청을 처리할 수 있다.") - void serviceWithInterface() { - final var request = mock(HttpServletRequest.class); - final var response = mock(HttpServletResponse.class); - final var requestDispatcher = mock(RequestDispatcher.class); - - when(request.getRequestURI()).thenReturn("/"); + when(request.getRequestURI()).thenReturn("/get-test"); when(request.getMethod()).thenReturn("GET"); when(request.getRequestDispatcher(anyString())).thenReturn(requestDispatcher); diff --git a/mvc/src/test/java/com/interface21/webmvc/servlet/view/JsonViewStudyTest.java b/mvc/src/test/java/com/interface21/webmvc/servlet/view/JsonViewStudyTest.java new file mode 100644 index 0000000000..e64dd2f02e --- /dev/null +++ b/mvc/src/test/java/com/interface21/webmvc/servlet/view/JsonViewStudyTest.java @@ -0,0 +1,85 @@ +package com.interface21.webmvc.servlet.view; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +class JsonViewStudyTest { + + @Test + @DisplayName("model 의 값을 JSON 으로 변환해 response 를 반환한다.") + void StingObjects() throws Exception { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + StringWriter stringWriter = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(stringWriter)); + User user = new User("name", "password"); + Map model = Map.of( + "user", user + ); + + var jsonView = new JsonView(); + jsonView.render(model, request, response); + + assertThat(stringWriter.toString()) + .isEqualTo(new ObjectMapper().writeValueAsString(user)); + } + + @Test + @DisplayName("model 값이 여러개면 Map 형식을 변환한다.") + void multipleObjects() throws Exception { + HttpServletRequest request = mock(HttpServletRequest.class); + HttpServletResponse response = mock(HttpServletResponse.class); + StringWriter stringWriter = new StringWriter(); + when(response.getWriter()).thenReturn(new PrintWriter(stringWriter)); + Map model = Map.of( + "user", new User("name", "password"), + "someModel", new SomeModel("attribute") + ); + + var jsonView = new JsonView(); + jsonView.render(model, request, response); + + assertThat(stringWriter.toString()) + .isEqualTo(new ObjectMapper().writeValueAsString(model)); + } + + private static class User { + private final String name; + private final String password; + + private User(String name, String password) { + this.name = name; + this.password = password; + } + + public String getName() { + return name; + } + + public String getPassword() { + return password; + } + } + + private static class SomeModel { + private final String attribute; + + private SomeModel(String attribute) { + this.attribute = attribute; + } + + public String getAttribute() { + return attribute; + } + } +} diff --git a/mvc/src/test/java/samples/TestController.java b/mvc/src/test/java/samples/TestController.java index 6ad9c741dd..deef078fa5 100644 --- a/mvc/src/test/java/samples/TestController.java +++ b/mvc/src/test/java/samples/TestController.java @@ -1,14 +1,14 @@ package samples; import com.interface21.context.stereotype.Controller; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import com.interface21.web.bind.annotation.RequestMapping; import com.interface21.web.bind.annotation.RequestMethod; import com.interface21.webmvc.servlet.ModelAndView; import com.interface21.webmvc.servlet.view.JspView; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Controller public class TestController { diff --git a/study/src/test/java/di/stage3/context/DIContainer.java b/study/src/test/java/di/stage3/context/DIContainer.java index b62feb1ed3..482bdc926c 100644 --- a/study/src/test/java/di/stage3/context/DIContainer.java +++ b/study/src/test/java/di/stage3/context/DIContainer.java @@ -1,6 +1,10 @@ package di.stage3.context; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.NoSuchElementException; import java.util.Set; +import java.util.stream.Collectors; /** * 스프링의 BeanFactory, ApplicationContext에 해당되는 클래스 @@ -10,11 +14,72 @@ class DIContainer { private final Set beans; public DIContainer(final Set> classes) { - this.beans = Set.of(); + beans = classes.stream() + .map(this::createBean) + .collect(Collectors.toSet()); + beans.forEach(this::injectDependency); + beans.forEach(this::validateDependency); + } + + private Object createBean(Class aClass) { + try { + Constructor defaultConstructor = aClass.getDeclaredConstructor(); + defaultConstructor.setAccessible(true); + return defaultConstructor + .newInstance(); + } catch (Exception e) { + throw new RuntimeException("인스턴스를 생성할 수 없습니다.", e); + } + } + + private void injectDependency(Object bean) { + Field[] declaredFields = bean.getClass().getDeclaredFields(); + for (Field field : declaredFields) { + field.setAccessible(true); + if (isFieldInjected(bean, field)) { + continue; + } + injectField(bean, field); + } + + } + + private boolean isFieldNull(Object bean, Field field) { + try { + return field.get(bean) == null; + } catch (IllegalAccessException e) { + throw new RuntimeException("필드를 조회할 수 없습니다."); + } + } + + private boolean isFieldInjected(Object bean, Field field) { + return !isFieldNull(bean, field); + } + + private void injectField(Object bean, Field field) { + try { + field.set(bean, getBean(field.getType())); + } catch (IllegalAccessException e) { + throw new RuntimeException("필드를 주입할 수 없습니다."); + } + } + + private void validateDependency(Object bean) { + Field[] declaredFields = bean.getClass().getDeclaredFields(); + for (Field field : declaredFields) { + field.setAccessible(true); + if (isFieldNull(bean, field)) { + throw new RuntimeException( + bean.getClass().getName() + ": " + field.getName() + " 의존성이 주입되지 않았습니다."); + } + } } @SuppressWarnings("unchecked") public T getBean(final Class aClass) { - return null; + return (T) beans.stream() + .filter(bean -> aClass.isAssignableFrom(bean.getClass())) + .findFirst() + .orElseThrow(() -> new NoSuchElementException(aClass + " 에 해당하는 빈이 존재하지 않습니다.")); } } diff --git a/study/src/test/java/di/stage4/annotations/ClassPathScanner.java b/study/src/test/java/di/stage4/annotations/ClassPathScanner.java index 9dab1fd9c4..2dd30c9307 100644 --- a/study/src/test/java/di/stage4/annotations/ClassPathScanner.java +++ b/study/src/test/java/di/stage4/annotations/ClassPathScanner.java @@ -1,10 +1,13 @@ package di.stage4.annotations; import java.util.Set; +import java.util.stream.Collectors; +import org.junit.platform.commons.support.ReflectionSupport; public class ClassPathScanner { public static Set> getAllClassesInPackage(final String packageName) { - return null; + return ReflectionSupport.streamAllClassesInPackage(packageName, clazz -> true, name -> true) + .collect(Collectors.toSet()); } } diff --git a/study/src/test/java/di/stage4/annotations/DIContainer.java b/study/src/test/java/di/stage4/annotations/DIContainer.java index 9248ecad7e..b54ec5ae45 100644 --- a/study/src/test/java/di/stage4/annotations/DIContainer.java +++ b/study/src/test/java/di/stage4/annotations/DIContainer.java @@ -1,6 +1,12 @@ package di.stage4.annotations; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.NoSuchElementException; import java.util.Set; +import java.util.stream.Collectors; /** * 스프링의 BeanFactory, ApplicationContext에 해당되는 클래스 @@ -10,15 +16,79 @@ class DIContainer { private final Set beans; public DIContainer(final Set> classes) { - this.beans = Set.of(); + beans = classes.stream() + .map(this::createBean) + .collect(Collectors.toSet()); + beans.forEach(this::injectDependency); + beans.forEach(this::validateDependency); } public static DIContainer createContainerForPackage(final String rootPackageName) { - return null; + Set> beanClasses = ClassPathScanner.getAllClassesInPackage(rootPackageName).stream() + .filter(clazz -> clazz.isAnnotationPresent(Repository.class) || clazz.isAnnotationPresent(Service.class) +// && !clazz.isInterface() + ) + .collect(Collectors.toSet()); + return new DIContainer(beanClasses); } + private Object createBean(Class aClass) { + try { + Constructor defaultConstructor = aClass.getDeclaredConstructor(); + defaultConstructor.setAccessible(true); + return defaultConstructor + .newInstance(); + } catch (Exception e) { + throw new RuntimeException(aClass.getName() + "인스턴스를 생성할 수 없습니다.", e); + } + } + + private void injectDependency(Object bean) { + getBeanDependencies(bean) + .forEach(field -> injectField(bean, field)); + } + + private List getBeanDependencies(Object bean) { + return Arrays.stream(bean.getClass().getDeclaredFields()) + .filter(field -> field.isAnnotationPresent(Inject.class)) + .toList(); + } + + private void injectField(Object bean, Field field) { + try { + field.setAccessible(true); + field.set(bean, getBean(field.getType())); + } catch (IllegalAccessException e) { + throw new RuntimeException("필드를 주입할 수 없습니다."); + } + } + + private void validateDependency(Object bean) { + getBeanDependencies(bean).stream() + .forEach(field -> validateBeanInjection(bean, field)); + } + + private void validateBeanInjection(Object bean, Field field) { + if (isFieldNull(bean, field)) { + throw new RuntimeException(bean.getClass().getName() + ": " + field.getName() + " 의존성이 주입되지 않았습니다."); + } + } + + private boolean isFieldNull(Object bean, Field field) { + try { + field.setAccessible(true); + return field.get(bean) == null; + } catch (IllegalAccessException e) { + throw new RuntimeException("필드를 조회할 수 없습니다."); + } + } + + @SuppressWarnings("unchecked") public T getBean(final Class aClass) { - return null; + return (T) beans.stream() + .filter(bean -> aClass.isAssignableFrom(bean.getClass())) + .findFirst() + .orElseThrow(() -> new NoSuchElementException(aClass + " 에 해당하는 빈이 존재하지 않습니다.")); } }