news 2026/10/28 5:18:21

Java Web老项目迁移实战:JSP+Servlet全链路排错指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java Web老项目迁移实战:JSP+Servlet全链路排错指南

简介:这是一套基于JSP技术实现的轻量级Web相册管理系统源码,面向Java Web初学者与课程设计实践者,解决照片上传、分类管理、在线浏览与用户交互等典型Web应用需求。资源包共115个文件,含20个JSP页面(负责前端展示与简单逻辑)、22个Java源文件与对应class字节码(涵盖PhotoServlet、AdminServlet等核心业务控制器及PhotoBiz、LeiBieBiz等业务组件)、26张示例图片与5个GIF动图(用于界面占位与效果示意),以及SQL建表脚本、web.xml配置、JDBC连接工厂等关键支撑文件,整体压缩包仅3.53MB,结构清晰、模块职责分明。已有434人学习下载,适合通过完整MVC分层实践掌握JSP+Servlet+MySQL整合开发流程,深入理解数据库连接池、用户权限控制、相册分类与评论功能的代码实现,并可直接部署于Tomcat环境进行本地调试与二次开发。

1. 一个十年前的老项目,现在跑通它反而成了最硬的实战考题

上周帮某高校实验室迁移一批教学 Demo,翻出这个「基于 JSP 的相册管理系统」源码包——压缩包里连web.xml都带着 DTD 声明,<%@ page contentType="text/html;charset=GBK"%>还在 header 里倔强地活着。本以为是过时的玩具,结果部署到 Tomcat 9 时卡在登录跳转死循环,查日志发现是RequestDispatcher.forward()被Filter拦了三次;改完又崩在 MySQL 8 的mysql-connector-java驱动兼容性上,报错java.sql.SQLException: Unknown system variable 'query_cache_size'。折腾六小时才让首页相册缩略图正常加载。这项目不是古董,它是 Java Web 基础链路的「全栈压力测试仪」:从 JSP 编译、Servlet 生命周期、JDBC 连接池、MySQL 字符集、再到 Filter/Listener 协作机制,一环断,全盘停。适合刚学完《Java Web 编程技术》但没真正串起请求链路的开发者,也适合想快速验证老系统迁移风险的运维同学——它不炫技,但每一步都踩在真实生产环境的兼容性刀尖上。


2. 技术栈解剖:为什么用 JSP 而不是 JSF?这个选择决定了整个项目的呼吸节奏

提示:标题里写的「jsf 相册」是典型关键词误标。实际代码中完全没出现javax.faces.*包,所有页面都是.jsp后缀,控制器用的是HttpServlet子类,web.xml中<servlet-class>指向com.album.servlet.LoginServlet。JSF 是组件化框架,而本项目是经典 MVC 三层裸写:JSP 做 View(含大量<%= %>脚本片段),Servlet 做 Controller(处理表单、转发),DAO 层直连 MySQL(无 Hibernate/MyBatis)。这种结构对新手更透明,但对现代 IDE 支持差——IntelliJ 会把 JSP 里的 Java 代码标成红色警告,因为默认不启用 JSP 编译器。

2.1 目录结构即架构:WebContent/下藏着所有运行时真相

项目解压后根目录结构如下(已过滤.idea、target等无关文件):

album-system/ ├── src/ # Java 源码 │ └── com/album/ │ ├── bean/ # Javabean:Album、Photo、User │ ├── dao/ # 数据访问层:AlbumDAO.java、PhotoDAO.java │ ├── servlet/ # 控制器:LoginServlet.java、UploadServlet.java │ └── util/ # 工具类:DBUtil.java(含 getConnection()) ├── WebContent/ # Web 根目录(Tomcat 部署时的 webapp) │ ├── css/ # style.css(仅 3 个 class:.album-list, .photo-item, .upload-form) │ ├── images/ # 用户上传的相册图片(部署后为空) │ ├── js/ # upload.js(含 FormData 提交逻辑) │ ├── WEB-INF/ │ │ ├── web.xml # 关键!定义 Servlet 映射、Filter、Listener │ │ └── lib/ # mysql-connector-java-5.1.38.jar(注意版本!) │ ├── index.jsp # 首页:展示相册列表(调用 AlbumDAO.getAllAlbums()) │ ├── login.jsp # 登录页(form action="/login" → LoginServlet) │ ├── album-detail.jsp # 相册详情页(URL 参数 ?id=123) │ └── upload.jsp # 上传页(含 <input type="file">) └── README.txt # 仅一行:"数据库脚本见 sql/album.sql"

这个结构暴露了两个关键事实:第一,没有构建工具——src/下的 Java 类直接被 Tomcat 的 JSP 编译器和WEB-INF/classes/加载器处理,不存在 Maven 的compile阶段;第二,静态资源与动态资源物理隔离——css/和js/在WebContent/下,而 Java 类在WEB-INF/classes/,符合 Servlet 规范,但导致路径引用极易出错(比如upload.jsp里写<script src="js/upload.js">是对的,但若写成<script src="../js/upload.js">就 404)。

2.2 核心依赖版本:MySQL 5.7 是它的舒适区,跨到 8.x 就要动手术

项目WEB-INF/lib/下只有一份 JDBC 驱动:mysql-connector-java-5.1.38.jar。这是决定能否连上数据库的生死线。我们来对比三个常见场景:

场景MySQL 版本驱动版本结果原因
教学机房环境5.7.215.1.38✅ 正常连接com.mysql.jdbc.Driver类存在,协议兼容
本地 Docker8.0.335.1.38❌ 启动报Unknown system variable 'query_cache_size'MySQL 8 移除了 query_cache 相关变量,旧驱动仍尝试读取
本地 Docker8.0.338.0.33✅ 但需改DBUtil.java驱动类名变为com.mysql.cj.jdbc.Driver,URL 必须加?serverTimezone=UTC

DBUtil.java中的关键代码段如下(原始版):

// src/com/album/util/DBUtil.java(原始) public static Connection getConnection() { Connection conn = null; try { Class.forName("com.mysql.jdbc.Driver"); // ← 这行在 MySQL 8 下会 ClassNotFoundException conn = DriverManager.getConnection( "jdbc:mysql://localhost:3306/albumdb?useUnicode=true&characterEncoding=UTF-8", "root", "123456" ); } catch (Exception e) { e.printStackTrace(); } return conn; }

必须修改两处:

  1. Class.forName("com.mysql.cj.jdbc.Driver")—— 注意cj(Connector/J)后缀;
  2. URL 补全时区参数:"jdbc:mysql://localhost:3306/albumdb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC"。
    漏掉任一,getConnection()返回null,后续 DAO 全部NullPointerException。

2.3 web.xml:那个被现代框架遗忘的“总调度台”

WebContent/WEB-INF/web.xml是本项目真正的中枢神经。它不只注册 Servlet,还通过Filter控制字符编码、用Listener监听应用启动加载数据库连接池(虽然本项目没做池化,但预留了ContextLoaderListener的壳)。截取关键片段:

<!-- web.xml 片段 --> <filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.album.filter.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 注意:这里匹配所有请求 --> </filter-mapping> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.album.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> <!-- 浏览器访问 /login → 走这个 Servlet --> </servlet-mapping> <listener> <listener-class>com.album.listener.AppContextListener</listener-class> </listener>

这个配置意味着:每个 HTTP 请求(包括/css/style.css)都会先经过EncodingFilter。如果EncodingFilter.doFilter()里写了request.setCharacterEncoding("UTF-8"),那对静态资源是无效且多余的,但不会报错;可一旦doFilter()里有response.getWriter().write("hello")这种操作,就会触发IllegalStateException: getWriter() has already been called—— 因为 Tomcat 已经为 CSS 文件打开了输出流。这是新手最容易翻车的点:以为 Filter 只处理 JSP/Servlet,其实它吃下整个/*。


3. 运行实操:从零部署到首页相册列表渲染的七步闭环

部署不是复制粘贴,而是理解每个环节如何咬合。以下步骤在 Windows 10 + IntelliJ IDEA 2023.2 + Tomcat 9.0.83 + MySQL 8.0.33 环境下实测通过(Linux/macOS 命令微调即可)。

3.1 第一步:初始化数据库,字符集必须显式指定

不要依赖 MySQL 默认配置。执行sql/album.sql前,先确认当前库字符集:

-- 进入 MySQL 命令行 CREATE DATABASE albumdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE albumdb; -- 再执行建表语句(album.sql 内容)

注意:utf8mb4而非utf8。MySQL 的utf8实际只支持 3 字节 UTF-8 字符(不支持 emoji),而utf8mb4才是完整 UTF-8。album.sql中建表语句若写CHARSET=utf8,必须手动改为CHARSET=utf8mb4,否则中文插入会变???。

3.2 第二步:替换 JDBC 驱动并修正 DBUtil

  1. 删除WEB-INF/lib/mysql-connector-java-5.1.38.jar;
  2. 下载mysql-connector-java-8.0.33.jar,放入WEB-INF/lib/;
  3. 修改src/com/album/util/DBUtil.java,将Class.forName(...)和 URL 按前文要求更新;
  4. 关键动作:在 IntelliJ 中右键WEB-INF/lib/→Add as Library...,确保编译期能 resolvecom.mysql.cj.jdbc.Driver。

3.3 第三步:配置 Tomcat 的 JVM 参数,绕过 JSP 编译玄学

Tomcat 9 默认使用org.apache.jasper.compiler.JspCompiler,但对老旧 JSP 中的<%! int count = 0; %>这类声明式脚本支持不稳定。在 IntelliJ 的 Tomcat 配置页 →Configuration→VM options中添加:

-Dorg.apache.jasper.compiler.Parser.STRICT_QUOTE_ESCAPING=false -Dorg.apache.jasper.compiler.Generator.SAVE_PRECOMPILED=false

这两项关闭严格引号转义和预编译缓存,能避免index.jsp中<img src="<%= photo.getPath() %>">报org.apache.jasper.JasperException: /index.jsp (line 42) quote symbol expected。

3.4 第四步:解决 JSP 页面的 GBK 与 UTF-8 冲突

原始index.jsp头部是:

<%@ page language="java" contentType="text/html; charset=GBK" pageEncoding="GBK"%>

但数据库和 HTML 页面实际用 UTF-8。必须统一为 UTF-8:

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <!-- 双重保险 -->

否则浏览器解析 HTML 时用 UTF-8,但 JSP 引擎用 GBK 解码<%= album.getName() %>,中文显示为乱码。

3.5 第五步:检查 web.xml 中的 URL 映射是否覆盖全部入口

web.xml中LoginServlet映射为/login,但login.jsp表单写的是:

<form action="login" method="post"> <!-- 注意:没有前置斜杠 -->

这会导致:当用户从http://localhost:8080/album-system/login.jsp提交时,浏览器实际请求http://localhost:8080/album-system/login(相对路径),而 Tomcat 期望的是http://localhost:8080/login(绝对路径)。解决方案只有两个:

  • 改 JSP:<form action="/login" method="post">(加/);
  • 或改 web.xml:<url-pattern>/album-system/login</url-pattern>(不推荐,耦合部署路径)。
    选前者,这是标准实践。

3.6 第六步:启动 Tomcat 并验证日志中的三个关键信号

启动后,紧盯catalina.out(或 IntelliJ Console):

  1. 出现INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory [...]→ 应用开始部署;
  2. 出现SEVERE [main] org.apache.catalina.core.StandardContext.startInternal One or more Filters failed to start.→ Filter 初始化失败,回查EncodingFilter.init();
  3. 出现INFO [main] org.apache.catalina.core.ApplicationContext.log ContextListener: contextInitialized()→AppContextListener启动成功,说明web.xml解析无误。
    没有第 2 条,且第 1、3 条都有,基本可认为部署成功。

3.7 第七步:手工触发一次相册创建,确认数据链路贯通

打开浏览器访问http://localhost:8080/album-system/login.jsp,用默认账号admin/123456登录。登录成功后跳转到index.jsp,此时应看到空相册列表。点击「新建相册」按钮(实际是链接<a href="create-album.jsp">),填写名称「测试相册」提交。此时观察:

  • 数据库album表应新增一条记录,name字段为「测试相册」;
  • index.jsp刷新后,列表中出现「测试相册」;
  • 点击该相册,进入album-detail.jsp,URL 为?id=1,页面显示「暂无照片」。
    这七步走完,证明从浏览器输入、Filter 拦截、Servlet 处理、DAO 写库、JSP 渲染的全链路已打通。任何一步卡住,都不是配置问题,而是对 Servlet 生命周期的理解断点了。

4. 避坑指南:那些让我重启三次 Tomcat 的血泪经验

4.1 现象:登录成功后跳转到空白页,浏览器地址栏显示http://localhost:8080/album-system/,但页面什么也不显示

原因:LoginServlet中response.sendRedirect("index.jsp")是相对重定向,而当前请求 URL 是http://localhost:8080/album-system/login,重定向后变成http://localhost:8080/album-system/index.jsp。但web.xml中未配置index.jsp的 welcome-file,Tomcat 默认只认index.html/index.htm/index.jsp作为根路径欢迎页,而album-system/下的index.jsp需要显式映射。
解决:在web.xml<web-app>根节点内添加:

<welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list>

4.2 现象:上传照片后,images/目录下生成了文件,但album-detail.jsp中<img src="images/xxx.jpg">404

原因:images/是WebContent/下的静态资源目录,按 Servlet 规范,它应该能被直接访问。但 IntelliJ 默认部署时,WebContent/下的images/不会被自动复制到out/artifacts/输出目录。
解决:在 IntelliJ 中,File → Project Structure → Artifacts,展开album-system:war exploded,在Output Layout标签页右侧点击+ → Directory Content,选择项目根目录下的WebContent/images/,确定。这样打包时images/才会进war。

4.3 现象:AlbumDAO.getAllAlbums()返回空集合,但数据库明明有数据

原因:DBUtil.getConnection()返回null,但 DAO 方法里没判空,直接调conn.prepareStatement(...)→NullPointerException。而try-catch把异常吞掉了,只打印堆栈,前端无感知。
解决:在AlbumDAO.java的每个方法开头加防御性检查:

public List<Album> getAllAlbums() { Connection conn = DBUtil.getConnection(); if (conn == null) { // ← 关键防御 System.err.println("DB connection failed in AlbumDAO.getAllAlbums()"); return new ArrayList<>(); } // ... 后续逻辑 }

4.4 现象:upload.jsp中 JavaScript 提交FormData,但UploadServlet的request.getParameter("albumId")总是null

原因:FormData提交的是multipart/form-data编码,getParameter()只能读application/x-www-form-urlencoded数据。albumId是普通字段,但request对象未被MultipartRequest包装。
解决:在UploadServlet的doPost()中,用 Apache Commons FileUpload 解析:

// 需引入 commons-fileupload-1.5.jar 和 commons-io-2.11.0.jar DiskFileItemFactory factory = new DiskFileItemFactory(); ServletFileUpload upload = new ServletFileUpload(factory); List<FileItem> items = upload.parseRequest(request); for (FileItem item : items) { if (item.isFormField()) { String fieldName = item.getFieldName(); // "albumId" String value = item.getString("UTF-8"); // ← 必须指定编码 if ("albumId".equals(fieldName)) { albumId = Integer.parseInt(value); } } }

4.5 现象:修改index.jsp后刷新页面,内容不变

原因:Tomcat 的 JSP 编译缓存未清除。JSP 第一次访问时被编译为org.apache.jsp.index_jsp.java,放在work/Catalina/localhost/album-system/org/apache/jsp/下,即使改了.jsp,只要.java和.class还在,Tomcat 就不重新编译。
解决:停止 Tomcat → 删除work/Catalina/localhost/album-system/整个目录 → 重启。或者,在conf/context.xml中<Context>节点加reloadable="true"(仅开发环境用,性能损耗大)。


5. 二次开发实战:给老系统加「软删除」功能,三步落地不伤筋骨

现在需求来了:运营同学说「删错相册太频繁,希望加回收站功能」。这不是推倒重来,而是在现有 DAO 层打补丁。核心思路是——不动表结构,只加逻辑标记;不动 Servlet 流程,只改 DAO 方法;不碰 JSP 渲染,只增一个「恢复」按钮。全程不改web.xml,不加新 Servlet,不引入新框架。

5.1 第一步:数据库层面,用is_deleted字段实现软删除

album表原结构(来自album.sql):

CREATE TABLE album ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );

执行 SQL 新增字段:

ALTER TABLE album ADD COLUMN is_deleted TINYINT(1) DEFAULT 0 COMMENT '0-未删除, 1-已删除';

注意:TINYINT(1)在 MySQL 中是整数类型,不是布尔型。Java 中用int接收,值为0或1。别用BOOLEAN,那是别名,底层还是TINYINT。

5.2 第二步:DAO 层改造,分离「物理查询」与「逻辑查询」

AlbumDAO.java原getAllAlbums()方法返回所有相册,现在要改成「只查未删除的」。但历史代码如LoginServlet可能依赖「查所有」,不能直接改。所以采用方法重载 + 命名区分:

// src/com/album/dao/AlbumDAO.java // 原方法保留,供内部管理用(比如回收站页面需要查全部) public List<Album> getAllAlbums() { String sql = "SELECT * FROM album ORDER BY create_time DESC"; // ... 执行查询 } // 新增方法:业务主流程用这个 public List<Album> getActiveAlbums() { String sql = "SELECT * FROM album WHERE is_deleted = 0 ORDER BY create_time DESC"; // ... 执行查询(复用原模板代码,只改 SQL) } // 新增软删除方法 public boolean softDeleteAlbum(int albumId) { String sql = "UPDATE album SET is_deleted = 1 WHERE id = ?"; // ... 执行 update,返回影响行数 > 0 则成功 } // 新增恢复方法 public boolean restoreAlbum(int albumId) { String sql = "UPDATE album SET is_deleted = 0 WHERE id = ?"; // ... 执行 update }

关键细节:getActiveAlbums()替换所有业务调用点。全局搜索AlbumDAO.getAllAlbums(),找到index.jsp对应的 Java 脚本段:

<% AlbumDAO dao = new AlbumDAO(); List<Album> albums = dao.getAllAlbums(); // ← 改这里 %>

改为:

<% AlbumDAO dao = new AlbumDAO(); List<Album> albums = dao.getActiveAlbums(); // ← 仅改这一行 %>

5.3 第三步:JSP 层增加「删除/恢复」双态按钮,用 CSS 控制显隐

在index.jsp相册列表项中,原删除按钮是:

<a href="delete-album.jsp?id=<%= album.getId() %>">删除</a>

现在改为条件渲染:

<% if (album.getIsDeleted() == 0) { // 未删除状态 %> <a href="delete-album.jsp?id=<%= album.getId() %>" onclick="return confirm('确定删除?')">删除</a> | <% } else { // 已删除状态 %> <a href="restore-album.jsp?id=<%= album.getId() %>" onclick="return confirm('确定恢复?')">恢复</a> | <% } %> <a href="album-detail.jsp?id=<%= album.getId() %>">查看</a>

同时,为delete-album.jsp和restore-album.jsp新建两个极简 Servlet:

  • DeleteAlbumServlet.java:调AlbumDAO.softDeleteAlbum(id),然后response.sendRedirect("index.jsp");
  • RestoreAlbumServlet.java:调AlbumDAO.restoreAlbum(id),同样重定向。

不用改web.xml,因为这两个 Servlet 的 URL 映射可以复用现有模式:在web.xml中新增:

<servlet> <servlet-name>DeleteAlbumServlet</servlet-name> <servlet-class>com.album.servlet.DeleteAlbumServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DeleteAlbumServlet</servlet-name> <url-pattern>/delete-album</url-pattern> </servlet-mapping> <!-- restore 同理 -->

5.4 验证:三步闭环,确保改动无副作用

  1. 功能验证:登录 → 创建相册 A → 点「删除」→ A 从首页消失 → 访问http://localhost:8080/album-system/index.jsp?showAll=1(可选,加个参数查全部)→ A 显示且带「恢复」按钮 → 点「恢复」→ A 回到首页;
  2. SQL 验证:SELECT * FROM album WHERE id = A_id,确认is_deleted在 0 和 1 之间切换;
  3. 兼容性验证:原有「上传照片」「修改相册描述」等功能是否照常工作——因为 DAO 的updateAlbum()方法没动,它只更新name和description,不碰is_deleted字段。

从那以后我每次给老系统加功能,都强制走一遍「数据库改字段 → DAO 加方法 → JSP 改调用」的三步闭环,绝不跨层操作。改 DAO 时必写单元测试(哪怕只是main方法),改 JSP 时必开浏览器开发者工具看 Network 请求是否 200。这套节奏让我在三个月内交付了 7 个类似的老系统改造需求,没出过一次线上事故。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 5:18:06

OPC DA与UA协议桥接核心组件实战指南

简介&#xff1a;本资源为工业自动化领域OPC标准开发与集成必备的核心组件合集&#xff0c;面向工控软件开发者、SCADA/HMI系统工程师及工业物联网项目实施人员&#xff0c;解决OPC 2.0与3.0双版本环境搭建、跨架构&#xff08;X86/X64&#xff09;部署及COM/DCOM基础服务注册等…

作者头像 李华
网站建设 2026/10/28 5:18:01

误差卡尔曼滤波在VINS-mono中的应用与优化

1. 误差卡尔曼滤波在VINS-mono中的核心价值视觉惯性导航系统&#xff08;VINS&#xff09;需要处理传感器噪声和累积误差这两个致命问题。传统卡尔曼滤波直接对系统状态进行估计&#xff0c;而误差状态卡尔曼滤波&#xff08;Error-State Kalman Filter&#xff0c;ESKF&#x…

作者头像 李华
网站建设 2026/10/28 5:17:18

基于CNN的街景餐饮识别系统设计与优化

1. 项目背景与核心价值这个项目源于我在本地餐饮协会的一次需求调研。当时协会负责人提到&#xff1a;"现在每条商业街都有上百家餐饮店铺&#xff0c;但消费者站在街头根本分不清各家卖什么&#xff0c;能不能用手机拍张街景就自动识别出所有餐馆类型&#xff1f;"这…

作者头像 李华
网站建设 2026/10/28 5:17:14

抽样分布与中心极限定理:统计学的核心基础

1. 从抛硬币到抽样分布&#xff1a;一个直观的入门让我们从一个简单的抛硬币实验开始。假设你有一枚均匀硬币&#xff0c;正面朝上的概率是0.5。如果你抛10次&#xff0c;理论上应该得到5次正面&#xff0c;但实际结果可能是4次或6次。这个差异就是抽样变异的直观体现。抽样分布…

作者头像 李华
网站建设 2026/10/28 5:17:10

t3code:以数据模型为锚点的 T3 栈 CRUD 代码生成实践

1. 我为什么自己折腾了一套 t3code先说背景。干我们这行的&#xff0c;最近两年接的 TypeScript 项目&#xff0c;跑不掉一个组合&#xff1a;Next.js tRPC Prisma Tailwind。这个组合网上叫 T3 Stack&#xff0c;社区模板 create-t3-app 一个命令拉下来&#xff0c;项目能跑…

作者头像 李华
网站建设 2026/10/28 5:16:50

sEMG+触觉反馈融合的神经接口手套技术解析

1. 不是“又一个VR手套”&#xff0c;而是把触觉反馈塞进指尖的物理接口革命最近在几个硬件开发者群里&#xff0c;有人甩出一张图&#xff1a;一只黑色半指手套&#xff0c;掌心嵌着三颗哑光金属圆点&#xff0c;食指根部贴着一枚薄如蝉翼的柔性传感器——配文就一句&#xff…

作者头像 李华