1. 概述
本文将重点介绍如何在Spring REST API中实现服务端分页,并搭配简单的AngularJS前端。我们还会探讨Angular中常用的表格组件——UI Grid。
2. 依赖
2.1. JavaScript依赖
要使Angular UI Grid正常工作,需要在HTML中引入以下脚本:
2.2. Maven依赖
后端使用Spring Boot,需要以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
⚠️ 注意:其他依赖未在此列出,完整列表请查看GitHub项目中的pom.xml。
3. 应用说明
这是一个简单的学生目录应用,允许用户在分页表格中查看学生信息。应用使用Spring Boot,运行在嵌入式Tomcat服务器和嵌入式数据库中。
在API实现方面,分页有多种方式(详见Spring REST分页指南)。我们的方案简单直接:在URI查询参数中传递分页信息,格式如:/student/get?page=1&size=2
。
4. 前端实现
4.1. UI-Grid配置
index.html
包含所需导入和表格实现:
<!DOCTYPE html>
<html lang="en" ng-app="app">
<head>
<link rel="stylesheet" href="https://cdn.rawgit.com/angular-ui/
bower-ui-grid/master/ui-grid.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/
1.5.6/angular.min.js"></script>
<script src="https://cdn.rawgit.com/angular-ui/bower-ui-grid/
master/ui-grid.min.js"></script>
<script src="view/app.js"></script>
</head>
<body>
<div ng-controller="StudentCtrl as vm">
<div ui-grid="gridOptions" class="grid" ui-grid-pagination>
</div>
</div>
</body>
</html>
关键点解析:
-
ng-app
:加载app
模块,其下所有元素都属于该模块 -
ng-controller
:加载StudentCtrl
控制器(别名vm
),其下所有元素都属于该控制器 -
ui-grid
:Angular UI Grid指令,使用gridOptions
作为配置(在app.js
的$scope
中定义)
4.2. AngularJS模块
首先在app.js
中定义模块:
var app = angular.module('app', ['ui.grid','ui.grid.pagination']);
✅ 注入ui.grid
启用表格功能,注入ui.grid.pagination
启用分页支持。
接着定义控制器:
app.controller('StudentCtrl', ['$scope','StudentService',
function ($scope, StudentService) {
var paginationOptions = {
pageNumber: 1,
pageSize: 5,
sort: null
};
StudentService.getStudents(
paginationOptions.pageNumber,
paginationOptions.pageSize).success(function(data){
$scope.gridOptions.data = data.content;
$scope.gridOptions.totalItems = data.totalElements;
});
$scope.gridOptions = {
paginationPageSizes: [5, 10, 20],
paginationPageSize: paginationOptions.pageSize,
enableColumnMenus:false,
useExternalPagination: true,
columnDefs: [
{ name: 'id' },
{ name: 'name' },
{ name: 'gender' },
{ name: 'age' }
],
onRegisterApi: function(gridApi) {
$scope.gridApi = gridApi;
gridApi.pagination.on.paginationChanged(
$scope,
function (newPage, pageSize) {
paginationOptions.pageNumber = newPage;
paginationOptions.pageSize = pageSize;
StudentService.getStudents(newPage,pageSize)
.success(function(data){
$scope.gridOptions.data = data.content;
$scope.gridOptions.totalItems = data.totalElements;
});
});
}
};
}]);
$scope.gridOptions
中的分页配置说明:
-
paginationPageSizes
:可选的每页条数 -
paginationPageSize
:默认每页条数 -
enableColumnMenus
:是否启用列菜单 -
useExternalPagination
:服务端分页时必须设为true
-
columnDefs
:列定义(需与服务器返回的JSON字段名匹配) -
onRegisterApi
:注册表格事件,这里监听分页变化事件
API请求服务:
app.service('StudentService',['$http', function ($http) {
function getStudents(pageNumber,size) {
pageNumber = pageNumber > 0?pageNumber - 1:0;
return $http({
method: 'GET',
url: 'student/get?page='+pageNumber+'&size='+size
});
}
return {
getStudents: getStudents
};
}]);
5. 后端与API实现
5.1. RESTful服务
支持分页的REST API实现:
@RestController
public class StudentDirectoryRestController {
@Autowired
private StudentService service;
@RequestMapping(
value = "/student/get",
params = { "page", "size" },
method = RequestMethod.GET
)
public Page<Student> findPaginated(
@RequestParam("page") int page, @RequestParam("size") int size) {
Page<Student> resultPage = service.findPaginated(page, size);
if (page > resultPage.getTotalPages()) {
throw new MyResourceNotFoundException();
}
return resultPage;
}
}
@RestController
是Spring 4.0引入的便捷注解,相当于@Controller
+ @ResponseBody
。
API接受两个参数:
-
page
:页码 -
size
:每页条数
❌ 踩坑提示:当请求页码超过总页数时抛出MyResourceNotFoundException
。
返回Spring Data的Page
对象,包含完整的分页元数据。
5.2. 服务实现
服务层根据页码和条数返回数据:
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
private StudentRepository dao;
@Override
public Page<Student> findPaginated(int page, int size) {
return dao.findAll(new PageRequest(page, size));
}
}
5.3. 持久层实现
使用嵌入式数据库和Spring Data JPA:
@EnableJpaRepositories("com.baeldung.web.dao")
@ComponentScan(basePackages = { "com.baeldung.web" })
@EntityScan("com.baeldung.web.entity")
@Configuration
public class PersistenceConfig {
@Bean
public JdbcTemplate getJdbcTemplate() {
return new JdbcTemplate(dataSource());
}
@Bean
public DataSource dataSource() {
EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
EmbeddedDatabase db = builder
.setType(EmbeddedDatabaseType.HSQL)
.addScript("db/sql/data.sql")
.build();
return db;
}
}
✅ 配置关键点:
-
@EnableJpaRepositories
:扫描Spring Data JPA仓库 -
@ComponentScan
:自动扫描所有Bean -
@EntityScan
:扫描实体类 - 数据源:使用嵌入式数据库(HSQL),启动时执行SQL脚本
仓库接口定义:
public interface StudentRepository extends JpaRepository<Student, Long> {}
简单粗暴!一行代码搞定CRUD操作,这就是Spring Data JPA的威力。
6. 分页请求与响应
调用API http://localhost:8080/student/get?page=1&size=5
,返回JSON示例:
{
"content":[
{"studentId":"1","name":"Bryan","gender":"Male","age":20},
{"studentId":"2","name":"Ben","gender":"Male","age":22},
{"studentId":"3","name":"Lisa","gender":"Female","age":24},
{"studentId":"4","name":"Sarah","gender":"Female","age":26},
{"studentId":"5","name":"Jay","gender":"Male","age":20}
],
"last":false,
"totalElements":20,
"totalPages":4,
"size":5,
"number":0,
"sort":null,
"first":true,
"numberOfElements":5
}
Page
对象字段说明:
-
last
:是否最后一页 -
first
:是否第一页 -
totalElements
:总记录数(传递给UI-Grid的totalItems
) -
totalPages
:总页数(由totalElements/size
计算得出) -
size
:每页条数(客户端传递的size
参数) -
number
:当前页码(后端数组索引从0开始,所以page-1
) -
sort
:排序参数 -
numberOfElements
:当前页实际记录数
7. 分页测试
使用RestAssured进行测试(RestAssured教程)。
7.1. 测试准备
添加静态导入:
io.restassured.RestAssured.*
io.restassured.matcher.RestAssuredMatchers.*
org.hamcrest.Matchers.*
Spring测试配置:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest("server.port:8888")
✅ 配置说明:
-
@SpringApplicationConfiguration
:加载Application.java
配置的上下文 -
@WebAppConfiguration
:声明使用WebApplicationContext
-
@IntegrationTest
:启动应用,使REST服务可用于测试
7.2. 测试用例
基础测试用例:
@Test
public void givenRequestForStudents_whenPageIsOne_expectContainsNames() {
given().params("page", "0", "size", "2").get(ENDPOINT)
.then()
.assertThat().body("content.name", hasItems("Bryan", "Ben"));
}
验证第一页返回指定学生(Bryan和Ben)。
更多测试用例:
@Test
public void givenRequestForStudents_whenResourcesAreRetrievedPaged_thenExpect200() {
given().params("page", "0", "size", "2").get(ENDPOINT)
.then()
.statusCode(200);
}
验证接口返回200状态码。
@Test
public void givenRequestForStudents_whenSizeIsTwo_expectNumberOfElementsTwo() {
given().params("page", "0", "size", "2").get(ENDPOINT)
.then()
.assertThat().body("numberOfElements", equalTo(2));
}
验证每页返回2条记录。
@Test
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources() {
given().params("page", "0", "size", "2").get(ENDPOINT)
.then()
.assertThat().body("first", equalTo(true));
}
验证首次请求返回第一页。
⚠️ 更多测试用例请参考GitHub项目。
8. 总结
本文展示了如何使用AngularJS的UI-Grid实现数据表格,以及如何实现所需的服务端分页。示例代码和测试用例可在GitHub项目中获取。
✅ 运行方式:
mvn spring-boot:run
访问地址:http://localhost:8080/