Java Servlet理解
迷思
我是半路出家的和尚,一上手java开发就被人带进了springboot的门。对于这种封装良好的框架,我的感情都比较复杂。好处是它封装了绝大部分的细节,让开发者把精力集中在业务逻辑上,减少了很多重复劳动,并且很多默认配置都是久经考验的best practice,能减少水平和我一样不咋地的程序员因为对底层知识了解不足给自己挖坑。不好的是他几乎屏蔽了所有的细节,作为一个喜欢追根问底的程序员,无法掌控自己的程序是一件让人心虚的事情,而且一旦你对“默认配置”不了解,写出了不符合“约定”的代码,debug时就老虎吃天,无处下爪。
所以在java web开发中,无数次看到servlet却又不了解它是什么,特别是在一些书上看到这是java web的基础,一定需要了解,这就让我更加无所适从。所以着力了解了一下,将结果总结梳理如下:
从静态资源到动态资源:CGI简史
早期的web应用主要用于浏览新闻等静态页面,但随着web的发展,用户需要一些交互操作,获取动态结果,因此出现了一些扩展机制来实现动态资源的生成。早期使用的web服务器机制是CGI,当用户访问一些特定网址,典型的如http://example.cpm/cgi-bin/helloworld.cgi时,web服务器运行CGI程序,获取结果,再包装好将其返回。
对于操作系统来讲,CGI程序并没有什么特别,只是一个普通的可执行为文件而已,任何一个可执行文件,只要他:1. 可以获取环境变量;2.可以读取标准输入;3.可以向标准输出写入文件。即可作为一个CGI。
实际运行时,web服务器在指定文件夹下寻找URL中指定的cgi程序,通过unix经典的fork-execute启动一个进程,将一些变量设置为环境变量,使CGI程序可以获取,再将一些输入写入CGI程序的标准输入之中,CGI处理后直接将结果写入其标准输出,此时它的标准输出已经重定向到web服务器,web服务器对其包装后返回。
CGI解决了一段时间的问题,但因为每次都启动一个新的进程,效率很低,资源消耗太大。因此后来出现了很多技术去优化CGI,如Fast-CGI,实际上就是维护了一个CGI的进程池。同时也有一些技术用于替换CGI,比如java的Servlet、python的WSGI。
所以总结一下,无论是CGI还是Servlet,他本质上都是一个用于生成动态资源的程序,至于这个程序到底是以进程的方式一个请求启动一次,还是以进程池的方式维护,还是在程序中以线程池的方式维护,则不是重点。
servlet和servlet容器:基本概念
虽然都是用于生成动态资源的,但无论是CGI还是fastCGI,显然都存在一些性能问题。作为继承者之一的servlet,相比传统CGI,他的性能更好,采用线程运行程序,因为用java写所以独立于平台,可以使用java庞大的轮子库等。所以在web app开发领域,servlet占据了一席之地。
与传统CGI不同的是,servlet并不是可以在操作系统中直接独立运行的可执行文件。实际上,他只是java的一个接口,实现了这个接口的所有java class文件都是一个servlet。这个接口如下:
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
public ServletConfig getServletConfig();
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;
public String getServletInfo();
public void destroy();
}
非常简单的五个方法,并且没有main方法。也就意味着,一个servlet并不能像一个传统CGI程序一样独立运行。那么它怎么运行呢?
答案是:它必须处于一个servlet容器中,才可以运行,其实应该说,被servlet容器调用。
servlet容器负责大部分接收请求、解析请求、路由等工作,而作为web开发者,我们只需要实现具体的业务代码——一个实现了servlet的类,再将这个类打包,然后把它放到servlet容器中,再设置好路由信息,那么当请求到来时,servlet容器就会调用这个servlet,完成相应的工作,返回相应的处理结果。
servlet容器有一套规范,实现了该规范,可以调用servlet完成工作的软件,都可以称为servlet容器。
因此,servlet容器才更像传统意义上的CGI进程,而servlet就是它的一个组成部分、一个工具而已。servlet容器接受webserver的调用,再初始化对应的servlet(init方法),之后使用该servlet处理请求(service方法),在请求处理完毕之后,一般来说servlet并不会被释放,以便更快响应之后的同路径请求,但如果因为内存不足而产生GC动作,servlet容器会调用servlet的destroy方法,释放相关资源。
根据servlet容器的工作模式不同,可以将servlet分为三类:
- 独立的Servlet容器
当我们使用基于Java技术的Web服务器时,Servlet容器作为构成Web服务器的一部分而存在。然而大多数的Web服务器并非基于Java,因此,就有了下面两种Servlet容器的工作模式。
-
进程内的Servlet容器
Servlet容器由Web服务器插件和Java容器两部分的实现组成。Web服务器插件在某个Web服务器内部地址空间中打开一个 JVM(Java虚拟机),使得Java容器可以在此JVM中加载并运行Servlet。如有客户端调用Servlet的请求到来,插件取得对此请求的控 制并将它传递(使用JNI技术)给Java容器,然后由Java容器将此请求交由Servlet进行处理。进程内的Servlet容器对于单进程、多线程 的服务器非常适合,提供了较高的运行速度,但伸缩性有所不足。
-
进程外的Servlet容器
Servlet容器运行于Web服务器之外的地址空间,它也是由Web服务器插件和Java容器两部分的实现组成的。Web服务器插件和Java容 器(在外部JVM中运行)使用IPC机制(通常是TCP/IP)进行通信。当一个调用Servlet的请求到达时,插件取得对此请求的控制并将其传递(使 用IPC机制)给Java容器。进程外Servlet容器对客户请求的响应速度不如进程内的Servlet容器,但进程外容器具有更好的伸缩性和稳定性。
可见,servlet容器并不是一个webserver,他只是一个传统意义上的CGI替代者而已。但因为既然他本身就要用来处理web请求,如果要求大家在写完servlet以后,还要部署一个独立的webserver,不太方便,所以很多servlet容器同时包含了webserver的功能,如Tomcat等,但不代表servlet容器就是一个web服务器。
从HelloWorld跑起:一个示例
- servlet class
现在我们大概知道了,servlet是处理动态资源的程序,而且他不能独立运行,需要运行于servlet容器中。为了使这个过程更直观,我们来写一个最简单的HelloWorld示例。
package com.lifeStory; //要注意到这个包名 import javax.servlet.*; import java.io.IOException; public class TestServlet implements Servlet { private ServletConfig config; @Override public void init(ServletConfig config) throws ServletException { this.config = config; } @Override public ServletConfig getServletConfig() { return config; } @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { req.getAttributeNames(); res.setContentType("text/html"); res.getWriter().println("<h1> Hello World! </h1>"); res.flushBuffer(); } @Override public String getServletInfo() { return "I'm a test servlet"; } @Override public void destroy() { //don't know what to do; } }在JDK1.8中,需要引入Servlet包,我用Maven:
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency>写完之后打包:
javac -cp ~/.m2/repository/javax/servlet/javax.servlet-api/4.0.1/javax.servlet-api-4.0.1.jar TestServlet.java之后可以找到一个
TestServlet.class文件,这就是我们需要的servlet文件了。 -
tomcat
我们以tomcat为例,快速部署如下(以macOS和tomcat 8.5.38为例):
- 下载tomcat。tomcat官方下载地址
有趣的是,这个官方下载地址的url,结尾是一个cgi。Emmm……
-
解压到本地,如桌面
-
为所有的
.sh文件赋予执行权限。 -
运行
./bin/startup.sh
完整命令如下:
cd ~/Desktop wget http://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-8/v8.5.38/bin/apache-tomcat-8.5.38.tar.gz tar -zxf apache-tomcat-8.5.38.tar.gz sudo chmod +x ./apache-tomcat-8.5.38/bin/*.sh ./apache-tomcat-8.5.38/bin/startup.sh- 打开http://localhost:8080,就可以看到安装成功的画面,此处不附了。
此时,我们也先不了解这个tomcat的那些乱七八糟的机制,我们只需要知道,tomcat要求我们把servlet文件放到
webapps/ROOT/WEB-INF/classes下即可,并且需要调整webapps/ROOT/WEB-INF/web.xml文件指定路由路径,即可运行这个servlet。那么以我们上面的servlet文件为例,因为他在一个包中
package com.lifeStory,所以根据要求,我们要这个class文件放到webapps/ROOT/WEB-INF/classes/com/lifeStory中,然后调整web.xml设置如下:<web-app> <servlet> <servlet-name>TestServlet</servlet-name> <servlet-class>com.lifeStory.TestServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>TestServlet</servlet-name> <url-pattern>/TestServlet</url-pattern> <!-->url-pattern是大小写敏感的</--> </servlet-mapping> </web-app>原来的
<web-app></web-app>中可能已经有一些其他元素了,我们不要动他,直接把新的内容添加进去即可。<servlet-mapping></servlet-mapping>指定了映射路径,即发往http://localhost:8080/TestServlet的请求,由名为TestServlet的servlet处理,而<servlet></servlet>指定了这个servlet的类是com.lifeStory.TestServlet。我们再请求http://localhost:8080/TestServlet,即可看到结果,一个硕大的
Hello world!。注意,这个路径是大小写敏感的。 - 下载tomcat。tomcat官方下载地址
-
we are going to
WARnow上面手动建目录结构(主要是要符合package结构)的方式对于一两个class而言还可以接受,但对于一个比较大的项目,显然是要玩死自己的节奏。为了更好地组织比较大型的项目,更常见的方式是将项目打包成一个
WAR包,上传到servlet容器中去。为了打包项目,我们的项目需要按照一定的层次结构来布置:
|--src |--main |--java |--com.lifeStory (包路径,两层目录) |--TestServlet.java |--webapp |--WEB-INF |--web.xml |--target (打包以后自己生成的,不需要手动建)这是默认设置,如果需要更改路径层次,则需要在
pom.xml中指定目录层次结构。我们先跑起来这个项目,再来看如何更改这个项目。目前我们的pom文件如下:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.lifeStory</groupId> <artifactId>testServlet</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <maven.compiler.target>1.8</maven.compiler.target> <maven.compiler.source>1.8</maven.compiler.source> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>testServlet</finalName> </build> </project>注意其中的
<packaging>war</packaging>和<build><finalName>testServlet</finalName></build>标签,这一个是制定了maven打包结果是war包,一个是指定了打包后war的名字是testServlet。mvn package接着在target目录下看到一个
testServlet.war,将它复制到tomcat/webapps目录下,然后重启tomcat,重启后你可以发现tomcat/webapps下多了一个文件夹,名字是testServlet,目录结构如下:|--tomcat |--webapps |--ROOT |--WEB-INF |--web.xml |--classes (这是我们之前放进去的目录) |--com |--lifeStroy |--TestServlet.class |--testServlet |--META-INF (具体内容暂时忽略) |--WEB-INF |--web.xml |--classes |--com |--lifeStroy |--TestServlet.class |--docs (以下都是tomcat自带的文件夹,具体内容暂时忽略) |--examples |--host-manager |--manager其实
testServlet文件夹就是testServlet.war解压后的结果。(手动解压可以
jar -xvf testServlet.war)之后可以打开
http://localhost:8080/testServlet/TestServlet,就可以看到和之前http://localhost:8080/TestServlet一样的结果。有了直观的认识,我们再返回来看tomcat的webapps文件夹结构,可见这个文件夹下每一个文件夹都代表了一个url路径,比如说我们建了
testServlet文件夹,那就意味着我们可以访问http://www.yourdomain.com:port/testServlet,至于这里面的子路径是否可以访问,则取决于该目录下的web.xml文件,我们的web.xml没有映射根目录,也没有index.html文件,因此无法访问http://www.yourdomain.com:port/testServlet,只能访问http://www.yourdomain.com:port/testServlet/TestServlet。所以说,之前我们看到的docs、examples、host-manager、manager,全都是可以访问的路径。我们可以用
http://localhost:8080/docs等路径来访问即可。这里面一个特殊的文件夹是ROOT,他是根路径,也就是说我们不需要用
http://localhost:8080/ROOT来访问,直接访问http://localhost:8080即可。
再看Servlet
-
什么是servlet
看过了上面的例子,再来回答什么是servlet。
所谓的servlet,原来其物理存在方式是一个java class,其作用是产生动态资源,而其工作方式是,被放在servlet容器中,并通过
web.xml指定路径映射关系。这样当用户从客户端访问某个路径时,serlvet容器将会根据web.xml指定的映射关系,调用映射到的servlet,调用它,产生结果,并返回结果。之后该servlet并不会被retire,而是一直放在内存中,以提高下一次web请求的响应速度。只有在内存不足产生GC的时候,才有可能被释放。
-
servlet与协议
众所周知一,网络协议不止HTTP一个,ftp、stmp等协议也被广泛应用。那么servlet是否支持其他协议呢?
答案是:理论上支持,但实际上不支持。
理论上支持的依据,从servlet的一些相关类中就可以找到端倪,比如
servlet.service方法,接受的两个参数包括:ServletRequest req, ServletResponse res,拆开ServletRequest的源码可以看到其中一个注释:/** * Returns the name of the scheme used to make this request, * for example, * <code>http</code>, <code>https</code>, or <code>ftp</code>. * Different schemes have different rules for constructing URLs, * as noted in RFC 1738. * * @return a <code>String</code> containing the name * of the scheme used to make this request */ public String getScheme();其中提到了
ftp方法,可见理论上servlet本身并不关注协议是什么。更强的例证在这段源码:/** * * Defines a generic, protocol-independent * servlet. To write an HTTP servlet for use on the * Web, extend {@link javax.servlet.http.HttpServlet} instead. * * <p><code>GenericServlet</code> implements the <code>Servlet</code> * and <code>ServletConfig</code> interfaces. <code>GenericServlet</code> * may be directly extended by a servlet, although it's more common to extend * a protocol-specific subclass such as <code>HttpServlet</code>. * * @author Various */ public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable{}这是几乎所有
Servlet实现类真正继承的父类(极少像上文那样直接implement Servlet),他也开宗明义地表示自己是protocol-independent。还有一篇文章专门写到了这个问题,Types of Servlets。那么为什么实际上不支持呢?因为servlet并不能单独运行,它必须部署在servlet容器中,servlet容器负责请求的理解,并且将请求封装为一个
ServletRequest类,再调用servlet.service()来处理。而不幸的是,目前主流的servlet容器,都只能理解Http协议。也就是说,我们在上面实验中那些可以访问的地址,当你在前面制订协议为ftp时,都不能得到回应。更多的消息可以看这里:Using GenericServlet for FTP,关键段落如下:
It is not really possible to make servlets for handling other protocols than HTTP.
The protocol is handled by the servlet container, not the servlet itself. If you want to have FTP commands sent to a servlet, you need a special servlet container that handles the FTP protocol and sends the received FTP commands to a servlet. As far as I know there are no servlet containers available that can do this.
But since Tomcat is open-source you COULD dive into the guts of the container and do it but that’s a PhD-thesis sized project.
- servlet与Http方法
众所周知,一个HTTP路径可以对应多种HTTP方法,典型的如Get、Post、Put、Delete等。而遗憾的是,因为如上一小节所述,
servlet是协议无关的,也就是说这种原始的servlet,无法从ServletRequest中获得HTTP的相关信息,也就不会对这些方法进行区分。所以在上面的例子中,大可以用任何Http方法去请求那个路径,得到的结果都是完全一致的。 -
HttpServlet——最常扩展的servlet父类
如上所述,主流servlet容器只能理解、包装和转发Http协议,所以其实servlet预留的那些协议无关的接口或抽象类我们都用不上。我们在开发servlet时用到的父类基本是
HttpServlet。而HttpServlet的主要注释方法如下:
/** * * Provides an abstract class to be subclassed to create * an HTTP servlet suitable for a Web site. A subclass of * <code>HttpServlet</code> must override at least * one method, usually one of these: * * <ul> * <li> <code>doGet</code>, if the servlet supports HTTP GET requests * <li> <code>doPost</code>, for HTTP POST requests * <li> <code>doPut</code>, for HTTP PUT requests * <li> <code>doDelete</code>, for HTTP DELETE requests * <li> <code>init</code> and <code>destroy</code>, * to manage resources that are held for the life of the servlet * <li> <code>getServletInfo</code>, which the servlet uses to * provide information about itself * </ul> * * <p>There's almost no reason to override the <code>service</code> * method. <code>service</code> handles standard HTTP * requests by dispatching them to the handler methods * for each HTTP request type (the <code>do</code><i>XXX</i> * methods listed above). * * <p>Likewise, there's almost no reason to override the * <code>doOptions</code> and <code>doTrace</code> methods. * * <p>Servlets typically run on multithreaded servers, * so be aware that a servlet must handle concurrent * requests and be careful to synchronize access to shared resources. * Shared resources include in-memory data such as * instance or class variables and external objects * such as files, database connections, and network * connections. * See the * <a href="https://docs.oracle.com/javase/tutorial/essential/concurrency/"> * Java Tutorial on Multithreaded Programming</a> for more * information on handling multiple threads in a Java program. * * @author Various */ public abstract class HttpServlet extends GenericServlet{}如注释所言,你只需要实现
doGet(),doPost()等方法就可以了。具体这个servlet会调用哪种方法,是在httpServlet.service()方法中决定的:@Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; if (!(req instanceof HttpServletRequest && res instanceof HttpServletResponse)) { throw new ServletException("non-HTTP request or response"); } request = (HttpServletRequest) req; response = (HttpServletResponse) res; service(request, response); } 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 = req.getDateHeader(HEADER_IFMODSINCE); if (ifModifiedSince < lastModified) { // 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); } }@Override的那个service方法将实际工作转发给了接受HttpServletRequest req, HttpServletResponse resp的service方法。从而获得http method,将工作转发给不同的方法:doGet(),doPost()来完成。可以看到,这是一个抽象类,但实际上,他的所有方法都有实现。原因是,他没法判断你扩展这个类是做什么,有可能你只需要处理Get方法,那强行要求你实现doPost方法,就不合理。所以这个抽象类实现了所有的方法,但是如果你真的敢不实现
doGet(),doPost()就去调用它,就会发现被抛异常了。值得注意的是,web.xml文件中的servlet映射只能映射路径,而不能映射方法,也就是说,对该路径的各种方法请求都会被转发到同一个servlet中进行处理,假如没实现相应的方法,那就只能接受异常了。返回的页面截图如下:

稍微仔细观察一下Tomcat
-
重要的几个就是/bin(存放启动和关闭tomcat的脚本)、/conf(配置文件,其中最重要的是server.xml文件),/webapps我们网站的真正内容存放的地方。
每个webapps的子文件夹都自动映射了一个url的子路径,如上文的
testServlet文件夹,就映射到了http://localhost:8080/testServlet/路径上。每个子文件夹中的web.xml文件则决定了url子路径的映射规则,比如上文中web.xml的规则:<servlet-mapping> <servlet-name>TestServlet</servlet-name> <url-pattern>/TestServlet</url-pattern> <!-->url-pattern大小写敏感<--> </servlet-mapping>确定了
/TestServlet这个路径(相对于http://localhost:8080/testServlet/)映射到TestServlet这个servlet上进行处理。而这个servlet的详细位置,也在web.xml中规定好:<servlet> <servlet-name>TestServlet</servlet-name> <servlet-class>com.lifeStory.TestServlet</servlet-class> </servlet>需要重点说一下的是
WEB-INF文件夹,这是tomcat的默认安全文件夹,即客户端不能通过直接访问http://localhost:8080/testServlet/WEB-INF/...获取这个文件夹下的任何资源,这个文件夹下的所有资源,都必须通过web.xml中写好的映射关系来访问。
嵌入式tomcat
重点概念:Context 简而言之,一个Context就是一个web应用
到目前为止,我们看到的都是直接编译servlet.class文件,或者将一堆servlet打成war包的形式,放到tomcat的指定目录下进行部署的。也就是说,tomcat服务器已经存在于服务器,我们将我们的web app打包放进去,让他来部署。但这种方式也有一些不足之处。比如说:
- 如果要部署一款应用到新的服务器上,往往首先要下载一款服务器,比如tomcat,之后修改端口号、添加个性化配置、解决我的web app的jar包依赖与其他web app的jar包依赖的冲突。这增加了部署的复杂度。
- 假如我们的服务器架构十分复杂,不仅提供HTTP服务,又提供其他服务如FTP等,这是一个统一的系统,而tomcat只是其中的一部分,根据系统的调度提供服务而已。这样将其部署为一个应用服务器就不是很合适。(书上讲的,其实这一点我不是很理解)
- 微服务架构带来的新变化。微服务与当前的Docker容器技术结合,可以更快部署,简化环境的复杂度。
总之,我们先来看看怎么在代码里以嵌入式的方式直接启动一个Tomcat。要注意的是,tomcat embed的大版本之间不兼容,我们先来看看8.x.x的启动方式,这是pom
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.38</version>
</dependency>
这是代码:
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class EmbedTomcat {
public static void main(String[] args) throws Exception {
Tomcat tomcat = new Tomcat();
HttpServlet servlet = new HomeServlet();
Context context = tomcat.addContext("/sample",null);
Tomcat.addServlet(context,"/servlet",servlet);
context.addServletMappingDecoded("/servlet","/servlet");
tomcat.init();
tomcat.start();
tomcat.getServer().await();
}
private static class HomeServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("request scheme: " + req.getScheme());
resp.getWriter().print("hello tomcat");
}
}
}
然后访问http://localhost:8080/sample/servlet即可看到结果。
这是最简单的,相当于对外只提供servlet服务,而不提供页面访问。那这种方式足够了,而且更简单(倒像是现在的restful接口)。但如果要提供一套标准web应用,提供jsp、html访问,这样是不行的。因为web应用的一些默认配置没有提供给context,tomcat不知道去哪里寻找这些静态资源,为解决这个问题,tomcat提供了addWebapp()方法来解决这个问题,首先在pom中添加:
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>8.5.38</version>
</dependency>
修改代码如下:
import org.apache.catalina.startup.Tomcat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class EmbedTomcat {
private static final int port = 9080;
public static void main(String[] args) throws Exception {
Tomcat tomcat = new Tomcat();
tomcat.addWebapp("/sample", "sample");
tomcat.addServlet("/sample", "servlet", new HomeServlet())
.addMapping("/servlet"); # 增加servlet的映射
tomcat.init();
tomcat.start();
tomcat.getServer().await();
}
}
注意tomcat.addWebapp("/sample", "sample");这一行,第二个参数实际上是添加了一个上下文地址,在独立启动tomcat中相当于webapps中新建了一个sample文件,可以指定绝对地址,也可以指定相对地址,如果像这样指定了相对地址,那么在IDEA里看起来就有这样的目录层次:

也就是说这个sample是相对于这个路径的。我们可以通过指定绝对地址来修改这个行为(推荐修改这个行为),只需要在上面的代码里添加以下两行:
String docBase = "src/main/webapp/sample";
tomcat.addWebapp("/sample", new File(docBase).getAbsolutePath());
修改后的目录结构如下:

注意上面tomcat.addWebapp("/sample", new File(docBase).getAbsolutePath())这一行,实际上它返回了一个context,即我们在第一个示例中创建的context,所以第二个示例代码还可以这么修改:
import org.apache.catalina.Context;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class EmbedTomcat {
public static void main(String[] args) throws Exception {
Tomcat tomcat = new Tomcat();
HttpServlet servlet = new HomeServlet();
String docBase = "src/main/webapp/sample";
Context context = tomcat.addWebapp("/sample", new File(docBase).getAbsolutePath());
Tomcat.addServlet(context, "/servlet", servlet);
context.addServletMappingDecoded("/servlet", "/servlet");
tomcat.init();
tomcat.start();
tomcat.getServer().await();
}
}
这样就完成了静态资源的部署。
简单的tomcat8部署就先说到这里,接下来看tomcat9的部署方式。先是pom文件
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.16</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>9.0.16</version>
</dependency>
之后是java:
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
public class EmbedTomcat {
public static void main(String[] args) throws Exception {
Tomcat tomcat = new Tomcat();
Connector connector = new Connector();
tomcat.getService().addConnector(connector);
connector.setPort(9527);
tomcat.setConnector(connector);
Context context = tomcat.addWebapp("/sample",
new File("src/main/webapp/sample").getAbsolutePath());
Tomcat.addServlet(context, "/servlet", new HomeServlet());
context.addServletMappingDecoded("/servlet", "/servlet");
tomcat.init();
tomcat.start();
tomcat.getServer().await();
}
}
其中部分代码是直接从spring源码里抄的。与tomcat8最大的不同是必须通过connector来设置port,不然不会监听端口。
可见无论是8、还是9,最重要的几项设置如下:
- addWebApp指定docBase,即这个context的静态资源位置。相当于将tomcat单独部署时的webapps下面的那些子文件夹。
-
通过addServlet添加context,相当于
web.xml中这一段# java代码: Tomcat.addServlet(context, "/servlet", new HomeServlet()); # web.xml <servlet> <servlet-name>TestServlet</servlet-name> <servlet-class>com.lifeStory.TestServlet</servlet-class> </servlet> - 添加servlet映射,相当于
web.xml中这一段:# java代码 context.addServletMappingDecoded("/servlet", "/servlet"); # web.xml <servlet-mapping> <servlet-name>TestServlet</servlet-name> <url-pattern>/TestServlet</url-pattern> </servlet-mapping>
到这一步聪明的弟兄们应该已经已经猜到了,既然这两者是相互对应的,那么可不可以手动建一个web.xml来替代这两行呢?答案是可以的。目录结构如下,其web.xml的内容也在截图中。
之后嵌入式代码中的这两行可以注释掉:
Tomcat.addServlet(context, "/servlet", new HomeServlet());
context.addServletMappingDecoded("/servlet", "/servlet");

Spring中的嵌入Tomcat与Controller
spring中的Servlet
至此,servlet到底是什么,tomcat又是什么,两者是什么关系,最简单的独立部署和嵌入式部署也完成了,接下来我们再来简单观察一下生产环境下最常用的Spring MVC框架是如何嵌入tomcat的吧。
我们在这里写一个极其简单的程序,首先是pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lifeStory</groupId>
<artifactId>simpleSpringWeb</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Main:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
Controller
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/")
public class SimpleController {
@GetMapping("/sample")
public String sample() {
return "hello, I'm sample";
}
}
在这里随便讲几句我自己对程序的理解:那就是没有魔法。所有的魔法都是别人帮你做了一个工具而已,所有的程序说一千道一万,最终还是化作二进制机器码,再转换为电流运行在电路板上,高级语言是为了抽象二进制机器码,框架是为了进一步抽象业务逻辑,然而中间的脏活累活总有人做,就像高级语言有编译器和链接器帮你把代码翻译为字节码,那么框架必然为你做了一些类似的工作。所以一定能追查下去,直到看到框架里每个人都能看得懂的基本语法。
所以追查这事情也是一样简单的,Spring框架用了嵌入式的tomcat,那么一定在哪里存在一个new Tomcat()语句,所以找到Tomcat这个类,find usage,可以看到有两个主要的类用到了它: TomcatReactiveWebServerFactory和TomcatServletWebServerFactory,这两个类里面有new Tomcat()语句,都打断点,启动一次,看到是TomcatServletWebServerFactory处暂停,找到如下代码:
@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory
: createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
可见这个方法不过是生成了一个WebServer的Bean,继续find这个方法的usage,找到类ServletWebServerApplicationContext,可以看这个类的注释,这个类就是一个context,而且:
any Servlet or Filter beans defined in the context will be automatically registered with the web server. In the case of a single Servlet bean, the ‘/’ mapping will be used. If multiple Servlet beans are found then the lowercase bean name will be used as a mapping prefix. Any Servlet named ‘dispatcherServlet’ will always be mapped to ‘/’. Filter beans will be mapped to all URLs (‘/*’)
可见Spring将http的根目录/映射到了dispatcherServlet上,打开全局搜索,寻找这个关键词,会找到这个类DispatcherServlet,这是一个Servlet,它实现了HttpServlet。那么他如何与我们的Controller打交道呢?根据我们上面对HTTPServlet的理解,HttpServlet的service()方法将实际工作委托给doService()方法,那我们在DispatcherServlet里寻找一下doService方法,还真有,并且在其源代码里发现了一个重要的方法调用:doDispatch,联系这个类的名字,可见这个方法应该是核心方法,打上断点,先跑一遍试试,逐步追踪下来发现了这一步:
紧接着有一段核心代码:
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
在ha.handle()方法中,命中我们自写的simpleController.sample()方法,至此,追踪基本可以说完成了,知道了Spring新建了一个DispatchServlet,并将其注册到/路径下,当请求到来时,内嵌的tomcat接收请求,并根据映射规则将其交由dispatchServlet实例处理,而他通过getHandler方法找到具体处理这个请求的controller方法,并通过handler唤醒它执行实际操作。
那么继续往深处追寻,进入getHandler()方法,源码如下(Spring源码用tab不是空格,差评):
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
打断点追寻,可见simpleController.sample()方法是在RequestMappingHandlerMapping这个类中查出来的,追查这个类,可见其内部使用了mappingRegistry这个域来保存相关的映射信息,这个域在RequestMappingHandlerMapping的超类AbstractHandlerMethodMapping中定义,且在public void register(T mapping, Object handler, Method method)这个方法中将controller中的内容注册到mappingRegistry中,如下图:
到此追踪就完成了,至于spring是如何将controller拆解,靠的主要是注解的解析,怎么调用register方法,靠的主要是AOP,不在此篇讨论范围之内了。
我们再回头研究一下上面引用中的话:
any Servlet or Filter beans defined in the context will be automatically registered with the web server. In the case of a single Servlet bean, the ‘/’ mapping will be used. If multiple Servlet beans are found then the lowercase bean name will be used as a mapping prefix. Any Servlet named ‘dispatcherServlet’ will always be mapped to ‘/’. Filter beans will be mapped to all URLs (‘/*’)
看他的意思,其实是可以直接注册其他servlet到Spring的web context,查了一下确实如此。新增代码如下:
// main
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@ServletComponentScan //关键注解
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
}
servlet如下:
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet(urlPatterns = "/myServlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
resp.getWriter().println("hello, I'm my servlet");
resp.getWriter().flush();
resp.getWriter().close();
}
}
追踪代码的话,会发现是ServletRegistrationBean这个类扫描了WebServlet注解,并将其注册到web context。
或者通过Register来配置:
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Collections;
@Configuration
public class ServletConfiguration {
@Bean
public MyServlet myServlet() {
return new MyServlet();
}
@Bean
public ServletRegistrationBean<MyServlet> myServletServletRegistrationBean1() {
ServletRegistrationBean<MyServlet> servletServletRegistrationBean = new ServletRegistrationBean<>();
servletServletRegistrationBean.setServlet(myServlet());
servletServletRegistrationBean.setUrlMappings(Collections.singleton("/myServlet"));
return servletServletRegistrationBean;
}
}
需要注意的是,@WebServlet和register方法不可共存。
另外需要注意的是,@WebServlet、@WebFilter、@WebListener是javax.servlet.annotation包内的内容,也就是说其实他属于servlet规范的一部分,不是Spring的注解,或者换句话说,Spring是实现了servlet规范,复用了这几个注解。
Spring与静态资源
Spring boot默认将对静态资源的请求导向resources/static/目录下,假如我们在这个文件夹下放一个index.html文件,如下图:

追踪对/的请求,可以看到还是到了dispatchServlet,在getHandlerAdapter()这一步,通过simpleUrlHandleMapping得到了一个handler,最终正确地访问了index.html。
Servlet与Listener、Filter、Interceptor
Listener
区别于servlet和filter的顺序执行模式,Listenner提供了另外一种处理request的方式,即监听某些事件的发生,在事件发生时进行某些处理,是一种比较典型的观察者设计模式的应用。为web应用提供了一种纵向维度的控制方法。典型应用比如监听session的创建和销毁,来监控目前系统的在线人数。目前servlet中提供了几类事件监听器,包括:
- ServletContext 相关接口
- ServletContextListener:用于监听 ServletContext 的启动和销毁。
- ServletContextAttributeListener:用于监听 application 范围的属性变化。
- HttpSession 相关接口
- HttpSessionListener](https://tomcat.apache.org/tomcat-9.0-doc/servletapi/javax/servlet/http/HttpSessionListener.html):用于监听 session 的创建和销毁。
- HttpSessionIdListener:用于监听 session 的 id 是否被更改。
- HttpSessionAttributeListener:用于监听 session 范围的属性变化。
- HttpSessionActivationListener:用于监听绑定在 HttpSession 对象中的 JavaBean 状态。
- HttpSessionBindingListener:用于监听对象与 session 的绑定和解绑。
- ServletRequest 相关接口
- ServletRequestListener:用于坚挺 ServletRequest 对象的初始化和销毁。
- ServletRequestAttributeListener:用于监听 ServletRequest 对象的属性变化。
简单举两个例子如下,其中@WebListener是Spring的注解,配合@ServletComponentScan注解,可以不用web.xml,直接将Listener注册到webContext。
@WebListener
public class MyListener2 implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
System.out.println(sre.getServletRequest().getRemoteHost());
}
}
@WebListener
public class MyListener implements ServletContextAttributeListener {
public void attributeAdded(ServletContextAttributeEvent event) {
System.out.println(event.getName() + "----->" + event.getValue().toString());
}
}
如果使用web.xml文件部署,则相应的配置是:
<listener>
2 <description>MyListener2监听器</description>
3 <listener-class>com.lifeStory.MyListener2</listener-class>
4 </listener>
Filter
Filter如其名,是一个过滤器,他的作用是在真正的servlet调用之前或者之后,或者之前和之后,对请求进行一定的处理。这种思想有点像面向切面编程的思想(AOP),所以这种Filter的作用也很容易想出来,一般就比如说设置字符集、控制转向、日志、权限认证、数据加密解密、图像转换过滤、数据压缩等。一个典型的Filter类似于Servlet,继承自Filter接口,通过web.xml文件配置。
其继承层次也是Filter >>> GenericFilter >>> HttpFilter,其中Filter包含三个接口,init、doFilter、典型的场景下,我们一般直接继承HttpServlet即可。如下:
import javax.servlet.DispatcherType;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class MyFilter extends HttpFilter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("经过Filter");
chain.doFilter(request, response);
response.getWriter().println("经过Filter处理");
((HttpServletResponse)response).setHeader("content-type", "text/html;charset=UTF-8");
System.out.println("处理后Filter");
}
}
web.xml中的配置如下:
<filter>
<filter-name>MyFilter</filter-name>
<filter-class>com.lifeStroy.MyFilter</filter-class>
<init-param>
<param-name>test-param</param-name>
<param-value>Initialization Paramter</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>MyFilter</filter-name>
<url-pattern>/*</url-pattern>
<!-->这是路径匹配<-->
</filter-mapping>
<filter-mapping>
<filter-name>MyFilter</filter-name>
<servlet-name>MyServlet</servlet-name>
<!-->这是servlet匹配,MyServlet定义的部分省略了<-->
</filter-mapping>
在SpringBoot中,可以不配置web.xml,而使用注解来要求Spring容器扫描Filter,注解如下:
@WebFilter(urlPatterns = "/myServlet", dispatcherTypes = {DispatcherType.REQUEST, DispatcherType.INCLUDE})
或者通过Register来配置:
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FilterConfiguration {
@Bean
public MyFilter myFilter(){
return new MyFilter();
}
@Bean
public FilterRegistrationBean<MyFilter> filterRegistrationBean1() {
FilterRegistrationBean<MyFilter> filterRegistrationBean = new FilterRegistrationBean<>();
filterRegistrationBean.setFilter(myFilter());
filterRegistrationBean.addUrlPatterns("/myServlet/*");
filterRegistrationBean.setOrder(6); //数字越小越靠前
return filterRegistrationBean;
}
这里有几个小Trick,包括:
- 既然Filter可以在Servlet之后或者之前调用,那么到底怎么决定那些代码在之前执行,哪些代码在之后执行;
- 既然同一路径、Servlet可以注册多个Filter,那么到底他们调用的顺序是什么?
答案1:注意doFilter()的三个参数是FilterChain,他有个方法是doFilter(),调用它之前就是Servelt调用前,调用它之后就是Servlet返回后。假如不调用这个方法,则不会调用之后的Filter和Servlet,请求直接返回。
答案2:根据web.xml中的filter-mapping的顺序来决定。有意思的是,当servlet调用前,是顺序的,servlet调用后返回时,filter应用的顺序是逆序的。那么在SpringBoot中呢,则用Filter的名字进行排序,如果不愿意受这种严格限制的话,就用Register注册的方法,显式指定顺序。
get一下myservlet方法,在终端里可以看到:
经过Filter
经过Filter2
处理后Filter2
处理后Filter
Interceptor
Interceptor其实是经典的AOP思想的落地应用,在Spring里,其实是基于动态代理的一种拦截器。因此只能应用于方法级。如果要实现更细粒度的控制,可以使用AspectJ,这就要求特定的编译器参与了。Interceptor主要有这么几种实现方式:
- 实现HandlerInterceptor接口
import org.springframework.http.HttpMethod; import org.springframework.lang.Nullable; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class MyInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { request.setAttribute("time", System.currentTimeMillis()); if (!HttpMethod.GET.matches(request.getMethod())) { response.setCharacterEncoding("utf-8"); //要在getWriter之前调用该方法,否则乱码 response.getWriter().println("只允许post方法"); response.addCookie(new Cookie("love", "you")); return false; } else { return true; } } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { //在controller的方法添加了@ResponseBody时,下面这行代码将不会产生任何效果 response.addCookie(new Cookie("love", "you")); System.out.println(request.getAttribute("time")); } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { System.out.println("Emmm..."); } }配置如下:
@Configuration public class InterceptorConfiguration implements WebMvcConfigurer { @Bean public MyInterceptor myInterceptorBean(){ return new MyInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(myInterceptorBean()).addPathPatterns("/**"); } }我们尝试运行一下:
$ curl -X POST http://127.0.0.1:8080/sample -I HTTP/1.1 200 Set-Cookie: love=you love: you Content-Length: 20 Date: Thu, 21 Mar 2019 15:13:30 GMT $ curl -X POST http://127.0.0.1:8080/sample 只允许post方法 $ curl -X GET http://127.0.0.1:8080/sample hello, I'm sample # 这是controller中写的正如代码中所见,
org.springframework.web.servlet.HandlerInterceptor是spring中包,因此可以通过Spring的方式来配置,或xml文件,或注解。但已经脱离了Tomcat容器的那一套,所以不必在web.xml文件中配置。这三个方法,其中
preHandle()方法在进入servlet、controller的方法(称为handler)之前被调用,如果返回false,则不会接下来的所有拦截器,以及handler,都不会执行;postHandle()方法在handler返回后、视图渲染前执行,afterCompletion()方法在视图渲染完毕后才调用,相当于一个回调函数了,根据官方的注释建议,这个方法主要是做一些资源清理之类的工作。需要注意的是:该Interceptor仅对controller生效,不对静态资源生效,根据注释,如果要对静态资源进行拦截,需要声明一个
MappedInterceptor对象,或者继承WebMvcConfigurationSupport对象并覆写其resourceHandlerMapping方法。/** * Add Spring MVC lifecycle interceptors for pre- and post-processing of * controller method invocations. Interceptors can be registered to apply * to all requests or be limited to a subset of URL patterns. * Note that interceptors registered here only apply to * controllers and not to resource handler requests. To intercept requests for * static resources either declare a * {@link org.springframework.web.servlet.handler.MappedInterceptor MappedInterceptor} * bean or switch to advanced configuration mode by extending * {@link org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport * WebMvcConfigurationSupport} and then override {@code resourceHandlerMapping}. */ default void addInterceptors(InterceptorRegistry registry) { }还有一点需要注意,如果Controller的方法添加了
@ResponseBody,那么在拦截器中无法修改响应信息头,比如所有的addCookie(),addHeader()方法均会无效果,如果在Controller类上添加了@RestController,相当于给所有的方法都添加了@ResponseBody。见spring拦截器中修改响应消息头 -
WebRequestInt·erceptor
这个拦截器和上面的拦截器非常相似,连三个方法的名字都是一样的。不同之处在于他们的参数,就是更加MVC风格了,我们可以看一下:
import org.springframework.lang.Nullable; import org.springframework.ui.ModelMap; import org.springframework.web.context.request.WebRequest; import org.springframework.web.context.request.WebRequestInterceptor; public class MyWebInterceptor implements WebRequestInterceptor { public void preHandle(WebRequest var1) throws Exception { System.out.println(var1.getSessionId()); } public void postHandle(WebRequest var1, @Nullable ModelMap var2) throws Exception { } public void afterCompletion(WebRequest var1, @Nullable Exception var2) throws Exception { } }configure中配置如下:
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class InterceptorConfiguration implements WebMvcConfigurer { @Bean public MyWebInterceptor myWebInterceptorBean(){ return new MyWebInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addWebRequestInterceptor(myWebInterceptorBean()).addPathPatterns("/"); } }三个方法调用的时机和作用与
HandlerInterceptor基本相同。 -
AsyncHandlerInterceptor
当handler开始一个异步请求时,HandlerInterceptor这类同步拦截器的
postHandle()和afterCompletion()都不会被调用,此时,如果注册了AsyncHandlerInterceptor,那么当handler开始处理请求时,AsyncHandlerInterceptor的afterConcurrentHandlingStarted()方法会被调用,作为postHandle()和afterCompletion()的一种替代方案。但是因为这是异步处理的,所以一般只建议使用这个方法的两个参数,而不要修改它,以免与handler的工作冲突。其接口如下:public interface AsyncHandlerInterceptor extends HandlerInterceptor { default void afterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { } }
当然,Interceptor本身只是一个概念,自己实现一个,然后用AOP注册到Controller上去也是没有问题的。
Filter和Interceptor的区别与联系
在很多情况下,这两者都是非常相像的。无论是其触发的时间点:都是在请求进入容器后,到达handler之前,以及handler处理完毕后,返回之前,很多时候其发挥的功能也类似,比如权限校验、日志、资源准备等等。
但他们也存在一些区别,首先在执行顺序上:
Filter1 ——> Filter2 ——> Interceptor1 ——> Interceptor2 ——> Handler ——> Interceptor2 ——> Interceptor1 ——> Filter2 ——> Filter1
另外在能力上,HandlerInterceptor的注释说:
HandlerInterceptor is basically similar to a Servlet Filter, but in contrast to the latter it just allows custom pre-processing with the option of prohibiting the execution of the handler itself, and custom post-processing. Filters are more powerful, for example they allow for exchanging the request and response objects that are handed down the chain. Note that a filter gets configured in web.xml, a HandlerInterceptor in the application context.
As a basic guideline, fine-grained handler-related preprocessing tasks are candidates for HandlerInterceptor implementations, especially factored-out common handler code and authorization checks. On the other hand, a Filter is well-suited for request content and view content handling, like multipart forms and GZIP compression. This typically shows when one needs to map the filter to certain content types (e.g. images), or to all requests.
也就是说:Interceptor只能禁止handler的执行,或者向request里添加一些东西,或者等handler处理完毕之后,再做一些定制化的工作,主要的用途还是抽取公用代码,也就是AOP的主要目的,在这个场景下,一般用来做日志或者授权之类的。而Filter就要更加强大,在Filter中,因为下面这段代码:
public class MyFilter extends HttpFilter {
@Override
public void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
System.out.println("经过Filter");
chain.doFilter(request, response); //这段代码的存在
response.getWriter().println("经过Filter处理");
response.setHeader("content-type", "text/html;charset=UTF-8");
System.out.println("处理后Filter");
}
}
Filter可以控制传给之后的处理链的request是什么,甚至可以替换掉request,因此它适合做内容处理,比如说multipart处理、Gzip的解压缩等。
而且,Filter属于servlet规范定义的内容,配置在web上下文中(web.xml)中,而Interceptor配置在应用上下文中。
到此,本文的探究之旅就告一段落了。servlet是什么,怎么运行,与Servlet相关的Filter、Listener、Interceptor概念也都有提到,特别是对于Spring如何使用嵌入式tomcat做了简单的介绍。
至于Tomcat容器的探究、以及对Spring servlet注册的更深入的探究,可能会放在有精力的下一阶段吧。
2 条评论
wood · 2019年5月31日 下午4:24
看到了我当年自学的样子
astupidcoder · 2019年6月9日 下午4:42
哈哈,大佬现在在做什么~