什么是服务定位器模式
服务定位器是一个了解如何提供各种应用所需的服务(或组件)的对象。在服务定位器中,每个服务(或组件)都只有一个单独的实例,并通过ID 唯一地标识。 用这个 ID 就能从服务定位器中得到这个服务(或组件)。
何时可以考虑使用服务定位器模式
服务定位器模式的目的是按需返回服务实例,当依赖是按需的或需要在运行时查找时,我们可以使用服务定位器模式将客户端与具体实现解耦。
服务定位器包含的组件
服务定位器执行流程
下面我们就以一个模拟发送短信的例子,来体验一把服务定位器模式。因spring已经提供了服务定位器,本示例就以spring提供的服务定位器为例
spring 服务定位器
spring的服务定位器主要是通过ServiceLocatorFactoryBean实现。它实现 FactoryBean接口,并封装了服务定位器模式的所有设计组件,为客户端提供了一个干净的 API 以按需获取对象
spring服务定位器实现流程
1、定义一个实体类,这个实体类后边插件绑定具体短信服务会用到
@Data @AllArgsConstructor @NoArgsConstructor @Builder public class SmsRequest implements Serializable { private Map<String,Object> metaDatas; private String to; private String message; private SmsType smsType; }
2、定义短信发送服务接口
public interface SmsProvider { SmsResponse sendSms(SmsRequest smsRequest); }
3、定义短信服务定位器工厂,用来选取具体的短信服务
public interface SmsFactory { SmsProvider getProvider(SmsType smsType); }
4、定义短信发送具体实现类
@Component public class AliyunSmsProvider implements SmsProvider { @Override public SmsResponse sendSms(SmsRequest smsRequest) { System.out.println("来自阿里云短信:" + smsRequest); return SmsResponse.builder() .code("200").message("发送成功") .success(true).result("阿里云短信的回执").build(); } }
注: 该具体服务必须是spring的bean
5、配置ServiceLocatorFactoryBean
@Bean @ConditionalOnMissingBean public FactoryBean smsFactory(){ ServiceLocatorFactoryBean serviceLocatorFactoryBean = new ServiceLocatorFactoryBean(); serviceLocatorFactoryBean.setServiceLocatorInterface(SmsFactory.class); // spring beanName映射,自定义名称映射关系, Properties properties = new Properties(); properties.setProperty(SmsType.ALIYUN.toString(),"aliyunSmsProvider"); properties.setProperty(SmsType.TENCENT.toString(),"tencentSmsProvider"); serviceLocatorFactoryBean.setServiceMappings(properties); return serviceLocatorFactoryBean; }
注: 短信服务定位器工厂,本质是通过beanName来找到具体的短信服务,如下示例
public interface SmsFactory { SmsProvider getProvider(String beanName); }
但为了保持一定的业务语义,我们可以通过
serviceLocatorFactoryBean.setServiceMappings(properties);
来实现业务类型–>beanName的映射
映射源码如下
private String tryGetBeanName(@Nullable Object[] args) { String beanName = ""; if (args != null && args.length == 1 && args[0] != null) { beanName = args[0].toString(); } // Look for explicit serviceId-to-beanName mappings. if (serviceMappings != null) { String mappedName = serviceMappings.getProperty(beanName); if (mappedName != null) { beanName = mappedName; } } return beanName; }
6、业务如何使用
@RequiredArgsConstructor public class SmsService { private final SmsFactory smsFactory; public SmsResponse sendSms(SmsRequest smsRequest){ return smsFactory.getProvider(smsRequest.getSmsType()).sendSms(smsRequest); } }
7、测试
@Autowired private SmsService smsService; @Test public void testAliyunSms(){ SmsRequest smsRequest = SmsRequest.builder() .message("模拟使用阿里云短信发送") .to("136000000001") .smsType(SmsType.ALIYUN) .build(); SmsResponse smsResponse = smsService.sendSms(smsRequest); Assert.assertTrue(smsResponse.isSuccess()); System.out.println(smsResponse); }
眼尖的朋友可能会发现,你上面实现的服务定位器,用如下方法
@Autowired private ApplicationContext applicationContext; @Override public void run(ApplicationArguments args) throws Exception { SmsProvider smsProvider = applicationContext.getBean("aliyunSmsProvider",SmsProvider.class); smsProvider.sendSms() }
也能实现,干嘛那么繁琐,如果你翻看源码,就会发现,他底层实现和上述的实现基本上一样
private Object invokeServiceLocatorMethod(Method method, Object[] args) throws Exception { Class<?> serviceLocatorMethodReturnType = getServiceLocatorMethodReturnType(method); try { String beanName = tryGetBeanName(args); Assert.state(beanFactory != null, "No BeanFactory available"); if (StringUtils.hasLength(beanName)) { // Service locator for a specific bean name return beanFactory.getBean(beanName, serviceLocatorMethodReturnType); } else { // Service locator for a bean type return beanFactory.getBean(serviceLocatorMethodReturnType); } } catch (BeansException ex) { if (serviceLocatorExceptionConstructor != null) { throw createServiceLocatorException(serviceLocatorExceptionConstructor, ex); } throw ex; } }
其实服务定位器模式和依赖注入都是控制反转概念的实现,服务定位器将一组职责相似的服务内聚到了一起,并实现服务提供方、服务使用方完全的解耦,上面举的例子也可以看成一种策略+工厂模式的具体实现。最后提一嘴,serviceLocatorFactoryBean.setServiceMappings(properties);这个也不是必须的,只要你业务语义和beanName名字一样即可
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-service-locator