访问外部接口的几种方式

访问外部接口指的是:使用代码发出Http请求,访问外部资源。

  • 使用SpringBoot的RestTemplate
  • 使用SpringCloud的Feign
  • 使用Spring WebFlux的WebClient

由于本人菜鸟一个,这三种方式的联系与差别并未深究,只知道RestTemplate是同步阻塞式模型
WebClient是异步回调式模型,同步阻塞意味着程序会一直等待请求响应,可能会对运行速度有影响。
本篇博客只介绍RestTemplate,其他两个方式的学习留作以后摸索。

RestTemplate的使用

RestTemplate依赖在Web场景启动器中,所以无需导入maven依赖

  • RestTemplate的构建方式

    @Configuration
    public class RestTemplateConfig {
        /*
          	使用RestTemplateBuilder构建RestTemplate,可以定制化一些内容,拦截器,消息转换器等
          	.setConnectTimeout(),指的是http建立连接阶段,即允许的最大握手时间
          	.setReadTimeout(),指的是设置读超时时间
          	.rootUri(),指的是如果发送的请求不是以http开头,则请求地址会自动带上rootUri根路径
          	最后build()返回RestTemplate,记得好像是使用了建造者模式。
        */
        @Bean
        public RestTemplate getRestTemplate(RestTemplateBuilder builder){
            RestTemplate restTemplate = 
                builder.setConnectTimeout(Duration.ofMillis(3000))
                .setReadTimeout(Duration.ofMillis(5000))
                .rootUri("http://api.example.com/")
                .build();
            return restTemplate;
        }
    
        /*
          	第二种方式是直接使用RestTemplate对象,使用默认设置。
          	两种方式任选一种。
        */
        @Bean
        public RestTemplate getRestTemplate(){
            return new RestTemplate();
        }
    }
  • RestTemplate中的方法使用
    因为RestTemplate类是为调用REST服务而设计的,所以它的主要方法与REST的请求方式紧密相连
    下图是rest请求方式对应的RestTemplate中方法

    这次主要讲GetPost请求方式对应的RestTemplate中的方法,再加上exchange方法
    Get请求
      1)getForEntity方法,三个重载方法,之后的每个方法都有三个重载方法
    	 /*
    	 	参数1:String类型或URI类型的请求地址
            参数2:指定返回的实体类型,class对象
            参数3:uri参数,可以是变长数组或map
         	返回值:ResponseEntity<T>是Spring对HTTP响应的封装,包括了几个重要的元素,
    			如响应码、contentType、contentLength、response header信息,response body信息等
    	 */
         public <T> ResponseEntity<T> getForEntity
    		(String url, Class<T> responseType, Object... uriVariables)
         public <T> ResponseEntity<T> getForEntity
    		(String url, Class<T> responseType, Map<String, ?> uriVariables)
         public <T> ResponseEntity<T> getForEntity(URI url, Class<T> responseType)
         案例:
        	ResponseEntity<Book> responseEntity = restTemplate.getForEntity
    			("http://127.0.0.1:8080/getbook?bookname={1}", Book.class, "java");
    		//响应体转换为Book类型
            Book book = responseEntity.getBody(); 
    		//获取响应状态码
            int statusCodeValue = responseEntity.getStatusCodeValue();  
    		//获取响应头信息
            HttpHeaders headers = responseEntity.getHeaders();  
        
      2)getForObject方法
         /*
         	参数1:String类型或URI类型的请求地址
            参数2:指定返回的实体类型,class对象
            参数3:uri参数,可以是变长数组或map
            返回值:responseType指定的Object类型
         */
         public <T> T getForObject
    		(String url, Class<T> responseType, Object... uriVariables)
         public <T> T getForObject
    		(String url, Class<T> responseType, Map<String, ?> uriVariables)
         public <T> T getForObject(URI url, Class<T> responseType)
    	 案例:
            Map<String, String> map = new HashMap<>();
        	map.put("bookname", "大主宰");
         	Book book = restTemplate.getForObject("http://127.0.0.1:8080/getbook?bookname={bookname}", Book.class, map);
        
    Post请求
      1)postForEntity方法
         /*
         	参数1:同getForEntity方法
            参数2:请求body,可以是HttpEntity类型(可设置request header),或其它Object类型
            参数3:同getForEntity方法
            参数4:同getForEntity方法
            返回值:同getForEntity方法
         */
         public <T> ResponseEntity<T> postForEntity
    		(String url, Object request, Class<T> responseType, Object... uriVariables)
         public <T> ResponseEntity<T> postForEntity
    		(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)
         public <T> ResponseEntity<T> postForEntity(URI url, Object request, Class<T> responseType)
         案例:
        	//参数是Book类型,返回值是ResponseEntity<Book>类型
    		ResponseEntity<Book> responseEntity = restTemplate.postForEntity
    			("http://127.0.0.1:8080/updateBook", book, Book.class);
            Book book = responseEntity.getBody();  
            int statusCodeValue = responseEntity.getStatusCodeValue();  
            HttpHeaders headers = responseEntity.getHeaders(); 
    	 案例2//post方法获取List数据
            List<ProductTypeVO> voList = Lists.newArrayList();
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
            headers.add("key1","value1");
            headers.add("key2","value2");
            HttpEntity<String> entity = new HttpEntity<>("", headers);
            ProductTypeVO[] responseEntity = restTemplate.postForObject
            	(url,entity,ProductTypeVO[].class);
            if (null != responseEntity) {
                voList = Arrays.asList(responseEntity);
            }
        
      2)postForObject方法
         /*
         	参数1:同getForObject方法
         	参数2:请求body,可以是HttpEntity类型(可设置request header),或其它Object类型
         	参数3:同getForObject方法
            参数4:同getForObject方法
            返回值:同getForObject方法
         */
         public <T> T postForObject
    		(String url, Object request, Class<T> responseType, Object... uriVariables)
         public <T> T postForObject
    		(String url, Object request, Class<T> responseType, Map<String, ?> uriVariables)
         public <T> T postForObject(URI url, Object request, Class<T> responseType)
    	 案例:
          	//参数是Book类型,返回值也是Book类型
    		Book book = restTemplate.postForObject("http://127.0.0.1:8080/updatebook", book, Book.class);
        
    Exchange方法
      public <T> ResponseEntity<T> exchange(String url, HttpMethod method, 
    	@Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType, Object... uriVariables)
      public <T> ResponseEntity<T> exchange(String url, HttpMethod method, 
    	@Nullable HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType, Map<String, ?> uriVariables)
      1)可以支持多种Http方法,只需要在参数中指定即可
      2)可以在请求中增加header和body信息,返回类型是ResponseEntity,
    	可以从中获取响应的状态码,header和body等信息  
      案例:
        HttpHeaders requestHeaders = new HttpHeaders();
        requestHeaders.set("MyRequestHeader", "MyValue");
    	//处理可以设置请求头,还可以设置请求体body,HttpEntity中类型为MultiValueMap<String, String>
        HttpEntity requestEntity = new HttpEntity(requestHeaders);
        HttpEntity<String> response = restTemplate.exchange(
           "http://example.com/hotels/{hotel}",
            HttpMethod.GET,     
            requestEntity, 
            String.class, "42");
        //响应头信息
        String responseHeader = response.getHeaders().getFirst("MyResponseHeader");   
        //获取响应体数据
    	String body = response.getBody();
  • Execute方法说明
    这个方法要单独拿出来讲,我们可以看restTemplate类发现,get,post,exchange等方法内部都会调用
    execute方法,而在该方法内部仅仅将String类型的URI转为java.net.URI类型后就调用了doExecute方法
    所以doExecute方法才是关键所在,下图是发出http请求的大概过程。

    然后在该方法中需要关注RequestCallback 和 ResponseExtractor两个类,这两个类作用有点复杂,
    我也不太懂就不瞎写了,以后有需要再去了解。

  • ClientHttpRequestFactory接口
    它是个函数式接口,用于根据URIHttpMethod创建出一个ClientHttpRequest来发送请求ClientHttpRequest它代表请求的客户端,该接口继承自HttpRequest、HttpOutputMessage,只有一个public ClientHttpResponse execute() throws IOException方法。
    其中Netty、HttpComponents、OkHttp3,HttpUrlConnection对它都有实现
    只要调用该接口实现类的execute()就能发送rest请求了。
    默认情况下,我们使用的是SimpleClientHttpRequestFactory,使用的是JDK内置的java.net.URLConnection作为client客户端。
    这部分对我来说还是太晦涩,暂时先不看,知道有这个接口就行,如果想指定某个该接口的实现类
    可以使用相应的构造函数创建RestTemplate,或者通过RestTemplateBuilder设置指定的工厂。

  • 配置RestTemplate的自定义拦截器

    • 注册拦截器

      @Bean
      public RestTemplate restTemplate(RestTemplateBuilder builder) {
          return builder
                  .additionalInterceptors(new RequestResponseLoggingInterceptor())
                  .build();
      }
    • 创建自定义拦截器,需要实现ClientHttpRequestInterceptor接口

      public class RequestResponseLoggingInterceptor implements ClientHttpRequestInterceptor {
          
          private final Logger log = LoggerFactory.getLogger(this.getClass());
        
          @Override
          public ClientHttpResponse intercept(HttpRequest request, byte[] body, 
      		ClientHttpRequestExecution execution) throws IOException{
              logRequest(request, body);
              ClientHttpResponse response = execution.execute(request, body);
              logResponse(response);
              //Add optional additional headers
              response.getHeaders().add("headerName", "VALUE");
              return response;
          }
        
          private void logRequest(HttpRequest request, byte[] body) throws IOException{
              if (log.isDebugEnabled())
              {
                  log.debug("=========request begin=========");
                  log.debug("URI         : {}", request.getURI());
                  log.debug("Method      : {}", request.getMethod());
                  log.debug("Headers     : {}", request.getHeaders());
                  log.debug("Request body: {}", new String(body, "UTF-8"));
                  log.debug("=========request end=========");
              }
          }
        
          private void logResponse(ClientHttpResponse response) throws IOException{
              if (log.isDebugEnabled())
              {
                  log.debug("=========response begin=========");
                  log.debug("Status code  : {}", response.getStatusCode());
                  log.debug("Status text  : {}", response.getStatusText());
                  log.debug("Headers      : {}", response.getHeaders());
                  log.debug("Response body: {}", StreamUtils.copyToString
      				(response.getBody(), Charset.defaultCharset()));
                  log.debug("=========response end=========");
              }
          }
      }
  • 参考链接

2023-3-20更新

一个使用案例

@RequestMapping("/testUrl")
	public Result<Object> testUrl(@RequestParam("id") String id){
		Result<Object> res;
		EamsBusinessDataInterface dataInterface = eamsBusinessDataInterfaceService.getById(id);
		if(ObjectUtil.isNotEmpty(dataInterface)){
			HttpMethod method = HttpMethod.resolve(dataInterface.getConnectionType());
			if (ObjectUtil.isNotEmpty(method)){
				HttpHeaders headers = new HttpHeaders();
				ResponseEntity<Result> responseEntity = null;
				String param = dataInterface.getParams().replaceAll("\n", "");
				//服务端响应的内容格式
				headers.setContentType(MediaType.APPLICATION_JSON);
				ArrayList<MediaType> mediaTypes = new ArrayList<>();
				mediaTypes.add(MediaType.APPLICATION_JSON);
				headers.setAccept(mediaTypes);
				if(method.matches("POST")){
					HttpEntity entity = new HttpEntity<>(param, headers);
					responseEntity = eamsRestTemplate.exchange(dataInterface.getTestUrl(), method, entity, Result.class);
				}else {
					HttpEntity entity = new HttpEntity<>(headers);
					//拼接get参数
					HashMap<String,String> paramMap = JSONUtil.toBean(param, HashMap.class);
					MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
					paramMap.entrySet().forEach(i->{
						params.add(i.getKey(),i.getValue());
					});
					UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(dataInterface.getTestUrl());
					URI uri = builder.queryParams(params).build().encode().toUri();
					responseEntity = eamsRestTemplate.exchange(uri, method, entity, Result.class);
				}

				if (ObjectUtil.isNotEmpty(responseEntity)&&ObjectUtil.equals(responseEntity.getStatusCode().value(), HttpStatus.HTTP_OK)){
					//设置调用结果的返回值和状态
					Result result = responseEntity.getBody();
					if (ObjectUtil.isNotEmpty(result)){
						if (!result.isSuccess()){
							dataInterface.setReturnResult(result.getMessage());
						}else {
							dataInterface.setReturnResult(JSON.toJSONString(result.getResult()));
						}
					}else{
						dataInterface.setReturnResult("无数据返回");
					}

					if (ObjectUtil.isNotEmpty(result)&&ObjectUtil.equals(result.getCode(),HttpStatus.HTTP_OK)){
						dataInterface.setStatus(String.valueOf(HttpStatus.HTTP_OK));
						eamsBusinessDataInterfaceService.updateById(dataInterface);
						return Result.OK("测试成功");
					}
				}
			}
		}
		dataInterface.setStatus(String.valueOf(HttpStatus.HTTP_INTERNAL_ERROR));
		res = Result.error("测试失败");
		eamsBusinessDataInterfaceService.updateById(dataInterface);
		return res;
	}
注意点:
	1.如果是post请求,那么请求参数可以放在header中,如果是get请求,那么请求参数需要拼接在url中
	2.restTemplate请求在请求https时,一般需要服务器方的证书才能发起请求,
		此时要么配置服务端的证书,要么配置信任所有https请求
	3.有时候restTemplate请求接口返回的数据乱码,则需要配置restTemplate中的媒体类型的编码
	4.如果报错no suitable HttpMessageConverter found for response type
		是因为服务端返回的数据格式你客户端消息转换器处理不了
		例如服务端设置response.setContentType("text/html"),表示服务端返回一个html文件
		但是客户端的消息转换器httpMessageConverter的媒体类型处理不了这个媒体类型数据,所以报错
	5.request的contentType和response的contentType,这个值是用来描述你传给对方的数据类型
		而不是期待对方传递的数据类型,你携带json参数或者返回json数据,就写“application/json”

文章浏览