系统生态化后,最头疼的事请就是强依赖上游系统提供的服务。当服务链路太长,开发过程要求部署整套服务,这个需要的资源是巨大的。最近着手解决这个问题,今天记录下来。

在解决这个问题的过程中,得到三种解决方案。如下:

  1. dubbo提供的服务降级(本地伪装)
  2. dubbo客户端本地存根
  3. 手动替换
    我选择的是第三种,至于为什么选择第三种,是因为前两种都需要修改原有的代码来实现,以及我还需要一个开关来控制是否需要调用mock服务。所以最后选择自己实现。

dubbo提供的服务降级

dubbo本身是提供降级服务的,不过比较简单,在官方解释中,也叫做本地伪装。

服务降级,设计是,在当服务提供方宕机或者超时后,客户端不抛出异常,而是通过mock数据返回所设定的值。

在spring配置文件中,按照以下方式配置:

1
<dubbo:reference interface="com.ofcoder.IWelcome" mock="com.ofcoder.WelcomeMock" />

或者:

1
@Reference(mock="com.ofcoder.WelcomeMock")

然后你需要提供所指定的Mock类,该类要求继承所需要降级的接口。

1
2
3
4
5
public class WelcomeMock implements IWelcome {
public String sayHello(String name) {
return "hey guy.";
}
}

那么当上游服务调用失败后,则会自动触发降级服务。mock的值还支持return、throw、force等关键字。

return

使用 return 来返回一个字符串表示的对象,作为 Mock 的返回值。合法的字符串可以是:

  1. empty: 代表空,基本类型的默认值,或者集合类的空值
  2. null: null
  3. true: true
  4. false: false
  5. JSON 格式: 反序列化 JSON 所得到的对象

throw

使用 throw 来返回一个 Exception 对象,作为 Mock 的返回值。

抛出默认的RPCException

1
<dubbo:reference interface="com.ofcoder.IWelcome" mock="throw" />

抛出指定的Exception:

1
<dubbo:reference interface="com.ofcoder.IWelcome" mock="throw com.ofcoder.MockException" />

force

force用来强制执行降级服务,这种情况不会调用远程服务。

强制抛异常:

1
<dubbo:reference interface="com.ofcoder.IWelcome" mock="force:throw com.ofcoder.MockException" />

dubbo客户端存根

降级是在服务调用之后,根据调用结果判断是否需要返回mock数据,而存根(Stub)是在调用之前增强服务。可以理解成AOP增强。

实现原理是由,dubbo把客户端某个接口生成Proxy实例,通过Stub的构造方法传给Stub对象,这个Stub对象由用户实现,用户可根据具体场景判断是否需要继续调用上游服务。可用来ThreadLocal缓存,提前验证参数,调用失败后伪造容错数据等等。

如果将服务降级加进来,调用过程可以归纳为:xxxStub -> xxxProxy -> xxxMock

使用方式跟降级很相似:

1
<dubbo:service interface="com.ofcoder.IWelcome" stub="com.ofcoder.WelcomeStub" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class WelcomeStub implements IWelcome {
private final IWelcome welcome;

// Stub 必须有可传入 Proxy 的构造函数
public WelcomeStub(IWelcome welcome){
this.welcome = welcome;
}

public String sayHello(String name) {
// do something
try {
return welcome.sayHello(name);
} catch (Exception e) {
return "hey guys";
}
}
}

手动替换

实现过程是,在spring的IOC初始化之后,扫描@Reference注解,将用该注解标注的接口,使用mocktio生成mock对象,然后替换掉spring中bean,并反射将mock值塞入指定对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Component
public class DubboMock implements CommandLineRunner {
private static final Logger logger = LoggerFactory.getLogger(DubboMock.class);
private static final String SCANNER_PACKAGE = "com.ofcoder.remote";
@Value("${ofcoder.mock.dubbo.enable}")
private boolean mockEnable;

@Autowired
private ApplicationContext applicationContext;

private void registerMock() {
DefaultListableBeanFactory defaultListableBeanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();

IWelcome welcome = Mockito.mock(IWelcome.class);
Mockito.doReturn(new Result(ResultEnum.SUCCESS)).when(welcome).sayHello(Mockito.any());
defaultListableBeanFactory.registerSingleton(IWelcome.class.getCanonicalName(), welcome);
}

private void doMock() {
Reflections reflections = new Reflections(new ConfigurationBuilder().setUrls(ClasspathHelper
.forPackage(SCANNER_PACKAGE)).setScanners(new FieldAnnotationsScanner()));

Set<Field> fields = reflections.getFieldsAnnotatedWith(Reference.class);
for (Field field : fields) {
Class<?> declaringClass = field.getDeclaringClass();
Class<?> fieldClass = field.getType();
logger.info("======declaringClass:{}, fieldClass:{}=====", declaringClass, fieldClass);

try {
Object target = applicationContext.getBean(declaringClass);
Object rpcObj = applicationContext.getBean(fieldClass);
field.setAccessible(true);
field.set(target, rpcObj);
logger.info("======mock {}.{} success. this is a thing worth cheering.=====", declaringClass.getName(), field.getName());
} catch (Exception e) {
logger.warn(String.format("======mock %s.%s occur exception. e.getMessage: %s", declaringClass.getSimpleName(), field.getName(), e.getMessage()));
}
}
}

@Override
public void run(String... args) throws Exception {

if (!mockEnable) {
return;
}

registerMock();

doMock();
}
}

这样做有一个缺点,在启动的时候需要上游系统提供了服务,不然dubbo会报no provider的错误,这里建议启动时不检查服务

1
@Reference(check = false)