动态指定 Spring 容器内接口实现的设计思路
假设在某个业务类中存在一个注入 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 注解,可以实现很多重要的操作。