本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《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实例在整个应用中通常只被创建一次,但它要面对成千上万的并发请求。这就是所谓的“ 单例多线程 ”模型。

整个生命周期分为三个阶段:

  1. 初始化(init)
  2. 服务处理(service)
  3. 销毁(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.do
  • name="userForm" 指向前面定义的Form
  • input="/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 ,两种方式部署:

  1. 自动部署 :复制到 $CATALINA_HOME/webapps/ ,重启自动解压;
  2. 管理界面 :登录 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! 🚀

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:《Java Web开发实战经典》是一本系统讲解Java Web应用开发的技术书籍,配套源代码帮助读者深入实践核心技能。本书聚焦Java语言与Struts框架的整合应用,涵盖Servlet、JSP、MVC设计模式、数据库操作、安全优化等关键技术。通过完整的开发流程训练,学习者可掌握企业级Web应用的构建方法,提升在实际项目中解决复杂问题的能力,为从事Java Web开发奠定坚实基础。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐