简介:这是一份基于JDBC+JSP+Servlet构建的图书管理系统完整项目,面向正在完成Java Web课程设计或期末大作业的高校学生。项目采用经典的三层架构设计,业务逻辑涵盖图书信息管理、借阅归还、读者管理等核心模块,可直接作为毕业设计或课程设计的参考原型。压缩包共141个文件,约5.12MB,包含24个java源码文件、10个jsp页面、48个class编译文件,以及xml配置、properties配置、jar依赖库和数据库sql脚本等,类型覆盖项目运行的各个环节,解压后按项目说明配置即可启动。已有100人学习下载。包内附详细的项目说明文档,从环境搭建到功能实现均有讲解,便于快速理解整体结构;数据库脚本和完整源码确保项目可复现运行,无需额外修改,能够帮助使用者省去从零搭建和调错的时间,专注于理解JDBC、Servlet、JSP之间的协作流程与分层开发思路。
1. 这个基于JDBC+JSP+Servlet的图书管理系统源码包,解决的是你交课设的最后一公里
每年课程设计季,“基于JDBC+JSP+Servlet的图书管理系统源码+数据库+项目说明”这类压缩包都是下载热门。它本质上是一套能直接运行的Java Web课设成品:Servlet处理浏览器请求,JSP渲染页面,JDBC操作MySQL,zip里还附带建库SQL和写好的项目说明文档。对两类人尤其有用——正在赶Java Web课程设计或毕业设计的学生,以及学完Servlet规范但没串过完整项目的初级开发者。我见过不少同学卡在最后一公里:代码跑不起来,却又不知道问题在数据库还是Tomcat。把这个包从头到尾复现一遍,你对一条HTTP请求怎么落到数据库再原路返回的理解,会比背十遍框架原理都牢靠。
2. 拆开源码包看结构:JDBC+JSP+Servlet怎么分工,一次登录请求怎么走通
2.1 为什么课程设计到现在还爱用这套“老组合”
先别急着吐槽技术老旧。JSP和Servlet是Java Web最底层的官方规范,JDBC是所有数据库访问API里最接近原生SQL的一套接口。Spring MVC再封装,底层还是DispatcherServlet往下转发;MyBatis再方便,底层还是JDBC的PreparedStatement。这套源码包在教学上有不可替代的价值:它能让你看清一次请求从URL到数据库表记录的全部路径,而这恰恰是框架帮开发者藏起来的东西。
市面上Java课程设计案例源码里,十个有八个是管理系统:图书、学生、超市、毕业论文过程管理……搜“基于JSP的毕业论文管理系统设计与实现”或者“servlet demo”,拆开看骨架几乎一模一样,只是把图书换成了论文课题。所以你花在这个源码包上的时间不是浪费——把一个系统的请求链路吃透,换个业务对象你也能交差。
许多课设题目要求里会写明“不得使用框架”,要你用JSP、Servlet和JDBC手工实现三层结构。这也是为什么这套老组合至今还在被大量下载:不是技术潮流,是教学大纲还停在这。理解它,你才看得懂框架到底替你做了哪几件事。
2.2 源码包里常见的目录布局:每个目录负责什么
解压一个典型的Java Web课设包,第一眼别慌,文件再多也逃不出下面这张表:
| 目录/文件 | 作用 |
|---|---|
| src/com.xxx/entity(或model/pojo) | 实体类,对应数据库里的表:Book、User、BorrowRecord |
| src/com.xxx/dao | DAO层,封装对book、user、borrow等表的增删改查 |
| src/com.xxx/servlet(或web/controller) | Servlet控制器,接收请求、调DAO、决定跳转 |
| WebContent(或webapp) | JSP页面和静态资源 |
| WebContent/WEB-INF/web.xml | 路由配置,也有项目用注解替代 |
| WebContent/WEB-INF/lib | 项目依赖jar包,重点看MySQL驱动 |
| 根目录/book.sql(或init.sql) | 建库建表+初始化数据 |
| 项目说明.doc/.md | 答辩用的那本“说明书”,源码+笔记都在里面 |
注意包名可能叫com.xxx.xxx,但绝大多数课设包都遵守entity/dao/servlet这种划分。如果你解压后只有一堆JSP和一个类,那多半是不完整的残包——那种建议直接放弃,别浪费时间改。
这里说个常见的误用:有人把业务逻辑写在JSP里,用<% %>直接查数据库。在纯课设里也许能跑,但这不是三层结构,答辩被问到会很难看。标准的做法是JSP只做展示和收集参数,Servlet做流程控制,DAO做数据库访问。
2.3 一次登录请求的完整旅程:JSP→Servlet→DAO→数据库
我们拿登录功能串一遍,这是理解整个源码包最快的路径。用户在login.jsp输入账号密码,表单提交到LoginServlet,Servlet把参数取出来交给UserDao,UserDao用JDBC去user表查记录,查到了就把用户塞进session,然后重定向到主页面;查不到就在request里放一条错误消息,再转发回login.jsp。这段代码在绝大多数包里长这样:
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); UserDao dao = new UserDao(); User user = dao.findUserByUsernameAndPassword(username, password); if (user != null) { req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }逻辑说明:doPost只处理POST请求,表单method="post"对应这里;req.getParameter从请求体里取表单字段;findUserByUsernameAndPassword内部本质是一条“SELECT * FROM user WHERE username=? AND password=?”,用PreparedStatement防SQL注入;成功用重定向,失败用转发,区别在于重定向会发起第二次请求、URL地址会变,转发是服务器内部跳转、地址栏不变。
参数说明里最容易被忽略的是setCharacterEncoding必须放在读取任何参数之前,放在取参数之后就会中文乱码。另外,JSP里表单的action建议写成${pageContext.request.contextPath}/login,不要只写login。很多404就是这种相对路径导致的:你在/login.jsp页面提交,action="login"会被解析成/login/login。
2.4 JDBC连接配置四件套:driver、url、用户名、密码
所有JDBC操作的第一步都是建立连接。源码包里通常有两种写法:一是写一个DBUtil工具类,二是用db.properties配置文件。无论哪种,核心都是driver、url、username、password四个值。以最常见的DBUtil为例:
public class DBUtil { private static String url = "jdbc:mysql://localhost:3306/bookdb?useSSL=false&characterEncoding=utf8"; private static String user = "root"; private static String password = "123456"; static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动未找到,请检查WEB-INF/lib下的jar包", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } public static void close(AutoCloseable... resources) { for (AutoCloseable r : resources) { if (r != null) { try { r.close(); } catch (Exception e) { } } } } }逻辑说明:静态块在类第一次加载时执行,Class.forName把驱动类注册到DriverManager,之后getConnection才能识别jdbc:mysql这种协议;close方法用可变参数统一关Connection、Statement、ResultSet,避免每个DAO里写三遍try-catch。
参数说明:url里的localhost:3306是MySQL地址和端口,bookdb是库名,useSSL=false是让本地连接不走SSL加密以免报警告,characterEncoding=utf8保证中文正常传输;user和password必须和你本机MySQL一致——源码包默认这个值是作者机器上的,不是你的。这套配置是整套源码包的命门,后面遇到的所有连接问题,百分之八十都能追溯到这里。下一章我就带你从建库开始,把这条路亲手走通。
3. 把数据库初始化走通:导入SQL与JDBC连接MySQL的实操步骤
不管源码包里代码多完整,数据库起不来,项目就永远停在启动页。这一章说的是从零到连上库的过程:先过一遍MySQL数据库常用命令,再处理驱动版本,最后用一个不依赖Tomcat的测试类把问题边界划清楚。
3.1 用mysql命令导入SQL:先建库再source
拿到压缩包先找.sql文件。常见的命名是book.sql、library.sql、init.sql,打开看一眼:文件开头如果有CREATE DATABASE语句,你可以跳过手动建库;如果没有,就老老实实先建库再导入。推荐用命令行,因为报错信息最直接:
# 先登录MySQL mysql -u root -p # 登录后在mysql>提示符下依次执行 mysql> CREATE DATABASE IF NOT EXISTS bookdb DEFAULT CHARACTER SET utf8mb4; mysql> USE bookdb; mysql> SOURCE D:/book_system/book.sql; mysql> SHOW TABLES;逻辑说明:SOURCE是mysql客户端的命令,作用是执行指定文件里的全部SQL;它和Windows下的mysql -uroot -p bookdb < book.sql效果一样,但SOURCE能看到每一条语句执行成功还是报错,排错体验更好。
参数说明:utf8mb4比utf8多支持emoji和生僻字,课设表结构用utf8mb4基本不会出字符集问题;SHOW TABLES执行后,你至少应该看到和系统功能对应的表,常见是book、book_type、borrow_record、user这种结构,如果一张表都没有,说明SQL文件没被正确执行。
很多人在这一步就翻车:SOURCE执行完报了一堆表已存在或字段重复。这通常是因为之前导入过一次,又重复执行了带CREATE TABLE IF NOT EXISTS之外的旧脚本。解决办法是先看SQL文件里有没有DROP TABLE IF EXISTS,没有的话手动把旧库删掉重建:mysql> DROP DATABASE bookdb;然后重新建库导入。不用怕,课设数据没有“后悔药”,重建成本极低。
用Navicat的也可以:连接本地MySQL,右键bookdb选“运行SQL文件”,注意文件编码选UTF-8,不然中文初始数据导入就是乱码。我一般会优先用命令行,因为Navicat对文件里包含DELIMITER这种临时分隔符时偶尔会卡壳。
3.2 驱动jar版本和MySQL版本的匹配关系
SQL导完,接下来解决lib目录下的mysql-connector-java.jar。这个jar是MySQL官方提供的JDBC驱动,版本必须和你本地MySQL大版本兼容,不然连接都建不起来。对照关系大致如下:
| 本地MySQL版本 | 驱动jar建议 | 驱动类名 | URL需额外注意 |
|---|---|---|---|
| MySQL 5.5 / 5.7 | mysql-connector-java 5.1.x | com.mysql.jdbc.Driver | 无需时区参数 |
| MySQL 8.0+ | mysql-connector-java 8.0.x | com.mysql.cj.jdbc.Driver | 加serverTimezone=Asia/Shanghai |
逻辑说明:MySQL 8.0开始官方把驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver,连接协议和默认认证方式也变了。如果你的源码包里lib下是5.1.x驱动,而本机装的是MySQL 8.0,两种处理方式:一是换8.0.x的jar,二是继续用5.1.x但把URL改成jdbc:mysql://localhost:3306/bookdb?useSSL=false&serverTimezone=Asia/Shanghai,同时把驱动类名改成com.mysql.cj.jdbc.Driver。我见过不少包里的DBUtil用的是旧类名,导致MySQL 8用户一启动就爆ClassNotFoundException。
判断驱动版本有个笨但有效的办法:把jar包复制出来,后缀改成zip,解压后看META-INF/MANIFEST.MF里的Implementation-Version。熟手一般直接在IDEA的External Libraries里展开jar看包路径,能同时看到版本和类名。记住,这个jar不仅要被项目引用,还必须出现在部署后的WEB-INF/lib里,否则编译通过、运行时报ClassNotFoundException,具体原因第五章细说。
提示:换新驱动jar时,记得同时检查DBUtil里的driver类名,这是最容易漏的一步。
3.3 不启动Tomcat也能测JDBC连接:写一个TestConn
把数据库和驱动都准备好之后,先别急着启动Tomcat。我强烈建议先写一个带有main方法的测试类,直接验证JDBC连接——这一步能帮你划清问题边界:连不上,是数据库账号密码问题,还是驱动没配置对,一眼就能看清,不用去Tomcat一坨日志里捞。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class TestConn { public static void main(String[] args) throws Exception { Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql://localhost:3306/bookdb?useSSL=false&characterEncoding=utf8"; Connection conn = DriverManager.getConnection(url, "root", "123456"); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT COUNT(*) FROM book"); if (rs.next()) { System.out.println("book表当前行数: " + rs.getInt(1)); } rs.close(); stmt.close(); conn.close(); } }逻辑说明:Class.forName注册驱动,DriverManager.getConnection建立物理连接,Statement执行SQL,ResultSet拿到结果集;注意连接使用后要手动关闭,裸JDBC没有自动释放这回事。
参数说明:url、账号、密码要和DBUtil里保持一致,尤其是密码,源码包里默认的123456只是示例,把它改成你本机的真实密码再跑。如果输出book表当前行数,说明库、表、驱动、账号全部正常,问题就不在数据库侧;如果抛Access denied,是账号密码不对;如果抛UnknownHost或Communications link failure,是MySQL没启动、端口不对或认证方式不匹配。
这里顺带说一个血泪经验:TestConn别放在src的默认包下,放在com.xxx.dao下一个临时类里跑就行,跑通了直接删掉,别让它留在提交版本里。有同学把TestConn忘在工程里,答辩时老师点开看到一堆硬编码密码,印象分直接扣一半。测试类的作用是划边界,不是留在源码里当摆设。
4. 在Tomcat里部署跑通:从IDEA导入到登录成功的关键配置
数据库通了,这一章解决Web工程本身的部署。很多人数据库没问题,却卡在IDEA和Tomcat之间:不是缺这个勾选,就是Artifact没配置对。这里按我常用的路径写,不绕弯。
4.1 IDEA导入项目并配置Tomcat:五个关键步骤
第一步,用IDEA打开解压后的源码目录,选中包含src和WebContent(或webapp)的根目录,不是选中里面任意一个子文件夹。第二步,确认项目SDK是Java 8或更高,课设老代码一般在Java 8下最稳。第三步,打开Project Structure(按Ctrl+Shift+Alt+S),在Libraries里把WEB-INF/lib下的jar包Add as Library——这一步不做,编译可能过,运行必报ClassNotFoundException。
第四步,配置Run/Debug Configurations,新增Tomcat Server Local,指定Tomcat主目录,注意Tomcat版本不要比JDK差太多,Java 8用Tomcat 8.5或9.0都比较省心。第五步,切到Deployment选项卡,添加Artifact,选带exploded后缀的那个(war exploded形式),把Application context改成/book或和包名一致。
这五步里最容易被忽略的是第三步和第五步。第三步影响运行时classpath,第五步决定访问URL是什么:Application context填/book,那你访问地址就是http://localhost:8080/book/index.jsp,填了别的就按填的来。很多“页面404”其实是这里填的和URL没对应上,不是代码问题。
4.2 servlet映射:web.xml和@WebServlet到底用哪个
打开源码包你会发现,老一点的课设用web.xml配置servlet,新一点的直接在类上写@WebServlet注解。两种方式等价,但千万不要同一段路由两处都配,容器启动会报错或行为错乱。web.xml的长相通常是:
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.xxx.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>逻辑说明:servlet-class是类的全限定名,url-pattern是外部访问路径;容器启动时会加载这段xml,把/login这个URL和LoginServlet绑定。
参数说明:如果类上已经有@WebServlet("/login"),就把web.xml里这段servlet和servlet-mapping整体删掉,二选一,保留注解;反过来如果从web.xml里删了但类上没有注解,这个路径就直接404。我一般先看web.xml——因为web.xml里除了servlet还配了欢迎页、字符编码过滤器等全局信息,一眼能看出这个包整体走的是哪套配置风格。
4.3 启动、登录验证:怎样才算真正跑通
配置无误后,启动Tomcat。观察IDEA底部Run窗口:看到“Artifact is deployed successfully”或“Deployment of web application archive … has finished”,说明工程已部署;如果报BUILD FAILURE或端口占用,先回看5.5节的排查。启动成功后,访问http://localhost:8080/book/index.jsp,能出现图书管理系统登录页,说明JSP引擎和web.xml已经不再报错,剩下就差登录这一哆嗦。
登录账号去哪找?先看项目说明文档,里面一般会写默认管理员账号;文档没写或写错,就打开SQL文件里的insert语句,搜索user表,明文密码或MD5密文都在那里。常见课设包默认账号多为admin/123456这种组合,但不同包差异很大,以你手上SQL导入后的user表为准。登录成功后验证四件事:列表页能出来、图书能新增、借书能成功、还书能更新库存。这四件事走通一遍,这套源码包在你本机才算真正落地。
验证过程中如果跳回登录页,最常见的两个原因:一是session失效,代码里通过session.getAttribute判断登录状态,改代码时不小心把session设进去的名字写错,就会反复“未登录”;二是Filter拦截了/、但放行了login.jsp却没放行静态资源,导致页面样式全丢。这两种情况都属于代码逻辑问题,用浏览器F12看Network和Console基本能定位,别一上来就怀疑数据库。
5. 复现这套源码的避坑清单:最常见的6个翻车现场和解决办法
这一章是我根据自己的使用经验整理的六条翻车记录,按“现象→原因→解决”写。每一条都真实发生过,且基本覆盖了下载这套源码的人能遇到的大部分“事故”。
5.1 启动后页面404或一直跳回登录页:访问路径与session失效
现象:启动Tomcat后访问页面404,或者登录成功后一刷新就跳回登录页。
原因:多数是Application context配置和实际访问路径不一致,或者JSP里用了相对路径;另一种是session里存的key和过滤器里读的key对不上。
解决:先确认浏览器地址栏的路径,再对照IDEA的Deployment里的Application context;JSP里的链接和表单action统一用${pageContext.request.contextPath}开头,彻底告别相对路径问题。session问题去全局搜索“setAttribute(”和“getAttribute(”,两边key必须完全一致,哪怕多个空格都会失败。
5.2 数据库中文全变问号:字符集从库到页面处处对不上
现象:SQL导入后中文正常,但页面新增的中文进了数据库变成??。
原因:可能发生在三个位置——库的默认字符集不是utf8、JDBC连接串没带characterEncoding=utf8、JSP页面编码不是UTF-8。
解决:先执行SHOW CREATE DATABASE bookdb;看默认字符集,不是utf8mb4就重建库;再检查DBUtil里URL是否带characterEncoding=utf8;最后确认每个JSP的pageEncoding="UTF-8"。这三处都对齐,中文才可能稳定。顺带一提,在doPost里设置request编码时,必须放在第一次读取参数的代码之前,否则那一次请求里的参数已经按ISO-8859-1解析过了,后面再设也白搭。
5.3 ClassNotFoundException: com.mysql.jdbc.Driver:jar包没进运行时classpath
现象:代码里import和编译都没问题,一运行抛ClassNotFoundException。
原因:IDEA把lib目录当成普通文件夹了,编译时引用到了jar,部署时jar没被带进WEB-INF/lib。
解决:右键lib目录,选“Add as Library”,并把jar包手工确认出现在Artifact的输出目录里;更稳妥的办法是检查Project Structure里的Artifacts,展开Output Layout看WEB-INF/lib下有没有那个jar。注意换驱动类名也一样,MySQL 8用户把类名改成com.mysql.cj.jdbc.Driver后,如果jar没进去,报错同一场戏。
5.4 连接MySQL8直接失败:认证插件和时区参数不对
现象:启动或TestConn时报Public Key Retrieval is not allowed,或Access denied for user 'root'。
原因:MySQL 8默认认证插件是caching_sha2_password,老驱动不认识;时区参数缺失也会让新版驱动拒绝连接。
解决:报Public Key时,在URL里加allowPublicKeyRetrieval=true&useSSL=false;报Access denied时,要么改URL里的密码,要么在MySQL里把root的认证方式拉回老版本:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;这一步执行完,项目不用改代码就能连上。如果还是不行,看看MySQL服务到底起没起,netstat -ano | findstr "3306"有输出才代表在监听。
5.5 Tomcat启动报端口占用:Address already in use: JVM_Bind
现象:启动Tomcat时日志抛java.net.BindException: Address already in use: JVM_Bind,或者提示Port 8080 was already in use。
原因:8080被其他进程占用,通常是另一个Tomcat实例、Java进程或本地开发工具。
解决:Windows下执行netstat -ano | findstr :8080,找到占用8080的PID,再用taskkill /F /PID 那个PID;不想杀进程就改Tomcat的conf/server.xml,把Connector的port改成8081,然后访问http://localhost:8081/book/index.jsp。注意改端口后,IDE里Tomcat配置的端口也要同步,有的IDE会接管server.xml,两个位置不一致时以实际运行日志为准。
5.6 改完代码刷新不生效:热部署配置和重启时机
现象:在IDEA里改了JSP或Servlet,刷新浏览器还是老页面。
原因:Tomcat默认没开热部署,或者IDEA的update配置只更新了资源没有更新类。
解决:Run配置里把On Upate Action设为“Update classes and resources”,JSP和静态文件基本可以实时刷新;Java类文件改了之后按Ctrl+F10触发重编译。但改了web.xml、Servlet的注解路径、新增了类这种结构性变化,热部署常常不顶事,最保险还是重启Tomcat。
这个现象看似玄学,其实都是IDE与Tomcat的部署机制差异:JSP会被容器当成模板每次读取,Java类只在类加载器加载过一次后才生效,两种更新粒度的行为本来就不一样。所以在开发期默认全部重启,稳定之后再用热部署,反而省时间。
6. 把课设源码改造成能写进简历的项目:三个低成本高回报的改动方向
如果你的目的不只是交作业,而是想把这份源码变成面试能讲的Demo,下面三个改动方向投入产出比最高。
6.1 加一个登录拦截Filter,把权限控制从“每个页面都写”里解放出来
@WebFilter("/*") public class AuthFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); if (uri.endsWith("login.jsp") || uri.contains("/login") || uri.contains("/static/")) { chain.doFilter(request, response); } else if (req.getSession().getAttribute("loginUser") == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); } else { chain.doFilter(request, response); } } }逻辑说明:这个过滤器把所有请求拦一道,登录页和登录接口放行,其余请求看session里有没有loginUser,没有就踢回登录页。你原来在每个页面里复制粘贴的登录判断,现在全都可以删掉。这种“拦截器思维”放到Spring里就是Interceptor,面试时能讲清楚。
6.2 把借阅记录做成借阅排行,展示一条聚合SQL
在管理后台加一个简单的“借阅排行”页面,数据源用一条GROUP BY查询:SELECT book_id, COUNT(*) AS cnt FROM borrow_record GROUP BY book_id ORDER BY cnt DESC LIMIT 10,再配合JOIN book表把书名带出来。这个改动的价值在于:它展示了你对聚合查询和表关联的掌握,比单表增删改查高一个档次;而且表结构里本来就有borrow_record,拿来就能查,不用新造数据。唯一要注意的是COUNT结果为空的情况——把空列表的页面状态处理干净,这一处细节能让你答辩时少被追问一轮。
6.3 把裸JDBC连接池换成Druid,注意别把它写成“高端炫技”
在DBUtil里用阿里Druid替换DriverManager.getConnection,核心就两行:初始化DruidDataSource,配置初始连接数、最大连接数。换成连接池后,项目在高并发下更稳,也说明你接触过工程化手段。但我不建议在答辩里把它吹成“性能优化”——课设场景本身压力不大。你说清楚“连接池是复用连接,省去每次TCP握手和认证开销”就够了,老师反而觉得你懂了本质。
我刚接触这套源码时,脑子里只有“能跑就行”。交课设前我把所有页面点了一遍,唯独漏了列表为空时的渲染分支,答辩现场被老师一张空表格问住,才意识到“正常路径跑通”不是终点。后来我养成一个习惯:任何页面都先测空数据、再测异常数据、最后测正常数据。这个顺序后来帮我躲掉不少坑。希望帮到你。
本文还有配套的精品资源,点击获取