属性和监听
本篇对应《head first servlet and jsp》的第五章。该章介绍了servletConfig和servletContext在不同场景下的应用,并探讨了线程安全问题和小小的解决方法。
servletConfig
servletConfig和servletContext解决了初始化参数的硬编码问题。比如我们想输出一句话,硬编码中我们是这样写的
PrintWriter out = response.getWriter();
out.printlin("I love servlet!!!");
众所周知,硬编码是一个非常不优雅的写法😱😱😱比如过了一段时间,我们学习了spring之后再也不用像现在这样麻烦地写servlet了。我想改成I love spring呢?(●’◡’●)
这个时候我们想到使用servletConfig ,通过xml部署文件来解决硬编码的问题
<servlet>
<servlet-name>servletStudy</servlet-name>
<servlet-class>com.highway.servlet.user.servletStudy</servlet-class>
<!--我们通过init-param标签设置初始化参数 -->
<init-param>
<param-name>speak</param-name>
<param-value>I love servlet</param-value>
</init-param>
</servlet>
然后我们的Java代码就可以写成这样了
out.printlin(getServletConfig().getInitParameter("speak"));
这样是不是显得非常优雅了呢( •̀ ω •́ )y
需要注意的是init-param标签写在一个servlet标签里,从这一点说明了servletConfig只对一个servlet有效。servletConfig也被称为servlet初始化参数 ,servlet初始化参数只会在servlet被容器初始化时有且仅有一次被读取,当这个servlet实例化后永远也不可能再回头读取servlet初始化参数了(除非你重新部署该项目)
我们接下来看看容器时怎么读取servlet初始化参数的吧
servletConfig的诞生
- 首先容器会读取该servlet的部署描述文件,包括servlet初始化参数 。换句话说就是这个servlet标签里的所有内容
- 容器为这个servlet创建一个ServletConfig实例
- 容器为每个init-param创建一个String键值对根据该例,Key是speak,Value是I love servlet
- 容器向ServletConfig提供init-param键值对的引用 ,注意是引用哦,因为是String嘛😋
- 容器创建servlet 。这个时候servlet才被创建哦😃😃😃
- 容器调用servlet的init()方法 ,传入一个ServletConfig的引用
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {}
该方法继承自GenericServlet
servletConfig的生命之旅到此结束了(●’◡’●)
下面将引出servletContext
ServletContext
当另外一个servlet也想大喊一声“I love servlet”时,我们就要跑到这个servlet的标签里再写一遍init-param ,这样一来代码复用又变得很差。那这个时候我们该怎么办呢?
我们需要一些全局性的东西,这个东西就是上下文初始化参数用英文的讲法就是ServletContext
ServletContext同样通过xml部署文件进行部署
<context-param>
<param-name>speak</param-name>
<param-value>I love servlet</param-value>
</context-param>
可以看到context-param标签并不需要写在一个servlet标签里,只需要写在web-app标签里就行了。
所以其实跟servletConfig来看其实区别只是在于一个是全局一个是局部而已。可以看到Java代码都非常的相似。
//getServletConfig()
out.printlin(getServletConfig().getInitParameter("speak"));
//getServletContext()
out.printlin(getServletContext().getInitParameter("speak"));
而这个区别其实从方法名上来看非常好理解的😋
❗❗❗每个servlet有一个ServletConfig,每个Web应用有一个ServlentContext
ServletContext的诞生
- 容器读部署文件,为每个context-param标签创建一个String键值对
- 容器创建一个ServletContext的实例
- 容器为ServletContext提供String键值对的引用
- 在Web应用中部署的各个servlet和JSP都能访问该ServletContext
❗❗❗如果这个Web应用是分布的,那么每个JVM有一个属于自己的ServletContext并且 99.9 % 99.9\% 99.9%的情况下,ServletContext的内容是相同的
接着一个问题随之而来,很显然的可以看到,这些初始化参数都是String键值对 ,可想而知String能做的事情非常的有限💀💀💀如果我想要初始化的是一个Object呢?
不要着急,我们拥有这样的技术( *^-^)ρ(^0^* )
监听器
监听器Listener可以在特定事件发生时出现并帮助我们完成一些事情,有各种各样的Listener在这里我们只讲解ServletContextListener ,看名知意这个Listener就是专门监听👂ServletContext的,也就是说在ServletContext发生了什么的时候,这个Listener会完成一些事情。至于是什么时候呢?下面将会揭晓。
你可能想马上知道ServletContextListener到底干了什么,居然可以让一个String初始化参数化String为Object ,没错,我看到这里也充满了好奇❓❓❓
ServletContextListener
ServletContextListener负责监听ServletContext ,同时ServletContextListener是一个接口,实现这个接口需要覆盖2个方法
package javax.servlet;
import java.util.EventListener;
public interface ServletContextListener extends EventListener {
//当context初始化时调用该函数
void contextInitialized(ServletContextEvent var1);
//当context销毁时调用该函数
void contextDestroyed(ServletContextEvent var1);
}
只要覆盖了这2个方法就能在ServletContext 初始化时和销毁时做一些事情了。
我们用一个例子看看Listener怎么化String为Object的
当然在此之前,需要说一说我们需要准备什么?
假设我们需要初始化一个名为highway的学生,那么我们需要准备什么呢❓
- 当然是不能缺少的ServletContextListener ,在本例中该Listener名为StudentListener
- 需要准备一个Student实体类 ,在本例中该实体类就叫Student
- 可能会迟到但永远不会缺席的servlet 😝😝😝 ,在本例中该servlet名为servletStudy
在此之前,先看看xml部署文件然后再来看看具体的实现
<web-app>
<!-- 上下文初始化参数-->
<context-param>
<param-name>name</param-name>
<param-value>highway</param-value>
</context-param>
<!-- 注册监听器-->
<listener>
<listener-class>com.highway.listener.StudentListener</listener-class>
</listener>
<!--配置servlet-->
<servlet>
<servlet-name>servletStudy</servlet-name>
<servlet-class>com.highway.servlet.user.servletStudy</servlet-class>
</servlet>
<!--配置servlet-mapping-->
<servlet-mapping>
<servlet-name>servletStudy</servlet-name>
<url-pattern>/study.do</url-pattern>
</servlet-mapping>
</web-app>
看完配置文件之后大概了解一下架子了吧,那么我们按照顺序进行准备吧,首先我们准备一个ServletContextListener
package com.highway.listener;
import com.highway.pojo.Student;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
public class StudentListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
//获取ServletContext
ServletContext servletContext = servletContextEvent.getServletContext();
//从ServletContext中获得param-name==name的值
String name = servletContext.getInitParameter("name");
//这是化String为Object的关键
Student student = new Student(name);
//将student设置为servletContext的Attribute
servletContext.setAttribute("student",student);
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
可以看到通过这样的方式new出来了一个Student ,所以其实并没有你想的那么神奇哦😜😜😜
Student类
package com.highway.pojo;
import java.io.Serializable;
public class Student implements Serializable {
private String name;
public Student() {
}
public Student(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
'}';
}
}
可以看到,在StudentListener中调用的有参构造,我们实例出了一个名为highway的学生,并且通过setAttribute()方法把这个实例设置为了ServletContext的属性。属性Attribute和String初始化参数init-param的区别让我们能够完成化String为Object的魔法🧙。
那么他们的区别在哪呢?会在下面介绍Attribute的时候再说,现在我们不需要关注他,只要知道这样就能把这个Object设置到ServletContext上,然后让所有的servlet访问到这个Object 。
现在将目光移回到我们最后一样东西servlet了
package com.highway.servlet.user;
import com.highway.pojo.Student;
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 servletStudy extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//从Context拿到这个student,注意的是一定要强制类型转换!强制类型转换!强制类型转换!
Student student = (Student)getServletContext().getAttribute("student");
//又是一个setAttribute,不用着急,现在来说还不重要
req.setAttribute("name",student.getName());
//转发至student.jsp
req.getRequestDispatcher("student.jsp").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
来看看student.jsp干了啥吧。其实就是把这个学生的名字打印出来(●ˇ∀ˇ●)(●ˇ∀ˇ●)(●ˇ∀ˇ●)
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title>主页</title>
</head>
<body>
<h2>Hello World!</h2><br/>
<div class="name">${name}</div><br/>
</body>
</html>
或许单看代码,你还有点糊涂,那么下面用文字来描述一遍吧。当然我肯定会建议你边看文字描述边翻代码对应。
- 容器读部署文件,注册Listener ,为每个context-param标签创建一个String键值对
- 容器创建一个ServletContext的实例
- 容器为ServletContext提供String键值对的引用,在本例中Key是name,Value是highway。并将这些键值对的引用交给ServletContext
- 容器创建一个实现ServletContextListener接口的StudentListener类
- 容器调用Listener的contextInitialized()方法 ,传入新的ServletContextEvent 。这个事件对象有一个ServletContext引用,所以事件处理代码可以从事件中得到上下文 ,并从上下文得到上下文初始化参数 。Listener向servletContextEvent要ServletContext的引用
ServletContext servletContext = servletContextEvent.getServletContext();
- 然后从ServletContext的引用中获得上下文初始化参数name
String name = servletContext.getInitParameter("name");
- Listener使用这个String来构造一个Student对象
Student student = new Student(name);
- Listener将这个Student对象设置为ServletContext的一个Attribute
servletContext.setAttribute("student",student);
- 到此为止,这个ServletContext就完成了(>人<;)那么剩下的事情我想你已经非常熟悉了,容器建立一个新的servlet ,当然在容器调用init()方法时已经建立了一个ServletConfig ,并且这个ServletConfig 里有个ServletContext的引用
- servletStudy得到一个请求,向ServletContext请求属性"student"
Student student = (Student)getServletContext().getAttribute("student");
一定要强制类型转换(>人<;)! 一定要强制类型转换(>人<;)! 一定要强制类型转换(>人<;)!
- 获得student的name ,向HttpServletRequest设置属性,然后getRequestDispatcher转发
req.setAttribute("name",student.getName());
req.getRequestDispatcher("student.jsp").forward(req, resp);
到此为止结束(●ˇ∀ˇ●)我相信你应该明白了👌👌👌如果不明白,那就重复看几遍吧(≧﹏ ≦)
Attribute是什么?
在上面的例子中,你可能早就已经迫不及待的想知道Attribute是什么了吧?
说起来其实非常简单😝😝😝
Attribute是一个对象,Attribute可以设置到3个servlet API对象中,分别是ServletContext 、HttpServletRequest 、HttpSession前2种我们在上面的代码已经使用过了,还剩下最后一种,那么什么是session呢?不要着急,我们**先把Attribute是什么?**的事情说完🤣
你可以简单的理解为是一个映射实例对象种的键值对 ,不同的是Key为一个String ,Value则是一个Object 。在实际中,我们并不❌知道也不❌关心具体实现,我们只关心🧡属性所在的作用域。
属性 | 参数 | |
---|---|---|
类型 | 应用/上下文 请求 会话 | 应用/上下文初始化参数 请求参数 servlet初始化参数 |
设置方法 | setAttribute(String name, Object value) | 不能设置应用和servlet初始化参数 对于请求参数可以,但这是另外一回事了 |
返回类型 | Object | String |
获取方法 | getAttribute(String name) | getInitParameter(String name) |
线程安全问题
上下文属性是线程安全的吗?
线程安全问题应该说跟Attribute本身没有什么关系,那么问题出在哪呢?你一定会觉得诧异👀
线程安全跟这个Attribute在哪个作用域有关(应用/上下文、请求、会话)
经过这么多的学习,相信你一定知道,该Web应用中的所有servlet都能访问这个共有的ServletContext ,那么线程A设置了一个Attribute ,这当然没有出现“所谓的”线程安全问题,但是如果这个时候我们加入一个线程B呢?(当然,这里我默认你已经学习多线程及线程同步的相关知识)
很显然这个时候线程不安全的问题就有可能出现了。
这时你肯定在想,那么我们应该怎么做呢?😱😥😭
我们可以在服务方法上加一个synchronized
public class servletStudy extends HttpServlet {
@Override
protected synchronized void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Student student = (Student)getServletContext().getAttribute("student");
req.setAttribute("name",student.getName());
req.getRequestDispatcher("student.jsp").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
这个方法解决了线程不安全的问题吗?🕓停下来想一想。
答案是没有。
给服务方法上加一个synchronized只是解决了一个servlet一次只会处理一个服务方法 ,但是其他的servlet还是可以访问到这个Attribute的哦(#°Д°)
到这里你应该明白synchronized应该加给谁了吧?很显然是要对这个Attribute加synchronized👏
那么怎么加synchronized也是需要思考💡的一个问题。
只有当处理这些上下文属性的所有其他代码也对ServletContext同步时才能奏效,如果有一段代码没有请求锁🔒的话,那么这个代码就能自由地访问上下文属性 ,这也表示我们的设计功亏一篑👻👻👻
通过这个例子来说明如何加锁🔒
public class StudentListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
ServletContext servletContext = servletContextEvent.getServletContext();
String name = servletContext.getInitParameter("name");
Student student = new Student(name);
synchronized(servletContext){
servletContext.setAttribute("student",student);
}
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
会话属性是线程安全的吗?
遗憾的是,会话属性任然不是线程安全的。ヾ(≧へ≦)〃
会话是什么呢?具体的会在会话章节进行介绍,这里就不花笔墨说了
还有啥是线程安全的呢
只有请求属性和局部变量是线程安全的!
为了保证线程安全,我们常常采用这2种办法
- 把变量声明为服务方法中的局部变量,而不是一个实例变量
这是什么意思呢?其实非常的简单。我们先来看看局部变量的写法吧(●ˇ∀ˇ●)
public class servletStudy extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Student student = (Student)getServletContext().getAttribute("student");
req.setAttribute("name",student.getName());
req.getRequestDispatcher("student.jsp").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}
可以看到我们只是在doGet()方法中new了一个学生,下面我们看看instance varivables实例变量的写法是什么吧
public class servletStudy extends HttpServlet {
Student student;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
Student student = (Student)getServletContext().getAttribute("student");
req.setAttribute("name",student.getName());
req.getRequestDispatcher("student.jsp").forward(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
对比一下,其实很容易就能看出为什么这样是线程不安全的了。经过这么多的学习,你当然知道每个servlet只有一个实例,但他可以有很多线程 。
那么问题来了,当这个servlet每接到一个请求就会启动一个线程处理,而这每个线程都在访问这个servlet的instance varivables 。
很多个线程访问同一个资源你突然警觉起来👮♂️这不就是线程不安全的本质吗?原来如此,难怪我们要用局部变量的方式呢。这下你茅塞顿开💡
- 在最合适的作用域使用Attribute
这又是什么意思呢?在3个属性中只有请求属性是线程安全的,那么我们能够用请求属性解决的事情就绝对不要去使用其他属性。这也是避免线程不安全的一种方法。