动态指定 Spring 容器内接口实现的设计思路

4 minute

假设在某个业务类中存在一个注入 bean 如下:

1@Autowired
2private TestService testService;

其中 TestService 是一个接口,假设我们有该接口有多种实现:TestServiceImpl1 和 TestServiceImpl2,在不同环境下按需调用接口对应的实现(比如可能需要能根据请求参数进行选择),这时如何实现呢?

可以通过反射的机制并配合 Spring 注解实现,以下是具体的实现过程分析。

首先我们可以设计一个 TestApiSwitcher:

1@Bean(name = "testService")
2@Primary
3public TestService getTestService() {
4    return Reflection.newProxy(TestService.class, switcherInvocationHandler);
5}

其中,@Bean 是 Spring 中的注解之一,通常用于在配置类中声明一个方法,并将其返回值作为一个 Bean 注册到 Spring 容器中。

而 @Primary 注解配合于 @Bean 注解,用于指定一个 Bean 作为默认首选的候选项。

当有多个同类型的 Bean 需要注入时,Spring 容器会根据类型匹配进行自动装配。但如果存在多个匹配的 Bean 时,会产生歧义性,导致无法确定要注入哪个 Bean。使用 @Primary 注解,可以将一个 Bean 标记为首选的候选项。当需要注入该类型的 Bean 时,如果代码中没有通过 @Resource 之类的注解进行指定, Spring 容器会优先选择被 @Primary 注解标记的 Bean 作为注入对象。

观察方法内部,我们通过 guava 库的反射操作 Reflection.newProxy 为 TestService 设置代理对象 switcherInvocationHandler,那么继续设计我们的 switcherInvocationHandler。

首先,我们需要判断被调用的方法是否是 Object 类中的方法(即通用的方法,如equals()、hashCode()、toString()等)。

  • 如果是通用方法,则直接在代理对象上调用该方法并返回结果。

  • 如果被调用的方法不是通用方法,则根据请求参数 args 进行路由,选择合适的实现类,进行对应方法的调用即可。

大致实现思路如下:

 1private class SwitcherInvocationHandler implements InvocationHandler {
 2    private final Set<String> METHODS_ON_OBJECT_CLASS = (Set) Arrays.stream(Object.class.getMethods()).map(Method::getName).collect(Collectors.toSet());
 3
 4    @Override
 5    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
 6        if (METHODS_ON_OBJECT_CLASS.contains(method.getName())) {
 7            return method.invoke(this, args);
 8        } else {
 9            try {
10                // 根据 args 进行路由
11                if (getArgInfo(args) == 1) return method.invoke(TestServiceImpl1, args);
12                if (getArgInfo(args) == 2) return method.invoke(TestServiceImpl2, args);
13                if (getArgInfo(args) == 3) return method.invoke(TestServiceImpl1, args);
14            } catch (InvocationTargetException e) {
15                throw e.getCause();
16            }
17        }
18    }
19}

其中, METHODS_ON_OBJECT_CLASS 是一个记录了 Object 类基本方法的集合。

SwitcherInvocationHandler 为一个代理类,实现了 InvocationHandler 接口,通过 JDK 动态代理进行实现。在每个方法调用前将会先进行 invoke 方法的调用。


总结:

对于一个 Spring 容器中的接口,如需根据请求参数路由到不同的实现,可以通过反射的方式实现,这其中首先需要指定一个 Primary 的 Bean 作为默认实现,在实现中对该 Bean 进行反射,代理接口方法,在具体方法被调用前根据参数路由到具体类中。反射是一把瑞士军刀,配合 Spring 注解,可以实现很多重要的操作。