Yufelix's Blog

Yufelix

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

9
2023-09-12

假设你在一个业务类中注入了这样一个 Bean:

@Autowired
private TestService testService;

TestService 是一个接口,假设它有两个实现类:TestServiceImpl1TestServiceImpl2。现在的问题是,如何根据不同的环境或请求参数,动态选择调用哪个实现类呢?

今天我们就来聊聊如何通过反射和 Spring 注解来实现这个需求。

第一步:设计一个 TestApiSwitcher

我们可以设计一个 TestApiSwitcher 来动态选择实现类:

public TestService getTestService() {
    SwitcherInvocationHandler switcherInvocationHandler = new SwitcherInvocationHandler();
    return Reflection.newProxy(TestService.class, switcherInvocationHandler);
}

这里用到了 @Bean 注解,它是 Spring 中的一个常用注解,通常用于在配置类中声明一个方法,并将其返回值注册为一个 Bean。而 @Primary 注解则用于指定一个 Bean 作为默认的首选实现。

当有多个同类型的 Bean 需要注入时,Spring 会根据类型匹配进行自动装配。但如果存在多个匹配的 Bean,Spring 会优先选择被 @Primary 注解标记的 Bean。

第二步:实现 SwitcherInvocationHandler

接下来,我们需要实现 SwitcherInvocationHandler。这个类的作用是根据请求参数动态选择实现类。

@Component
public class TestServiceProxyConfig {
    @Autowired
    private TestServiceImpl1 testServiceImpl1;
    @Autowired
    private TestServiceImpl2 testServiceImpl2;

    @Bean(name = "testService")
    @Primary
    public TestService getTestService() {
        SwitcherInvocationHandler switcherInvocationHandler = new SwitcherInvocationHandler();
        return (TestService) Proxy.newProxyInstance(TestService.class.getClassLoader(),
                        new Class[] { TestService.class }, switcherInvocationHandler);
    }

    private class SwitcherInvocationHandler implements InvocationHandler {
        private final Set<String> METHODS_ON_OBJECT_CLASS = (Set) Arrays.stream(Object.class.getMethods())
                        .map(Method::getName).collect(Collectors.toSet());

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (METHODS_ON_OBJECT_CLASS.contains(method.getName())) {
                return method.invoke(this, args);
            } else {
                try {
                    // 根据 args 进行路由
                    if (getArgInfo(args) == 1)
                        return method.invoke(testServiceImpl1, args);
                    if (getArgInfo(args) == 2)
                        return method.invoke(testServiceImpl2, args);
                    if (getArgInfo(args) == 3)
                        return method.invoke(testServiceImpl1, args);
                } catch (InvocationTargetException e) {
                    throw e.getCause();
                }
            }
            return null;
        }
    }

    private int getArgInfo(Object[] args) {
        // 按需实现具体逻辑
        int count = 0;
        if (args.length > 0 && args[0] instanceof Usr) {
            Usr usr = (Usr) args[0];
            if (usr.getAge() != null) {
                count++;
            }
            if (usr.getName() != null) {
                count++;
            }
        }
        return count;
    }
}

在这个实现中,METHODS_ON_OBJECT_CLASS 是一个记录了 Object 类基本方法的集合。SwitcherInvocationHandler 是一个代理类,实现了 InvocationHandler 接口,通过 JDK 动态代理进行实现。在每个方法调用前,都会先调用 invoke 方法。

总结

通过反射和 Spring 注解,我们可以轻松实现根据请求参数动态选择接口实现类的需求。首先,我们需要指定一个 @Primary 的 Bean 作为默认实现,然后在实现中对该 Bean 进行反射,代理接口方法。在具体方法被调用前,根据参数路由到具体的实现类中。

反射就像一把瑞士军刀,配合 Spring 注解,可以实现很多强大的功能。希望这篇文章能帮助你更好地理解如何动态选择 Spring 容器中的接口实现。如果你有任何问题或想法,欢迎在评论区留言讨论!