📄HttpServlet.class
public abstract class HttpServlet extends GenericServlet {
protected void doGet ...
protected void doPost ...
protected void service ...
}
서블릿 컨테이너는 이름과 시그니처가 약속된 메서드만 호출한다. 그래서 doGet
, doPost
, init
, destroy
, service
같은 훅 메서드를 정확히 override
해야 한다.
전체 생명 주기 |
---|
배포/시작 |
웹앱이 배포되고 컨테이너(Tomcat 등)가 시작 |
⬇️ |
클래스 로딩 & 인스턴스 생성 |
서블릿 클래스 로딩 👉 인스턴스 1개 생성(기본) |
⬇️ |
초기화 |
init (ServletConfig) 👉 보통 init() ovverride로 리소스 준비 |
⬇️ |
요청 처리 루프 (요청마다 반복) |
▫️ URL 매핑 결정 👉 필터 체인 전처리 👉 스레드에서 service(req, resp) 호출 ▫️ HttpServlet (HttpServletRequest, HttpServletResponse)가 HTTP 메서드에 따라 doGet/doPost/... 로 분기▫️ 응답 커밋 👉 필터 체인 후처리 👉 커넥션 반환 |
⬇️ |
종료 |
앱/컨테이너가 내려갈 때 destroy() 호출 👉 리소스 정리, 스레드풀 종료 |
⬇️ |
재로딩/핫리로드 |
변경/재배포 시 기존 인스턴스 destroy() 후 새 인스턴스 init() |
🕵️ 인스턴스는 보통 하나이고 요청마다 다른 스레드가
service()
를 호출한다. 따라서 인스턴스 필드에 요청상태를 저장하면 안됨
메서드 | 누가 호출? | 언제 | 기본 동작(override ❌) |
---|---|---|---|
init() | 컨테이너 | 서블릿 초기화 1회 | 아무것도 안함 (리소스 준비는 개발자 구현) |
service(ServletRequest, ServletResponse) | 컨테이너 | 모든 요청 | HTTP가 아니면 예외 HTTP면 아래 service로 위임 |
service(HttpServletRequest, HttpServletResponse) | HttpServlet | HTTP 요청 | 메서드별로 doXXX 디스패치, GET/HEAD의 캐싱 처리 포함 |
doGet() | HttpServlet#service | GET | 405/400 에러 (미구현시) |
doPost() | HttpServlet#service | POST | 405/400 에러 (미구현시) |
doPut() / doDelete() | HttpServlet#service | PUT / DELETE | 405/400 에러 (미구현시) |
doHead() | HttpServlet#service | HEAD | 기본은 doGet 재사용 (바디 제거) |
doOptions() | HttpServlet#service | OPTIONS | 서브클래스의 doXXX 존재 여부를 반영해 Allow헤더 자동 구성 |
doTrace() | HttpServlet#service | TRACE | 요청 라인 / 헤더 에코 (보안상 비활성 권장) |
destroy() | 컨테이너 | 종료 1회 | 리로스 정리는 개발자 구현 |
인터페이스
init(ServletConfig)
: 최초 1회 초기화service(ServletRequest, ServletResponse)
: 요청마다 호출destroy()
: 종료 시 1회 정리getServletConfig()
, getServletInfo()
포인트 : 여기엔 로직이 없고 "생명주기 훅이 이런 이름/시그니처로 제공된다" 수준의 스펙만 있다.
public interface Servlet {
void init(ServletConfig var1) throws ServletException;
ServletConfig getServletConfig();
void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
String getServletInfo();
void destroy();
}
추상 클래스 - 프로토콜 무관 기본 구현
Servlet
, ServletConfig
Servlet
의 계약을 기본 구현으로 편하게 제공init(ServletConfig)
안에서 this.config
보관 후 매개변수 없는 init()
을 호출getServletConfig()
, getServletContext()
, getInitParameter()
등 유틸 제공log()
같은 로깅 편의 메서드 제공service(ServletRequest, ServletResponse)
는 abstract
실제 요청 처리는 하위 클래스가 구현하도록 남겨둔다.public abstract class GenericServlet implements Servlet, ServletConfig, Serializable {
private static final String LSTRING_FILE = "javax.servlet.LocalStrings";
private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.LocalStrings");
private transient ServletConfig config;
public void destroy() {
}
public String getInitParameter(String name) {
ServletConfig sc = this.getServletConfig();
if (sc == null) {
throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
} else {
return sc.getInitParameter(name);
}
}
public Enumeration<String> getInitParameterNames() {
ServletConfig sc = this.getServletConfig();
if (sc == null) {
throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
} else {
return sc.getInitParameterNames();
}
}
public ServletConfig getServletConfig() {
return this.config;
}
public ServletContext getServletContext() {
ServletConfig sc = this.getServletConfig();
if (sc == null) {
throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
} else {
return sc.getServletContext();
}
}
public String getServletInfo() {
return "";
}
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {
}
public void log(String msg) {
this.getServletContext().log(this.getServletName() + ": " + msg);
}
public void log(String message, Throwable t) {
this.getServletContext().log(this.getServletName() + ": " + message, t);
}
public abstract void service(ServletRequest var1, ServletResponse var2) throws ServletException, IOException;
public String getServletName() {
ServletConfig sc = this.getServletConfig();
if (sc == null) {
throw new IllegalStateException(lStrings.getString("err.servlet_config_not_initialized"));
} else {
return sc.getServletName();
}
}
}
추상 클래스 - HTTP 전용 구현 + 메서드 분기
GenericServlet
GET/POST/PUT/DELETE/ . . .
메서드 별로 자동 분기해주는 틀을 제공service
2종public void service(ServletRequest, ServletResponse)
HTTP 전용 객체로 캐스팅 후 아래 HTTP 전용 service(HttpServletRequest, HttpServletResponse)
호출protected void service(HttpServletRequest, HttpServletResponse)
req.getMethod()
로 분기해서 doGet/doPost/doPut/doDelete . . .
호출doGet/doPost/doPut/doDelete/ . . .
getLastModified(HttpServletRequest)
doGet
과 연계되는 조건부 GET(if-Modified-Since) 지원 포인트 타임스탬프를 반환하면 Last-Modified
헤더 처리와 304 응답 최적화가 가능 (미구현 시 -1)public abstract class HttpServlet extends GenericServlet {
private static final String METHOD_DELETE = "DELETE";
private static final String METHOD_HEAD = "HEAD";
private static final String METHOD_GET = "GET";
private static final String METHOD_OPTIONS = "OPTIONS";
private static final String METHOD_POST = "POST";
private static final String METHOD_PUT = "PUT";
private static final String METHOD_TRACE = "TRACE";
private static final String HEADER_IFMODSINCE = "If-Modified-Since";
private static final String HEADER_LASTMOD = "Last-Modified";
private static final String LSTRING_FILE = "javax.servlet.http.LocalStrings";
private static ResourceBundle lStrings = ResourceBundle.getBundle("javax.servlet.http.LocalStrings");
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
protected long getLastModified(HttpServletRequest req) {
return -1L;
}
protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
NoBodyResponse response = new NoBodyResponse(resp);
this.doGet(req, response);
response.setContentLength();
}
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(405, msg);
} else {
resp.sendError(400, msg);
}
}
protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_put_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
protected void doDelete(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_delete_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
}
private Method[] getAllDeclaredMethods(Class<? extends HttpServlet> c) {
Class<?> clazz = c;
Method[] allMethods;
for(allMethods = null; !clazz.equals(HttpServlet.class); clazz = clazz.getSuperclass()) {
Method[] thisMethods = clazz.getDeclaredMethods();
if (allMethods != null && allMethods.length > 0) {
Method[] subClassMethods = allMethods;
allMethods = new Method[thisMethods.length + allMethods.length];
System.arraycopy(thisMethods, 0, allMethods, 0, thisMethods.length);
System.arraycopy(subClassMethods, 0, allMethods, thisMethods.length, subClassMethods.length);
} else {
allMethods = thisMethods;
}
}
return allMethods != null ? allMethods : new Method[0];
}
protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Method[] methods = this.getAllDeclaredMethods(this.getClass());
boolean ALLOW_GET = false;
boolean ALLOW_HEAD = false;
boolean ALLOW_POST = false;
boolean ALLOW_PUT = false;
boolean ALLOW_DELETE = false;
boolean ALLOW_TRACE = true;
boolean ALLOW_OPTIONS = true;
for(int i = 0; i < methods.length; ++i) {
String methodName = methods[i].getName();
if (methodName.equals("doGet")) {
ALLOW_GET = true;
ALLOW_HEAD = true;
} else if (methodName.equals("doPost")) {
ALLOW_POST = true;
} else if (methodName.equals("doPut")) {
ALLOW_PUT = true;
} else if (methodName.equals("doDelete")) {
ALLOW_DELETE = true;
}
}
StringBuilder allow = new StringBuilder();
if (ALLOW_GET) {
allow.append("GET");
}
if (ALLOW_HEAD) {
if (allow.length() > 0) {
allow.append(", ");
}
allow.append("HEAD");
}
if (ALLOW_POST) {
if (allow.length() > 0) {
allow.append(", ");
}
allow.append("POST");
}
if (ALLOW_PUT) {
if (allow.length() > 0) {
allow.append(", ");
}
allow.append("PUT");
}
if (ALLOW_DELETE) {
if (allow.length() > 0) {
allow.append(", ");
}
allow.append("DELETE");
}
if (ALLOW_TRACE) {
if (allow.length() > 0) {
allow.append(", ");
}
allow.append("TRACE");
}
if (ALLOW_OPTIONS) {
if (allow.length() > 0) {
allow.append(", ");
}
allow.append("OPTIONS");
}
resp.setHeader("Allow", allow.toString());
}
protected void doTrace(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String CRLF = "\r\n";
StringBuilder buffer = (new StringBuilder("TRACE ")).append(req.getRequestURI()).append(" ").append(req.getProtocol());
Enumeration<String> reqHeaderEnum = req.getHeaderNames();
while(reqHeaderEnum.hasMoreElements()) {
String headerName = (String)reqHeaderEnum.nextElement();
buffer.append(CRLF).append(headerName).append(": ").append(req.getHeader(headerName));
}
buffer.append(CRLF);
int responseLength = buffer.length();
resp.setContentType("message/http");
resp.setContentLength(responseLength);
ServletOutputStream out = resp.getOutputStream();
out.print(buffer.toString());
}
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
if (method.equals("GET")) {
long lastModified = this.getLastModified(req);
if (lastModified == -1L) {
this.doGet(req, resp);
} else {
long ifModifiedSince = req.getDateHeader("If-Modified-Since");
if (ifModifiedSince < lastModified) {
this.maybeSetLastModified(resp, lastModified);
this.doGet(req, resp);
} else {
resp.setStatus(304);
}
}
} else if (method.equals("HEAD")) {
long lastModified = this.getLastModified(req);
this.maybeSetLastModified(resp, lastModified);
this.doHead(req, resp);
} else if (method.equals("POST")) {
this.doPost(req, resp);
} else if (method.equals("PUT")) {
this.doPut(req, resp);
} else if (method.equals("DELETE")) {
this.doDelete(req, resp);
} else if (method.equals("OPTIONS")) {
this.doOptions(req, resp);
} else if (method.equals("TRACE")) {
this.doTrace(req, resp);
} else {
String errMsg = lStrings.getString("http.method_not_implemented");
Object[] errArgs = new Object[1];
errArgs[0] = method;
errMsg = MessageFormat.format(errMsg, errArgs);
resp.sendError(501, errMsg);
}
}
private void maybeSetLastModified(HttpServletResponse resp, long lastModified) {
if (!resp.containsHeader("Last-Modified")) {
if (lastModified >= 0L) {
resp.setDateHeader("Last-Modified", lastModified);
}
}
}
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
if (req instanceof HttpServletRequest && res instanceof HttpServletResponse) {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
this.service(request, response);
} else {
throw new ServletException("non-HTTP request or response");
}
}
}
호출 흐름 |
---|
컨테이너 |
⬇️ |
service(ServletRequest, ServletResponse) |
⬇️ |
(HttpServletRequest/Response로 캐스팅) |
⬇️ |
service(HttpServletRequest, HttpServletResponse) |
⬇️ |
HTTP 메서드별 doXXX ... 로 분기 |
doGet()
, doPost()
만 구현하면 충분service(HttpServletRequest, HttpServletResponse)
를 오버라이드하고 반드시 super.service(req, resp)
호출해서 기본 분기 유지@Override
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
long t0 = System.nanoTime();
try {
super.service(req, resp); // doGet/doPost 분기 유지!
} finally {
log(req.getMethod() + " " + req.getRequestURI() + " took " + (System.nanoTime()-t0));
}
}
💡정말 필요할 때만 :
service(ServletRequest, ServletResponse)
를 건드리자 (비-HTTP 요청 차단 등 특수 케이스)
@WebServlet("/hello")
public class HelloServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setCharacterEncoding("UTF-8");
resp.setContentType("text/plain;charset=UTF-8");
resp.getWriter().println("Hello, GET!");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
// 폼 처리 등
resp.getWriter().println("Hello, POST!");
}
}