PAYATHON 2026

AngularJs 在 Spring MVC 客户端服务器应用中的用法

支付阿杰

结论

AngularJS 可以用于应用的浏览器端交互层,处理产品搜索、商品列表、wishlist、订单状态和历史记录等页面。Spring MVC、Hibernate 和 MySQL 则继续负责服务端控制、业务处理和数据持久化。

一种相对稳妥的集成方式是:

  • 由 Spring MVC 提供返回 JSON 的 REST API。
  • AngularJS 使用 $http 调用 API,负责页面的数据绑定和交互。
  • JSTL 继续生成服务器端页面框架,并处理尚未迁移的功能。
  • jQuery 插件可以暂时保留,但不能和 AngularJS 同时修改同一块 DOM。
  • 按页面或独立组件逐步迁移,无需一次重写整个应用。

先澄清一个概念:AngularJS 通常被归类为 MVC 或 MVVM 风格的前端框架,并不是 MCP。它提供数据绑定、指令、依赖注入、表单处理、路由和 HTTP 服务等功能。

AngularJS 已经结束官方长期支持。新项目不建议再把它作为主要技术栈。如果项目正在维护现有 AngularJS 系统,或者只打算进行有限的渐进式改造,下面的方案仍然适用。

哪些部分适合使用 AngularJS

AngularJS 适合用在数据变化频繁、交互较多的页面。

产品搜索与展示

用户输入搜索条件后,页面可以通过 $http 请求 Spring MVC API 并刷新商品列表,不必重新加载整个页面。

这类页面通常包括:

  • 搜索条件绑定
  • 分页和排序
  • 分类筛选
  • 商品详情弹窗
  • 加载状态和错误提示

Wishlist

wishlist 一般包含添加、删除和修改数量等即时操作。AngularJS 可以管理这些页面状态,再通过 REST API 将变更保存到服务器。

订单状态与历史记录

AngularJS 可以加载订单列表、展开订单明细、按日期筛选,也可以定时查询订单状态。

登录和支付

AngularJS 可以处理表单校验和提交,但身份认证、权限判断、金额计算、订单确认及支付结果验证必须由服务端完成。客户端校验只能改善使用体验,不能作为安全依据。

整体结构

系统可以分成浏览器端和服务端两部分:

Browser
  ├── JSTL:生成页面外壳、公共导航和初始配置
  ├── AngularJS:数据绑定、表单、列表和交互
  └── $http:调用 JSON API
              ↓
Spring MVC
  ├── REST Controller
  ├── Service
  └── Hibernate
              ↓
           MySQL

AngularJS 不会替代 Hibernate 或 MySQL,也不应直接访问数据库。它替代的主要是一部分 JSTL、jQuery DOM 操作和手写 Ajax 代码。

使用 Spring MVC 提供 REST API

Spring MVC 可以继续返回传统 JSP 页面,同时增加返回 JSON 的控制器。

下面的示例省略了具体的异常处理、分页实现和权限校验:

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public List<ProductDto> search(
            @RequestParam(defaultValue = "") String keyword) {
        return productService.search(keyword);
    }

    @GetMapping("/{id}")
    public ProductDto getProduct(@PathVariable Long id) {
        return productService.findById(id);
    }
}

接口可以返回如下 JSON:

[
  {
    "id": 101,
    "name": "Mechanical Keyboard",
    "price": 499.00,
    "available": true
  }
]

接口最好返回专门的 DTO,不要直接序列化 Hibernate Entity。直接返回 Entity 可能引发以下问题:

  • 懒加载异常
  • 双向关联造成循环序列化
  • 向客户端暴露不该返回的字段
  • API 格式与数据库模型耦合

使用 AngularJS 调用 Spring MVC API

在 JSP 页面中加载 AngularJS,并把需要迁移的区域交给 AngularJS 管理:

<script src="${pageContext.request.contextPath}/static/angular.min.js"></script>
<script src="${pageContext.request.contextPath}/static/js/product-app.js"></script>

<div ng-app="productApp" ng-controller="ProductController as vm"
     ng-cloak>
    <form ng-submit="vm.search()">
        <input type="text"
               ng-model="vm.keyword"
               placeholder="Search products">

        <button type="submit" ng-disabled="vm.loading">
            Search
        </button>
    </form>

    <p ng-if="vm.loading">Loading...</p>
    <p ng-if="vm.error">{{ vm.error }}</p>

    <ul>
        <li ng-repeat="product in vm.products track by product.id">
            <span ng-bind="product.name"></span>
            <span ng-bind="product.price | currency"></span>
        </li>
    </ul>
</div>

对应的 AngularJS 代码如下:

(function () {
    "use strict";

    angular
        .module("productApp", [])
        .controller("ProductController", ProductController);

    ProductController.$inject = ["$http"];

    function ProductController($http) {
        var vm = this;

        vm.keyword = "";
        vm.products = [];
        vm.loading = false;
        vm.error = null;
        vm.search = search;

        function search() {
            vm.loading = true;
            vm.error = null;

            $http.get("/api/products", {
                params: {
                    keyword: vm.keyword
                }
            }).then(function (response) {
                vm.products = response.data;
            }).catch(function () {
                vm.error = "商品加载失败,请稍后重试。";
            }).finally(function () {
                vm.loading = false;
            });
        }
    }
})();

如果应用部署在非根路径下,例如 /shop,不要把 /api/products 写死在代码中。可以由 JSP 输出应用上下文:

<script>
    window.appContextPath =
        "${pageContext.request.contextPath}";
</script>

调用时拼接该路径:

$http.get(window.appContextPath + "/api/products", {
    params: {
        keyword: vm.keyword
    }
});

与 JSTL 共存

JSTL 在服务器生成 JSP 时执行,AngularJS 则在页面到达浏览器后执行。两者处于不同的执行阶段:

JSTL:服务器端渲染
AngularJS:浏览器端渲染

JSTL 可以输出:

  • 当前用户的基本信息
  • 应用上下文路径
  • 权限标志
  • 国际化文本
  • 页面初始配置

例如:

<div ng-app="accountApp"
     ng-init="currentUserId='${sessionScope.currentUser.id}'">
</div>

复杂对象如果直接拼进 JavaScript 或 HTML 属性,容易出现转义和 XSS 问题。订单、商品和用户资料等复杂数据更适合通过受保护的 REST API 获取。

传统 JSTL 区域和 AngularJS 区域也可以放在同一个页面中:

<header>
    <c:if test="${not empty sessionScope.currentUser}">
        Welcome,
        <c:out value="${sessionScope.currentUser.displayName}" />
    </c:if>
</header>

<main ng-app="productApp">
    <!-- 这一区域由 AngularJS 管理 -->
</main>

<footer>
    <!-- 仍由 JSP/JSTL 输出 -->
</footer>

需要明确每块 DOM 由谁管理。在 AngularJS 管理的区域内,不要再用 JSTL 生成需要动态变化的列表,也不要使用 jQuery 随意替换其中的 HTML。

与 jQuery 和现有 Ajax 共存

AngularJS 提供精简的 DOM 操作能力,也可以通过 $http 发起 Ajax 请求。已经迁移到 AngularJS 的功能应优先使用:

  • ng-model 处理输入
  • ng-click 处理点击
  • ng-repeat 渲染列表
  • ng-if 或 ng-show 控制显示
  • $http 调用服务
  • directive 封装第三方 UI 插件

不要用 jQuery 和 AngularJS 修改同一个元素:

// 不推荐:绕过 AngularJS 直接修改其管理的 DOM
$("#productList").html(html);

这样会让 AngularJS 保存的数据状态与页面上的实际内容不一致。

如果必须继续使用现有的 jQuery 插件,可以将插件包装成 directive:

angular
    .module("productApp")
    .directive("datePicker", function () {
        return {
            restrict: "A",
            link: function (scope, element) {
                element.datepicker();

                scope.$on("$destroy", function () {
                    element.datepicker("destroy");
                });
            }
        };
    });

这样能把 jQuery 的影响限制在特定组件内,并在组件销毁时清理事件和资源。

表单提交和 Spring Security

如果系统使用基于 Session 的 Spring Security,AngularJS 发出的同源请求通常会自动携带 Cookie。不过,POST、PUT、PATCH 和 DELETE 请求仍需正确处理 CSRF Token。

JSP 可以把 Token 输出到页面:

<meta name="_csrf" content="${_csrf.token}">
<meta name="_csrf_header" content="${_csrf.headerName}">

然后为 AngularJS 请求统一添加请求头:

angular
    .module("productApp")
    .config(["$httpProvider", function ($httpProvider) {
        var tokenElement = document.querySelector('meta[name="_csrf"]');
        var headerElement =
            document.querySelector('meta[name="_csrf_header"]');

        if (tokenElement && headerElement) {
            $httpProvider.defaults.headers.common[
                headerElement.getAttribute("content")
            ] = tokenElement.getAttribute("content");
        }
    }]);

CSRF 的具体传递方式取决于项目使用的 Spring Security 版本和配置,应以项目现有的安全配置为准。

以下工作必须在服务端执行:

  • 用户身份验证
  • 接口权限检查
  • 重新读取商品价格
  • 计算优惠和税费
  • 检查库存
  • 检查订单归属
  • 验证支付回调签名
  • 校验请求参数

浏览器提交的价格、用户 ID、订单总额和支付成功状态都不能直接信任。

渐进式迁移

登录、支付和完整的订单流程风险较高,不适合一开始就全部改写。可以先从产品搜索页面入手:

  1. 保留现有的 Spring MVC、Service、Hibernate 和数据库代码。
  2. 为产品搜索增加 JSON API。
  3. 在一个 JSP 页面中划出独立的 AngularJS 区域。
  4. 用 $http 替换该区域原有的 $.ajax()。
  5. 用 ng-repeat、ng-model 等指令替换手写的 DOM 更新。
  6. 补充加载中、无数据、失败和重复提交等状态。
  7. 验证认证、CSRF、XSS、越权访问和接口参数校验。
  8. 运行稳定后,再迁移 wishlist 或订单查询。

采用这种方式,旧页面和新组件可以长期共存。出现问题时,影响范围也更容易控制。

注意事项

AngularJS 不能替代服务端 MVC

Spring MVC 负责 HTTP 接口、认证授权和业务入口,AngularJS 负责浏览器中的视图与交互。两者位于不同层次,可以同时使用。

避免多处维护页面状态

如果商品列表由 AngularJS 加载,就不要再让 JSTL 和 jQuery 分别维护其他版本的商品列表。同一个区域最好只有一个状态来源。

保持 REST API 稳定

错误响应可以使用统一格式,例如:

{
  "code": "PRODUCT_NOT_FOUND",
  "message": "Product does not exist"
}

客户端应根据 HTTP 状态码和业务错误码处理结果,不要依赖服务器异常堆栈或 HTML 错误页面。

处理并发和重复提交

wishlist、订单提交和支付操作都可能因重复点击而多次触发。客户端可以禁用按钮,但服务端仍要通过幂等键、订单状态检查或其他机制避免重复处理。

新项目应考虑其他方案

如果系统还没有引入 AngularJS,只是希望改善前端体验,应优先评估仍在维护的现代 Angular、React 或 Vue。现有的 Spring MVC REST API 设计仍可复用,主要区别在于浏览器端的实现方式。

备注:内容仅供参考。