*Java 沉淀重走长征路*之——《Java Web 应用开发完全指南:从零到企业实战(两万字深度解析)》

写在前面

如果你正在学习Java Web开发,面对Servlet、JSP、Filter、Session这些概念感到眼花缭乱,不知道它们之间有什么关系,也不知道学了能做什么------那么这篇文章就是为你准备的。

本文采用「问题驱动」的方式,带你重新理解Java Web开发的每一个核心知识点。我们不堆砌概念,而是先搞清楚每个技术「解决了什么痛点」,再学习「怎么用」,最后落地到「企业如何实战」。

让我们开始这段Java Web探索之旅。


阶段一:问题锚定------我们为什么要学Java Web?

场景化抛出问题

想象这样一个场景:你要开发一个用户登录功能。用户在浏览器输入用户名密码,点击登录,系统验证身份后,跳转到欢迎页面。

看起来很简单,对吧?但仔细想想,这里面有一堆问题需要解决:

  1. 用户输入的数据怎么传给服务器? 用户在表单里填的信息,浏览器怎么包装?服务器怎么解析?

  2. 服务器怎么处理登录请求? 谁来接收HTTP请求?谁来调用业务逻辑?

  3. 登录成功后,怎么记住用户? HTTP协议是无状态的,第二次请求时服务器怎么知道「这个用户已经登录过」?

  4. 页面怎么动态显示用户信息? 不可能给每个用户都写一个静态HTML页面吧?

  5. 所有请求都要做权限校验,难道每个页面都写一遍验证代码?

  6. 项目启动时要加载一些初始化数据,写在哪儿?

这些问题,就是Java Web要解决的核心问题。

技术定位梳理

Java Web,简单说就是用Java语言开发网站应用的一套技术体系。它的核心价值在于:

  • 接收并处理HTTP请求(用户访问网站,服务器得知道他要干什么)

  • 动态生成响应内容(不同用户看到不同的页面)

  • 管理用户会话状态(记住谁登录了、购物车里有什么)

  • 提供标准化的开发组件(让开发者专注于业务逻辑,不用重复造轮子)

技术边界说明

Java Web能做什么?几乎所有你能想到的网站后端功能:用户登录注册、商品展示、订单处理、数据管理......

但Java Web不是万能的:

  • 它不负责页面好不好看(那是CSS的事)

  • 它不处理用户交互特效(那是JavaScript的事)

  • 它不适合做实时性极高的游戏服务器(那需要Netty等NIO框架)

清楚了这些问题,我们再来看具体的技术组件,就会有「原来它是为解决这个问题而生的」的恍然大悟感。


阶段二:基础认知------构建最小知识体系

在深入每个技术之前,我们需要先建立一个「最小化」的知识框架,理解这些概念到底是干什么的。

一丢丢前端基础:用户看到的和用户操作的

核心概念拆解

前端就是你打开一个网站看到的页面:文字、图片、按钮、输入框。用户在前端填写表单、点击按钮,这些操作会产生HTTP请求,发送到后端服务器。

最简单的HTML表单长这样:

html 复制代码
<form action="login" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    <input type="submit" value="登录">
</form>

用户点击登录后,浏览器会把用户名和密码打包,发送到login这个地址。

为什么要懂一点前端?

因为Java Web开发中,你既要处理后端逻辑,也要写前端页面(JSP本质上就是HTML+Java代码)。理解请求怎么发出去的,才能知道后端怎么接。

XML:配置文件的「老祖宗」

问题锚定:程序的某些参数(比如数据库连接地址、端口号)如果硬编码在代码里,改起来要重新编译,太麻烦了。能不能把这些「可变的部分」抽出来单独管理?

核心概念:XML(可扩展标记语言)就是一种用来存储结构化数据的文本格式。它用标签来定义数据,长得像HTML,但标签不是预定义的,你可以自己发明。

复制代码
<user>
    <name>张三</name>
    <age>25</age>
</user>

最小示例

以前Java Web项目的配置文件web.xml长这样:

复制代码
<web-app>
    <servlet>
        <servlet-name>LoginServlet</servlet-name>
        <servlet-class>com.example.LoginServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>LoginServlet</servlet-name>
        <url-pattern>/login</url-pattern>
    </servlet-mapping>
</web-app>

核心原理:XML通过标签嵌套表达层级关系,通过属性添加元信息。程序可以解析XML文件,读取里面的配置项,实现「配置分离」。

现在还用吗? 现在更流行用properties或YAML格式,因为更简洁。但XML依然广泛存在于老项目和一些中间件配置中,理解它对维护老系统很有帮助。

JSON:前后端通信的「普通话」

问题锚定:前端和后端要交换数据,怎么格式化这些数据?XML太啰嗦了,能不能有一种更轻量、更易读的格式?

核心概念:JSON(JavaScript Object Notation)是一种轻量级的数据交换格式。它长得特别像JavaScript里的对象,但语言无关,几乎所有编程语言都支持。

json

复制代码
{
    "name": "张三",
    "age": 25,
    "hobbies": ["读书", "编程"]
}

对比XML

  • XML:<user><name>张三</name><age>25</age></user>

  • JSON:{"name":"张三","age":25}

JSON明显更简洁,而且解析速度更快。现在的Web开发中,JSON已经是前后端通信的事实标准。

最小示例

前端发一个JSON格式的POST请求:

javascript 复制代码
fetch('/api/user', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({name: '张三', age: 25})
})

后端收到后,可以方便地解析成Java对象。

Servlet:Java Web的「发动机」

问题锚定:用户发了HTTP请求过来,总得有段代码来处理吧?谁来接收请求参数?谁来调用业务方法?谁来返回响应页面?

核心概念:Servlet就是Java Web中专门处理HTTP请求的Java程序。它运行在Web服务器(如Tomcat)里面,负责:

  • 接收HTTP请求

  • 解析请求参数

  • 调用业务逻辑(查询数据库、计算等)

  • 生成响应(HTML页面、JSON数据等)

最小示例

创建一个最简单的Servlet:

java 复制代码
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, 
                         HttpServletResponse response) 
            throws ServletException, IOException {
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println("<html><body>");
        out.println("<h1>Hello, Java Web!</h1>");
        out.println("</body></html>");
    }
}

web.xml中配置访问路径:

XML 复制代码
<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>

启动Tomcat,访问http://localhost:8080/你的应用/hello,就能看到页面了。

核心原理:Servlet的生命周期由容器(Tomcat)管理:

  1. 初始化 :第一次访问或启动时,调用init()方法

  2. 服务 :每次请求,调用service()方法(根据请求类型分发给doGet()doPost()

  3. 销毁 :应用卸载时,调用destroy()方法

JSP:在HTML里写Java的「便利贴」

问题锚定 :用Servlet输出HTML太痛苦了,每一行HTML都要用out.println()包起来,要是页面复杂一点,代码根本没法维护。能不能直接在HTML里嵌入Java代码?

核心概念:JSP(Java Server Pages)本质上就是HTML,但允许在里面插入Java代码片段。它运行时会先被容器翻译成Servlet,再执行。

最小示例

创建一个hello.jsp文件:

XML 复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<body>
    <h1>当前时间:<%= new java.util.Date() %></h1>
    <%-- 这是JSP注释 --%>
    <%
        String name = request.getParameter("name");
        if (name != null) {
            out.println("你好," + name);
        }
    %>
</body>
</html>

访问http://localhost:8080/你的应用/hello.jsp?name=张三,页面会显示当前时间和欢迎语。

核心原理:JSP的工作原理是「先翻译后编译」:

  1. 客户端请求JSP页面

  2. JSP容器将JSP文件翻译成Servlet源码(.java文件)

  3. 编译成字节码(.class文件)

  4. 执行这个Servlet,输出HTML到浏览器

JSTL:让JSP页面更干净的「标签库」

问题锚定:JSP虽然方便,但如果在页面里写大量Java代码(<% ... %>),页面会变得混乱难维护。能不能用类似HTML标签的方式来写逻辑?

核心概念:JSTL(JSP标准标签库)就是一套预定义的标签,用来替代JSP中的Java代码。比如循环、判断、格式化等。

最小示例

使用JSTL的<c:forEach>遍历列表:

XML 复制代码
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<body>
    <ul>
        <c:forEach items="${userList}" var="user">
            <li>${user.name} - ${user.age}岁</li>
        </c:forEach>
    </ul>
</body>
</html>

配合EL表达式(${user.name}),JSP页面可以做到几乎不含Java代码,只关注数据展示。

Filter:请求的「安检门」

问题锚定:网站有很多功能,比如大部分页面都需要检查用户是否登录、记录访问日志、处理中文乱码。难道每个Servlet都写一遍这些代码?

核心概念:Filter(过滤器)就像一道安检门,在请求到达Servlet之前、响应返回客户端之前,可以对请求和响应进行拦截和处理。

最小示例

写一个过滤器处理全站中文乱码:

java 复制代码
public class EncodingFilter implements Filter {
    public void doFilter(ServletRequest request, ServletResponse response, 
                         FilterChain chain) 
            throws IOException, ServletException {
        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");
        chain.doFilter(request, response); // 放行,继续执行后续过滤器或目标Servlet
    }
}

核心原理 :多个Filter可以组成过滤器链,按顺序执行。每个Filter决定是继续执行链(chain.doFilter()),还是直接返回响应(拦截)。

Listener:容器的「监控摄像头」

问题锚定:应用启动时想加载一些初始化数据(比如缓存字典表),应用关闭时要释放资源。谁来监听这些「事件」?

核心概念:Listener(监听器)用来监听Web应用中的各种事件:应用启动/关闭、会话创建/销毁、属性添加/移除等。

最小示例

监听应用启动,加载配置:

java 复制代码
public class AppStartupListener implements ServletContextListener {
    public void contextInitialized(ServletContextEvent sce) {
        System.out.println("应用启动,加载初始化数据...");
        // 加载配置文件、初始化数据库连接池等
    }
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("应用关闭,释放资源...");
    }
}

Cookie:存在浏览器里的「小纸条」

问题锚定:用户登录后,下次再来访问,怎么记住他?HTTP是无状态的,每次请求都是独立的。

核心概念:Cookie是服务器发给浏览器、保存在浏览器上的一小段文本。浏览器下次访问同一网站时,会自动携带这些Cookie。

最小示例

在Servlet中设置Cookie:

复制代码
Cookie cookie = new Cookie("username", "张三");
cookie.setMaxAge(60 * 60 * 24); // 有效期1天
response.addCookie(cookie);

读取Cookie:

复制代码
Cookie[] cookies = request.getCookies();
if (cookies != null) {
    for (Cookie cookie : cookies) {
        if ("username".equals(cookie.getName())) {
            String username = cookie.getValue();
        }
    }
}

Session:存在服务器里的「档案袋」

问题锚定:Cookie存在浏览器,容量有限(4KB左右),而且用户可能禁用Cookie。更严重的是,敏感信息(如用户角色、权限)存在浏览器不安全。怎么办?

核心概念:Session是服务器端为每个用户创建的一个独立存储空间。服务器会给每个Session分配一个唯一的ID(Session ID),通过Cookie(或URL重写)把这个ID传给浏览器。浏览器下次请求时带上这个ID,服务器就知道对应哪个Session了。

最小示例

存数据到Session:

复制代码
HttpSession session = request.getSession(); // 获取或创建Session
session.setAttribute("user", userObject); // 存对象
session.setMaxInactiveInterval(30 * 60); // 设置30分钟超时

取数据:

复制代码
User user = (User) session.getAttribute("user");
if (user != null) {
    // 用户已登录
}

核心原理:Session的生命周期:

  1. 创建 :首次调用request.getSession()时创建

  2. 维护:每次请求携带Session ID,服务器更新最后访问时间

  3. 销毁 :超时未访问、主动调用invalidate()、或应用关闭


阶段三:核心用法拆解------每个技术怎么用

了解了基础概念,我们来深入看看每个技术在企业开发中的常见用法和坑点。

3.1 前端基础:表单提交与请求方式

两种请求方法

  • GET :参数在URL后面,?name=value形式。用于获取数据(如查看商品详情),有长度限制,不安全(参数暴露)。

  • POST:参数在请求体里。用于提交数据(如登录、注册),无长度限制,相对安全。

中文乱码问题

Tomcat 8之前,GET请求的默认编码是ISO-8859-1,中文会乱码。解决方案:

  • POST乱码:在Servlet开头加request.setCharacterEncoding("UTF-8");

  • GET乱码:修改Tomcat的server.xml,添加URIEncoding="UTF-8"

3.2 XML:配置与解析

常见用法

  • Web应用配置web.xml中配置Servlet、Filter、Listener

  • 框架配置文件:如MyBatis的Mapper XML文件

  • 数据传输:早期WebService使用XML格式

解析方式

  • DOM:把整个XML加载到内存,形成树,方便操作但耗内存

  • SAX:事件驱动,边读边解析,适合大文件

  • JAXB:Java对象和XML互相转换,最常用

3.3 JSON:序列化与反序列化

企业常用库

  • Jackson:Spring Boot默认集成,功能强大

  • Fastjson:阿里出品,速度快但存在一些安全漏洞

  • Gson:Google出品,简洁易用

示例(Jackson)

复制代码
// Java对象转JSON
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user);

// JSON转Java对象
User user = mapper.readValue(json, User.class);

3.4 Servlet:接收请求与返回响应

核心API

  • HttpServletRequest:获取请求参数、请求头、Session等

  • HttpServletResponse:设置响应状态码、响应头、写入响应内容

常见用法

复制代码
// 获取参数
String username = request.getParameter("username");
String[] hobbies = request.getParameterValues("hobby");

// 请求转发(服务器内部跳转)
request.getRequestDispatcher("/success.jsp").forward(request, response);

// 重定向(浏览器重新发起请求)
response.sendRedirect("login.jsp");

// 返回JSON数据
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":200,\"msg\":\"成功\"}");

坑点

  • 请求转发 vs 重定向:转发是服务器内部跳转,URL不变;重定向是浏览器重新请求,URL改变。

  • Servlet线程安全:Servlet默认单例多线程,成员变量要注意线程安全问题。

3.5 JSP:脚本元素与内置对象

JSP组成

  • 指令<%@ page ... %>,设置页面属性

  • 声明<%! ... %>,声明成员变量和方法

  • 脚本<% ... %>,写Java代码片段

  • 表达式<%= ... %>,输出表达式结果

  • 注释<%-- ... --%>,JSP注释(客户端看不到)

9大内置对象(直接可用):

  • request:HttpServletRequest

  • response:HttpServletResponse

  • session:HttpSession

  • application:ServletContext(应用上下文)

  • out:JspWriter(输出流)

  • pageContext:页面上下文

  • config:ServletConfig

  • page:当前Servlet实例

  • exception:异常对象(仅错误页面可用)

坑点

  • 避免在JSP中写大量Java代码(难以维护)

  • JSP最终会被编译成Servlet,所以JSP里的变量其实是_jspService()方法的局部变量

3.6 JSTL+EL:无脚本JSP开发

EL表达式${对象.属性},自动调用getter方法。支持算术运算、逻辑运算。

JSTL核心标签

复制代码
<%-- 条件判断 --%>
<c:if test="${user != null}">
    欢迎,${user.name}
</c:if>

<%-- 多条件选择 --%>
<c:choose>
    <c:when test="${score >= 90}">优秀</c:when>
    <c:when test="${score >= 60}">及格</c:when>
    <c:otherwise>不及格</c:otherwise>
</c:choose>

<%-- 循环 --%>
<c:forEach items="${list}" var="item" varStatus="status">
    第${status.count}个:${item.name}
</c:forEach>

<%-- 格式化日期 --%>
<fmt:formatDate value="${date}" pattern="yyyy-MM-dd"/>

3.7 Filter:拦截与控制

配置方式

  1. web.xml配置
复制代码
<filter>
    <filter-name>AuthFilter</filter-name>
    <filter-class>com.example.AuthFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>AuthFilter</filter-name>
    <url-pattern>/*</url-pattern> <!-- 拦截所有请求 -->
</filter-mapping>
  1. 注解配置(Servlet 3.0+):
复制代码
@WebFilter("/*")
public class AuthFilter implements Filter { ... }

常见应用场景

  • 权限控制:检查Session中是否有登录用户,没有则跳转到登录页

  • 编码设置:统一设置请求和响应的编码

  • 日志记录:记录请求的IP、URL、耗时

  • 敏感词过滤:替换响应内容中的敏感词

  • 压缩响应:对响应内容进行GZIP压缩

执行顺序 :按filter-mapping的配置顺序(或Filter类名的字母顺序)执行。

3.8 Listener:监听与回调

监听器类型

  • ServletContextListener:监听应用启动/关闭

  • HttpSessionListener:监听会话创建/销毁(可用于统计在线人数)

  • ServletRequestListener:监听请求到达/结束

  • 各种属性监听器:ServletContextAttributeListenerHttpSessionAttributeListenerServletRequestAttributeListener

在线人数统计示例

复制代码
@WebListener
public class OnlineUserListener implements HttpSessionListener {
    private static int onlineCount = 0;
    
    public void sessionCreated(HttpSessionEvent se) {
        onlineCount++;
        System.out.println("新用户上线,当前在线:" + onlineCount);
    }
    
    public void sessionDestroyed(HttpSessionEvent se) {
        onlineCount--;
        System.out.println("用户下线,当前在线:" + onlineCount);
    }
}

3.9 Cookie:客户端存储

重要属性

  • setMaxAge(int expiry):有效期(秒),负数为浏览器关闭即失效,0为删除Cookie

  • setPath(String uri):指定哪些路径携带此Cookie,默认当前路径及其子路径

  • setDomain(String domain):指定哪些域名携带此Cookie

  • setHttpOnly(boolean httpOnly):设为true可防止XSS攻击窃取Cookie

  • setSecure(boolean secure):设为true只会在HTTPS下发送

坑点

  • 不同浏览器对Cookie数量和大小有限制(一般50个左右,4KB左右)

  • Cookie中不能存中文(需要URL编码)

  • 敏感信息不要存Cookie

3.10 Session:服务端状态管理

核心方法

  • setAttribute(String name, Object value):存数据

  • getAttribute(String name):取数据

  • removeAttribute(String name):移除数据

  • invalidate():销毁Session

  • setMaxInactiveInterval(int interval):设置超时时间(秒)

  • getId():获取Session ID

Session实现机制

默认通过Cookie实现:Cookie中存JSESSIONID=xxxx,服务器根据这个ID找对应的Session。

如果用户禁用了Cookie,可以通过URL重写:

复制代码
String newURL = response.encodeRedirectURL("index.jsp");
response.sendRedirect(newURL);

这样如果Cookie不可用,Session ID会附加在URL后面:index.jsp;jsessionid=xxxx

分布式Session问题

在多台服务器部署时,用户请求可能落到不同服务器,需要解决Session共享问题。解决方案:

  1. 粘性Session:让同一用户的请求始终落到同一台服务器(但服务器宕机就丢了)

  2. Session复制:在集群间同步Session(网络开销大)

  3. 集中式存储:用Redis统一存储Session(最常用方案)

3.11(使用芋道举例:)芋道请求处理的全链路流程图


第一阶段:容器启动与监听(Listener)

在请求还没进来之前,Listener 已经完成了舞台的搭建。

  • 关键类JdbcContextHolder 或自定义的 ConfigListener

  • 源码路径 :通常在 yudao-module-system 的初始化逻辑中。

  • 动作

    1. 项目启动,ServletContextListener 触发。

    2. 芋道会加载数据库中的字典数据 (Dict Data)和系统参数到 Redis 缓存。

    3. 结果:此时服务器已准备就绪,等待第一个 HTTP 请求。


第二阶段:安全与协议过滤(Filter)

请求到达 Tomcat 后,首先进入 Filter 链。这是 Spring Security 的主战场。

  1. CacheRequestBodyFilter(最先执行):

    • 动作 :发现这是一个 POST 请求,它把 request.getInputStream() 读出来存进字节数组,包装成 CacheRequestBodyWrapper

    • 目的:为了后面拦截器记日志时,还能读到 Body 数据。

  2. TokenAuthenticationFilter

    • 源码路径yudao-spring-boot-starter-security

    • 动作 :从 Header 提取 Authorization,去 Redis 查这个 Token 对应的 LoginUser

    • 结果 :将用户信息塞入 SecurityContextHolder


第三阶段:进入 Spring MVC 核心(Servlet)

经过所有过滤器后,请求终于传给了 DispatcherServlet。它像个接线员,开始寻找该由哪个 Controller 处理。


第四阶段:业务逻辑拦截(Interceptor)

在进入 Controller 之前,Interceptor 开始细化处理。

  1. TenantContextWebInterceptor(多租户拦截):

    • 动作 :读取 Header 里的 tenant-id

    • 逻辑 :执行 TenantContextHolder.setTenantId(tenantId)

    • 影响 :这行代码非常关键!它利用 ThreadLocal 保证了当前线程后续所有的数据库操作(MyBatis-Plus)都会自动带上这个租户 ID。

  2. ApiAccessLogInterceptor(前置逻辑):

    • 动作:记录下请求开始的时间戳。

第五阶段:业务执行(Controller)

终于到了你写的业务代码,比如 @GetMapping("/get-info")

  • 此时,你可以直接调用 SecurityFrameworkUtils.getLoginUserId() 获取当前用户。

  • 执行 SQL 时,多租户插件已静默生效。


第六阶段:收尾与日志(Interceptor & Filter 后置)
  1. ApiAccessLogInterceptorafterCompletion):

    • 动作 :计算 结束时间 - 开始时间

    • 逻辑 :从之前的 CacheRequestBodyFilter 留下的包装类里取出 Body,连同执行结果一起,异步 写入数据库 system_api_access_log 表。

  2. Listener

    • 如果是请求结束,ServletRequestListener 会清理 ThreadLocal,防止内存泄漏。

⚖️ 核心差异总结表(实战视角)
特性 Filter Interceptor Listener
在芋道里改谁? 认证、XSS、跨域、请求体缓存 租户切换、操作日志、权限细化 缓存预热、系统配置加载
能拿到 Body 吗? 能(通过 Wrapper 改造) 能(前提是 Filter 先包装了) 基本不拿
异常处理 需手动写 JSON 返回 可以交给全局异常处理器 无法感知业务异常
依赖注入 需要通过 FilterRegistrationBean 直接使用 @Autowired 直接使用 @Autowired

阶段四:场景融合------各技术如何协同作战

理解了单个技术的用法,我们来看它们在真实业务场景中如何配合。

场景一:用户登录与权限控制(全流程)

这是一个最经典的场景,几乎串联了所有Java Web核心技术。

流程图示

复制代码
1. 用户访问首页 → Filter拦截检查登录 → 未登录 → 重定向到登录页
2. 用户提交登录表单 → LoginServlet接收请求 → 调用业务层验证 → 验证成功 → 创建Session → 跳转首页
3. 首页加载 → JSP从Session取用户信息 → 动态显示欢迎语
4. 用户注销 → LogoutServlet → 销毁Session → 清理Cookie → 跳转登录页

完整实现

1. 登录页面 login.jsp

复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>登录</title>
</head>
<body>
    <h2>用户登录</h2>
    <%-- 显示错误信息 --%>
    <c:if test="${not empty error}">
        <p style="color:red">${error}</p>
    </c:if>
    
    <form action="${pageContext.request.contextPath}/login" method="post">
        用户名:<input type="text" name="username"><br>
        密码:<input type="password" name="password"><br>
        <input type="checkbox" name="remember">记住我<br>
        <input type="submit" value="登录">
    </form>
</body>
</html>

2. 登录Servlet LoginServlet.java

java 复制代码
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    
    private UserService userService = new UserService(); // 业务层
    
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        // 获取参数
        String username = request.getParameter("username");
        String password = request.getParameter("password");
        String remember = request.getParameter("remember");
        
        // 调用业务层验证
        User user = userService.login(username, password);
        
        if (user != null) {
            // 登录成功,创建Session
            HttpSession session = request.getSession();
            session.setAttribute("user", user);
            session.setMaxInactiveInterval(30 * 60); // 30分钟
            
            // 如果勾选了"记住我",设置Cookie
            if ("on".equals(remember)) {
                Cookie cookie = new Cookie("username", username);
                cookie.setMaxAge(7 * 24 * 60 * 60); // 一周
                cookie.setHttpOnly(true);
                response.addCookie(cookie);
            }
            
            // 重定向到首页
            response.sendRedirect(request.getContextPath() + "/index");
        } else {
            // 登录失败,返回错误信息
            request.setAttribute("error", "用户名或密码错误");
            request.getRequestDispatcher("/login.jsp").forward(request, response);
        }
    }
}

3. 权限控制过滤器 AuthFilter.java

java 复制代码
@WebFilter("/*")
public class AuthFilter implements Filter {
    
    // 不需要登录就能访问的路径
    private static final List<String> ALLOWED_PATHS = Arrays.asList(
        "/login", "/login.jsp", "/register", "/register.jsp", "/css", "/js"
    );
    
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 
            throws IOException, ServletException {
        
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        
        String path = request.getServletPath();
        
        // 放行静态资源和登录相关路径
        if (isAllowedPath(path)) {
            chain.doFilter(request, response);
            return;
        }
        
        // 检查是否登录
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute("user") != null) {
            // 已登录,放行
            chain.doFilter(request, response);
        } else {
            // 未登录,重定向到登录页
            response.sendRedirect(request.getContextPath() + "/login.jsp");
        }
    }
    
    private boolean isAllowedPath(String path) {
        for (String allowed : ALLOWED_PATHS) {
            if (path.startsWith(allowed)) {
                return true;
            }
        }
        return false;
    }
}

4. 首页 index.jsp

复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <title>首页</title>
</head>
<body>
    <h2>欢迎,${sessionScope.user.name}!</h2>
    <p>你的角色:${sessionScope.user.role}</p>
    
    <a href="${pageContext.request.contextPath}/logout">注销</a>
    
    <h3>功能列表</h3>
    <ul>
        <c:forEach items="${functionList}" var="func">
            <li><a href="${func.url}">${func.name}</a></li>
        </c:forEach>
    </ul>
</body>
</html>

5. 注销Servlet LogoutServlet.java

java 复制代码
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        // 销毁Session
        HttpSession session = request.getSession(false);
        if (session != null) {
            session.invalidate();
        }
        
        // 清理记住我的Cookie
        Cookie cookie = new Cookie("username", "");
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        
        // 跳转登录页
        response.sendRedirect(request.getContextPath() + "/login.jsp");
    }
}

场景二:购物车功能(Session的应用)

购物车是Session的典型应用场景------每个用户有自己的购物车,数据保存在服务器端。

java 复制代码
@WebServlet("/cart")
public class CartServlet extends HttpServlet {
    
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        
        String action = request.getParameter("action");
        HttpSession session = request.getSession();
        
        // 从Session获取购物车,如果没有则创建
        Cart cart = (Cart) session.getAttribute("cart");
        if (cart == null) {
            cart = new Cart();
            session.setAttribute("cart", cart);
        }
        
        if ("add".equals(action)) {
            // 添加商品
            String productId = request.getParameter("productId");
            int quantity = Integer.parseInt(request.getParameter("quantity"));
            cart.addItem(productId, quantity);
            
            // 返回购物车总件数(用于页面右上角更新)
            response.setContentType("application/json");
            response.getWriter().write("{\"totalCount\":" + cart.getTotalCount() + "}");
            
        } else if ("list".equals(action)) {
            // 查看购物车
            request.setAttribute("cart", cart);
            request.getRequestDispatcher("/cart.jsp").forward(request, response);
        }
    }
}

场景三:应用启动初始化(Listener的应用)

很多应用需要在启动时加载配置、初始化线程池、预热缓存等,这时就用到了ServletContextListener

java 复制代码
@WebListener
public class AppInitListener implements ServletContextListener {
    
    public void contextInitialized(ServletContextEvent sce) {
        ServletContext context = sce.getServletContext();
        
        // 1. 加载配置文件
        String configPath = context.getInitParameter("configLocation");
        Properties props = loadConfig(configPath);
        
        // 2. 初始化数据库连接池
        DataSource dataSource = createDataSource(props);
        context.setAttribute("dataSource", dataSource);
        
        // 3. 预热缓存(加载热门数据)
        CacheManager.init(dataSource);
        
        // 4. 启动定时任务
        ScheduleManager.start();
        
        context.log("应用启动完成");
    }
    
    public void contextDestroyed(ServletContextEvent sce) {
        // 释放资源
        CacheManager.shutdown();
        ScheduleManager.stop();
        sce.getServletContext().log("应用关闭,资源已释放");
    }
}

阶段五:企业级实战------构建完整项目

理论知识学完了,我们动手做一个小型但完整的项目,模拟企业开发的全流程。

项目:员工信息管理系统

功能需求

  • 员工信息列表展示(分页)

  • 添加新员工

  • 编辑员工信息

  • 删除员工

  • 部门管理

  • 用户登录/权限控制

技术栈

  • Servlet 4.0(处理请求)

  • JSP + JSTL + EL(展示页面)

  • Filter(权限控制、编码处理)

  • Listener(加载初始化数据)

  • Cookie(记住我功能)

  • Session(用户登录状态)

  • JDBC(数据库操作)

  • MySQL(数据库)

  • Tomcat 9(服务器)

5.1 项目结构

复制代码
src/
├── main/
│   ├── java/
│   │   └── com/example/ems/
│   │       ├── controller/        # Servlet(控制器)
│   │       │   ├── EmployeeServlet.java
│   │       │   ├── DepartmentServlet.java
│   │       │   ├── LoginServlet.java
│   │       │   └── LogoutServlet.java
│   │       ├── service/            # 业务逻辑层
│   │       │   ├── EmployeeService.java
│   │       │   └── DepartmentService.java
│   │       ├── dao/                 # 数据访问层
│   │       │   ├── EmployeeDAO.java
│   │       │   └── DepartmentDAO.java
│   │       ├── entity/              # 实体类
│   │       │   ├── Employee.java
│   │       │   └── Department.java
│   │       ├── filter/              # 过滤器
│   │       │   ├── AuthFilter.java
│   │       │   └── EncodingFilter.java
│   │       ├── listener/            # 监听器
│   │       │   └── AppInitListener.java
│   │       ├── util/                # 工具类
│   │       │   ├── DBUtil.java
│   │       │   └── PageUtil.java
│   │       └── config/              # 配置
│   │           └── Constants.java
│   └── webapp/
│       ├── WEB-INF/
│       │   └── web.xml
│       ├── view/                     # JSP页面
│       │   ├── employee/
│       │   │   ├── list.jsp
│       │   │   ├── add.jsp
│       │   │   └── edit.jsp
│       │   ├── department/
│       │   │   └── list.jsp
│       │   ├── login.jsp
│       │   └── index.jsp
│       └── static/                    # 静态资源
│           ├── css/
│           └── js/

5.2 核心代码实现

实体类 Employee.java
java 复制代码
public class Employee {
    private int id;
    private String name;
    private String gender;
    private int age;
    private String position;
    private int deptId;
    private Date hireDate;
    private Department department; // 关联部门对象
    
    // getter/setter 省略
}
数据访问层 EmployeeDAO.java
java 复制代码
public class EmployeeDAO {
    
    private DataSource dataSource;
    
    public EmployeeDAO() {
        this.dataSource = DBUtil.getDataSource();
    }
    
    // 分页查询员工(含部门名称)
    public List<Employee> findEmployeesByPage(int pageNum, int pageSize) {
        String sql = "SELECT e.*, d.name as dept_name FROM employee e " +
                     "LEFT JOIN department d ON e.dept_id = d.id " +
                     "LIMIT ?, ?";
        int offset = (pageNum - 1) * pageSize;
        
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, offset);
            pstmt.setInt(2, pageSize);
            
            ResultSet rs = pstmt.executeQuery();
            List<Employee> list = new ArrayList<>();
            while (rs.next()) {
                Employee emp = new Employee();
                emp.setId(rs.getInt("id"));
                emp.setName(rs.getString("name"));
                emp.setGender(rs.getString("gender"));
                emp.setAge(rs.getInt("age"));
                emp.setPosition(rs.getString("position"));
                emp.setDeptId(rs.getInt("dept_id"));
                emp.setHireDate(rs.getDate("hire_date"));
                
                // 设置关联部门信息
                Department dept = new Department();
                dept.setId(rs.getInt("dept_id"));
                dept.setName(rs.getString("dept_name"));
                emp.setDepartment(dept);
                
                list.add(emp);
            }
            return list;
        } catch (SQLException e) {
            throw new RuntimeException("查询员工失败", e);
        }
    }
    
    // 获取总记录数(用于分页)
    public int countEmployees() {
        String sql = "SELECT COUNT(*) FROM employee";
        try (Connection conn = dataSource.getConnection();
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {
            rs.next();
            return rs.getInt(1);
        } catch (SQLException e) {
            throw new RuntimeException("统计员工失败", e);
        }
    }
    
    // 新增员工
    public void insertEmployee(Employee emp) {
        String sql = "INSERT INTO employee(name, gender, age, position, dept_id, hire_date) " +
                     "VALUES(?, ?, ?, ?, ?, ?)";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, emp.getName());
            pstmt.setString(2, emp.getGender());
            pstmt.setInt(3, emp.getAge());
            pstmt.setString(4, emp.getPosition());
            pstmt.setInt(5, emp.getDeptId());
            pstmt.setDate(6, new java.sql.Date(emp.getHireDate().getTime()));
            pstmt.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("新增员工失败", e);
        }
    }
    
    // 更新员工
    public void updateEmployee(Employee emp) {
        String sql = "UPDATE employee SET name=?, gender=?, age=?, position=?, dept_id=?, hire_date=? " +
                     "WHERE id=?";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setString(1, emp.getName());
            pstmt.setString(2, emp.getGender());
            pstmt.setInt(3, emp.getAge());
            pstmt.setString(4, emp.getPosition());
            pstmt.setInt(5, emp.getDeptId());
            pstmt.setDate(6, new java.sql.Date(emp.getHireDate().getTime()));
            pstmt.setInt(7, emp.getId());
            pstmt.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("更新员工失败", e);
        }
    }
    
    // 删除员工
    public void deleteEmployee(int id) {
        String sql = "DELETE FROM employee WHERE id=?";
        try (Connection conn = dataSource.getConnection();
             PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.setInt(1, id);
            pstmt.executeUpdate();
        } catch (SQLException e) {
            throw new RuntimeException("删除员工失败", e);
        }
    }
}
业务层 EmployeeService.java
java 复制代码
public class EmployeeService {
    
    private EmployeeDAO employeeDAO = new EmployeeDAO();
    
    public PageResult<Employee> getEmployeesByPage(int pageNum, int pageSize) {
        // 校验参数
        if (pageNum < 1) pageNum = 1;
        if (pageSize < 1) pageSize = 10;
        if (pageSize > 100) pageSize = 100; // 防止页大小过大
        
        // 查询数据
        List<Employee> list = employeeDAO.findEmployeesByPage(pageNum, pageSize);
        int total = employeeDAO.countEmployees();
        
        // 计算总页数
        int totalPages = (int) Math.ceil((double) total / pageSize);
        
        return new PageResult<>(list, pageNum, pageSize, total, totalPages);
    }
    
    public void addEmployee(Employee emp) {
        // 业务校验
        if (emp.getName() == null || emp.getName().trim().isEmpty()) {
            throw new BusinessException("员工姓名不能为空");
        }
        if (emp.getAge() < 18 || emp.getAge() > 65) {
            throw new BusinessException("年龄必须在18-65岁之间");
        }
        
        employeeDAO.insertEmployee(emp);
    }
    
    // 其他业务方法...
}
控制器 EmployeeServlet.java
java 复制代码
@WebServlet("/employee/*")
public class EmployeeServlet extends HttpServlet {
    
    private EmployeeService employeeService = new EmployeeService();
    private DepartmentService departmentService = new DepartmentService();
    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        String pathInfo = request.getPathInfo();
        
        if (pathInfo == null || "/list".equals(pathInfo)) {
            list(request, response);
        } else if ("/add".equals(pathInfo)) {
            showAddForm(request, response);
        } else if ("/edit".equals(pathInfo)) {
            showEditForm(request, response);
        } else if ("/delete".equals(pathInfo)) {
            delete(request, response);
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
    
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        String pathInfo = request.getPathInfo();
        
        if ("/add".equals(pathInfo)) {
            add(request, response);
        } else if ("/update".equals(pathInfo)) {
            update(request, response);
        } else {
            response.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
    }
    
    // 员工列表(带分页)
    private void list(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        // 获取页码参数
        int pageNum = 1;
        String pageParam = request.getParameter("page");
        if (pageParam != null && !pageParam.isEmpty()) {
            pageNum = Integer.parseInt(pageParam);
        }
        
        int pageSize = 10; // 可配置化
        
        // 调用业务层
        PageResult<Employee> pageResult = employeeService.getEmployeesByPage(pageNum, pageSize);
        
        // 存入request
        request.setAttribute("pageResult", pageResult);
        
        // 转发到JSP
        request.getRequestDispatcher("/WEB-INF/view/employee/list.jsp")
               .forward(request, response);
    }
    
    // 新增员工表单
    private void showAddForm(HttpServletRequest request, HttpServletResponse response) 
            throws ServletException, IOException {
        // 获取部门列表(用于下拉框)
        List<Department> depts = departmentService.getAllDepartments();
        request.setAttribute("departments", depts);
        request.getRequestDispatcher("/WEB-INF/view/employee/add.jsp")
               .forward(request, response);
    }
    
    // 新增员工提交
    private void add(HttpServletRequest request, HttpServletResponse response) 
            throws IOException, ServletException {
        try {
            // 参数封装
            Employee emp = new Employee();
            emp.setName(request.getParameter("name"));
            emp.setGender(request.getParameter("gender"));
            emp.setAge(Integer.parseInt(request.getParameter("age")));
            emp.setPosition(request.getParameter("position"));
            emp.setDeptId(Integer.parseInt(request.getParameter("deptId")));
            
            String hireDateStr = request.getParameter("hireDate");
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
            emp.setHireDate(sdf.parse(hireDateStr));
            
            // 调用业务层
            employeeService.addEmployee(emp);
            
            // 成功,重定向到列表页(带成功消息)
            response.sendRedirect(request.getContextPath() + "/employee/list?success=添加成功");
            
        } catch (Exception e) {
            // 失败,返回表单并显示错误
            request.setAttribute("error", e.getMessage());
            request.setAttribute("departments", departmentService.getAllDepartments());
            request.getRequestDispatcher("/WEB-INF/view/employee/add.jsp")
                   .forward(request, response);
        }
    }
    
    // 其他方法...
}
列表页面 list.jsp(JSTL+EL)
复制代码
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<html>
<head>
    <title>员工管理</title>
    <link rel="stylesheet" href="${pageContext.request.contextPath}/static/css/style.css">
</head>
<body>
    <div class="container">
        <h2>员工列表</h2>
        
        <%-- 显示操作消息 --%>
        <c:if test="${not empty param.success}">
            <div class="alert alert-success">${param.success}</div>
        </c:if>
        <c:if test="${not empty param.error}">
            <div class="alert alert-error">${param.error}</div>
        </c:if>
        
        <div class="toolbar">
            <a href="${pageContext.request.contextPath}/employee/add" class="btn btn-primary">
                新增员工
            </a>
        </div>
        
        <table class="table">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>姓名</th>
                    <th>性别</th>
                    <th>年龄</th>
                    <th>职位</th>
                    <th>部门</th>
                    <th>入职日期</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody>
                <c:forEach items="${pageResult.list}" var="emp">
                    <tr>
                        <td>${emp.id}</td>
                        <td>${emp.name}</td>
                        <td>${emp.gender}</td>
                        <td>${emp.age}</td>
                        <td>${emp.position}</td>
                        <td>${emp.department.name}</td>
                        <td><fmt:formatDate value="${emp.hireDate}" pattern="yyyy-MM-dd"/></td>
                        <td>
                            <a href="${pageContext.request.contextPath}/employee/edit?id=${emp.id}">
                                编辑
                            </a>
                            <a href="${pageContext.request.contextPath}/employee/delete?id=${emp.id}"
                               οnclick="return confirm('确定删除吗?')">
                                删除
                            </a>
                        </td>
                    </tr>
                </c:forEach>
                
                <c:if test="${empty pageResult.list}">
                    <tr>
                        <td colspan="8" class="text-center">暂无数据</td>
                    </tr>
                </c:if>
            </tbody>
        </table>
        
        <%-- 分页组件 --%>
        <div class="pagination">
            <c:if test="${pageResult.pageNum > 1}">
                <a href="?page=${pageResult.pageNum-1}">上一页</a>
            </c:if>
            
            <c:forEach begin="1" end="${pageResult.totalPages}" var="i">
                <c:choose>
                    <c:when test="${i == pageResult.pageNum}">
                        <span class="current">${i}</span>
                    </c:when>
                    <c:otherwise>
                        <a href="?page=${i}">${i}</a>
                    </c:otherwise>
                </c:choose>
            </c:forEach>
            
            <c:if test="${pageResult.pageNum < pageResult.totalPages}">
                <a href="?page=${pageResult.pageNum+1}">下一页</a>
            </c:if>
            
            <span>共 ${pageResult.total} 条记录</span>
        </div>
    </div>
</body>
</html>

5.3 企业开发规范落地

在这个项目中,我们遵循了以下企业开发规范:

  1. 分层架构:Controller(Servlet)→ Service(业务逻辑)→ DAO(数据访问)→ Entity(实体),各层职责清晰

  2. 命名规范

    • 类名:大驼峰,如EmployeeService

    • 方法名:小驼峰,如getEmployeesByPage

    • 常量:全大写下划线,如MAX_PAGE_SIZE

  3. 异常处理 :自定义BusinessException,在Controller层统一处理并展示给用户

  4. 日志记录 :使用ServletContext.log()或集成Log4j记录关键操作

  5. 配置分离 :数据库连接、分页大小等配置在Constants.java或配置文件中

  6. 参数校验:在Service层进行业务校验,避免脏数据进入数据库

  7. 防止SQL注入:使用PreparedStatement代替Statement

5.4 测试与部署

单元测试(JUnit)

java 复制代码
public class EmployeeServiceTest {
    
    private EmployeeService employeeService;
    
    @Before
    public void setUp() {
        employeeService = new EmployeeService();
    }
    
    @Test
    public void testAddEmployee_ValidData() {
        Employee emp = new Employee();
        emp.setName("张三");
        emp.setAge(25);
        emp.setGender("男");
        emp.setPosition("开发");
        emp.setDeptId(1);
        emp.setHireDate(new Date());
        
        // 应正常执行,不抛出异常
        employeeService.addEmployee(emp);
    }
    
    @Test(expected = BusinessException.class)
    public void testAddEmployee_InvalidAge() {
        Employee emp = new Employee();
        emp.setName("李四");
        emp.setAge(16); // 年龄太小,应抛出异常
        // 其他属性...
        employeeService.addEmployee(emp);
    }
}

部署步骤

  1. Maven打包:mvn clean package,生成ems.war

  2. 将war包复制到Tomcat的webapps目录

  3. 启动Tomcat:bin/startup.sh(Linux)或bin/startup.bat(Windows)

  4. 访问:http://localhost:8080/ems/


阶段六:复盘升华------从会用到用好

6.1 最佳实践总结

经过前面的学习和实战,我们来提炼一些企业开发中经过验证的最佳实践。

Servlet最佳实践
  • 一个Servlet只处理一类业务:不要写万能Servlet,按模块拆分(如EmployeeServlet、DepartmentServlet)

  • 使用路径映射简化URL@WebServlet("/employee/*") + 路径分发

  • 避免在Servlet中写业务逻辑:Servlet只做参数解析、调用Service、页面跳转

JSP最佳实践
  • 杜绝Java脚本(<% ... %>):全部用EL+JSTL替代,页面更干净

  • 使用JSP包含机制复用页面<%@ include file="header.jsp" %><jsp:include>

  • JSP放在WEB-INF下:防止直接访问,只能通过Servlet转发

Filter最佳实践
  • Filter顺序要合理:编码Filter在前,权限Filter在后

  • 不要过度使用Filter:影响性能,只做横切关注点的事情

  • 使用@WebFilterurlPatterns精确控制拦截范围

Session最佳实践
  • Session中只存必要数据:不要存大对象,影响内存和序列化性能

  • 设置合理的超时时间 :web.xml中配置<session-timeout>30</session-timeout>

  • 敏感信息加密存储:用户角色、权限等敏感信息可以考虑加密

  • 登录后重置Session ID:防止Session Fixation攻击

  • 集群环境使用集中式存储:Redis统一管理Session

Cookie最佳实践
  • 设置HttpOnly和Secure:防止XSS窃取和HTTP明文传输

  • 不要存敏感信息:密码、令牌等不能存Cookie

  • 设置合理的Path:避免Cookie被无关路径携带

  • 敏感Cookie设置较短有效期

6.2 技术演进路线

Java Web技术一直在演进,了解发展脉络有助于理解为什么有些技术现在用得少了:

1. 视图层演进
复制代码
JSP(早期)→ JSP+JSTL(中期)→ FreeMarker/Velocity → Thymeleaf(现代)

JSP现在逐渐被模板引擎取代,因为前后端分离趋势下,后端更专注于提供API。

2. 控制层演进
复制代码
Servlet(原始)→ Struts2 → Spring MVC(现代)

Spring MVC本质上还是Servlet,但封装得极其优雅,成为事实标准。

3. 数据层演进
复制代码
JDBC(原始)→ MyBatis → JPA/Hibernate → MyBatis-Plus/Spring Data JPA

现代开发很少直接写JDBC,都用ORM框架简化数据库操作。

4. 构建方式演进
复制代码
手动部署 → Ant → Maven → Gradle

现代项目几乎都用Maven或Gradle管理依赖和构建。

5. 架构演进
复制代码
单体应用 → 垂直拆分 → 分布式服务 → 微服务 → Service Mesh

Java Web从单一的JSP+Servlet,发展到现在复杂的微服务架构。

6.3 高频面试题

Q1:Servlet是单例还是多例?线程安全吗?

A:Servlet默认单例多线程。容器只创建一个Servlet实例,每个请求由不同线程执行。因此成员变量需要考虑线程安全,尽量不定义可修改的成员变量。

Q2:Cookie和Session的区别?

A:

  • Cookie存在客户端,Session存在服务端

  • Cookie容量小(4KB),Session容量理论上无限

  • Cookie相对不安全(可被篡改),Session安全

  • 典型应用:记住我功能用Cookie,登录状态用Session

Q3:请求转发(forward)和重定向(sendRedirect)的区别?

A:

  • 转发是服务器内部跳转,URL不变,可以携带request范围内的数据

  • 重定向是浏览器重新发起请求,URL改变,无法直接携带request数据

  • 转发只能跳转到站内资源,重定向可以站外

Q4:Filter和Interceptor有什么区别?

A:Filter是Servlet规范的一部分,基于回调函数,只能在Servlet前后起作用;Interceptor是Spring MVC的概念,基于反射,更细粒度,可以深入到方法调用前后。

Q5:Session共享问题怎么解决?

A:

  1. 粘性Session:负载均衡配置同一用户始终访问同一服务器

  2. Session复制:Tomcat自带集群会话复制功能

  3. 集中式存储:Redis统一存储Session(最推荐)

  4. 客户端存储:JWT等令牌机制,服务端无状态

Q6:JSP的9大内置对象有哪些?

A: request、response、session、application、out、pageContext、config、page、exception

Q7:如果客户端禁用了Cookie,Session还能用吗?

A:能,但需要URL重写。通过response.encodeURL()在URL后面附加jsessionid=xxx

Q8:如何防止Session固定攻击?

A: 用户登录成功后,调用session.invalidate()销毁旧Session,然后创建新Session。


写在最后

Java Web开发涉及的知识点看起来很多,但它们并不是孤立的。Servlet是地基,JSP是展示,Filter是拦截,Listener是监听,Session是状态,Cookie是标识------它们组合在一起,才构成了一个完整的Web应用。

希望这篇长达两万字的指南,能帮你建立起Java Web的知识体系。从「问题锚定」开始,到「基础认知」理解概念,到「核心用法」掌握操作,到「场景融合」看到配合,到「实战落地」完整开发,再到「复盘升华」总结经验------这正是企业开发中需要具备的思维方式。

如果你读完这篇文章,能动手把那个员工管理系统做出来,那么恭喜你,你已经具备了Java Web开发的基础能力。接下来就是不断练习、不断踩坑、不断总结的过程。

技术在变,但解决问题的本质不变。理解每个技术「为什么存在」,比记住「怎么用」更重要。

祝你在Java Web的世界里玩得开心!

相关推荐
无名-CODING14 小时前
Java 爬虫进阶:动态网页、多线程与 WebMagic 框架实战
java·爬虫·okhttp
weixin_7042660514 小时前
Spring 注解驱动开发与 Spring Boot 核心知识点梳理
java·spring boot·spring
开开心心就好14 小时前
伪装文件历史记录!修改时间的黑科技软件
java·前端·科技·r语言·edge·pdf·语音识别
8Qi814 小时前
Redis哨兵模式(Sentinel)深度解析
java·数据库·redis·分布式·缓存·sentinel
饼干哥哥14 小时前
聊了50个AI出海的市场团队,我总结了达人营销的7宗罪
前端
词元Max14 小时前
2.5 Python 类型注解与运行时类型检查
开发语言·python
qq_4275060814 小时前
vscode使用kimi code的简单经验分享
前端·vscode·ai编程
恋猫de小郭14 小时前
Claude Code 源码里有意思设定:伪造、投毒、卧底、封号
前端·人工智能·ai编程
wangchunting14 小时前
数据结构-树
java·数据结构
无籽西瓜a14 小时前
【西瓜带你学设计模式 | 第五期 - 建造者模式】建造者模式 —— 产品构建实现、优缺点与适用场景及模式区别
java·后端·设计模式·软件工程·建造者模式