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

简介:Java Web编程是构建互联网应用的核心技术,涵盖服务器端开发、前端交互与数据管理。本资源合集“Java Web开发全栈技术帮助文档合集”整合了四大关键技术手册:Struts2.0、Spring、DHTML和JavaScript,全面覆盖MVC框架、依赖注入、动态网页与客户端脚本等核心内容。经过系统整理与实践验证,该文档合集适用于各层次开发者,助力掌握Java Web开发全流程,提升构建高效、稳定、交互性强的Web应用的能力。
java web编程 帮助文档大全

1. Java Web编程基础概述

Java Web技术的发展历程与核心架构解析

Java Web技术自J2EE时代起逐步演进,历经Servlet、JSP、MVC框架到现代微服务架构的变迁。其核心基于请求-响应模型,依托Servlet容器(如Tomcat)管理Web组件生命周期。整个体系以HTTP协议为基础,通过Servlet处理动态请求,JSP生成视图内容,并逐步形成以MVC为指导的设计范式,实现业务、控制与展示的分离。

Web应用的工作原理与HTTP协议基础

Web应用运行在客户端-服务器模型之上,浏览器发起HTTP请求,服务器解析并返回响应。HTTP作为无状态协议,依赖Cookie/Session机制维持会话。Java Web中,HttpServletRequest和HttpServletResponse接口封装了请求与响应数据流,开发者可通过读取参数、设置头信息等方式控制通信过程。

// 示例:简单的Servlet处理GET请求
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        resp.setContentType("text/html;charset=UTF-8");
        PrintWriter out = resp.getWriter();
        out.println("<h1>Hello World!</h1>");
    }
}

代码说明
- doGet 方法响应HTTP GET请求;
- resp.setContentType 设置响应内容类型;
- PrintWriter 输出HTML内容至客户端。

该示例展示了最基本的请求响应流程,是理解Java Web工作机理的起点。后续章节将在此基础上深入框架集成与全栈开发。

2. Struts2.0 MVC框架原理与配置

Struts2 是一个基于MVC设计模式的Java Web应用框架,它在Web开发中扮演着控制器(Controller)的核心角色。相较于早期的Struts1,Struts2并非其简单升级版本,而是融合了WebWork设计理念的一次重构,具备更强的扩展性、更灵活的拦截机制以及对OGNL表达式语言的深度集成。该框架通过将请求处理流程模块化,解耦前端展示与业务逻辑,使得开发者能够专注于Action层的设计和业务实现。本章节深入剖析Struts2的整体架构、核心组件协作机制、配置管理方式、类型转换体系及国际化支持策略,帮助开发者从底层理解其运行机理,并掌握实际项目中的高级用法。

2.1 Struts2框架的核心组件与工作流程

Struts2 的请求处理模型建立在一个高度可插拔的拦截器链基础上,整个流程始于一个过滤器(Filter),终于视图渲染,中间经过多个关键组件协同工作。理解这些组件的作用及其交互顺序,是掌握Struts2运行机制的前提。

2.1.1 框架整体架构:FilterDispatcher与ActionMapper的作用

尽管在较新版本中 FilterDispatcher 已被弃用并由 StrutsPrepareAndExecuteFilter 取代,但其职责仍延续至今——作为所有HTTP请求进入Struts2世界的入口点。当Tomcat接收到客户端请求后,会根据 web.xml 中定义的过滤器映射规则,将匹配 /action/* *.action 的请求交由该过滤器处理。

<!-- web.xml 配置示例 -->
<filter>
    <filter-name>struts2</filter-name>
    <filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>struts2</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

逻辑分析:
- 第1~3行:声明名为 struts2 的过滤器,指定其实现类为 StrutsPrepareAndExecuteFilter
- 第4~7行:将该过滤器绑定到所有URL路径上,确保每个请求都经过Struts2处理管道。
- 参数说明: <url-pattern>/*</url-pattern> 表示拦截所有请求,包括静态资源;若需优化性能,建议使用 /actions/* 等更精确路径。

一旦请求进入过滤器,Struts2便启动两个阶段:
1. 准备阶段(Prepare) :读取配置文件、初始化ActionContext、设置本地线程变量;
2. 执行阶段(Execute) :解析请求路径,查找对应Action,调用拦截器栈,执行业务方法。

在此过程中, ActionMapper 扮演了“路由器”的角色。它的主要任务是从URL中提取出要调用的Action名称、命名空间和方法名。例如,对于请求 /user/login.action ActionMapper 解析结果如下:

属性
Action Name login
Namespace /user
Method execute(默认)
// 自定义ActionMapper示例
public class CustomActionMapper implements ActionMapper {
    @Override
    public ActionMapping getMapping(HttpServletRequest request, ConfigurationManager configManager) {
        String uri = request.getRequestURI();
        if (uri.contains("/api/v1")) {
            return new ActionMapping("restAction", "/rest", "handleRequest", null);
        }
        return null;
    }
}

代码逐行解读:
- 第3行:实现 ActionMapper 接口,重写 getMapping 方法;
- 第5行:获取当前请求URI;
- 第6~8行:判断是否属于REST风格接口,若是则返回特定的Action映射;
- 返回值 ActionMapping 封装了目标Action信息,供后续处理器使用。

注意:自定义 ActionMapper 需在 struts.xml 中注册:

<constant name="struts.mapper.class" value="com.example.CustomActionMapper"/>

这种机制允许开发者完全控制URL到Action的映射逻辑,适用于API网关、微服务路由等复杂场景。

graph TD
    A[客户端请求] --> B{StrutsPrepareAndExecuteFilter}
    B --> C[ActionMapper解析URL]
    C --> D[创建ActionMapping]
    D --> E[构建ProxyFactory生成代理]
    E --> F[调用Interceptor Stack]
    F --> G[执行Action方法]
    G --> H[Result处理跳转]
    H --> I[响应客户端]

该流程图清晰展示了从请求进入容器到最终响应输出的完整链路,体现了Struts2以拦截器为核心驱动的设计哲学。

2.1.2 请求处理生命周期:从客户端到Action执行的完整链路

Struts2的请求生命周期是一个典型的“责任链”模式应用。每一个环节都有明确的责任边界,且可通过配置进行定制或替换。以下是详细步骤分解:

1. 请求到达 Filter 层

如前所述, StrutsPrepareAndExecuteFilter 是入口。它首先调用 prepare() 方法完成环境初始化,包括加载 struts.xml 、构建配置管理器、设置 ValueStack ActionContext

2. 创建 ValueStack 与 ActionContext

每个请求都会创建独立的 ValueStack 实例,它是OGNL表达式的求值上下文,存储Action实例、参数、会话等对象。 ActionContext 则是线程局部变量(ThreadLocal),封装了请求范围内的上下文数据。

ActionContext ctx = ActionContext.getContext();
Map<String, Object> session = ctx.getSession();
ValueStack stack = ctx.getValueStack();

参数说明:
- ctx.getSession() 获取HttpSession包装对象;
- getValueStack() 返回当前请求的OGNL栈,用于JSP标签取值。

3. 查找并实例化 Action

通过 ActionProxyFactory 创建 ActionProxy ,后者负责实例化具体的Action类。此过程涉及依赖注入(如果整合Spring)、拦截器装配等操作。

4. 执行拦截器栈(Interceptor Stack)

这是Struts2最强大的特性之一。拦截器按预定义顺序依次执行 intercept() 方法,形成环绕通知式的增强逻辑。常见拦截器包括:
- params : 参数自动注入
- validation : 输入校验
- fileUpload : 文件上传处理
- conversionError : 类型转换异常捕获

<!-- struts.xml 中定义拦截器栈 -->
<interceptors>
    <interceptor-stack name="myStack">
        <interceptor-ref name="defaultStack"/>
        <interceptor-ref name="timer"/> <!-- 记录执行时间 -->
    </interceptor-stack>
</interceptors>

<default-interceptor-ref name="myStack"/>

上述配置将 timer 拦截器加入默认栈,可用于性能监控。

5. 调用 Action 方法

默认调用 execute() 方法,也可通过 method 参数指定其他方法:

/user/delete.action?method=remove

此时,Struts2利用反射机制调用对应方法,并将其返回值(如 "success" )传递给Result系统。

6. Result 处理跳转

根据Action返回的结果码,选择相应的Result类型进行页面跳转或数据输出。常见的有:
- dispatcher : 转发至JSP页面
- redirect : 重定向
- json : 返回JSON数据(需引入JSON插件)

<result name="success">/WEB-INF/pages/userList.jsp</result>
<result name="input">/login.jsp</result>
7. 视图渲染与响应输出

最后由Servlet容器完成JSP编译、HTML生成,并将响应写回客户端。

整个生命周期可用下表概括:

阶段 组件 主要功能
入口 StrutsPrepareAndExecuteFilter 启动处理流程
映射 ActionMapper 解析URL获取Action信息
上下文 ActionContext & ValueStack 存储共享数据
控制 Interceptor Stack 横切关注点处理
业务 Action 执行具体逻辑
输出 Result 决定跳转方式
渲染 JSP/FreeMarker等 页面生成

这一分层结构不仅提高了系统的可维护性,也为权限控制、日志记录、缓存等非功能性需求提供了统一接入点。

2.1.3 ValueStack与OGNL表达式引擎的集成机制

ValueStack 是Struts2数据流转的核心,本质上是一个复合栈结构,包含两部分:
- Object Stack(对象栈) :存放Action实例、ModelDriven模型、Action链中的前驱Action等;
- Context Map(上下文映射) :封装request、session、application、parameters等作用域。

OGNL(Object-Graph Navigation Language)是一种功能强大的表达式语言,允许通过简洁语法访问ValueStack中的任意属性。

OGNL基本语法示例:
表达式 含义
%{name} 访问Action中的name属性
%{#session.user} 访问Session中的user对象
%{#parameters.id[0]} 获取第一个id参数
%{@java.lang.Math@PI} 调用静态字段

在JSP页面中广泛使用的Struts2标签库(如 <s:property> <s:textfield> )均基于OGNL实现:

<s:property value="username"/> 
<!-- 等价于 OGNL: %{username} -->
<s:if test="%{#session.loginStatus == true}">
    欢迎回来!
</s:if>

代码解释:
- <s:property> 默认从ValueStack顶部开始查找变量;
- <s:if test="..."> 中的test属性接受布尔型OGNL表达式;
- #session 表示从Context Map中访问Session作用域。

为了验证ValueStack的行为,可以编写测试Action:

public class TestAction extends ActionSupport {
    private String username;
    private List<String> roles = Arrays.asList("admin", "user");

    public String execute() {
        ActionContext ctx = ActionContext.getContext();
        ValueStack stack = ctx.getValueStack();
        stack.set("tempMsg", "This is temporary message");
        return SUCCESS;
    }

    // getter/setter省略
}

然后在JSP中输出:

<p>用户名:<s:property value="username"/></p>
<p>角色列表:<s:property value="roles"/></p>
<p>临时消息:<s:property value="#tempMsg"/></p>

这里 username roles 来自对象栈(即TestAction实例),而 #tempMsg 显式指明从上下文中获取。

此外,Struts2还支持 ModelDriven 模式,即将数据模型单独封装并压入ValueStack顶端,便于前后端数据绑定:

public class UserAction implements ModelDriven<User> {
    private User user = new User();

    public User getModel() {
        return user;
    }

    public String save() {
        System.out.println("保存用户:" + user.getName());
        return SUCCESS;
    }
}

此时表单字段可直接绑定到 user.name user.age ,无需在Action中显式声明。

综上所述,ValueStack与OGNL的结合极大地简化了数据访问逻辑,使开发者摆脱繁琐的 request.setAttribute() 和类型转换工作,真正实现了“约定优于配置”的设计理念。

| 特性 | 描述 |
|------|------|
| 数据集中管理 | 所有相关对象统一存放于ValueStack |
| 表达式灵活 | 支持跨作用域、静态方法、集合操作 |
| 标签无缝集成 | JSP标签自动解析OGNL表达式 |
| 安全控制 | 可通过`excludedClasses`限制敏感类访问 |
| 扩展性强 | 支持自定义TypeConverter影响OGNL行为 |

正是这种深层次的数据抽象能力,使得Struts2在当时成为企业级Java Web开发的主流选择之一。

3. Action与拦截器设计实现

在Java Web开发中,Struts2作为MVC框架的重要代表之一,其核心机制围绕 Action 拦截器(Interceptor) 构建。这两者共同构成了请求处理的主干流程:Action负责业务逻辑的封装与响应结果的生成,而拦截器则提供了对请求执行过程进行横切控制的能力。通过合理设计Action类结构与定制化拦截器链,开发者可以在不侵入业务代码的前提下,实现权限校验、日志记录、性能监控、事务管理等通用功能。

本章将深入剖析Struts2中Action的设计原则与上下文访问方式,并系统讲解自定义拦截器的开发流程、拦截器栈的组合策略以及结果跳转机制。重点在于揭示框架底层如何通过 ActionContext ValueStack 、OGNL表达式引擎协同工作,同时结合实际编码示例展示从单个拦截器编写到多拦截器优先级调度的完整实践路径。

3.1 Action类的设计原则与执行上下文

Action是Struts2框架中处理用户请求的核心组件,每一个HTTP请求最终都会映射到一个具体的Action实例上执行。理解Action的设计模式及其运行时上下文环境,对于掌握整个框架的数据流转机制至关重要。

3.1.1 实现Action接口与继承ActionSupport的区别

在Struts2中,定义一个Action有两种主要方式:一是直接实现 com.opensymphony.xwork2.Action 接口,二是继承 org.apache.struts2.interceptor.ActionSupport 类。虽然两者都能完成基本的请求处理任务,但在灵活性、扩展性和开发效率上有显著差异。

接口实现方式
public class LoginAction implements Action {
    private String username;
    private String password;

    @Override
    public String execute() throws Exception {
        if ("admin".equals(username) && "123456".equals(password)) {
            return SUCCESS;
        } else {
            return ERROR;
        }
    }

    // getter and setter methods
}

该方式最轻量,仅需实现 execute() 方法即可。适用于简单场景或对框架依赖最小化的项目。但由于没有默认的行为支持(如输入验证、国际化消息获取等),所有功能都需要手动编码实现。

继承ActionSupport方式
public class UserAction extends ActionSupport {
    private String name;
    private int age;

    @Override
    public String execute() {
        if (age < 0) {
            this.addActionError("年龄不能为负数");
            return INPUT;
        }
        return SUCCESS;
    }

    public String list() {
        // 自定义方法
        return "list";
    }

    // getter and setter
}

ActionSupport 是Struts2提供的抽象基类,实现了 Action 接口并额外集成了以下能力:
- 输入校验支持( validate() 方法)
- 国际化资源访问( getText()
- 错误/消息收集( addActionError() addActionMessage()
- 默认结果常量( SUCCESS , ERROR , INPUT 等)

因此,在大多数企业级应用中推荐使用继承 ActionSupport 的方式以提升开发效率。

对比维度 实现Action接口 继承ActionSupport
开发复杂度
功能完整性 需自行实现 内建支持校验、i18n、错误处理
耦合性 低(仅依赖XWork) 高(强依赖Struts2库)
扩展性 灵活 受限于父类设计
适用场景 极简微服务、POJO测试 标准Web应用、需要丰富交互反馈

结论 :除非有特殊解耦需求,否则应优先选择继承 ActionSupport

3.1.2 ActionContext与ValueStack的数据访问方式

Struts2采用了一套独特的数据存取模型——基于 ActionContext ValueStack 的上下文隔离机制,确保线程安全的同时提供强大的属性访问能力。

ActionContext:线程绑定的执行环境容器

ActionContext 是一个与当前线程绑定的上下文对象,存储了请求处理过程中所需的各种环境信息:

ActionContext ctx = ActionContext.getContext();
Map<String, Object> session = ctx.getSession();
Map<String, Object> application = ctx.getApplication();
HttpServletRequest request = (HttpServletRequest) ctx.get(ServletActionContext.HTTP_REQUEST);

它内部使用 ThreadLocal 实现线程隔离,保证每个请求独享自己的上下文空间。常见用途包括:
- 访问Session/Application域
- 获取Locale(用于国际化)
- 存储临时数据供JSP页面读取

ValueStack:OGNL表达式的求值栈

ValueStack 是Struts2的核心数据结构,本质上是一个“对象栈”,存放Action实例本身及其他相关对象(如ModelDriven中的model)。当JSP页面使用 <s:property value="username"/> 时,实际上是通过OGNL引擎在 ValueStack 中查找匹配属性。

ValueStack stack = ActionContext.getContext().getValueStack();
stack.push(new User("张三", 25)); // 将对象压入栈顶

此时,在页面中可通过 <s:property value="name"/> 直接访问 User 对象的 name 字段,无需指定前缀。

数据访问流程图(Mermaid)
graph TD
    A[HTTP请求到达] --> B{Struts2 Filter拦截}
    B --> C[创建ActionContext]
    C --> D[初始化ValueStack]
    D --> E[实例化Action并压入ValueStack]
    E --> F[调用拦截器链]
    F --> G[执行Action.execute()]
    G --> H[结果渲染: JSP/Freemarker]
    H --> I[通过OGNL从ValueStack取值]
    I --> J[响应返回客户端]

此流程体现了Struts2“无状态Action + 有状态ValueStack”的设计理念:Action本身是无状态的(每次请求新建),但通过ValueStack保持了视图层可访问的状态数据。

参数说明与逻辑分析
  • ActionContext.getContext() :静态方法,获取当前线程绑定的上下文。
  • getValueStack() :返回 OgnlValueStack 实例,支持push/pop操作。
  • OGNL表达式自动遍历栈中所有对象寻找匹配属性,顺序为从栈顶到底部。

这种机制使得开发者可以灵活地向页面传递数据,而不必局限于Action本身的属性。

3.1.3 多方法Action的分发机制(dynamicMethodInvocation)

传统MVC中,通常一个Action对应多个请求动作(如add、edit、delete)。Struts2提供了两种方式来实现多方法分发:

方式一:method参数显式指定
<action name="user_*" class="com.example.UserAction" method="{1}">
    <result name="success">/success.jsp</result>
</action>

配合URL /user_add.action /user_delete.action {1} 会匹配通配符 * 的内容并调用对应方法。

方式二:开启DMI(Dynamic Method Invocation)
<constant name="struts.enable.DynamicMethodInvocation" value="true"/>

启用后可通过如下语法调用特定方法:

/user!save.action
/order!updateStatus.action

注意:出于安全考虑,DMI默认关闭,且在现代开发中建议使用RESTful风格替代。

安全风险与最佳实践

DMI存在潜在的安全漏洞(如反射调用私有方法),因此生产环境中应禁用DMI,改用命名空间+通配符配置:

<package name="user" namespace="/user" extends="struts-default">
    <action name="add" class="com.example.UserAction" method="add">
        <result>/user/add.jsp</result>
    </action>
    <action name="delete" class="com.example.UserAction" method="delete">
        <result type="redirectAction">list</result>
    </action>
</package>

该方式更清晰、可控,符合高内聚低耦合的设计思想。

3.2 自定义拦截器的开发与注册

拦截器是Struts2实现AOP思想的关键组件,能够在Action执行前后插入自定义逻辑,广泛应用于权限控制、日志审计、性能统计等领域。

3.2.1 Interceptor接口的三个核心方法解析(init、intercept、destroy)

所有拦截器必须实现 com.opensymphony.xwork2.Interceptor 接口,包含三个生命周期方法:

public class LoggingInterceptor implements Interceptor {

    @Override
    public void init() {
        System.out.println("LoggingInterceptor 初始化");
    }

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        long start = System.currentTimeMillis();
        System.out.println("开始执行 Action: " + invocation.getAction().getClass().getSimpleName());

        try {
            return invocation.invoke(); // 放行请求
        } finally {
            long elapsed = System.currentTimeMillis() - start;
            System.out.println("执行耗时: " + elapsed + "ms");
        }
    }

    @Override
    public void destroy() {
        System.out.println("LoggingInterceptor 销毁");
    }
}
方法详解:
方法名 调用时机 用途说明
init() 拦截器实例创建时调用一次 初始化资源,如数据库连接、缓存等
intercept(ActionInvocation) 每次请求经过该拦截器时调用 核心逻辑所在,决定是否放行
destroy() 应用关闭时调用 释放资源,避免内存泄漏

其中 ActionInvocation 是关键参数,代表当前执行上下文,调用 invoke() 表示继续执行下一个拦截器或目标Action。

执行逻辑逐行分析:
  1. long start = System.currentTimeMillis();
    记录方法开始时间,用于后续性能计算。

  2. System.out.println(...)
    输出即将执行的Action名称,便于调试。

  3. return invocation.invoke();
    触发后续拦截器链或Action执行。若此处不调用,则请求被中断(可用于权限拒绝)。

  4. finally 块确保即使抛出异常也能输出耗时信息,体现良好的资源管理习惯。

3.2.2 权限控制拦截器的实战编码

以下是一个典型的登录权限拦截器实现:

public class AuthInterceptor implements Interceptor {

    @Override
    public void init() {}

    @Override
    public String intercept(ActionInvocation invocation) throws Exception {
        ActionContext ctx = invocation.getInvocationContext();
        Map<String, Object> session = ctx.getSession();

        Object user = session.get("loginUser");
        String actionName = invocation.getProxy().getActionName();

        // 允许登录相关操作无需认证
        if ("login".equals(actionName) || "doLogin".equals(actionName)) {
            return invocation.invoke();
        }

        if (user == null) {
            return "login"; // 返回全局结果
        }

        return invocation.invoke();
    }

    @Override
    public void destroy() {}
}
注册拦截器(struts.xml)
<interceptors>
    <interceptor name="auth" class="com.example.AuthInterceptor"/>
    <interceptor-stack name="secureStack">
        <interceptor-ref name="auth"/>
        <interceptor-ref name="defaultStack"/>
    </interceptor-stack>
</interceptors>

<default-interceptor-ref name="secureStack"/>

该配置使所有Action默认受权限保护,未登录用户访问非登录页将被重定向至登录界面。

3.2.3 日志记录与性能监控拦截器的应用场景

除了权限控制,拦截器还可用于非功能性需求增强:

场景 拦截器作用 示例
请求日志 记录IP、时间、参数 审计追踪
性能监控 统计Action执行时间 运维优化
编码转换 统一设置request/response字符集 中文乱码解决
缓存控制 添加Cache-Control头 提升前端性能
异常统一处理 捕获异常并跳转友好页面 用户体验保障

例如,结合SLF4J实现结构化日志:

private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);

@Override
public String intercept(ActionInvocation invocation) throws Exception {
    HttpServletRequest request = 
        (HttpServletRequest) invocation.getInvocationContext().get(HTTP_REQUEST);
    String clientIp = request.getRemoteAddr();
    String uri = request.getRequestURI();

    logger.info("Request started: IP={}, URI={}, Action={}", 
                clientIp, uri, invocation.getAction().getClass().getSimpleName());

    String result = invocation.invoke();

    logger.info("Request finished with result: {}", result);
    return result;
}

此类拦截器可在不影响业务逻辑的情况下实现可观测性增强。

3.3 拦截器栈的组合与优先级控制

3.3.1 defaultStack的组成分析与定制化改造

Struts2内置的 defaultStack 是最常用的拦截器栈,其定义如下:

<interceptor-stack name="defaultStack">
    <interceptor-ref name="exception"/>
    <interceptor-ref name="alias"/>
    <interceptor-ref name="servletConfig"/>
    <interceptor-ref name="i18n"/>
    <interceptor-ref name="prepare"/>
    <interceptor-ref name="chain"/>
    <interceptor-ref name="scopedModelDriven"/>
    <interceptor-ref name="modelDriven"/>
    <interceptor-ref name="fileUpload"/>
    <interceptor-ref name="checkbox"/>
    <interceptor-ref name="multiselect"/>
    <interceptor-ref name="staticParams"/>
    <interceptor-ref name="actionMappingParams"/>
    <interceptor-ref name="params">
        <param name="excludeParams">dojo\..*,^struts\..*</param>
    </interceptor-ref>
    <interceptor-ref name="conversionError"/>
    <interceptor-ref name="validation">
        <param name="excludeMethods">input,back,cancel,browse</param>
    </interceptor-ref>
    <interceptor-ref name="workflow">
        <param name="excludeMethods">input,back,cancel,browse</param>
    </interceptor-ref>
</interceptor-stack>

各拦截器职责简述:

拦截器名 功能描述
exception 异常捕获与结果映射
params 自动填充Action属性
validation 字段验证
workflow 控制输入失败时停留在input结果
modelDriven 支持ModelDriven接口
定制化示例:移除文件上传支持以提高安全性
<interceptor-stack name="safeStack">
    <interceptor-ref name="exception"/>
    <interceptor-ref name="alias"/>
    <interceptor-ref name="servletConfig"/>
    <interceptor-ref name="i18n"/>
    <interceptor-ref name="prepare"/>
    <interceptor-ref name="modelDriven"/>
    <interceptor-ref name="params"/>
    <interceptor-ref name="conversionError"/>
    <interceptor-ref name="validation"/>
    <interceptor-ref name="workflow"/>
</interceptor-stack>

适用于不需要文件上传功能的管理系统,减少攻击面。

3.3.2 多个拦截器之间的执行顺序与数据传递

拦截器按声明顺序依次执行 intercept() 方法,形成“环绕”式调用链。其执行顺序遵循“先进后出”原则:

[1] Logging → [2] Auth → [3] Params → Action → [3] Params ← [2] Auth ← [1] Logging

这意味着前置逻辑按序执行,后置逻辑逆序回收。

数据共享机制

拦截器间可通过 ActionContext ActionInvocation 共享数据:

// 在某个拦截器中设置标记
invocation.getInvocationContext().put("startTime", System.currentTimeMillis());

// 在另一个拦截器中读取
Long start = (Long) ctx.get("startTime");

也可通过 ValueStack 传递对象供Action使用。

3.3.3 拦截器中异常的捕获与统一处理

拦截器应具备异常处理能力,防止因中间环节崩溃导致系统不可用:

@Override
public String intercept(ActionInvocation invocation) {
    try {
        return invocation.invoke();
    } catch (BusinessException e) {
        invocation.getStack().setValue("errorMessage", e.getMessage());
        return "error";
    } catch (Exception e) {
        Logger.error("Unexpected error in interceptor", e);
        return "systemError";
    }
}

结合 global-results 可在 struts.xml 中定义全局错误跳转:

<global-results>
    <result name="systemError">/error/system.jsp</result>
</global-results>

实现故障隔离与用户体验一致性。

3.4 Action结果跳转与视图渲染

3.4.1 Result接口与常见结果类型(dispatcher、redirect等)

Struts2通过 Result 接口抽象视图跳转行为,不同实现对应不同的导航方式:

类型 结果类 行为说明
dispatcher ServletDispatcherResult 转发(服务器内部跳转,默认)
redirect ServletRedirectResult 重定向(浏览器跳转,URL变化)
redirectAction ServletActionRedirectResult 重定向到另一Action
chain ChainResult Action链式调用,共享ValueStack
json JsonResponse 返回JSON数据(需插件支持)

配置示例:

<result name="success" type="redirect">/home.jsp</result>
<result name="detail" type="chain">
    <param name="actionName">viewDetail</param>
</result>

3.4.2 动态结果映射的配置技巧

可通过OGNL动态计算结果路径:

<result name="success" type="dispatcher">
    /pages/${category}/detail.jsp
</result>

前提是Action中有 getCategory() 方法返回目录名。

也可在Action中动态设置结果:

public String execute() {
    if (isAdmin()) {
        setResult("/admin/dashboard.jsp");
        return "custom";
    }
    return SUCCESS;
}

// getter
public String getResult() { return resultPath; }

配合配置:

<result name="custom">${result}</result>

实现高度灵活的视图路由。

3.4.3 使用FreeMarker或Velocity作为模板引擎的集成方案

Struts2支持多种模板引擎,以FreeMarker为例:

添加依赖(Maven)
<dependency>
    <groupId>org.apache.struts</groupId>
    <artifactId>struts2-freemarker-plugin</artifactId>
    <version>2.5.30</version>
</dependency>
配置结果类型
<result name="freemarker" type="freemarker">/templates/user.ftl</result>
FreeMarker模板示例(user.ftl)
<html>
<body>
<h1>欢迎 ${user.name}!</h1>
<ul>
<#list orders as order>
    <li>订单编号:${order.id},金额:${order.amount}</li>
</#list>
</ul>
</body>
</html>

模板引擎的优势在于分离逻辑与展示,支持更复杂的页面逻辑,适合内容管理系统或报表类应用。

综上所述,Action与拦截器的设计不仅关乎功能实现,更直接影响系统的可维护性、安全性与扩展能力。通过深入理解其工作机制并合理运用高级特性,能够构建出既高效又稳健的企业级Web应用。

4. Spring框架IoC与DI核心机制

在企业级Java开发中,Spring框架的出现彻底改变了传统硬编码依赖管理的方式。其最为核心的两个概念——控制反转(Inversion of Control, IoC)和依赖注入(Dependency Injection, DI),不仅提升了代码的可测试性与解耦程度,也奠定了现代轻量级容器设计的思想基础。随着微服务架构的普及,理解Spring的IoC/DI机制已不再是“选修内容”,而是每一个资深开发者必须掌握的底层能力。本章将深入剖析Spring容器的工作原理,从设计思想到具体实现,从配置方式到运行时行为,全面揭示Bean生命周期中的关键环节。

4.1 控制反转(IoC)的设计思想与容器实现

控制反转是一种软件工程中的设计原则,它将对象创建和依赖管理的责任从应用程序自身转移到外部容器。传统的程序设计中,一个类往往需要主动去实例化其所依赖的其他类,这种做法导致了高度的耦合。而通过IoC,这些职责被反转给了Spring容器来统一管理和调度,从而实现了组件之间的松耦合。

4.1.1 BeanFactory与ApplicationContext的区别与选择

BeanFactory 是Spring框架中最基本的IoC容器接口,提供了最基本的依赖查找和Bean管理功能。它是工厂模式的典型实现,负责根据配置元数据(XML、注解或Java Config)创建并管理Bean实例。

// 示例:使用BeanFactory加载XML配置
Resource resource = new ClassPathResource("applicationContext.xml");
BeanFactory factory = new XmlBeanFactory(resource);
UserService userService = (UserService) factory.getBean("userService");

尽管 BeanFactory 功能完备,但在实际开发中更推荐使用其子接口 ApplicationContext 。后者不仅包含了 BeanFactory 的所有功能,还扩展了更多企业级特性:

特性 BeanFactory ApplicationContext
国际化支持(MessageSource)
资源访问(ResourceLoader)
事件发布机制(ApplicationEventPublisher)
自动注册BeanPostProcessor和BeanFactoryPostProcessor
默认预实例化singleton Bean ❌(延迟加载)

参数说明
- ClassPathResource : 用于从类路径下加载资源文件。
- XmlBeanFactory : 已被标记为过时,在新项目中应避免使用。
- getBean(String name) : 根据bean名称获取实例,需手动进行类型转换。

classDiagram
    class BeanFactory {
        <<interface>>
        +Object getBean(String name)
        +boolean containsBean(String name)
        +Class getType(String name)
    }
    class ApplicationContext {
        <<interface>>
        +void publishEvent(ApplicationEvent event)
        +String[] getMessageCodes()
        +Resource getResource(String location)
    }
    BeanFactory <|-- ApplicationContext
    ApplicationContext --> MessageSource : extends
    ApplicationContext --> ResourceLoader : extends
    ApplicationContext --> ApplicationEventPublisher : extends

上述流程图展示了 ApplicationContext 如何继承并增强 BeanFactory 的能力。在Web应用中,通常使用 WebApplicationContext ,它是 ApplicationContext 的专用变体,能够与Servlet环境集成,并通过 ContextLoaderListener 在启动时自动初始化。

因此,在大多数场景下,应优先选用 ApplicationContext 实现类如 AnnotationConfigApplicationContext FileSystemXmlApplicationContext ,以获得完整的功能支持。

4.1.2 容器启动过程:配置加载、Bean注册与预实例化

Spring容器的启动是一个复杂但有序的过程,主要包括以下几个阶段:

  1. 配置元数据读取 :解析XML、注解或Java配置类;
  2. BeanDefinition注册 :将每个Bean的定义信息封装为 BeanDefinition 对象并注册到 BeanDefinitionRegistry
  3. BeanFactoryPostProcessor执行 :允许在实例化前修改Bean定义;
  4. 单例Bean预实例化 :对作用域为singleton且非懒加载的Bean进行创建;
  5. BeanPostProcessor注册与应用 :用于拦截Bean的初始化前后过程。

下面通过一段简化代码模拟该流程:

@Configuration
@ComponentScan(basePackages = "com.example.service")
public class AppConfig {
}

// 启动容器
AnnotationConfigApplicationContext ctx = 
    new AnnotationConfigApplicationContext(AppConfig.class);

// 获取Bean
UserService userService = ctx.getBean(UserService.class);

逐行逻辑分析
- 第1–3行: @Configuration 表明这是一个配置类, @ComponentScan 指定要扫描的包路径,自动发现带有 @Component 及其衍生注解的类。
- 第6–7行: AnnotationConfigApplicationContext 构造函数接收配置类作为输入,触发整个容器初始化流程。
- 内部会调用 AnnotatedBeanDefinitionReader 解析 AppConfig 类;
- 扫描指定包下的组件,生成对应的 BeanDefinition
- 注册进 DefaultListableBeanFactory
- 最后调用 finishBeanFactoryInitialization() 方法完成单例Bean的创建。

这个过程中,Spring还会自动注册诸如 AutowiredAnnotationBeanPostProcessor 等基础设施组件,确保后续依赖注入等功能正常工作。

4.1.3 依赖查找与依赖注入的本质差异

虽然两者都涉及获取Bean实例,但它们在编程模型上有本质区别。

依赖查找(Dependency Lookup) 是指主动调用容器的方法来获取某个Bean,例如:

UserService userService = (UserService) applicationContext.getBean("userService");

这种方式由开发者显式控制,具有较高的灵活性,但也带来了侵入性——代码直接依赖于Spring API。

依赖注入(Dependency Injection) 则是由容器在运行时自动将所需依赖注入到目标对象中,无需手动调用 getBean() 。例如:

@Service
public class OrderService {
    private final UserService userService;

    public OrderService(UserService userService) {
        this.userService = userService;
    }

    public void processOrder() {
        userService.saveOrder();
    }
}

在此例中, OrderService 的构造函数接受 UserService 参数,Spring容器会在创建 OrderService 实例时自动提供已存在的 UserService Bean。

维度 依赖查找 依赖注入
控制权 开发者主动获取 容器被动注入
耦合度 高(依赖Spring API) 低(仅依赖类型)
可测试性 较差 更好(可通过构造函数传参模拟)
使用频率 较少(多用于特殊场景) 广泛使用

值得一提的是,Spring提倡“优先使用依赖注入”的最佳实践。只有在某些动态场景(如策略模式中根据条件选择不同实现)才考虑结合 ObjectProvider<T> ApplicationContext 进行延迟查找。

4.2 依赖注入(DI)的多种实现方式

Spring支持多种依赖注入方式,开发者可根据业务需求灵活选择。不同的注入方式在可变性、可测试性和语义表达上各有优劣。

4.2.1 构造函数注入与Setter注入的适用场景对比

构造函数注入
@Service
public class PaymentService {
    private final NotificationService notificationService;
    private final FraudCheckService fraudCheckService;

    public PaymentService(NotificationService notificationService,
                          FraudCheckService fraudCheckService) {
        this.notificationService = notificationService;
        this.fraudCheckService = fraudCheckService;
    }
}
  • 优点
  • 强制依赖不可变(final字段);
  • 易于单元测试(直接传参即可);
  • 避免空指针异常(未注入无法实例化);
  • 缺点
  • 当依赖过多时,构造函数显得臃肿;
  • 不适用于循环依赖(除非启用代理);
Setter注入
@Service
public class ReportService {
    private DataSource dataSource;

    @Autowired
    public void setDataSource(DataSource dataSource) {
        this.dataSource = dataSource;
    }
}
  • 优点
  • 支持可选依赖;
  • 更容易应对变化(后期赋值);
  • 缺点
  • 对象可能处于不完整状态;
  • 字段无法声明为 final

Spring官方建议:对于强依赖使用构造函数注入,对于可选依赖可使用Setter或字段注入。

4.2.2 自动装配模式(byType、byName、constructor)的行为特征

Spring支持四种自动装配模式,通过 <bean autowire="..."> 或 Java 配置设定:

模式 描述 匹配规则
no 默认,不自动装配 手动配置
byName 根据属性名匹配Bean名称 userService → userService
byType 根据属性类型匹配唯一Bean UserService → 唯一UserService实例
constructor 类似byType,应用于构造函数参数 构造函数参数类型匹配
<bean id="orderService" class="com.example.OrderService" autowire="byType"/>

⚠️ 注意:当存在多个相同类型的Bean时, byType 会导致 NoUniqueBeanDefinitionException ,此时需配合 @Primary @Qualifier 解决。

4.2.3 使用@Autowired、@Qualifier与@Resource进行精准注入

@Service
public class BillingService {

    @Autowired
    @Qualifier("emailNotificationService")
    private NotificationService notificationService;

    @Resource(name = "jdbcDataSource")
    private DataSource dataSource;
}
  • @Autowired :Spring特有,基于类型自动装配,可加 @Qualifier 指定名称;
  • @Qualifier :限定符,解决类型冲突;
  • @Resource :JSR-250标准注解,等价于 @Autowired + @Qualifier ,默认按名称查找;
flowchart TD
    A[开始注入] --> B{是否存在@Resource?}
    B -- 是 --> C[按name查找Bean]
    B -- 否 --> D{是否存在@Autowired?}
    D -- 是 --> E{是否有@Qualifier?}
    E -- 是 --> F[按type+name查找]
    E -- 否 --> G[按type查找]
    G -- 多个匹配 --> H[报错: NoUniqueBeanDefinitionException]
    G -- 唯一匹配 --> I[成功注入]

此流程图清晰地描述了Spring在处理字段注入时的决策路径。合理组合这些注解可以有效提升注入的精确性和可维护性。

4.3 Bean的作用域与生命周期管理

4.3.1 singleton、prototype、request、session作用域的实际影响

Spring支持五种标准作用域:

作用域 描述 使用场景
singleton 每容器一个实例(默认) 无状态服务类
prototype 每次请求都创建新实例 有状态对象
request 每HTTP请求一个实例 Web层临时对象
session 每用户会话一个实例 用户偏好设置
application ServletContext级别单例 全局缓存

示例配置:

@Scope("prototype")
@Component
public class ShoppingCart { }

若在singleton Bean中注入prototype Bean,需注意:默认情况下只会注入一次。解决方案包括使用 @Lookup 方法或将其定义为 ObjectFactory<T>

4.3.2 初始化方法与销毁回调的声明方式

可通过以下方式指定生命周期钩子:

@Component
public class DatabaseConnectionPool implements InitializingBean, DisposableBean {

    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化连接池
    }

    @Override
    public void destroy() throws Exception {
        // 关闭连接
    }

    @PostConstruct
    public void init() {
        // 更推荐的初始化方式
    }

    @PreDestroy
    public void cleanup() {
        // 更推荐的销毁方式
    }
}
  • InitializingBean.afterPropertiesSet() DisposableBean.destroy() 是Spring特定接口;
  • @PostConstruct / @PreDestroy 是JSR-250标准,更通用;
  • 推荐使用注解方式,减少对Spring API的依赖。

4.3.3 BeanPostProcessor扩展点的使用场景

BeanPostProcessor 允许在Bean初始化前后插入自定义逻辑:

@Component
public class CustomBeanPostProcessor implements BeanPostProcessor {

    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        System.out.println("Before init: " + beanName);
        return bean;
    }

    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("After init: " + beanName);
        return bean;
    }
}

典型应用场景包括:
- 日志记录;
- 性能监控(统计初始化耗时);
- AOP代理生成(如@Transactional生效机制);

4.4 注解驱动开发与组件扫描机制

4.4.1 @Component、@Service、@Repository的语义区分

这三个注解均派生自 @Component ,功能相同,但语义不同:

注解 层级 附加行为
@Component 通用组件
@Service 业务逻辑层 无特殊处理
@Repository 数据访问层 触发异常翻译(如SQLException → DataAccessException)
@Repository
public class UserDao {
    // Spring会自动为其添加PersistenceExceptionTranslationPostProcessor增强
}

4.4.2 @Configuration与@Bean的Java配置风格实践

@Configuration
public class DataSourceConfig {

    @Bean
    @Primary
    public DataSource mysqlDataSource() {
        return new DriverManagerDataSource("jdbc:mysql://localhost/db", "user", "pass");
    }

    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }
}
  • @Configuration 类会被CGLIB代理,确保 @Bean 方法调用仍返回容器管理的实例;
  • @Bean 方法参数会自动注入,体现DI理念;

4.4.3 组件自动扫描路径配置与过滤规则设定

@ComponentScan(
    basePackages = "com.example",
    includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = RestController.class),
    excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = LegacyService.class)
)

支持的过滤类型包括:
- ANNOTATION:按注解;
- ASSIGNABLE_TYPE:按父类或接口;
- ASPECTJ:AspectJ表达式;
- REGEX:正则匹配类名;

该机制极大增强了模块化配置的能力,使大型系统能灵活控制组件加载范围。

5. Spring AOP与事务管理实战

在企业级Java开发中,系统的可维护性、扩展性和稳定性是衡量架构质量的重要指标。随着业务逻辑的日益复杂,横切关注点(Cross-Cutting Concerns)如日志记录、权限校验、性能监控、事务控制等逐渐脱离核心业务流程,却广泛存在于多个模块之中。传统的面向对象编程难以优雅地处理这些分散且重复的逻辑。为此,Spring框架引入了 面向切面编程 (Aspect-Oriented Programming, AOP),通过解耦横切逻辑与主业务流程,显著提升了代码的内聚性和复用性。与此同时,数据库操作中的数据一致性问题也要求有一套可靠的事务管理机制。Spring基于AOP实现了声明式事务管理,使得开发者无需侵入业务代码即可实现事务控制。本章将深入剖析Spring AOP的核心原理与代理机制,并结合实际场景讲解事务管理的配置方式、传播行为及常见陷阱的规避策略。

5.1 AOP核心概念与代理机制

Spring AOP 是构建于动态代理技术之上的轻量级切面实现,其设计目标是在不修改源码的前提下增强方法执行过程。理解AOP的前提是掌握其核心术语及其运行时协作关系。

5.1.1 切面、连接点、通知、切入点的基本定义

AOP 的基本模型由以下几个关键元素构成:

  • 切面(Aspect) :一个模块化的横切关注点封装,通常是一个带有 @Aspect 注解的Java类,包含通知和切入点定义。
  • 连接点(Join Point) :程序执行过程中能够被拦截的点,如方法调用、异常抛出或字段访问。在Spring AOP中,仅支持 方法级别的连接点
  • 切入点(Pointcut) :用于匹配一组连接点的表达式,决定哪些方法需要被增强。它是对连接点的进一步筛选。
  • 通知(Advice) :在特定连接点上执行的动作,即“织入”到目标方法中的额外逻辑。根据执行时机可分为前置通知、后置通知、环绕通知等。
  • 织入(Weaving) :将切面应用到目标对象并创建代理对象的过程,可以在编译期、类加载期或运行期完成;Spring AOP 使用 运行期织入 ,借助动态代理实现。

以下表格总结了不同类型的通知及其触发时机:

通知类型 注解 触发时机 是否能阻止执行
前置通知 @Before 目标方法执行前
后置通知 @After 目标方法执行后(无论是否异常)
返回通知 @AfterReturning 目标方法成功返回后
异常通知 @AfterThrowing 目标方法抛出异常后
环绕通知 @Around 包裹整个方法调用过程

下面以一个日志切面为例,展示如何使用注解定义一个完整的AOP切面:

@Aspect
@Component
public class LoggingAspect {

    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {}

    @Before("serviceLayer()")
    public void logBefore(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        System.out.println("即将执行方法: " + methodName + ", 参数: " + Arrays.toString(args));
    }

    @AfterReturning(pointcut = "serviceLayer()", returning = "result")
    public void logAfterReturn(JoinPoint joinPoint, Object result) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("方法 " + methodName + " 执行成功,返回值: " + result);
    }

    @AfterThrowing(pointcut = "serviceLayer()", throwing = "ex")
    public void logAfterThrow(JoinPoint joinPoint, Exception ex) {
        String methodName = joinPoint.getSignature().getName();
        System.out.println("方法 " + methodName + " 执行失败,异常: " + ex.getMessage());
    }
}
代码逻辑逐行解读分析:
  • 第1行: @Aspect 表明该类是一个切面类,需配合AOP自动代理启用;
  • 第2行: @Component 将此类注册为Spring Bean,便于容器扫描和织入;
  • 第4–5行:定义名为 serviceLayer() 的切入点,匹配 com.example.service 包下所有类的所有方法;
  • 第7–10行: @Before 在切入点匹配的方法执行前输出日志信息;
  • JoinPoint 提供了当前连接点的上下文信息,如方法名、参数列表等;
  • 第12–15行: @AfterReturning 在方法正常返回后记录结果, returning = "result" 指定了接收返回值的参数名称;
  • 第17–20行: @AfterThrowing 捕获异常并打印错误信息, throwing = "ex" 明确异常变量绑定。

此切面可在不改动任何Service代码的情况下统一添加日志功能,体现了AOP“非侵入式增强”的优势。

5.1.2 JDK动态代理与CGLIB代理的选择机制

Spring AOP底层依赖两种代理技术来生成代理对象:

  • JDK 动态代理 :基于接口实现,利用 java.lang.reflect.Proxy 类在运行时创建实现了相同接口的代理类。它要求目标类必须实现至少一个接口。
  • CGLIB 代理 :基于继承机制,通过字节码生成库(如ASM)创建目标类的子类,在子类中重写方法并插入增强逻辑。适用于没有实现接口的类。

Spring会根据以下规则自动选择代理方式:

graph TD
    A[目标类是否实现接口?] -->|是| B{proxyTargetClass=false?}
    A -->|否| C[强制使用CGLIB]
    B -->|是| D[使用JDK动态代理]
    B -->|否| E[使用CGLIB代理]

说明 proxyTargetClass <aop:config> @EnableAspectJAutoProxy 中的一个属性,默认为 false 。当设置为 true 时,即使目标类实现了接口,也会优先使用CGLIB代理。

举例来说,若有一个服务类:

@Service
public class UserService {
    public void saveUser(String name) {
        System.out.println("保存用户: " + name);
    }
}

由于 UserService 未实现任何接口,Spring 将自动采用 CGLIB 创建代理。若改为:

public interface UserServiceInterface {
    void saveUser(String name);
}

@Service
public class UserServiceImpl implements UserServiceInterface {
    public void saveUser(String name) {
        System.out.println("保存用户: " + name);
    }
}

此时,只要 proxyTargetClass=false (默认),Spring就会使用JDK代理。

尽管CGLIB更灵活,但也存在一些限制:
- 无法代理 final 方法(因为不能被重写);
- 不能代理 private 方法;
- 需要额外引入 cglib asm 库(Spring Boot已默认包含);

因此,在设计服务层时推荐优先面向接口编程,以便兼容JDK代理,提升性能与稳定性。

5.1.3 AspectJ注解风格(@Aspect、@Before、@After等)的应用

虽然Spring AOP并非完整的AspectJ实现,但它支持大部分AspectJ注解语法,极大简化了切面开发。

启用注解驱动AOP的关键配置如下(Java Config方式):

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ComponentScan(basePackages = "com.example")
public class AppConfig {
}

其中:
- @EnableAspectJAutoProxy 开启基于注解的自动代理;
- proxyTargetClass = true 强制使用CGLIB代理,避免因接口缺失导致代理失败。

接下来定义一个权限检查切面:

@Aspect
@Component
public class SecurityAspect {

    @Around("@annotation(com.example.annotation.RequiresPermission)")
    public Object checkPermission(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        RequiresPermission permission = signature.getMethod()
            .getAnnotation(RequiresPermission.class);

        String requiredPerm = permission.value();
        String userPerm = getCurrentUserPermission(); // 模拟获取当前用户权限

        if (!userPerm.equals(requiredPerm)) {
            throw new SecurityException("权限不足,需要: " + requiredPerm);
        }

        return pjp.proceed(); // 继续执行原方法
    }

    private String getCurrentUserPermission() {
        return "admin"; // 简化模拟
    }
}

配套自定义注解:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresPermission {
    String value();
}

应用场景示例:

@Service
public class AdminService {

    @RequiresPermission("admin")
    public void deleteUser(Long id) {
        System.out.println("删除用户 ID: " + id);
    }
}
代码解析:
  • @Around 实现最强大的通知类型,可完全控制方法执行流程;
  • ProceedingJoinPoint.proceed() 是调用原始方法的关键,若不调用则目标方法不会执行;
  • 利用反射获取方法上的注解信息,实现细粒度权限判断;
  • 若权限不符,直接抛出异常中断流程,体现“环绕”控制能力。

这种基于注解的AOP模式非常适合实现安全控制、缓存、限流等功能,具有高度可读性和可维护性。

5.2 基于XML与注解的AOP配置对比

虽然现代Spring开发普遍采用注解驱动,但XML配置仍具一定的灵活性和集中管理优势,尤其适合大型项目中统一管理切面规则。

5.2.1 aop:config与aop:aspect的配置语法详解

使用XML方式进行AOP配置时,需引入aop命名空间:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="
           http://www.springframework.org/schema/beans
           https://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 目标Bean -->
    <bean id="userService" class="com.example.service.UserServiceImpl"/>

    <!-- 切面Bean -->
    <bean id="loggingAspect" class="com.example.aspect.LoggingAspect"/>

    <!-- AOP配置 -->
    <aop:config>
        <aop:aspect ref="loggingAspect">
            <aop:pointcut id="serviceMethods"
                          expression="execution(* com.example.service.*.*(..))"/>
            <aop:before method="logBefore" pointcut-ref="serviceMethods"/>
            <aop:after-returning method="logAfterReturn"
                                 pointcut-ref="serviceMethods"
                                 returning="result"/>
            <aop:after-throwing method="logAfterThrow"
                                pointcut-ref="serviceMethods"
                                throwing="ex"/>
        </aop:aspect>
    </aop:config>
</beans>
参数说明:
  • aop:config :AOP配置根元素;
  • aop:aspect ref="..." :引用已定义的切面Bean;
  • aop:pointcut :定义切入点, expression 属性书写表达式;
  • aop:before / after-returning / after-throwing :分别对应不同通知类型;
  • method :指定切面类中具体执行的方法名;
  • pointcut-ref :引用已命名的切入点ID;
  • returning / throwing :用于传递返回值或异常对象。

相比注解方式,XML的优点在于:
- 配置集中,便于全局查看;
- 不污染Java代码,适合纯配置化部署;
- 可动态调整切面逻辑而无需重新编译。

缺点也很明显:
- 冗长繁琐,易出错;
- 缺乏类型安全性;
- 调试困难,IDE支持弱。

5.2.2 切入点表达式的编写规范(execution、within、this等)

切入点表达式是AOP的核心,决定了增强作用范围。常用关键字包括:

关键字 用途 示例
execution() 匹配方法执行 execution(* com.service.UserService.save*(..))
within() 匹配类所在的类型 within(com.service.*)
this() 匹配代理对象类型 this(com.service.UserService)
target() 匹配目标对象类型 target(com.service.UserService)
args() 匹配参数类型 args(java.lang.String)
@annotation() 匹配标注特定注解的方法 @annotation(Loggable)

组合使用示例如下:

@Pointcut("execution(* com.example.dao.*.*(..)) && args(id, ..)")
public void daoAccessById(Long id) {}

@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(secure)")
public void securedServiceMethods(RequiresPermission secure) {}

上述第一个切入点匹配DAO层中接受 Long id 作为第一个参数的方法;第二个则同时满足方法位于Service包下且带有 @RequiresPermission 注解。

合理使用复合表达式可以精准定位增强目标,避免过度代理影响性能。

5.2.3 环绕通知中ProceedingJoinPoint的调用控制

@Around 是最强大的通知类型,因为它可以完全控制方法的执行流程,包括是否执行、何时执行、异常处理以及结果修改。

示例:实现方法执行时间监控

@Aspect
@Component
public class PerformanceMonitorAspect {

    @Around("execution(* com.example.service.*.*(..))")
    public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();

        Object result;
        try {
            result = pjp.proceed(); // 执行原方法
        } catch (Exception e) {
            System.err.println("方法执行出错: " + e.getMessage());
            throw e; // 继续向上抛出
        } finally {
            long duration = System.currentTimeMillis() - start;
            String methodName = pjp.getSignature().toShortString();
            System.out.println("[" + methodName + "] 执行耗时: " + duration + "ms");
        }

        return result;
    }
}
逻辑分析:
  • pjp.proceed() 是关键调用,代表执行目标方法;
  • 使用 try-finally 结构确保无论成功或失败都能记录耗时;
  • 可在此基础上添加阈值告警、异步上报等高级功能;
  • 若希望跳过原方法(如缓存命中),可直接返回缓存值而不调用 proceed()

该模式广泛应用于性能监控、缓存、重试机制等领域。

5.3 声明式事务管理的实现原理

Spring 的声明式事务管理建立在AOP基础之上,通过代理机制在方法前后自动开启、提交或回滚事务,极大简化了事务控制代码。

5.3.1 PlatformTransactionManager接口的实现类选择

Spring通过 PlatformTransactionManager 接口统一抽象事务管理器,主要实现类有:

实现类 适用场景
DataSourceTransactionManager 单数据源,基于JDBC或MyBatis
JpaTransactionManager JPA/Hibernate环境
JtaTransactionManager 分布式事务(JTA),跨多个资源

典型配置如下(Java方式):

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

配合 @EnableTransactionManagement 启用事务注解支持:

@Configuration
@EnableTransactionManagement
public class TransactionConfig {
    // ...
}

5.3.2 @Transactional注解的属性配置(propagation、isolation、rollbackFor)

@Transactional 是声明式事务的核心注解,其常用属性包括:

属性 说明 默认值
propagation 事务传播行为 REQUIRED
isolation 隔离级别 DEFAULT
timeout 超时时间(秒) -1(不限)
readOnly 是否只读 false
rollbackFor 哪些异常触发回滚 RuntimeException Error
noRollbackFor 哪些异常不触发回滚

示例:

@Service
public class TransferService {

    @Autowired
    private AccountDao accountDao;

    @Transactional(
        propagation = Propagation.REQUIRED,
        isolation = Isolation.READ_COMMITTED,
        rollbackFor = {InsufficientFundsException.class, SQLException.class},
        timeout = 30
    )
    public void transferMoney(String from, String to, BigDecimal amount) 
            throws InsufficientFundsException {
        accountDao.debit(from, amount);
        accountDao.credit(to, amount);
    }
}

当 debit 操作成功但 credit 失败时,整个事务将回滚,保证资金一致性。

5.3.3 事务传播行为在嵌套调用中的实际表现

事务传播行为决定了多个事务方法相互调用时的行为。最常见的 Propagation.REQUIRED 表示:如果有事务则加入,否则新建。

其他常用传播行为:

传播行为 描述
REQUIRED 加入现有事务或新建
REQUIRES_NEW 挂起当前事务,总是新建
SUPPORTS 支持当前事务,无则非事务执行
NOT_SUPPORTED 不支持事务,总是非事务执行
NEVER 不允许事务,否则抛异常
MANDATORY 必须存在事务,否则抛异常

案例说明:

@Service
public class OuterService {

    @Autowired
    private InnerService innerService;

    @Transactional
    public void outerMethod() {
        // do something
        innerService.innerMethod(); // 调用另一个事务方法
    }
}

@Service
class InnerService {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void innerMethod() {
        // 此方法始终运行在独立事务中
    }
}

在这种情况下,即使 outerMethod 回滚, innerMethod 的更改也不会受影响,因其事务已提交。

5.4 事务失效问题排查与最佳实践

尽管声明式事务极为便利,但在实际开发中常因误用而导致“事务不生效”。

5.4.1 私有方法调用导致事务不生效的原因分析

@Service
public class UserService {

    @Transactional
    private void updateUserInternal() {
        // 修改数据库
    }

    public void updateUser() {
        updateUserInternal(); // 自调用,无法触发代理
    }
}

原因:Spring事务依赖代理机制,只有外部调用才能进入代理逻辑。内部方法调用绕过了代理,导致事务失效。

解决方案:
- 将方法改为 public 并由外部调用;
- 使用 ApplicationContext 获取代理对象进行自我调用;
- 重构逻辑,避免自调用。

5.4.2 异常被捕获后未触发回滚的解决方案

@Transactional
public void riskyOperation() {
    try {
        dangerousCall();
    } catch (Exception e) {
        log.error("捕获异常", e);
        // 事务不会回滚!
    }
}

解决办法:手动设置回滚标志:

TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

或抛出运行时异常:

throw new RuntimeException(e);

5.4.3 多数据源环境下分布式事务的初步探讨

单体应用中可通过 JtaTransactionManager + Atomikos 实现多数据源事务协调。微服务时代更多采用最终一致性方案(如TCC、Saga、消息队列补偿)来应对分布式事务挑战。

未来趋势是结合 Spring Cloud Alibaba Seata 等框架,实现跨服务的全局事务管理。

6. Java Web全栈开发整合实战

6.1 Spring + Spring MVC + MyBatis框架整合

在企业级Java Web开发中,Spring + Spring MVC + MyBatis(简称SSM)已成为主流的全栈技术组合。该架构充分发挥了各框架的优势:Spring负责业务逻辑与事务管理,Spring MVC处理Web请求调度,MyBatis提供灵活的SQL控制能力。

6.1.1 web.xml中ContextLoaderListener与DispatcherServlet配置协同

web.xml 是 Java Web 应用的部署描述符,SSM 整合的关键在于正确配置 Spring 容器与 Spring MVC 的启动顺序。

<!-- 配置Spring根容器 -->
<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>classpath:applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<!-- 配置Spring MVC前端控制器 -->
<servlet>
    <servlet-name>dispatcherServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath:spring-mvc.xml</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
    <servlet-name>dispatcherServlet</servlet-name>
    <url-pattern>/</url-pattern>
</servlet-mapping>
  • ContextLoaderListener 启动父容器(IoC容器),加载 Service、DAO 等组件。
  • DispatcherServlet 启动子容器,仅管理 Controller,可访问父容器中的 Bean,但反之不行。
  • 两个配置文件分工明确: applicationContext.xml 负责数据源、事务、Service 扫描; spring-mvc.xml 负责注解驱动、视图解析、Controller 扫描。

6.1.2 SqlSessionFactoryBean与DataSource的注入流程

MyBatis 的核心是 SqlSessionFactory ,通过 Spring 的 SqlSessionFactoryBean 实现集成。

<!-- 数据源配置(使用Druid) -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/ssm_db?useSSL=false&amp;serverTimezone=UTC"/>
    <property name="username" value="root"/>
    <property name="password" value="123456"/>
</bean>

<!-- 配置SqlSessionFactoryBean -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="configLocation" value="classpath:mybatis-config.xml"/>
    <property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>

<!-- 扫描Mapper接口 -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    <property name="basePackage" value="com.example.dao"/>
    <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
  • MapperScannerConfigurer 自动将 Mapper 接口注册为 Spring Bean,无需手动实现。
  • mybatis-config.xml 可配置别名、插件(如分页)、缓存等全局设置。

6.1.3 Service层事务控制与DAO层接口绑定

Spring 声明式事务通过 AOP 实现,需启用注解支持:

<!-- 启用事务注解 -->
<tx:annotation-driven transaction-manager="transactionManager"/>

<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
</bean>

在 Service 层添加 @Transactional 注解:

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void transferMoney(String from, String to, BigDecimal amount) {
        userMapper.decreaseBalance(from, amount);
        int i = 1/0; // 模拟异常,触发回滚
        userMapper.increaseBalance(to, amount);
    }
}
组件 职责
DataSource 连接数据库池
SqlSessionFactoryBean 创建 MyBatis 会话工厂
MapperScannerConfigurer 自动注册 Mapper 接口
TransactionManager 控制事务边界
ContextLoaderListener 加载业务层Bean
DispatcherServlet 处理HTTP请求

该整合模式确保了代码低耦合、高内聚,同时具备良好的可测试性与扩展性。后续章节将进一步探讨前后端交互设计与安全控制机制。

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

简介:Java Web编程是构建互联网应用的核心技术,涵盖服务器端开发、前端交互与数据管理。本资源合集“Java Web开发全栈技术帮助文档合集”整合了四大关键技术手册:Struts2.0、Spring、DHTML和JavaScript,全面覆盖MVC框架、依赖注入、动态网页与客户端脚本等核心内容。经过系统整理与实践验证,该文档合集适用于各层次开发者,助力掌握Java Web开发全流程,提升构建高效、稳定、交互性强的Web应用的能力。


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

Logo

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

更多推荐