Java Web综合实战:手写Servlet+JDBC三层架构项目
1. 从零到一这个综合案例到底在练什么做 Java Web 开发的人学到一定阶段都会遇到一个尴尬期单个知识点拆开看都懂Servlet 会写、JSP 会写、JDBC 能连数据库、Session 也知道怎么用但真给一个完整需求脑子里就乱了——先写哪一层数据怎么流转页面和后台怎么对接这个综合案例存在的意义就是把这些散落的珠子串成一条链子。我带的几个新人里几乎所有人卡在同一个地方不是不会写代码而是不知道一个真实项目里代码该怎么组织。这个综合案例本质上是一个贯穿式实战训练它把 Java Web 的核心技术栈——Servlet、JSP、JDBC、Session/Cookie、过滤器、监听器——全部揉进一个可运行的业务系统里。做完它你对一个请求从浏览器发出到数据库返回这条完整链路会有肌肉记忆级别的理解。它适合谁适合已经学完 Java 基础、了解 HTML/CSS 基本语法、能写简单 SQL 的人。如果你连HttpServletRequest和HttpServletResponse都没见过建议先把单个知识点过一遍再回来。这个案例不是入门教程它是从会语法到能做项目之间的那座桥。我个人的判断是这个案例的价值不在于最终做出来的系统有多复杂而在于你在做的过程中被迫做的那些决策——为什么用三层架构、为什么 DAO 要抽接口、为什么过滤器要放在那个位置。这些决策背后的思考才是真正能带到工作中的东西。2. 整体架构设计为什么这么分层2.1 三层架构不是教条是踩坑踩出来的刚接触 Java Web 的人最容易犯的错就是把所有逻辑塞进一个 Servlet 里。我见过一个 Servlet 文件超过八百行的代码里面既有参数校验、又有 SQL 拼接、还有页面跳转逻辑。这种代码能跑但改一个需求就要通读全文稍微动一下就可能引发连锁反应。三层架构表现层、业务逻辑层、数据访问层的核心目的就一个让变化的影响范围可控。表现层负责接收请求和返回响应业务层负责处理规则数据层负责和数据库打交道。当数据库表结构变了你只需要改数据层当业务规则变了你只需要改业务层当页面样式变了你只需要改表现层。具体到这个综合案例我建议的包结构是这样的com.example.web ├── controller (Servlet处理请求) ├── service (业务逻辑) │ └── impl (业务实现) ├── dao (数据访问接口) │ └── impl (数据访问实现) ├── entity (实体类对应数据库表) ├── util (工具类如数据库连接) └── filter (过滤器)这个结构看起来简单但每一步都有讲究。比如service和dao都拆了接口和实现有人觉得多此一举——就一个实现类抽接口干嘛我的经验是接口是给未来的自己留的后路。当你需要换一种数据库实现、或者加一层缓存的时候有接口在改动就是新增一个实现类而不是去动所有调用方。2.2 为什么不用框架偏要手写很多人会问都什么年代了为什么不用 Spring Boot 直接上这个问题问得好但答案也很直接手写一遍你才知道框架帮你做了什么。用 Spring Boot 的时候Autowired一写对象就注入了Transactional一加事务就管了。但如果你不知道这些注解背后发生了什么出了问题你连排查的方向都没有。手写这个综合案例你会亲手写数据库连接的获取和释放、亲手控制事务的提交和回滚、亲手做请求参数的封装。做完之后再用框架你看待那些注解的眼光会完全不一样。我自己的经历是先手写了一遍 Servlet JDBC 的完整项目后来学 Spring 的时候几乎没遇到理解障碍。因为我知道DispatcherServlet本质上就是一个大的 Servlet知道IoC容器本质上就是帮我管理了对象的创建和依赖关系。2.3 数据库设计从需求倒推表结构综合案例通常会做一个简单的信息管理系统比如用户管理、商品管理或者订单管理。不管做哪个数据库设计都是第一步也是最容易埋坑的一步。我的习惯是先画页面原型再列字段最后建表。为什么因为页面上的每一个输入框、每一个展示项都对应着数据库里的一个字段。如果你先建表再想页面很容易出现表里有的字段页面上用不到页面上要的字段表里没有的尴尬。以用户管理为例一个最基本的表结构字段名类型说明注意事项idINT AUTO_INCREMENT主键自增不要用业务字段做主键usernameVARCHAR(50)用户名加唯一索引登录用passwordVARCHAR(100)密码存哈希值不要存明文emailVARCHAR(100)邮箱可空注意长度create_timeDATETIME创建时间默认 CURRENT_TIMESTAMP这里有个新手常犯的错用username做主键。用户名是可以改的一旦改了所有关联表的外键都要跟着改维护成本极高。主键用无意义的自增 ID业务字段加唯一约束这是铁律。3. 核心模块拆解与实操要点3.1 数据库连接管理别每次都 new 一个 Connection最原始的 JDBC 写法是每次操作都DriverManager.getConnection()用完close()。这种写法在学习阶段没问题但在实际项目里是灾难——每次建立连接都要经过 TCP 握手、身份验证开销极大。正确的做法是用连接池。连接池的核心思想是预先创建一批连接放在池子里用的时候借用完还而不是频繁创建销毁。常见的连接池有 Druid、HikariCP、C3P0。我一般推荐 Druid因为它的监控功能对排查问题很有帮助。配置连接池的步骤引入连接池的 jar 包或 Maven 依赖编写配置文件druid.properties包含 URL、用户名、密码、初始连接数、最大连接数等写一个工具类在静态代码块中加载配置并初始化连接池提供getConnection()和close()方法供外部调用public class JdbcUtils { private static DataSource dataSource; static { try { Properties props new Properties(); props.load(JdbcUtils.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 依次关闭注意判空和异常处理 } }注意连接池的close()方法不是真的关闭连接而是把连接归还给池子。所以你在 DAO 里调用的close()实际上是还连接不要被方法名误导。这里有个我踩过的坑连接池的最大连接数不是越大越好。数据库能承受的连接数是有限的如果连接池配置了 100 个连接但数据库最多只允许 50 个那多出来的请求就会等待超时。一般根据数据库的max_connections参数和应用的并发量来定小项目 10 到 20 个足够了。3.2 Servlet 的职责边界只做该做的事Servlet 是表现层的核心但很多人的 Servlet 写得又臭又长。我的原则是Servlet 只做三件事——拿参数、调 Service、跳页面。任何业务判断、任何 SQL 语句都不应该出现在 Servlet 里。一个标准的 Servlet 处理流程WebServlet(/user/*) public class UserServlet extends HttpServlet { private UserService userService new UserServiceImpl(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getPathInfo(); if (/list.equals(action)) { list(req, resp); } else if (/delete.equals(action)) { delete(req, resp); } // ... } private void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListUser users userService.findAll(); req.setAttribute(users, users); req.getRequestDispatcher(/WEB-INF/views/user/list.jsp) .forward(req, resp); } }这种写法的好处是一个 Servlet 处理一类资源的请求通过路径区分具体操作。比每个操作写一个 Servlet 要清爽得多。实操心得req.getPathInfo()返回的是 Servlet 映射路径之后的部分。比如WebServlet(/user/*)配WebServlet(/user/list)的请求getPathInfo()返回/list。这个技巧在做 RESTful 风格的 URL 时特别有用。还有一个细节JSP 放在WEB-INF下面。为什么因为WEB-INF目录下的资源浏览器不能直接访问必须通过 Servlet 转发。这样可以防止用户绕过 Servlet 直接访问 JSP避免一些安全问题。3.3 Service 层事务控制的正确姿势Service 层是业务逻辑的聚集地也是事务控制的边界。什么叫事务控制的边界就是说一个 Service 方法应该是一个原子操作要么全部成功要么全部失败。举个典型例子转账操作。A 账户扣钱、B 账户加钱这两步必须在一个事务里。如果扣钱成功了但加钱失败了数据就不一致了。这个事务的控制点就在 Service 层的方法上。手写事务控制的模板public void transfer(int fromId, int toId, BigDecimal amount) { Connection conn null; try { conn JdbcUtils.getConnection(); conn.setAutoCommit(false); // 开启事务 accountDao.deduct(conn, fromId, amount); accountDao.add(conn, toId, amount); conn.commit(); // 提交事务 } catch (Exception e) { try { if (conn ! null) conn.rollback(); // 回滚 } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException(转账失败, e); } finally { JdbcUtils.close(conn, null, null); } }这里的关键点是Connection 对象必须从 Service 层传递到 DAO 层。因为事务是绑定在 Connection 上的如果 DAO 层自己从连接池拿了一个新的 Connection那就不在同一个事务里了。这就是为什么 DAO 方法的参数里要带一个Connection。踩坑记录我见过有人在 DAO 里用ThreadLocal存 Connection然后在 Service 里设置和获取。这种写法在单线程下没问题但一旦用了线程池或者异步处理就会出现连接错乱。对于初学者老老实实把 Connection 当参数传最稳妥。3.4 过滤器统一处理编码和登录校验过滤器Filter是 Java Web 里非常实用的一个组件它可以在请求到达 Servlet 之前、响应返回浏览器之前做一些统一处理。最典型的两个应用场景是编码统一和登录校验。编码过滤器解决的是中文乱码问题。虽然现在大部分项目都用 UTF-8但如果不显式设置POST 请求的中文参数在某些服务器上还是会乱码。一个编码过滤器长这样WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); // 放行 } }登录校验过滤器则是拦截未登录的请求把它们重定向到登录页。这里有个细节登录页本身和登录接口不能拦截否则会陷入死循环。所以需要判断请求路径放行白名单。WebFilter(/*) public class LoginFilter implements Filter { private static final ListString WHITE_LIST Arrays.asList( /login.jsp, /login, /css/, /js/, /images/ ); Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getRequestURI().substring( request.getContextPath().length()); // 白名单放行 for (String white : WHITE_LIST) { if (path.startsWith(white)) { chain.doFilter(req, resp); return; } } // 检查登录状态 Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }注意过滤器的执行顺序由web.xml中的配置顺序或注解的Order决定。编码过滤器一定要放在最前面否则后面的过滤器拿到的参数可能已经是乱码了。4. 完整实操流程从建库到跑通4.1 环境准备与项目骨架搭建动手之前先把环境理清楚。我用的组合是JDK 8 或 11、Tomcat 8.5 或 9、MySQL 5.7 或 8.0、IDEA 作为开发工具。这个组合比较稳定踩坑少。项目骨架的搭建步骤在 IDEA 里创建一个 Java Enterprise 项目或者创建一个普通 Java 项目再手动配置 Web 支持在pom.xml里引入依赖javax.servlet-api、javax.servlet.jsp-api、mysql-connector-java、druid、jstl配置web.xml的版本和编码创建包结构controller、service、dao、entity、util、filter在src/main/resources下放druid.properties依赖版本的选择有个小技巧Servlet API 的版本要和 Tomcat 的版本匹配。Tomcat 9 对应 Servlet 4.0Tomcat 8.5 对应 Servlet 3.1。如果版本不匹配可能会遇到ClassNotFoundException或者方法找不到的问题。4.2 实体类与 DAO 层的编写实体类就是数据库表的 Java 映射字段名和表字段一一对应提供 getter/setter、无参构造、toString()。这里有个细节实体类不要用基本类型用包装类型。比如用Integer而不是int因为数据库里的字段可能是 NULL基本类型无法表示 NULL。DAO 层的编写有个通用模式我一般会写一个BaseDao抽象类把增删改查的通用逻辑抽出来public abstract class BaseDao { protected int update(String sql, Object... params) { try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(更新失败, e); } } protected T ListT queryList(String sql, RowMapperT mapper, Object... params) { // 查询逻辑用 RowMapper 做结果集映射 } }RowMapper是一个函数式接口负责把ResultSet的一行转成一个对象。这样不同的 DAO 只需要提供不同的映射逻辑查询的通用流程不用重复写。实操心得PreparedStatement不仅性能好预编译还能防止 SQL 注入。永远不要用字符串拼接来构造 SQL尤其是参数来自用户输入的时候。我见过一个登录功能用SELECT * FROM user WHERE username name 拼接结果输入 OR 11就能直接登录这是非常低级的错误。4.3 业务层与事务的落地Service 层的实现类里每个方法都要考虑事务问题。对于只读操作查询不需要事务对于写操作增删改必须加事务。我一般会在 Service 实现类里注入 DAO 对象然后按照获取连接、开启事务、调用 DAO、提交或回滚、释放连接的模板来写。为了减少重复代码可以写一个TransactionTemplate工具方法public class TransactionTemplate { public interface Callback { void execute(Connection conn) throws SQLException; } public static void execute(Callback callback) { Connection conn null; try { conn JdbcUtils.getConnection(); conn.setAutoCommit(false); callback.execute(conn); conn.commit(); } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } throw new RuntimeException(e); } finally { JdbcUtils.close(conn, null, null); } } }用的时候只需要把业务逻辑写在回调里public void transfer(int fromId, int toId, BigDecimal amount) { TransactionTemplate.execute(conn - { accountDao.deduct(conn, fromId, amount); accountDao.add(conn, toId, amount); }); }这种写法把事务的模板代码和业务代码分离了看起来清爽很多。4.4 表现层与页面联调表现层包括 Servlet 和 JSP。Servlet 负责接收请求、调用 Service、把数据放进 request 域、转发到 JSP。JSP 负责展示数据尽量少写 Java 代码用 EL 表达式和 JSTL 标签。一个典型的列表页面 JSP% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle用户列表/title/head body table tr thID/thth用户名/thth邮箱/thth操作/th /tr c:forEach items${users} varuser tr td${user.id}/td td${user.username}/td td${user.email}/td td a href${pageContext.request.contextPath}/user/edit?id${user.id}编辑/a a hrefjavascript:del(${user.id})删除/a /td /tr /c:forEach /table /body /html联调的时候最容易出问题的地方是路径。${pageContext.request.contextPath}获取的是应用的上下文路径比如/myapp。所有链接和表单提交的 action 都要加上这个前缀否则在非根路径部署时会 404。踩坑记录有一次部署到 Tomcat 上页面能打开但所有链接都 404。排查了半天发现是链接写成了/user/list而应用部署在/myapp下正确路径应该是/myapp/user/list。从那以后我养成了习惯所有路径都用${pageContext.request.contextPath}开头。5. 常见问题与排查技巧实录5.1 中文乱码三种场景三种解法中文乱码是 Java Web 开发中出现频率最高的问题没有之一。它出现的场景主要有三种每种的原因和解法都不一样。场景一POST 请求参数乱码。原因是 Tomcat 默认用 ISO-8859-1 解码请求体。解法是在读取参数之前设置request.setCharacterEncoding(UTF-8)。注意必须在第一次调用getParameter()之前设置否则无效。场景二GET 请求参数乱码。GET 请求的参数在 URL 里Tomcat 8 以后默认用 UTF-8 解码一般不会乱码。如果用的是 Tomcat 7 或更早版本需要在server.xml的 Connector 标签上加URIEncodingUTF-8。场景三响应内容乱码。原因是浏览器不知道用什么编码解析响应。解法是response.setContentType(text/html;charsetUTF-8)。对于 JSP 页面还要在 page 指令里加contentTypetext/html;charsetUTF-8。乱码场景原因解决方案POST 参数请求体解码方式不对request.setCharacterEncoding(UTF-8)GET 参数URL 解码方式不对Connector 加URIEncodingUTF-8响应内容浏览器解析编码不对response.setContentType(...)JSP 页面页面编码声明缺失page 指令加contentType5.2 数据库连接泄漏最隐蔽的性能杀手连接泄漏的表现是系统运行一段时间后所有数据库操作都卡住日志里出现等待连接超时。原因是借出去的连接没有归还连接池里的连接被耗尽。排查连接泄漏的方法在连接池配置里开启泄漏检测。Druid 可以配置removeAbandonedtrue和removeAbandonedTimeout180意思是连接借出超过 180 秒还没归还就强制回收并打印堆栈。通过堆栈就能定位到是哪段代码忘了关连接。预防连接泄漏的根本方法是用 try-with-resources。从 Java 7 开始实现了AutoCloseable接口的资源可以放在 try 的括号里代码块结束时自动关闭不用手写 finally。try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // 使用 conn 和 ps } catch (SQLException e) { // 异常处理 } // 这里 conn 和 ps 已经自动关闭了实操心得我现在的习惯是只要看到getConnection()就条件反射地想到这个连接在哪里关。如果一段代码里拿了连接但没有对应的关闭逻辑那基本可以断定有泄漏。5.3 事务不生效八成是连接不一致事务不生效的典型表现是明明在 Service 里开启了事务但出了异常数据还是写进去了。原因通常是Service 和 DAO 用的不是同一个 Connection。前面说过事务是绑定在 Connection 上的。如果 Service 从连接池拿了一个 Connection 并开启了事务但 DAO 内部又自己从连接池拿了一个新的 Connection那 DAO 的操作就不在事务范围内。排查方法在 Service 和 DAO 里分别打印conn.hashCode()如果两个值不一样就说明用的不是同一个连接。解法就是前面说的把 Connection 从 Service 传到 DAO。还有一个容易忽略的点MySQL 的存储引擎必须是 InnoDB。MyISAM 引擎不支持事务如果你建表的时候没指定引擎默认可能是 MyISAM那事务怎么配都不会生效。建表时加上ENGINEInnoDB。5.4 404 与 500快速定位思路404 是找不到资源500 是服务器内部错误。这两个错误的排查思路完全不同。404 的常见原因URL 写错了大小写、拼写、路径前缀Servlet 的WebServlet注解值写错了项目没有正确部署检查 Tomcat 的webapps目录web.xml里配置了错误的url-pattern500 的常见原因空指针异常最常见类型转换异常SQL 语法错误类找不到依赖缺失排查 500 的关键是看异常堆栈。堆栈的第一行是异常类型和消息往下是调用链最下面通常是你自己的代码。从下往上看找到第一个属于你项目的类问题基本就在那里。踩坑记录有一次遇到 500堆栈显示NullPointerException但那一行代码看起来没问题。后来发现是request.getParameter()返回了 null直接调用了.trim()。从那以后我养成了习惯所有getParameter()的返回值都先判空再使用。5.5 常见问题速查表问题现象可能原因排查方向页面 404路径错误、Servlet 未注册检查 URL、注解、web.xml页面 500空指针、SQL 错误看异常堆栈定位到自己的代码中文乱码编码设置缺失检查 request/response 编码连接超时连接泄漏开启泄漏检测检查 close事务不生效连接不一致、引擎不对打印 hashCode检查表引擎数据查不到条件错误、事务未提交打印 SQL检查事务提交页面样式丢失静态资源路径错误检查 contextPath 前缀6. 从能跑到能看代码优化的几个方向6.1 把重复代码抽干净项目跑通之后回头看看代码你会发现很多重复。比如每个 Servlet 里都有获取参数、调用 Service、转发页面的流程每个 DAO 里都有获取连接、执行 SQL、关闭资源的模板。优化的方向是抽取基类。Servlet 可以抽一个BaseServlet用反射根据请求路径调用对应的方法这样每个子 Servlet 只需要写业务方法不用再写doGet/doPost的分发逻辑。DAO 可以抽一个BaseDao把增删改查的通用逻辑封装起来。这种重构的好处不只是代码变短了更重要的是修改点集中了。比如要加一个统一的日志记录只需要在基类里加一次所有子类都生效。6.2 参数校验前置新手容易把参数校验散落在各处Servlet 里校验一次、Service 里又校验一次、DAO 里还校验一次。这种重复不仅浪费代码还容易出现校验规则不一致的问题。我的做法是在 Service 层做统一的参数校验。Servlet 只负责收集参数不做业务判断DAO 只负责数据存取不做规则校验。Service 是业务规则的唯一入口所有校验都在这里做。校验失败的时候抛一个自定义的BusinessException里面带上错误信息。Servlet 捕获这个异常把错误信息放进 request 域转发回原来的页面。这样用户能看到友好的错误提示而不是一个 500 错误页。6.3 日志代替 System.out学习阶段用System.out.println打印调试信息没问题但项目稍微正式一点就应该用日志框架。日志框架的好处是可以控制输出级别DEBUG/INFO/WARN/ERROR、可以输出到文件、可以按时间切割、可以配置不同的输出格式。我一般用 SLF4J Logback 的组合。在pom.xml里引入依赖在src/main/resources下放一个logback.xml配置文件然后在代码里用LoggerFactory.getLogger()获取 logger 对象。private static final Logger logger LoggerFactory.getLogger(UserServiceImpl.class); public User findById(int id) { logger.debug(查询用户id{}, id); User user userDao.findById(id); if (user null) { logger.warn(用户不存在id{}, id); } return user; }日志级别要合理使用DEBUG 用于调试细节INFO 用于关键流程WARN 用于可恢复的异常ERROR 用于需要立即关注的错误。生产环境一般把级别调到 INFO 或 WARN避免日志文件膨胀太快。6.4 密码不能存明文这是安全底线但我在很多学习项目里看到密码直接存明文。正确的做法是存哈希值而且要用加盐的哈希。最简单的做法是用 MD5 加盐但 MD5 已经被证明不够安全了。现在推荐用 BCrypt它会自动生成盐值并嵌入到哈希结果里验证的时候不需要单独存盐。// 注册时 String hashed BCrypt.hashpw(rawPassword, BCrypt.gensalt()); user.setPassword(hashed); // 登录时 if (BCrypt.checkpw(inputPassword, user.getPassword())) { // 登录成功 }BCrypt 的另一个好处是计算速度慢。慢是故意的因为慢可以增加暴力破解的成本。MD5 计算太快攻击者每秒可以尝试上亿次而 BCrypt 每秒只能尝试几千次。7. 这个案例做完之后下一步往哪走做完这个综合案例你应该对 Java Web 的完整链路有了实感。接下来有几个方向可以深入方向一引入框架。把手写的 Servlet 换成 Spring MVC把手写的 JDBC 换成 MyBatis 或 JPA。你会发现框架帮你省掉了大量模板代码但底层原理你已经通过手写搞清楚了用起来心里有底。方向二前后端分离。把 JSP 换成纯 HTML AJAX后端只提供 JSON 接口。这是现在主流的开发模式你需要学习如何设计 RESTful 接口、如何处理跨域、如何做接口鉴权。方向三加缓存和消息队列。当系统有一定并发量的时候数据库会成为瓶颈。引入 Redis 做缓存、引入消息队列做异步处理是提升性能的常见手段。方向四部署和运维。把项目打包成 WAR 或可执行 JAR部署到服务器上配置 Nginx 做反向代理用 Docker 做容器化。这些技能在实际工作中非常实用。我个人的建议是先把框架方向走通再考虑其他。因为框架是日常开发的基础设施不掌握框架后面的缓存、消息队列、微服务都无从谈起。而手写这个综合案例的经历会让你在学习框架的时候少走很多弯路——你知道框架在帮你做什么也知道框架出问题的时候该往哪里查。最后分享一个我自己的习惯每做完一个项目我都会写一份踩坑记录把遇到的问题、排查过程、最终解法记下来。这份记录在下次遇到类似问题的时候能帮我节省大量时间。这个综合案例涉及的知识点很密集踩坑记录会特别有价值建议你也养成这个习惯。

相关新闻

Keras回归实战:波士顿房价预测与分类任务差异解析

Keras回归实战:波士顿房价预测与分类任务差异解析

1. 从房价预测切入:为什么回归任务是深度学习的另一条腿很多人入门深度学习,第一个接触的都是分类任务——手写数字识别、猫狗分类、情感判断,输出的是离散的类别标签。但真实世界里大量问题要的不是“属于哪一类”,而是“值是多少…

2026/10/15 4:56:06 阅读更多 →
基于Spring Boot的医考答题系统:题库管理、自动评分与错题本实战解析

基于Spring Boot的医考答题系统:题库管理、自动评分与错题本实战解析

做医考答题系统这个项目,前后折腾了大概一个多月。从最开始接到需求、梳理业务,到最终把整套Spring Boot系统跑起来并完成部署交付,中间踩了不少坑,也积累了一些实实在在的经验。今天就把这套医考答题练习系统的设计思路、核心实现…

2026/10/15 4:55:06 阅读更多 →
C++机器学习库选型与部署实践:dlib、mlpack与Tiny-DNN深度解析

C++机器学习库选型与部署实践:dlib、mlpack与Tiny-DNN深度解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 4:55:06 阅读更多 →

最新新闻

Vue项目实现Office文档在线预览:DOCX、XLSX、PPT一次搞定

Vue项目实现Office文档在线预览:DOCX、XLSX、PPT一次搞定

做前端这几年,被文档预览折磨的次数真不算少。之前接一个合同管理需求,用户提了个特别朴素的要求:附件里的Word合同不要下载到本地再打开,在网页里直接能看就行。我一开始也心想,这有什么难的?不就是嵌一个…

2026/10/15 5:37:22 阅读更多 →
C00系统PLC梯形图维修指南:钻攻中心信号排查与修改实战

C00系统PLC梯形图维修指南:钻攻中心信号排查与修改实战

简介:面向日本兄弟钻攻中心C00系统的操作与维护人员,这是一份系统讲解PLC(可编程逻辑控制器)应用的官方技术手册,内容由浅入深,特别适合现场工程师和培训学员快速掌握设备控制逻辑。手册从PLC基础概念与系统…

2026/10/15 5:37:22 阅读更多 →
PyTorch核心机制与实战:动态图、自动微分、显存优化及多卡训练

PyTorch核心机制与实战:动态图、自动微分、显存优化及多卡训练

1. 从一次模型训练卡死说起:为什么值得重新认识PyTorch很多人第一次接触深度学习框架,都是从一行import torch开始的。但真正让PyTorch在众多框架中站稳脚跟的,不是它简洁的API,而是它背后那套"动态图自动微分"的组合拳…

2026/10/15 5:37:22 阅读更多 →
PyTorch深度学习实战:从张量到分布式训练与部署全链路指南

PyTorch深度学习实战:从张量到分布式训练与部署全链路指南

1. 从一张显卡到一套完整训练链路:我为什么把PyTorch当作深度学习的主力框架如果你刚接触深度学习,或者已经用过其他框架但总觉得“跑得起来却调不明白”,那PyTorch大概率是你绕不开的一站。它不是一个“点一下就能出结果”的黑盒工具&#x…

2026/10/15 5:37:22 阅读更多 →
兄弟钻攻中心C00系统PLC手册:地址、梯形图与维修实战

兄弟钻攻中心C00系统PLC手册:地址、梯形图与维修实战

简介:日本兄弟钻攻中心C00系统PLC系统手册是针对S500X1/S700X1机型的技术文档,专供经过认证或培训的操作与维护人员使用,用于掌握钻攻中心PLC程序编辑、调试与故障排查。内容从PLC基础概念、系统连接示意图讲起,再进入PLC功能操作…

2026/10/15 5:37:21 阅读更多 →
LLM request timed out排查指南:OpenClaw接入第三方API超时解决方案

LLM request timed out排查指南:OpenClaw接入第三方API超时解决方案

LLM request timed out 这个报错,接第三方 API 的朋友应该不陌生。OpenClaw 本身是个开源智能体框架,跑起来很轻,但一旦把模型源切到第三方兼容接口,超时就像定时炸弹一样时不时爆一下。这篇文章把我自己调通的全过程写出来&#…

2026/10/15 5:36:21 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/14 3:29:58 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 7:53:34 阅读更多 →