*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的世界里玩得开心!

相关推荐
青柠代码录2 小时前
【Vue3】SCSS 基础篇
前端·scss
li星野2 小时前
QT面试题
java·数据库·qt
不光头强2 小时前
抽象类和接口的区别
java·开发语言·python
阳火锅2 小时前
AI时代的到来,我想打造这样一款产品。
前端·javascript·vue.js
xiaoye37082 小时前
Spring 的自动装配 vs 手动注入
java·spring
好学且牛逼的马2 小时前
Spring Boot 核心注解完全手册
java·spring boot·后端
llxxyy卢2 小时前
polar-web题目
开发语言·前端·javascript
OpenTiny社区2 小时前
不仅是修复 Bug:TinyVue 3.29.0 把“无障碍信息”写进了组件的 DNA 里
前端·javascript·vue.js
鹿鹿鹿鹿isNotDefined2 小时前
Pixelium Design 更新:首版表格上线,完善表单、导航、反馈及视觉组件
前端·javascript