1. Servlet介绍
(1) Servlet是什么?
狭义的说,Servlet是JavaEE的一种技术规范。
广义的说,运行在服务器端的实现了该规范的java程序,都叫做Servlet。
(2) 服务器不是Tomcat吗,跟Servlet有啥关系?
Servlet只是Java程序,它需要部署到服务器上才能发挥作用。
而事实上,服务器可以分为很多种,比如应用服务器、Web服务器、数据库服务器等等,而Tomcat只是Web服务器中的一种,它被称为轻量级的Web服务器。
因此,Tomcat既是Web服务器,也是Servlet容器。
(3) 为啥Tomcat被称为轻量级的Web服务器,它到底轻在哪了呢?
原来,Sun公司在创建Java语言的时候,推出了3个版本:JavaSE、JavaEE、JavaME。
- JavaSE是标准版,用来开发部署在桌面、服务器等环境中使用的Java应用程序。比如电脑上的应用软件。
- JavaME是微型版,用来开发移动设备、机顶盒以及嵌入式等环境中的Java程序。(跟Android可不是一个东西)
- JavaEE是企业版,是在JavaSE的基础上构建的,用来帮助我们开发和部署可移植、健壮、可伸缩且安全的服务器端Java应用程序。它提供Web服务、组件模型、管理和通信 API,可以用来实现企业级的面向服务体系结构。比如用来做服务应用、网站开发等。
在JavaEE中,包含一大堆技术规范,而Tomcat主要只是实现了其中的Servlet和JSP规范,所以,就很轻。
(4) 服务器为什么需要Servlet?
服务器自己不能实现功能吗,要Servlet干啥?
我们先来看下客户端在访问服务器时,Tomcat中发生了些什么:
- 服务器启动创建线程池,并监听TCP端口。
- 客户端创建TCP连接发送HTTP请求(其实就是一段文本),服务器从线程池中取出一个线程来处理发送过来的请求,按照HTTP协议的格式来进行解析。解析的结果封装为一个HttpServletRequest对象,同时,线程会创建一个HttpServletResponse对象,用来收集业务逻辑处理之后的响应信息。
- 解析之后,线程根据请求路径,去Servlet容器中找到对应配置的Servlet对象,这个过程就是路由。
- Servlet对象调用其service方法,将request和response作为入参,然后进入业务逻辑。
- 业务逻辑处理完之后,线程将response中的数据按照HTTP协议的格式生成HTTP响应的文本。
- 线程通过TCP连接发送该响应文本,然后断掉连接,并回到线程池中。
可以看到,除了业务逻辑之外,其他的比如接收请求和响应请求等都是共性功能。程序员其实只关心自己的业务逻辑,所以这些共性功能就由服务器来实现。虽然这些共性功能的处理逻辑是不同的,但是它们都遵循了Servlet规范。
所以,Servlet其实就是服务器按照规范,实现共性功能的那部分程序。它一般由服务器厂商提供接口,然后由由程序员自己继承其接口进行差异化实现,或者可以直接使用某些框架内的实现。
(5) Servlet和MVC中的Controller、Service、Dao是啥关系?
Controller、Service、Dao其实已经是业务逻辑的实现层了,Servlet在它们的前边进行请求的解析,又在后边进行响应的处理。
所以说,Servlet是客户端与服务器或应用程序之间的中间层。
(6) Servlet的生命周期是什么?
其实根据上边的介绍,这个问题的答案已经呼之欲出了:
1) 加载和实例化
Servlet对象的加载其实有两种情况,一种是在服务启动时进行加载,一种是请求到来时进行加载,它取决于Servlet的配置load-on-startup。
load-on-startup的值表示该Servlet的优先级,如果值是非负数,越小优先级越高,在启动时就越优先加载这个Servlet。如果值是负数或者没有设置,则请求到来时才进行加载。
2) 初始化
加载完成后,Servlet容器会创建Servlet实例并调用init方法进行初始化。init方法只会被调用一次,它用来创建或加载一些数据,这些数据将被用于Servlet的整个生命周期。
3) 处理请求
service方法时执行实际任务的主要方法。Servlet 容器(即 Web 服务器)调service方法来处理来自客户端(浏览器)的请求,并把格式化的响应写回给客户端。
每次服务器接收到一个Servlet请求时,服务器会产生一个新的线程并调用服务。service方法检查 HTTP 请求类型(GET、POST、PUT、DELETE 等),并在适当的时候调用 doGet、doPost、doPut,doDelete 等方法。
4) 终止
当Web应用被终止,或者Servlet容器终止运行,或者Servlet容器重新装载Servlet新实例时,Servlet容器会调用Servlet的destroy方法进行销毁。destory方法也只会被调用一次,在调用之后,该servlet对象被标记为垃圾,等待回收。
其实整个生命周期的过程,看看Tomcat中Servlet接口也就大概清楚了:
2. Servlet实现
介绍完了这么多,那到底应该怎么去实现一个Servlet呢?
其实很简单,既然规范都已经有了,直接去创建一个实现了Servlet接口的类,并实现其中的方法就行了。
就拿Tomcat来举例,一般可以直接继承HttpServlet抽象类,它实现了Servlet接口。
public class MyServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException {
// 设置响应内容类型
response.setContentType("text/html");
// 实际的逻辑是在这里
PrintWriter out = response.getWriter();
out.println("<h1>Hello World!</h1>");
}
}
上边就实现了一个简单的Servlet,编译后部署在Tomcat目录下的 /webapps/ROOT/WEB-INF/classes 中,并在Tomcat目录下的 /webapps/ROOT/WEB-INF/web.xml 文件中新增以下内容:
<web-app>
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>MyServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/MyServlet</url-pattern>
</servlet-mapping>
</web-app>
最后启动Tomcat,在浏览器上访问http://localhost:8080/MyServlet 就可以看到Hello World!的页面。
不对吧,Servlet不是要实现init、service、destory等等方法吗?这里怎么实现了个doGet方法?
其实在HttpServlet及其父类GenericServlet类中就能找到答案:原来这些方法已经实现好了,只不过有的被实现为空了。
主要可以关注下具体执行逻辑的service方法:
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
// 省略
doGet(req, resp);
// 省略
} else if (method.equals(METHOD_HEAD)) {
// 省略
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 {
// 省略
}
}
原来如此,这里用了模板方法模式,开发者只需要去实现doGet、doPost等等方法就行了。
在service方法的注释上也说的很清楚:There's no need to override this method.
我们上边只实现了doGet方法,那如果来的是Post请求,会怎么样呢?
点开doPost方法的默认实现,可以看到其中的sendMethodNotAllowed方法会在response中写入错误信息:
private void sendMethodNotAllowed(HttpServletRequest req, HttpServletResponse resp, String msg) throws IOException {
String protocol = req.getProtocol();
// Note: Tomcat reports "" for HTTP/0.9 although some implementations
// may report HTTP/0.9
if (protocol.length() == 0 || protocol.endsWith("0.9") || protocol.endsWith("1.0")) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
} else {
resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
}
}
这下明白了,原来我们平时经常见到的400、404、405之类的错误码是这么回事。
3. SprintBoot中的Servlet
使用过SpringBoot的同学可能会有疑问:我咋没实现过什么Servlet,难道不是直接就开始写Controller的代码吗?
那有没有想过这样一种可能:Servlet已经给你实现好了。
(1) DispatcherServlet
事实上,这应该属于SpringMVC的功劳,它所实现的DispatcherServlet类是SpringMVC统一的入口,所有的请求都通过它。
DispatcherServlet是前端控制器,配置在web.xml中,Servlet依自已定义的具体规则拦截匹配的请求,分发到目标Controller来处理。
详细的DispatcherServlet分析可以看这篇文章,这里我做一个大概的流程总结:
1) Tomcat启动
-
DispatcherServlet同样也是继承自HttpServlet,它的init方法在其父类HttpServletBean中实现,主要作用是加载web.xml中DispatcherServlet的配置。
-
加载配置之后,调用initServletBean方法建立webApplicationContext上下文,将SpringMVC配置文件中定义的Bean加载到上下文中,并将上下文添加到ServletContext中。
-
建立好上下文后,通过onRefresh方法初始化SpringMVC。初始化会把Spring容器和SpringMVC容器中所有的HandlerMapping实例和HandlerAdapter实例放入列表中,这些实例是在初始化webApplicationContext上下文时创建的。
2) 客户端发送请求
-
Tomcat收到客户端的请求之后,如果匹配到DispatcherServlet在web.xml中配置的映射路径,就会将请求转交给DispatcherServlet处理。
-
列表中的每个HandlerMapping实例都会根据请求信息,去寻找处理该请求的Handler(执行程序,比如Controller中的方法)。
-
DispatcherServlet会从列表中找到可以处理该Handler的HandlerAdapter实例来进行处理,得到ModelAndView对象。
3) 服务端响应请求
- 根据返回的ModelAndView,选择一个适合的ViewResolver返回给DispatcherServlet。ViewResolver结合Model和View来渲染视图,最后将渲染结果返回给客户端。
(2) ServletContainerInitializer
看完上边的介绍后的同学可能还有个疑问:虽然有了DispatcherServlet,但是我也确实没配置过web.xml文件啊?
我们得弄清楚,为什么需要web.xml,因为它是用来配置Servlet的。如果我们配置好了DispatcherServlet并创建了上下文,那就不需要再配置web.xml了。
于是,在Servlet 3.0规范中,出现了一个新接口:ServletContainerInitializer,它的作用是能够通过编程的方式,动态的来注册Servlet、Filter、Listener的功能。
1) Tomcat中的ServletContainerInitializer
打开TomcatStarter类,可以看到它也是实现了ServletContainerInitializer,并实现了其中的onStartUp方法。
@Override
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
try {
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}
}
catch (Exception ex) {
this.startUpException = ex;
// Prevent Tomcat from logging and re-throwing when we know we can
// deal with it in the main thread, but log for information here.
if (logger.isErrorEnabled()) {
logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
+ ex.getMessage());
}
}
}
通过调试可以看到,this.initializers中就有ServletWebServerApplicationContext,通过调用其中的selfInitialize方法,从而实现了对DispatcherServlet和上下文的配置。
2) SpringMVC中的ServletContainerInitializer
在spring-web包的META-INF/services下,有一个javax.servlet.ServletContainerInitializer文件,里面只有一行:
org.springframework.web.SpringServletContainerInitializer
原来是使用了SPI的方式,加载了SpringServletContainerInitializer类。
这个类的代码如下:
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = Collections.emptyList();
if (webAppInitializerClasses != null) {
initializers = new ArrayList<>(webAppInitializerClasses.size());
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
}
@HandlesTypes(WebApplicationInitializer.class)注解会将所有WebApplicationInitializer接口的子类和接口传到onStartup方法的形参webAppInitializerClasses上,将不是接口和抽象类的类进行实例化,并添加到集合中。最后遍历调用集合中每一个对象的onStartup方法。
因此,如果我们想自定义初始化器,动态添加Servlet、Filter和Listener,也可以直接实现WebApplicationInitializer接口,交给Spring初始化就好了。
更详细的内容可以参考这篇文章。
(3) 自定义实现Servlet
除了上边介绍的方式,还有两种更为简洁常用的注册Servlet的方法。
1) @WebServlet注解实现
@WebServlet("/hello")
public class MyServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse response) throws IOException {
// 设置响应内容类型
response.setContentType("text/html");
// 实际的逻辑是在这里
PrintWriter out = response.getWriter();
out.println("<h1>Hello World!</h1>");
}
}
在Servlet类上使用了@WebServlet注解,这样访问http://localhost:8080/MyServlet 也可以看到Hello World!
当然,别忘了在启动类上增加@ServletComponentScan注解,这样才能扫描到我们创建的Servlet。
2) ServletRegistrationBean实现
@Configuration
public class ServletAutoConfiguration {
@Bean
public ServletRegistrationBean<HttpServlet> myServlet() {
return new ServletRegistrationBean<>(new MyServlet(), "/hello");
}
}
ServletRegistrationBean继承自RegistrationBean,后者是SpringBoot中广泛应用的注册类。
除了注册Servelt,它还可以用来注册Filter (FilterRegistrationBean) 和ServletListener(ServletListenerRegistrationBean)。
参考引用
Tomcat外传:zhuanlan.zhihu.com/p/54121733
为什么java需要servlet:www.zhihu.com/question/49…
web.xml中load-on-startup的作用:www.cnblogs.com/shamo89/p/9…
Servlet生命周期:www.runoob.com/servlet/ser…
Servlet实例:
www.runoob.com/servlet/ser…
DispatcherServlet源码分析:www.jianshu.com/p/9b7883c6a…
使用SpringBoot之后web.xml去哪儿了:blog.csdn.net/weixin_3412…
SpringBoot的底层入口SpringServletContainerInitializer:blog.csdn.net/weixin_4373…
SpringMVC源码深度解析之SpringServletContainerInitializer原理分析:blog.csdn.net/chuanyingca…