Java Web开发实战经典项目源码精析
简介:《Java Web开发实战经典》是一本系统讲解Java Web应用开发的技术书籍,配套源代码帮助读者深入实践核心技能。本书聚焦Java语言与Struts框架的整合应用,涵盖Servlet、JSP、MVC设计模式、数据库操作、安全优化等关键技术。通过完整的开发流程训练,学习者可掌握企业级Web应用的构建方法,提升在实际项目中解决复杂问题的能力,为从事Java Web开发奠定坚实基础。
Java Web全栈开发实战:从环境搭建到企业级部署
哎呀,说到Java Web开发,是不是总觉得它“古老”又“繁琐”?别急着划走!😎 虽然现在Spring Boot满天飞,但真正搞懂底层原理的开发者,才不会被框架牵着鼻子走。今天咱们就来一次 深度穿越之旅 ——从最原始的JDK配置开始,一步步走到Struts时代的企业架构,最后再稳稳落地到生产部署与测试验证。
准备好了吗?🚀 这不是一篇“照着敲就能跑”的快餐教程,而是一次 知其然更知其所以然 的技术复盘。你会发现:原来Servlet线程安全问题这么致命?Struts的ActionForm居然是靠内省绑定的?MVC模式早在20年前就已经玩得风生水起了!
我们先从一个再普通不过的场景说起:你在公司接到第一个任务——“搭个Web环境,跑个Hello World”。你以为只是装个Tomcat那么简单?No no no,这里面水可深了。🌊
环境基石:JDK + Tomcat 的黄金搭档 ✅
想写Java程序?第一步永远是装好JDK(Java Development Kit)。别小看这一步,多少人卡在 java -version 报错上一整天。😅
你需要做的不仅仅是下载安装包,还得手动设置两个关键环境变量:
JAVA_HOME=C:\Program Files\Java\jdk1.8.0_301
PATH=%JAVA_HOME%\bin;%PATH%
🤔 为什么必须配
JAVA_HOME?因为像Tomcat这样的容器会通过这个变量自动找到你的Java运行时。它是整个生态系统的“定位信标”。
验证是否成功?命令行里敲一句:
java -version
javac -version
如果看到类似输出:
java version "1.8.0_301"
Java(TM) SE Runtime Environment (build 1.8.0_301-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.301-b09, mixed mode)
恭喜你,已经迈出了第一步!👏
接下来就是大名鼎鼎的 Apache Tomcat ——轻量级Servlet容器的代表作。去官网下个Tomcat 9+版本,解压后进入 bin 目录,Windows用户双击 startup.bat ,Linux/MaxOS用户执行:
chmod +x startup.sh
./startup.sh
浏览器打开 http://localhost:8080 ,看到那只熟悉的猫🐱,说明服务起来了!
💡 小贴士:如果你遇到端口冲突,可以修改
conf/server.xml中的<Connector port="8080">为其他值,比如8081。
这时候你可能会问:“我写的代码放哪儿?”别急,这就引出Java Web项目的标准结构。
目录结构揭秘:WEB-INF 是道防火墙 🔒
一个典型的Java Web项目长这样:
mywebapp/
├── src/main/java/ ← Java源码
├── src/main/resources/ ← 配置文件
├── src/main/webapp/ ← Web资源根目录
│ ├── WEB-INF/
│ │ ├── web.xml ← 核心配置
│ │ ├── classes/ ← 编译后的class
│ │ └── lib/ ← 第三方jar包
│ └── index.jsp ← 可公开访问的首页
└── pom.xml ← Maven构建文件
重点来了: /WEB-INF/ 下的所有内容对外不可见 !这意味着即使用户知道路径如 /WEB-INF/web.xml ,也无法直接通过URL访问。这是Servlet规范内置的安全机制,防止敏感信息泄露。
那谁来管这些配置呢?答案是: web.xml ——曾经的“皇帝诏书”。
web.xml 的权力中心地位 📜
早期Java Web完全依赖XML进行声明式配置。比如你想让访问 /hello 的请求由某个类处理,就得这么写:
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.example.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/hello</url-pattern>
</servlet-mapping>
再加上欢迎页设置:
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
整个流程就像一张地图,告诉Tomcat:“当请求进来时,请按图索骥。”
不过自Servlet 3.0起,注解开始流行起来。你可以用一行代码替代上面那段XML:
@WebServlet("/hello")
public class HelloServlet extends HttpServlet { ... }
是不是清爽多了?🎉 但注意! web.xml 依然有它的不可替代性——例如全局上下文参数、过滤器链、错误页面映射等高级控制,还是得靠它来统筹。
开发效率革命:Maven 构建自动化 🛠️
手动编译、打包、拷贝jar?那是上个世纪的做法。现代开发早已进入“一键构建”时代,核心工具就是 Maven 。
用一条命令就能生成标准Web项目骨架:
mvn archetype:generate \
-DgroupId=com.example \
-DartifactId=mywebapp \
-DarchetypeArtifactId=maven-archetype-webapp \
-DinteractiveMode=false
然后在 pom.xml 中引入必要的依赖,比如Servlet API:
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
这里有个关键点: <scope>provided</scope> 表示这个库由运行环境(即Tomcat)提供,打包时不打进WAR文件。否则会出现类加载冲突,轻则警告,重则启动失败。💣
最终执行:
mvn clean package
会在 target/ 目录下生成一个 .war 文件,直接扔进Tomcat的 webapps 就能跑起来!
协议底座:HTTP 与 MVC 模式的天然契合 🔄
所有Web交互的本质都是 HTTP 请求-响应模型 。客户端发个GET或POST请求,服务器回个HTML、JSON或者跳转指令。
而这种模式,恰好完美契合 MVC(Model-View-Controller)设计思想 :
| 角色 | 技术实现 | 职责 |
|---|---|---|
| Model | POJO / JavaBean / Service | 封装数据和业务逻辑 |
| View | JSP / HTML 页面 | 展示数据给用户 |
| Controller | Servlet | 接收请求 → 调用Model → 决定跳转哪个View |
举个例子:用户登录。
- 用户提交表单 → Controller(LoginServlet)接收
- Servlet调用UserService验证账号密码(Model)
- 成功则跳转dashboard.jsp(View),失败则返回错误提示
这种职责分离让系统更易维护、扩展性强,也为后来的Struts、Spring MVC提供了设计蓝本。
Servlet与JSP动态网页开发实践
现在让我们深入到底层,看看真正的动态网页是怎么运作的。💻
很多人以为“Java Web = Spring Boot”,但其实一切的起点,是那个看似简单的 HttpServlet 。
Servlet生命周期:单例多线程的艺术 ⚙️
你知道吗?一个Servlet实例在整个应用中通常只被创建一次,但它要面对成千上万的并发请求。这就是所谓的“ 单例多线程 ”模型。
整个生命周期分为三个阶段:
- 初始化(init)
- 服务处理(service)
- 销毁(destroy)
来看一段经典实现:
public class HelloServlet implements Servlet {
private ServletConfig config;
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
System.out.println("HelloServlet 初始化完成");
}
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
res.setContentType("text/html;charset=UTF-8");
res.getWriter().println("<h1>欢迎访问 HelloServlet!</h1>");
}
@Override
public void destroy() {
System.out.println("HelloServlet 正在销毁...");
}
}
虽然语法没错,但实际开发中我们几乎不会直接实现 Servlet 接口,而是继承 HttpServlet ,因为它已经帮我们封装好了HTTP方法分发逻辑:
public class InfoServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
resp.setContentType("application/json;charset=UTF-8");
PrintWriter out = resp.getWriter();
out.println("{");
out.println(" \"method\": \"" + req.getMethod() + "\",");
out.println(" \"uri\": \"" + req.getRequestURI() + "\"");
out.println("}");
out.flush();
}
}
当你访问 /info 时,就会收到一个JSON格式的响应,包含当前请求的方法和路径。
⚠️ 注意:一定要在读取参数前设置编码!
java req.setCharacterEncoding("UTF-8"); // 必须放在第一句!
否则中文参数可能乱码,尤其是在POST请求中。😤
GET vs POST:不只是方法名的区别 🆚
| 特性 | GET | POST |
|---|---|---|
| 数据位置 | URL查询字符串 | 请求体 |
| 安全性 | 参数暴露在地址栏 | 相对安全 |
| 幂等性 | 是(重复请求无副作用) | 否(可能造成多次下单) |
| 缓存支持 | 浏览器可缓存 | 不缓存 |
| 长度限制 | 约2KB(取决于浏览器) | 几乎无限 |
所以记住一条铁律: 涉及敏感操作(如登录、支付)一律使用POST!
处理表单也很简单:
<form action="LoginServlet" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="登录">
</form>
对应Servlet:
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws IOException {
req.setCharacterEncoding("UTF-8"); // 关键!
String username = req.getParameter("username");
String password = req.getParameter("password");
if ("admin".equals(username) && "123456".equals(password)) {
resp.sendRedirect("welcome.jsp");
} else {
resp.sendError(401, "用户名或密码错误");
}
}
这里用了 sendRedirect() 实现客户端跳转,URL会发生变化;如果是 forward() 则是服务器内部转发,地址不变。
🔐 生产提醒:密码绝对不能明文比对!要用BCrypt加密存储,避免数据泄露酿成大祸。
Session与Cookie:维持状态的双子星 🌟
HTTP本身是无状态协议,怎么记住“你是谁”?答案就是 Session + Cookie 组合拳。
工作原理如下:
sequenceDiagram
participant Client
participant Server
Client->>Server: 请求登录页
Server->>Client: Set-Cookie: JSESSIONID=ABC123
Client->>Server: 提交表单 (携带 Cookie)
Server->>Server: 查找 Session 并保存 user=admin
Server->>Client: 响应成功 (仍携带 JSESSIONID)
Client->>Server: 访问主页 (自动附带 Cookie)
Server->>Server: 根据 JSESSIONID 获取 Session 数据
Server->>Client: 显示用户信息
代码实现也超简单:
// 登录成功后保存session
HttpSession session = req.getSession();
session.setAttribute("user", username);
session.setMaxInactiveInterval(30 * 60); // 30分钟过期
读取session:
HttpSession session = req.getSession(false); // false表示不创建新session
if (session != null && session.getAttribute("user") != null) {
out.println("当前用户:" + session.getAttribute("user"));
} else {
resp.sendRedirect("login.html");
}
至于Cookie,常用于记录非敏感信息,比如上次登录时间:
Cookie cookie = new Cookie("lastLogin", LocalDateTime.now().toString());
cookie.setMaxAge(7 * 24 * 60 * 60); // 7天有效
cookie.setPath("/");
cookie.setHttpOnly(true); // 防XSS
cookie.setSecure(true); // HTTPS专用
resp.addCookie(cookie);
🛡️ 安全建议总结:
- 敏感信息绝不存Cookie;
- 加
HttpOnly防止JS盗取;- 加
Secure强制HTTPS传输;- 设置合理过期时间;
- 使用
SameSite=Lax防CSRF攻击。
Struts框架深度整合与企业级应用构建
讲完基础组件,我们来聊聊历史上的明星框架—— Struts 1.x 。
也许你会说:“都2025年了还学Struts?” 且慢!💡 很多银行、政府系统的老旧系统仍在使用Struts,而且它的设计理念深刻影响了后续所有MVC框架。
ActionForm:表单自动绑定的秘密 🔐
Struts最大的亮点之一,就是 ActionForm 自动绑定 。
以前我们要一个个getParameter:
String name = req.getParameter("name");
int age = Integer.parseInt(req.getParameter("age"));
User user = new User(name, age);
Struts说:太low了!让我来帮你。
只需定义一个Form类:
public class UserLoginForm extends ActionForm {
private String username;
private String password;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public ActionErrors validate(ActionMapping mapping, HttpServletRequest request) {
ActionErrors errors = new ActionErrors();
if (username == null || username.trim().isEmpty()) {
errors.add("username", new ActionError("error.username.required"));
}
return errors;
}
}
只要字段名和HTML表单一致,框架就会 通过反射+内省机制自动填充属性值 !
背后的流程是这样的:
sequenceDiagram
participant Client
participant ActionServlet
participant RequestProcessor
participant ActionForm
participant Action
Client->>ActionServlet: POST /login.do
ActionServlet->>RequestProcessor: process()
RequestProcessor->>ActionForm: 实例化并setUsername/setPassword
RequestProcessor->>ActionForm: 执行validate()
alt 验证失败
ActionForm-->>RequestProcessor: 返回错误
RequestProcessor-->>Client: 跳转回 login.jsp
else 验证成功
RequestProcessor->>Action: 创建LoginAction
Action->>Service: 处理业务
Action-->>RequestProcessor: 返回ActionForward
RequestProcessor-->>Client: 跳转目标页面
end
厉害吧?不仅免去了手动赋值,还能统一做验证、支持国际化提示消息。
还有更炫的—— DynaActionForm ,连Java类都不用写了:
<form-bean name="userForm" type="org.apache.struts.action.DynaActionForm">
<form-property name="id" type="java.lang.Integer"/>
<form-property name="name" type="java.lang.String"/>
<form-property name="emails" type="java.util.List"/>
</form-bean>
适合快速原型开发,但失去了编译期检查能力,长期维护风险高。
Action类:业务逻辑的调度中枢 🧩
每个HTTP请求最终都会落到某个 Action 类的 execute() 方法上:
public class LoginAction extends Action {
public ActionForward execute(
ActionMapping mapping,
ActionForm form,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
UserLoginForm userForm = (UserLoginForm) form;
String username = userForm.getUsername();
String password = userForm.getPassword();
UserService userService = new UserServiceImpl();
boolean isValid = userService.authenticate(username, password);
if (isValid) {
HttpSession session = request.getSession();
session.setAttribute("user", username);
return mapping.findForward("success");
} else {
ActionErrors errors = new ActionErrors();
errors.add("login", new ActionError("error.login.failed"));
saveErrors(request, errors);
return mapping.getInputForward(); // 回到原页面
}
}
}
配合 struts-config.xml 注册:
<action path="/login"
type="com.example.actions.LoginAction"
name="userForm"
scope="request"
input="/login.jsp"
validate="true">
<forward name="success" path="/welcome.jsp"/>
</forward>
</action>
其中:
path="/login"对应/login.doname="userForm"指向前面定义的Forminput="/login.jsp"是验证失败后跳转页validate="true"开启自动校验
这套机制实现了高度解耦:URL → Form → Action → Forward,清晰可控。
struts-config.xml:XML时代的统治者 📁
作为Struts的“大脑”, struts-config.xml 控制着整个应用的行为流:
<struts-config>
<form-beans>
<form-bean name="userForm" type="com.example.forms.UserLoginForm"/>
</form-beans>
<global-forwards>
<forward name="home" path="/index.jsp"/>
</global-forwards>
<action-mappings>
<action path="/login" ...>
<forward name="success" path="/welcome.jsp"/>
</action>
</action-mappings>
<message-resources parameter="ApplicationResources"/>
<plug-in className="org.apache.struts.validator.ValidatorPlugIn">
<set-property property="pathnames" value="/validator-rules.xml"/>
</plug-in>
</struts-config>
虽然现在大家吐槽“XML太啰嗦”,但在当时,这种集中式配置极大提升了大型项目的可管理性和审计能力。
更重要的是,它奠定了“ 前端控制器(Front Controller) ”的设计范式——所有请求先经过 ActionServlet 统一分发,再路由到具体Action。
ActionServlet:前端控制器的鼻祖 🏗️
来看看它的核心流程:
graph TD
A[HTTP请求到达] --> B{是否为 *.do?}
B -- 是 --> C[调用ActionServlet.service()]
C --> D[解析module和action path]
D --> E[查找匹配的ActionMapping]
E --> F[实例化或复用ActionForm]
F --> G[自动绑定表单数据]
G --> H[执行validate()]
H --> I{验证通过?}
I -- 否 --> J[转发至input页面]
I -- 是 --> K[查找对应Action类]
K --> L[调用execute()]
L --> M[返回ActionForward]
M --> N[执行跳转或重定向]
这个“拦截→解析→绑定→验证→执行→响应”的链条,正是现代MVC框架的雏形。
甚至连Spring MVC的DispatcherServlet,也是沿着这条思路演进而来的。
Java Web系统部署上线与全链路测试验证
终于到了上线环节!🎉 别高兴太早,真正的挑战才刚开始。
WAR包打包与部署全流程 📦
使用Maven打包:
<packaging>war</packaging>
<build>
<finalName>mywebapp</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.0</version>
</plugin>
</plugins>
</build>
执行:
mvn clean package
得到 target/mywebapp.war ,两种方式部署:
- 自动部署 :复制到
$CATALINA_HOME/webapps/,重启自动解压; - 管理界面 :登录
http://localhost:8080/manager,上传即可。
对于多租户系统,还可以配置虚拟主机:
<Host name="app1.company.com" appBase="webapps/app1"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve"
directory="logs" prefix="app1_access" suffix=".log"/>
</Host>
每个域名独立日志、独立应用基路径,运维更清晰。
部署策略选择:热 vs 冷 vs 蓝绿 🎯
| 策略 | 特点 | 适用场景 |
|---|---|---|
| 热部署 | 动态替换类文件,不停机 | 开发调试 |
| 冷部署 | 停止→更新→重启 | 生产环境升级 |
| 蓝绿部署 | 双环境切换,零停机 | 高可用系统 |
| 滚动更新 | 分批替换实例 | Kubernetes集群 |
⚠️ 注意:Tomcat的
reloadable="true"虽支持热加载,但容易引发内存泄漏,生产环境慎用!
全链路测试:确保上线万无一失 ✅
单元测试覆盖Service层
@Test
public void testUserRegistration() {
UserService service = new UserService();
User user = new User("test@company.com", "123456");
boolean result = service.register(user);
assertTrue(result);
assertNotNull(user.getId());
}
建议覆盖率不低于70%,尤其关注异常分支。
Postman模拟接口测试
构建测试集合,涵盖各种情况:
| 方法 | URL | 参数 | 预期状态码 |
|---|---|---|---|
| POST | /api/login | {“email”:”a”,”pwd”:”123”} | 200 |
| GET | /api/profile | - | 401(未登录) |
| PUT | /api/user/1 | {“name”:”John”} | 204 |
支持Newman集成CI/CD流水线,实现自动化回归测试。
业务流程状态流转验证
stateDiagram-v2
[*] --> 登录页
登录页 --> 主页: 凭证正确
登录页 --> 登录页: 密码错误
主页 --> 订单页: 点击下单
订单页 --> 支付页: 提交订单
支付页 --> 完成页: 支付成功
完成页 --> 主页: 返回首页
每个节点都要验证数据持久化、会话状态、页面跳转是否正确。
文件上传下载完整性检测
@Test
public void testFileUpload() throws Exception {
MockMultipartFile file = new MockMultipartFile(
"file", "test.jpg", "image/jpeg", "data".getBytes()
);
mockMvc.perform(multipart("/upload").file(file))
.andExpect(status().isOk())
.andExpect(jsonPath("$.size").value(4));
}
同时检查临时目录清理、文件类型白名单、防重命名策略。
总结与思考:站在巨人的肩膀上看未来 🌍
看完这一整套流程,你会发现: 技术从未真正消失,只是不断演化 。
今天我们用Spring Boot几行代码搞定的事,在二十年前需要手动配置一堆XML、自己处理编码问题、小心翼翼管理Session……但正是这些“笨重”的实践,锤炼出了今天的最佳设计。
掌握Servlet、JSP、Struts,并不是为了回到过去,而是为了:
- 理解框架背后的真相;
- 在排查线上问题时游刃有余;
- 设计更合理的架构方案;
- 从容应对遗留系统维护;
- 向更高阶的微服务、云原生平稳过渡。
所以,下次有人问你“Java Web还值得学吗”,你可以自信地说:
👉 “当然!因为所有的‘高级’,都建立在扎实的‘基础’之上。” 💪
Keep coding, keep exploring! 🚀
简介:《Java Web开发实战经典》是一本系统讲解Java Web应用开发的技术书籍,配套源代码帮助读者深入实践核心技能。本书聚焦Java语言与Struts框架的整合应用,涵盖Servlet、JSP、MVC设计模式、数据库操作、安全优化等关键技术。通过完整的开发流程训练,学习者可掌握企业级Web应用的构建方法,提升在实际项目中解决复杂问题的能力,为从事Java Web开发奠定坚实基础。
更多推荐



所有评论(0)