Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MVC 구현하기 1, 2단계] 허브(방대의) 미션 제출합니다. #339

Closed
wants to merge 18 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
52ee778
패키지 위치 변경 및 코드 정리
kang-hyungu Sep 11, 2023
e7c21ab
서블릿 학습 테스트 코드 개선
kang-hyungu Sep 11, 2023
448deb4
feat: Reflection 학습 테스트
greeng00se Sep 12, 2023
f75221c
feat: Servlet과 Servlet Container 학습 테스트
greeng00se Sep 12, 2023
dc1a228
feat: AnnotationHandlerMappingTest 클래스의 실패하는 테스트를 통과시키기
greeng00se Sep 12, 2023
68749ca
docs: README 추가
greeng00se Sep 12, 2023
23c22a7
feat: 컨트롤러를 찾아서 인스턴스 생성하는 역할을 담당하는 ControllerScanner 추가
greeng00se Sep 12, 2023
85981d9
feat: 기존의 AnnotationHandlerMapping 클래스에 ControllerScanner 사용하도록 적용
greeng00se Sep 12, 2023
2b173bc
feat: HandlerMapping 인터페이스 추가
greeng00se Sep 12, 2023
158ca43
feat: HandlerMapping 리스트를 가지고 있는 HandlerMappingRegistry 추가
greeng00se Sep 12, 2023
1855f54
refactor: ControllerScanner Exception 던지지 않도록 수정
greeng00se Sep 12, 2023
c18218b
feat: HandlerExecution을 실행시키는 HandlerExecutorHandlerAdapter 추가
greeng00se Sep 12, 2023
0561504
refactor: HandlerAdapter throw Exception 추가
greeng00se Sep 12, 2023
ccfc53d
feat: ControllerHandlerAdapter 클래스 추가
greeng00se Sep 12, 2023
9ae6bb1
feat: 핸들러를 처리할 수 있는 핸들러 어댑터를 반환하는 HandlerAdapterRegistry 클래스 추가
greeng00se Sep 12, 2023
28bc769
feat: 임시로 View에 getViewName 메서드 추가
greeng00se Sep 12, 2023
0309d86
feat: DispatcherServlet에 HandlerMappingRegistry, HandlerAdapterRegist…
greeng00se Sep 12, 2023
7680e6a
feat: HandlerExecutor 추가
greeng00se Sep 12, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 --info -x :study:build
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,5 @@ Temporary Items

tomcat.*
tomcat.*/**

**/WEB-INF/classes/**
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,16 @@
# @MVC 구현하기

### 1 단계 - @MVC 프레임워크 구현하기

- [x] AnnotationHandlerMappingTest 클래스의 실패하는 테스트를 통과시키기

### 2단계 - 점진적인 리팩터링

- [x] RegisterController를 어노테이션 기반으로 변경해도 동작하도록 만들기
- [x] 컨트롤러를 찾아서 인스턴스 생성하는 역할을 담당하는 ControllerScanner 추가
- [x] 기존의 AnnotationHandlerMapping 클래스에 ControllerScanner 사용하도록 적용
- [x] HandlerMapping 인터페이스 추가
- [x] HandlerMapping 리스트를 가지고 있는 HandlerMappingRegistry 추가
- [x] HandlerExecution 메서드를 호출하는 HandlerExecutorHandlerAdapter 추가
- [x] Controller 메서드를 호출하는 ControllerHandlerAdapter 추가
- [x] HandlerAdapter를 리스트를 가지고 있는 HandlerAdapterRegistry 추가
24 changes: 0 additions & 24 deletions app/src/main/java/com/techcourse/AppWebApplicationInitializer.java

This file was deleted.

33 changes: 16 additions & 17 deletions app/src/main/java/com/techcourse/Application.java
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -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) {
Expand All @@ -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();
}
}
}
82 changes: 82 additions & 0 deletions app/src/main/java/com/techcourse/DispatcherServlet.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package com.techcourse;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.Optional;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import webmvc.org.springframework.web.servlet.ModelAndView;
import webmvc.org.springframework.web.servlet.View;
import webmvc.org.springframework.web.servlet.mvc.HandlerAdapter;
import webmvc.org.springframework.web.servlet.mvc.HandlerAdapterRegistry;
import webmvc.org.springframework.web.servlet.mvc.HandlerExecutor;
import webmvc.org.springframework.web.servlet.mvc.HandlerMapping;
import webmvc.org.springframework.web.servlet.mvc.HandlerMappingRegistry;
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 final HandlerMappingRegistry handlerMappingRegistry = new HandlerMappingRegistry();
private final HandlerAdapterRegistry handlerAdapterRegistry = new HandlerAdapterRegistry();
private HandlerExecutor handlerExecutor;

public DispatcherServlet() {
}

@Override
public void init() {
handlerExecutor = new HandlerExecutor(handlerAdapterRegistry);
}

@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 {
final Optional<Object> handler = handlerMappingRegistry.getHandler(request);
if (handler.isEmpty()) {
response.setStatus(404);
return;
}
final ModelAndView modelAndView = handlerExecutor.handle(request, response, handler.get());
render(modelAndView, request, response);
} catch (Throwable e) {
log.error("Exception : {}", e.getMessage(), e);
throw new ServletException(e.getMessage());
}
}

private void render(
final ModelAndView modelAndView,
final HttpServletRequest request,
final HttpServletResponse response
) throws Exception {
final View view = modelAndView.getView();
final String viewName = view.getViewName();
if (viewName.startsWith(JspView.REDIRECT_PREFIX)) {
response.sendRedirect(viewName.substring(JspView.REDIRECT_PREFIX.length()));
return;
}

final var requestDispatcher = request.getRequestDispatcher(viewName);
requestDispatcher.forward(request, response);
// 3 단계 - 현재는 임시로 view에 getViewName 사용
// final View view = modelAndView.getView();
// view.render(modelAndView.getModel(), request, response);
}

public void addHandlerMapping(final HandlerMapping handlerMapping) {
handlerMappingRegistry.addHandlerMapping(handlerMapping);
}

public void addHandlerAdapter(final HandlerAdapter handlerAdapter) {
handlerAdapterRegistry.addHandlerAdapter(handlerAdapter);
}
}
41 changes: 41 additions & 0 deletions app/src/main/java/com/techcourse/DispatcherServletInitializer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package com.techcourse;

import jakarta.servlet.ServletContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import web.org.springframework.web.WebApplicationInitializer;
import webmvc.org.springframework.web.servlet.mvc.asis.ControllerHandlerAdapter;
import webmvc.org.springframework.web.servlet.mvc.tobe.AnnotationHandlerMapping;
import webmvc.org.springframework.web.servlet.mvc.tobe.HandlerExecutionHandlerAdapter;

/**
* 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();

dispatcherServlet.addHandlerMapping(new ManualHandlerMapping());
dispatcherServlet.addHandlerMapping(new AnnotationHandlerMapping("com.techcourse.controller"));
dispatcherServlet.addHandlerAdapter(new HandlerExecutionHandlerAdapter());
dispatcherServlet.addHandlerAdapter(new ControllerHandlerAdapter());

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");
}
}
25 changes: 14 additions & 11 deletions app/src/main/java/com/techcourse/ManualHandlerMapping.java
Original file line number Diff line number Diff line change
@@ -1,39 +1,42 @@
package com.techcourse;

import com.techcourse.controller.*;
import com.techcourse.controller.LoginController;
import com.techcourse.controller.LoginViewController;
import com.techcourse.controller.LogoutController;
import com.techcourse.controller.RegisterViewController;
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 java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import webmvc.org.springframework.web.servlet.mvc.HandlerMapping;
import webmvc.org.springframework.web.servlet.mvc.asis.Controller;
import webmvc.org.springframework.web.servlet.mvc.asis.ForwardController;

public class ManualHandlerMapping implements HandlerMapping {

private static final Logger log = LoggerFactory.getLogger(ManualHandlerMapping.class);

private static final Map<String, Controller> controllers = new HashMap<>();

@Override
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());
controllers.put("/register/view", new RegisterViewController());
controllers.put("/register", new RegisterController());

log.info("Initialized Handler Mapping!");
controllers.keySet()
.forEach(path -> log.info("Path : {}, Controller : {}", path, controllers.get(path).getClass()));
}

@Override
public Controller getHandler(HttpServletRequest request) {
final String requestURI = request.getRequestURI();
public Object getHandler(final HttpServletRequest httpServletRequest) {
return getHandler(httpServletRequest.getRequestURI());
}

public Controller getHandler(final String requestURI) {
log.debug("Request Mapping Uri : {}", requestURI);
return controllers.get(requestURI);
}
Expand Down
83 changes: 83 additions & 0 deletions app/src/main/java/com/techcourse/TomcatStarter.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.techcourse;

public class UncheckedServletException extends RuntimeException {

public UncheckedServletException(Exception e) {
super(e);
}
}
Loading
Loading