内容聚合类系统在如今的开发需求中非常常见,无论是企业内部的资讯门户,还是面向用户的资讯类小程序,都绕不开从第三方新闻 API 拉取数据这一环节。很多人第一步就能把接口调通,但真正要做成稳定的服务,还需要处理数据格式统一、定时抓取、失败重试、消息推送等一系列问题。本文将以 Spring Boot 为基础,完整演示一套热点资讯聚合与推送方案的落地过程。

一、整体架构设计与依赖准备
在动手写代码之前,先明确整体的数据流向。一个典型的资讯聚合服务包含四个部分:抓取层负责调用外部新闻 API;解析层把不同来源的数据映射成统一的实体;存储层将数据落入数据库并做好去重;推送层通过定时任务把热点内容分发给用户。这种分层设计的好处是,后续新增一个新闻源时,只需要在抓取层和解析层扩展,不影响其他模块。
依赖方面,除了 spring-boot-starter-web 之外,建议引入 WebClient 所需的 spring-boot-starter-webflux、数据库相关的 mybatis-plus 或 spring-data-jpa,以及 lombok 简化实体类编写。下面是一个基础的 pom 依赖配置:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>这里同时引入了 web 和 webflux 两个 starter,Web 容器仍然使用 Tomcat,而 WebClient 可以作为 HTTP 客户端使用,两者并不冲突。相比传统的 RestTemplate,WebFlux 提供的 WebClient 支持异步非阻塞调用,在并发抓取多个新闻源时性能优势明显。
二、调用新闻 API 并统一数据模型
不同的新闻 API 返回的字段差异很大,比如有的接口返回 title、有的叫 news_title,日期格式也可能是时间戳或者字符串。为了方便后续处理,第一件事是设计一个统一的实体类,把所有外部数据都转换成这个结构。常见的聚合数据、天行数据等新闻接口,核心字段无非是标题、来源、链接、发布时间和分类,可以抽象成如下实体:
@Data
@TableName("t_news")
public class NewsArticle {
@TableId(type = IdType.AUTO)
private Long id;
private String title; // 新闻标题
private String source; // 来源,如新浪、网易
private String category; // 分类,如科技、财经
private String url; // 原文链接
private String digest; // 摘要
private LocalDateTime publishTime;
private Date createTime;
}接下来编写抓取服务。使用 WebClient 调用接口时,建议把 API 地址和密钥放到配置文件中,而不是硬编码在代码里,方便切换环境。抓取逻辑的核心是发起请求、解析 JSON、转换为实体列表这三步:
@Service
public class NewsFetchService {
private final WebClient webClient;
@Value("${news.api.url}")
private String apiUrl;
@Value("${news.api.key}")
private String apiKey;
public NewsFetchService(WebClient.Builder builder) {
this.webClient = builder.build();
}
public List<NewsArticle> fetchHotNews() {
NewsResponse response = webClient.get()
.uri(apiUrl + "?key={key}&num=20", apiKey)
.retrieve()
.bodyToMono(NewsResponse.class)
.block(Duration.ofSeconds(10));
if (response == null || response.getNewslist() == null) {
return Collections.emptyList();
}
return response.getNewslist().stream()
.map(this::convertToArticle)
.collect(Collectors.toList());
}
private NewsArticle convertToArticle(NewsItem item) {
NewsArticle article = new NewsArticle();
article.setTitle(item.getTitle());
article.setSource(item.getSource());
article.setDigest(item.getDigest());
article.setUrl(item.getUrl());
article.setPublishTime(parseTime(item.getCtime()));
article.setCreateTime(new Date());
return article;
}
}解析时间字段时要格外小心,很多接口返回的是秒级时间戳字符串,直接用 LocalDateTime.parse 会抛异常。建议写一个健壮的解析方法,兼容时间戳和常见格式字符串。另外,block 操作虽然方便,但如果聚合的新闻源较多,可以考虑用 Flux 合并多个请求,通过 subscribeOn 指定调度器实现并行抓取。
三、数据去重与定时抓取任务
新闻接口的数据是持续更新的,定时抓取时必然遇到重复数据的问题。最简单有效的方案是给原文链接字段建立唯一索引,入库时用 INSERT IGNORE 或者在代码层先查询再插入。数据量大的场景下,推荐使用 Redis 的 Set 结构缓存已抓取的链接摘要,新数据先过 Redis 判断,命中的直接丢弃,减轻数据库压力。
@Service
@RequiredArgsConstructor
public class NewsStoreService {
private final NewsArticleMapper newsMapper;
private final StringRedisTemplate redisTemplate;
public int saveIfAbsent(List<NewsArticle> articles) {
String key = "news:fetched:urls";
int saved = 0;
for (NewsArticle article : articles) {
Boolean isNew = redisTemplate.opsForSet()
.add(key, article.getUrl());
if (Boolean.TRUE.equals(isNew)) {
newsMapper.insert(article);
saved++;
}
}
return saved;
}
}定时任务可以用 Spring 自带的 @Scheduled 注解实现,不需要额外引入 Quartz。下面的例子每隔三十分钟抓取一次,并且错开高峰时段。需要注意的是,启动类上必须加上 @EnableScheduling,否则任务不会执行:
@Component
@RequiredArgsConstructor
public class NewsSyncTask {
private final NewsFetchService fetchService;
private final NewsStoreService storeService;
private final NewsPushService pushService;
@Scheduled(cron = "0 */30 * * * ?")
public void syncHotNews() {
try {
List<NewsArticle> articles = fetchService.fetchHotNews();
int count = storeService.saveIfAbsent(articles);
if (count > 0) {
pushService.pushLatest(articles);
}
log.info("新闻同步完成,新增 {} 条", count);
} catch (Exception e) {
log.error("新闻同步失败", e);
}
}
}这里有一个容易被忽视的细节:如果部署了多个实例,@Scheduled 任务会在每台机器上都执行一遍,造成重复抓取。解决办法是引入分布式锁,比如用 Redisson 的 tryLock,或者使用 xxl-job 这类任务调度平台统一管理触发。
四、热点推送的实现方式与容错处理
推送环节的选型取决于业务场景。面向 App 用户可以用极光推送、个推等第三方服务;面向公众号或企业内部可以用 Server 酱、企业微信机器人 Webhook。以企业微信机器人为例,只需要向指定的 Webhook 地址发送 POST 请求即可,实现成本非常低:
String.format("【%s】%s\n%s",
a.getSource(), a.getTitle(), a.getUrl()))
.collect(Collectors.joining("\n\n"));
Map<String, Object> body = new HashMap<>();
body.put("msgtype", "text");
body.put("text", Collections.singletonMap("content", content));
webClient.post()
.uri(webhookUrl)
.bodyValue(body)
.retrieve()
.bodyToMono(String.class)
.retryWhen(Retry.backoff(3, Duration.ofSeconds(2)))
.block();
}
}外部 API 调用不可能百分之百成功,容错策略必不可少。上面代码中使用了 retryWhen 配置指数退避重试,这是 WebClient 自带的能力,比手写循环重试优雅得多。除此之外,还应该设置合理的超时时间,避免外部接口拖垮自身线程;对抓取任务做兜底日志,失败时记录上下文便于排查。
最后补充一点,如果新闻源较多,抓取频率较高,可以在入口处增加一层简单的限流,用 Guava 的 RateLimiter 控制对外部 API 的请求速率,既保护了对方的接口配额,也降低了被封禁的风险。整体方案跑通之后,后续扩展推荐功能、热点排名、用户订阅分类等能力,都可以在这个基础架构上平滑叠加。
Spring Boot新闻API资讯聚合修改时间:2026-09-03 22:41:13