Spring ServiceLocator 介绍及应用

在 Spring 中应用 ServiceLocator 方式来获取 Spring Bean 的介绍参考了那么多,其实还是数官方的 ServiceLocatorFactoryBean JavaDoc 文档最言简意该了。Spring 的 ServiceLocator 仿佛用处不大,说到底就是类似于下方找寻某个 Spring Bean 一样:

ApplicationContext context = ...; Service service = context.getBean(ServiceImpl.class); Service service = context.getBean("myService");

只是有了 ServiceLocatorFactoryBean(它本质上就是一个 FactoryBean) 后我们不需要直接与 ApplicationContext 打交道,且多个的 Spring Bean 可以从相关的一个 FactoryBean 获得。下面用两个例子来演示(代码中刨去了 package 和 import 部分的代码)

一:实现类只有一个 Spring Bean 时

接口类 Parser(我们要定位就是它的实现类)

1public interface Parser {
2    <T> T parse(String content);
3}

Parser 的一个实现类 JsonParser, 并且用 @Named 注册到了 Spring  上下文

 1@Named
 2public class JsonParser implements Parser {
 3
 4    @Override
 5    public JsonNode parse(String content) {
 6        try {
 7            return new ObjectMapper().readTree(content);
 8        } catch (IOException e) {
 9            throw new UncheckedIOException(e);
10        }
11    }
12}

用于查找 Parser 实现的接口(我们无需为它提供实现)

1public interface ParserFactory {
2   Parser getParser();
3}

声明用于查找 Parser 实现的 FactoryBean(即 ServiceLocatorFactoryBean)

 1@Configuration
 2public class AppConfig {
 3
 4    @Bean
 5    public FactoryBean parserFactory() {
 6        ServiceLocatorFactoryBean factoryBean = new ServiceLocatorFactoryBean();
 7        factoryBean.setServiceLocatorInterface(ParserFactory.class); //指定的是 ParserFactory 接口
 8        return factoryBean;
 9    }
10}

最后是应用 ServiceLocatorFacoryBean 的客户端代码

 1@SpringBootApplication
 2public class Client implements CommandLineRunner {
 3
 4    @Inject
 5    private ParserFactory parserFactory;
 6
 7    public static void main(String[] args) {
 8        SpringApplication.run(Client.class, args);
 9    }
10
11    @Override
12    public void run(String... args) throws Exception {
13        Parser parser = parserFactory.getParser();  //通过 ParserFactory 的 getParser() 定位到相应类型的 Parser 实例
14        JsonNode json = parser.parse("{\"gretting\": \"Hello World!\"}");
15        System.out.println(json.at("/gretting").asText());
16    }
17}

执行上面的代码,输出为解析后 JSON 的 gretting 值

Hello World!

上面发生了什么

进到 ServiceLocatorFactoryBean 的内部类 ServiceLocatorInvocationHandler, 关键代码如下:

 1private class ServiceLocatorInvocationHandler implements InvocationHandler {
 2
 3    private Object invokeServiceLocatorMethod(Method method, Object[] args) throws Exception {
 4        Class<?> serviceLocatorMethodReturnType = getServiceLocatorMethodReturnType(method);
 5        try {
 6            String beanName = tryGetBeanName(args);
 7            if (StringUtils.hasLength(beanName)) {
 8                // Service locator for a specific bean name
 9                return beanFactory.getBean(beanName, serviceLocatorMethodReturnType);
10            }
11            else {
12                // Service locator for a bean type
13                return beanFactory.getBean(serviceLocatorMethodReturnType);
14            }
15        }
16        catch (BeansException ex) {
17            ......
18        }
19    }
  1. ParserFactory.getParser() 不带参数的方式会找到一个指定相应类型的 Bean,如果 Spring 上下文中注册了两个 Parser 将会出错
  2. ParserFactory.getParser(beanName) 如果是带了参数,将以此参数作为 Spring Bean 的名称去 Spring 上下文中查找

现在可以测试一下第一种情况有多个 Parser 实现时的异常,再加一个 Parser 的 XML 实现 XmlParser

 1@Named
 2public class XmlParser implements Parser {
 3
 4    @Override
 5    public Document parse(String content) {
 6        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
 7
 8        try {
 9            DocumentBuilder builder = factory.newDocumentBuilder();
10            return builder.parse(new InputSource(new StringReader(content)));
11        } catch (Exception e) {
12            throw new RuntimeException(e);
13        }
14    }
15}

运行之前相同的 Client 代码,Spring 应用无法启动,异常是

Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'yanbin.blog.Parser' available: expected single matching bean but found 2: jsonParser,xmlParser
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveNamedBean(DefaultListableBeanFactory.java:1039)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:344)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:339)
    at org.springframework.beans.factory.config.ServiceLocatorFactoryBean$ServiceLocatorInvocationHandler.invokeServiceLocatorMethod(ServiceLocatorFactoryBean.java:377)
    at org.springframework.beans.factory.config.ServiceLocatorFactoryBean$ServiceLocatorInvocationHandler.invoke(ServiceLocatorFactoryBean.java:363)
    at com.sun.proxy.$Proxy35.getParser(Unknown Source)
    at yanbin.blog.Client.run(Client.java:22)

原因是找到两个 Parser 实现 jsonParser 和 xmlParser, 不知道该注入哪一个,所以报错。这时候就要用带参数的  ServiceLocator 方式,即

二:实现类有多个 Spring Bean 时

多个实现类时就必须用 Bean Name 来区分了,在有了前面的 Parser 的两个实现 jsonParser 和  xmlParser 时,ParserFactory 相应的方法要带上一个 Bean Name 参数了

新的 ParserFactory 类代码变为

1public interface ParserFactory {
2   Parser getParser(String beanName);
3}

Client 类中相关应用代码现在是

 1@SpringBootApplication
 2public class Client implements CommandLineRunner {
 3
 4    @Inject
 5    private ParserFactory parserFactory;
 6
 7    @Override
 8    public void run(String... args) throws Exception {
 9        Parser xmlParser = parserFactory.getParser("xmlParser");
10        Document doc = xmlParser.parse("<greeting>Hello World!</greeting>");
11        System.out.println(xmlParser.getClass().getSimpleName() + ": " + doc.getElementsByTagName("greeting").item(0).getTextContent());
12
13        Parser jsonParser = parserFactory.getParser("jsonParser");
14        JsonNode json = jsonParser.parse("{\"gretting\": \"Hello World!\"}");
15        System.out.println(jsonParser.getClass().getSimpleName() + ": " + json.at("/gretting").asText());
16    }
17}

执行后输出如下:

XmlParser: Hello World!
JsonParser: Hello World!

Spring Bean name 映射

默认时带参数的定位方法(如 parserFactory.getParser(beanName) 的参数是 Spring Bean 的名称,我们也可以在声明 ServiceLocatorFactoryBean 时自定义名称映射关系。比如在前面的 AppConfig 中 parserFactory() 方法中为 factoryBean 调用 setServiceMappings() 方法

1    Properties properties = new Properties();
2    properties.setProperty("j", "jsonParser");
3    properties.setProperty("x", "xmlParser");
4    factoryBean.setServiceMappings(properties);

那么在获取 jsonParser 或 xmlParser 实例时可以分别用 "j" 和  "x" 来获得

1    Parser xmlParser = parserFactory.getParser("x");
2    Parser jsonParser = parserFactory.getParser("j");

这样体验下来 ServiceLocatorFactoryBean 意义真的不是那么的大。如果我们在通过注入或实现 ApplicationContextAware 接口后获得了 ApplicationContext(BeanFactory) 后,就可直接调用 BeanFactory 的各个 getBean(...) 方法来得到任意想要的 Spring Bean。唯一能想到的好处就是前面提到过的,用于把类似的 Spring Bean 组织到一起来,由一个 ServiceLocator 来提供。

永久链接 https://yanbin.blog/spring-servicelocator-pattern/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。