实际这是一篇给出临时性解决方案的博文,因为这个问题有望在Spring Cloud Alibaba2.2.3版本以后解决掉,https://github.com/alibaba/spring-cloud-alibaba/issues/1346,这里也介绍了这个Bug的相关情况。
在使用RestTemplate的时候,一般情况下遇到网络故障或服务超时会报出例如:java.net.SocketTimeoutException: Read timed out, java.net.SocketException: connetct time out 这种通信异常的报错,但是当使用了@SentinelRestTemplate注解后,开启了Sentinel的限流降级以后,SentinelProtectInterceptor拦截器中com.alibaba.cloud.sentinel.custom.SentinelProtectInterceptor.intercept(HttpRequest, byte[], ClientHttpRequestExecution) 方法,Catch方法中,会吞掉错误的异常抛出,使得response返回null, 这个时候,RestTemplate调用的Response变为null,致使程序会报出 java.lang.NullPointerException的错误。
Entry hostEntry = null;
Entry hostWithPathEntry = null;
ClientHttpResponse response = null;
try {
hostEntry = SphU.entry(hostResource, EntryType.OUT);
if (entryWithPath) {
hostWithPathEntry = SphU.entry(hostWithPathResource, EntryType.OUT);
}
response = execution.execute(request, body);
if (this.restTemplate.getErrorHandler().hasError(response)) {
Tracer.trace(
new IllegalStateException("RestTemplate ErrorHandler has error"));
}
}
catch (Throwable e) {
if (!BlockException.isBlockException(e)) {
Tracer.trace(e);
// 这里会吞噬掉错误不再像外部抛出
}
else {
return handleBlockException(request, body, execution, (BlockException) e);
}
}
finally {
if (hostWithPathEntry != null) {
hostWithPathEntry.exit();
}
if (hostEntry != null) {
hostEntry.exit();
}
}
return response; // 而Response直接返回为null
sca的官方也在https://github.com/alibaba/spring-cloud-alibaba/pull/1374 这里,Rethrow non block exception and trace entry,
解决后的代码 https://github.com/alibaba/spring-cloud-alibaba/pull/1374/commits/4c5bf778632153bfbaf5dc092533f4c7303eb9d7
catch (Throwable e) {
if (!BlockException.isBlockException(e)) {
Tracer.trace(e);
if (entryWithPath) {
Tracer.traceEntry(e, hostWithPathEntry);
}
Tracer.traceEntry(e, hostEntry);
throw (IOException) e;
// 增加了对非限流降级错误的异常抛出
}
else {
return handleBlockException(request, body, execution, (BlockException) e);
}
}
在Catch中增加了rethrow exception的处理,如果还能够忍受这种情况持续下去直到Sentinel官方发布新版本去解决这个问题。那么就可以阅读到此了,只需要默默等待。
如果你无法忍受这个情况,在新版本发布前解决掉,那么只能自己想办法了。解决起来也很简单,就是覆写Sentinel的拦截器,
1、去掉 @SentinelRestTemplate 注解的使用,先临时注释掉吧,待后续官方解决后再恢复回来吧
2、自定义一个SentinelProtectInterceptor 拦截器,同时修改不抛异常的bug,
@Component
@ConditionalOnClass(name = "org.springframework.web.client.RestTemplate")
@ConditionalOnProperty(name = "resttemplate.sentinel.enabled", havingValue = "true", matchIfMissing = true)
public class SentinelProtectInterceptor implements ClientHttpRequestInterceptor {
@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
throws IOException {
URI uri = request.getURI();
String hostResource = request.getMethod().toString() + ":" + uri.getScheme()
+ "://" + uri.getHost()
+ (uri.getPort() == -1 ? "" : ":" + uri.getPort());
String hostWithPathResource = hostResource + uri.getPath();
boolean entryWithPath = true;
if (hostResource.equals(hostWithPathResource)) {
entryWithPath = false;
}
hostWithPathResource = SentinelRestTemplateUtil.UrlCleanerUtil.urlCleaner(hostWithPathResource);
Entry hostEntry = null;
Entry hostWithPathEntry = null;
ClientHttpResponse response = null;
try {
hostEntry = SphU.entry(hostResource, EntryType.OUT);
if (entryWithPath) {
hostWithPathEntry = SphU.entry(hostWithPathResource, EntryType.OUT);
}
response = execution.execute(request, body);
if (staticRestTemplate().getErrorHandler().hasError(response)) {
Tracer.trace(
new IllegalStateException("RestTemplate ErrorHandler has error"));
}
}
catch (Throwable e) {
if (!BlockException.isBlockException(e)) {
Tracer.trace(e);
if (entryWithPath) {
Tracer.traceEntry(e, hostWithPathEntry);
}
Tracer.traceEntry(e, hostEntry);
throw (IOException) e;
}
else {
return handleBlockException(request, body, execution, (BlockException) e);
}
}
finally {
if (hostWithPathEntry != null) {
hostWithPathEntry.exit();
}
if (hostEntry != null) {
hostEntry.exit();
}
}
return response;
}
}
3、 注入这个拦截器备用
@Autowired(required = false)
private SentinelProtectInterceptor sentinelProtectInterceptor;
4、RestTemplate 实例化的时候增加自定义Sentinel拦截器的实现。
@Bean
// @SentinelRestTemplate(fallback = "fallback", fallbackClass = SentinelRestTemplateUtil.FallbackUtil.class, blockHandler = "blockHandler", blockHandlerClass = SentinelRestTemplateUtil.BlockHandlerUtil.class, urlCleaner = "urlCleaner", urlCleanerClass = SentinelRestTemplateUtil.UrlCleanerUtil.class)
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
// 其他代码省略.....................
// 这里增加拦截器
if (sentinelProtectInterceptor != null) {
restTemplate.getInterceptors().add(sentinelProtectInterceptor);
}
return restTemplate;
}
5、至于 fallback,blockhandler,urlCleaner的代码,自己处理即可
@Slf4j
public class SentinelRestTemplateUtil {
public static class FallbackUtil {
/* handle degrade isDegradeFailure 降级 */
// com.alibaba.cloud.sentinel.custom.SentinelProtectInterceptor.handleBlockException(HttpRequest, byte[], ClientHttpRequestExecution, BlockException)
public static SentinelClientHttpResponse fallback(HttpRequest request,
byte[] body, ClientHttpRequestExecution execution, BlockException ex) {
log.error("RestTemplate call url={}, 已触发降级",request.getURI(), ex);
return new SentinelClientHttpResponse("{\"msg\":\"restTemplate request fallback success\"}");
}
}
public static class BlockHandlerUtil {
/* handle flow 限流 */
// com.alibaba.cloud.sentinel.custom.SentinelProtectInterceptor.handleBlockException(HttpRequest, byte[], ClientHttpRequestExecution, BlockException)
public static SentinelClientHttpResponse blockHandler(HttpRequest request,
byte[] body, ClientHttpRequestExecution execution, BlockException ex) {
log.error("RestTemplate call url=:{},已触发限流",request.getURI(), ex);
return new SentinelClientHttpResponse("{\"msg\":\"restTemplate request blockHandler success\"}");
}
}
public static class UrlCleanerUtil {
/* Url清洗 hostWithPathResource */
// com.alibaba.cloud.sentinel.custom.SentinelProtectInterceptor.intercept(HttpRequest, byte[], ClientHttpRequestExecution)
public static String urlCleaner(String url) {
return url;
}
}
}
通过以上的配置便可以临时解决掉Sentinel遇到异常返回null的Response而不抛出异常原因的问题。待官方版本解决发布后再考虑还原回去继续使用@SentinelRestTemplate注解即可。