DispatcherServlet
프런트 컨트롤러라고 합니다. 클라이언트의 모든 요청을 받은 후 이를 처리할 핸들러에게 위임하고 위임을 받은 핸들러가 요청을 처리하면 그 결과를 받아 사용자에게 응답 결과를 보여줍니다.
DispatcherServlet은 여러 컴포넌트를 사용해서 작업을 처리합니다.
요청을 받으면 선처리 작업을 합니다.
Spiring MVC는 지역화를 지원합니다. 각 국가의 사용자에 따라서 언어를 다르게 뷰를 전달하게 할 수 있습니다.
각 브라우저에 언어 셋팅이 되어있기 때문에 그러한 헤더 정보에서 언어 설정값을 이용해 LOCALE을 결정할 수 있습니다.
이러한 작업을 선처리 작업을 합니다.
그이후에는 RequestContextHolder에 request를 저장합니다.
이 객체는 스레드 로컬 객체입니다.
각 쓰레드마다 지역변수를 저장해 해당 스레드 안에서만 사용할 수 있는 객체입니다.
요청을 받아 응답할 때까지 HttpServletRequest , HttpServeltResponse 등을 Spring이 관리하는 객체 안에서 사용할 수 있도록 해주는 것입니다.
우리가 Controller에서 request, response를 인자로 요청했을 때 자유롭게 사용할 수 있게 해 줍니다.
그 이후
FlashMap을 복원합니다. (spring version 3) 이 플래시 맵은 redirect로 값을 전달할 때 사용됩니다.
딱 한번 값을 유지시킬 수 있게 합니다. 같은 도메인상에서 새로고침 등과 같은 일이 발생한 경우 데이터를 플래시 맵에 담아 한번 사용하고 나면 spring에서 값을 처리하게 됩니다.
또한 만약 사용자가 파일 업로드를 한경우에는 파일 정보를 읽어 들이는 별도의 처리가 필요합니다.
MultipartResolver가 request를 결정하도록 합니다. 그 이후 요청을 처리하는 핸들러를 결정하고 실행합니다.
여기까지 선처리입니다.
선처리 작업 시 여러 가지 컴포넌트가 사용됩니다.
LocaleResolver , FlashMapManager , RequestContextHolder, MultiPartResolver 등등이 사용됩니다.
그 이후 DispatcherServlet은 실행할 컨트롤러를 검색하는데 그때 HandlerMapping을 사용해 HandlerExecutionChain을 결정하게 됩니다.
만약 존재하지 않으면 404 에러를 발생시키고 존재한다면 찾은 Mapping을 가지고 HandlerAdapter를 결정합니다.
만약 Adapter실행 중 에러가 난다면 서버의 잘못입니다.(ServletException)
존재하면 반환된 값을 가지고 요청을 처리합니다.
요청을 처리하는 도중 사용 가능한 인터셉터가 존재한다면 전처리, 후처리 인터셉터를 확인해 실행합니다.
그 이후 핸들러가 실행되며 ModelAndView를 리턴 유무에 따라 실행결과가 달라지며 후처리를 통해 뷰를 렌더링 합니다.
ModelAndView는 컨트롤러 처리결과를 보여줄 view와 model을 전달하는 클래스입니다.
RequestToViewNameTranslator는 컨트롤러에서 뷰 이름이나 뷰 오브젝트를 제공하지 않았을 경우 url과 같은 요청 정보를 참고해서 자동으로 뷰 이름을 생성해주는 전략 오브젝트입니다.
DispathcherServlet의 Hierarchy를 보면 상위 클래스를 볼 수 있다
Spring-MVC는 Servlet 스펙을 기반으로 만들어진 프레임워크입니다.
그렇기에 Servlet interface를 구현하고 있습니다. 우리가 만든 웹 애플리케이션에 HTTP 요청이 전달되면 실행하는 WAS에서 servlet interface를 실행합니다.
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package javax.servlet;
import java.io.IOException;
/**
* Defines methods that all servlets must implement.
*
* <p>
* A servlet is a small Java program that runs within a Web server. Servlets
* receive and respond to requests from Web clients, usually across HTTP, the
* HyperText Transfer Protocol.
*
* <p>
* To implement this interface, you can write a generic servlet that extends
* <code>javax.servlet.GenericServlet</code> or an HTTP servlet that extends
* <code>javax.servlet.http.HttpServlet</code>.
*
* <p>
* This interface defines methods to initialize a servlet, to service requests,
* and to remove a servlet from the server. These are known as life-cycle
* methods and are called in the following sequence:
* <ol>
* <li>The servlet is constructed, then initialized with the <code>init</code>
* method.
* <li>Any calls from clients to the <code>service</code> method are handled.
* <li>The servlet is taken out of service, then destroyed with the
* <code>destroy</code> method, then garbage collected and finalized.
* </ol>
*
* <p>
* In addition to the life-cycle methods, this interface provides the
* <code>getServletConfig</code> method, which the servlet can use to get any
* startup information, and the <code>getServletInfo</code> method, which allows
* the servlet to return basic information about itself, such as author,
* version, and copyright.
*
* @see GenericServlet
* @see javax.servlet.http.HttpServlet
*/
public interface Servlet {
/**
* Called by the servlet container to indicate to a servlet that the servlet
* is being placed into service.
*
* <p>
* The servlet container calls the <code>init</code> method exactly once
* after instantiating the servlet. The <code>init</code> method must
* complete successfully before the servlet can receive any requests.
*
* <p>
* The servlet container cannot place the servlet into service if the
* <code>init</code> method
* <ol>
* <li>Throws a <code>ServletException</code>
* <li>Does not return within a time period defined by the Web server
* </ol>
*
*
* @param config
* a <code>ServletConfig</code> object containing the servlet's
* configuration and initialization parameters
*
* @exception ServletException
* if an exception has occurred that interferes with the
* servlet's normal operation
*
* @see UnavailableException
* @see #getServletConfig
*/
public void init(ServletConfig config) throws ServletException;
/**
*
* Returns a {@link ServletConfig} object, which contains initialization and
* startup parameters for this servlet. The <code>ServletConfig</code>
* object returned is the one passed to the <code>init</code> method.
*
* <p>
* Implementations of this interface are responsible for storing the
* <code>ServletConfig</code> object so that this method can return it. The
* {@link GenericServlet} class, which implements this interface, already
* does this.
*
* @return the <code>ServletConfig</code> object that initializes this
* servlet
*
* @see #init
*/
public ServletConfig getServletConfig();
/**
* Called by the servlet container to allow the servlet to respond to a
* request.
*
* <p>
* This method is only called after the servlet's <code>init()</code> method
* has completed successfully.
*
* <p>
* The status code of the response always should be set for a servlet that
* throws or sends an error.
*
*
* <p>
* Servlets typically run inside multithreaded servlet containers that can
* handle multiple requests concurrently. Developers must be aware to
* synchronize access to any shared resources such as files, network
* connections, and as well as the servlet's class and instance variables.
* More information on multithreaded programming in Java is available in <a
* href
* ="http://java.sun.com/Series/Tutorial/java/threads/multithreaded.html">
* the Java tutorial on multi-threaded programming</a>.
*
*
* @param req
* the <code>ServletRequest</code> object that contains the
* client's request
*
* @param res
* the <code>ServletResponse</code> object that contains the
* servlet's response
*
* @exception ServletException
* if an exception occurs that interferes with the servlet's
* normal operation
*
* @exception IOException
* if an input or output exception occurs
*/
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
/**
* Returns information about the servlet, such as author, version, and
* copyright.
*
* <p>
* The string that this method returns should be plain text and not markup
* of any kind (such as HTML, XML, etc.).
*
* @return a <code>String</code> containing servlet information
*/
public String getServletInfo();
/**
* Called by the servlet container to indicate to a servlet that the servlet
* is being taken out of service. This method is only called once all
* threads within the servlet's <code>service</code> method have exited or
* after a timeout period has passed. After the servlet container calls this
* method, it will not call the <code>service</code> method again on this
* servlet.
*
* <p>
* This method gives the servlet an opportunity to clean up any resources
* that are being held (for example, memory, file handles, threads) and make
* sure that any persistent state is synchronized with the servlet's current
* state in memory.
*/
public void destroy();
}
서블릿은 javax.servlet.package에 정의된 인터페이스입니다. 이 인터페이스는 서블릿의 LifeCycle을 위한 3가지 필수적인 메서드들을 정의합니다.
1. init()
2. service()
3. destory()
이러한 메서드들은 모든 서블릿에서 구현되고 서버에 의해 호출됩니다.
1. init()
init() 메서드는 서블릿 생명 주기중 초기화 단계의 호출됩니다. javax.servlet.ServletConfig 인터페이스를 구현하는 객체가 전달되며, 이를 통해 서블릿이 웹 애플리케이션에서 초기화 매개변수에 접근할 수 있도록 합니다.
2.service()
service()는 초기화 이후 각각의 요청들이 왔을 때 호출되는 메서드입니다. java의 main메서드랑 비슷한 부분이 있습니다.
각각의 요청들이 하나씩 처리하게 되면 사용자는 무한히 기다릴 수도 있으므로 별도로 나누어진 스레드를 사용해 요청을 처리합니다. 웹 컨테이너는 모든 요청에 대해 서블릿의 service() 메서드를 요청합니다. service() 메서드는 요청의 종류를 판별하고 요청을 처리할 적절한 doGet(), doPost()의 메서드로 전달합니다.
3.destroy()
destroy메서드는 서블릿 객체가 파괴되어야 할 때 호출됩니다. 해당 서블릿이 가지고 있던 자원을 해제합니다.
서블릿 객체의 생명주기에서 서블릿 클래스가 클래스 로더에 의해 컨테이너에 동적으로 로드됩니다.
각 요청은 자체적인 스레드를 사용하며, 서블릿 객체는 동시에 여러 개의 스레드를 제공할 수 있습니다.
파괴가 되어야 할 때는 JVM의 GC로 처리되어야 합니다.
이제 Servlet의 주석을 보면
Called by the servlet container to allow the servlet to respond to a request.
서블릿 컨테이너에 호출되어 서블릿이 요청에 응답할 수 있다 라고 쓰여있습니다.
여기서의 request , response는 우리가 JSP에서 자주 사용했던 HTTP request, response를 의미합니다.
service메서드를 보면 servlet request, servlet response. 가 존재하며 우리는 Http request, Http response를 사용했습니다. 따라서 서블릿 요청을 http로 형 변환하는 과정이 필요할 것 같아 servlet interface를 상속받은 구현체를 찾아봅니다.
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {
...........
/**
* Called by the servlet container to allow the servlet to respond to a
* request. See {@link Servlet#service}.
* <p>
* This method is declared abstract so subclasses, such as
* <code>HttpServlet</code>, must override it.
*
* @param req
* the <code>ServletRequest</code> object that contains the
* client's request
* @param res
* the <code>ServletResponse</code> object that will contain the
* servlet's response
* @exception ServletException
* if an exception occurs that interferes with the servlet's
* normal operation occurred
* @exception IOException
* if an input or output exception occurs
*/
@Override
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
}
}
코드가 길어서 생략했습니다. 추상 클래스인 GenericServlet은 servlet을 implements 하고 있으며 service()를 구현하고 있습니다. 그런데 GenericServlet 또한 추상 메서드로 구현한 것을 발견했습니다.
그렇기에 다시 GenericServlet을 상속하거나, implement 한 클래스를 찾습니다.
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
HttpServletRequest request;
HttpServletResponse response;
try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException(lStrings.getString("http.non_http"));
}
service(request, response);
}
}
HttpServlet이 구현한 @override service()입니다. ServletRequest/Response를 HttpServlet으로 형 변환을 한 뒤 service()을 호출하고 있습니다.
구현한 service()는 다음과 같습니다.
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
// servlet doesn't support if-modified-since, no reason
// to go through further expensive logic
doGet(req, resp);
} else {
long ifModifiedSince;
try {
ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
} catch (IllegalArgumentException iae) {
// Invalid date header - proceed as if none was set
ifModifiedSince = -1;
}
if (ifModifiedSince < (lastModified / 1000 * 1000)) {
// If the servlet mod time is later, call doGet()
// Round down to the nearest second for a proper compare
// A ifModifiedSince of -1 will always be less
maybeSetLastModified(resp, lastModified);
doGet(req, resp);
} else {
resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
}
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} else {
//
// Note that this means NO servlet supports whatever
// method was requested, anywhere on this server.
//
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(HttpServletResponse.SC_NOT_IMPLEMENTED, errMsg);
}
}
눈 아프지만 결국 req.getMethod를 통해 요청된 알맞은 메서드를 실행시켜줍니다.
그중 post()를 확인해봅니다.
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_post_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
} else {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
}
}
HttpServlet이 구현한 doPost()는 단순히 프로토콜을 얻은 다음 조건 처리로 에러만 출력하고 있습니다.
HttpServlet 클래스의 하위 클래스인 FrameworkServlet 클래스도 doGet 메서드와 같은 HTTP method를 처리할 수 있는 메서드가 존재한다.
HttpServlet#service 메서드는 HttpServlet#doGet 메서드를 호출하는 게 아니라, FrameworkServlet#doGet 메서드를 호출한다.
FrameworkServlet 클래스에서 한 가지 주의 깊게 볼 것이 있다.
FrameworkServlet부터는 Spring-MVC에서 구현한 클래스다.
FrameworkServlet은 org.springframework.web.servlet 패키지에 존재한다. 이 클래스부터 하위 클래스는 Spring의 구현체다.
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
현재 HTTP request의 Locale을 구한다
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes =
buildRequestAttributes(request, response, previousAttributes);
HTTP request의 속성(session, request, response)을 담고 있는
ServletRequestAttributes 클래스를 생성한다.
WebAsyncManager asyncManager =
WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor
(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
현재 요청된 정보를 ThreadLocal에 담는다.
try {
doService(request, response);
FrameworkServlet#doService 메소드를 호출한다.
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
여기서 호출하는 doService()는 FrameworkServlet의 하위 클래스인 DispathcherServlet이 doService()를 구현하고 있다.
public class DispatcherServlet extends FrameworkServlet {
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
//HTTP 요청 log를 찍는다.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
// attributesSnapshot에 HTTP reqeust 속성을 보관한다.
}
}
}
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
WebApplicationContext, LocaleResolver, ThemeResolver, ThemeSource를 HTTP reuqest 속성에 담는다.
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
//FlashMap에 속성을 저장한다
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
try {
doDispatch(request, response);
//DispatcherServlet#doDispatch(request, response) 메소드 호출
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}
}
여기서 잠깐 스프링 MVC의 구성요소를 알아보자
DispatcherServelt은 모든 연결을 담당한다. 웹 브라우저로부터 요청이 들어오면 DispatcherServelt은 그 요청을 처리하기 위한 컨트롤러 객체를 검색한다. 다만 HandlerMapping이라는 빈 객체에게 컨트롤러 검색을 요청하게 된다.
그럼 HandlerMapping은 클라이언트의 요청 경로를 이용해서 이를 처리할 컨트롤러 빈객체를 DispatcherServelt에 전달하게 된다.
컨트롤러 객체를 DispatcherServelt이 전달받게 되면 바로 컨트롤러의 메서드를 실행하는 것이 아니라 @Controller , Controller interface , HttpRequestHandler interface를 동일한 방식으로 처리하기 위해 HandlerAdapter 빈을 사용한다.
DispatcherServelt는 요청 처리를 HandlerAdapter에게 위임하며 HandlerAdapter는 컨트롤러의 알맞은 메서드를 호출해서 요청을 처리하고 그 결과를 ModelAndView 객체로 변환해서 DispatcherServelt에게 리턴하게 됩니다.
처리결과를 받은 DispatcherServelt은 ViewResolver 빈 객체를 사용합니다.
ViewResolver는 리턴 받은 view 이름을 통해 해당되는 view객체를 찾거나 생성해 리턴합니다. 응답을 생성하기 위해 JSP를 사용하는 ViewResolver는 매번 새로운 View객체를 생성해서 리턴하게 됩니다.
DispatcherServelt는 리턴한 view객체에게 응답 결과 생성을 요청한다. JSP를 사용하는 경우 VIEW객체는 JSP를 실행 함으로써 웹브라우저에게 전송할 응답 결과를 생성하고 모든 과정이 종료됩니다.
DispatcherServlet의 doDispatcher메서드를 호출했습니다.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
//Multipart 요청 인지 체크한다.
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
/*HTTP 요청에 해당하는 HandlerExecutionChain을 구한다.
URL을 기준으로 HandlerMapping을 찾는다.
HandlerExecutionChain은 HandlerMapping에 의해 생성된다.
HandlerExecutionChain은 Interceptor List를 포함하고 있다.*/
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
//HandlerExecutionChain이 null이면 HTTP Status 404를 response 한다
return;
}
// Determine handler adapter for the current request.
//Controller를 실행하기 위해 HandlerAdapter를 구한다.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
//HandlerInterceptor로 전처리(Interceptor#preHandle)를 실행한다.
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
//HandlerAdapter#handle 메소드를 호출해 실제 로직(Controller)을 실행한 후 ModelAndView를 생성한다.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
//Interceptor로 후처리(Interceptor#postHandle)를 실행한다.
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
/*View를 렌더링 하거나, Exception을 핸들링한다.
View 렌더링은 ViewResolver가 담당한다.
Exception 핸들링은 HandlerExceptionResolver가 담당한다. */
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
'Spring|Spring-boot' 카테고리의 다른 글
[Spring-boot] 자동설정 (0) | 2020.06.26 |
---|---|
[Spring] @InitBinder 애노테이션을 이용한 컨트롤러 범위 Vaildator (0) | 2020.06.24 |
[Spring] 커맨드 객체 중첩 , 콜렉션 프로퍼티 (0) | 2020.06.23 |
[Spring] 단순 연결을 위한 컨트롤러 처리 (0) | 2020.06.23 |
[Spring] Java Configuration (0) | 2020.06.22 |
댓글