前面我们学习过Servlet组件和Filter组件,它们的共同特点是都会用到HttpServletRequest
和HttpServletResponse
这两个对象,实际上,它们就是Servlet规范中HTTP请求和HTTP响应的封装类。这篇笔记我们学习如何在Servlet规范中处理HTTP请求和响应,以及如何实现文件上传、文件下载等功能。
Servlet规范中,和HTTP请求相关的方法都是围绕HttpServletRequest
类定义的。
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请求可以包含一个请求体(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-urlencoded
和multipart/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 {
// ... 具体业务逻辑
}
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容器关闭。
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转发(Forward)和重定向(Redirect)是Servlet中的两个重要概念,我们这里分别进行介绍。
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设计模式时极为有用,后文将会详细介绍。
重定向就很好理解了,它其实就是让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方式延时重定向。
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
头信息,响应头也是如此。
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数组来获取我们需要的内容。
通过前面学习,我们可以看到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页面,返回给了浏览器。