Spring Boot + WebMagic + MyBatis 多数据源爬虫环境搭建实战
简介:面向Java后端开发者与爬虫初学者的一套完整爬虫框架实战工程,围绕SpringBoot整合WebMagic、Mybatis并落地多数据源方案,解决单数据源难以兼顾缓存与关系型存储的痛点。压缩包共134个文件、约64.89MB,以42个java源码与44个class编译文件为主体,辅以16个xml配置、5个properties及yml等配置文件,另有jsp页面与日志、工程元数据文件,目录结构清晰,便于对照阅读与二次开发。已有1290人学习下载。工程中可见ElasticSearchUtils、BookInfoCrawler、NovelCrawler、ScreenShotCrawler等抓取与工具类,以及BaseDataSourceConfig、SecondDbDataSourceConfig等数据源配置类,读者可据此掌握Downloader、Pipeline、Scheduler组件的定制方式、多数据源连接池配置与AOP事务切换思路,并参考ElasticSearch写入实践,快速搭建可扩展的采集与存储链路。
1. 从一次爬虫项目启动失败说起:Spring Boot + WebMagic + MyBatis 多数据源环境怎么搭
上周帮一个做数据采集的朋友重启他的爬虫项目,代码拉下来直接mvn spring-boot:run,结果启动日志里一片红:DataSource注入冲突、WebMagic的Spider拿不到Pipeline里的 Mapper、MyBatis 的SqlSessionFactory只认了一个库。这套 Spring Boot + WebMagic + MyBatis 多数据源的组合,几乎是中小型爬虫项目的标配——Spring Boot 管生命周期,WebMagic 管抓取调度,MyBatis 管落库,多数据源把「抓取元数据」和「业务结果数据」分开放。装不好不会报编译错误,而是运行到一半数据写错库、爬虫线程池把连接池打满,排查起来相当玄学。这篇记录的是我按版本锁定、依赖顺序、数据源隔离、验证跑通这条线走完的完整过程,适合刚接手这类项目、卡在环境配置上的开发者照着复现。
2. 版本与依赖清单:先把 Spring Boot、WebMagic、MyBatis 的版本对齐
这套环境翻车最多的地方不是代码,是版本。WebMagic 的发布节奏和 Spring Boot 完全不在一个频道上,MyBatis 的 Spring Boot Starter 又对 Spring Boot 大版本敏感,三者随便挑「最新版」拼在一起,十有八九在启动阶段就炸。我一般先把版本钉死再动手,后面所有配置都围绕这张表来。
2.1 我实际用的版本组合
| 组件 | 版本 | 选它的理由 |
|---|---|---|
| JDK | 8 或 11 | WebMagic 0.9.x 在 JDK 8 上最稳,JDK 11 需要额外注意反射相关告警 |
| Spring Boot | 2.7.x | 2.7 是 2.x 最后一个稳定线,对DataSource自动配置行为可预期 |
| WebMagic | 0.9.0 | 0.9.x 是使用面最广的稳定版,Spider、Pipeline、Downloader接口成熟 |
| MyBatis Spring Boot Starter | 2.3.x | 与 Spring Boot 2.7 匹配,避免SqlSessionFactory自动配置冲突 |
| 数据库驱动 | MySQL Connector/J 8.0.x | 与 MySQL 8 服务端匹配,注意serverTimezone参数 |
| 连接池 | HikariCP(Spring Boot 内置) | 不额外引 Druid,减少多数据源下的配置面 |
版本对齐的核心原则只有一条:Spring Boot 定大版本,MyBatis Starter 跟着 Spring Boot 走,WebMagic 单独锁小版本。WebMagic 0.9.0 内部依赖的 HttpClient、Jsoup 版本较老,如果项目里又引了新版 Jsoup 做页面解析,容易出现NoSuchMethodError,这种运行期报错比启动失败更难查。
2.2 pom.xml 里的依赖顺序与排除项
依赖不是引进来就行,顺序和排除项决定了会不会出现类冲突。下面是我项目里pom.xml的关键片段:
<!-- Spring Boot 父工程,锁定整体版本线 --> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <!-- Web 与 JDBC 基础能力 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <!-- WebMagic 核心,排除它自带的旧版 slf4j 绑定 --> <dependency> <groupId>us.codecraft</groupId> <artifactId>webmagic-core</artifactId> <version>0.9.0</version> <exclusions> <exclusion> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> </exclusion> </exclusions> </dependency> <!-- 需要注解式抽取时再加,不需要就别引,减少依赖面 --> <dependency> <groupId>us.codecraft</groupId> <artifactId>webmagic-extension</artifactId> <version>0.9.0</version> </dependency> <!-- MyBatis 与 Spring Boot 整合 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <!-- MySQL 驱动,运行期才需要 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency> </dependencies>这段配置里有两个点值得说清楚。第一,webmagic-core排掉slf4j-log4j12,是因为 Spring Boot 默认用 Logback,两个绑定同时存在会在启动时打印「Class path contains multiple SLF4J bindings」,虽然不致命,但日志会重复输出,排查问题时干扰很大。第二,webmagic-extension只在用@TargetUrl、@ExtractBy这类注解抽取时才需要,纯Page.getHtml().xpath()的写法可以不引,少一个依赖就少一类冲突。
2.3 目录结构约定
多数据源项目最容易乱的是包结构。我习惯按「数据源归属」而不是「技术分层」来切,这样 Mapper 扫描路径和事务边界都好界定:
src/main/java/com/example/crawler ├── config │ ├── PrimaryDataSourceConfig.java │ └── SecondaryDataSourceConfig.java ├── primary # 抓取元数据:任务表、URL 队列表 │ ├── mapper │ └── entity ├── secondary # 业务结果:抓取到的结构化数据 │ ├── mapper │ └── entity ├── pipeline # WebMagic Pipeline 实现 └── CrawlerApplication.javaprimary和secondary两个包各自持有 Mapper,配置类里用@MapperScan分别指向,避免一个@MapperScan扫全项目导致 Mapper 绑错SqlSessionFactory。这个约定看起来是小事,但后面排查「数据写错库」时,包结构清晰能省掉大量时间。
3. 多数据源配置:两个 DataSource 怎么隔离、怎么绑定 Mapper
多数据源的难点不在「配两个连接」,而在「让每个 Mapper 明确知道自己该用哪个连接」。Spring Boot 的自动配置默认只认一个DataSource,一旦你手动声明了第二个,自动配置就会退让,所有细节都得自己接管。这一章按「配置类 → 参数 → Mapper 绑定」的顺序落地。
3.1 application.yml 里的双数据源参数
先把连接参数写进配置文件,用前缀区分,避免键名冲突:
spring: datasource: primary: jdbc-url: jdbc:mysql://127.0.0.1:3306/crawler_meta?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: crawler_meta password: change_me driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 minimum-idle: 2 connection-timeout: 30000 secondary: jdbc-url: jdbc:mysql://127.0.0.1:3306/crawler_data?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: crawler_data password: change_me driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000这里有个高频坑:多数据源场景下键名必须用jdbc-url而不是url。HikariCP 绑定的是jdbcUrl属性,写成url时 Spring Boot 不会报错,但连接池初始化时拿不到地址,启动直接抛jdbcUrl is required。我第一次配的时候就是照抄单数据源的url写法,卡了半小时。
连接池大小也要分开考虑。primary存的是任务和 URL 队列,读写频繁但单次数据量小,10 个连接够用;secondary是抓取结果落库,批量插入多,给到 20。两个池加起来 30 个连接,要确认 MySQL 的max_connections留有余量,否则爬虫并发一上来就是Too many connections。
3.2 主数据源配置类
主数据源要加@Primary,让没显式指定的事务和自动配置有个默认落点:
@Configuration @MapperScan( basePackages = "com.example.crawler.primary.mapper", sqlSessionFactoryRef = "primarySqlSessionFactory" ) public class PrimaryDataSourceConfig { // 用 @Primary 标记默认数据源,未指定时优先注入它 @Primary @Bean(name = "primaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.primary") public DataSource primaryDataSource() { return DataSourceBuilder.create().build(); } @Primary @Bean(name = "primarySqlSessionFactory") public SqlSessionFactory primarySqlSessionFactory( @Qualifier("primaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); // 指定该数据源对应的 Mapper XML 位置 bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/primary/*.xml")); return bean.getObject(); } @Primary @Bean(name = "primaryTransactionManager") public DataSourceTransactionManager primaryTransactionManager( @Qualifier("primaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }@ConfigurationProperties(prefix = "spring.datasource.primary")负责把 yml 里的参数绑到DataSourceBuilder上,省掉手写setJdbcUrl之类的代码。sqlSessionFactoryRef是@MapperScan的关键参数,它告诉扫描器「这个包下的 Mapper 全部用primarySqlSessionFactory」,这是多数据源不串库的根本保障。mapperLocations按数据源分目录存放 XML,路径写错时启动不会报错,但运行期调用会抛Invalid bound statement,所以路径要和实际目录严格对应。
3.3 从数据源配置类
从数据源配置和主数据源结构一致,去掉@Primary,换掉前缀和扫描包:
@Configuration @MapperScan( basePackages = "com.example.crawler.secondary.mapper", sqlSessionFactoryRef = "secondarySqlSessionFactory" ) public class SecondaryDataSourceConfig { @Bean(name = "secondaryDataSource") @ConfigurationProperties(prefix = "spring.datasource.secondary") public DataSource secondaryDataSource() { return DataSourceBuilder.create().build(); } @Bean(name = "secondarySqlSessionFactory") public SqlSessionFactory secondarySqlSessionFactory( @Qualifier("secondaryDataSource") DataSource dataSource) throws Exception { SqlSessionFactoryBean bean = new SqlSessionFactoryBean(); bean.setDataSource(dataSource); bean.setMapperLocations( new PathMatchingResourcePatternResolver() .getResources("classpath:mapper/secondary/*.xml")); return bean.getObject(); } @Bean(name = "secondaryTransactionManager") public DataSourceTransactionManager secondaryTransactionManager( @Qualifier("secondaryDataSource") DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }两个配置类都写完后,启动时 Spring 会创建两个DataSource、两个SqlSessionFactory、两个事务管理器。这里要注意:方法参数上的@Qualifier不能省。如果只按类型注入,容器里有两个DataSource时会抛NoUniqueBeanDefinitionException,报错信息里会列出两个候选 Bean 名,看到这个报错基本就是漏了@Qualifier。
3.4 WebMagic 的 Pipeline 怎么拿到指定数据源的 Mapper
WebMagic 的Pipeline是普通 Spring Bean,只要在构造时注入对应 Mapper 即可,但要注意Spider的创建方式——如果用Spider.create(new MyProcessor())手动 new,Pipeline 里的依赖不会自动注入。我一般把Spider也交给 Spring 管理:
@Component public class ArticlePipeline implements Pipeline { // 注入从数据源的 Mapper,抓取结果写业务库 private final ArticleMapper articleMapper; public ArticlePipeline(ArticleMapper articleMapper) { this.articleMapper = articleMapper; } @Override public void process(ResultItems resultItems, Task task) { Article article = resultItems.get("article"); if (article != null) { // 单条插入,批量场景可改为攒批后 foreach 插入 articleMapper.insert(article); } } }ArticleMapper位于secondary.mapper包下,被secondarySqlSessionFactory扫描,所以这条写入链路天然落到业务库。如果 Pipeline 里同时要写元数据库(比如记录抓取状态),就再注入一个primary包下的 Mapper,两个 Mapper 各自绑定各自的SqlSessionFactory,互不干扰。这里的原则是:Mapper 的包归属决定数据源,不要在业务代码里手动切数据源,手动切容易在事务里失效。
4. 爬虫核心装配:Spider 启动、线程数与抓取入口的落地写法
数据源通了之后,才轮到爬虫本体。WebMagic 的装配看起来简单,但线程数、去重、启动时机这几个参数配错,表现是「跑得动但跑不久」——要么内存涨、要么目标站点封 IP、要么任务重复抓。这一章把启动链路拆开写。
4.1 Spider 的创建与核心参数
@Configuration public class SpiderConfig { @Bean(destroyMethod = "close") public Spider articleSpider(ArticlePipeline articlePipeline, ArticleProcessor articleProcessor) { return Spider.create(articleProcessor) // 抓取入口,可换成从元数据库读取待抓 URL .addUrl("https://example.com/list") // 抓取线程数,按目标站点承受能力和本机资源调 .thread(5) // 每个站点两次请求的间隔,单位毫秒 .setDownloader(new HttpClientDownloader()) .addPipeline(articlePipeline); } }thread(5)是抓取并发数,不是越大越好。单机跑、目标站点没有明确限流时,5 到 10 是比较稳的区间;调到 20 以上,常见结果是目标站点返回 429 或直接断连,反而拖慢整体进度。addUrl这里写死了一个入口,实际项目里我一般改成从primary库的 URL 队列表读取,用for循环addUrl,这样重启后能续抓。destroyMethod = "close"保证应用关闭时 Spider 的线程池被回收,不加的话本地开发反复重启会残留线程。
4.2 Processor 里的抽取逻辑与异常兜底
@Component public class ArticleProcessor implements PageProcessor { // 列表页链接规则与详情页链接规则 private Site site = Site.me() .setRetryTimes(3) .setSleepTime(1000) .setTimeOut(10000) .setUserAgent("Mozilla/5.0 (compatible; Crawler/1.0)"); @Override public void process(Page page) { // 列表页:抽取详情链接加入待抓队列 if (page.getUrl().regex("/list").match()) { page.addTargetRequests( page.getHtml().xpath("//a[@class='item']/@href").all()); } else { // 详情页:抽取字段,抽取失败时跳过而不是抛异常中断 String title = page.getHtml().xpath("//h1/text()").get(); if (title == null) { page.setSkip(true); return; } Article article = new Article(); article.setTitle(title); article.setUrl(page.getUrl().get()); page.putField("article", article); } } @Override public Site getSite() { return site; } }setRetryTimes(3)和setSleepTime(1000)是防封的基本盘,重试次数别设太高,否则一个失效链接会拖住线程。page.setSkip(true)是抽取失败时的兜底,跳过当前页而不是让异常冒到 Spider 层——WebMagic 对 Processor 里抛出的异常处理比较粗,一个页面解析失败可能影响整批任务的状态记录。page.putField("article", article)的 key 要和 Pipeline 里resultItems.get("article")严格一致,写错时 Pipeline 拿到 null,数据静默丢失,这种问题不看日志很难发现。
4.3 启动时机与优雅停止
爬虫不适合在CommandLineRunner里同步跑,会阻塞应用启动。我一般用异步方式触发,并留一个手动停止的入口:
@Component public class CrawlerBootstrap implements ApplicationRunner { private final Spider articleSpider; public CrawlerBootstrap(Spider articleSpider) { this.articleSpider = articleSpider; } @Override public void run(ApplicationArguments args) { // 异步启动,避免阻塞 Spring 启动流程 new Thread(() -> articleSpider.run(), "crawler-main").start(); } }ApplicationRunner在容器初始化完成后执行,此时数据源、Mapper、Pipeline 都已就绪。用独立线程跑spider.run(),主线程继续走完启动流程,健康检查接口能正常响应。停止时调用spider.stop(),它会等当前任务处理完再退出,比直接kill -9安全——直接杀进程的后果是连接池里的连接没归还、正在写的数据可能半条落库,这种脏数据后面清理起来很麻烦。
5. 避坑与排查:多数据源爬虫项目最常见的五类翻车
这套环境的问题有个共同特征:启动成功不代表配置正确,很多错误要跑到第一条数据落库才暴露。下面五条是我实际踩过、并且带别人排查时反复出现的,按「现象 → 原因 → 解决」记下来。
5.1 启动报 jdbcUrl is required
现象:应用启动时 HikariCP 抛jdbcUrl is required with driverClassName,但 yml 里明明写了连接地址。
原因:多数据源配置下把键名写成了url。单数据源时 Spring Boot 会自动把spring.datasource.url映射到jdbcUrl,但自定义前缀 +DataSourceBuilder的组合不会做这层映射。
解决:把spring.datasource.primary.url改成spring.datasource.primary.jdbc-url,两个数据源都要改。改完重启,日志里能看到 HikariPool 正常初始化的记录。
5.2 Mapper 注入报 NoUniqueBeanDefinitionException
现象:启动时提示expected single matching bean but found 2: primaryDataSource, secondaryDataSource。
原因:某个@Bean方法或构造器参数按类型注入DataSource,容器里有两个候选,无法决定用哪个。
解决:在所有注入DataSource、SqlSessionFactory、TransactionManager的地方补@Qualifier("beanName")。配置类内部的方法参数最容易漏,逐个检查一遍。
5.3 数据写进了错误的库
现象:抓取任务正常跑,但业务表里没数据,去元数据库一看,数据全写进了任务表所在的库。
原因:@MapperScan的basePackages范围写大了,比如主数据源配置类扫了com.example.crawler整个包,把从数据源的 Mapper 也扫了进去,后加载的SqlSessionFactory覆盖了绑定。
解决:两个@MapperScan的包路径必须精确到各自的mapper子包,不能有包含关系。改完后在启动日志里搜Mapped Statements collection,确认每个 Mapper 的方法都注册在预期的SqlSessionFactory下。
5.4 爬虫跑一段时间后连接池耗尽
现象:运行十几分钟后开始报Connection is not available, request timed out,重启后又能跑一阵。
原因:Pipeline 里每条数据一个事务、一次连接获取,抓取速度快于数据库写入速度时,连接被长时间占用;或者某处手动开了SqlSession没关。
解决:Pipeline 改成攒批写入,比如每 50 条或每 2 秒 flush 一次,用foreach批量插入;同时确认没有手动openSession()的代码。连接池的maximum-pool-size不要盲目调大,先看是不是写入模式的问题。
5.5 抽取字段为 null 但无报错
现象:数据能落库,但某些字段一直是 null,日志里没有任何异常。
原因:XPath 或 CSS 选择器写错,page.getHtml().xpath(...).get()返回 null,代码里没做判空就直接 set 进实体;或者putField的 key 和 Pipeline 里get的 key 不一致。
解决:在 Processor 里对关键字段做判空并打日志,把当前 URL 和选择器一起输出;putField的 key 抽成常量,Processor 和 Pipeline 共用同一个常量,杜绝手写字符串不一致。
6. 装完怎么验证:三条链路跑通才算环境搭好
环境搭没搭好,不看启动日志有没有报错,看三条链路是否各自跑通:元数据库能读能写、业务库能批量落库、爬虫能完整跑完一轮并优雅停止。我习惯按下面的顺序验证,每一步都有明确的观察点。
6.1 数据源连通性验证
先不启动爬虫,单独验证两个数据源。写一个临时的CommandLineRunner或者用测试类,分别从两个DataSource拿连接执行select 1:
@SpringBootTest class DataSourceSmokeTest { @Autowired @Qualifier("primaryDataSource") private DataSource primaryDataSource; @Autowired @Qualifier("secondaryDataSource") private DataSource secondaryDataSource; @Test void bothDataSourcesConnect() throws Exception { // 分别取连接执行简单查询,确认两个池都能拿到连接 try (Connection c1 = primaryDataSource.getConnection(); Connection c2 = secondaryDataSource.getConnection()) { assert c1.isValid(3); assert c2.isValid(3); // 打印实际连接的库名,确认没有串库 System.out.println("primary -> " + c1.getCatalog()); System.out.println("secondary -> " + c2.getCatalog()); } } }getCatalog()打印出来的库名必须和 yml 里配置的一致,这一步能直接抓出「两个数据源指向同一个库」的低级错误。isValid(3)的超时给 3 秒,超过说明网络或连接池有问题。
6.2 Mapper 绑定验证
数据源通了不代表 Mapper 绑对了。对每个数据源各调一个 Mapper 方法,观察 SQL 实际打到哪个库。最直接的办法是开 MyBatis 的 SQL 日志:
logging: level: com.example.crawler.primary.mapper: debug com.example.crawler.secondary.mapper: debug启动后调用一次articleMapper.insert(...),日志里应该出现==> Preparing: INSERT INTO article ...,同时确认这条 SQL 出现在secondary数据源的连接上。如果日志里 SQL 正常但数据没进业务库,回到 5.3 检查@MapperScan范围。
6.3 爬虫完整跑一轮的观察点
前两步通过后,启动应用,观察这几个点:
| 观察点 | 正常表现 | 异常时看哪里 |
|---|---|---|
| Spider 启动 | 日志出现抓取线程启动、URL 入队记录 | SpiderConfig的addUrl是否为空 |
| 页面抽取 | 每个详情页有字段抽取日志 | Processor 的选择器与判空逻辑 |
| Pipeline 落库 | 业务库行数随抓取增长 | 5.3、5.4 两条 |
| 停止 | 调用spider.stop()后线程退出、连接归还 | destroyMethod是否配置 |
跑一轮之后,去业务库count(*)对比抓取日志里的成功条数,两个数字对得上,说明抽取、Pipeline、数据源绑定整条链路是通的。对不上就按差值定位:差在抽取阶段看 Processor 日志,差在落库阶段看 SQL 日志。
6.4 我每次都会走一遍的习惯
这套环境我前后配过好几次,最省时间的做法不是一次配全,而是先单数据源跑通爬虫,再加第二个数据源。单数据源阶段确认 Spider、Processor、Pipeline 没问题,加第二个数据源时只需要关注配置类和@MapperScan,问题范围小一半。从那以后我每次搭多数据源项目,都强制先跑通单库版本再拆,拆完必跑 6.1 和 6.2 两个验证,不跳过。希望这套顺序能帮到你,少在启动日志里绕圈。
本文还有配套的精品资源,点击获取