1. JSP与Servlet的本质区别与协作关系
在Java Web开发领域,JSP(JavaServer Pages)和Servlet是两种基础且核心的技术组件。虽然它们最终都会被编译为Servlet运行,但在实际开发中却承担着不同的职责。
1.1 技术定位差异
JSP本质上是一种简化版的Servlet,它允许开发者将Java代码直接嵌入HTML页面中。这种设计使得JSP特别适合处理视图层的展示逻辑。在实际项目中,我们通常使用JSP来:
- 动态生成HTML内容
- 展示从数据库查询的结果集
- 处理页面布局和样式渲染
而Servlet则是纯粹的Java类,它更擅长处理业务逻辑和控制流程。典型的Servlet应用场景包括:
- 接收和验证用户输入
- 调用业务逻辑层处理数据
- 控制页面跳转和流程转向
1.2 代码组织方式对比
从代码结构来看,Servlet采用严格的Java类写法,所有HTML内容都需要通过字符串拼接或输出流写入:
java复制PrintWriter out = response.getWriter();
out.println("<html><body>");
out.println("<h1>Hello World</h1>");
out.println("</body></html>");
而JSP则采用更自然的混合写法,可以直接在HTML中嵌入Java代码:
jsp复制<html>
<body>
<h1>当前时间:<%= new java.util.Date() %></h1>
</body>
</html>
1.3 性能与编译过程
虽然JSP最终会被转换为Servlet,但两者的编译时机有所不同:
- JSP在首次被访问时才会被编译为Servlet类
- 后续请求直接调用已编译的类
- Servlet则是直接编写Java代码,无需额外转换
在实际生产环境中,我们通常会进行JSP预编译来避免首次访问的延迟问题。可以通过在web.xml中配置jsp-config或使用应用服务器的管理工具实现。
提示:现代Java Web开发中,通常建议遵循MVC模式,使用Servlet作为Controller,JSP仅负责View层展示。避免在JSP中编写复杂业务逻辑。
2. JSP内置对象深度解析与应用场景
JSP提供了9个内置对象,这些对象无需声明即可直接使用。理解它们的生命周期和作用范围对开发高质量的Web应用至关重要。
2.1 核心内置对象功能详解
| 对象名称 | 类型 | 作用域 | 典型应用场景 |
|---|---|---|---|
| request | HttpServletRequest | request | 获取客户端请求参数、头信息 |
| response | HttpServletResponse | page | 设置响应内容、重定向 |
| session | HttpSession | session | 存储用户会话数据 |
| application | ServletContext | application | 存储全局共享数据 |
| out | JspWriter | page | 向客户端输出内容 |
| config | ServletConfig | page | 获取Servlet配置信息 |
| page | Object | page | 当前页面实例(相当于this) |
| pageContext | PageContext | page | 获取其他所有内置对象 |
| exception | Throwable | page | 处理页面异常(需isErrorPage=true) |
2.2 关键对象使用技巧
request对象的最佳实践:
- 使用
getParameter()获取表单数据时,应考虑字符编码问题 - 对于文件上传,需要检查
contentType是否为multipart/form-data - 通过
setAttribute()传递数据给JSP时,建议使用统一的前缀避免命名冲突
session对象的注意事项:
- 不宜存储大量数据,会占用服务器内存
- 默认使用Cookie实现,应考虑浏览器禁用Cookie的情况
- 分布式环境下需要特殊处理,如使用Redis存储session
pageContext对象的高级用法:
jsp复制<%
// 获取其他内置对象
HttpServletRequest req = (HttpServletRequest)pageContext.getRequest();
// 跨作用域查找属性
Object value = pageContext.findAttribute("key");
// 管理页面作用域属性
pageContext.setAttribute("key", "value", PageContext.REQUEST_SCOPE);
%>
3. JSP作用域机制与数据共享策略
JSP的四种作用域决定了数据的可见范围和生命周期,合理选择作用域可以优化内存使用并保证数据安全。
3.1 作用域对比分析
| 作用域 | 实现类 | 生命周期 | 适用场景 |
|---|---|---|---|
| page | PageContext | 当前页面执行期间 | 页面内部临时变量 |
| request | HttpServletRequest | 一次请求期间(可能跨多个页面) | 转发(forward)时传递数据 |
| session | HttpSession | 用户会话期间 | 用户登录状态、个性化设置 |
| application | ServletContext | 应用运行期间 | 全局配置、共享资源 |
3.2 作用域选择原则
-
尽量使用最小作用域:能使用page就不使用request,能使用request就不使用session,以此减少服务器资源占用。
-
线程安全考虑:
- application作用域的数据需要同步控制
- session作用域在用户并发请求时也可能存在线程问题
-
分布式环境适配:
- session数据需要支持序列化
- 考虑使用集中式存储替代本地session
3.3 作用域典型使用模式
页面间数据传递:
java复制// Servlet中设置request属性
request.setAttribute("message", "操作成功");
request.getRequestDispatcher("/result.jsp").forward(request, response);
// JSP中获取数据
${requestScope.message}
用户状态保持:
jsp复制<%
// 登录成功后设置session
session.setAttribute("user", userObj);
// 其他页面检查登录状态
if(session.getAttribute("user") == null) {
response.sendRedirect("login.jsp");
}
%>
全局计数器实现:
jsp复制<%
Integer count = (Integer)application.getAttribute("visitorCount");
if(count == null) count = 0;
application.setAttribute("visitorCount", count + 1);
%>
总访问量:${applicationScope.visitorCount}
4. Session与Cookie机制全解析
4.1 存储机制对比
Session存储特点:
- 服务器端存储,安全性较高
- 默认使用内存存储,重启服务器会丢失
- 可配置为持久化到数据库或Redis
- 存储数据大小理论上只受服务器内存限制
Cookie存储特点:
- 客户端浏览器存储,存在安全风险
- 数据会随每个请求自动发送到服务器
- 单个Cookie通常不超过4KB
- 每个域名下的Cookie数量有限制(通常20个左右)
4.2 安全实践指南
增强Session安全性:
- 定期更换session ID
- 设置HttpOnly和Secure标志
- 实现session超时机制
- 对敏感操作要求重新认证
Cookie安全配置:
java复制Cookie cookie = new Cookie("token", "abc123");
cookie.setHttpOnly(true); // 防止XSS攻击
cookie.setSecure(true); // 仅HTTPS传输
cookie.setMaxAge(3600); // 有效期1小时
response.addCookie(cookie);
4.3 无Cookie环境下的Session维护
当浏览器禁用Cookie时,可以通过URL重写保持session:
java复制String url = response.encodeURL("product.jsp");
out.println("<a href=\"" + url + "\">产品列表</a>");
或者使用隐藏表单字段:
html复制<form action="checkout" method="post">
<input type="hidden" name="jsessionid" value="<%=session.getId()%>">
<!-- 其他表单字段 -->
</form>
重要提示:URL重写会暴露session ID,存在安全风险,应确保使用HTTPS协议传输。
5. Spring MVC与Struts框架深度对比
5.1 架构设计差异
Spring MVC核心特点:
- 方法级别拦截,更细粒度控制
- 与Spring容器无缝集成
- 支持注解驱动开发
- RESTful风格原生支持
Struts2核心特点:
- 类级别拦截,通过配置文件定义动作
- 内置拦截器栈提供丰富功能
- 值栈和OGNL表达式简化数据访问
- 插件机制扩展性强
5.2 性能与开发效率
Spring MVC优势:
- 方法独立,减少线程安全问题
- 注解配置更简洁
- 与Spring生态完美整合
- 测试驱动开发更友好
Struts2优势:
- 内置功能丰富,如表单验证、类型转换
- 标签库功能强大
- 成熟的插件生态系统
- 对传统Java EE开发者更友好
5.3 迁移与选型建议
从Struts迁移到Spring MVC:
- 将Action类改为Controller类
- 使用@Controller和@RequestMapping注解
- 用方法参数替代值栈访问
- 使用Spring的验证框架替代Struts验证
新项目选型考虑因素:
- 团队技术储备
- 项目规模和复杂度
- 与其他系统的集成需求
- 长期维护成本
6. Web安全防护实战方案
6.1 SQL注入防御体系
多层次防护策略:
- 输入验证:使用正则表达式过滤特殊字符
java复制if(!Pattern.matches("^[a-zA-Z0-9_]+$", input)) { throw new IllegalArgumentException("非法输入"); } - 参数化查询:必须使用PreparedStatement
java复制String sql = "SELECT * FROM users WHERE username = ?"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, username); - 最小权限原则:数据库用户只授予必要权限
- ORM框架安全使用:注意HQL/SQL注入可能
6.2 XSS攻击全面防护
防御矩阵实施:
- 输入过滤:去除或转义特殊HTML字符
java复制String safe = input.replace("<", "<") .replace(">", ">"); - 输出编码:使用JSTL的c:out标签
jsp复制<c:out value="${userInput}" escapeXml="true"/> - 内容安全策略(CSP):
html复制<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline'"> - HttpOnly Cookie:防止JavaScript窃取
6.3 CSRF防护最佳实践
多因素防护方案:
- 同步令牌模式:
jsp复制<form action="/transfer" method="post"> <input type="hidden" name="csrfToken" value="${csrfToken}"> <!-- 其他表单字段 --> </form> - 双重Cookie验证:
javascript复制// 前端设置 document.cookie = "csrfToken=abc123; SameSite=Strict"; - 关键操作二次认证:如短信验证码
- SameSite Cookie属性:
java复制Cookie cookie = new Cookie("sessionId", "xyz"); cookie.setSameSite("Strict");
7. Servlet生命周期与高级特性
7.1 生命周期扩展控制
除了基本的init-service-destroy流程,Servlet还支持更精细的生命周期控制:
加载时机控制:
xml复制<servlet>
<servlet-name>Initializer</servlet-name>
<servlet-class>com.example.InitializerServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
异步处理支持:
java复制@WebServlet(urlPatterns="/async", asyncSupported=true)
public class AsyncServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
AsyncContext ctx = req.startAsync();
executor.submit(() -> {
// 长时间处理
ctx.getResponse().getWriter().write("Done");
ctx.complete();
});
}
}
7.2 线程安全实践
Servlet是单例多线程模型,必须注意线程安全问题:
常见陷阱:
- 使用实例变量存储请求相关数据
- 在service方法中修改共享资源
- 不正确的缓存实现
安全模式:
- 使用局部变量而非实例变量
- 对共享资源进行同步控制
- 使用线程安全集合类
- 考虑使用ThreadLocal存储线程特定数据
7.3 过滤器与监听器高级应用
过滤器链实现:
java复制@WebFilter("/*")
public class LoggingFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) {
long start = System.currentTimeMillis();
chain.doFilter(req, res); // 继续过滤器链
long elapsed = System.currentTimeMillis() - start;
System.out.println("请求处理时间:" + elapsed + "ms");
}
}
监听器典型场景:
java复制@WebListener
public class AppListener implements ServletContextListener {
public void contextInitialized(ServletContextEvent sce) {
// 应用启动时初始化资源
DataSource ds = createDataSource();
sce.getServletContext().setAttribute("dataSource", ds);
}
public void contextDestroyed(ServletContextEvent sce) {
// 应用关闭时释放资源
DataSource ds = (DataSource)sce.getServletContext()
.getAttribute("dataSource");
shutdownDataSource(ds);
}
}
在实际项目中,我通常会建立一套完整的Servlet生命周期监控体系,通过组合使用过滤器和监听器,实现对请求处理时间的统计、异常的统一处理以及资源的自动化管理。特别是在高并发场景下,合理的生命周期管理能显著提升应用稳定性和性能。