본문 바로가기

Backend Development/Spring boot

[Spring boot] JSP embedded tomcat (tomcat-embedded-jasper) 동작 원리

JSP를 실행하면 실제로는 JSP로부터 생성된 서블릿이 실행된다.

 

① 클라이언트가 JSP를 실행을 요청하면 서블릿 컨테이너는 JSP 파일에 대응하는 자바 서블릿을 찾아서 실행한다.
② 대응하는 서블릿이 없거나 JSP 파일이 변경됐으면 JSP 엔진을 통해 서블릿 자바 소스를 생성한다.
③ 자바 컴파일러가 서블릿 자바 소스를 클래스 파일로 컴파일한다. (JSP 파일이 변경될때마다 반복)
④ JSP로부터 생성된 서블릿은 서블릿 구동 방식에 의해 service() 메소드가 호출되고 서블릿이 생성한 HTML 화면을 웹 브라우저로 보낸다.

 

Spring boot에서 embedded tomcat으로 jsp 구동시 compiled 된 jsp 파일 위치

Spring boot로 윈도우즈에서 embedded tomcat으로 서버 구동 시 컴파일된 jsp 위치를 찾기가 어렵다. 확인해본 결과 아래 위치에 tomcat 배포시 생성됨을 확인할 수 있었다.

 

C:/Users/[사용자계정/AppData/Local/Temp/tomcat.9090.10723202586356060977/work/Tomcat/localhost/ROOT/org/apache/jsp/WEB_002dINF/jsp/login/loginForm_jsp.java, loginForm_jsp.class

 

 

컴파일된 _jsp.java 파일 구조 

 

loginForm_jsp.java를 열어보면 다음과 같다.

 

생성된 loginForm_jsp 클래스는 HttpJspBase를 상속받는다. HttpJspBase는 톰캣에 포함된 클래스이며 톰캣의 JSP 엔진은 서블릿 소스를 생성할 때 이 클래스를 사용한다. (서블릿 컨테이너마다 다르다.)

/*
 * Generated by the Jasper component of Apache Tomcat
 * Version: Apache Tomcat/9.0.54
 * Generated at: 2022-03-04 08:12:35 UTC
 * Note: The last modified time of this file was set to
 *       the last modified time of the source file after
 *       generation to assist with modification tracking.
 */
package org.apache.jsp.WEB_002dINF.jsp.login;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;

public final class loginForm_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
                 org.apache.jasper.runtime.JspSourceImports {

  private static final javax.servlet.jsp.JspFactory _jspxFactory =
          javax.servlet.jsp.JspFactory.getDefaultFactory();

  private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;

 

org/apache/tomcat/embed/tomcat-embed-jasper/9.0.54/tomcat-embed-jasper-9.0.54-sources.jar!/org/apache/jasper/runtime/HttpJspBase.java:30

 

HttpJspBase는 javax.servlet.jsp.HttpJspPage 인터페이스를 구현한다.

HttpJspPage Servlet -> JspPage -> HttpJspPage 순으로 상속되기 때문에 HttpJspPage를 구현하는 클래스는 서블릿 클래스이다. 그러므로 HttpJspBase를 상속받는 HelloWorld_jsp 역시 서블릿이 된다.

 

HttpJspBase 코드를 살펴보면, init() jspInit()을, destroy() jspDestroy()를, service _jspService를 호출한다.

만약 JSP에서 init()이나 destroy() override할 일이 있다면 해당 메소드 대신 jspInit() jspDestroy() override한다.

 

/**
 * This is the super class of all JSP-generated servlets.
 *
 * @author Anil K. Vijendran
 */
public abstract class HttpJspBase extends HttpServlet implements HttpJspPage {

    private static final long serialVersionUID = 1L;

    protected HttpJspBase() {
    }

    @Override
    public final void init(ServletConfig config)
        throws ServletException
    {
        super.init(config);
        jspInit();
        _jspInit();
    }

    @Override
    public String getServletInfo() {
        return Localizer.getMessage("jsp.engine.info", Constants.SPEC_VERSION);
    }

    @Override
    public final void destroy() {
        jspDestroy();
        _jspDestroy();
    }

    /**
     * Entry point into service.
     */
    @Override
    public final void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        _jspService(request, response);
    }

    @Override
    public void jspInit() {
    }

    public void _jspInit() {
    }

    @Override
    public void jspDestroy() {
    }

    protected void _jspDestroy() {
    }

    @Override
    public abstract void _jspService(HttpServletRequest request,
                                     HttpServletResponse response)
        throws ServletException, IOException;
}

 

컴파일된 JSP java파일에는 아래 _jsp_Service 메소드가 생성되어 있고 servelet 실행 시 아래 코드가 불리면서 html 렌더링이 수행된다.

 

public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {

    if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
      final java.lang.String _jspx_method = request.getMethod();
      if ("OPTIONS".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        return;
      }
      if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET, POST or HEAD. Jasper also permits OPTIONS");
        return;
      }
    }

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;


    try {
      response.setContentType("text/html; charset=UTF-8");
      pageContext = _jspxFactory.getPageContext(this, request, response,
      			null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;

      out.write('\r');
      out.write('\n');
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
     
      
  ...중략
  
      out.write("		</div>\r\n");
      out.write("	</div>\r\n");
      out.write("</body>\r\n");
      out.write("\r\n");
      out.write("<form id=\"pageMoveForm\" name=\"pageMoveForm\" action=\"\" method=\"get\">\r\n");
      out.write("</form>\r\n");
      out.write("</html>\r\n");
    } catch (java.lang.Throwable t) {
      if (!(t instanceof javax.servlet.jsp.SkipPageException)){
        out = _jspx_out;
        if (out != null && out.getBufferSize() != 0)
          try {
            if (response.isCommitted()) {
              out.flush();
            } else {
              out.clearBuffer();
            }
          } catch (java.io.IOException e) {}
        if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
        else throw new ServletException(t);
      }
    } finally {
      _jspxFactory.releasePageContext(_jspx_page_context);
    }
  }

 

JSP 내장 객체(JSP Implicit Objects)

request, response, pageContext, session, application, config, out, page

_jspService()의 매개변수는 HttpServlet의 doGet(), doPost()와 동일한 HttpServletRequest HttpServletResponse 객체이다. 단 매개변수의 이름은 반드시 request, response여야 한다.

 

_jspService()의 로컬 변수 중 pageContext, session, application, config, out, page는 반드시 이 이름으로 존재해야 한다.

 

이렇게 _jspServce()에 선언된 request, response, pageContext, session, application, config, out, page 객체들은 이 메소드가 호출될 때 반드시 준비되는 객체들이기 때문에 'JSP 내장 객체(Implicit Objects)'라 한다.

이 객체들은 별도의 선언 없이 JSP에서 사용할 수 있다.

 

 

컴파일된 JSP java 구동 Call Stack

 

localhost:9090으로 loginForm.jsp를 호출해보면 다음과 같은 Call Stack으로 jsp가 구동됨을 알 수 있다.

 

getPageContext:53, JspFactoryImpl (org.apache.jasper.runtime)
_jspService:1, loginForm_jsp (org.apache.jsp.WEB_002dINF.jsp.login)
service:70, HttpJspBase (org.apache.jasper.runtime)
service:764, HttpServlet (javax.servlet.http)
service:466, JspServletWrapper (org.apache.jasper.servlet)
serviceJspFile:379, JspServlet (org.apache.jasper.servlet)
service:327, JspServlet (org.apache.jasper.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:113, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:113, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:113, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:113, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:711, ApplicationDispatcher (org.apache.catalina.core)
processRequest:459, ApplicationDispatcher (org.apache.catalina.core)
doForward:385, ApplicationDispatcher (org.apache.catalina.core)
forward:313, ApplicationDispatcher (org.apache.catalina.core)
forward:171, HeaderWriterFilter$HeaderWriterRequestDispatcher (org.springframework.security.web.header)
renderMergedOutputModel:171, InternalResourceView (org.springframework.web.servlet.view)
render:316, AbstractView (org.springframework.web.servlet.view)
render:1400, DispatcherServlet (org.springframework.web.servlet)
processDispatchResult:1145, DispatcherServlet (org.springframework.web.servlet)
doDispatch:1084, DispatcherServlet (org.springframework.web.servlet)
doService:963, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:655, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:764, HttpServlet (javax.servlet.http)
internalDoFilter:227, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:113, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:36, XssEscapeServletFilter (com.navercorp.lucy.security.xss.servletfilter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilter:327, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
invoke:115, FilterSecurityInterceptor (org.springframework.security.web.access.intercept)
doFilter:81, FilterSecurityInterceptor (org.springframework.security.web.access.intercept)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:121, ExceptionTranslationFilter (org.springframework.security.web.access)
doFilter:115, ExceptionTranslationFilter (org.springframework.security.web.access)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:126, SessionManagementFilter (org.springframework.security.web.session)
doFilter:81, SessionManagementFilter (org.springframework.security.web.session)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:105, AnonymousAuthenticationFilter (org.springframework.security.web.authentication)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:149, SecurityContextHolderAwareRequestFilter (org.springframework.security.web.servletapi)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:63, RequestCacheAwareFilter (org.springframework.security.web.savedrequest)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:48, TokenAuthenticationFilter (com.sdp.common.security)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:103, LogoutFilter (org.springframework.security.web.authentication.logout)
doFilter:89, LogoutFilter (org.springframework.security.web.authentication.logout)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doHeadersAfter:90, HeaderWriterFilter (org.springframework.security.web.header)
doFilterInternal:75, HeaderWriterFilter (org.springframework.security.web.header)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilter:110, SecurityContextPersistenceFilter (org.springframework.security.web.context)
doFilter:80, SecurityContextPersistenceFilter (org.springframework.security.web.context)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:55, WebAsyncManagerIntegrationFilter (org.springframework.security.web.context.request.async)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
doFilter:336, FilterChainProxy$VirtualFilterChain (org.springframework.security.web)
doFilterInternal:211, FilterChainProxy (org.springframework.security.web)
doFilter:183, FilterChainProxy (org.springframework.security.web)
invokeDelegate:358, DelegatingFilterProxy (org.springframework.web.filter)
doFilter:271, DelegatingFilterProxy (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:119, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:189, ApplicationFilterChain (org.apache.catalina.core)
doFilter:162, ApplicationFilterChain (org.apache.catalina.core)
invoke:197, StandardWrapperValve (org.apache.catalina.core)
invoke:97, StandardContextValve (org.apache.catalina.core)
invoke:540, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:135, StandardHostValve (org.apache.catalina.core)
invoke:92, ErrorReportValve (org.apache.catalina.valves)
invoke:78, StandardEngineValve (org.apache.catalina.core)
service:357, CoyoteAdapter (org.apache.catalina.connector)
service:382, Http11Processor (org.apache.coyote.http11)
process:65, AbstractProcessorLight (org.apache.coyote)
process:895, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1722, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:834, Thread (java.lang)

 

JSP 페이지 생성 시 Session 생성을 없애는 방법

 

JSP에서 Session 생성을 없애려면 JSP 소스에 아래 코드를 삽입한다.

 

<%@ page session="false" %>

 

위 설정이 JSP 컴파일시에 어떻게 session 생성을 없애는지 확인해본 결과는 다음과 같다.

 

org/apache/tomcat/embed/tomcat-embed-jasper/9.0.54/tomcat-embed-jasper-9.0.54-sources.jar!/org/apache/jasper/compiler/Validator.java:120

 

@Override
public void visit(Node.PageDirective n) throws JasperException {

    JspUtil.checkAttributes("Page directive", n, pageDirectiveAttrs,
            err);

    // JSP.2.10.1
    Attributes attrs = n.getAttributes();
    for (int i = 0; attrs != null && i < attrs.getLength(); i++) {
        String attr = attrs.getQName(i);
        String value = attrs.getValue(i);

        if ("language".equals(attr)) {
            if (pageInfo.getLanguage(false) == null) {
                pageInfo.setLanguage(value, n, err, true);
            } else if (!pageInfo.getLanguage(false).equals(value)) {
                err.jspError(n, "jsp.error.page.conflict.language",
                        pageInfo.getLanguage(false), value);
            }
        } else if ("extends".equals(attr)) {
            if (pageInfo.getExtends(false) == null) {
                pageInfo.setExtends(value);
            } else if (!pageInfo.getExtends(false).equals(value)) {
                err.jspError(n, "jsp.error.page.conflict.extends",
                        pageInfo.getExtends(false), value);
            }
        } else if ("contentType".equals(attr)) {
            if (pageInfo.getContentType() == null) {
                pageInfo.setContentType(value);
            } else if (!pageInfo.getContentType().equals(value)) {
                err.jspError(n, "jsp.error.page.conflict.contenttype",
                        pageInfo.getContentType(), value);
            }
        } else if ("session".equals(attr)) {
            if (pageInfo.getSession() == null) {
                pageInfo.setSession(value, n, err);
            } else if (!pageInfo.getSession().equals(value)) {
                err.jspError(n, "jsp.error.page.conflict.session",
                        pageInfo.getSession(), value);
            }
        } 
        //JSP에서 session 생성을 안하려면 
        //<%@ page session="false" %>를 JSP에 입력한다. 그러면
        //pageInfo.setSession을 false로 설정하고 JSP compile시에 
        //pageContext.getSession 를 삽입하지 않는다.

        ... 생략