在openfeign客户端如何获取到服务端抛出的准确异常信息??
- 相关参考
- 背景引入
- 浏览器直接访问Spring的Restful接口(最普遍、简单的访问)
- 示例
- 结论
- openfeign客户端调用的情况
- 调用过程
- 示例场景之一(其他场景可类比)
- 结论1: 服务器端返回的异常信息,在openfeign客户端直接通过通过try。。catch。。。是获取不到的,需要通过响应信息reposne来获取服务器端返回的信息!
- 如何实现openfeign的response的拦截
- openfeign拦截到response(含服务器端返回的信息),如何将服务器端返回的信息返回给调用方法的呢?
- 测试
- 特别注意
相关参考
-
openfeign客户端A调用服务B,服务B抛出异常时,客户端A接收的几种情况
-
openfeign集成sentinel实现服务降级
-
OpenFeign客户端调用,服务端查询结果为null并返回给feign客户端,引发客户端报错
-
openfeign客户端调用远程服务端接口,传递参数为null及服务端接口返回值为null的情况
背景引入
浏览器直接访问Spring的Restful接口(最普遍、简单的访问)
示例
直接在controller层抛出一个异常:观察浏览器接收到的准确信息是什么 ?
Controller代码如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class ClassForTest {Logger logger = LoggerFactory.getLogger(this.getClass());@GetMapping("/ex/handler")public String testExceptionHandler() throws Exception {throw new Exception("抛出了异常哈。。。");
// int x = 3/0;
// return "nothing";}
}
浏览器返回:
上图的返回信息是SpringMVC默认处理方式,如果服务端抛出了异常默认就会返回上述信息!
那么,如何获取到自己想要的或者服务端返回的真实异常信息!!
编写自定义异常处理即可获取到想要的真实异常信息,如下:
import javax.servlet.http.HttpServletResponse;import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;@ControllerAdvice
public class CustomExceptionHandler {/*** 全局异常处理* @param e* @param response* @return*/@ExceptionHandler(value =Exception.class)@ResponseBodypublic TestEntity myExHandler(Exception e,HttpServletResponse response) {TestEntity te = new TestEntity();te.setName("sf solo!");te.setAge(18);te.setErrorCode("500");te.setErrorMsg(e.getMessage());return te;}
}
上述代码中TestEntity为自定义返回实体
import lombok.Data;
@Data
public class TestEntity {Integer age = 10;String name;String errorMsg;String errorCode;
}
关于上述自定义异常处理类中的@ControllerAdvice注解和@ExceptionHandler注解的说明:
- @ControllerAdvice
带此注解的类是一个含有 @ExceptionHandler, @InitBinder, or @ModelAttribute注解方法的组件类,这个组件类可以贯穿于多个Controller类之间!
- @ExceptionHandler
此注解用于处理异常(在处理类/处理方法中)。
带有此注解的处理方法有非常复杂的签名。
参数和返回可以有如下类型:
测试!是否能够获取到真实信息?
将抛出异常的地方改一下:
@GetMapping("/ex/handler")public String testExceptionHandler() throws Exception {
// throw new Exception("抛出了异常哈。。。");int x = 3/0;return "nothing";}
测试:
结论
浏览器访问服务端,通过自定义异常处理可以获取到服务端准确的异常信息!
openfeign客户端调用的情况
调用过程
- 用户端(浏览器/Postman/App)—>服务A(openfeign客户端)—>服务B(服务端)
- 其他更复杂的调用链,省。。。
示例场景之一(其他场景可类比)
- openfeign客户端调用代码
1.返回的类型为自定类型。
2.对调用进行异常捕获。
try {ResponseInfo responseInfo = checklistServiceFeignClient.uploadReportWithCaSignature(endoscopicreport);} catch (Exception e) {logger.error("发生异常(插入报告信息失败) " , e);// 设置操作记录相关信息responseInfo = new ResponseInfo();if (e instanceof CustomException) {responseInfo.setRespCode(ResponseInfo.ERROR_CODE);responseInfo.setRespMsg(e.getMessage());} else {responseInfo.setRespCode(ResponseInfo.ERROR_CODE);responseInfo.setRespMsg("服务器异常");}}
- 服务端代码: 下面主要贴出实现类的代码(抛出异常的地方)
1.声明抛出异常
2.抛出异常
3.其他业务代码省略
@Overridepublic ResponseInfo uploadReportWithCaSignature(Endoscopicreport endoscopicreport) throws Exception {if (1==1)throw new CustomException("后台业务异常。");
// 其他,省略。。。。。。}
- 测试
- 因为openfeign调用是内部调用,不像浏览器可以直接看到返回结果,所以需要通过debug模式在代码中查看返回信息或者直接打印信息!
- 那么,在代码哪个地方进行debug调试查看服务器端返回的信息呢?
直接在openfeign客户端调用的地方是看不到服务器端返回的真实信息的!客户端调用处的调试信息如下:
上图中的异常信息是:openfeign客户端接收到服务器端的返回信息后将其转换成 返回类型(这里是ResponseInfo类型)时抛出的异常(HttpMessageConverter转换异常),而并非是服务器端返回的信息。
这就需要在openfeign客户端收到响应response之后(在返回成ResponseInfo类型之前)进行debug。
也就是要写一个response的拦截器(以便查看response中的具体内容—服务器端返回的信息)。
- openfeign客户端的reposne拦截器(因为示例整体是使用okhhtp来实现的,所以一下response拦截器涉及okhttp)
import java.io.IOException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;import javax.servlet.http.HttpServletRequest;import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import com.alibaba.cloud.commons.lang.StringUtils;
import com.github.pagehelper.PageInfo;import lombok.extern.slf4j.Slf4j;
import okhttp3.Interceptor;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;@Configuration
public class FeignOkHttpClientConfig {// private static int MAX_RETRY = 10;@Beanpublic OkHttpClient.Builder okHttpClientBuilder() {return new OkHttpClient.Builder().addInterceptor(new FeignOkHttpClientResponseInterceptor());}public static class FeignOkHttpClientResponseInterceptor implements Interceptor {Logger logger = LoggerFactory.getLogger(this.getClass());@Overridepublic Response intercept(Chain chain) throws IOException {// int retryNum = 0;Request originalRequest = chain.request();Response response = chain.proceed(originalRequest);MediaType mediaType = response.body().contentType();String bodyContent = response.body().string();HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
// 根据业务使用requestreturn response.newBuilder().body(ResponseBody.create(mediaType, bodyContent)).build();}}
}
从上述代码中的String bodyContent = response.body().string();的`bodyContent 中可以获取到服务器端返回的信息!
所以,这才是服务器端返回的真实信息!,而非上面的HttpMessageConverter转换异常信息!
结论1: 服务器端返回的异常信息,在openfeign客户端直接通过通过try。。catch。。。是获取不到的,需要通过响应信息reposne来获取服务器端返回的信息!
例外的情况:
如果openfeign客户端调用的返回类型是String,则可以直接获取到,因为默认情况下返回的是String类型(上述中的。。。Whitelabel Error Page。。。),因为这样就不存在转换异常了!这个在https://blog.csdn.net/qq_29025955/article/details/134294967这里面有体现。
如何实现openfeign的response的拦截
参考:
如何实现对openfeign的请求request和响应response的拦截
openfeign拦截到response(含服务器端返回的信息),如何将服务器端返回的信息返回给调用方法的呢?
通过配置openfeign客户端的fallbackFactory属性,可以获取到信息(异常信息)!
FallbackFactory的配置参考
:openfeign集成sentinel实现服务降级
- openfeign客户端代码(增加openfeign的fallbackFactory)
@FeignClient(contextId = "202344171019", name = "checklist-service",fallbackFactory=ChecklistFeignFallback.class)
//@FeignClient(contextId = "202344171019", name = "checklist-service")
public interface ChecklistServiceFeignClient {
// 接口清单}
- ChecklistFeignFallback.java
import org.springframework.stereotype.Component;
import feign.hystrix.FallbackFactory;@Component
public class ChecklistFeignFallback implements FallbackFactory<ChecklistServiceFeignClient> {@Overridepublic ChecklistFeignFallbackImpl create(Throwable cause) {System.out.println("++++++++++++调用了create方法()++++++++++++++++");ChecklistFeignFallbackImpl cffi = new ChecklistFeignFallbackImpl();cffi.setThrowable((Exception)cause);return cffi;}
}
上面的类中涉及到类ChecklistFeignFallbackImpl,此类实现了ChecklistServiceFeignClient接口:
- ChecklistFeignFallbackImpl.java
public class ChecklistFeignFallbackImpl implements ChecklistServiceFeignClient {private Exception throwable;@Overridepublic ResponseInfo uploadReportWithCaSignature(Endoscopicreport endoscopicreport) throws IOException, Exception{ResponseInfo responseInfo = ResponseInfo.newInstance();responseInfo.setRespCode(ResponseInfo.ERROR_CODE);responseInfo.setRespMsg(throwable.getMessage());return responseInfo;}// 其他接口方法的实现。。。
}
所以,服务器端抛出异常后最终会走到ChecklistFeignFallbackImpl的方法里面去,可以在这里面写具体的业务实现。异常信息private Exception throwable;会通过ChecklistFeignFallback的create方法设值进去!!!,然后在这个实现类里面通过throwable.getMessage()取值出来!
测试
注意,测试前需要在服务器端,添加自定义异常处理,以避免返回SpringMvc处理后的默认的。。。Whitelabel Error Page。。。信息。
在服务器端添加自定义的异常处理
import javax.servlet.http.HttpServletResponse;import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;@ControllerAdvice
public class GlobalExceptionAdvice {@ExceptionHandler(value =Exception.class)@ResponseBodypublic String myExceptionHandler(Exception e,HttpServletResponse response) {return "++++++++++全局异常="+e.getMessage();}}
-
启动并测试
在ChecklistFeignFallback 中debug调试: 出现新问题!!!
原因分析
为什么会返回HttpMessageConverter的转换异常?
分析1:因为openfeign的reposne拦截器中获取到的信息是文本“++++++++++全局异常=后台业务异常。” ,然后openfeign将此文本尝试转换成ResponseInfo类型,这是无法转换,自然就会报错了。
这种情况,不用fallbackFactory,在调用的地方的try…catch…也能够捕获到这个转换异常(上文有提到。)兜了半天,虽然服务器端的异常信息已经到了openfeign客户端的response中,但是,仍然没有呈现到用户端(最前端)。 现在就差临门一脚了!!
分析2:虽然服务器端的异常信息到了reposne中,但是response的状态仍然是200(正常返回),我们(业务上)认为是异常(不正常的),
但是对于Http请求/响应来说,这是完全正常的,所以状态是200。 而也正是因为这个200的状态,
openfeign认为这次请求完全没有问题,于是就按照正常流程执行,将结果返回给调用接口(尝试返回成接口定义的返回类型ResponseInfo),这样就导致生成了转换异常。
解决方案
-
在服务器端的自定义异常处理中将response的状态设置为非200类,即是设值大于等于300!
-
再次测试
这次获取到了服务器端异常信息,但是多了很多不需要的信息,如下浏览器弹出显示:
-
如何去掉多余信息?
使用Feign的ErrorDecoder,抛出自定义异常(含异常信息)
注意!只有reposne的状态大于等于300的时候,才会进入下面的Exception ErrorDecoder的decode(String methodKey, Response response) 方法。
import java.io.IOException;import org.springframework.context.annotation.Configuration;import feign.Response;
import feign.Util;
import feign.codec.ErrorDecoder;@Configuration
public class CustomFeignErrorDecoder implements ErrorDecoder {@SuppressWarnings("deprecation")@Overridepublic Exception decode(String methodKey, Response response) {String errorMsg = "";if (response != null) {try {errorMsg = Util.toString(response.body().asReader());} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}return new Exception(errorMsg);}}
- 再再次测试,成功!
浏览器弹窗提示:
特别注意
- 服务端抛出的异常,会被服务端吞没掉,并不会直接将异常信息返回给openfeign客户端
被吞没掉的异常,处理后以。。。Whitelabel Error Page。。。形式返回给openfeign客户端,并且response的状态是200(成功!),所以客户端会认为此次请求完全没问题,正常执行流程。
- 所以,当需要服务器端的异常信息时,那就需要在服务器端自定义异常的处理,并返回异常(注意,这里是返回异常信息,不是抛出异常,其实本质就是设置response的body的内容),同时将reposne的状态设置为非200大类,(300及以上)。
openfeign客户端收到服务器端返回的非200信息时,通过ErrorDecoder和fallbackFactory进行处理,最终将异常信息呈现给用户。
- 以上的各个过程,都可以根据实际业务需求,灵活处理。