本文内容均来自书籍《Head First Servlets & JSP》
文章目录
四、作为 Servlet
Servlet 的生命周期和 API
一个 Servlet 大概的生命周期流程:
用户点击一个链接 -> 容器解析该链接,发现请求的是一个 Servlet -> 创建两个对象 HttpServletRequest 和 HttpServletResponse -> 查找到正确的 Servlet,为其创建或分配一个线程 -> 将请求和响应对象作为参数传入 Servlet 的 service 方法,并调用该方法 -> 根据用户时 GET 或 POST 请求,service 方法调用 doGet 或 doPost 方法 -> 响应给用户后,该线程撤销或返回到容器的线程池中
每个客户为每个请求分配一个单独的线程,容器会创建一对新的请求和响应对象
-
ServletConfig
- 每一个 Servlet 对象存在一个 ServletConfig 对象,用于向 Servlet 传递部署时的信息
- 用于访问 ServletContext
-
ServletContext
- 每个 Web 应用存在一个 ServletContext,用于访问 Web 参数或放置应用消息
-
Http 方法
- GET :请求 URL 上的资源文件
- POST:请求一个服务并携带请求体参数
- HEAD:获取请求头信息
- TRACE:要求请求消息回送,方便测试和排错
- PUT:将要包含的信息体放在请求的 URL 上
- DELETE:请求删除资源文件
- OPTIONS:获取一个 HTTP 方法列表,所请求 URL 上的东西可以对这些 HTTP 请求做出响应
- CONNECT:要求连接以便建立隧道
-
幂等和非幂等请求
-
幂等操作:可以多次执行同一件事,而不会产生副作用
特点:任意多次执行所产生的影响均与一次执行的影响相同,不会影响系统状态
-
GET 请求是幂等的,能执行多次,且不会产生不好的副作用;POST 请求是非幂等的,当 POST 提交数据时可能产生不可逆的影响
-
重定向与请求分派
Servlet 重定向:
// 不以斜线开头,相对原请求增加 URL
response.sendRedirect("https://www.baidu.com");
// 以斜线开头,容器会相对于 Web 应用本身建立 URL
response.sendRedirect("/foo/czen.html");
Servlet 请求分派:
RequestDispatcher view = request.getRequestDisapatcher("result.jsp");
view.forward(request, response);
重定向:将客户的请求直接转发到 另一个 URL 来处理,是在客户端(浏览器)上发生的;请求分派:让另一个服务接管当前服务,或者说是将当前请求传递给服务器上另一个组件,是在服务器端发生的。
五、作为 Web 应用
初始化参数
容器初始化一个 Servlet 时,会为这个 Servlet 创建一个唯一的 ServletConfig 。容器从 DD 中读取 Servlet 初始化参数交给 ServletConfig,当调用 Servlet 的 init() 方法时,传入 ServletConfig 的引用。
- ServletConfig
其主要任务时提供初始化参数,用于 Servlet 的配置
out.println(getServletConfig().getInitParameter("adminEmail"));
- ServletContext
上下文初始化参数,相较于 ServletConfig 对单个 Servlet 初始化参数,ServletContext 针对整个 Web 应用初始化数据,对于整个 Web 应用只存在一个 ServletContext。
out.println(getServletContext().getInitParameter("adminEmail"));
每个 Servlet 有一个 ServletConfig;每个 Web 应用有一个 ServletContext,ServletContext 中的参数内容对整个 Web 应用中的 Servlet 和 JSP 都可用。
一个简单的 ServletContextListener
1、创建一个上下文监听者类,用于在 Servlet 初始化前从 ServletContext 中获取该数据(String 或者对象类型)
public class MyServletContextListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent event) {
ServletContext context = event.getServletContext();
// 从上下文中获取参数
String dogBreed = context.getInitParameter("breed");
Dog dog = new Dog(dogBreed);
context.setAttribute("dog", dog);
}
public void contextDestoryed(ServletContextEvent event) {}
}
2、编写部署描述文件
<webapp>
<listener>
<listener-class>
com.example.MyServletContextListener
</listener-class>
</listener>
</webapp>
3、编写 Servlet 测试类判断上下文监听者是否初始化数据成功
// 在 Servlet 的 doGet() 方法中获取上下文参数
Dog dog = (Dog) getServletContext().getAttribute("dog");
out.println("Dog's breed is " + dog.getBreed());
有了 ServletContextListener 后,Servlet 的执行流程:
-容器读取 DD 中的 <listener> 和 <context-param> 标签,创建一个新的 ServletContext
-为 ServletContextListener 创建一个新的实例,传入 ServletContextEvent ,获取 ServletContext 的一个引用,获取并初始化参数,构造完成对象后,再放入 ServletContext 中
-容器创建一个新的 Servlet,利用初始化参数构建 ServletConfig,为 ServletConfig 提供 ServletContext 的一个引用,调用 Servlet 的 init() 方法,Servlet 得到一个请求,从 ServletContext 中获取提前初始化好的参数
- 8 个监听者
- ServletContextAttributeListener
- HttpSessionListener
- ServletRequestListener
- ServletRequestAttributeListener
- HttpSessionBindingListener
属性本身在增加到一个会话或者从会话删除时,得到通知 - HttpSessionAttributeListener
会话中增加、删除和替换某种类型的属性时,得到通知 - ServletContextListener
- HttpSessionActivationListener
三个作用域:上下文、请求和会话
上下文 ServletContext
应用中的每一部分都能访问,上下文作用域不是线程安全的。上下文的生命周期就是该应用部署的生命周期,服务器或应用关闭时,其生命周期结束。
Head First Quest01
Q:如何确保上下文作用域的线程安全?
A:对上下文加锁,而非 Sevlet 加锁;若对 Servlet 的服务方法 doGet() 方法加锁,无法保证线程安全,因为它只会防止同一个 Servlet 中的其他线程访问上下文属性,无法阻止另外一个 Servlet 访问上下文属性;正确的做法是对上下文对象本身同步。
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
// 其他逻辑代码
synchronized(getServletContext()) { // 在需要获取的上下文作用域中加锁保证线程安全
getServletContext().setAttribute("hello","22");
out.println(getServletContext().getAttribute("hello"));
}
}
TIPS:通过加锁保证线程同步一定要在最短时间内完成目标,使持有锁的时间最短,让同步块尽可能的小,进入同步块获取资源后尽快释放锁,以保证其他线程的运行速度。
会话 HttpSession
能访问特定 HttpSession 的部分才能访问;用于维护与一个客户的会话状态,一个客户一个会话中存在多个请求;会话属性也是非线程安全的,可以通过在 Httpsession 对象中加锁来保证线程的安全。
请求 ServletRequest
能访问特定 ServletRequest 的部分才能访问,为线程安全的。请求的生命周期会持续到 Servlet 的 Service() 方法结束。
Person p = new Person();
ArrayList result = p.getCatsNames(p);
// 将数据放在请求作用域中确保线程安全性
request.setAttribute("personCatsNames", result);
// 为视图 JSP 获取一个分派器
RequestDispatcher view = request.getRequestDispatcher("result.jsp");
// 让 JSP 接管该请求
view.forward(request, response);
/*
* view.include()
* 将请求发送给别人来完成,再返回发送者
* view.forward()
* 将请求完全交给其他应用处理
*/
六、会话状态
Head First Thinking01
T:现在我们的啤酒网站要实现一个功能,当用户选择一种啤酒品牌后,可以根据该客户以往选择的啤酒数据,返回一个新的建议;如果该客户以往的数据信息不足,则要求用户再回答一个问题,并给出建议
A:主要的解决方法有三个:(1 让 servlet 成为一个有状态会话 bean 的客户端,每次接受请求后,获取到该客户对应的 bean;(2 使用数据库存储客户的历史数据;(3 使用一个 HttpSession 对象保存跨多个请求的会话状态
HttpSession 对象可以保存跨同一个客户多个请求的会话状态,即一个特定的客户在整个会话期间,HttpSession 会持久存在
PrintWriter out = response.getWriter();
/**
* getSession 方法会使用已存在的会话或创建一个新的会话
* 这一步表示从请求中获取到会话 ID,会话 ID 由 cookie 携带的 JSESSIONID 存储
*/
HttpSession session = request.getSession();
if(session.isNew()) {
out.print("This is a new session!")
}
/**
* 若客户端禁用了 cookie,则每次都会生成新的 session,且不会返回携带由会话 ID 的 cookie 响应数据
* encodeURL() 相当于对 URL 进行了重写,在 URL 后添加 JSESSIONID 存储该请求的会话状态
*/
out.print("<a href='" + response.encodeURL("/Beer.do")) + "'> click me </a>");
Head Firse Thinking02
T:服务器中大量 session 会占用系统资源,如何在会话结束时,安全的删除一个会话信息呢?
Q:下面的 session 方法能够启发你,即为每个请求的会话设置一个存活时间
getCreateTime() // 返回第一次会话创建的时间
getLastAccesedTime() // 返回最后一次得到该会话 ID 的请求过去了多长时间
setMaxInactiveInterval() // 设置当前会话的最大存活时间
invalidate() // 结束会话
cookie 的设计实际上是为了支持 session,cookie 是在客户端和服务端之间进行交互的一段数据(键值对);浏览器发送请求时携带 cookie,服务器根据请求返回一个携带该 cookie 的响应。
- cookie 的交换是自动完成的
- 可以使 cookie 的存活时间长于 session,即意味着客户的会话信息能以 cookie 的形式持久化到计算机中
// 一个关于 cookie 的简单示例:获取用户名存储到 cookie 中返回给客户
public class CookieTest extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response) {
String name = request.getParameter("username");
Cookie cookie = new Cookie("username", name);
cookie.setMaxAge(30 * 60); // 设置 cookie 存活时间为 30 分钟
response.addCookie(cookie);
RequestDispatcher view = request.getRequestDispatcher("result.jsp");
view.forward(request, response);
}
}
当部署的是分布式应用时,只有 HttpSession 对象(及其属性)能从一个 VM 迁移到另一个 VM;对于一个给定的 Web 应用,VM 中不会出现相同的会话 ID。
后记
毕业后因为找不到工作,去餐厅打工了一段时间,最后还是决定去 Java 培训。目前培训课程已经结束,即将面临找工作阶段,有时候有些后悔,当初为什么要选 Java,相比于大多数转计算机的竞争者来说,我当初选 Java 只是想尽快就业。没想到 Java 已经卷到如此地步,不禁令人感慨:对于 Java 这一行来说,新人极难入行,况且大量培训班的存在,本身就让大多企业失去了对新手培训的耐心。有时候又觉得很好笑,大批大批的人去卷是为了赚钱,而另一批卷下来的人又开始教别人怎么去卷,如此循环,造就了如此高的入门壁垒。大环境下,20 年后,整个行业,整个社会又会是什么样呢?
上一篇:Head First Servlets & JSP 笔记 01
下一篇:Head First Servlets & JSP 笔记 03