Java EE 博客系统(Servlet版)

文章目录

  • 1. 基本情况
  • 2. 准备工作
  • 3. 博客列表页
  • 4. 博客详情页
  • 5. 实现登录
  • 6. 强制要求登录
  • 7. 显示用户信息
  • 8. 退出登录
  • 9. 发布博客
  • 10. 如果程序出现问题怎么办?

1. 基本情况

这里的博客系统主要是四个界面

  1. 博客列表页
    显示出当前网站上都有哪些博客
  2. 博客详情页
    点击列表上的某个博客,就能进入对应详情页(显示出博客的具体内容)
  3. 博客编辑页
    让用户输入博客内容,并且发送到服务器
  4. 登录页

这里主要来写后端的代码,前端代码已经准备就绪,直接导入即可


任务:
基于上述的页面,编写服务器/前后端交互代码

通过这些代码,完成博客系统,完整的功能

  1. 实现博客列表页
    让页面从服务器拿到博客数据(数据库)
  2. 实现博客详情页
    点击博客详情的时候,可以从服务器拿到博客的完整数据
  3. 实现登录功能
  4. 实现强制要求登录
    (当前处于未登录的情况下,其他的界面,博客列表、博客详情、博客编辑…就会强制跳转到登录页)
    要求用户登录后才能使用
  5. 实现显示用户信息
    从服务器获取到
    博客列表页,拿到的是当前登录的用户的信息
    博客详情页,拿到的是文章作者的信息
  6. 实现退出登录
  7. 发布博客
    博客编辑页,输入文章标题和内容之后,点击发布,就能把这个数据上传到 服务器上并保存

在这些功能搞定之后,一个功能相对完整的博客网站,就初具规模了

2. 准备工作

  1. 创建项目,引入依赖,把当前的前端界面引入到项目中

  1. 数据库设计

设计好对应的表结构,并且把数据库相关代码,也进行封装

  • 找到实体
    博客(blog 表)
    用户(user 表)
  • 确认实体之间的关系
    一对多
    一个博客,只属于一个用户
    一个用户,可以发布多个博客
    这样就应该在博客列表中,引入一个 userId 这样的属性

blog (blogId, title, content, postTime, userId)
user (userId, username, password)
在这里插入图片描述
在这里插入图片描述

接下来,把数据库的代码进行一些封装
在这里插入图片描述

在进行网站开发的工程中,一种常见的代码组织结构,MVC
M model:操作数据的代码
V view:操作/构造界面的代码
C controler:业务逻辑,处理前端请求

由于这套组织结构比较古老,在现在写的过程中,也不会完全遵守

在这里插入图片描述
DBUtil 这个类,封装数据建立连接的操作

在这里插入图片描述
当前这个懒汉模式是不安全的
当前 servlet 本身就是在多线程环境下执行的
tomcat 收到多个请求的时候,就会使用 多线程 的方式,执行不同的 servlet 代码
这里就可能有现成不安全的问题

因此要加锁
在这里插入图片描述

package model;

import com.mysql.jdbc.Connection;
import com.mysql.jdbc.jdbc2.optional.MysqlDataSource;

import javax.sql.DataSource;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

// 通过这个类, 封装数据库建立连接的操作.
// 由于接下来代码中, 有多个 Servlet 都需要使用数据库. 就需要有一个单独的地方来把 DataSource 这里的操作进行封装.
// 而不能只是放到某个 Servlet 的 init 中了.
// 此处可以使用 单例模式 来表示 dataSource
public class DBUtil {
    private volatile static DataSource dataSource = null;

    private static DataSource getDataSource() {
        if (dataSource == null) {
            synchronized (DBUtil.class) {
                if (dataSource == null) {
                    dataSource = new MysqlDataSource();
                    ((MysqlDataSource) dataSource).setURL("jdbc:mysql://127.0.0.1:3306/servlet_blog_system?characterEncoding=utf8&useSSL=false");
                    ((MysqlDataSource) dataSource).setUser("root");
                    ((MysqlDataSource) dataSource).setPassword("123456");
                }
            }
        }
        return dataSource;
    }

    public static Connection getConnection() throws SQLException {
        return (Connection) getDataSource().getConnection();
    }

    public static void close(Connection connection, PreparedStatement statement, ResultSet resultSet) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

DBUtil 完成对于数据库建立连接和关闭连接的实现


  1. 创建实体类

大部分表,都需要搞一个专门的类来表示
表里的一条数据,就会对应到这个类的一个对象

这样就可以把数据库中的数据和代码联系起来了


  1. 针对博客表和用户表操作

这里再创建两个类,来完成准对博客表和用户表的增删改查操作

这两个类,叫做BlogDao 和 UserDao

DAO(Data Access Object):数据访问对象
通过这两个类的对象,来完成针对数据库表的操作

写一个复杂一些的代码,往往需要先理清楚思路
相比于细节来说,理清思路是更复杂的
(为了实现这个代码,要写哪些类,有哪些方法)
在这里插入图片描述
在这里插入图片描述

package model;

import com.mysql.jdbc.Connection;
import com.mysql.jdbc.JDBC4PreparedStatement;

import javax.xml.stream.events.DTD;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

// 通过 BlogDao 来完成针对 blog 表的操作
public class BlogDao {
    // 1. 新增操作 (提交博客就会用到)
    public void insert(Blog blog) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            //1. 建立连接
            connection = DBUtil.getConnection();
            //2. 构造 SQL
            String sql = "insert into blog values (null, ?, ?, now(), ?)";
            statement = connection.prepareStatement(sql);
            statement.setString(1, blog.getTitle());
            statement.setString(2, blog.getContent());
            statement.setInt(3, blog.getUserId());
            //3. 执行 SQL
            statement.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            DBUtil.close(connection, statement, null);
        }
    }

    // 2. 查询博客列表 (博客列表页)
    //    把数据库里所有的博客都拿到.
    public List<Blog> getBlogs() {
        List<Blog> blogList = new ArrayList<>();
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "select * from blog order by postTime desc";
            statement = connection.prepareStatement(sql);
            resultSet = statement.executeQuery();
            while (resultSet.next()) {
                Blog blog = new Blog();
                blog.setBlogId(resultSet.getInt("blogId"));
                blog.setTitle(resultSet.getString("title"));
                // 此处读到的正文是整个文章内容. 太多了. 博客列表页, 只希望显示一小部分. (摘要)
                // 此处需要对 content 做一个简单截断. 这个截断长度 100 这是拍脑门出来的. 具体截取多少个字好看, 大家都可以灵活调整.
                String content = resultSet.getString("content");
                if (content.length() > 100) {
                    content = content.substring(0, 100) + "...";
                }
                blog.setContent(content);
                blog.setPostTime(resultSet.getTimestamp("postTime"));
                blog.setUserId(resultSet.getInt("userId"));
                blogList.add(blog);
            }

        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return blogList;
    }

    // 3. 根据博客 id 查询指定的博客
    public Blog getBlog(int blogId) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "select * from blog where blogId = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, blogId);
            resultSet = statement.executeQuery();
            // 由于此处是拿着 blogId 进行查询. blogId 作为主键, 是唯一的.
            // 查询结果非 0 即 1 , 不需要使用 while 来进行遍历
            if (resultSet.next()) {
                Blog blog = new Blog();
                blog.setBlogId(resultSet.getInt("bligId"));
                blog.setTitle(resultSet.getString("title"));
                // 这个方法是期望在获取博客详情页的时候, 调用. 不需要进行截断, 应该要展示完整的数据内容
                blog.setContent(resultSet.getString("content"));
                blog.setPostTime(resultSet.getTimestamp("postTime"));
                blog.setUserId(resultSet.getInt("userId"));
                return blog;
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection, statement, resultSet);
        }
        return null;
    }

    // 4. 根据博客 id, 删除博客
    public void delete(int blogId) {
        Connection connection = null;
        PreparedStatement statement = null;
        try {
            connection = DBUtil.getConnection();
            String sql = "delete from blog where blogId = ?";
            statement = connection.prepareStatement(sql);
            statement.setInt(1, blogId);
            statement.executeUpdate();
        } catch (SQLException e) {
            e.printStackTrace();
        }finally {
            DBUtil.close(connection, statement, null);
        }
    }
}

这里的 JDBC 代码,大同小异
因此,后面就会有一些数据库的框架
(MyBatis,MyBatisPlus,JPA…)
这里封装了 JDBC 的代码
这些框架 本质上 就是帮我们自动生成 JDBC 的代码

3. 博客列表页

在博客列表页加载的时候,通过 ajax 给服务器发起请求
从服务器(数据库)拿到博客列表数据,并求显示到页面上


  1. 约定前后端交互接口
    请求
    GET /blog
    响应
    HTTP/1.1 200 OK
    Content-Type: applicantion/json
    在这里插入图片描述

  1. 让浏览器给服务器发起请求
    在这里插入图片描述

  1. 服务器处理上述请求,返回响应数据(查询数据库)
    在这里插入图片描述由于是 list,Jackson 就会把结果转成 数组
    每个元素又是一个 Blog 对象

  1. 让前端代码,处理上述数据
    构造成 html 片段,显示在页面上
    形如:
    在这里插入图片描述

这里构造页面的过程,还是之前的 api
(1)querySelector:获取页面已有的元素
(2)createElement:创建新的元素
(3).innerHtml:设置元素里的内容
(4).className:设置元素的 class 属性
(5)appendChild:把这个元素添加到另一个元素的末尾

html 中
显示 >:
需要使用转义字符 &gt;
显示 <:
也需要使用转义字符 &lt;
在这里插入图片描述

a 标签在 html 中称为“超链接”
点击之后能够跳转到新的页面
在这里插入图片描述

这个代码,和前面的 jdbc 的感觉类似
在这里插入图片描述

对于前端页面来说
生成页面的方式其实有很多种
此处使用的比较朴素的方式(基于 dom api 的方式)
dom api 就是属于是浏览器提供的标准的 api(不属于任何的第三方框架和库)

定位就类似于 jdbc api

前端也有一些框架和库,是把 dom api 又进行了封装,用起来更简单一些


这里有一个问题,就是返回的时间,是时间戳
在这里插入图片描述

在这里,Jackson 在进行主要的工作
(1)Jackson 发现 blogs 是一个 list,于是就会循环遍历里面的每个元素
(2)针对每个元素(Blog 对象),通过反射的方式,获取到都是哪些属性,属性的名字,属性的值
在获取属性值的时候,就是通过调取 get 方法

在这里插入图片描述
这个时候,我们就要对代码进行改变
修改 getPostTime 方法,让其直接返回一个“格式化时间”
在这里插入图片描述
制定了一个格式化字符串
描述了当前时间日期具体的格式
各种语言,表示格式化时间都有这样的字符串
但是不同语言,表示的含义是不同的

在这里插入图片描述


此时还有一个问题,就是希望新加入的博客在上面,以前写的博客在下面

这个时候,该如何做呢?
返回 list,把 list 先逆序一下?

此处的结果顺序,是从数据库里查询出来的
一个 sql 如果不加 order by,结果的顺序是不可预期的

此处科学的做法,应该是加上 order by,时间逆序
在这里插入图片描述

4. 博客详情页

点击查看全文,就可以跳转到 带有不同 blogId 的 query string
后续在博客详情页中,就可以给服务器发起 ajzx 请求,根据这里的 blogId ,查询数据库中,博客的具体内容再返回
前端还是把得到的数据给构造到页面上
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述


  1. 约定前后端交互接口
    请求
    GET /blog?blogId=1
    请求
    HTTP/1.1 200 OK
    Content-Type: applivation/json
    在这里插入图片描述

这个请求,是希望在博客详情页的时候,通过 ajax 发给服务器


  1. 让前端代码,通过 ajax 发起请求

此处有个问题,发起 ajax 请求的时候要带有 blogId
blogId 当前处于 博客详情页 url 中
在这里插入图片描述
这里我们可以通过 location.search 方式拿到 页面 url 中的 query string
在这里插入图片描述


  1. 让服务器处理这个请求

这里依然使用 servlet 处理,一个路径对应到一个 servle

当前是使用一个 servlet 处理两种请求
博客列表页,不带 query string
博客详情页,带有 query string
就可以根据 query string 是否存在的情况,来区分是哪种请求
分别返回不同的数据即可

使用两个 servlet 处理这里的两个请求,也可以
就约定成不同的路径即可
使用一个 servlet 也可以,这里没有一个明确的标准

在这里插入图片描述


  1. 前端拿到响应之后,把响应数据,构成页面的 html 片段
    在这里插入图片描述
    在这里插入图片描述
    由于显影中,带有 Content-Type:application/json
    jquery 自动帮我们把字符串转成 js 对象了
    直接通过 . 的方式就能访问属性了
    在这里插入图片描述
    在博客列表页,需要循环遍历,构造的页面内容页更复杂
    此处就更简单一些,只需要设置三个内容即可

在这里插入图片描述
写完代码之后,再点击某个博客,就可以看到,有的博客里面的详情页,还是之前的旧的内容

这个问题是浏览器缓存引起的

浏览器加载页面的时候,是通过网络获取的(网络速度比较慢)
浏览器有时候就会把已经加载过的界面,在本地硬盘保存一份,后续再访问同一个界面
就不需要通过网络加载,直接加载本地硬盘的这一份

默认认为 html 出现修改的概率比较低
但是也不是完全不会修改

如何克服缓存的干扰,前端有专业的解决方案
我们在这里可以直接使用 ctrl + F5,强制刷新界面


当前博客详情页,虽然能用出博客的正文,但是显示的是正文的 md 原始数据
作为博客网站,正确的做法应该是显示出 md 渲染后的效果

此处仍然 通过 第三方库 (editor.md)
在这里插入图片描述

这个是 editormd 这个库给的一个全局变量
把依赖正确引入了,这个变量就能直接使用

这个方法的效果,就是把 blog.content 这里的 md 的原始数据,渲染成 html,放到 id 为 content 的 div 中
在这里插入图片描述
一个 html 标签,可以有很多的属性
class 属性,往往是用来和 css 样式配合的
id 属性,则是一个“身份标识”要求一个界面中,id 必须是唯一的
在这里插入图片描述

5. 实现登录

在这里插入图片描述

在登录界面,在输入框中填写 用户名和密码
点击登录,就会给服务器发起 http 请求(这里使用 form)

服务器处理登录请求,读取用户名密码,在数据库查询、匹配
如果正确,就登录成功,创建会话,跳转到博客列表页

由于这里,登录成功,直接进行重定向跳转,就不要浏览器额外写代码处理,直接浏览器自动跳转


  1. 约定前后段交换接口
    请求
    POST /login
    Content-Type: application/x-www-form-urlencoded
    username=zhangsan&password-123
    在这里插入图片描述

form 表单,提交成功,可以直接使用 302 重定义跳转
如果使用 ajax,ajax 处理响应就需要写代码来完成跳转(不是浏览器自动完成了)


  1. 让前端发起请求
    form

username=zhangsan&password-123
这里的 input 标签,name 属性就是这里 body 中的 key

什么时候一个元素要有 id,什么时候没有呢?
看个人的需要,灵活处理

在这里插入图片描述


  1. 让服务器处理请求,并返回响应
    在这里插入图片描述

在这里插入图片描述

package servlet;

import model.User;
import model.UserDao;

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;
import java.time.temporal.Temporal;

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 读取参数中的用户名和密码
        req.setCharacterEncoding("utf8");
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        //    验证一下参数, 看下是否合理.
        if (username == null || username.length() == 0 || password == null || password.length() == 0) {
            resp.setContentType("text/html; charset=utf8");
            resp.getWriter().write("您输入的用户名或者密码为空!");
            return;
        }
        // 2. 查询数据库, 看看这里的用户名密码是否正确.
        UserDao userDao = new UserDao();
        User user = userDao.getUserByName(username);
        if (user == null) {
            // 用户名不存在!
            resp.setContentType("text/html; charset=utf8");
            resp.getWriter().write("您输入的用户名或密码不正确!");
            return;
        }
        if (!password.equals(user.getPassword())) {
            // 密码不正确!
            resp.setContentType("text/html; charset=utf8");
            resp.getWriter().write("您输入的用户名或密码不正确!");
            return;
        }
        // 3. 创建会话
        HttpSession session = req.getSession(true);
        session.setAttribute("user", user);
        // 4. 跳转到主页了.
        resp.sendRedirect("blog_list.html");
    }
}

6. 强制要求登录

在博客列表页、详情页、编辑页,判断当前用户是否已经登录
如果未登录,则强制跳转到登录页(要求用户必须登录后才能使用)

在上述的页面中,在页面加载中,给服务器发起 ajax
从服务器获取一下当前的登录状态


  1. 约定前后端交互接口

GET /login

登录成功:HTTP/1.1 200
登录失败:HTTP/1.1 403

也可以通过其他的方式,比如都是返回 200,但是在 body 中给不同的结果


  1. 让前端代码发起这个请求,并响应
    在这里插入图片描述

一个页面,触发的 ajax是可以有多个的
一个页面通常都会触发多个 ajax
这些 ajax 之间是“并发执行”这样的效果

js 中是没有“多线程”这样的机制
而 ajax 是一种特殊的情况,能够起到类似于“多线程”的效果

当页面中发起两个或者多个 ajax 的时候,这些 ajax 请求就相当于并发的发送出去
彼此之间不会相互干预
(不是 串行 执行,不是执行完一个 ajax,得到响应之后,再执行下一个)

同时发出去多个请求,谁的响应先回来了,就先执行谁的回调函数


  1. 让服务器处理上述请求

在这里插入图片描述
当前虽然等登录服了,一旦重启服务器,仍然会被判定为未登录状态

登录状态是通过服务器这里的 session 来存储的
session 这是服务器内存中的类似于 hashmap 这样的结构
一旦服务器重启了,hashmap 里面原有的内容就没了

但是这种设定,并不科学,相比支架,我们还有更好的解决方案

  1. 我们把会话进行持久化保存(文件,数据库,redis…)
  2. 使用令牌的方式保存(把用户信息,在服务器里面,还是保存在浏览器这里,相当于服务器没有在内存中存贮内存中的用户的身份)

这里我们需要让多个界面都有这样的机制,这里就可以把一些公共的代码,单独拿出来
放到某个 .js 文件中
通过 html 中的 script 标签,来引用这样的文件内容
此时,就可以在 html 中调用对应的公共代码了
在这里插入图片描述
在这里插入图片描述

7. 显示用户信息

博客列表页:显示的是当前登录的用户的信息

博客详情页:显示的是当前文章的作者信息

在页面加载的时候,给服务器发起 ajax 请求
在服务器返回对应的用户数据
根据发起请求不同的界面,服务器返回不同的信息即可


  1. 约定前后端交换接口

博客列表页,获取当前登录的用户信息
请求:
GET /userInfo
响应:
HTTP/1.1 200 OK
application/json
在这里插入图片描述
博客详情页,获取当前文章的作者信息
请求:
GET /authorInfo?blogId=1
响应:
HTTP/1.1 200 OK
application/json
在这里插入图片描述


  1. 先让前端代码,发起这样的请求

博客列表页:
在这里插入图片描述
博客详情页:
在这里插入图片描述


  1. 编写服务器代码,来处理上述请求

博客列表页:
在这里插入图片描述
博客详情页:
在这里插入图片描述
这里是通过两步 sql 分别查询的
先查 blog 表里面的 blog 对象
再查 user 表

其实也可以一步 sql 搞定
比如:可以使用联合查询,把 blog user 进行笛卡尔积,找出匹配的结果
也可以使用子查询,把两个 sql 合并在一个完成

当然,一步完成是要付出代价的
联合查询来说,笛卡尔积,对于数据库是一个不小的开销
子查询来说,这样的 sql 可读性可能比较差


  1. 在前端代码中,处理响应
    把响应中的数据,给写到刚才页面的对应的位置上

在这里插入图片描述
在这里插入图片描述
以下这个请求从服务器拿到了当前用户的信息
进一步的就把用户的名字显示到页面上了
在这里插入图片描述
在这里插入图片描述

8. 退出登录

博客列表、博客详情、博客编辑 的导航栏中,都有一个“注销”按钮

在这里插入图片描述
注销 这个东西是 a 标签
可以有一个 href 属性
点击就会触发一个 http 请求
并且可能会引起浏览器跳转到另一个页面

让用户点击“注销”的时候,就能够触发一个 HTTP 请求(GET 请求)

服务器收到这个 GET 请求的时候,就会把会话里的 user 这个 Attribute 给删了
由于在判断用户是否是登录状态的逻辑中,需要同时验证,会话存在,且 这里的 user Attribute 也存在
只要破坏一个,就可以是登录状态发生改变了


为什么不直接删除 session 本身?
主要因为,sevlet 没有提供,删除 session 的方法
虽然有间接的方式(session 可以摄者国企时间,设置一个非常短的过期时间),也可以起到删除的效果,但是不太优雅

session 提供了 removeAttribute 这样的方法,可以把 user 这个 Attribute 给删了


  1. 约定前后端交互接口
    请求:
    GET /logout
    响应:
    直接重定向到登录页
    HTTP/1.1 302
    Location: login.html

  1. 编写前端代码,发送请求
    不用写 ajax,直接就给 a 标签设置 href 属性即可

在这里插入图片描述


  1. 编写后端代码,处理这个请求,完成退出登录的操作

在这里插入图片描述

9. 发布博客

当点击提交的时候,就需要构造 http 请求,把此时的页面中的标题和正文都传输到服务器这边
服务器把这个数据存入数据库即可

此处这里的 http 请求,可以使用 ajax,也可以使用 form
这种填写输入框,提交数据的场景,使用 form 会更方便


  1. 约定前后端交互接口

请求:
POST blog
Content-Type: x-www-form-urlencoded

title=这是标题&content=这是正文
(上面的中文 都是要 urlencode,form 表单直接就能完成这个操作)

响应:
HTTP/1.1 302
Location: blog_list.html


  1. 编写前端代码,构造请求

标题本身就是自己写的一个 input,给他加上 name 属性,很容易
但是博客正文,是由 editor md 构成的一个编辑器,这里如何添加 name 属性呢?

editor md 的开发者们,也考虑到了这种情况
在官方文档中也有这样的例子

在这里插入图片描述
这个 div 就是 editor.md 的编辑器的容器
在这个 div 里,搞一个隐藏的 textarea 标签(多行编辑框,把 name 属性加到 textarea 属性上)
并且在初始化 editormd 对象的时候,加上一个 对应的属性即可
在这里插入图片描述
name=“content”:是 form 中键值对的 key
“display: none;” :让这个 textarea 隐藏起来
在这里插入图片描述
这个代码是初始化 editormd 的编辑器的代码
在这里插入图片描述
在这里插入图片描述


  1. 编写服务器代码,处理刚才的请求
    在这里插入图片描述
    在这里插入图片描述

10. 如果程序出现问题怎么办?

这个时候,我们可以使用抓包来实现

抓包的目的,是为了先确定,在点击刷新这个过程中,浏览器和服务器之间有几次 http 交互
每一次交互,请求是什么样的,响应是什么样的

解下来就需要观察,抓包结果中,这几个 http 交互的请求是否都符合预期

  1. 先开请求发没有发
    如果你的浏览器都没发这个请求,说明前端代码有问题
    就需要检查你的前端带啊,ajax 是怎么写的

  2. 再看请求中各个部分是否正确,是否符合约定的接口要求
    如果请求不符合预期,说明还是前端代码有问题,检查 ajax 代码

  3. 如果请求没问题,需要再检查响应数据
    如果请求正常,相应数据不符合预期
    此时就下检查后端代码,是否你的后端代码
    没能正确的完成数据库查询操作等
    (尤其要注意服务器的控制套是否出现异常信息)

  4. 如果请求和响应都没有问题,说明服务器已经返回正确的数据了,但是页面没有把这些数据正确的显示出来
    此时还是要检查前端代码,尤其是检查前端处理响应的这里的逻辑
    (尤其要注意,浏览器控制台是否有报错)

当确定范围之后,进一步排查问题,还需要在代码中,加入更多的日志
System.out.println
console.log

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/306862.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

常孝元宇宙《神由都城》发布会成功召开

2024年1月9日,2024常孝元宇宙《神由都城》发布会在北京市中国科技会堂举办,由中国移动通信联合会元宇宙产业工作委员会主办,常州神由之星数字信息产业发展有限公司、常州孝道文化产业股份有限公司共同承办。 本次发布会以“创新引领、协同发展”为主题,邀请第十二届全国政协副主…

Linux截图方法推荐

因为经常会遇到以图为证的情况&#xff0c;而办公设备基本都是linux,所以汇总一下常见的linux截图方式。 1&#xff1a;在 Linux 中系统集成的截图的默认方式 你想要截取整个屏幕&#xff1f;屏幕中的某个区域&#xff1f;某个特定的窗口&#xff1f; 如果只需要获取一张屏幕…

Jenkins流水线怎么做?

问CHAT&#xff1a;Jenkins流水线怎么做&#xff1f; CHAT回复&#xff1a;Jenkins流水线是一种创建、测试和部署应用程序的方法。以下是为Jenkins创建流水线的步骤&#xff1a; 1. 安装Jenkins&#xff1a;首先你需要在你的服务器上安装Jenkins。这个过程可能会根据你的操作系…

UltraScale 和 UltraScale+ 生成已加密文件和已经过身份验证的文件

注释 &#xff1a;如需了解更多信息&#xff0c;请参阅《使用加密和身份验证确保 UltraScale/UltraScale FPGA 比特流的安全》 (XAPP1267)。 要生成加密比特流&#xff0c;请在 Vivado IDE 中打开已实现的设计。在主工具栏中&#xff0c;依次选择“Flow” → “Bitstream Setti…

强化学习求解TSP(一):Qlearning求解旅行商问题TSP(提供Python代码)

一、Qlearning简介 Q-learning是一种强化学习算法&#xff0c;用于解决基于奖励的决策问题。它是一种无模型的学习方法&#xff0c;通过与环境的交互来学习最优策略。Q-learning的核心思想是通过学习一个Q值函数来指导决策&#xff0c;该函数表示在给定状态下采取某个动作所获…

基于FPGA的万兆以太网学习(1)

万兆(10G) 以太网测速视频:FPGA 实现UDP万兆以太网的速度测试 1 代码结构 2 硬件需求 SFP+屏蔽笼可以插入千兆或万兆光模块。SFP+信号定义与 SFP 一致。 3 Xilinx IP 10 Gigabit Ethernet Subsystem IP说明 文章链接: Xilinx IP 10 Gigabit Ethernet Subsystem IP 4 E…

VM虚拟机的ip突然不见了——吐血解决分享,同秃然的

问题&#xff1a;再虚拟机上不管是输入 ip a,还是ifconfig&#xff0c;还是ip addr,还是root都是一个效果&#xff0c;只有主机的IP&#xff0c;虚拟机的IP不见了 网上好多方法是改ens33文件的&#xff0c;可是我的打开之后是空白的&#xff0c;根本就没有东西&#xff0c;和我…

layui组织机构树(treeSelect)

前端 <% page language"java" import"java.util.*" pageEncoding"UTF-8"%> <!doctype html> <html> <head><meta charset"utf-8"><title>智慧养老平台</title><%include file"../…

认识Linux指令之 “more less” 命令

01.more命令 语法&#xff1a;more [选项][文件] 功能&#xff1a;more命令&#xff0c;功能类似 cat 常用选项&#xff1a; -n 对输出的所有行编号 q 退出more cat适合打开查看一些小文件 当遇到大文本文件的时候&#xff0c;使用more命令&#xff0c;more可以打满一屏…

HarmonyOS@Prop装饰器:父子单向同步

Prop装饰器&#xff1a;父子单向同步 Prop装饰的变量可以和父组件建立单向的同步关系。Prop装饰的变量是可变的&#xff0c;但是变化不会同步回其父组件。 说明 从API version 9开始&#xff0c;该装饰器支持在ArkTS卡片中使用。 概述 Prop装饰的变量和父组件建立单向的同…

如何在没有密码的情况下将 iPhone 13/14/15 恢复出厂设置

您想知道如何在没有密码的情况下将 iPhone 13/14/15 恢复出厂设置吗&#xff1f; 出厂重置 iPhone 13/14/15 成为所有 iPhone 机型中最简单的。大多数情况下&#xff0c;iPhone 13/14/15 是在 iOS 15 或更高版本的 iOS 版本上&#xff0c;Apple 更新了无需密码重置 iPhone 13/…

【Python程序开发系列】一文总结API的基本概念、功能分类、认证方式、使用方法和开发流程

这是Python程序开发系列原创文章&#xff0c;我的第195篇原创文章。 一、什么是API&#xff1f; API是软件开发中非常重要的概念&#xff0c;它简化了不同组件之间的交互和集成&#xff0c;提供了对其他软件或服务功能的访问和调用方式。 API是应用程序编程接口&#xff08;Ap…

Elasticsearch:Search tutorial - 使用 Python 进行搜索 (四)

在本节中&#xff0c;你将了解另一种机器学习搜索方法&#xff0c;该方法利用 Elastic Learned Sparse EncodeR 模型或 ELSER&#xff0c;这是一种由 Elastic 训练来执行语义搜索的自然语言处理模型。这是继之前的文章 “Elasticsearch&#xff1a;Search tutorial - 使用 Pyth…

Redis 主从、哨兵和分片集群简单介绍

Redis 主从集群架构 单节点 redis 并发能力有上限&#xff0c;要进一步提高 redis 并发能力&#xff0c;就要搭建主从集群&#xff0c;实现读写分离 主从同步原理 Replicaition id&#xff1a;每台 master 机器都一个 repl_id&#xff0c;是数据集的表示&#xff0c;若 salv…

深入理解 Flink(一)Flink 架构设计原理

大数据分布式计算引擎设计实现剖析 MapReduce MapReduce 执行引擎解析 MapReduce 的组件设计实现图 Spark 执行引擎解析 Spark 相比于 RM 的真正优势的地方在哪里&#xff1a;&#xff08;Simple、Fast、Scalable、Unified&#xff09; DAG 引擎中间计算结果可以进行内存持…

【动态规划】【矩阵】C++算法329矩阵中的最长递增路径

作者推荐 【动态规划】C算法312 戳气球 题目 给定一个 m x n 整数矩阵 matrix &#xff0c;找出其中 最长递增路径 的长度。 对于每个单元格&#xff0c;你可以往上&#xff0c;下&#xff0c;左&#xff0c;右四个方向移动。 你 不能 在 对角线 方向上移动或移动到 边界外&…

[足式机器人]Part2 Dr. CAN学习笔记 - Ch02动态系统建模与分析

本文仅供学习使用 本文参考&#xff1a; B站&#xff1a;DR_CAN Dr. CAN学习笔记 - Ch02动态系统建模与分析 1. 课程介绍2. 电路系统建模、基尔霍夫定律3. 流体系统建模4. 拉普拉斯变换&#xff08;Laplace&#xff09;传递函数、微分方程4.1 Laplace Transform 拉式变换4.2 收…

使用SpirngBoot时部分编译报错解决方案:

1. 类文件具有错误的版本 61.0, 应为 52.0 请删除该文件或确保该文件位于正确的类路径子目录中。 报错截图&#xff1a; 解决方案&#xff1a; 找到springboot的java版本看是多少版本&#xff0c;springboot 3.0以上的版本需要最低JDK17的版本&#xff0c;所以查看你自己…

基于Spring-boot-websocket的聊天应用开发总结

目录 1.概述 1.1 Websocket 1.2 STOMP 1.3 源码 2.Springboot集成WS 2.1 添加依赖 2.2 ws配置 2.2.1 WebSocketMessageBrokerConfigurer 2.2.2 ChatController 2.2.3 ChatInRoomController 2.2.4 ChatToUserController 2.3 前端聊天配置 2.3.1 index.html和main.j…

电脑如何关闭自动更新?阻止系统自动更新方法

随着科技的发展&#xff0c;电脑已经成为我们生活中不可或缺的一部分。然而&#xff0c;有时候电脑的自动更新功能会给我们带来一些不必要的麻烦。因此&#xff0c;本文将介绍如何关闭电脑的自动更新功能&#xff0c;以便更好地管理电脑。 关闭自动更新功能的原因 电脑的自动…