ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Servlet从入门到实战:IDEA配置、生命周期与JDBC+MySQL案例

Servlet从入门到实战:IDEA配置、生命周期与JDBC+MySQL案例 直接从最痛的那个问题开始聊吧。我见过太多Java初学者学完了JavaSEJava基础打得还算扎实集合、IO、多线程都能写一进入JavaWeb阶段就懵了。特别是“Servlet”这个概念书上看懂了视频里跟着敲了但自己打开IDEA想从零搭一个JavaWeb项目却发现连“这玩意儿到底怎么跑起来”都没搞明白。更别提用Servlet连MySQL做增删改查了文件建了一堆启动Tomcat直接报404整个人直接心态爆炸。这篇文章我不打算给你念课本。我会按照我自己当时踩坑摸出来的路子从IDEA里如何正确配置JavaWeb项目环境到Servlet底层到底是个什么玩意再到怎么用ServletJDBCMySQL串起一个真正能跑的完整案例一步一步拆给你看。内容包括完整的项目结构、关键代码、配置细节以及那些网上的破烂教程从来不会告诉你的坑。这不是一篇概述文而是一篇可以直接跟着敲的实践记录。1. 先别急着写代码IDEA里JavaWeb项目的正确“打开方式”很多新手第一步就死在了环境配置上。明明代码是从教程里一行行抄的项目结构也对为什么启动Tomcat就是报错这里我先带你梳理一下在IDEA中让一个JavaWeb项目“跑起来”背后到底发生了什么。1.1 项目结构的前世今生从Web应用目录到WAR包JavaWeb项目本质上是一个遵循Servlet规范、可以部署到Servlet容器最常用的就是Tomcat里的Web应用。一个标准的Web应用目录结构是这个样子的my-webapp/ ├── WEB-INF/ │ ├── web.xmlServlet 3.0之前必须之后可以用注解替代 │ ├── classes/编译后的.class文件和配置文件 │ └── lib/依赖的jar包 ├── css/静态资源 ├── js/静态资源 └── index.html入口页面IDEA在编译JavaWeb项目时最终做的事情就是把你项目里的源代码、依赖、静态资源打包成上面这样一套目录结构然后把它“交给”本地的Tomcat。Tomcat一看,哦,这是一个符合规范的Web应用,就把里面的Servlet类加载到JVM里,开始监听对应端口。这里最关键的IDEA配置是Project Structure项目结构里的 Artifacts。你需要保证:项目的编译输出路径(Output Directory)指向WEB-INF/classes;依赖的jar包,Tomcat lib目录下的基础依赖可能不算,你自己的jar包要放到WEB-INF/lib里;静态资源直接放到应用根目录。IDEA的配置里有一个“Web Application: Exploded”的Artifact类型,就是从开发目录直接映射出上面这个结构,开发调试比打包成WAR再部署方便得多。这也是为什么你在Run Configuration(运行配置)里选Tomcat Server时,Deployment选项卡里要选带“exploded”后缀的那一项。注意如果你自己建项目时没选Web应用模板,或者是从别的地方拽过来一个普通Java项目,大概率会遇到Error: java: package javax.servlet does not exist。这说明你的项目根本没引入Servlet API这个依赖,Artifacts结构也是空的。这时候别慌,在Project Structure的Libraries里把Tomcat安装目录下的lib/servlet-api.jar(新版叫jakarta.servlet-api.jar)加上,Libraries和Artifacts两边都得有,才会正常打包进去。1.2 本地Tomcat的版本选择Java版本和Jakarta命名空间这里要插入一个非常现实的问题——版本。不同版本的Tomcat对应的Java版本和Servlet规范差异很大Tomcat版本Servlet规范Java版本要求包名Tomcat 9.xServlet 4.0Java 8javax.servletTomcat 10.xServlet 5.0Java 8jakarta.servletTomcat 11.xServlet 6.0Java 11jakarta.servlet很多人在网上找的教程是老的,用的是javax.servlet包名,结果装了个Tomcat 10,代码引包一直报错找不到,或者编译通过但启动后ClassNotFound。如果你是新学,建议直接用Tomcat 9 Java 8/11/17的组合,报错最少;如果你用Tomcat 10,代码里的import语句必须把所有javax.servlet改成jakarta.servlet,别无他法。IDEA的Run Configuration里配置Tomcat Server时,还有一个隐藏深坑Application server的路径。如果你下载的是Tomcat解压版,必须把路径选到解压目录目录下直接能看到bin、conf、lib这些文件夹,选错了IDEA会直接报“Tomcat location is not valid”。配好Tomcat后,启动时你会看到控制台刷出类似这样的日志Using CATALINA_BASE: 本机临时目录/.../Tomcat_..._xxx ... Deploying web application archive [xxx.war] 或 Deploying web application directory [xxx] ... INFO [main] org.apache.coyote.AbstractProtocol.start - Starting ProtocolHandler [http-nio-8080]看到Starting ProtocolHandler [http-nio-8080]说明Tomcat已经起来了。如果这时浏览器访问http://localhost:8080/你的项目名还是404,而且控制台没有任何部署信息,那基本可以锁定是IDEA的Run Configuration没配好。2. Servlet是什么拆开那个“过渡层”的底层逻辑好,环境通了,就该直面Servlet本体了。教材上喜欢说“Servlet是运行在服务器端的小程序”,这句话没毛病,但完全不能解决问题。你真正需要搞清楚的是Servlet在Web请求链路中到底站在哪一环,它和JSP、Filter、Listener这些家伙有什么关系。2.1 用餐厅做类比为什么中间非得要一个Servlet想象你开了一家餐厅Web服务器。客人浏览器到店,想吃的菜其实是一份动态生成的菜单动态网页。如果客人直接冲到后厨数据库去翻食材,第一,厨房太忙会乱套;第二,客人看不懂原始食材数据库里的二进制数据;第三,总不能每个客人都直接进后厨操作吧Servlet就是你请来的一批固定岗位的服务员。客人把需求写在点餐单上——这就是HTTP请求里的URL、请求参数、请求头服务员拿到点餐单,按照店里规定的流程业务逻辑去后厨取数据——这就是访问数据库、处理业务;然后服务员把菜品端上桌——这就是把数据放进HttpServletResponse里返回给浏览器。中间服务员还负责清洗Filter、分桌Listener、摆盘JSP渲染页面等等。一句话总结Servlet是连接HTTP请求和Java业务逻辑之间的“交通警察”。每一个URL背后,都可能是一个或多个Servlet实例在工作。2.2 Servlet的生命周期容器说了算没写过真实项目的初学者通常很难理解Servlet的生命周期为什么重要。我直接说结论init()方法只在Servlet第一次被请求访问时执行一次,或者是容器启动时根据load-on-startup配置提前加载执行。它负责初始化数据库连接池、加载配置文件这类“一次性”的资源。service()方法是核心,每次请求都会执行,内部会根据HTTP请求方法分发给doGet()或doPost()。destroy()方法在应用卸载或容器关闭时执行一次,用来释放资源。下面的代码演示了完整的覆盖方式WebServlet(name lifecycleServlet, urlPatterns /lifecycle) public class LifecycleServlet extends HttpServlet { public LifecycleServlet() { System.out.println(构造方法被调用); } Override public void init() throws ServletException { System.out.println(init被调用——只初始化一次); } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { System.out.println(doGet被调用——每次GET请求都执行); resp.setContentType(text/html;charsetUTF-8); resp.getWriter().write(h3Servlet生命周期演示/h3); } Override public void destroy() { System.out.println(destroy被调用——容器关闭时执行); } }这里有一个非常经典的坑——线程安全。Servlet是单实例多线程的容器只会创建这个Servlet类的一个实例,但多个请求可能同时执行它的service()方法。你如果在这段代码里写了某个成员变量private int count 0;然后在doGet里count,那个数值会出现并发问题,偶尔多个用户同时访问会把正确的数值覆盖掉。所以只要你看到有人把“每次请求状态不同”的数据写进Servlet实例字段,基本就能断定他没写过多线程相关的代码。HttpServletRequest和HttpServletResponse这两个对象本身就是线程不安全的,不要让它们在成员变量里被多个线程共享。2.3 请求从浏览器到Servlet的映射规则URLPatterns你写完Servlet后,Web容器怎么知道http://localhost:8080/hello这个请求应该交给哪个Servlet处理靠的就是URL映射。最早的写法是在web.xml里配servlet servlet-namehello/servlet-name servlet-classcom.example.servlet.HelloServlet/servlet-class /servlet servlet-mapping servlet-namehello/servlet-name url-pattern/hello/url-pattern /servlet-mapping现代开发更推荐直接用WebServlet注解,省去大量XML配置。注意注解里的两种写法// 完全匹配只有访问 /hello 时才进入 WebServlet(/hello) // 路径前缀匹配/user/list /user/add 等都会进入 WebServlet(/user/*)路径匹配的优先级有个隐形规则越具体的匹配优先级越高。比如同时存在/user/*和/user/query两个Servlet,访问/user/query时,命中的是后者,而不是通配符。这个规则一旦搞错,排查404和请求进错Servlet的时间会让你怀疑人生。3. 打通整条数据链用ServletJDBCMySQL实现一个完整案例工具都认识完了,下面直接上一道硬菜一个学生信息管理的ServletMySQL完整项目。这个案例基本上是从各大培训机构的经典JavaWeb项目简化出来的,能覆盖注册POST表单、查询GET请求、数据库交互、页面跳转这四个从Servlet到JSP的核心知识点。我带你从头写一遍,代码可以直接复制,比较完整。3.1 创建数据库和表结构先在MySQL里准备一张学生表作为我们后面所有操作的落点CREATE DATABASE IF NOT EXISTS servlet_demo DEFAULT CHARACTER SET utf8mb4; USE servlet_demo; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, name VARCHAR(50) NOT NULL COMMENT 学生姓名, email VARCHAR(100) NOT NULL UNIQUE COMMENT 邮箱, age INT COMMENT 年龄, created_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; INSERT INTO student(name, email, age) VALUES (张三, zhangsantest.com, 21), (李四, lisitest.com, 22);这里用utf8mb4而不是utf8,务必养成这个习惯。utf8在MySQL里最多只能存3字节的字符,而一个emoji或某些生僻汉字在utf8mb4下需要4个字节,用utf8字段存中文虽然一般没问题,但遇到表情符号就会报Incorrect string value错误。现在的项目里大家统一用utf8mb4,数据库、表、连接URL里的characterEncodingUTF-8连字符集都配齐,从源头规避乱码。3.2 引入依赖mysql-connector-java老版本的JavaWeb项目没有Maven,手动把jar包拖进WEB-INF/lib里。现在主流做法是用Maven管理依赖,所以在pom.xml里加入dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency这里有一个容易让新手困惑的地方为什么用Maven引入了依赖,代码里面还得用Class.forName(com.mysql.cj.jdbc.Driver)其实从JDBC 4.0开始,DriverManager在启动时会自动通过ServiceLoader机制加载META-INF/services/java.sql.Driver里注册好的驱动类。所以理论上你不写Class.forName也能连上数据库。但老版本MySQL驱动5.x和早期的8.x在部分环境里自动加载时有坑,而且显式加载驱动会让你对“JDBC底层发生了什么”更有概念。稳妥起见还是写上一行,反正也不费事。3.3 编写JDBC工具类万变不离其宗的六步法JDBC操作数据库,无论框架怎么封装,底层永远是六个步骤加载驱动、获取连接、创建Statement/PreparedStatement、执行SQL、处理ResultSet结果集、释放资源。来,写一个工具类package com.example.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/servlet_demo?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8allowPublicKeyRetrievaltrue; private static final String USERNAME root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } public static void close(AutoCloseable... resources) { for (AutoCloseable resource : resources) { if (resource ! null) { try { resource.close(); } catch (Exception e) { e.printStackTrace(); } } } } }注意看着几个参数serverTimezoneAsia/ShanghaiMySQL 8.x驱动默认UTC时区,不设置时区会报The server time zone value XXX is unrecognized。用亚洲上海时区,写入数据库的时间字段才是本地时间。useSSLfalse本地开发不必启用SSL握手,不然每次连接多一次耗时的加密握手过程。allowPublicKeyRetrievaltrueMySQL 8.x用caching_sha2_password认证时,非SSL连接下首次连接需要这个参数来从服务器获取公钥,不写会报Public Key Retrieval is not allowed。3.4 核心Servlet实现POST接收参数与GET查询列表接下来是重头戏写一个用于处理“添加学生”和“查询学生列表”的Servletpackage com.example.servlet; import com.example.entity.Student; import com.example.service.StudentService; import com.example.util.DBUtil; 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 java.io.IOException; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; WebServlet(/student) public class StudentServlet extends HttpServlet { // 处理新增学生POST /student Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String name req.getParameter(name); String email req.getParameter(email); String ageStr req.getParameter(age); if (name null || name.trim().isEmpty() || email null || email.trim().isEmpty()) { resp.getWriter().write(scriptalert(姓名和邮箱不能为空);history.back();/script); return; } int age 0; try { age Integer.parseInt(ageStr); } catch (NumberFormatException e) { resp.getWriter().write(scriptalert(年龄必须是数字);history.back();/script); return; } String sql INSERT INTO student(name, email, age) VALUES(?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, name.trim()); ps.setString(2, email.trim()); ps.setInt(3, age); int rows ps.executeUpdate(); if (rows 0) { // 添加成功走重定向防止表单重复提交 resp.sendRedirect(req.getContextPath() /student); } else { resp.getWriter().write(scriptalert(添加失败);history.back();/script); } } catch (SQLException e) { // 唯一键冲突时email重复也会走到这里 e.printStackTrace(); resp.getWriter().write(scriptalert(添加失败邮箱可能已存在);history.back();/script); } } // 查询学生列表GET /student Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String sql SELECT id, name, email, age FROM student ORDER BY id DESC; ListStudent studentList new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Student stu new Student(); stu.setId(rs.getInt(id)); stu.setName(rs.getString(name)); stu.setEmail(rs.getString(email)); stu.setAge(rs.getInt(age)); studentList.add(stu); } } catch (SQLException e) { e.printStackTrace(); } req.setAttribute(list, studentList); // 转发到JSP页面渲染 req.getRequestDispatcher(/list.jsp).forward(req, resp); } }几个细节专门展开讲一下为什么POST提交成功的最后用的是sendRedirect,而不是forward转发这是网上教程最容易忽略的一个区别。req.getRequestDispatcher(/xxx).forward(req, resp)是服务器内部转发,URL地址栏不动,而且本次请求的request对象原封不动地传到新页面。这意味着什么用户按F5刷新,浏览器会重新提交刚才的POST请求,数据库里就会插入两条一模一样的记录。而我这里的做法是先执行完添加,然后sendRedirect让浏览器重新发起一个GET请求到同一个URL,列表页刷新了,数据库里也只有一条新记录。“POST后重定向刷新”这个模式,业界叫PRGPost/Redirect/Get,是防止表单重复提交最基础的手段。为什么用PreparedStatement而不是Statement第一,预编译能防止SQL注入。用户在输入框里填了 OR 11这种字符串,Statement拼接SQL会直接变成SELECT * FROM student WHERE email OR 11,把所有数据捞出来;而PreparedStatement把参数值当成纯字符串处理,永远不可能拼接进SQL结构里。第二,预编译可以复用SQL语法结构,数据库缓存的执行计划效率更高。我见过有些视频教程为了省事在demo里用Statement,新手跟着学,形成肌肉记忆后进公司写代码,那是要被code review打回重写的。3.5 JSP页面把列表数据展示出来建一个src/main/webapp/list.jsp。JSP的本质也是一个Servlet——Tomcat会把JSP翻译成Java类再编译执行。所以JSP里可以直接用% %表达式和c:forEach这类EL标签如果引入JSTL来输出数据% page contentTypetext/html;charsetUTF-8 languagejava % !DOCTYPE html html head meta charsetUTF-8 title学生列表/title style body { font-family: Microsoft YaHei, sans-serif; margin: 40px; } table { border-collapse: collapse; width: 80%; margin-top: 20px; } th, td { border: 1px solid #ccc; padding: 8px 12px; text-align: left; } th { background-color: #f5f5f5; } /style /head body h2学生信息列表/h2 form action${pageContext.request.contextPath}/student methodpost 姓名input typetext namename required / 邮箱input typeemail nameemail required / 年龄input typenumber nameage min1 max120 required / button typesubmit添加学生/button /form table tr thID/th th姓名/th th邮箱/th th年龄/th /tr c:forEach items${list} varstu tr td${stu.id}/td td${stu.name}/td td${stu.email}/td td${stu.age}/td /tr /c:forEach /table /body /html注意${pageContext.request.contextPath}这个表达式的作用——它取到当前项目的上下文路径,也就是http://localhost:8080/servlet_demo_war中间那段项目名。表单的action如果写成写死的/student,等你换了项目名或者部署路径一变,表单提交就会404。动态获取contextPath是JavaWeb开发里一个很基础但特别重要的习惯。这里引用了JSTL的c:forEach,所以还需在pom.xml里加两个依赖jakarta.servlet.jsp.jstl-api和jakarta.servlet.jsp.jstl-implTomcat 9以前的javax版本则加jstl和javax.servlet.jsp.jstl相关的包。不加这两个jar,JSP页面会报According to TLD or attribute directive的错,因为找不到c:forEach这个标签的定义。4. 避坑手册问题排查链路从404到中文乱码网上那些项目案例,你照着敲多半跑不通,跑通了也总有几个毛病,比如中文乱码、启动报错、表单重复提交。我们把最常见的坑集中拆一遍,顺便讲清楚怎么一步步定位问题。4.1 404错误排查思路不是所有404都因为路径写错404是JavaWeb新手遇到的第一座大山。访问一个页面报404,先按下面顺序排查第一步,看Tomcat控制台,确认应用是否真的部署成功。如果控制台没有Deploying web application的日志,但你手动启动Tomcat能跑起来,那大概率是IDEA没把你配置的Artifact附到Tomcat的deployment里。解决方式Run Configuration里,Deployment页签,点“”,选择Artifact,把你项目的exploded版本加进去,Application context填/或者项目名。第二步,确认访问的URL前缀是否带上了contextPath。你在IDEA的Deployment里配置了Application context: /servlet_demo_war,访问就得写http://localhost:8080/servlet_demo_war/student;如果你配成/,直接写http://localhost:8080/student。大部分404都是这个细节造成的。第三步,确认Servlet的注解/XML映射是否和请求路径完全一致。注意WebServlet(/student)是有斜杠的,有些人写成WebServlet(student),访问还是用/student,必404。注解路径匹配的是contextPath后的第一个路径段,大小写敏感。第四步,看一下Filter的拦截路径。如果项目里加了Filter且urlPatterns配置成/*,Filter里又没有放行静态资源和特定路径,所有请求都会卡在Filter里,表现可能就是页面能出但数据不显示,或者直接被response.sendRedirect走了,看起来像404。排查时在Filter的doFilter第一行打印请求URI,立刻就能看到请求到底走到了哪里。4.2 中文乱码的三处“关卡”中文乱码问题可以拆成三个独立层面,你逐个确认就不会被它折磨第一个层面,请求参数乱码。表单以POST方式提交中文,Tomcat默认用ISO-8859-1解码,所以到了Servlet里你收到的字符串就已经是乱的。解决方式是在ServletdoPost的第一行调用req.setCharacterEncoding(UTF-8),但要注意这个调用必须在读取任何getParameter()之前执行,否则无效。GET请求参数编码不在request body里,setCharacterEncoding对URL里的query string不生效,你需要去改Tomcat的server.xml里的Connector标签,加上URIEncodingUTF-8。Connector port8080 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /第二个层面,响应输出乱码。给响应写数据时,必须在获取Writer之前设置resp.setContentType(text/html;charsetUTF-8);如果写成resp.setCharacterEncoding(UTF-8)而忘了setContentType,HTTP头里的Content-Type是没有charsetUTF-8的,浏览器默认按本地编码比如Windows的GBK解析HTML片段,照样乱。如果用了response.setHeader(Content-Type, text/html; charsetUTF-8)也可以,但直接setContentType更简洁。第三个层面,数据库存取乱码。你POST进来时是正常的中文,数据库表也是utf8mb4,连库的URL却没带characterEncodingUTF-8,那数据存进去就乱URL带了编码,但表和库的字符集是latin1,也乱。检查表字符集的语句SHOW TABLE STATUS FROM servlet_demo LIKE student;重点看Collation列如果你看到的是latin1_swedish_ci,先ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;再把表里已有的脏数据删掉重插。4.3 ClassNotFoundException和NoClassDefFoundError依赖和编译输出目录利润新手在IDEA里运行Web项目,遇到java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver这类错误,通常不是代码问题,而是MySQL驱动jar没有出现在最终部署的WEB-INF/lib里。排查分两步先在IDEA的Project Structure里看Libraries,确认mysql-connector-java-8.0.33.jar有没有在项目依赖里;然后看Artifacts选项卡的Output Layout,展开Available Elements,右侧是否有你需要的jar包。如果右侧只有项目的classes,jar包在左侧Available Elements里,你需要双击把它加到WEB-INF/lib里,IDEA才会在构建时把它复制过去。用Maven项目的人,还经常遇到在外部库里有jar,但Artifacts固定的lib列表里没同步进去的情况——如果Maven依赖加了但Artifacts没更新,Maven项目跑不起来时优先去Project Structure看一眼。另外有一个很隐蔽的情况如果你在Project Structure的Libraries里,把Tomcat的servlet-api.jar也选进去,然后在Artifacts的available elements里见到两个同名jar一个是你自己加的,一个是Tomcat提供的,部署时IDE把两个都打进WEB-INF/lib,启动Tomcat时会看到The web application [] appears to have started a thread named [mysql-cj-abandoned-connection-cleanup]这类警告,严重时,JVM里同一个类出现两个ClassLoader加载的两份字节码,直接抛LinkageError。规范做法是编译时需要servlet-api,但部署时这个jar不应该进WEB-INF/lib,因为Tomcat自己在lib目录里已经有一个了。这个问题我当年排查了整整一个下午。4.4 表单重复提交和请求转发一个看似简单实则致命的行为差异表单重复提交这个问题,在很多“抄来的项目案例”里几乎都存在,但讲师们默认不提。当用户填写完注册信息,点提交,如果后台逻辑是// 错误示例 req.getRequestDispatcher(/list.jsp).forward(req, resp);然后用户按F5,甚至按CtrlR,浏览器会弹出一个“确认重新提交表单”的提示,确认后同一份数据又被插入一遍。同理,如果你在list.jsp的点击删除链接,用的是window.location.href发GET请求,那个GET里带了一个id参数,防范CSRF和幂等性的场景也会变得复杂起来。我的建议一直很明确任何会改变后端状态的请求增、删、改,成功处理之后一律重定向到“查询展示”的URL。这样一来,地址栏最终落在GET请求上,F5刷新也只是重新查一次列表,不会再命中POST。至于重定向后新请求里的request对象,里面原有的参数全部丢弃,这正是我们要的效果。5. 从Servlet到框架这个阶段你必须建立的几个心智模型文章接近尾声,但我最后想聊的可能是对你未来影响最大的东西——当你学完了Servlet,真切地跑通了一个JavaWeb项目,你接下来该怎么继续往前走,以及Servlet这个阶段到底在你的技术栈里是什么位置。5.1 你写的不是SERVLET,你学的是HTTP和设计思想很多初学者有个错觉Servlet是上古产物,现在企业里谁还直接写Servlet错得很离谱。现在主流的Spring MVC,底层核心就是DispatcherServlet——它本身就是一个Servlet。Spring Boot内置的Tomcat,仍然遵循Servlet规范。你学会Servlet,学到的并不是那一组API怎么用,而是理解了这组本质问题——HTTP请求到达服务器,容器怎么把它解析成Java对象;响应怎么从Java对象编码回HTTP报文;Filter在请求处理链上怎么拦截和放行;Listener怎么监听应用生命周期和会话属性变化。你把这些问题搞清楚之后,再用Spring MVC时,看到RequestMapping注解,你就知道它在做的事情,无非是帮你省去了你昨天手动写的WebServlet(/student)doGetdoPost那一堆样板代码。5.2 从“能用”到“够用”你还需要补哪些东西跑通上面这个案例,说明你已经具备了JavaWeb的基础骨架。但要上生产环境,下面这几块内容至少得补齐连接池直接用DriverManager.getConnection每次请求都新建连接,数据库连接开销大,并发上去了立刻卡死。至少要会用HikariCP或Druid配置数据源。MVC分层和封装把DBUtil里的逻辑迁移成DAO层、Service层、Controller层分开的结构,SQL操作全部收敛到DAO里,不要像我早些年版里那样直接把JDBC写死在Servlet中——Servlet只负责接收HTTP请求和跳转页面,业务交给Service,数据库访问交给DAO,这才是后面学框架的基础。事务控制一个业务操作涉及多条SQL时比如转账,一方扣款一方加款,必须把连接的事务管理从“自动提交”切到“手动提交,失败回滚”。JDBC里就是conn.setAutoCommit(false)和conn.commit()的组合,这个概念是必须掌握的。JSP的替代品JSP在现在的开发里基本存量维护,新的项目通常使用Thymeleaf模板引擎或前后端分离架构后端提供JSON接口。但学的时候还是建议你把JSP跑一下,因为JSP和Servlet的关系、内置对象、EL表达式的使用逻辑,都根植于Servlet API这些核心概念上。5.3 一个完整的学习路线参考如果你在Servlet这个阶段卡了比较久,可以重新调整你的学习节奏。我的个人建议是这个顺序先死磕HTTP协议至少三天——GET/POST的区别、状态码、请求头响应头的意义、Cookies和Session的工作机制,这部分是整个Web开发的底层修养;然后才是Servlet API,跟着本文这种带完整数据库链路的案例走一遍,重点把请求生命周期、转发与重定向、Filter拦截这些概念做扎实;接着是JSP/EL/JSTL,能看懂页面渲染逻辑即可;再往后接触Maven和连接池,把”依赖管理“和”资源复用“这两个生产级概念补上;最后再进入Spring生态,那时候你会发现,很多之前觉得玄乎的“自动配置”,底层就是你熟悉的Servlet规范和JDBC。踩过一遍坑之后,再回头看JavaWeb,除了那几行代码,其实你真正带走的是从“我照着教程敲了一段不知道干嘛的代码”升级到“我知道请求进来之后发生了什么”的掌控感。后面再学任何Web框架,你都很难再回到那种“添加一个依赖、代码没报错、但心里完全没底”的状态了。
返回列表