Spring中Interface与对应Impl Bean扫描冲突问题

最近在做一个SpringBoot的脚手架,熟悉Spring的各种特性与一些第三方工具的使用。在做到Interface与Impl注入到Controller调用的时候报Bean冲突问题。

问题重现

定义一个Interface文件 – GreetingService

1
2
3
4
5
6
7
package com.zackku.service.hello.service;

import ...

public interface GreetingService {
List<Greeting> findGreats(String content, Integer offset, Integer rows);
}

对应的Impl实现 – GreetingServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.zackku.service.hello.service.impl;

import ...

@Service
public class GreetingServiceImpl implements GreetingService {
private static final String TEMPLATE = "this is %s!";

private GreetingMapper greetingMapper;

@Autowired
public GreetingServiceImpl(GreetingMapper greetingMapper) {
this.greetingMapper = greetingMapper;
}

@Override
public List<Greeting> findGreats(String content, Integer offset, Integer rows) {
List<Greeting> greets = greetingMapper.find(content, offset, rows);
return greets;
}
}

注入调用方 – GreetingController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.zackku.api.hello.controller;

import ...
@RestController
@RequestMapping("/greeting")
public class GreetingController {
private GreetingService greetingService;

@Autowired
public GreetingController(GreetingService greetService) {
this.greetingService = greetService;
}

@RequestMapping("/findList")
public List<Greeting> find(@RequestParam(value = "content") String content,
@RequestParam(value = "offset", defaultValue = "0") Integer offset,
@RequestParam(value = "rows", defaultValue = "2") Integer rows) {
List<Greeting> greets = greetingService.findGreats(content, offset, rows);
return greets;
}
}

以上代码在项目启动时候会报错

Parameter 0 of constructor in com.zackku.api.hello.controller.GreetingController required a single bean, but 2 were found:
- greetingServiceImpl: defined in file […/com/zackku/service/hello/service/impl/GreetingServiceImpl.class]
- greetingService: defined in file […/com/zackku/service/hello/service/GreetingService.class]

原因分析

居然Service的Interface和Impl会冲突?百思不得其解,况且我的Interface也没写注入的注解,即使写了也不应该会冲突。因为Spring的接口注入规则就是只会注入对应接口实现,除非实现有多个。
问题找了好久,最后Debug源码发现,在GreetingController 中 greetingService.findGreats的调用居然被MapperProxy代理。这样的话大概猜想到是GreetingService被多次扫描注册了。
回看组件扫描的配置

1
2
3
4
5
6
7
8
9
10
11
package com.zackku.api;
import ...
@ComponentScan(basePackages = {"com.zackku"})
@MapperScan(basePackages = {"com.zackku.service"})
@EnableAutoConfiguration
@EnableCaching
public class ApiApplication extends SpringBootServletInitializer {
public static void main(String args[]) {
SpringApplication.run(ApiApplication.class, args);
}
}

再看看MapperScan的扫描原理:MapperScan在指定的basePackages下,扫描所有的interface并注册。

原来MapperScan并没有想象中那么智能去扫指定的Mapper。当ComponentScan与MapperScan扫的路径有交集的时候,里面的interface会被重复的注册使用。最终导致报Bean冲突错误。

解决方法

MapperScan注解有一个markerInterface的参数,

The scanner will register all interfaces in the base package that also have
the specified interface class as a parent.

也就是说,MapperScan可以只扫描继承该类的接口
具体如下:

  1. 新增一个MapperInterface接口,空的就行

    1
    public interface MapperInterface {}
  2. GreetingMapper继承该接口

    1
    public interface GreetingMapper extends MapperInterface {}

以后新增的Mapper也要继承该接口

  1. 修改扫描配置
    1
    @MapperScan(basePackages = {"com.zackku.service"}, markerInterface = MapperInterface.class)

这样就完美解决问题了。

源码参考

Java web 项目脚手架: https://github.com/Zack-Ku/java-web-scaffold

听说你想请我喝下午茶?~