从零搭建纯Servlet工程:深入理解Web请求的完整处理链路

发布时间:2026/10/22 3:42:33

从零搭建纯Servlet工程:深入理解Web请求的完整处理链路 如果你翻过当前流行的Web开发脚手架自动生成的工程你会发现一个很现实的问题当一个请求从浏览器发出来到你写的业务代码之间中间到底发生了什么很多人其实说不清楚。依赖太多、封装太深反而让人忘了Web应用最原始的样子。这篇文章想做的就是回到Servlet本身——用手工方式搭建一个纯原生Java Web工程不引入任何框架把请求到达、参数读取、页面返回这条最基础的链路彻底走一遍。适合刚学完Java语法、想真正弄懂Web原理的新手也适合被框架包裹太久、想返璞归真的后端开发者。1. 为什么还要折腾纯Servlet工程1.1 框架依赖带来的“黑盒焦虑”现在的Web开发脚手架确实太方便了。选好依赖、点一下生成一个能跑的应用就出来了。但方便的背后是“魔法”你改了配置框架就自动装配了数据源你加了一个注解动态代理就在背后帮你去调方法。能跑的时候一切正常一旦出了问题为了定位一个小错误可能要翻自动配置源码、分析代理调用链。这不是说框架不好而是说它把底层逻辑藏得太深。对一个想把基础打牢的开发者而言依赖越多、封装越厚越难看清“一个Web请求到底是怎么被处理的”。我见过有同学调了半年Spring Boot接口却不清楚Servlet接口长什么样也不明白请求进入容器之后是容器先解析HTTP、创建请求对象再交给他的Controller。这些东西在框架里被自动处理了但理解它们恰恰是后端基础里很重要的一块。纯Servlet工程的最大价值就是彻底剥离掉这些魔法。工程里没有自动装配没有依赖注入只有一个最朴素的Servlet容器外加你自己写的Servlet、Filter和Listener。所有代码都是透明的你写的每一行都会被容器按规则调用不存在什么“隐式增强”。1.2 纯Servlet下每条请求链路都看得见搭好一个纯Servlet工程之后一次请求的路径是非常清晰的浏览器发起HTTP请求建立TCP连接。Servlet容器通常指Tomcat这类实现Servlet规范的开源容器接收到请求解析HTTP协议把请求内容封装成ServletRequest和ServletResponse对象。容器根据URL匹配规则找到对应的Servlet。容器调用Servlet的service方法service方法再根据HTTP方法分派到doGet、doPost等方法。你的业务代码读取请求内容、处理数据、写入响应。容器把响应内容发送回浏览器。这条链路里没有任何“自动注入”的环节。你会直观地看到Servlet本身是单实例多线程的多个请求会并发调用同一个Servlet实例实例化时机由容器决定资源释放要你自己处理线程安全问题要自己面对。这些恰恰是框架替你隐藏掉的关键内容。理解了这条链路再回头去看那些自带依赖的框架你会发现它们只是在Servlet之上做了一层封装而已很多“高级功能”本质还是Servlet容器提供的机制。1.3 什么情况下纯Servlet反而是更好的选择有人可能会问既然框架这么好为什么在小项目里反而推荐纯Servlet我个人的看法是很多内部工具系统、教学演示项目、接口服务业务规模根本不需要IoC容器和AOP切面。用ServletFilterJDBC就能实现的简单服务强行引入一整套框架只会带来更多依赖、更长启动时间、更复杂的部署配置。依赖管理的“繁琐”在哪里如果你用Maven或者Gradle一个Spring Boot起步依赖会带走几十个间接依赖版本冲突、传递依赖、依赖锁定稍有不慎就出问题。而纯Servlet工程编译期依赖几乎只有一个servlet-api包运行期由容器提供。整个WEB-INF/lib目录可以是空的构建产物就是一个干干净净的war包。这种“轻”不是幻想而是实打实把依赖减少了几个数量级。2. 搭一个能跑的最小工程目录结构、容器与部署2.1 Servlet容器的选择一个绕不开的版本话题搭建纯Servlet工程绕不开Servlet容器。最常用的就是Tomcat这类开源Servlet容器。但这里有一个特别容易踩的版本坑从Jakarta EE 9开始Servlet API的包名从javax.servlet迁移到了jakarta.servlet。如果你用Tomcat 10及以上版本代码里import的是javax.servlet编译可能通过但运行时会直接抛出类找不到或者方法签名不匹配的异常。为了避免这个问题我建议新手固定用一套组合。如果你想用javax.servlet包名就用Tomcat 8.5或9.0如果你坚持用Tomcat 10那就必须改用jakarta.servlet包名。两者在代码写法上几乎一致就是import语句不同。选型时不要混用这一点比选择具体容器版本更重要。可以用下面这个简易对照来记容器版本Servlet API包名典型Servlet规范版本Tomcat 8.x / 9.xjavax.servletServlet 3.1 / 4.0Tomcat 10.0.xjakarta.servletServlet 5.0Tomcat 10.1.x / 11.xjakarta.servletServlet 6.0 / 6.1我下面写的代码统一用javax.servlet包名做演示对应Tomcat 9这套组合这是目前资料最多、最不容易出错的配置。2.2 标准Web工程目录是“长”这样的纯Servlet工程不依赖Maven的约定目录结构它遵循的是Web应用标准目录布局。你完全可以手工创建结构如下my-webapp/ ├── src/ │ └── com/ │ └── demo/ │ └── web/ │ └── HelloServlet.java ├── web/ │ ├── index.html │ └── WEB-INF/ │ ├── web.xml │ ├── classes/ │ └── lib/这里最核心的是WEB-INF目录它是Servlet规范强制的内部目录。浏览器无法直接访问WEB-INF下任何资源只有容器内部的转发和代码逻辑才能读取它。WEB-INF/classes存放编译后的class文件WEB-INF/lib存放项目自己的jar包。对于纯Servlet工程lib甚至可以是空目录因为servlet-api由容器提供不需要打包进去。很多人第一次手工建工程时容易把class文件编译到工程根目录结果容器根本加载不到。这是因为Servlet容器只会把WEB-INF/classes路径加入类加载范围根目录下的class不会被识别。记住一点class文件必须落在WEB-INF/classes下jar包必须放在WEB-INF/lib下这是Web容器加载类的默认顺序。2.3 两条部署路线开发工具集成与手工编译发布搭建和部署有两条路线。第一条是直接用开发工具集成容器配置好Web应用上下文一键启动调试。这条路线方便但很多步骤被工具隐藏了新手不太容易理解到底发生了什么。我建议你至少完整走一遍手工路线把“编译——打包——部署”这三个步骤都经历过之后再决定要不要用开发工具。手工路线用命令行就能完成# 1. 编译Servlet源码class文件输出到WEB-INF/classes javac -encoding UTF-8 \ -cp $TOMCAT_HOME/lib/servlet-api.jar \ -d web/WEB-INF/classes \ src/com/demo/web/HelloServlet.java # 2. 打war包 jar cvf my-webapp.war -C web . # 3. 把war包放到容器的webapps目录下启动容器 cp my-webapp.war $TOMCAT_HOME/webapps/ $TOMCAT_HOME/bin/startup.sh手工编译时javac需要指定servlet-api.jar作为classpath这是整个工程编译期唯一需要的依赖。打完war包后放到容器的webapps目录启动容器容器会自动解压并部署这个应用。访问路径默认是http://localhost:8080/my-webapp/也就是war包名就是上下文路径。2.4 最小web.xml与注册Servlet的两种姿势WEB-INF/web.xml是整个原生工程里唯一的配置文件。一个最小可用的web.xml大概长这样?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 display-namemy-webapp/display-name /web-app在Servlet 3.0以前所有Servlet都必须在这个文件里注册。现在有两种注册方式一种是web.xml声明式的一种是Servlet类上加WebServlet注解的。注解方式让工程更简洁但web.xml方式的映射关系更直观适合理解URL到Servlet的绑定机制。web.xml的配置中心思想是这样先给Servlet一个内部名字把这个名字和实现类绑定再定义URL和内部名字的映射两步一起生效。servlet servlet-namehelloServlet/servlet-name servlet-classcom.demo.web.HelloServlet/servlet-class load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namehelloServlet/servlet-name url-pattern/hello/url-pattern /servlet-mapping有一点需要留意如果web.xml存在且设置了metadata-completetrue容器会跳过注解扫描只按web.xml配置来。向上面的web.xml没有加这个属性所以注解仍然会生效。如果你同时用两种方式注册同一个Servlet容易产生混乱建议一个工程只选择一种注册风格。3. 让第一个Servlet跑起来一个请求到达代码前的完整过程3.1 写一个最基础的Servlet所谓Servlet本质是一个继承了HttpServlet类的Java类。下面这个例子使用了WebServlet注解把URL路径/hello映射到HelloServletpackage com.demo.web; 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; WebServlet(urlPatterns /hello) public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType(text/html;charsetUTF-8); response.getWriter().write(h1Hello, Servlet/h1); response.getWriter().write(p当前时间 new java.util.Date() /p); } }当你编译并部署这个类后直接在浏览器访问http://localhost:8080/my-webapp/hello就能看到页面输出。注意到doGet方法被Override标记这说明父类HttpServlet已经提供了service方法这个方法会查看HTTP请求类型再调用对应的doXxx方法。如果你想手动验证这一点可以重写service方法打印日志或者故意只实现doPost再用GET方式访问会看到容器返回405错误。3.2 Servlet生命周期谁实例化它、谁调用它、谁销毁它理解Servlet生命周期是理解容器和代码关系的关键它由容器管理开发者只能通过回调方法感知。生命周期分三个阶段。第一个阶段是初始化容器加载Servlet类创建实例调用init方法。init默认在第一次请求到达时才触发这是“懒加载”策略。如果你希望容器启动就初始化比如做耗时资源准备在web.xml里配置load-on-startup大于0的整数值数字越小越先加载。第二个阶段是服务阶段每个请求到来时容器调用service方法service根据HTTP方法分派到doGet、doPost等。这里有个极其重要的细节Servlet实例只有一个所有请求共享它。也就是说容器是单实例多线程地调用你的ServletdoGet里面的实例变量会被多个线程同时访问存在并发安全问题。所以在原生Servlet里不要在实例变量里放可修改的共享状态必要时用ThreadLocal或加锁解决。第三个阶段是销毁容器在应用卸载或关闭时销毁Servlet实例调用destroy方法。这个方法适合做资源释放但不要在里面做耗时操作因为它只是收尾钩子。整个生命周期可以用一句话概括容器负责“创建、调用、回收”你只负责“继承、重写、处理”。3.3 URL到Servlet的映射规则精确匹配、路径匹配、扩展名匹配URL怎么找到Servlet是入门Web最容易混淆的点。Servlet规范定义了四种匹配规则优先级从高到低排列匹配类型写法示例说明精确匹配/hello只能精确命中这个路径路径匹配/api/*匹配/api下的任意二级路径扩展名匹配*.do匹配所有以.do结尾的URL默认匹配/处理未被其他Servlet匹配的请求有一个高频坑路径匹配/api/*和默认匹配/不是一回事。/是容器内置的默认Servlet通常用来处理静态资源而如果你把某个Servlet的URL映射配置成/*它会把所有请求都揽到自己手里包括静态图片、HTML页面、JSP页面全部变成这个Servlet处理静态资源就访问不到了。这种现象特别容易出现在新手刚学会路径映射时把/*当成“全匹配万能写法”结果页面全白。还有一个经验如果你使用JSP页面容器本身会映射一个JSP Servlet来翻译和编译JSP文件。你自己写的Servlet如果映射了/*也会把JSP的翻译流程覆盖掉导致JSP直接当作纯文本返回或报错。因此在实际项目中很少有Servlet需要映射到/*。大多数情况下要么精确映射某个业务路径要么用/api/*做REST风格接口。4. 处理请求并输出内容参数读取、中文乱码与页面跳转4.1 接收GET/POST参数的正确打开方式Servlet处理请求参数的API很直接HttpServletRequest接口里提供了几个方法。最常用的是getParameter(key)它返回第一个匹配的值。如果同名的参数有多个比如多选框用getParameterValues(key)拿到String数组。如果你只想拿参数名集合可以遍历getParameterNames()。WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); String[] hobbies request.getParameterValues(hobby); response.setContentType(text/html;charsetUTF-8); response.getWriter().println(username username); response.getWriter().println(hobby String.join(,, hobbies null ? new String[0] : hobbies)); } }这里要记住一个使用时机getParameter不是直接读InputStream而是容器解析请求体之后的结果所以你不应该在同一个请求里既调用getParameter又调用getInputStream读取原始内容后者会直接干扰参数解析。关于请求方法一般建议查询类操作用doGet涉及数据提交和状态变更的用doPost。Servlet容器对URI长度有限制GET方式把参数挂在URL后面太长容易被拦截POST把参数放在请求体里能承载更多数据所以上传和提交业务不要用GET。4.2 中文乱码问题的一次性解法中文乱码几乎是所有Servlet新手第一个遇到的综合问题。它不是单一原因造成的而是请求编码和响应编码两头都可能出错。先看请求方向。POST请求的参数在请求体里读取前必须设置请求编码并且要先于第一次getParameter调用request.setCharacterEncoding(UTF-8);如果你没有设置容器按默认的ISO-8859-1解码请求体中文就变成了乱码。这个方法必须在读取参数前调用如果先读了一次参数编码已经定了之后再设置就无效。再看GET请求。GET参数在URL上编码由容器对URI的解析规则决定。Tomcat 8.0之后的版本URI的默认编码已经是UTF-8通常不用额外处理。如果你用的是老版本或者容器配置被改成了ISO-8859-1需要在容器的server.xml里给Connector加上URIEncodingUTF-8配置。注意server.xml改动需要重启容器才生效。最后看响应方向。响应默认编码是ISO-8859-1返回中文会变成问号。在写任何内容之前设置响应类型和编码response.setContentType(text/html;charsetUTF-8);这段代码等价于同时设置Content-Type响应头和字符集。最稳妥的方式是把编码过滤器放在全局让所有请求和响应都默认UTF-8下一章会写这个过滤器。4.3 转发与重定向什么时候用哪个页面跳转有两个标准做法转发(forward)和重定向(sendRedirect)。它们的区别表面看是“服务端跳转”和“客户端跳转”但实际使用中存在本质差异。转发是由服务端内部完成的跳转浏览器地址栏不变一个请求全程只用一次往返。你可以在转发前把数据放进request作用域目标Servlet或页面能直接取出。典型的用法是MVC模式里控制层转发给视图层request.setAttribute(message, 查询成功); request.getRequestDispatcher(/result.jsp).forward(request, response);重定向则是服务端告诉浏览器“你再去访问另一个地址”浏览器地址栏会变化产生第二次独立请求。正因为是两次请求request作用域里的数据会丢失如果需要在重定向后传递数据要手动把参数拼在URL后面或者借助Sessionresponse.sendRedirect(request.getContextPath() /index.jsp);怎么选如果只是服务端内部展示同一个请求处理后的结果用转发如果涉及“登录成功跳到首页”“提交完成后回到列表页”“防止表单重复提交”等场景必须用重定向这样才能刷新地址栏避免用户刷新页面时把提交动作重复执行一遍。这个场景判断比记两者的API差异更重要。5. 不引框架也能优雅过滤器、监听器与原生数据库访问5.1 用Filter统一处理编码和登录校验Filter是Servlet规范里一个非常强大的组件它的作用是“拦截请求、做些处理、放行或拦截”。你可以把它想象成安检口请求必须经过Filter然后决定是否放进后面的Servlet。最常见的Filter就是全局编码过滤器。它解决了上一章说到的乱码问题因为Filter的执行时机在Servlet之前package com.demo.web.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; WebFilter(/*) public class EncodingFilter implements Filter { Override public void init(FilterConfig filterConfig) throws ServletException { } Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } Override public void destroy() { } }这个Filter把上一章的所有编码处理集中到一个地方工程里所有请求在到达Servlet时都已经是UTF-8编码。注意最后必须调用chain.doFilter(request, response)否则请求链在这里被截断后面的Servlet永远不会执行。如果某个请求需要被拦截比如未登录用户访问受保护资源只要不调用chain.doFilter而是直接重定向到登录页就能实现拦截效果。登录校验的Filter是初学者最容易理解的拦截场景。它通过HttpSession判断用户是否已登录如果session中没有目标对象就重定向到登录页面已经登录的才放行。这个模式几乎是所有Web系统“统一鉴权”的雏形理解了它再看框架里的拦截器、过滤器链会发现内核机制完全一样只是封装形式不同。5.2 用Listener管理应用级共享数据Listener对应的是“事件监听”思想容器在特定生命周期事件发生时会通知实现Listener接口的类调用对应回调方法。最常用的是ServletContextListener它监听整个应用的启动和关闭。package com.demo.web.listener; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; WebListener public class AppListener implements ServletContextListener { Override public void contextInitialized(ServletContextEvent sce) { sce.getServletContext().setAttribute(appStartTime, System.currentTimeMillis()); } Override public void contextDestroyed(ServletContextEvent sce) { } }这个监听器的价值在于应用启动时你可以初始化全局配置、加载公共数据、启动后台任务。所有Servlet都能通过getServletContext()获取这些全局属性。注意ServletContext是全局共享的类似“应用级缓存”所有请求、所有Servlet都能访问。在这里放的数据必须考虑并发读写不要放可变对象做频繁修改。Listener和Servlet、Filter共同组成了Servlet规范的三驾马车Servlet处理具体业务Filter过滤请求前后Listener监听生命周期配合起来非常完整。一个纯Servlet工程里如果用了这三个组件代码结构完全能做到职责清晰、条理分明。5.3 回到最原始的JDBC数据访问数据访问是Web工程绕不开的话题。在纯Servlet时代最直接的数据库做法就是JDBC不用ORM不用连接池直接用DriverManager拿连接package com.demo.web.jdbc; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; public class UserDao { private static final String URL jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingUTF-8; private static final String USER root; private static final String PASSWORD 123456; public String findUserNameById(int id) { String sql SELECT name FROM users WHERE id ?; try (Connection conn DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return rs.getString(name); } } } catch (Exception e) { throw new RuntimeException(数据库查询失败, e); } return null; } }这里有几个经验要说明。第一JDBC 4.0之后大部分驱动不再需要Class.forName手动注册只要你把驱动jar放到WEB-INF/lib里DriverManager就能通过SPI自动发现驱动。第二PreparedStatement比Statement更安全能防止SQL注入还能预编译提升性能这是必须养成的习惯。第三上面的代码用try-with-resources自动关闭资源避免忘记关Connection这一点非常重要数据库连接是宝贵资源任何路径都要关闭。如果你在生产环境使用建议换用连接池比如HikariCP只需把对应jar放进WEB-INF/lib并用DataSource代替DriverManager。但在学习阶段用原生JDBC跑通查询流程能让你看清Connection、Statement、ResultSet这三层结构的真实模样也更容易理解后续引入连接池和ORM框架到底解决了什么问题。6. 搭建与运行中的典型坑从404到500的完整排查路径6.1 部署后白屏或404先从上下文路径查起一个war包部署后浏览器访问返回404最常见的根因并不是你写的Servlet错了而是访问地址里的上下文路径不对。上下文路径就是应用在容器中的根路径默认等于war包文件名。如果你打包出来的文件叫my-webapp.war那么访问路径必须是http://localhost:8080/my-webapp/xxx。少了这一层直接访问http://localhost:8080/xxx容器会去根应用里找资源自然找不到。排查这类问题时先问自己三个问题。第一war包解压后的目录名是什么它决定了上下文路径。第二我要访问的Servlet映射路径是什么它必须和上下文路径拼在一起才能访问。第三如果访问根路径出现404可以去$TOMCAT_HOME/logs下看部署日志确认应用是否部署成功。使用开发工具集成部署时上下文路径可能不是war包名而是开发工具里配置的“Application context”或“Deployment context”。这也解释了为什么很多在开发工具里能跑、发布到服务器却404的现象本质上都是上下文路径变了。遇到404先打印一下request.getContextPath()和request.getRequestURI()能快速确认容器看到的真实路径。6.2 改代码不生效编译、重载、缓存三个层面另一种高频问题代码改了重启后还是旧行为怎么排查三个层面逐一过。第一层是编译。如果你手工编译javac没有重新执行或者编译输出目录没有覆盖到WEB-INF/classesclass文件就是旧的。可以先删掉旧的编译输出再重新编译确认时间戳更新。第二层是重载。在开发工具里调试时修改代码后如果不触发热重载容器仍然运行着旧class。war包部署方式更严格部署时容器会解压war包到某目录如果你直接改的是解压后的class重启容器前它可能不会重新解压。稳妥做法是删除旧的应用发布目录和war包把新war包放进去重新启动。第三层是浏览器缓存。静态页面或图片更新不明显很可能是浏览器缓存了响应。可以强制刷新或者用开发者工具勾选禁用缓存。对于接口返回多加点不可变的响应头能减少这类问题。请注意这三层是按成本从低到高排查的先看编译再看服务端最后才怀疑浏览器。6.3 500错误与依赖缺失classpath才是幕后黑手500代表服务器内部错误通常是你的代码抛了异常。纯Servlet工程里出问题最多的三个方向第一类找不到。报ClassNotFoundException或NoClassDefFoundError基本可以断定是classpath问题。你用了某个jar包里的类但jar没有放进WEB-INF/lib或者编译时用的servlet-api版本和运行容器版本不一致。排查时看异常信息里的完整类名再去WEB-INF/lib目录确认对应jar是否存在。第二Servlet映射问题。报Servlet [xxx] was not found说明URL对应的Servlet没有正确注册检查WebServlet的urlPatterns和实际访问路径是否匹配以及web.xml里servlet-class是不是写错了全限定类名。第三空指针或业务异常。这类问题要靠日志定位。手工部署时容器的控制台输出是首要排查入口建议启动容器不要加后台运行参数直接把日志打到前台窗口。生产环境则用日志文件配合栈信息一层层往上追。看到500不要慌错误页面上的异常栈已经告诉了你问题所在先读栈顶再顺着往下找自己的业务代码位置。大多数情况下一个完整栈就能把问题锁死在具体一行。6.4 HTTP状态码与可能原因速查表最后放一个实用的速查表遇到问题先对照状态码缩小范围。这张表是我在实际排查中反复用到的能解决大部分“莫名其妙跑不通”的入门问题状态码含义最常见原因200请求成功无302临时重定向未登录被Filter重定向到登录页或代码主动sendRedirect400请求格式错误URL参数格式不合法请求体不是服务端期待的格式403服务器拒绝执行访问了无权限资源或WEB-INF下资源被浏览器直接访问404资源不存在上下文路径错误、Servlet映射路径错误、静态资源位置不对405方法不被允许请求方法与Servlet实现的doXxx方法不匹配500服务器内部错误代码抛异常最常见503服务不可用容器启动尚未完成或后端依赖不可用如果你遇到405先查代码里到底重写了哪些doXxx方法。很多同学只写了doGet却用POST请求访问接口自然得到405。如果你遇到403先检查是不是把敏感文件放在了web根目录下WEB-INF目录可以避免这个问题所以不要把配置文件和jsp放在web根目录外部避免被直接下载。最后想说的我之前用了很长时间的脚手架工程直到一个偶然机会手动搭了一个纯Servlet的war包部署到服务器才真正把开发流程里的糊涂账彻底看清。后来再看Spring Boot的自动配置会觉得它其实没有多神秘只是在Servlet规范之上做了一套默认策略而已。如果你也想把Web基础补牢建议找一个简单需求例如一个带登录、列表、详情的小系统完全用ServletFilterListenerJDBC徒手实现一遍不要用任何框架。跑通之后再回头学那些带依赖的脚手架工程理解成本会低很多。再分享一个小技巧调试Servlet时不要急着打断点先在所有回调方法里加System.out.println输出通过控制台日志看到方法被调用的顺序和参数然后再精确定位。这套肉眼可见的排查方式比直接上调试器更能帮你建立对容器执行流程的直觉。
延伸阅读

更多相关文章

2026/10/22 3:37:33

端到端自动驾驶算法全解析:从架构原理到工程落地实践

1. 端到端算法到底在解决什么问题1.1 从模块化堆叠到单一网络:一场架构范式的迁移过去做自动驾驶感知决策,主流思路是模块化流水线:感知模块负责检测障碍物、车道线、交通标志,预测模块推断其他交通参与者的未来轨迹,规…

2026/10/22 3:37:33

SSVEP-BCI脑机接口实战:从神经机制到CCA解码与系统搭建

简介:围绕稳态视觉诱发电位(SSVEP)的脑机接口(BCI)项目,提供了一套可运行的完整实现。内容涵盖EEG信号处理、CCA(典型相关分析)、基于幅度谱/复杂谱特征的CNN分类算法,适…

2026/10/22 3:37:33

图神经网络实战:消息传递原理、模型实现与调优技巧

1. 为什么GNN突然成了必须掌握的技能这几年做机器学习相关的工作,应该都感受到了一个明显的变化:越来越多的业务数据,本质上是一张“图”。社交网络里的用户关系、电商场景里的用户和商品交互、风控里面的资金流转、推荐系统里的点击序列、分…

2026/10/22 4:47:42

AI生成原型与D2C设计转代码:从一句话到可交付前端的高效实践

前阵子我被一个紧急需求折磨到凌晨:产品会上临时定下来的新模块,第二天一早要拿去给决策层演示。按老办法,我得先在白纸上排信息架构,再打开设计工具一页页画线框、连接线、标注交互说明,一整套下来至少半天。那天我实…

2026/10/22 4:47:42

国庆AI七天:开源模型、多模态与推理优化实战解析

1. 七天假期,七次心跳:AI圈的节奏为什么停不下来国庆七天假,普通人想着去哪儿玩、怎么避开人潮,而AI圈的人打开手机,发现每天都有新东西冒出来。不是某家发新模型,就是某个开源项目突然冲上热榜&#xff0c…

2026/10/22 4:47:42

NLP系统设计思考:从需求拆解到模型选型的实战指南

自然语言处理设计思考我入行做NLP也有几年了,最大的一个感受是:市面上缺的从来不是更花哨的模型,而是能把系统真正设计好的人。很多项目一上来就奔着“上大模型”“微调BERT”去,结果落在业务里跑不起来,或者指标好看但…

2026/10/22 4:47:42

HaaS506 UART串口驱动实战:从引脚接线到GPS/AT指令数据解析

做物联网开发这些年,我几乎每个项目都离不开 UART 串口。前阵子调试一款基于 HaaS506 开发板的网关设备,GPS 模组的数据流突然中断,我查了半天,最后发现问题出在驱动初始化顺序上——串口对象还没配置完成就开始挂中断读取&#x…

2026/10/22 4:47:42

NLP项目设计实战:从问题定义到系统落地的关键要点

1. NLP项目设计第一步:先定义问题,再谈算法做自然语言处理项目最忌讳什么?我总结就是一句话:还没搞清楚要解决什么问题,就开始选模型、调参数。这种情况在真实的项目里太常见了——某团队接了一个文本分类需求&#xf…

2026/10/22 4:42:42

YOLOv8自定义数据集训练全流程:数据准备、参数调优与部署实践

做目标检测开发这几年,YOLO系列我从v3一路用到v8,说实话,以前给别人讲训练流程,我都是直接甩一段命令让他自己跑。但后来发现,真正卡住新手的往往不是训练那一下,而是训练之前的数据准备和训练之后的结果分…

2026/10/21 4:53:08

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/22 0:01:49

电商智能运营体系从0到1:架构设计、落地步骤与避坑指南

前两年我们团队接了一个电商平台的运营中台项目,目标很直白:把过去靠运营拍脑袋做的选品、定价、发券、推活动这些事,逐步用数据算法和自动化流程代替。项目名字内部就叫“电商智能运营体系”。最近有不少朋友问我当时是怎么设计的、踩过哪些…

2026/10/22 0:01:49

若依框架AI代码生成不稳定?用Agent Skill三层结构+五步法搞定

最近一段时间,我一直想把后台管理系统里那些机械重复的增删改查代码交给AI去写。框架用的是若依,按理说这类业务代码模式非常固定,应该是AI最擅长的事情。可真试下来却不是那么回事——你让AI生成一个用户管理模块,它给出的Contro…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑