重定向与转发
Redirect
重定向是指当浏览器请求一个URL时,服务器返回一个重定向指令,告诉浏览器地址已经变了,麻烦使用新的URL再重新发送新请求。
例如,我们已经编写了一个能处理/first
的FirstServlet
,如果收到的路径为/first
,希望能重定向到/second
,可以再编写一个SecondServlet
:
@WebServlet("/first")
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String name = req.getParameter("name");
resp.sendRedirect("/second"+(name==null?"":"?name="+name));
}
}
如果浏览器发送GET /hi
请求,RedirectServlet
将处理此请求。由于RedirectServlet
在内部又发送了重定向响应,因此,浏览器会收到如下响应:
HTTP/1.1 302 Found
Location: /hello
当浏览器收到302响应后,它会立刻根据Location
的指示发送一个新的GET /hello
请求,这个过程就是重定向:
┌───────┐ GET /hi ┌───────────────┐
│Browser│ ────────────> │RedirectServlet│
│ │ <──────────── │ │
└───────┘ 302 └───────────────┘
┌───────┐ GET /hello ┌───────────────┐
│Browser│ ────────────> │ HelloServlet │
│ │ <──────────── │ │
└───────┘ 200 <html> └───────────────┘
观察Chrome浏览器的网络请求,可以看到两次HTTP请求:
重定向有两种:一种是302响应,称为临时重定向,一种是301响应,称为永久重定向。两者的区别是,如果服务器发送301永久重定向响应,浏览器会缓存/hi
到/hello
这个重定向的关联,下次请求/hi
的时候,浏览器就直接发送/hello
请求了。
重定向有什么作用?重定向的目的是当Web应用升级后,如果请求路径发生了变化,可以将原来的路径重定向到新路径,从而避免浏览器请求原路径找不到资源。
HttpServletResponse
提供了快捷的redirect()
方法实现302重定向。如果要实现301永久重定向,可以这么写:
resp.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); // 301
resp.setHeader("Location", "/second");
Forward
Forward是指内部转发。当一个Servlet处理请求的时候,它可以决定自己不继续处理,而是转发给另一个Servlet处理。
例如,我们已经编写了一个能处理/first
的FirstServlet
,继续编写一个能处理/second
的SecondServlet
:
@WebServlet("/first")
public class FirstServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getRequestDispatcher("/second").forward(req,resp);
}
}
FirstServlet
在收到请求后,它并不自己发送响应,而是把请求和响应都转发给路径为/second
的Servlet,即下面的代码:
req.getRequestDispatcher("/second").forward(req, resp);
后续请求的处理实际上是由HelloServlet
完成的。这种处理方式称为转发(Forward),我们用流程图画出来如下:
┌────────────────────────┐
│ ┌───────────────┐ │
│ ────>│ FirstServlet │ │
┌───────┐ GET /first │ └───────────────┘ │
│Browser│ ──────────────> │ │ │
│ │ <────────────── │ ▼ │
└───────┘ 200 <html> │ ┌───────────────┐ │
│ <────│ SecondServlet │ │
│ └───────────────┘ │
│ Web Server │
└────────────────────────┘
转发和重定向的区别在于,转发是在Web服务器内部完成的,对浏览器来说,它只发出了一个HTTP请求:
使用Session和Cookie
在Web应用程序中,我们经常要跟踪用户身份。当一个用户登录成功后,如果他继续访问其他页面,Web程序如何才能识别出该用户身份?
因为HTTP协议是一个无状态协议,即Web应用程序无法区分收到的两个HTTP请求是否是同一个浏览器发出的。为了跟踪用户状态,服务器可以向浏览器分配一个唯一ID,并以Cookie的形式发送到浏览器,浏览器在后续访问时总是附带此Cookie,这样,服务器就可以识别用户身份。
Session
我们把这种基于唯一ID识别用户身份的机制称为Session。每个用户第一次访问服务器后,会自动获得一个Session ID。如果用户在一段时间内没有访问服务器,那么Session会自动失效,下次即使带着上次分配的Session ID访问,服务器也认为这是一个新用户,会分配新的Session ID。
JavaEE的Servlet机制内建了对Session的支持。我们以登录为例,当一个用户登录成功后,我们就可以把这个用户的名字放入一个HttpSession
对象,以便后续访问其他页面的时候,能直接从HttpSession
取出用户名:
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = req.getParameter("username");
String password= req.getParameter("password");
System.out.println(username+" : "+password);
if("admin".equals(username)&&"123".equals(password)){
//登录成功
//记录当前登录状态
req.getSession().setAttribute("currentUser",username);
resp.sendRedirect("/index");
}else{
resp.sendError(404);
}
}
}
上述LoginServlet
在判断用户登录成功后,立刻将用户名放入当前HttpSession
中:
HttpSession session = req.getSession();
session.setAttribute("currentUser", username);
在IndexServlet
中,可以从HttpSession
取出用户名:
@WebServlet("/index")
public class IndexServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html;charset=utf-8");
PrintWriter out = resp.getWriter();
HttpSession session = req.getSession();
String currentUser = null;
if(session.getAttribute("currentUser")!=null){
currentUser = session.getAttribute("currentUser").toString();
out.write("<h1>欢迎登陆 "+currentUser+"</h1>");
out.write("<h2><a href='logout'>登出</a></h2>");
}else{
resp.sendRedirect("index.html");
}
}
}
如果用户已登录,可以通过访问/logout
登出。登出逻辑就是从HttpSession
中移除用户相关信息:
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.getSession().removeAttribute("currentUser");
resp.sendRedirect("/index");
}
}
对于Web应用程序来说,我们总是通过HttpSession
这个高级接口访问当前Session。如果要深入理解Session原理,可以认为Web服务器在内存中自动维护了一个ID到HttpSession
的映射表,我们可以用下图表示:
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐
│ ┌───────────────┐ │
┌───>│ IndexServlet │<──────────┐
│ │ └───────────────┘ ▼ │
┌───────┐ │ ┌───────────────┐ ┌────────┐
│Browser│──┼─┼───>│ LoginServlet │<────>│Sessions││
└───────┘ │ └───────────────┘ └────────┘
│ │ ┌───────────────┐ ▲ │
└───>│LogoutServlet │<──────────┘
│ └───────────────┘ │
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘
而服务器识别Session的关键就是依靠一个名为JSESSIONID
的Cookie。在Servlet中第一次调用req.getSession()
时,Servlet容器自动创建一个Session ID,然后通过一个名为JSESSIONID
的Cookie发送给浏览器:
这里要注意的几点是:
JSESSIONID
是由Servlet容器自动创建的,目的是维护一个浏览器会话,它和我们的登录逻辑没有关系;- 登录和登出的业务逻辑是我们自己根据
HttpSession
是否存在一个"user"
的Key判断的,登出后,Session ID并不会改变; - 即使没有登录功能,仍然可以使用
HttpSession
追踪用户,例如,放入一些用户配置信息等。
除了使用Cookie机制可以实现Session外,还可以通过隐藏表单、URL末尾附加ID来追踪Session。这些机制很少使用,最常用的Session机制仍然是Cookie。
使用Session时,由于服务器把所有用户的Session都存储在内存中,如果遇到内存不足的情况,就需要把部分不活动的Session序列化到磁盘上,这会大大降低服务器的运行效率,因此,放入Session的对象要小,通常我们放入一个简单的User
对象就足够了:
public class User {
public long id; // 唯一标识
public String email;
public String name;
}
在使用多台服务器构成集群时,使用Session会遇到一些额外的问题。通常,多台服务器集群使用反向代理作为网站入口:
┌────────────┐
┌───>│Web Server 1│
│ └────────────┘
┌───────┐ ┌─────────────┐ │ ┌────────────┐
│Browser│────>│Reverse Proxy│───┼───>│Web Server 2│
└───────┘ └─────────────┘ │ └────────────┘
│ ┌────────────┐
└───>│Web Server 3│
└────────────┘
如果多台Web Server采用无状态集群,那么反向代理总是以轮询方式将请求依次转发给每台Web Server,这会造成一个用户在Web Server 1存储的Session信息,在Web Server 2和3上并不存在,即从Web Server 1登录后,如果后续请求被转发到Web Server 2或3,那么用户看到的仍然是未登录状态。
要解决这个问题,方案一是在所有Web Server之间进行Session复制,但这样会严重消耗网络带宽,并且,每个Web Server的内存均存储所有用户的Session,内存使用率很低。
另一个方案是采用粘滞会话(Sticky Session)机制,即反向代理在转发请求的时候,总是根据JSESSIONID的值判断,相同的JSESSIONID总是转发到固定的Web Server,但这需要反向代理的支持。
无论采用何种方案,使用Session机制,会使得Web Server的集群很难扩展,因此,Session适用于中小型Web应用程序。对于大型Web应用程序来说,通常需要避免使用Session机制。
Cookie
实际上,Servlet提供的HttpSession
本质上就是通过一个名为JSESSIONID
的Cookie来跟踪用户会话的。除了这个名称外,其他名称的Cookie我们可以任意使用。
如果我们想要设置一个Cookie,例如,记录用户选择的语言,可以编写一个LanguageServlet
:
@WebServlet(urlPatterns = "/pref")
public class LanguageServlet extends HttpServlet {
private static final Set<String> LANGUAGES = Set.of("en", "zh");
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String lang = req.getParameter("lang");
if (LANGUAGES.contains(lang)) {
// 创建一个新的Cookie:
Cookie cookie = new Cookie("lang", lang);
// 该Cookie生效的路径范围:
cookie.setPath("/");
// 该Cookie有效期:
cookie.setMaxAge(8640000); // 8640000秒=100天
// 将该Cookie添加到响应:
resp.addCookie(cookie);
}
resp.sendRedirect("/");
}
}
创建一个新Cookie时,除了指定名称和值以外,通常需要设置setPath("/")
,浏览器根据此前缀决定是否发送Cookie。如果一个Cookie调用了setPath("/user/")
,那么浏览器只有在请求以/user/
开头的路径时才会附加此Cookie。通过setMaxAge()
设置Cookie的有效期,单位为秒,最后通过resp.addCookie()
把它添加到响应。
如果访问的是https网页,还需要调用setSecure(true)
,否则浏览器不会发送该Cookie。
因此,务必注意:浏览器在请求某个URL时,是否携带指定的Cookie,取决于Cookie是否满足以下所有要求:
- URL前缀是设置Cookie时的Path;
- Cookie在有效期内;
- Cookie设置了secure时必须以https访问。
我们可以在浏览器看到服务器发送的Cookie:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rH1p20ri-1598159060760)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1598025875650.png)]
如果我们要读取Cookie,例如我们在ParseCookies读取名为lang
的Cookie以获取用户设置的语言
package com.softeem.server;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebServlet("/parseCookies")
public class ParseCookie extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Cookie [] cookies = req.getCookies();
String currentLang= "EN"; //set default
if(cookies!=null){
for (Cookie cookie : cookies) {
System.out.println(cookie.getName());
if(cookie.getName().equals("lang")){
System.out.println(cookie.getValue());
}
}
}
resp.sendRedirect("/");
}
}
可见,读取Cookie主要依靠遍历HttpServletRequest
附带的所有Cookie。
JSP开发
我们从前面的章节可以看到,Servlet就是一个能处理HTTP请求,发送HTTP响应的小程序,而发送响应无非就是获取PrintWriter
,然后输出HTML:
PrintWriter pw = resp.getWriter();
pw.write("<html>");
pw.write("<body>");
pw.write("<h1>Welcome, " + name + "!</h1>");
pw.write("</body>");
pw.write("</html>");
pw.flush();
只不过,用PrintWriter输出HTML比较痛苦,因为不但要正确编写HTML,还需要插入各种变量。如果想在Servlet中输出一个类似新浪首页的HTML,写对HTML基本上不太可能。
那有没有更简单的输出HTML的办法?
有!
我们可以使用JSP。
JSP是Java Server Pages的缩写,它的文件必须放到/src/main/webapp
下,文件名必须以.jsp
结尾,整个文件与HTML并无太大区别,但需要插入变量,或者动态输出的地方,使用特殊指令<% ... %>
。
我们来编写一个hello.jsp
,内容如下:
<html>
<head>
<title>Hello World - JSP</title>
</head>
<body>
<%-- JSP Comment --%>
<h1>Hello World!</h1>
<p>
<%
out.println("Your IP address is ");
%>
<span style="color:red">
<%= request.getRemoteAddr() %>
</span>
</p>
</body>
</html>
整个JSP的内容实际上是一个HTML,但是稍有不同:
- 包含在
<%--
和--%>
之间的是JSP的注释,它们会被完全忽略; - 包含在
<%
和%>
之间的是Java代码,可以编写任意Java代码; - 如果使用
<%= xxx %>
则可以快捷输出一个变量的值。
JSP页面内置了几个变量:
- out:表示HttpServletResponse的PrintWriter;
- session:表示当前HttpSession对象;
- request:表示HttpServletRequest对象。
这几个变量可以直接使用。
访问JSP页面时,直接指定完整路径。例如,http://localhost:8080/hello.jsp
,浏览器显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zenyuEoi-1598159060772)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1598026083986.png)]
SP和Servlet有什么区别?其实它们没有任何区别,因为JSP在执行前首先被编译成一个Servlet。在Tomcat的临时目录下,可以找到一个hello_jsp.java
的源文件,这个文件就是Tomcat把JSP自动转换成的Servlet源码:
package org.apache.jsp;
import ...
public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports {
...
public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {
...
out.write("<html>\n");
out.write("<head>\n");
out.write(" <title>Hello World - JSP</title>\n");
out.write("</head>\n");
out.write("<body>\n");
...
}
...
}
可见JSP本质上就是一个Servlet,只不过无需配置映射路径,Web Server会根据路径查找对应的.jsp
文件,如果找到了,就自动编译成Servlet再执行。在服务器运行过程中,如果修改了JSP的内容,那么服务器会自动重新编译。
JSP高级功能
JSP的指令非常复杂,除了<% ... %>
外,JSP页面本身可以通过page
指令引入Java类:
<%@ page import="java.io.*" %>
<%@ page import="java.util.*" %>
这样后续的Java代码才能引用简单类名而不是完整类名。
使用include
指令可以引入另一个JSP文件:
<html>
<body>
<%@ include file="header.jsp"%>
<h1>Index Page</h1>
<%@ include file="footer.jsp"%>
</body>
JSP Tag
JSP还允许自定义输出的tag,例如:
<c:out value = "${sessionScope.user.name}"/>
JSP Tag需要正确引入taglib的jar包,并且还需要正确声明,使用起来非常复杂,对于页面开发来说,不推荐使用JSP Tag,因为我们后续会介绍更简单的模板引擎
小结
JSP是一种在HTML中嵌入动态输出的文件,它和Servlet正好相反,Servlet是在Java代码中嵌入输出HTML;
JSP可以引入并使用JSP Tag,但由于其语法复杂,不推荐使用;
JSP本身目前已经很少使用,我们只需要了解其基本用法即可。