Technical Specifications at a Glance
| Parameter | Description |
|---|---|
| Language | Java |
| Frameworks | Spring Boot, MyBatis |
| Pagination Protocol | SQL LIMIT/OFFSET + COUNT |
| Core Dependency | pagehelper-spring-boot-starter 1.4.6 |
| Integration Method | MyBatis Interceptor |
| Applicable Scenarios | List queries, admin dashboards, filtered pagination |
| GitHub Stars | Not provided in the source |
Pagination is a foundational capability for backend list APIs
Without pagination, an API fetches all records in a single request. This may not be obvious with small datasets, but as table size grows, query latency, network transfer cost, and frontend rendering pressure all increase together.
The value of PageHelper is straightforward: you do not need to rewrite the original SQL structure. You only declare pagination parameters before the query, and PageHelper automatically adds the count and limit logic. This approach is especially friendly to existing MyBatis projects.
PageHelper offers several clear advantages
- It has low intrusion into business SQL.
- It fits naturally into the MyBatis ecosystem.
- It centralizes pagination logic in the service layer, which improves maintainability.
- It automatically generates total-count queries, making frontend pagination controls easier to implement.
<dependency>
<!-- Add the PageHelper Spring Boot Starter -->
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.4.6</version>
</dependency>
This configuration connects PageHelper directly to a Spring Boot project.
A common pagination response structure should be defined first
To keep API contracts stable between frontend and backend, define a unified pagination object up front. The most common fields include the total record count and the current page data collection, so the frontend can directly calculate total pages and render tables.
public class PageBean
<T> {
private Long total; // Total record count
private List
<T> items; // Current page data
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
public List
<T> getItems() {
return items;
}
public void setItems(List
<T> items) {
this.items = items;
}
}
This class serves as a unified container for paginated results and avoids redesigning the response structure for every endpoint.
Pagination must be started before the query runs
The key to using PageHelper is a single step: call PageHelper.startPage(pageNum, pageSize) before executing the Mapper query. It stores the pagination parameters in the current thread context so the next MyBatis query can be intercepted.
// Start pagination: this must be placed immediately before the query
PageHelper.startPage(pageNum, pageSize);
// Execute the Mapper query normally: no need to write LIMIT manually
List
<Article> articles = articleMapper.list(userId, categoryId, state);
This code enables pagination without changing the signature of the business query method.
The Controller layer should only handle parameters and responses
The Controller should not manage pagination details. It should only receive the page number, page size, and filter conditions, then return the result to the frontend as-is. This keeps responsibilities clear and makes it easier to add authorization, logging, and parameter validation later.
@GetMapping("/articles")
public Result<PageBean<Article>> list(
Integer pageNum,
Integer pageSize,
@RequestParam(required = false) Integer categoryId,
@RequestParam(required = false) String state) {
// Call the service layer to execute the paginated query
PageBean
<Article> pageBean = articleService.list(pageNum, pageSize, categoryId, state);
return Result.success(pageBean);
}
This code exposes the paginated API while keeping the Controller layer lightweight.
The Service layer should own the core pagination assembly logic
The actual pagination workflow should live in the Service layer. It needs to start pagination, execute the query, convert the result into a Page object, and extract both the total count and the current page data.
@Override
public PageBean
<Article> list(Integer pageNum, Integer pageSize,
Integer categoryId, String state) {
// 1. Create the pagination result object
PageBean
<Article> pb = new PageBean<>();
// 2. Start pagination and intercept the next SQL query
PageHelper.startPage(pageNum, pageSize);
// 3. Get the current user context to build business conditions
Map<String, Object> map = ThreadLocalUtil.get();
Integer userId = (Integer) map.get("id");
// 4. Execute the query; the result can actually be cast to Page
List
<Article> articles = articleMapper.list(userId, categoryId, state);
Page
<Article> page = (Page<Article>) articles;
// 5. Extract the total count and current page results
pb.setTotal(page.getTotal());
pb.setItems(page.getResult());
return pb;
}
This code connects the three key steps of pagination parameter injection, business query execution, and result extraction.
Mapper SQL can keep its original dynamic query style
One of PageHelper’s biggest advantages is that you usually do not need to rewrite Mapper XML just for pagination. You can focus only on business conditions, and the pagination plugin will automatically rewrite the final SQL.
<select id="list" resultType="org.example.pojo.Article">
select * from article
<where>
<if test="categoryId != null">
category_id = #{categoryId}
</if>
<if test="state != null">
and state = #{state}
</if>
and create_user = #{userid}
</where>
</select>
This SQL only expresses filtering conditions. The plugin automatically appends the LIMIT and COUNT logic.
PageHelper relies on the MyBatis interceptor mechanism internally
PageHelper is not based on Spring AOP. It uses the MyBatis Interceptor mechanism. startPage() writes the page number and page size into ThreadLocal, and the next query in the same thread is intercepted and rewritten.
The plugin typically executes two kinds of SQL statements: one SELECT COUNT(*) query to calculate the total number of records, and one query with LIMIT offset, pageSize to fetch the current page data. This is the fundamental reason it can provide low-intrusion pagination.
The image content in the source is primarily a platform logo and brand mark

AI Visual Insight: The original image is a brand identifier, so no technical visual analysis is required.
Boundary control matters when using PageHelper in production
First, validate pageNum and pageSize to prevent negative page numbers or excessively large page sizes. Second, startPage() must apply only to the query that immediately follows it. Otherwise, thread reuse can cause pagination to affect the wrong SQL statement.
In addition, for complex joins or subqueries, pay attention to the cost of the automatically generated count SQL. If the count query is too expensive, consider optimizing the total-count query separately.
public void validatePageParam(Integer pageNum, Integer pageSize) {
// Protect the minimum page number
if (pageNum == null || pageNum < 1) {
throw new IllegalArgumentException("pageNum must be greater than or equal to 1");
}
// Protect the page size upper bound to avoid overloading the database
if (pageSize == null || pageSize < 1 || pageSize > 100) {
throw new IllegalArgumentException("pageSize must be between 1 and 100");
}
}
This code blocks invalid parameters before the paginated query runs and helps reduce database pressure.
This integration pattern fits most backend list retrieval scenarios
If your project uses Spring Boot + MyBatis and includes standard list pages such as articles, users, orders, or logs, PageHelper is a low-cost integration with stable benefits.
It separates pagination from business SQL, which makes code cleaner and pagination behavior more predictable. For small and medium-sized backend systems, this approach is usually efficient enough.
FAQ
1. Why does PageHelper require an immediate call before the query?
Because it uses ThreadLocal to store pagination parameters and only intercepts the next query in the current thread. If another SQL statement runs in between, pagination may be applied to the wrong query.
2. Why can the query result be cast directly to Page?
Because once pagination is enabled, PageHelper wraps the MyBatis query result in a Page instance. This lets you read pagination metadata such as getTotal() and getResult() directly.
3. Do I still need to write LIMIT manually in Mapper SQL after using PageHelper?
No. Writing LIMIT manually often duplicates or even conflicts with the plugin’s behavior. The correct approach is to let SQL focus only on business filtering and let PageHelper handle pagination automatically.
[AI Readability Summary]
This article walks through the full pagination implementation flow for integrating PageHelper with Spring Boot and MyBatis. It covers dependency configuration,
PageBeanencapsulation, practical Controller/Service/Mapper patterns, and the interceptor mechanism, helping developers roll out high-performance pagination with minimal intrusion.