• 异常排查 | 重复Cookie访问导致HTTP请求引发空指针异常


    在这里插入图片描述

    近几日,遇到一个困惑了我很久的异常,是浏览器页面向Tomcat服务器发起HTTP请求时,服务器发还回来的一处异常

    java.lang.NullPointerException: Cannot invoke "java.lang.Integer.intValue()" because the return value of "javax.servlet.ServletContext.getAttribute(String)" is null
    
    • 1

    一、场景描述

    首先来说一下我是在做什么的过程中遇到这个问题

    • 现在我需要实现一个监听器,去监听在线用户人数,也去实时记录一下当前这个页面中有多少用户在线,这一块的话需要使用到Servlet三大组件之一的【监听器】,我使用的是HttpSession这个接口,然后通过重写内部的sessionCreated()sessionDestroyed()方法去判定当前页面的用户接入与离开
    • 当我开启Tomcat服务器,然后打开三个浏览器去进行测试的时候,是没有问题的,只要访问一次在线人数就 + 1

    在这里插入图片描述

    不过呢,当然在重新启动Smart Tomcat的时候,就出现了下面的情况,请看【问题描述】~

    二、异常说明

    • 首先我们来看看这个异常,很显眼的几个大字可以看出HTTP状态500 - 内部服务器错误,这是HTTP的一个状态码,服务器发还回这个状态的话表示是【我们在后端的代码出问题了,异常实在代码中抛出的】

    在这里插入图片描述

    • 但是光看状态码可不行,我们还要精准地定位到是哪个地方出了问题,仔细观察可以发现,我在后端代码中使用的getAttrubute(String)获取到的内容是空的
    • 而且在下面的【例外情况】中可以看到NullPointerException空指针异常的问题,这个问题其实还挺大的,如果有看过我 指针入门到进阶教程就可以知道对于空指针来说其实是很危险的,其造成的后果堪比【野指针】
    • 不仅如此,我们还可以从服务器发还回来的响应中看到是在哪个类中哪个方法的哪一行出现了问题,所以我们就可以根据这个去进行精准定位了✌

    三、查找问题

    知道问题在哪了,接下去就让我们到IDEA中的查找一下后端的代码吧【文末会给出

    • 首先我们就来到这个类的方法中,然后找到指定的那一行,这个类的话是用来获取当前网页在线人数并构造HTML页面显示给用户看的,但是可以看到在这个20行的地方就已经触发了【空指针异常】,那自然下面的代码就不会执行了,此时我们应该去看看这句代码出了什么问题

    在这里插入图片描述

    • 对于上面这句代码呢,就是在获取我下方这个监听器类中所统计的当前页面人数,代码逻辑我这里就不讲了,这里是在排查异常,不过Servlet的代码没有Spring Boot那么多的注解,还是好理解的
    • 那若是我们获取到的这个人数为null的话也就是表示根本没有去进行一个统计,即这个【online】压根就没有更新,那监听器这里为什么没有监听到呢?我一开始也非常纳闷这一块╮(╯▽╰)╭

    在这里插入图片描述

    四、调试排查

    于是这个时候我只能通过调试去进行观察了

    • 首先在ServletContext这个上下文对象获取online的地方打一个断点,然后在Chrome浏览器页面前端进行用户登录点击

    在这里插入图片描述

    • 点击下一步后,因为当前所获取的这个online还是为【null】,所以就代表当前这个用户是第一次接入的,此时会将这个online的值设为1

    在这里插入图片描述

    • 然后在当前访问结束后,服务器便发回了这么一个页面,告知当前页面的访问者有1个

    在这里插入图片描述

    • 接下去呢,我就通过IE浏览器切进行访问,一样得通过DeBug进去,可以看到此时的obj已经不为null了,所以不会进入第一个分支,而是会进入第二个if分支

    在这里插入图片描述

    • 在执行完成后,我们可以在【context】中的attributes中的去找到这个online观察,就可以看到它的值为2,这也就是意味着当前这个网页的访问者有2位了

    在这里插入图片描述

    • 然后呢,我再通过Edge浏览器去进行访问,此时可以看到获取到的online值为2,表明此时已经有2个人在线了,那么当这个用户再接入的时候,online的值就会变为3

    在这里插入图片描述

    • 然后继续下去就可以看到这个online确实变为3了,也就表示此时有3个人正在访问这个页面

    在这里插入图片描述
    在这里插入图片描述

    五、思考分析

    但此时当我将Tomcat去进行一个重启后,再进行访问的话,就会造成下面这样的错误

    在这里插入图片描述

    • 于是我就通过这个【服务器重启】来推断到底是出了什么问题,以下是我的思考:

    🤔因为服务器这边重启了,所以之前保存的用户信息都不见了,但是呢这个时候客户端却不知道,还是以它之前的那个状态在进行访问,因为服务器之前在第一次访问的时候已经给了每个连接进来的用户创建了一个会话,并且生成了一个SessionId,分别代表了它们各自的身份,而每个SessionId又是存放在浏览器Cookie中的,因为前后端这里的逻辑就对接不上了,服务器完全不认识这个人,所以便返回给它一个【内部服务器错误】👈

    所以经过我上面的一番思考,便想到了可能和浏览器这边的Cookie有点关系,于是我就去翻找了每个浏览器的Cookie

    • 于是,我就发现了这个网页所在的Cookie,似乎就明白了一些东西😮因为当前这个浏览器的这个页面已经拥有了一份服务器所生成的SessionId,就保存在当前的这份【Cookie】中,于是当服务器重启后再度去进行访问的时候,那Tomcat这只猫🐱便说:“你谁呀,我不认识你呀”
      在这里插入图片描述那么问题其实就是出在这个Cookie上,那一定有同学会问:这该怎么解决呢?

    在这里插入图片描述

    六、解决方案

    • 我这里只想到了两种解决方法:
    1. 一个就是在当前网页中加一个【退出】按钮,然后让用户想要退出的时候就惦记这个按钮,此时不仅仅是宣告这名用户的退出,而且要将其在浏览器中所保存的Cookie也随之销毁,这个的话你可以通过前端三剑客中的JavaScript来进行完成,这里暂不做介绍,后续有机会更新
    2. 第二种办法就是我正在使用的,那就是在后端这里做一下判断,我们自己去拦截一下这个空指针异常,而不是让服务器将此错误发还给用户
    • 看到代码,我没有直接用count去接受强转后的在线人数,而是先通过一个【Object】父类去接收一下这个HttpSession对象,因为这个对象也是一个键值对的形式,是我们程序员自己构造的,因此其返回值类型就要是一个总的父类,可以接受任何种类的内容
    • 若是这个对象中的内容为空的话,此时表明触发了空指针异常,那么我们就要自己捕获一下,然后告知用户他此前登录过,需要手动清除一下浏览器缓存才可以继续登录(当然这是模拟登录,现实中不可能这样)
    Object obj = req.getServletContext().getAttribute("online");
    if (obj == null){
        System.out.println("当前用户此前登录过");
        resp.setContentType("text/html;charset=utf-8");
        resp.getWriter().write("

    当前用户此前登录过,请清除浏览器缓存后再行登录

    "
    ); } int count = (int)obj; resp.setContentType("text/html;charset=utf-8"); resp.getWriter().write("在线人数为:" + count);
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 可以看到若是我此时再去重启一下服务器,用一个已经接进来过的用户再次进入的话,就会构造出如下这样一个页面去提示用户先清除一下当前浏览器的缓存,然后再登录

    在这里插入图片描述

    但是,浏览器缓存要怎么清理呢?

    温馨提示:每个浏览器都是不一样的,读者可自己试着摸索

    在这里插入图片描述


    然后展示一下整体代码,可供测试

    前端登录页面

    DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Documenttitle>
    head>
    <body>
        <form action="onlineUsers", method="post">
            <label>用户名label>
            <input type="text" name="userName">
            <input type="submit" value="登录">
        form>
    body>
    html>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    监听器实现类

    import javax.servlet.ServletContext;
    import javax.servlet.ServletRequestEvent;
    import javax.servlet.ServletRequestListener;
    import javax.servlet.http.HttpSession;
    import javax.servlet.http.HttpSessionEvent;
    import javax.servlet.http.HttpSessionListener;
    
    public class UsersCount implements HttpSessionListener{
        @Override
        public void sessionCreated(HttpSessionEvent event) {
            System.out.println("会话创建");
            // 1.获取ServletContext对象,在整个容器中只有一份
            ServletContext servletContext = event.getSession().getServletContext();
            System.out.println(event.getSession().getId());
    
            // 2.在线人数
            Object obj = servletContext.getAttribute("online");
            // 3.判断当前用户是否是第一个在线的
            if(obj == null){
                servletContext.setAttribute("online", 1);
            }else{
                int i = (int)obj;             // 若其不是第一个上线的,就 + 1
                servletContext.setAttribute("online", i + 1);
            }
        }
    
        @Override
        public void sessionDestroyed(HttpSessionEvent event) {
            System.out.println("会话销毁");
            event.getSession().invalidate();
            // 1.获取ServletContext对象,在整个容器中只有一份
            ServletContext servletContext = event.getSession().getServletContext();
            // 2.在线人数
            Object obj = servletContext.getAttribute("online");
    
            if(obj == null){
                servletContext.setAttribute("online", 0);
            }else{
                int i = (int)obj;             // 若其不是第一个上线的,就 + 1
                servletContext.setAttribute("online", i - 1);
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    登录验证的Servlet类

    import javax.servlet.ServletException;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.io.IOException;
    
    @WebServlet("/onlineUsers")
    public class loginServlet extends HttpServlet {
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            String userName = req.getParameter("userName");
            if(!userName.equals("zhangsan")){
                resp.sendRedirect("login.html");
            }
    
            HttpSession httpSession = req.getSession(true);
            httpSession.setAttribute("userName", userName);
            resp.sendRedirect("show");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    展示结果处理是Servlet类

    @WebServlet("/show")
    public class showServlet extends HttpServlet {
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            HttpSession httpSession = req.getSession(false);
            if(httpSession == null){
                System.out.println("未登录,请重新登录");
                resp.sendRedirect("login.html");
                return;
            }
    
            Object obj = req.getServletContext().getAttribute("online");
            if (obj == null){
                System.out.println("当前用户此前登录过");
                resp.setContentType("text/html;charset=utf-8");
                resp.getWriter().write("

    当前用户此前登录过,请清除浏览器缓存后再行登录

    "
    ); } int count = (int)obj; resp.setContentType("text/html;charset=utf-8"); resp.getWriter().write("在线人数为:" + count); } }
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    七、写在最后

    以上就是本文所排查的问题,你学会了吗?我们来回顾一下

    • 对于这种服务器返还的HTTP响应状态码,向来是很多同学都很苦恼的东西,大家都喜欢看到自己程序正常的样子,但是却不希望看到自己的程序出问题,出一堆Bug,然后面对满屏的报错两眼一瞪👀手足无措
    • 这其实对于学习编程来说是非常不好的,跟着我这么一步步地调试、排查、分析下来,相信你一定有了自己的理解,下次在面对遇到的问题时就不会毫无办法,也可以学着自己去调试分析,找出问题所在🔍
    • 虽然这很消耗时间,而且又要动脑,但是总比你用眼睛盯着代码看一个小时毫无动作要来得好吧,所以行动起来吧,同学们!

    2023年5月25日晚8点记

    在这里插入图片描述

  • 相关阅读:
    解决kafka.errors.NodeNotReadyError: NodeNotReadyError
    【iOS ARKit】存储与共享
    My Eighty-sixth Page - 买股票的最佳时机Ⅲ - By Nicolas
    postgresql源码学习(40)—— 崩溃恢复② - 恢复起点
    Nginx介绍,nginx高级应用,nginx虚拟主机配置
    【Centos8】Centos8+使用MegaCLI查看硬件RAID情况
    77. 组合、216. 组合总和 III、17. 电话号码的字母组合
    使用minio进行文件存储
    springMVC学习笔记-请求映射,参数绑定,响应,restful,响应状态码,springMVC拦截器
    mybatis
  • 原文地址:https://blog.csdn.net/Fire_Cloud_1/article/details/130756565