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/


原始标题:Pagination with Spring REST and AngularJS Table