老实说,像“JavaWeb_09”这种编号式的项目标题,懂的都懂,这多半是某套培训课程或者自学体系里第九个实战节点。这个阶段卡得特别有意思:前面几章还在学静态页面、单个Servlet、简单的JDBC查询,到了09,所有零碎的知识点开始“合流”——你得用IDEA把一个Web项目完整跑起来,前端表单发请求,后端Servlet接数据,Service层做业务判断,DAO层操作MySQL,最后再返回动态页面。这个链路一旦走通,JavaWeb基本就入门了;走不通,卡三天都算正常。
这篇文章我就拿一个非常典型的案例来拆解——“员工管理系统(EMS)”,围绕它把IDEA配置、项目结构、数据库设计、核心编码、以及各种“玄学报错”全部串一遍。写得比较细,适合刚学完Servlet和MySQL基础、准备动手做第一个完整案例的读者,也适合那些项目能跑但不知道每行配置为什么这么写的“半懂型”选手。
1. 案例定位与整体架构:为什么每个JavaWeb案例都长这样
1.1 核心需求解析:管理系统的“增删改查”才是万变不离其宗
你去看网上那些“JavaWeb项目完整案例MySQL”的成品,十个里有八个是管理系统——学生管理系统、图书管理系统、员工管理系统、订单管理系统。名字千奇百怪,需求本质就一句话:对一张或者几张业务表做增删改查,再加上一个登录验证。
以“员工管理系统”为例,需求拆解开无非是:
- 用户通过登录页面输入用户名密码,后端校验通过后才能进入主界面。
- 主界面展示员工列表,支持按姓名模糊查询或按部门筛选。
- 点击“新增”跳转表单页,填写员工信息(姓名、性别、部门、薪资等)后提交保存。
- 列表页每条记录后带“编辑”“删除”按钮,对应更新和删除操作。
- 所有操作反馈到MySQL数据库,页面数据实时刷新。
别嫌它土。增删改查是整个Web应用的骨架,JavaWeb阶段做得越熟练,后面学框架(Spring MVC、MyBatis)时就越轻松。因为底层的路径分发、参数封装、请求转发、重定向这些思路,框架只是帮你“包装”了,本质完全一样。
1.2 技术选型:为什么不直接用框架,而要手写Servlet+JSP
在这个阶段刻意“不用框架”,是为了搞清楚Web应用的最底层流转方式。选型上,最稳妥的组合是:
| 技术组件 | 具体选型 | 选型理由 |
|---|---|---|
| 开发工具 | IntelliJ IDEA(Community或Ultimate均可) | 社区版免费,配置Tomcat和Maven都方便 |
| JDK版本 | JDK 8或JDK 11 | 兼容性最好,很多老教程和依赖都基于这两个版本 |
| Web服务器 | Tomcat 8.5/9.x | 经典Servlet容器,原生支持Servlet规范 |
| 依赖管理 | Maven | 方便引入MySQL驱动、Jackson等库,避免手动塞jar包 |
| 数据库 | MySQL 5.7/8.0 | 主流、免费,案例资料多 |
| 后端 | Servlet(注解版,不用web.xml配置) | 3.0以上支持@WebServlet注解,配置量最小 |
| 前端 | JSP + JSTL + EL表达式 | 服务端渲染,适合JavaWeb阶段的学习曲线 |
| JDBC | 数据库连接池(Druid或C3P0) | 直接加载DriverManager太浪费连接,连接池是生产级标配 |
之所以保留JSP而不搞前后端分离,是因为这个阶段的重心是JavaWeb的整链路打通,而不是Vue+接口设计。JSP能直观看到数据怎么从数据库流到了HTML标签里,这对建立全局视图极有帮助。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. IDEA运行JavaWeb项目的配置:这一步做不好,后面全是白搭
2.1 Maven骨架与web模块的初始化
创建项目时不建议直接选“Java Enterprise”里的Web模板,那个向导有时候会夹带私货(比如帮你勾了一堆用不上的依赖)。我更推荐空白Maven项目手动转Web工程,路径更清晰,出了问题也容易定位。
操作步骤是:
- IDEA里选择 File → New → Project → Maven,不勾选任何骨架,GroupId填
com.example,ArtifactId填employee-system。 - 项目生成后,对着项目根目录右键 → Add Framework Support → 勾选 Web Application。
- 确认
src/main/java、src/main/resources、src/main/webapp三个目录已经就位。如果资源目录缺失,手动创建并在pom.xml里做目录映射。
最终 pom.xml 的依赖区域至少要包含这么几样:
xml复制<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
<!-- JSP 相关 -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.3</version>
<scope>provided</scope>
</dependency>
<!-- JSTL 标签库 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- MySQL 驱动(注意版本与本地库一致) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 阿里巴巴 Druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
<!-- Jackson 用于 JSON 响应(搜索接口用) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
</dependencies>
依赖版本别追新,能用就行。特别提醒:Servlet API 和 JSP API 在 Tomcat 里已经带了,依赖上加 <scope>provided</scope> 防止和容器自带的冲突,这个问题不处理,运行时会出各种双jar包混乱的诡异报错。
2.2 Tomcat集成与Artifact管理的关键点
IDEA里集成Tomcat,路径是 Run → Edit Configurations → 左上角“+” → Tomcat Server → Local。如果你这边看不到“Tomcat Server”选项,通常是缺少Smart Tomcat插件,或者IDEA版本和Tomcat版本不匹配。更稳的办法是直接装一个 Smart Tomcat 插件(插件市场搜“Smart Tomcat”,作者是陈老二),这个插件对IDEA社区版极其友好,配置简单且热部署敏感度高于官方集成方式。
使用Smart Tomcat时需要注意几个拦路坑:
告诉我文本的进步在哪里,缺失了什么,关系、矛盾、多重视角如何破解的,给予分析,启迪
-
Deployment(部署包)选择:官方Tomcat配置中,Deployment要选 “
employee-system:war exploded”而不是war包。war exploded模式会把项目编译输出目录直接映射到Tomcat的webapps下,改代码后浏览器刷新立即可见,这对调试体验的改善是决定性的。war整包需要重新构建再部署,每次改Java代码都要等半天。 -
Application context(虚拟路径):强烈建议把
/employee-system_war_exploded这种又臭又长的路径改成/ems。改法是:Run/Debug Configurations → Deployment → Application context,手敲/ems。这样浏览器访问地址就是http://localhost:8080/ems/login.jsp,简洁好记,也降低因为路径拼错导致的404频率。 -
Libraries(依赖库):在 Artifact 设置中,你需要在右侧“Available Elements”里找到
Maven: mysql-connector-java-8.0.33等所有依赖,双击把它们加入左侧/WEB-INF/lib目录。这一步不做,项目大概率会运行时报ClassNotFoundException: com.mysql.cj.jdbc.Driver。这是新手最容易忽视、报错后最懵的一个点,因为编译阶段是正常的——IDEA编译用的是Maven依赖,而Tomcat启动时用的是Artifact里的lib清单,两个清单默认不同步。
2.3 JSP页面与静态资源的目录保护
Web项目目录中有一个安全问题很值得注意:webapp 下直属的 JSP,比如 login.jsp、register.jsp,都是可以直接被浏览器URL访问的。但 WEB-INF 目录下的JSP则默认受容器保护,外部无法直接通过URL访问,只能通过Servlet内部转发(forward)来渲染。
所以我个人的习惯是:
text复制webapp
├── static/
│ ├── css/
│ ├── js/
│ └── images/
├── WEB-INF/
│ ├── pages/
│ │ ├── login.jsp
│ │ ├── employeeList.jsp
│ │ └── employeeEdit.jsp
│ └── web.xml
└── index.jsp
login.jsp 放 WEB-INF 下之后,访问方式变了:页面加载要靠 LoginServlet 转发,直接在浏览器敲 login.jsp 路径反而404。这样设计能避免一些业务页面被绕过权限校验后直接打开,也顺便熟悉了请求转发的用法。还有个隐藏好处:WEB-INF 下的JSP在IDEA里编写时能自动识别为webapp资源目录,不会报一堆红色波浪线的虚拟路径错误。
3. 数据库设计与DAO层的“地基”工程
3.1 建库建表与连接配置的初始化步骤
数据库是JavaWeb项目的“地基”。建库时字符集必须提前锁定为 utf8mb4,否则后面页面传中文姓名进MySQL,在控制台和数据表里看着正常,JSP显示页面上就成了乱码。utf8mb4兼容性远好于单纯的utf8,现在MySQL 8.0默认字符集已是utf8mb4,习惯性地在create database语句里显式声明,能避开老数据库升级后的一堆坑。
员工管理案例,两张表足够:
sql复制-- 部门表
CREATE TABLE department (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 员工表
CREATE TABLE employee (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
gender CHAR(1) DEFAULT '男',
department_id INT,
salary DECIMAL(10,2),
entry_date DATE,
FOREIGN KEY (department_id) REFERENCES department(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 管理员表(登录验证)
CREATE TABLE admin (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(64) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
密码字段别用明文存储,哪怕只是学习项目,写个SHA-256哈希也花不了几分钟。MySQL里可以直接用 SHA2('123456', 256) 生成哈希值插入,将来登录时就拿输入密码做同样哈希再比对,养成习惯,后面用BCrypt之类的框架就不陌生了。
3.2 数据库连接池配置:为什么用Druid而不是DriverManager
早期教程里连接数据库都是:
java复制Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, user, password);
这段代码每执行一次SQL就创建一个物理连接,用完直接丢弃。几十个并发请求就能把MySQL的连接数打满,数据库会报 Too many connections。真实项目里没人这么干,用的就是连接池——把数据库连接复用起来,用的时候借,用完还回池子里,避免频繁建连断开。
Druid配置方式推荐外置配置文件,把参数和Java代码解耦。在 src/main/resources 下新建 druid.properties:
properties复制driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/employee_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
username=root
password=你的密码
initialSize=5
maxActive=20
maxWait=3000
validationQuery=SELECT 1
然后写一个工具类 DBUtil:
java复制public class DBUtil {
private static DataSource dataSource;
static {
try (InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("druid.properties")) {
Properties props = new Properties();
props.load(in);
dataSource = DruidDataSourceFactory.createDataSource(props);
} catch (Exception e) {
throw new ExceptionInInitializerError(e);
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void close(ResultSet rs, Statement stmt, Connection conn) {
if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } }
if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } }
if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } }
}
}
useSSL=false 是因为本地开发压根没配SSL证书,开着反而会反复警告;serverTimezone=Asia/Shanghai 是因为MySQL 8.0的时区默认是UTC,不显式指定的话,Java程序写入DATETIME类型字段的时候时间会比北京时间早8个小时。这俩参数是“不配不知道,配了忘不掉”的典型。
3.3 DAO层与PreparedStatement的防注入必修课
DAO层写SQL查询时,任何涉及用户输入的查询条件,一律用参数占位符,禁止字符串拼接。这一点是教学项目里最常见的坏习惯,直接导致SQL注入风险。
反例(绝对不要这么写):
java复制String sql = "SELECT * FROM employee WHERE name LIKE '%" + keyword + "%'";
正例:
java复制public List<Employee> searchByName(String keyword) {
String sql = "SELECT * FROM employee WHERE name LIKE ?";
try (Connection conn = DBUtil.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, "%" + keyword + "%");
try (ResultSet rs = ps.executeQuery()) {
List<Employee> list = new ArrayList<>();
while (rs.next()) {
Employee e = new Employee();
e.setId(rs.getInt("id"));
e.setName(rs.getString("name"));
// ...
list.add(e);
}
return list;
}
} catch (SQLException e) {
throw new RuntimeException("查询员工失败", e);
}
}
JDBC规范里 PreparedStatement 的预编译不仅防注入,在多次执行相同SQL时还有缓存效率优势。JavaWeb的这些细节,到后面的MyBatis框架中本质上就是这些概念的封装——底层还是PrepareStatement那一套。在学习阶段亲手把连接、语句、结果集的关闭流程走通,后面学MyBatis时的“#{}的预编译原理”强调的是同一个东西。
4. Servlet层与JSP页面的“传参接力”
4.1 数据流转设计:一次查询请求,底层代码是怎么跑的
从浏览器点击“查询”开始,一次完整的JavaWeb数据流转是这样的:
- 浏览器向
EmployeeListServlet发送GET请求(带查询参数keyword)。 - Servlet调用
employeeService.search(keyword),把请求参数传给Service层。 - Service层避免直接写SQL,而是调用DAO层接口。
- DAO层拿着PreparedStatement去数据库执行查询,把结果集的每条记录封装成Employee对象,放进List集合返回。
- Servlet把得到的List放进request域:
request.setAttribute("employees", list); - Servlet执行
request.getRequestDispatcher("WEB-INF/pages/list.jsp").forward(request, response);,把请求转发给JSP。 - JSP通过JSTL标签遍历List,拼出
<tr>表格行,输出成HTML返回浏览器。
这个链路里最值得琢磨的是第6步:为什么用 forward 而不是 sendRedirect。
forward:服务端内部跳转,浏览器地址栏不变。request对象里放的数据在JSP里直接能取,整个过程一次请求,一次响应。sendRedirect:浏览器收到302状态码后再发起第二次请求,地址栏会改变。request域里的数据在新的请求中丢失,必须把参数再拼到URL上传递。
查询列表用forward天经地义。但如果是“新增保存成功后跳转列表”,反而要用sendRedirect——因为如果不重定向,用户按F5刷新就会重复提交POST请求,造成数据库里出现两条一模一样的记录。这就是经典的“表单重复提交”问题,用重定向规避比任何前端按钮置灰都保险。
4.2 三层封装的重要性:别把所有代码堆进Servlet
很多自学案例代码是这样的:Servlet里直接连着JDBC,一行行写 Class.forName、DriverManager、executeQuery,然后循环封装,再输出HTML标签。项目能跑,但改起来要命。
“JavaWeb_09”这个阶段的案例建议明确拆出三层:
| 层级 | 类命名规范 | 职责 |
|---|---|---|
| Servlet层 | EmployeeListServlet |
接收请求参数,调用Service,设置响应内容类型,跳转页面 |
| Service层 | EmployeeService |
业务逻辑处理,如参数合法性判断、密码哈希、事务边界控制 |
| DAO层 | EmployeeDao |
只负责SQL执行和结果集封装,不涉及请求/响应 |
举个例子,新增员工时要求姓名不能为空、薪资必须大于0。校验逻辑放哪?放Servlet的话,将来如果换了接口调用方式(比如改RESTful),校验要重写;放Service是最合理的——它面向业务而非面向HTTP协议。理解这个分层逻辑后,你后面看Spring的Service、Repository、Controller时完全不会有隔阂。
4.3 JSP页面的EL表达式与JSTL遍历
列表页面使用EL表达式取值时,注意区分 pageScope、requestScope、sessionScope 的优先级。如果你在Servlet里 request.setAttribute("empList", list),页面上直接 ${empList} 就能拿到,因为默认按照page → request → session → application作用域顺序查找。
JSTL遍历表格:
jsp复制<c:forEach items="${empList}" var="emp" varStatus="status">
<tr>
<td>${status.count}</td>
<td>${emp.name}</td>
<td>${emp.gender}</td>
<td>${emp.department.name}</td>
<td>${emp.salary}</td>
<td>${emp.entryDate}</td>
<td>
<a href="${pageContext.request.contextPath}/employee/edit?id=${emp.id}">编辑</a>
<a href="${pageContext.request.contextPath}/employee/delete?id=${emp.id}" onclick="return confirm('确定删除?')">删除</a>
</td>
</tr>
</c:forEach>
几个细节:
status.count是从1开始计数的序号,不需要再手动存变量。${emp.department.name}需要Employee对象里有Department类型的属性,且Department类有getName()方法。也就是说DAO层的联表查询不能只查employee表,而要把部门信息一并查出来封装成对象关系——这一步做得好不好,直接决定页面能不能取到值。- 拼接URL里尽量使用
${pageContext.request.contextPath}拿上下文路径。这个写法会输出成/ems,比硬编码更有利于换环境部署。
5. 常见报错与排查实录:遇到问题比写代码更“长肉”
5.1 IDEA启动后404:先分清是路径错还是部署错
JavaWeb阶段报错频率最高的一定是404。排查逻辑给到大家:
第一步,看浏览器地址栏。如果是 http://localhost:8080/ 直接访问,IDEA默认创建的是 index.jsp,这个页面在Tomcat欢迎文件里能自动识别。404的话,多半是Artifact里webapp目录没有index.jsp,或者根本没有生成target。
第二步,看IDEA控制台的Tomcat启动日志。重点搜两个关键词:Deployment of web application directory 和 Completed deployment。如果日志显示“The origin server did not find a current representation for the target resource”,说明Tomcat成功启动了,但webapp下找不到路径对应的资源。这时检查:
- Application context设置的是不是
/ems,而浏览器访问的是http://localhost:8080/ems/xxx。 - JSP是否被放在
WEB-INF下但直接用URL访问了(这种情况必然404,得通过Servlet转发)。
第三步,看 target/employee-system/WEB-INF/classes 下有没有编译好的class文件。如果目录是空的,说明编译阶段出问题了,鼠标点一下 Build → Rebuild Project,观察编译输出。
5.2 数据库连接报错:五种“Communications”错误的区别
日志里看到 Communications link failure 基本上是数据库连接问题,但对应的原因五花八门:
| 报错信息典型特征 | 最可能原因 | 处理方法 |
|---|---|---|
Access denied for user 'root'@'localhost' |
用户名或密码错误 | 检查druid.properties里的配置 |
Unknown database 'employee_db' |
数据库名拼错 | 检查url里的路径,是否有大小写问题 |
Connection refused |
MySQL服务没启动 | Windows服务管理器启动MySQL,Linux执行systemctl start mysqld |
Public Key Retrieval is not allowed |
MySQL 8.0连接时未允许公钥检索 | url里加 allowPublicKeyRetrieval=true |
The server time zone value is unrecognized |
时区问题 | url里加 serverTimezone=Asia/Shanghai |
遇到连接类报错,先确认 DriverManager 能不能连上。写个简单的main方法测试连接,如果指定url、用户名、密码直接java连一下还是报错,那大概率是MySQL服务层配置问题,与项目代码无关——把问题先切分到独立环节,排查效率高很多。
5.3 页面中文乱码:两层乱码要分开治理
乱码分两种:页面显示乱码和数据入库乱码。页面乱码的治理核心在响应头设置:
java复制response.setContentType("text/html;charset=UTF-8");
这个要在 response.getWriter() 之前调用,否则无效。同时JSP文件顶部要有:
jsp复制<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
数据入库乱码的治理要点在请求编码上。Tomcat 8.0以上版本,POST请求经过form表单提交时默认编码是ISO-8859-1,所以Servlet里取参数之前必须调用:
java复制request.setCharacterEncoding("UTF-8");
代码要放在 request.getParameter() 之前。如果你发现请求参数单个字段重新编码就能解决,也可以用 new String(param.getBytes("ISO-8859-1"), "UTF-8"),但这段代码治标不治本,不如把setCharacterEncoding放在Filter里统一处理:
java复制public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
request.setCharacterEncoding("UTF-8");
chain.doFilter(req, res);
}
写一个EncodingFilter并注册到所有路径,这个坑一次性消除。这个思路到后面所有框架里都存在,就是“编码过滤器”,学了不亏。
5.4 Tomcat端口被占用:不是每次都需要改端口
Port 8080 was already in use 算是人手一份的报错。遇到这个,第一条反应别去web.xml里改8081,那是掩盖问题。用命令行找出占用进程并杀掉才是根治:
Windows:
bash复制netstat -ano | findstr :8080
taskkill /PID [查到PID] /F
macOS/Linux:
bash复制lsof -i :8080
kill -9 [查到PID]
如果本地经常残留僵尸java进程,建议在IDEA运行配置的Server标签页里勾上“Allow multiple instances”旁边的开关,看实际需求来。更多情况下,重启IDEA、清理掉僵死的Tomcat进程,问题自然解决。
6. 案例收尾与一发入魂的部署细节
收尾阶段有两件最容易被忽略却又非常提气的事情:Filter登录验证和war包部署。
6.1 用Filter拦截未登录用户:给项目装上“门禁”
很多自学的案例,登录页面做完了,列表页面却支持直接通过地址栏访问,这样权限校验形同虚设。在Servlet规范里,Filter干这事最合适。
创建一个 LoginFilter:
java复制@WebFilter("/*")
public class LoginFilter 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.startsWith(req.getContextPath() + "/static/")) {
chain.doFilter(req, resp);
return;
}
// 检查session中是否有登录标记
Object user = req.getSession().getAttribute("loginUser");
if (user == null) {
resp.sendRedirect(req.getContextPath() + "/login.jsp");
return;
}
chain.doFilter(req, resp);
}
}
解析一下这个Filter的三个关键行为:
req.getSession()在无session时会自动新建一个session对象,每个匿名请求都会创建会话,浪费内存。但这里为了判断是否登录别无选择,可在Servlet登录逻辑中通过req.getSession(true)明确创建或获取;false参数只在会话存在时返回会话对象,否则返回null,配合判空减少无谓session创建。uri.startsWith(req.getContextPath() + "/static/")放行静态资源,否则CSS和JS也会被拦截,页面会裸奔。- Filter的顺序和匹配规则在web.xml或注解里保持一致性,多个Filter时要看注册顺序,Chain的执行顺序就是注册顺序。
这个Filter一加上,整个项目的完整度立刻高一个档次——体会一下“页面不能随便被别人绕过登录强行打开”的掌控感,这就是做真实项目和交作业的区别。
6.2 打包war并配置Tomcat:从“IDEA里有能跑”到“换台电脑也能跑”
IDEA中部署用的是war exploded,但最终交付或放到服务器上,需要的是完整war包。打包方法:
- 在 Maven 侧边栏双击
package,IDEA会执行构建,在target目录下生成employee-system.war。 - 把war包拷贝到独立Tomcat的
webapps目录下。 - 启动Tomcat,观察日志中
Deploying web application archive相关输出。 - 自动解压后访问
http://localhost:8080/employee-system/login.jsp。这里的访问路径取决于war包名,所以改名时要连同应用上下文一起对应起来。
打包时注意MySQL连接配置。如果你的 druid.properties 用的是localhost和root账号,部署到服务器后这些配置往往需要变。个人项目中可以把配置文件外置,比如Tomcat的lib目录下放一个覆盖配置,或通过环境变量读取。教学项目可以直接在properties里用 ${MYSQL_HOST} 这类占位符,再在启动Tomcat前设置环境变量导出,从学习阶段就养成可配置化习惯。
我在实际使用中发现,很多JavaWeb项目做完后代码逻辑没啥问题,反而是在环境迁移时暴露出“依赖和配置写死”的毛病。早期多花十分钟做配置文件外置,后面省下的不止十分钟——本地没问题、服务器起不来的滋味,谁体验过谁知道。
最后再分享一个小技巧:写完Filter和DAO层后,用 curl 命令从命令行发起请求,对比从浏览器操作时Tomcat日志中输出的SQL记录,能比点击页面快很多地确认某个请求对应哪条SQL。这个阶段把请求路径、Servlet方法、SQL语句三者串联成“肌肉记忆”,等学Spring Boot写REST接口时,你会感谢现在这个较真的自己。
