04 | 实战:纯手工打造和运行一个Servlet

作为Java程序员,我们可能已经习惯了使用IDE和Web框架进行开发,IDE帮我们做了编译、打包的工作,而Spring框架在背后帮我们实现了Servlet接口,并把Servlet注册到了Web容器,这样我们可能很少有机会接触到一些底层本质的东西,比如怎么开发一个Servlet?如何编译Servlet?如何在Web容器中跑起来?

今天我们就抛弃IDE、拒绝框架,自己纯手工编写一个Servlet,并在Tomcat中运行起来。一方面进一步加深对Servlet的理解;另一方面,还可以熟悉一下Tomcat的基本功能使用。

主要的步骤有:

1.下载并安装Tomcat。
2.编写一个继承HttpServlet的Java类。
3.将Java类文件编译成Class文件。
4.建立Web应用的目录结构,并配置web.xml
5.部署Web应用。
6.启动Tomcat。
7.浏览器访问验证结果。
8.查看Tomcat日志。

下面你可以跟我一起一步步操作来完成整个过程。Servlet 3.0规范支持用注解的方式来部署Servlet,不需要在web.xml里配置,最后我会演示怎么用注解的方式来部署Servlet。

1. 下载并安装Tomcat

最新版本的Tomcat可以直接在官网上下载,根据你的操作系统下载相应的版本,这里我使用的是Mac系统,下载完成后直接解压,解压后的目录结构如下。

下面简单介绍一下这些目录:

/bin:存放Windows或Linux平台上启动和关闭Tomcat的脚本文件。
/conf:存放Tomcat的各种全局配置文件,其中最重要的是server.xml
/lib:存放Tomcat以及所有Web应用都可以访问的JAR文件。
/logs:存放Tomcat执行时产生的日志文件。
/work:存放JSP编译后产生的Class文件。
/webapps:Tomcat的Web应用目录,默认情况下把Web应用放在这个目录下。

2. 编写一个继承HttpServlet的Java类

我在专栏上一期提到,javax.servlet包提供了实现Servlet接口的GenericServlet抽象类。这是一个比较方便的类,可以通过扩展它来创建Servlet。但是大多数的Servlet都在HTTP环境中处理请求,因此Servlet规范还提供了HttpServlet来扩展GenericServlet并且加入了HTTP特性。我们通过继承HttpServlet类来实现自己的Servlet只需要重写两个方法:doGet和doPost。

因此今天我们创建一个Java类去继承HttpServlet类,并重写doGet和doPost方法。首先新建一个名为MyServlet.java的文件,敲入下面这些代码:

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class MyServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        System.out.println("MyServlet 在处理get()请求...");
        PrintWriter out = response.getWriter();
        response.setContentType("text/html;charset=utf-8");
        out.println("<strong>My Servlet!</strong><br>");
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        System.out.println("MyServlet 在处理post()请求...");
        PrintWriter out = response.getWriter();
        response.setContentType("text/html;charset=utf-8");
        out.println("<strong>My Servlet!</strong><br>");
    }

}

这个Servlet完成的功能很简单,分别在doGet和doPost方法体里返回一段简单的HTML。

3. 将Java文件编译成Class文件

下一步我们需要把MyServlet.java文件编译成Class文件。你需要先安装JDK,这里我使用的是JDK 10。接着你需要把Tomcat lib目录下的servlet-api.jar拷贝到当前目录下,这是因为servlet-api.jar中定义了Servlet接口,而我们的Servlet类实现了Servlet接口,因此编译Servlet类需要这个JAR包。接着我们执行编译命令:

javac -cp ./servlet-api.jar MyServlet.java

编译成功后,你会在当前目录下找到一个叫MyServlet.class的文件。

4. 建立Web应用的目录结构

我们在上一期学到,Servlet是放到Web应用部署到Tomcat的,而Web应用具有一定的目录结构,所有我们按照要求建立Web应用文件夹,名字叫MyWebApp,然后在这个目录下建立子文件夹,像下面这样:

MyWebApp/WEB-INF/web.xml

MyWebApp/WEB-INF/classes/MyServlet.class

然后在web.xml中配置Servlet,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
  http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
  version="4.0"
  metadata-complete="true">

    <description> Servlet Example. </description>
    <display-name> MyServlet Example </display-name>
    <request-character-encoding>UTF-8</request-character-encoding>

    <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>

你可以看到在web.xml配置了Servlet的名字和具体的类,以及这个Servlet对应的URL路径。请你注意,servlet和servlet-mapping这两个标签里的servlet-name要保持一致。

5. 部署Web应用

Tomcat应用的部署非常简单,将这个目录MyWebApp拷贝到Tomcat的安装目录下的webapps目录即可。

6. 启动Tomcat

找到Tomcat安装目录下的bin目录,根据操作系统的不同,执行相应的启动脚本。如果是Windows系统,执行startup.bat.;如果是Linux系统,则执行startup.sh

7. 浏览访问验证结果

在浏览器里访问这个URL:http://localhost:8080/MyWebApp/myservlet,你会看到:

My Servlet!

这里需要注意,访问URL路径中的MyWebApp是Web应用的名字,myservlet是在web.xml里配置的Servlet的路径。

8. 查看Tomcat日志

打开Tomcat的日志目录,也就是Tomcat安装目录下的logs目录。Tomcat的日志信息分为两类 :一是运行日志,它主要记录运行过程中的一些信息,尤其是一些异常错误日志信息 ;二是访问日志,它记录访问的时间、IP地址、访问的路径等相关信息。

这里简要介绍各个文件的含义。

  • catalina.***.log

主要是记录Tomcat启动过程的信息,在这个文件可以看到启动的JVM参数以及操作系统等日志信息。

  • catalina.out

catalina.out是Tomcat的标准输出(stdout)和标准错误(stderr),这是在Tomcat的启动脚本里指定的,如果没有修改的话stdout和stderr会重定向到这里。所以在这个文件里可以看到我们在MyServlet.java程序里打印出来的信息:

MyServlet在处理get请求…

  • localhost.**.log

主要记录Web应用在初始化过程中遇到的未处理的异常,会被Tomcat捕获而输出这个日志文件。

  • localhost_access_log.**.txt

存放访问Tomcat的请求日志,包括IP地址以及请求的路径、时间、请求协议以及状态码等信息。

  • manager.***.log/host-manager.***.log

存放Tomcat自带的Manager项目的日志信息。

用注解的方式部署Servlet

为了演示用注解的方式来部署Servlet,我们首先修改Java代码,给Servlet类加上@WebServlet注解,修改后的代码如下。

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet("/myAnnotationServlet")
public class AnnotationServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        
   System.out.println("AnnotationServlet 在处理get请求...");
        PrintWriter out = response.getWriter();
        response.setContentType("text/html; charset=utf-8");
        out.println("<strong>Annotation Servlet!</strong><br>");

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        System.out.println("AnnotationServlet 在处理post请求...");
        PrintWriter out = response.getWriter();
        response.setContentType("text/html; charset=utf-8");
        out.println("<strong>Annotation Servlet!</strong><br>");

    }

}  

这段代码里最关键的就是这个注解,它表明两层意思:第一层意思是AnnotationServlet这个Java类是一个Servlet,第二层意思是这个Servlet对应的URL路径是myAnnotationServlet。

@WebServlet("/myAnnotationServlet")

创建好Java类以后,同样经过编译,并放到MyWebApp的class目录下。这里要注意的是,你需要删除原来的web.xml,因为我们不需要web.xml来配置Servlet了。然后重启Tomcat,接下来我们验证一下这个新的AnnotationServlet有没有部署成功。在浏览器里输入:http://localhost:8080/MyWebApp/myAnnotationServlet,得到结果:

Annotation Servlet!

这说明我们的AnnotationServlet部署成功了。可以通过注解完成web.xml所有的配置功能,包括Servlet初始化参数以及配置Filter和Listener等。

本期精华

通过今天的学习和实践,相信你掌握了如何通过扩展HttpServlet来实现自己的Servlet,知道了如何编译Servlet、如何通过web.xml来部署Servlet,同时还练习了如何启动Tomcat、如何查看Tomcat的各种日志,并且还掌握了如何通过注解的方式来部署Servlet。我相信通过专栏前面文章的学习加上今天的练习实践,一定会加深你对Servlet工作原理的理解。之所以我设置今天的实战练习,是希望你知道IDE和Web框架在背后为我们做了哪些事情,这对于我们排查问题非常重要,因为只有我们明白了IDE和框架在背后做的事情,一旦出现问题的时候,我们才能判断它们做得对不对,否则可能开发环境里的一个小问题就会折腾我们半天。

课后思考

我在Servlet类里同时实现了doGet方法和doPost方法,从浏览器的网址访问默认访问的是doGet方法,今天的课后思考题是如何访问这个doPost方法。

不知道今天的内容你消化得如何?如果还有疑问,请大胆的在留言区提问,也欢迎你把你的课后思考和心得记录下来,与我和其他同学一起讨论。如果你觉得今天有所收获,欢迎你把它分享给你的朋友。

精选留言

  • feitian

    2019-05-20 07:51:39

    既然是纯手工,就应该把servlet那套完整的写出来,不应该再用tomcat容器而应该手写实现tomcat的核心代码。最核心的应该是类似HttpServlet功能的实现,把这个从最初的servlet接口实现了才算讲透,不然还是有点和稀泥的感觉。我觉得应该按照这种思路讲会更好,请参考我写的mytomcat,https://github.com/feifa168/mytomcat
  • darren

    2019-05-18 15:18:36

    发现xml与注解不能同时起作用,那在用xml方式的老项目中就没办法使用注解的方式了吗?
    作者回复

    web.xml 和注解可以同时工作的。

    例子里的web.xml和注解不能同时工作的原因是web.xml中的配置metadata-complete="true", 你需要把它设置成metadata-complete="false"。

    metadata-complete为true的意思是,告诉Tomcat不要去扫描Servlet注解了。

    2019-05-19 17:50:45

  • 不负

    2019-05-18 19:44:32

    老师,实践中发现个问题:虽然response.setContentType("text/html;charset=utf-8"),但是out.println中有输出中文还是乱码的
    作者回复

    调下顺序,像下面这样:
    response.setContentType("text/html; charset=utf-8");
    PrintWriter out = response.getWriter();

    getWrite的源码如下:
    ------
    public PrintWriter getWriter()
    throws IOException {

    if (usingOutputStream) {
    throw new IllegalStateException
    (sm.getString("coyoteResponse.getWriter.ise"));
    }

    if (ENFORCE_ENCODING_IN_GET_WRITER) {
    /*
    * If the response's character encoding has not been specified as
    * described in <code>getCharacterEncoding</code> (i.e., the method
    * just returns the default value <code>ISO-8859-1</code>),
    * <code>getWriter</code> updates it to <code>ISO-8859-1</code>
    * (with the effect that a subsequent call to getContentType() will
    * include a charset=ISO-8859-1 component which will also be
    * reflected in the Content-Type response header, thereby satisfying
    * the Servlet spec requirement that containers must communicate the
    * character encoding used for the servlet response's writer to the
    * client).
    */
    setCharacterEncoding(getCharacterEncoding());
    }

    usingWriter = true;
    outputBuffer.checkConverter();
    if (writer == null) {
    writer = new CoyoteWriter(outputBuffer);
    }
    return writer;
    }
    -----

    你看注释里它说:如果调这个方法之前没有指定Response的字符编码,就用默认的ISO-8859-1,ISO-8859-1不包括中文字符。

    2019-05-19 18:35:53

  • 熊斌

    2019-11-17 22:49:34

    按老师的步骤操作,结果一致,开心!
    整个过程遇到以下几个问题,记录一下,如果有遇到同样问题的小伙伴可以借鉴下解决问题的思路(大神可以略过):

    首先说明一下我的运行环境:
    操作系统:win10
    Tomcat版本:apache-tomcat-9.0.27(免安装版本)
    JDK版本:jdk-13.0.1(免安装版)

    问题1:javac失败
    原因&解决方案:未配置系统环境变量,在系统环境变量path中,加入E:\xxx\jdk-13.0.1\bin,问题解决;

    问题2:脱离IDE,新建MyServlet.java,执行文章中的javac命令失败
    原因&解决方案:按文章中的命令原封不动执行的话,java源文件需要建到tomcat的lib目录下,否则会导包失败。然后在lib目录下,shift+右键(在此处打开命令窗口)打开cmd窗口,执行成功,生成了MyServlet.class

    问题3:tomcat startup.bat执行一闪而过(如果一闪而过,说明没启动成功,拿文中的http://localhost:8080/MyWebApp/myservlet 访问的话什么都没有)
    原因&解决方案:按照https://blog.csdn.net/scau_lth/article/details/83218335 文章中,第二点方案得以解决。

    问题4:访问http://localhost:8080/ 成功跳转tomcat主页,访问http://localhost:8080/MyWebApp/myservlet 404(这种情况一般就是容器启动加载你的应用失败,需要根据日志具体分析是哪块的问题)

    原因&解决方案:查看apache-tomcat-9.0.27\logs 目录下的日志发现,17-Nov-2019 22:19:40.620 信息 [main] org.apache.catalina.startup.HostConfig.deployDirectory 把web 应用程序部署到目录 [F:\workspace\apache-tomcat-9.0.27\webapps\MyWebApp]
    17-Nov-2019 22:19:40.674 严重 [main] org.apache.tomcat.util.digester.Digester.fatalError Parse fatal error at line [2] column [6]
    org.xml.sax.SAXParseException; systemId: file:/F:/workspace/apache-tomcat-9.0.27/webapps/MyWebApp/WEB-INF/web.xml; lineNumber: 2; columnNumber: 6; 不允许有匹配 "[xX][mM][lL]" 的处理指令目标。

    Context [/MyWebApp] startup failed due to previous errors
    我的web.xml是直接从文章中拷贝的,复制粘贴时多了空格,修改后启动成功

    修改后保存,容器会重新加载,看日志你会发现Context with name [/MyWebApp] is completed
    再次使用http://localhost:8080/MyWebApp/myservlet 浏览器访问时会看到结果 MyServlet
  • 2019-05-19 19:01:15

    @Amanda 不知道怎么直接回复你,我跟你一样也是遇到乱码的问题。我也设置了response.setCharacterEncoding(utf8),在getWriter之前,但依然是一样的乱码。
    原因在与javac编译生成的class文件是用的gbk的编码。换句话说,你生成的class源文件就已经是中文乱码了。
    所以在javac的时候加上 -encoding UTF-8就好了。
    ps:我是Windows的环境。
  • Monday

    2019-05-18 10:20:39

    1、postman
    2、curl 命令发送post
    3、用HttpClient发送

    周六早上坚持打卡,本章节绝大多数知识以前有接触过,只有@WebServlet注解是新知识,现在业务开发一般都是写SpringMVC容器中的Controller来代替类似文中的Servlet类。

    问题:基于Spring+SpringMVC+Mybais的框架搭建的项目,平常开发的都是写Controller与Service、DAO。
    1、请问Servlet容器只管理DispatchServlet这一个Servlet吗?
    2、有什么可视化工具可以直接查看各种容器中管理的对象吗?

    谢谢!
    作者回复

    1. 你可以向Servlet容器注册多个Servlet。
    2. 你看这个有没有帮助
    https://github.com/spring-projects/spring-framework/issues/14296

    2019-05-18 19:43:01

  • allean

    2019-05-18 16:05:54

    IDE和框架诞生之初是为了让程序员从繁琐的底层配置中抽离出来专注于业务开发,然而大多数人在享受IDE和框架带来的便捷时,也成了温水里的青蛙,对于实现原理认识模糊,渐渐沦落为一个CRUD,这不是一件好事,啊~幡然醒悟
  • 吃饭饭

    2019-09-26 22:38:50

    分享一个编译源码的帖子:https://mp.weixin.qq.com/s/wp1LZcdK2eLRZU3HlubW9w
  • 蓝士钦

    2019-06-13 21:48:57

    课后思考:
    访问doPost()方法有两种方式
    1. 使用postMan等工具发起post请求
    2. 在代码中doGet()方法去调用doPost()

    疑问:
    doGet和doPost其实在网络层没有任何区别,通过浏览器地址栏中发起的是get请求,get请求其实也能携带像post请求一样的请求体参数,具体区别其实是不同浏览器和服务器实现方式的区别。
    常见的面试题很喜欢考post和get的区别,之所以区分get和post是为了http协议更加解耦吗?就像业务拆分一样专职专工
    作者回复

    get和post的重要区别是,前者不能改变服务端的数据,是幂等;而Post可以改变服务端数据。

    2019-06-13 22:09:15

  • 今夜秋风和

    2019-05-18 11:10:43

    老师,验证的时候默认增加了 super.doGet(req, resp);在http1.1写一下不能工作,查看httpServlet 源码里面 对协议做了限制,http 1.1 协议默认不支持。这个为什么是这样设计的呢?
    源代码:
    String protocol = req.getProtocol();
    String msg = lStrings.getString("http.method_get_not_supported");
    if (protocol.endsWith("1.1")) {
    resp.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, msg);
    } else {
    resp.sendError(HttpServletResponse.SC_BAD_REQUEST, msg);
    }
    第二个是如果是那个注解访问的,可以不用删除web.xml,把web.xml里面的url-pattern 改成注解同样的路由,也可以支持;如果web.xml 路由自定义一个的话,测试发现自定义的会有404,是不是注解的路由优先级会更高呢?
    3.如果把web.xml删除,servlet容器启动的时候是不是会自动扫描注解类,将它注册到容器中?
    作者回复

    super.doGet(req, resp); 调的是HttpServlet的doGet方法,但是这个doGet需要你去实现的。

    HttpServlet的service方法会调doXXX方法,并且HttpServlet里的各种doXXX方法的默认实现都是直接返回错误。

    为什么HttpServlet要这样设计呢?这是因为它需要做一个限制:程序员要么重写HttpServlet的service方法,要么重写HttpServlet的doXXX方法。

    web.xml和注解可以同时工作,你需要把web.xml中的metadata-complete="true"设置成false。

    Tomcat启动时会扫描注解,同时记下Servlet的名字和映射路径,如果你设置了延迟记载Servlet,通过浏览器访问时Tomcat才会加载和实例化Servlet。

    2019-05-19 09:16:42

  • Geek_0db340

    2019-05-18 19:24:38

    表单提交method=post 就可以啦
  • 桔子

    2019-05-31 10:43:01

    李老师,doGet方法的request和response的初始化代码在哪里呢,只知道是servlet容器创建的,但是去哪里可以看到容器初始化response的源码呢。
    作者回复

    在Tomcat中CoyoteAdapter类的service方法里

    2019-06-01 11:03:12

  • Geek_ebda96

    2019-05-23 16:56:51

    李老师,请教一个问题,你这里所说的servlet和spring mvc里面的controller是什么关系,servlet里面可以直接接收请求,处理请求业务,controller只是通过dispatch servlet再接入进来的?
    作者回复

    你说的没错,具体是这样的 Tomcat的Wrapper组件-Filter-DispatcherServlet-Controller

    2019-05-23 19:12:35

  • 风翱

    2019-05-18 07:47:23

    可以利用工具,例如postman。 也可以编写代码,利用http的post方法去调用。 或者像楼上所说的不管通过get还是post都通知转发到doPost中。
  • 清风

    2019-05-22 22:26:12

    注解是高版本的Servlet才支持的吧,好像是2.5以上
    作者回复

    Servlet3.0开始支持

    2019-05-23 00:10:29

  • 郑童文

    2019-05-20 01:08:31

    请问老师: 我们在servlet的实现类中import的是javax.servlet.http.HttpServlet 请问为什么需要Tomcat的servlet-api.jar呢?难道javax.servlet.http.HttpServlet这个类不是jdk中的吗?谢谢!
    作者回复

    JDK不带Servlet Jar包。

    2019-05-20 20:21:29

  • KL3

    2019-05-18 00:37:14

    把业务逻辑写在dopost里,然后doget方法调用dopost方法
  • 大白

    2020-02-08 21:03:03

    实践过程遇到几个问题,解决方法如下,供参考:
    1、 编译MyServlet.java时报错:Desktop\MyServlet.java:13: 错误: 编码 GBK 的不可映射字符 (0x80)。
    原因:系统字符集不匹配。
    解决办法:编译时添加“-encoding utf-8”参数即可解决。
    2、 tomcat官网下载windows版,免安装tomcat,启动报错。
    原因:startup.bat文件中的CATALINA_HOME参数未设置。
    解决办法:在startup.bat文件中添加SET CATALINA_HOME=”tomcat安装路径”即可。
    3、 启动tomcat后,项目启动报错。
    原因:web.xml文件格式编写错误,web.xml文件第一行为空行。
    解决办法:删除空行及空格,顶格写。
  • GeekAmI

    2019-11-26 09:44:24

    curl -X POST http://127.0.0.1:8080/MyWebApp/myAnnotationServlet
  • 业余爱好者

    2019-06-18 22:08:28

    “javac -cp ./servlet-api.jar MyServlet.java”执行报错:
    "MyServlet.java:16: 错误: 编码GBK的不可映射字符
    System.out.println("MyServlet 鍦ㄥ鐞? get锛堬級璇锋眰...");"
    换成:“javac -encoding UTF-8 -cp ./servlet-api.jar MyServlet.java”解决。百度搬运工。。