WebFlux 分页支持
webflux是spring框架构建响应式web新框架,但是后端只能使用非关系型数据库,包括:redis,mongo,Cassandra,elasticsearch,此文档中演示通过elasticsearch构建全异步支持的日志服务,重点在对分页支持上。
基础组件:
- spring-boot 2.2.1
- spring-boot-starter-data-elasticsearch
- spring-boot-starter-webflux
实体类
@AllArgsConstructor
@NoArgsConstructor
@Document(indexName = "application-log*", type = "doc")
public class AppLog implements Serializable {
@Id
private String id;
@Field
@JsonProperty("app")
private String app;
@Field
private String logger;
@Field
private String thread;
@Field
private String level;
@Field
private String tid;
@Field(name = "@timestamp" ,type = FieldType.Date,fielddata = true)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
@JsonProperty("timestamp")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss,SSS")
private Date timestamp;
@Field
private String details;
}
Repository类
@Repository
public interface AppLogRepository extends ReactiveElasticsearchRepository<AppLog,String> {
Flux<AppLog> findByApp(String app,Pageable page);
Mono<Long> countByApp(String app);
Service类
@Service
@Slf4j
public class AppLogServiceImpl implements AppLogService {
public static final String APPLOG_INDEX = "application-log";
private final AppLogRepository appLogRepository;
public AppLogServiceImpl(AppLogRepository appLogRepository){
this.appLogRepository = appLogRepository;
}
@Override
public Flux<AppLog> findByApp(String app, Pageable page) {
return appLogRepository.findByApp(app,page);
}
@Override
public Mono<Long> count(String app) {
return appLogRepository.countByApp(app);
}
RestController类
这里是重点,此处分页采用在Response Header中增加总数,通过通过Link 规范(RFC)输出上下页信息。
@GetMapping("/app/{app}")
public Mono<ResponseEntity> getApplog(@PathVariable String app,Pageable page){
Mono<List<AppLog>> applogs = appLogService.findByApp(app,page).collectList();
Mono<Long> total = appLogService.count(app);
return applogs.zipWith(total,(log,size) -> ResponseEntity.ok().headers(PaginationUtil.generatePaginationHttpHeaders(page,log,size,"base")).body(applogs));
}
此处:
- Pageable为Spring的分页类,可以直接引入
- 调用count是因为findByApp返回的是Flux类型,没有分页信息
- zipWith是Mono的一个函数,用于合并两个Publisher对象,此处将分页后的数据和总数合并到一个Tuple对象中,以便在构造ResponseEntity时使用
- generatePaginationHttpHeaders构造了输出头
public static <T> HttpHeaders generatePaginationHttpHeaders(Pageable pageable, List list, Long totalNumber, String baseUrl) {
HttpHeaders headers = new HttpHeaders();
int totalPages = Math.toIntExact(totalNumber/pageable.getPageSize()+1);
headers.add("X-Total-Count", Long.toString(totalNumber));
String link = "";
if ((pageable.getPageNumber() + 1) < totalPages) {
link = "<" + PaginationUtil.generateUri(baseUrl, pageable.getPageNumber() + 1, list.size()) + ">; rel=\"next\",";
}
// prev link
if ((pageable.getPageNumber()) > 0) {
link += "<" + PaginationUtil.generateUri(baseUrl, pageable.getPageNumber() - 1, list.size()) + ">; rel=\"prev\",";
}
// last and first link
int lastPage = 0;
if (totalPages > 0) {
lastPage = totalPages - 1;
}
link += "<" + PaginationUtil.generateUri(baseUrl, lastPage, list.size()) + ">; rel=\"last\",";
link += "<" + PaginationUtil.generateUri(baseUrl, 0, list.size()) + ">; rel=\"first\"";
headers.add(HttpHeaders.LINK, link);
return headers;
}
static String generateUri(String baseUrl, int page, int size) {
return UriComponentsBuilder.fromUriString(baseUrl).queryParam("page", page).queryParam("size", size).toUriString();
}