Servlet


准备工作

Tomcat

Tomcat 就是一个典型的 Web 应用服务器软件,通过运行 Tomcat 服务器,我们就可以快速部署我们的 Web 项目,并交由 Tomcat 进行管理,我们只需要直接通过浏览器访问我们的项目即可。

安装Tomcat

下载地址:https://tomcat.apache.org/download-10.cgi

  • 点击左侧的downloads,选择对应的版本

  • 下载完成后,解压,我放到了opt/目录下

启动Tomcat

  1. 打开终端,执行命令cd /opt/apache-tomcat-10.0.20/bin,进入到tomcat的bin目录下

  2. 输入:./startup.sh + 回车

    • 如出现错误:“Permission denied” ,赋予超级管理员权限sudo chmod 755 *.sh
    • 如果出现乱码,说明编码格式配置有问题,打开conf文件夹,找到 logging.properties文件,将ConsoleHandler的默认编码格式修改为GBK编码格式:java.util.logging.ConsoleHandler.encoding = GBK
  3. 打开浏览器,输入网址 http://localhost:8080/,如果出现一只三角猫,表示tomcat安装成功

  4. 关闭Tomcat:在bin目录下,终端输入命令:./shutdown.sh

Tomcat目录

$ tree -L 1
.
├── bin
├── conf
├── lib
├── logs
├── temp
├── webapps
└── work
  • bin目录:所有可执行文件,包括启动和关闭服务器的脚本
  • conf目录:服务器配置文件目录
  • lib目录:Tomcat服务端运行的一些依赖
  • logs目录:所有的日志信息都在这里
  • temp目录:存放运行时产生的一些临时文件,不用关心
  • work目录:工作目录,Tomcat会将jsp文件转换为java文件
  • webapps目录:所有的Web项目都在这里,每个文件夹都是一个Web应用程序:

我们发现,官方已经给我们预设了一些项目了,访问后默认使用的项目为ROOT项目,也就是我们默认打开的网站。

Tomcat还自带管理页面,我们打开:http://localhost:8080/manager,提示需要用户名和密码,

需要先去conf/tomcat-users.xml配置用户名和密码

<role rolename="manager-gui"/>
<user username="admin" password="password" roles="manager-gui"/>

现在再次打开管理页面,已经可以成功使用此用户进行登陆了。登录后,展示给我们的是一个图形化界面,我们可以快速预览当前服务器的一些信息,包括已经在运行的Web应用程序,甚至还可以查看当前的Web应用程序有没有出现内存泄露。还有一个虚拟主机管理页面,用于一台主机搭建多个Web站点

Maven创建Web项目

  • 1、打开IDEA,新建一个项目,选择 Java Enterprise(社区版没有此选项)

  • 2、项目模板选择Web应用程序

  • 3、然后需要配置Web应用程序服务器,将前面下载的Tomcat服务器集成到IDEA中。

    • 首先点击新建,然后设置Tomcat主目录即可,配置完成后,点击下一步即可,依赖项使用默认即可,然后点击完成,之后IDEA会自动帮助我们创建Maven项目。
  • 4、创建完成后,直接点击右上角即可运行此项目了,但是我们发现,有一个Servlet页面不生效。因为 Tomcat 10 以上的版本比较新,Servlet API包名发生了一些变化

    • 因此我们需要修改一下依赖
    • 包名也需要全部从 javax 改为 jakarta ,我们需要手动修改一下。
<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>5.0.0</version>
    <scope>provided</scope>
</dependency>

我们可以使用Maven 的 package 命令将项目直接打包为war包(默认),默认在项目的target目录下,然后放入webapp文件夹,就可以直接运行我们通过Java编写的Web应用程序了,访问路径为文件的名称。

Servlet 简介

Servlet 是 Server Applet 的缩写,译为“服务器端小程序”,是一种使用 Java 语言来开发动态网站的技术。

Servlet 是 Java EE 的一个标准,大部分的 Web 服务器都支持此标准,包括 Tomcat,就像之前的JDBC一样,由官方定义了一系列接口,而具体实现由我们来编写,最后交给Web服务器(如Tomcat)来运行我们编写的Servlet。

创建Servlet

使用注解配置

如何创建一个Servlet呢,只需要实现Servlet类即可,并添加注解@WebServlet来进行注册。

@WebServlet("/test")
public class TestServlet implements Servlet {
        ...//实现接口方法
}

现在就可以访问一下我们的页面:http://localhost:8080/xxx/test

使用web.xml配置

除了直接编写一个类,我们也可以在web.xml中进行注册,现将类上@WebServlet的注解去掉:

<servlet>
    <servlet-name>test</servlet-name>
    <servlet-class>com.example.webtest.TestServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>test</servlet-name>
    <url-pattern>/test</url-pattern>
</servlet-mapping>

这样的方式也能注册Servlet,但是显然直接使用注解更加方便,因此之后我们一律使用注解进行开发。

Servlet生命周期

接着来看看,一个Servlet是如何运行的。首先我们需要了解,Servlet中的方法各自是在什么时候被调用的,我们先编写一个打印语句来看看

public class TestServlet implements Servlet {

    public TestServlet(){
        System.out.println("我是构造方法!");
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {
        System.out.println("我是init");
    }

    @Override
    public ServletConfig getServletConfig() {
        System.out.println("我是getServletConfig");
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("我是service");
    }

    @Override
    public String getServletInfo() {
        System.out.println("我是getServletInfo");
        return null;
    }

    @Override
    public void destroy() {
        System.out.println("我是destroy");
    }
}

我们首先启动一次服务器,然后访问我们定义的页面,然后再关闭服务器,得到如下的顺序:

我是构造方法!
我是init
我是service

我是destroy

我们可以多次尝试去访问此页面,但是init构造方法只会执行一次,而每次访问都会执行的是service方法,因此,一个Servlet的生命周期为:

  • 首先执行构造方法完成 Servlet 初始化
  • Servlet 初始化后调用 init () 方法
  • Servlet 调用 service() 方法来处理客户端的请求
  • Servlet 销毁前调用 destroy() 方法
  • 最后,Servlet 是由 JVM 的垃圾回收器进行垃圾回收的。

Servlet 生命周期

在Web应用程序运行时,每当浏览器向服务器发起一个请求时,都会创建一个线程执行一次service方法,来让我们处理用户的请求,并将结果响应给用户。

service方法中有两个参数,ServletRequestServletResponse,实际上,用户发起的HTTP请求,就被Tomcat服务器封装为了一个ServletRequest对象,我们得到是其实是Tomcat服务器帮助我们创建的一个实现类,HTTP请求报文中的所有内容,都可以从ServletRequest对象中获取,同理,ServletResponse就是我们需要返回给浏览器的HTTP响应报文实体类封装。

那么我们来看看ServletRequest中有哪些内容,我们可以获取请求的一些信息:

@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
    //首先将其转换为HttpServletRequest(继承自ServletRequest,一般是此接口实现)
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        
        System.out.println(request.getProtocol());  //获取协议版本
        System.out.println(request.getRemoteAddr());  //获取访问者的IP地址
            System.out.println(request.getMethod());   //获取请求方法
        //获取头部信息
        Enumeration<String> enumeration = request.getHeaderNames();
        while (enumeration.hasMoreElements()){
            String name = enumeration.nextElement();
            System.out.println(name + ": " + request.getHeader(name));
        }
}

我们发现,整个HTTP请求报文中的所有内容,都可以通过HttpServletRequest对象来获取,当然,它的作用肯定不仅仅是获取头部信息,我们还可以使用它来完成更多操作

再来看看ServletResponse,这个是服务端的响应内容,填写想要发送给浏览器显示的内容:

//转换为HttpServletResponse(同上)
HttpServletResponse response = (HttpServletResponse) servletResponse;
//设定内容类型以及编码格式(普通HTML文本使用text/html,之后会讲解文件传输)
response.setHeader("Content-type", "text/html;charset=UTF-8");
//获取Writer直接写入内容
response.getWriter().write("我是响应内容!");
//所有内容写入完成之后,再发送给浏览器

现在我们在浏览器中打开此页面,就能够收到服务器发来的响应内容了。其中,响应头部分,是由Tomcat帮助我们生成的一个默认响应头。

HttpServlet

public abstract class GenericServlet implements Servlet {}
public abstract class HttpServlet extends GenericServlet {}
  • HttpServlet 继承自 GenericServlet 类
  • GenericServlet 实现了 Servlet 接口

Servlet有一个直接实现抽象类GenericServlet,这个类完善了配置文件读取和Servlet信息相关的的操作,但是依然没有去实现service方法

HttpServlet,它是遵循HTTP协议的一种Servlet,继承自GenericServlet,它根据HTTP协议的规则,完善了service方法。

因此只需要继承 HttpServlet 就可以编写我们的 Servlet 了,并且它已经帮助我们提前实现了一些操作,这样就会给我们省去很多的时间。

@WebServlet("/test")
public class TestServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().write("<h1>测试一下下</h1>");
    }
}

@WebServlet注解

可以直接使用 WebServlet 注解来快速注册一个 Servlet

属性名 类型 描述 必需
name String 指定 Servlet 的 name 属性。 如果没有显式指定,则取值为该 Servlet 的完全限定名,即包名+类名。
value String[ ] 该属性等价于 urlPatterns 属性,两者不能同时指定。 如果同时指定,通常是忽略 value 的取值。
urlPatterns String[ ] 代表当前Servlet的访问路径。
loadOnStartup int 指定 Servlet 的加载顺序。
initParams WebInitParam[ ] 指定一组 Servlet 初始化参数。
asyncSupported boolean 声明 Servlet 是否支持异步操作模式。
description String 指定该 Servlet 的描述信息。
displayName String 指定该 Servlet 的显示名。
@WebServlet(urlPatterns = "/test/*")
//可省略urlPatterns
@WebServlet("/test/*")

上面的路径表示,所有匹配/test/随便什么的路径名称,都可以访问此Servlet

也可以进行某个扩展名称的匹配:

@WebServlet("*.js")

这样的话,获取任何以js结尾的文件,都会由我们自己定义的Servlet处理。

还可以为一个Servlet配置多个访问路径:

@WebServlet({"/test1", "/test2"})

接着看 loadOnStartup 属性,此属性决定了是否在Tomcat启动时就加载此Servlet,默认情况下,Servlet只有在被访问时才会加载,它的默认值为-1,表示不在启动时加载,我们可以将其修改为大于等于0的数,来开启启动时加载。并且数字的大小决定了此Servlet的启动优先级。

@Log
@WebServlet(value = "/test", loadOnStartup = 1)
public class TestServlet extends HttpServlet {

    @Override
    public void init() throws ServletException {
        super.init();
        log.info("我被初始化了!");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().write("<h1>恭喜你解锁了全新玩法</h1>");
    }
}

POST请求完成登录

创建一个 Servlet,让其能够接收一个 POST 请求:

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        req.getParameterMap().forEach((k, v) -> {
            System.out.println(k + ": " + Arrays.toString(v));
        });
    }
}

ParameterMap存储了我们发送的POST请求所携带的表单数据,我们可以直接将其遍历查看,浏览器发送了什么数据。

现在我们再来修改一下前端:

<body>
    <h1>登录到系统</h1>
    <form method="post" action="login">
        <hr>
        <div>
            <label>
                <input type="text" placeholder="用户名" name="username">
            </label>
        </div>
        <div>
            <label>
                <input type="password" placeholder="密码" name="password">
            </label>
        </div>
        <div>
            <button>登录</button>
        </div>
    </form>
</body>

现在我们点击登录按钮,会自动向后台发送一个POST请求,请求地址为当前地址+/login,也就是我们上面编写的Servlet路径。

上传和下载文件

首先将icon.png放入到resource文件夹中,接着我们编写一个Servlet用于处理文件下载:

@WebServlet("/file")
public class FileServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
      resp.setContentType("image/png");  
      OutputStream outputStream = resp.getOutputStream();
      InputStream inputStream = Resources.getResourceAsStream("icon.png");

    }
}

为了更加快速地编写IO代码,我们可以引入一个工具库:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.6</version>
</dependency>

使用此类库可以快速完成IO操作:

resp.setContentType("image/png");
OutputStream outputStream = resp.getOutputStream();
InputStream inputStream = Resources.getResourceAsStream("icon.png");
//直接使用copy方法完成转换
IOUtils.copy(inputStream, outputStream);

现在我们在前端页面添加一个链接,用于下载此文件:

<hr>
<a href="file" download="icon.png">点我下载高清资源</a>

下载文件搞定,那么如何上传一个文件呢?

首先我们编写前端部分:

<form method="post" action="file" enctype="multipart/form-data">
    <div>
        <input type="file" name="test-file">
    </div>
    <div>
        <button>上传文件</button>
    </div>
</form>

注意必须添加enctype="multipart/form-data",来表示此表单用于文件传输。

现在来修改一下Servlet代码:

@MultipartConfig
@WebServlet("/file")
public class FileServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        try(FileOutputStream stream = new FileOutputStream("/Users/nagocoler/Documents/IdeaProjects/WebTest/test.png")){
            Part part = req.getPart("test-file");
            IOUtils.copy(part.getInputStream(), stream);
            resp.setContentType("text/html;charset=UTF-8");
            resp.getWriter().write("文件上传成功!");
        }
    }
}

注意,必须添加@MultipartConfig注解来表示此Servlet用于处理文件上传请求。

现在再运行服务器,并将我们刚才下载的文件又上传给服务端。

使用XHR请求数据

现在我们希望,网页中的部分内容,可以动态显示,比如网页上有一个时间,旁边有一个按钮,点击按钮就可以刷新当前时间。

这个时候就需要我们在网页展示时向后端发起请求了,并根据后端响应的结果,动态地更新页面中的内容,要实现此功能,就需要用到JavaScript来帮助我们,首先在js中编写我们的XHR请求,并在请求中完成动态更新:

function updateTime() {
    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState === 4 && xhr.status === 200) {
            document.getElementById("time").innerText = xhr.responseText
        }
    };
    xhr.open('GET', 'time', true);
    xhr.send();
}

接着修改一下前端页面,添加一个时间显示区域:

<hr>
<div id="time"></div>
<br>
<button onclick="updateTime()">更新数据</button>
<script>
    updateTime()
</script>

最后创建一个Servlet用于处理时间更新请求:

@WebServlet("/time")
public class TimeServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
        String date = dateFormat.format(new Date());
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().write(date);
    }
}

现在点击按钮就可以更新了。

GET请求也能传递参数,这里做一下演示。

重定向与请求转发

当我们希望用户登录完成之后,直接跳转到网站的首页,那么这个时候,我们就可以使用重定向来完成。当浏览器收到一个重定向的响应时,会按照重定向响应给出的地址,再次向此地址发出请求。

实现重定向很简单,只需要调用一个方法即可,我们修改一下登陆成功后执行的代码:

resp.sendRedirect("time");

调用后,响应的状态码会被设置为302,并且响应头中添加了一个Location属性,此属性表示,需要重定向到哪一个网址。

接着来看请求转发,请求转发其实是一种服务器内部的跳转机制,我们知道,重定向会使得浏览器去重新请求一个页面,而请求转发则是服务器内部进行跳转,它的目的是,直接将本次请求转发给其他Servlet进行处理,并由其他Servlet来返回结果,因此它是在进行内部的转发。

req.getRequestDispatcher("/time").forward(req, resp);

现在,在登陆成功的时候,我们将请求转发给处理时间的Servlet,注意这里的路径规则和之前的不同,我们需要填写Servlet上指明的路径,并且请求转发只能转发到此应用程序内部的Servlet,不能转发给其他站点或是其他Web应用程序。

现在再次进行登陆操作,我们发现,返回结果为一个405页面,证明了,我们的请求现在是被另一个Servlet进行处理,并且请求的信息全部被转交给另一个Servlet,由于此Servlet不支持POST请求,因此返回405状态码。

那么也就是说,该请求包括请求参数也一起被传递了,那么我们可以尝试获取以下POST请求的参数。

现在我们给此Servlet添加POST请求处理,直接转交给Get请求处理:

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    this.doGet(req, resp);
}

再次访问,成功得到结果,但是我们发现,浏览器只发起了一次请求,并没有再次请求新的URL,也就是说,这一次请求直接返回了请求转发后的处理结果。

那么,请求转发有什么好处呢?它可以携带数据!

req.setAttribute("test", "我是请求转发前的数据");
req.getRequestDispatcher("/time").forward(req, resp);
System.out.println(req.getAttribute("test"));

通过setAttribute方法来给当前请求添加一个附加数据,在请求转发后,我们可以直接获取到该数据。

重定向属于2次请求,因此无法使用这种方式来传递数据,那么,如何在重定向之间传递数据呢?我们可以使用即将要介绍的ServletContext对象。

最后总结,两者的区别为:

  • 请求转发是一次请求,重定向是两次请求
  • 请求转发地址栏不会发生改变, 重定向地址栏会发生改变
  • 请求转发可以共享请求参数 ,重定向之后,就获取不了共享参数了
  • 请求转发只能转发给内部的Servlet

ServletContext对象

ServletContext全局唯一,它是属于整个Web应用程序的,我们可以通过getServletContext()来获取到此对象。

此对象也能设置附加值:

ServletContext context = getServletContext();
context.setAttribute("test", "我是重定向之前的数据");
resp.sendRedirect("time");
System.out.println(getServletContext().getAttribute("test"));

因为无论在哪里,无论什么时间,获取到的ServletContext始终是同一个对象,因此我们可以随时随地获取我们添加的属性。

它不仅仅可以用来进行数据传递,还可以做一些其他的事情,比如请求转发:

context.getRequestDispatcher("/time").forward(req, resp);

它还可以获取根目录下的资源文件(注意是webapp根目录下的,不是resource中的资源)

初始化参数

初始化参数类似于初始化配置需要的一些值,比如我们的数据库连接相关信息,就可以通过初始化参数来给予Servlet,或是一些其他的配置项,也可以使用初始化参数来实现。

我们可以给一个Servlet添加一些初始化参数:

@WebServlet(value = "/login", initParams = {
        @WebInitParam(name = "test", value = "我是一个默认的初始化参数")
})

它也是以键值对形式保存的,我们可以直接通过Servlet的getInitParameter方法获取:

System.out.println(getInitParameter("test"));

但是,这里的初始化参数仅仅是针对于此Servlet,我们也可以定义全局初始化参数,只需要在web.xml编写即可:

<context-param>
    <param-name>lbwnb</param-name>
    <param-value>我是全局初始化参数</param-value>
</context-param>

我们需要使用ServletContext来读取全局初始化参数:

ServletContext context = getServletContext();
System.out.println(context.getInitParameter("lbwnb"));

参考

Sponsor❤️

您的支持是我不断前进的动力,如果您感觉本文对您有所帮助的话,可以考虑打赏一下本文,用以维持本博客的运营费用,拒绝白嫖,从你我做起!🥰🥰🥰

支付宝 微信

文章作者: 简简
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 简简 !
评论
填上邮箱会收到评论回复提醒哦!!!
 上一篇
Cookie & Session Cookie & Session
HTTP 协议是一种无状态协议,即每次服务端接收到客户端的请求时,都是一个全新的请求,服务器并不知道客户端的历史请求记录。Cookie 和 Session 的主要目的就是为了弥补 HTTP 的无状态特性。 Cookie什么是Cookie?
2022-04-20
下一篇 
Maven Maven
Maven 是 Apache 下的一个纯 Java 开发的项目管理和整合工具。它将项目的开发和管理过程抽象成一个项目对象模型(POM)。
2022-04-18
  目录