Spring REST 與 AngularJS 表單分頁

REST,Spring
Remote
0
03:09 PM · Dec 01 ,2025

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>
</code>

注意: 其他依賴項在此處未指定,完整的列表請查看 pom.xmlGitHub 項目中。

3. 關於應用程序該應用程序是一個簡單的學生目錄應用程序,允許用户以分頁表格網格的形式查看學生詳情。

該應用程序使用 Spring Boot 並在嵌入式 Tomcat 服務器上運行,並使用嵌入式數據庫。

在 API 端,有幾種分頁方式,詳細描述在 Spring REST Pagination 文章中,強烈建議您閲讀此文章與本文檔一起閲讀。

我們的解決方案很簡單——將分頁信息作為 URI 查詢參數,如下所示:/student/get?page=1&size=2

4. The Client Side

First, we need to create the client-side logic.

4.1. The UI-Grid

Our index.html will have the imports we need and a simple implementation of the table grid:



    
        
        
        
        
    
    
        

Let’s have a closer look at the code:

  • ng-app – is the Angular directive that loads the module app. All elements under these will be part of the appmodule
  • ng-controller – is the Angular directive that loads the controller StudentCtrl with an alias of vm. All elements under these will be part of the StudentCtrl controller
  • ui-grid – is the Angular directive that belongs to Angular ui-grid and uses gridOptions as its default settings, gridOptions is declared under $scope in app.js

4.2. The AngularJS Module

Let’s first define the module in app.js:

var app = angular.module('app', ['ui.grid','ui.grid.pagination']);

We declared the app module and we injected ui.grid to enable UI-Grid functionality; we also injected ui.grid.pagination to enable pagination support.

Next, we’ll define the controller:

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;
                 });
            });
        }
    };
}]);

Let’s now have a look at the custom pagination settings in $scope.gridOptions:

  • paginationPageSizes – defines the available page size options
  • paginationPageSize – defines the default page size
  • enableColumnMenus – is used to enable/disable the menu on columns
  • useExternalPagination – is required if you are paginating on the server side
  • columnDefs – the column names that will be automatically mapped to the JSON object returned from the server. The field names in the JSON Object returned from the server and the column name defined should match.
  • onRegisterApi – the ability to register public methods events inside the grid. Here we registered the gridApi.pagination.on.paginationChanged to tell UI-Grid to trigger this function whenever the page was changed.

And to send the request to the 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 服務

以下是具有分頁支持的簡單 RESTful 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,我們聲明瞭它接受兩個參數,即 pagesize,這些參數也將確定返回給客户端的記錄數量。

我們還添加了一個簡單的驗證,如果頁碼大於總頁數,則會拋出 MyResourceNotFoundException

最後,我們將 Page 作為響應返回——這是一個 Spring Data 中非常有用的組件,它具有分頁數據。

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(來自 Spring Boot)來掃描實體類。

我們還聲明瞭簡單的數據源——使用嵌入數據庫,該數據庫在啓動時將運行提供的 SQL 腳本。

現在,我們創建存儲庫:

>
public interface StudentRepository extends JpaRepository<Student, Long> {}

基本上,這裏我們只需要做這些;如果您想更深入地瞭解如何設置和使用功能強大的 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
}

需要注意的是,服務器返回一個 org.springframework.data.domain.Page DTO,封裝了我們的 Student 資源。

Page 對象將具有以下字段:

  • last – 如果它是最後一頁,則設置為 true,否則設置為 false
  • first – 如果它是第一頁,則設置為 true,否則設置為 false
  • totalElements – 總行/記錄數。 在我們的示例中,我們將其傳遞給 ui-grid 選項 $scope.gridOptions.totalItems 以確定將有多少頁可用
  • totalPages – 頁總數,該值是從 (totalElements / size) 計算得出的
  • size – 每頁記錄數,該值通過客户端參數 size 傳遞
  • number – 客户端發送的頁碼,在我們的響應中,由於我們使用 Student 數組,該數組採用零索引,因此頁碼在後端中減 1
  • sort – 頁面的排序參數
  • numberOfElements – 頁面的行/記錄數

7. Testing Pagination

現在,我們來設置用於測試我們的分頁邏輯的測試,使用 RestAssured;要了解有關 RestAssured 的更多信息,您可以查看此教程。

7.1. Preparing the Test

為了簡化測試類的開發,我們將添加靜態導入:

io.restassured.RestAssured.*
io.restassured.matcher.RestAssuredMatchers.*
org.hamcrest.Matchers.*

接下來,我們將設置啓用 Spring 的測試:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = Application.class)
@WebAppConfiguration
@IntegrationTest("server.port:8888")

@SpringApplicationConfiguration 有助於 Spring 知道如何加載 ApplicationContext,在本例中,我們使用了 Application.java 來配置我們的 ApplicationContext

@WebAppConfiguration 被定義為告訴 Spring,用於加載的 ApplicationContext 應該是 WebApplicationContext

@IntegrationTest 被定義為觸發應用程序啓動時運行測試,這使得我們的 REST 服務可供測試使用。

7.2. The Tests

這是我們第一個測試用例:

@Test
public void givenRequestForStudents_whenPageIsOne_expectContainsNames() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .assertThat().body("content.name", hasItems("Bryan", "Ben"));
}

此測試用例旨在測試當 page 1 和 size 2 被傳遞到 REST 服務時,從服務器返回的 JSON 內容應包含名稱 BryanBen

讓我們分析一下測試用例:

  • givenRestAssured 的一部分,用於啓動構建請求,您也可以使用 with()
  • getRestAssured 的一部分,如果使用它會觸發 GET 請求,使用 post() 用於 POST 請求
  • hasItemshamcrest 的一部分,檢查值是否有匹配

我們添加了幾個更多的測試用例:

@Test
public void givenRequestForStudents_whenResourcesAreRetrievedPaged_thenExpect200() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .statusCode(200);
}

此測試斷言當實際調用點時,會收到 OK 響應:

@Test
public void givenRequestForStudents_whenSizeIsTwo_expectNumberOfElementsTwo() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .assertThat().body("numberOfElements", equalTo(2));
}

此測試斷言當請求頁面大小為 2 時,返回的頁面大小實際上為 2:

@Test
public void givenResourcesExist_whenFirstPageIsRetrieved_thenPageContainsResources() {
    given().params("page", "0", "size", "2").get(ENDPOINT)
      .then()
      .assertThat().body("first", equalTo(true));
}

此測試斷言當資源被調用時,第一次調用時,“first”頁的值為 true。

有許多其他測試在存儲庫中,所以請務必查看 GitHub 項目。

8. 結論

本文介紹瞭如何使用 UI-Grid

AngularJS

中實現數據表格網格,以及如何實現所需的服務器端分頁。

要運行 Spring boot 項目,只需執行 mvn spring-boot:run

並訪問本地地址 http://localhost:8080/。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.