属性和监听

属性和监听

本篇对应《head first servlet and jsp》的第五章。该章介绍了servletConfigservletContext在不同场景下的应用,并探讨了线程安全问题和小小的解决方法。

servletConfig

servletConfigservletContext解决了初始化参数的硬编码问题。比如我们想输出一句话,硬编码中我们是这样写的

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才被创建哦😃😃😃
  • 容器调用servletinit()方法 ,传入一个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应用中部署的各个servletJSP都能访问该ServletContext

❗❗❗如果这个Web应用是分布的,那么每个JVM有一个属于自己的ServletContext并且 99.9 % 99.9\% 99.9%的情况下,ServletContext的内容是相同的

接着一个问题随之而来,很显然的可以看到,这些初始化参数都是String键值对 ,可想而知String能做的事情非常的有限💀💀💀如果我想要初始化的是一个Object呢?

不要着急,我们拥有这样的技术( *^-^)ρ(^0^* )

监听器

监听器Listener可以在特定事件发生时出现并帮助我们完成一些事情,有各种各样的Listener在这里我们只讲解ServletContextListener ,看名知意这个Listener就是专门监听👂ServletContext的,也就是说在ServletContext发生了什么的时候,这个Listener会完成一些事情。至于是什么时候呢?下面将会揭晓。

你可能想马上知道ServletContextListener到底干了什么,居然可以让一个String初始化参数StringObject ,没错,我看到这里也充满了好奇❓❓❓

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怎么化StringObject

当然在此之前,需要说一说我们需要准备什么?

假设我们需要初始化一个名为highway的学生,那么我们需要准备什么呢❓

  1. 当然是不能缺少的ServletContextListener ,在本例中该Listener名为StudentListener
  2. 需要准备一个Student实体类 ,在本例中该实体类就叫Student
  3. 可能会迟到但永远不会缺席的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的属性。属性AttributeString初始化参数init-param的区别让我们能够完成化StringObject的魔法🧙‍。

那么他们的区别在哪呢?会在下面介绍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>

或许单看代码,你还有点糊涂,那么下面用文字来描述一遍吧。当然我肯定会建议你边看文字描述边翻代码对应。

  1. 容器读部署文件,注册Listener ,为每个context-param标签创建一个String键值对
  2. 容器创建一个ServletContext的实例
  3. 容器为ServletContext提供String键值对的引用,在本例中Key是name,Value是highway。并将这些键值对的引用交给ServletContext
  4. 容器创建一个实现ServletContextListener接口的StudentListener
  5. 容器调用ListenercontextInitialized()方法 ,传入新的ServletContextEvent 。这个事件对象有一个ServletContext引用,所以事件处理代码可以从事件中得到上下文 ,并从上下文得到上下文初始化参数ListenerservletContextEventServletContext的引用
ServletContext servletContext = servletContextEvent.getServletContext();
  1. 然后从ServletContext的引用中获得上下文初始化参数name
String name = servletContext.getInitParameter("name");
  1. Listener使用这个String来构造一个Student对象
Student student = new Student(name);
  1. Listener将这个Student对象设置为ServletContext的一个Attribute
servletContext.setAttribute("student",student);
  1. 到此为止,这个ServletContext就完成了(>人<;)那么剩下的事情我想你已经非常熟悉了,容器建立一个新的servlet ,当然在容器调用init()方法时已经建立了一个ServletConfig ,并且这个ServletConfig 里有个ServletContext的引用
  2. servletStudy得到一个请求,向ServletContext请求属性"student"
Student student = (Student)getServletContext().getAttribute("student");

一定要强制类型转换(>人<;)! 一定要强制类型转换(>人<;)! 一定要强制类型转换(>人<;)!

  1. 获得studentname ,向HttpServletRequest设置属性,然后getRequestDispatcher转发
req.setAttribute("name",student.getName());
req.getRequestDispatcher("student.jsp").forward(req, resp);

到此为止结束(●ˇ∀ˇ●)我相信你应该明白了👌👌👌如果不明白,那就重复看几遍吧(≧﹏ ≦)

Attribute是什么?

在上面的例子中,你可能早就已经迫不及待的想知道Attribute是什么了吧?

说起来其实非常简单😝😝😝

Attribute是一个对象,Attribute可以设置到3个servlet API对象中,分别是ServletContextHttpServletRequestHttpSession前2种我们在上面的代码已经使用过了,还剩下最后一种,那么什么是session呢?不要着急,我们**先把Attribute是什么?**的事情说完🤣

你可以简单的理解为是一个映射实例对象种的键值对 ,不同的是Key为一个StringValue则是一个Object 。在实际中,我们并不❌知道也不❌关心具体实现,我们只关心🧡属性所在的作用域。

属性参数
类型应用/上下文
请求
会话
应用/上下文初始化参数
请求参数
servlet初始化参数
设置方法setAttribute(String name, Object value)不能设置应用和servlet初始化参数
对于请求参数可以,但这是另外一回事了
返回类型ObjectString
获取方法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应该加给谁了吧?很显然是要对这个Attributesynchronized👏

那么怎么加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每接到一个请求就会启动一个线程处理,而这每个线程都在访问这个servletinstance varivables

很多个线程访问同一个资源你突然警觉起来👮‍♂️这不就是线程不安全的本质吗?原来如此,难怪我们要用局部变量的方式呢。这下你茅塞顿开💡

  • 在最合适的作用域使用Attribute
    这又是什么意思呢?在3个属性中只有请求属性是线程安全的,那么我们能够用请求属性解决的事情就绝对不要去使用其他属性。这也是避免线程不安全的一种方法。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue.js中的计算属性监听属性是用来响应数据变化的两种方式。 计算属性是一种定义在Vue实例中的属性,它的值是根据其他属性计算得出的。当计算属性依赖的属性发生变化时,计算属性会自动重新计算并更新其值。计算属性可以通过在Vue实例的选项中定义,也可以使用实例方法`xxx.$watch()`来监听属性的变化。[1]例如,在上述代码中,定义了一个计算属性`newstr`,它的值与`str`属性的值相同,当`str`属性发生变化时,`newstr`属性的值也会自动更新。 监听属性是一种用来监听和响应Vue实例中的数据变化的方式。当监听属性发生变化时,可以执行特定的操作。监听属性可以通过在Vue实例的选项中定义,也可以使用实例方法`xxx.$watch()`来监听属性的变化。[2]例如,在上述代码中,定义了一个监听属性`name`,当`name`属性发生变化时,会弹出一个提示框显示原值和新值。 计算属性监听属性的效果是一样的,都可以用来响应数据变化。但是它们的实现方式有所不同。计算属性基于它的依赖缓存,只有相关依赖发生变化才会重新计算值,而监听属性则需要手动定义监听函数来执行特定的操作。[3]另外,计算属性在重新渲染时只会调用一次,而使用方法则在每次重新渲染时都会调用执行。 总结起来,计算属性适用于需要根据其他属性计算得出值的场景,而监听属性适用于需要在属性变化时执行特定操作的场景。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值