在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程

在openfeign客户端如何获取到服务端抛出的准确异常信息??

  • 相关参考
  • 背景引入
    • 浏览器直接访问Spring的Restful接口(最普遍、简单的访问)
      • 示例
      • 结论
  • openfeign客户端调用的情况
    • 调用过程
    • 示例场景之一(其他场景可类比)
      • 结论1: 服务器端返回的异常信息,在openfeign客户端直接通过通过try。。catch。。。是获取不到的,需要通过响应信息reposne来获取服务器端返回的信息!
    • 如何实现openfeign的response的拦截
    • openfeign拦截到response(含服务器端返回的信息),如何将服务器端返回的信息返回给调用方法的呢?
      • 测试
  • 特别注意

相关参考

  1. openfeign客户端A调用服务B,服务B抛出异常时,客户端A接收的几种情况

  2. openfeign集成sentinel实现服务降级

  3. OpenFeign客户端调用,服务端查询结果为null并返回给feign客户端,引发客户端报错

  4. 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";
	}
}

浏览器返回:
在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第1张图片
上图的返回信息是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)
    @ResponseBody
	public 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类之间!
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第2张图片
  • @ExceptionHandler
    此注解用于处理异常(在处理类/处理方法中)。
    带有此注解的处理方法有非常复杂的签名。
    参数和返回可以有如下类型:
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第3张图片
    测试!是否能够获取到真实信息?

在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第4张图片
将抛出异常的地方改一下:

	@GetMapping("/ex/handler")
	public String testExceptionHandler() throws Exception {
//		throw new Exception("抛出了异常哈。。。");
		int x = 3/0;
		return "nothing";
	}

测试:
在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第5张图片

结论

浏览器访问服务端,通过自定义异常处理可以获取到服务端准确的异常信息!

openfeign客户端调用的情况

调用过程

  1. 用户端(浏览器/Postman/App)—>服务A(openfeign客户端)—>服务B(服务端)
  2. 其他更复杂的调用链,省。。。

示例场景之一(其他场景可类比)

  • 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.其他业务代码省略
	@Override
	public ResponseInfo uploadReportWithCaSignature(Endoscopicreport endoscopicreport) throws Exception {
		if (1==1)
			throw new CustomException("后台业务异常。");
// 其他,省略。。。。。。
			}
  • 测试
  1. 因为openfeign调用是内部调用,不像浏览器可以直接看到返回结果,所以需要通过debug模式在代码中查看返回信息或者直接打印信息!
  2. 那么,在代码哪个地方进行debug调试查看服务器端返回的信息呢?
    直接在openfeign客户端调用的地方是看不到服务器端返回的真实信息的!客户端调用处的调试信息如下:
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第6张图片
    上图中的异常信息是:openfeign客户端接收到服务器端的返回信息后将其转换成 返回类型(这里是ResponseInfo类型)时抛出的异常(HttpMessageConverter转换异常),而并非是服务器端返回的信息。

这就需要在openfeign客户端收到响应response之后(在返回成ResponseInfo类型之前)进行debug。
也就是要写一个response的拦截器(以便查看response中的具体内容—服务器端返回的信息)。

  1. 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;

	@Bean
	public OkHttpClient.Builder okHttpClientBuilder() {
		return new OkHttpClient.Builder().addInterceptor(new FeignOkHttpClientResponseInterceptor());
	}

	public static class FeignOkHttpClientResponseInterceptor implements Interceptor {

		Logger logger = LoggerFactory.getLogger(this.getClass());

		@Override
		public 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();
// 根据业务使用request

			return response.newBuilder().body(ResponseBody.create(mediaType, bodyContent)).build();
		}
	}
}

从上述代码中的String bodyContent = response.body().string();的`bodyContent 中可以获取到服务器端返回的信息!
在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第7张图片
所以,这才是服务器端返回的真实信息!,而非上面的HttpMessageConverter转换异常信息!

结论1: 服务器端返回的异常信息,在openfeign客户端直接通过通过try。。catch。。。是获取不到的,需要通过响应信息reposne来获取服务器端返回的信息!

例外的情况:

  1. 如果openfeign客户端调用的返回类型是String,则可以直接获取到,因为默认情况下返回的是String类型(上述中的。。。Whitelabel Error Page。。。),因为这样就不存在转换异常了!这个在https://blog.csdn.net/qq_29025955/article/details/134294967这里面有体现。

如何实现openfeign的response的拦截

参考:如何实现对openfeign的请求request和响应response的拦截

openfeign拦截到response(含服务器端返回的信息),如何将服务器端返回的信息返回给调用方法的呢?

通过配置openfeign客户端的fallbackFactory属性,可以获取到信息(异常信息)!

在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第8张图片
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> {
	@Override
	public 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;
		@Override
	public 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)
    @ResponseBody
	public String myExceptionHandler(Exception e,HttpServletResponse response) {
		return "++++++++++全局异常="+e.getMessage();
	}
	
}

  • 启动并测试
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第9张图片
    在ChecklistFeignFallback 中debug调试: 出现新问题!!!
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第10张图片
    原因分析
    为什么会返回HttpMessageConverter的转换异常?
    分析1:因为openfeign的reposne拦截器中获取到的信息是文本“++++++++++全局异常=后台业务异常。” ,然后openfeign将此文本尝试转换成ResponseInfo类型,这是无法转换,自然就会报错了。
    这种情况,不用fallbackFactory,在调用的地方的try…catch…也能够捕获到这个转换异常(上文有提到。)

    兜了半天,虽然服务器端的异常信息已经到了openfeign客户端的response中,但是,仍然没有呈现到用户端(最前端)。 现在就差临门一脚了!!

    分析2:虽然服务器端的异常信息到了reposne中,但是response的状态仍然是200(正常返回),我们(业务上)认为是异常(不正常的),但是对于Http请求/响应来说,这是完全正常的,所以状态是200。 而也正是因为这个200的状态,openfeign认为这次请求完全没有问题,于是就按照正常流程执行,将结果返回给调用接口(尝试返回成接口定义的返回类型ResponseInfo),这样就导致生成了转换异常。

解决方案

  1. 在服务器端的自定义异常处理中将response的状态设置为非200类,即是设值大于等于300!
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第11张图片

  2. 再次测试
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第12张图片
    这次获取到了服务器端异常信息,但是多了很多不需要的信息,如下浏览器弹出显示:
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第13张图片

  3. 如何去掉多余信息?

使用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")
	@Override
	public 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 block
				e.printStackTrace();
			}
		}
		return new Exception(errorMsg);
	}

}

  1. 再再次测试,成功!
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第14张图片
    浏览器弹窗提示:
    在openfeign客户端如何获取到服务端抛出的准确异常信息?? openfeign调用(请求/响应)的各个大致过程_第15张图片

特别注意

  1. 服务端抛出的异常,会被服务端吞没掉,并不会直接将异常信息返回给openfeign客户端
  2. 被吞没掉的异常,处理后以。。。Whitelabel Error Page。。。形式返回给openfeign客户端,并且response的状态是200(成功!),所以客户端会认为此次请求完全没问题,正常执行流程。
  3. 所以,当需要服务器端的异常信息时,那就需要在服务器端自定义异常的处理,并返回异常(注意,这里是返回异常信息,不是抛出异常,其实本质就是设置response的body的内容),同时将reposne的状态设置为非200大类,(300及以上)。
  4. openfeign客户端收到服务器端返回的非200信息时,通过ErrorDecoder和fallbackFactory进行处理,最终将异常信息呈现给用户。
  5. 以上的各个过程,都可以根据实际业务需求,灵活处理。

你可能感兴趣的:(openfeign调用,服务器端异常传递问题)