Tutorial — 5-minute walkthrough¶
This walks through a complete paginated endpoint from controller → service → MyBatis mapper. By the end you'll have a working GET /reports?page=0&size=20&sort=createdAt,desc returning a JSON response shaped like Spring Data Page.
Project layout¶
src/main/java/com/example/report/
├── ReportController.java
├── ReportService.java
├── ReportMapper.java
└── Report.java
src/main/resources/
├── application.yml
└── mapper/
└── ReportMapper.xml
1. Controller¶
ReportController.java
package com.example.report;
import kr.devslab.easypaging.annotation.AutoPaginate;
import kr.devslab.easypaging.core.PageResponse;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/reports")
class ReportController {
private final ReportService reports;
ReportController(ReportService reports) {
this.reports = reports;
}
@GetMapping
@AutoPaginate(maxSize = 50)
public PageResponse<Report> list(Pageable pageable) {
return PageResponse.from(reports.findAll(), pageable);
}
}
The @AutoPaginate annotation does the work:
- Reads
Pageablefrom the request (Spring MVC'sPageableHandlerMethodArgumentResolverpopulates it from?page=,?size=,?sort=) - Sets up PageHelper so the next MyBatis query gets
LIMIT/OFFSET - Validates the sort parameter against SQL injection (rejects with HTTP 400)
- Wraps the mapper's
Listinto aPageResponseenvelope - Cleans up PageHelper's internal state — even if the mapper throws
2. Service¶
ReportService.java
package com.example.report;
import java.util.List;
import org.springframework.stereotype.Service;
@Service
class ReportService {
private final ReportMapper mapper;
ReportService(ReportMapper mapper) {
this.mapper = mapper;
}
public List<Report> findAll() {
// In real code: tenant filtering, authorization, domain rules here.
return mapper.findAll();
}
}
The aspect's PageHelper setup happens before this method runs, so mapper.findAll() is automatically paginated.
3. Mapper (interface + XML)¶
The interface is just a method signature:
ReportMapper.java
package com.example.report;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ReportMapper {
List<Report> findAll();
}
The XML carries the SQL — no LIMIT/OFFSET needed, the aspect injects them at runtime:
src/main/resources/mapper/ReportMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.report.ReportMapper">
<select id="findAll" resultType="com.example.report.Report">
SELECT id, title, created_at AS createdAt
FROM reports
</select>
</mapper>
4. Configuration¶
application.yml
spring:
datasource:
url: jdbc:postgresql://localhost:5432/reports
username: app
password: ${DB_PASSWORD}
mybatis:
mapper-locations: classpath:mapper/**/*.xml
configuration:
map-underscore-to-camel-case: true
pagehelper:
helper-dialect: postgresql # or mysql, oracle, etc.
easy-paging:
default-page-size: 20
max-page-size: 500
5. Try it¶
{
"content": [
{ "id": 137, "title": "Latest", "createdAt": "2026-05-15T..." },
/* 4 more */
],
"page": 0,
"size": 5,
"totalElements": 137,
"totalPages": 28,
"first": true,
"last": false,
"empty": false
}
What just happened¶
- The controller declared
Pageable pageable. Spring MVC auto-bound it from query parameters. @AutoPaginateran before the method body — it set up PageHelper for the next query and validated the sort parameter.- The method body called
reports.findAll()→mapper.findAll(). PageHelper intercepted the SQL and addedLIMIT 5 OFFSET 0 ORDER BY created_at desc. PageResponse.from()extracted the pagination metadata from PageHelper'sPage(which is the runtime type ofList<Report>) and packaged it into a clean JSON envelope.- The aspect's
finallycleared PageHelper'sThreadLocal— preventing state from leaking to the next request on this thread.
Next steps¶
- Offset pagination guide —
@AutoPaginateoptions and return-type choices - Keyset pagination — for unbounded time-series tables
- Custom response format — match your company's API conventions
- Sorting & page numbering — multi-column sort, null handling, 0-based convention