HTTP请求和响应

前面我们学习过Servlet组件和Filter组件,它们的共同特点是都会用到HttpServletRequestHttpServletResponse这两个对象,实际上,它们就是Servlet规范中HTTP请求和HTTP响应的封装类。这篇笔记我们学习如何在Servlet规范中处理HTTP请求和响应,以及如何实现文件上传、文件下载等功能。

HTTP请求处理

Servlet规范中,和HTTP请求相关的方法都是围绕HttpServletRequest类定义的。

读取GET请求参数

GET请求可以包含一组URL参数键值对,下面例子我们通过代码读取了URL参数。

package com.gacfox.demo.demoweb;

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 DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String key = request.getParameter("key");
        System.out.println(key);
    }
}

我们使用了request.getParameter()方法读取请求参数,此时如果我们通过浏览器访问例如http://localhost:8080/demoweb/demo?key=1的地址,对应的值就会打印出来。不过这里要注意,URL参数是可以存在多个同名键的,例如http://localhost:8080/demoweb/demo?key=1&key=2,如果是这种情况,我们就需要使用request.getParameterValues("key")方法,它会返回一个字符串数组。

读取POST请求体内容

POST请求可以包含一个请求体(Body),下面例子演示如何读取请求体的内容。

package com.gacfox.demo.demoweb;

import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class DemoServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        ServletInputStream inputStream = request.getInputStream();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
        byte[] requestBodyBytes = outputStream.toByteArray();
        inputStream.close();
        outputStream.close();
        System.out.println(new String(requestBodyBytes, StandardCharsets.UTF_8));
    }
}

代码中,我们使用request.getInputStream()获取了请求体的输入流,然后我们将内容一次性的读入内存,并将其转换为字符串类型。对于JSON、XML等作为请求体报文的情况,上面的程序十分实用。不过如果是请求体较大的情况,我们可能就不可以将所有数据一次性读入内存了。

此外,除了直接操作ServletInputStream,我们也可以使用request.getReader()直接获取一个对应输入流的BufferedReader对象,这里就不过多演示了。

读取表单请求

我们知道表单请求常见的有application/x-www-form-urlencodedmultipart/form-data两种类型,它们的区别在于请求体的格式不同。Servlet规范中对于前者处理方式和读取GET请求参数是完全一致的,而后者功能更为强大,它还支持文件上传,我们这里主要介绍后者。

对于multipart/form-data表单格式,我们需要使用Multipart请求处理方式,在这种处理方式中,multipart/form-data表单的每一个字段都会被解析为一个Part对象,我们可以通过表单字段的名字取出Part对象,进一步获取其输入流,下面是一个例子。

<servlet>
    <servlet-name>DemoServlet</servlet-name>
    <servlet-class>com.gacfox.demo.demoweb.DemoServlet</servlet-class>
    <multipart-config/>
</servlet>

首先我们需要在web.xml中配置Servlet的<multipart-config>属性,其中可以指定缓冲路径等,我们这里都留作默认因此没有额外的配置。

package com.gacfox.demo.demoweb;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class DemoServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        Part part = request.getPart("text");
        InputStream inputStream = part.getInputStream();
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int bytesRead;
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
        }
        byte[] requestBodyBytes = outputStream.toByteArray();
        inputStream.close();
        outputStream.close();
        System.out.println(new String(requestBodyBytes, StandardCharsets.UTF_8));
    }
}

代码中,我们通过request.getPart()获取了表单字段,随后我们将该字段的所有内容一次性读入内存并转换为字符串打印了出来。注意multipart/form-data表单经常用于文件上传,对于文件字段则可能一次性读入内存是不合适的,此时我们应该将其写入磁盘,或者保存到对象存储系统中。

除了在XML中进行配置,我们也可以在Servlet类上标注@MultipartConfig注解,它和XML配置的效果是完全一致的,下面是一个例子。

@MultipartConfig
@WebServlet(name = "DemoServlet", urlPatterns = "/demo")
public class DemoServlet extends HttpServlet {
    // ... 具体业务逻辑
}

生成HTTP响应

Servlet规范中,和HTTP响应相关的方法都是围绕HttpServletResponse类定义的。

写入响应输出流

Servlet中,我们可以直接获取ServletOutputStream以二进制方式写入响应内容,下面是一个例子。

package com.gacfox.demo.demoweb;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

public class DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String text = "Hello, world!";
        byte[] textBytes = text.getBytes(StandardCharsets.UTF_8);
        response.getOutputStream().write(textBytes);
    }
}

当然,对于文本我们也可以直接获取PrintWriter并写入内容,对于文本信息这比直接操作ServletOutputStream方便。

package com.gacfox.demo.demoweb;

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 DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        String text = "<html><body>Hello, this is my first demo page!!!</body></html>";
        response.setHeader("Content-Type", "text/html");
        response.getWriter().write(text);
    }
}

通常来说,在Servlet中写入响应的ServletOutputStream或是PrintWriter对象不需要手动关闭,这些流会在响应完成时由Servlet容器关闭。

设置HTTP响应状态码

response.setStatus()方法用于设置Servlet处理HTTP请求后的响应状态码,这个状态码在Servlet规范中定义了一系列的枚举值,我们可以直接使用它们。下面例子中,我们手动设置了一个200 OK的状态,当然,如果你不手动设置状态码,如果处理函数正常执行完成,那么它默认就是200

package com.gacfox.demo.demoweb;

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 DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.setStatus(HttpServletResponse.SC_OK);
    }
}

Servlet转发和重定向

Servlet转发(Forward)和重定向(Redirect)是Servlet中的两个重要概念,我们这里分别进行介绍。

Forward转发

Servlet转发是指在服务端程序内部,将一个HTTP请求交给下一个Servlet继续执行,下面是一个例子。

FooServlet.java

package com.gacfox.demo.demoweb;

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 FooServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        request.setAttribute("msg", "fooServlet处理过该请求");
        request.getRequestDispatcher("/bar").forward(request, response);
    }
}

BarServlet.java

package com.gacfox.demo.demoweb;

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 BarServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println(request.getAttribute("msg"));
    }
}

上面我们定义了两个Servlet,FooServlet对应的路径是/foo,而BarServlet对应的路径是/bar,我们在FooServlet中的doGet()方法里处理过请求后,又调用了request.getRequestDispatcher("/bar").forward(request, response)方法,它将请求转发给了/bar路径,即由BarServlet继续执行。在FooServlet中设置的请求域信息也会在BarServlet中被打印出来。

值得注意的是Servlet转发(Forward)是在JVM内部发生的,用户完全无法感知其中的运行逻辑,在用户看来他只发送了一个请求得到了一个响应,这和HTTP协议的301、302重定向是不同的。Servlet转发可以用于将多个Servlet在一次请求上下文中串联起来,共同组合成一个业务逻辑,这个特性在基于Servlet和JSP实现MVC设计模式时极为有用,后文将会详细介绍。

Redirect重定向

重定向就很好理解了,它其实就是让Servlet返回一个HTTP重定向响应,默认情况下它是一个302响应。

package com.gacfox.demo.demoweb;

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 FooServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.sendRedirect(request.getContextPath() + "/bar");
    }
}

上面代码会返回一个302重定向,让用户的浏览器重定向到/bar路径。重定向响应的用途也十分广泛,比如在未授权访问时,让用户直接跳转到登录页,此时使用重定向响应是十分合适的。

注意:重定向时不要写Cookie,现代浏览器基本都会在得到重定向响应时丢弃写入Cookie的操作。如果必须在重定向时写Cookie,请使用前端JavaScript方式延时重定向。

读写HTTP头信息

Servlet规范中提供了读写HTTP头信息的方法,下面是一个例子。

package com.gacfox.demo.demoweb;

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 DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 读取请求头
        String ua = request.getHeader("User-Agent");
        System.out.println(ua);
        // 写入响应头
        response.setHeader("Token", "abc123");
    }
}

这里要注意的是HTTP协议中规定了头信息是大小写不敏感的。Servlet规范中,request.getHeader("User-Agent")request.getHeader("user-agent")没有区别,它们都可以获得User-Agent头信息,响应头也是如此。

读写Cookie

Servlet规范中提供了Cookie类来处理Cookie信息,对于HttpOnly、Cookie有效期等特性均有完整支持,下面是读写Cookie的例子。

package com.gacfox.demo.demoweb;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 读取Cookie
        Cookie[] cookies = request.getCookies();
        for (Cookie c : cookies) {
            if ("text".equals(c.getName())) {
                System.out.println(c.getValue());
                break;
            }
        }
        // 写入Cookie
        Cookie cookie = new Cookie("text", "abc123");
        cookie.setMaxAge(60);
        response.addCookie(cookie);
    }
}

这里Servlet规范中对于Cookie读取的API可能设计的不太友好,我们必须遍历Cookie数组来获取我们需要的内容。

使用MVC设计模式

通过前面学习,我们可以看到Servlet输出一个页面是比较麻烦的,在实际开发中通常使用JSP作为服务端模板引擎来使用,然而直接在JSP中编写大量的后端逻辑又很不合适,因此一个最佳实践是使用MVC模式。MVC即(Model-View-Controller)设计模式,它是一个广泛使用的服务端表现层分层架构。在Servlet规范中,一个最佳实践是将JSP作为模板,将Servlet作为控制器,将普通JavaBean作为模型类。

我们知道,JSP本质还是Servlet,因此我们通过前面学习过的Forward转发就可以将Servlet和JSP串联起来,作为控制器和视图之间的桥梁。

User.java

package com.gacfox.demo.demoweb.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private String username;
    private Integer age;
}

DemoServlet.java

package com.gacfox.demo.demoweb;

import com.gacfox.demo.demoweb.model.User;

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 DemoServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        User u = new User("Tom", 18);
        request.setAttribute("user", u);
        request.getRequestDispatcher("/WEB-INF/demo.jsp").forward(request, response);
    }
}

demo.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Demo</title>
</head>
<body>
<p>${user.username}</p>
<p>${user.age}</p>
</body>
</html>

上面代码我们实现了一个最简单的MVC模式的服务端程序,我们的Servlet中生成了一个User对象,并交给了demo.jsp渲染为了HTML页面,返回给了浏览器。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap