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

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

写在前面

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

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

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

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

场景化抛出问题

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

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

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

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

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

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

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

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

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

技术定位梳理

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

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

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

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

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

技术边界说明

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

但Java Web不是万能的:

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

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

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

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

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

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

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

核心概念拆解

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

最简单的HTML表单长这样:

用户名:

密码:

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

为什么要懂一点前端?

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

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

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

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

张三

25

最小示例:

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

LoginServlet

com.example.LoginServlet

LoginServlet

/login

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

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

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

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

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

json

{

"name": "张三",

"age": 25,

"hobbies": ["读书", "编程"]

}

对比XML:

XML:张三25

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

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

最小示例:

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

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:

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("");

out.println("

Hello, Java Web!

");

out.println("");

}

}

在web.xml中配置访问路径:

HelloServlet

com.example.HelloServlet

HelloServlet

/hello

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

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

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

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

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

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

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

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

最小示例:

创建一个hello.jsp文件:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

当前时间:<%= new java.util.Date() %>

<%-- 这是JSP注释 --%>

<%

String name = request.getParameter("name");

if (name != null) {

out.println("你好," + name);

}

%>

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

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

客户端请求JSP页面

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

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

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

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

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

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

最小示例:

使用JSTL的遍历列表:

<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>

  • ${user.name} - ${user.age}岁

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

Filter:请求的「安检门」

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

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

最小示例:

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

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应用中的各种事件:应用启动/关闭、会话创建/销毁、属性添加/移除等。

最小示例:

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

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的生命周期:

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

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

销毁:超时未访问、主动调用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核心标签:

<%-- 条件判断 --%>

欢迎,${user.name}

<%-- 多条件选择 --%>

优秀

及格

不及格

<%-- 循环 --%>

第${status.count}个:${item.name}

<%-- 格式化日期 --%>

3.7 Filter:拦截与控制

配置方式:

web.xml配置:

AuthFilter

com.example.AuthFilter

AuthFilter

/*

注解配置(Servlet 3.0+):

@WebFilter("/*")

public class AuthFilter implements Filter { ... }

常见应用场景:

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

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

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

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

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

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

3.8 Listener:监听与回调

监听器类型:

ServletContextListener:监听应用启动/关闭

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

ServletRequestListener:监听请求到达/结束

各种属性监听器:ServletContextAttributeListener、HttpSessionAttributeListener、ServletRequestAttributeListener

在线人数统计示例:

@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共享问题。解决方案:

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

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

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

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

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

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

关键类:JdbcContextHolder 或自定义的 ConfigListener。

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

动作:

项目启动,ServletContextListener 触发。

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

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

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

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

CacheRequestBodyFilter(最先执行):

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

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

TokenAuthenticationFilter:

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

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

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

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

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

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

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

TenantContextWebInterceptor(多租户拦截):

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

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

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

ApiAccessLogInterceptor(前置逻辑):

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

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

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

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

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

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

ApiAccessLogInterceptor(afterCompletion):

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

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

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" %>

登录

用户登录

<%-- 显示错误信息 --%>

${error}

用户名:

密码:

记住我

2. 登录Servlet LoginServlet.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

@WebFilter("/*")

public class AuthFilter implements Filter {

// 不需要登录就能访问的路径

private static final List 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" %>

首页

欢迎,${sessionScope.user.name}!

你的角色:${sessionScope.user.role}

注销

功能列表

5. 注销Servlet LogoutServlet.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的典型应用场景——每个用户有自己的购物车,数据保存在服务器端。

@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。

@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

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

public class EmployeeDAO {

private DataSource dataSource;

public EmployeeDAO() {

this.dataSource = DBUtil.getDataSource();

}

// 分页查询员工(含部门名称)

public List 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 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

public class EmployeeService {

private EmployeeDAO employeeDAO = new EmployeeDAO();

public PageResult getEmployeesByPage(int pageNum, int pageSize) {

// 校验参数

if (pageNum < 1) pageNum = 1;

if (pageSize < 1) pageSize = 10;

if (pageSize > 100) pageSize = 100; // 防止页大小过大

// 查询数据

List 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

@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 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 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" %>

员工管理

员工列表

<%-- 显示操作消息 --%>

${param.success}

${param.error}

ID 姓名 性别 年龄 职位 部门 入职日期 操作
${emp.id} ${emp.name} ${emp.gender} ${emp.age} ${emp.position} ${emp.department.name}

编辑

οnclick="return confirm('确定删除吗?')">

删除

暂无数据

<%-- 分页组件 --%>

5.3 企业开发规范落地

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

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

命名规范:

类名:大驼峰,如EmployeeService

方法名:小驼峰,如getEmployeesByPage

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

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

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

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

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

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

5.4 测试与部署

单元测试(JUnit):

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);

}

}

部署步骤:

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

将war包复制到Tomcat的webapps目录

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

访问: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放在WEB-INF下:防止直接访问,只能通过Servlet转发

Filter最佳实践

Filter顺序要合理:编码Filter在前,权限Filter在后

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

使用@WebFilter的urlPatterns精确控制拦截范围

Session最佳实践

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

设置合理的超时时间:web.xml中配置30

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

登录后重置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:

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

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

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

客户端存储: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的世界里玩得开心!

相关推荐

BIOS重置方法指南
365bet体育娱乐

BIOS重置方法指南

📅 02-04 👁️ 3562
创维32寸智能电视
365bet是什么

创维32寸智能电视

📅 01-21 👁️ 8395
z标志是什么车
365bet是什么

z标志是什么车

📅 08-29 👁️ 9779