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、订单总额和支付成功状态都不能直接信任。
渐进式迁移
登录、支付和完整的订单流程风险较高,不适合一开始就全部改写。可以先从产品搜索页面入手:
- 保留现有的 Spring MVC、Service、Hibernate 和数据库代码。
- 为产品搜索增加 JSON API。
- 在一个 JSP 页面中划出独立的 AngularJS 区域。
- 用
$http替换该区域原有的$.ajax()。 - 用
ng-repeat、ng-model等指令替换手写的 DOM 更新。 - 补充加载中、无数据、失败和重复提交等状态。
- 验证认证、CSRF、XSS、越权访问和接口参数校验。
- 运行稳定后,再迁移 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 设计仍可复用,主要区别在于浏览器端的实现方式。
备注:内容仅供参考。
