RestTemplate 和okHttp学习

RestTemplate 使用

GET请求

两类:getForEntity 和 getForObject;

getForEntity:

如果开发者需要获取响应头的话,那么就需要使用 getForEntity 来发送 HTTP 请求,此时返回的对象是一个 ResponseEntity 的实例。这个实例中包含了响应数据以及响应头。

3种请求方式:

1、restTemplate.getForEntity(url, String.class, name);

restTemplate.getForEntity(请求地址, 返回的数据类型, 占位符的可变长度参数); ​ 可以使用responseEntity.getStatusCode()得到http状态码,responseEntity.getHeaders()得到响应头,responseEntity.getBody()得到响应体。

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String sayHello(String name) {
        return "hello " + name + " !";
    }
}
​
//上面是provider,下面是consumer
​
@GetMapping("/hello")
    public String hello(String name) {
        List<ServiceInstance> list = discoveryClient.getInstances("provider");
        ServiceInstance instance = list.get(0);
        String host = instance.getHost();
        int port = instance.getPort();
        String url = "http://" + host + ":" + port + "/hello?name={1}";
        ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class, name);
        StringBuffer sb = new StringBuffer();
        HttpStatus statusCode = responseEntity.getStatusCode();
        String body = responseEntity.getBody();
        sb.append("statusCode:")
                .append(statusCode)
                .append("</br>")
                .append("body:")
                .append(body)
                .append("</br>");
        HttpHeaders headers = responseEntity.getHeaders();
        Set<String> keySet = headers.keySet();
        for (String s : keySet) {
            sb.append(s)
                    .append(":")
                    .append(headers.get(s))
                    .append("</br>");
        }
        return sb.toString();
    }
2、restTemplate.getForEntity(url, String.class, map);

restTemplate.getForEntity(请求地址, 返回的数据类型, 参数的map);

Map<String, Object> map = new HashMap<>();
String url = "http://" + host + ":" + port + "/hello?name={name}";
map.put("name", name);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url, String.class, map);
3、restTemplate.getForEntity(uri, String.class);

restTemplate.getForEntity(请求地址, 返回的数据类型);直接使用字符串拼接得到url。

String url = "http://" + host + ":" + port + "/hello?name="+ URLEncoder.encode(name,"UTF-8");
URI uri = URI.create(url);
ResponseEntity<String> responseEntity = restTemplate.getForEntity(uri, String.class);
//但需要注意的是,这种传参方式,参数如果是中文的话,需要对参数进行编码,使用 URLEncoder.encode 方法来实现。
getForObject:

getForObject 方法和 getForEntity 方法类似,getForObject 方法也有三个重载的方法,参数和 getForEntity 一样,因此这里我就不重复介绍参数了,这里主要说下 getForObject 和 getForEntity 的差异,这两个的差异主要体现在返回值的差异上, getForObject 的返回值就是服务提供者返回的数据,使用 getForObject 无法获取到响应头。

String url = "http://" + host + ":" + port + "/hello?name=" + URLEncoder.encode(name, "UTF-8");
URI uri = URI.create(url);
String s = restTemplate.getForObject(uri, String.class);

POST请求

三类:postForEntity 、 postForObject和postForLocation; 这里的方法类型虽然有三种,但是这三种方法重载的参数基本是一样的,因此这里我还是以 postForEntity 方法为例,来剖析三个重载方法的用法,最后再重点说下 postForLocation 方法。

postForEntity

在 POST 请求中,参数的传递可以是 key/value 的形式,也可以是 JSON 数据,分别来看:

1、传递key/value形式的参数
@PostMapping("/hello2")
public String sayHello2(String name) {
    return "Hello " + name + " !";
}
​
//上面是provider,下面是consumer
​
@GetMapping("/hello5")
public String hello5(String name) {
    List<ServiceInstance> list = discoveryClient.getInstances("provider");
    ServiceInstance instance = list.get(0);
    String host = instance.getHost();
    int port = instance.getPort();
    String url = "http://" + host + ":" + port + "/hello2";
    MultiValueMap map = new LinkedMultiValueMap();
    map.add("name", name);
    ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, map, String.class);
    return responseEntity.getBody();
}

在这里, postForEntity 方法第一个参数是请求地址,第二个参数 map 对象中存放着请求参数 key/value,第三个参数则是返回的数据类型。当然这里的第一个参数 url 地址也可以换成一个 Uri 对象,效果是一样的。这种方式传递的参数是以 key/value 形式传递的,在 post 请求中,也可以按照 get 请求的方式去传递 key/value 形式的参数,传递方式和 get 请求的传参方式基本一致,例如下面这样:此时第二个参数可以直接传一个 null。

@GetMapping("/hello6")
public String hello6(String name) {
    List<ServiceInstance> list = discoveryClient.getInstances("provider");
    ServiceInstance instance = list.get(0);
    String host = instance.getHost();
    int port = instance.getPort();
    String url = "http://" + host + ":" + port + "/hello2?name={1}";
    ResponseEntity<String> responseEntity = restTemplate.postForEntity(url, null, String.class,name);
    return responseEntity.getBody();
}
2、传递json数据

post 请求也可以直接传递 json 数据,在 post 请求中,可以自动将一个对象转换成 json 进行传输,数据到达 provider 之后,再被转换为一个对象。具体操作步骤如下: ​ 首先在 RestTemplate 项目中创建一个新的maven项目,叫做 commons ,然后在 commons 中创建一个 User 对象,如下:

public class User {
    private String username;
    private String address;
    //省略getter/setter
}

然后分别在 provider 和 consumer 的 pom.xml 文件中添加对 commons 模块的依赖,如下:

<dependency>
    <groupId>com.justdojava</groupId>
    <artifactId>commons</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

这样,在 provider 和 consumer 中就都能使用 User 对象了。首先在 provider 中创建一个添加用户的接口,如下:

@Controller
@ResponseBody
public class UserController {
    @PostMapping("/user")
    public User hello(@RequestBody User user) {
        return user;
    }
}

这里的接口很简单,只需要将用户传来的 User 对象再原封不动地返回去就行了,然后在 consumer 中添加一个接口来测试这个接口,如下:

@GetMapping("/hello7")
public User hello7() {
    List<ServiceInstance> list = discoveryClient.getInstances("provider");
    ServiceInstance instance = list.get(0);
    String host = instance.getHost();
    int port = instance.getPort();
    String url = "http://" + host + ":" + port + "/user";
    User u1 = new User();
    u1.setUsername("sun");
    u1.setAddress("cd");
    ResponseEntity<User> responseEntity = restTemplate.postForEntity(url, u1, User.class);
    return responseEntity.getBody();
}

看到这段代码有人要问了,这不和前面的一样吗?是的,唯一的区别就是第二个参数的类型不同,这个参数如果是一个 MultiValueMap 的实例,则以 key/value 的形式发送,如果是一个普通对象,则会被转成 json 发送。

postForObject

postForObject 和 postForEntity 基本一致,就是返回类型不同而已,这里不再赘述。

postForLocation

postForLocation 方法的返回值是一个 Uri 对象,因为 POST 请求一般用来添加数据,有的时候需要将刚刚添加成功的数据的 URL 返回来,此时就可以使用这个方法,一个常见的使用场景如用户注册功能,用户注册成功之后,可能就自动跳转到登录页面了,此时就可以使用该方法。例如在 provider 中提供一个用户注册接口,再提供一个用户登录接口,如下:

@RequestMapping("/register")
public String register(User user) throws UnsupportedEncodingException {
    return "redirect:/loginPage?username=" + URLEncoder.encode(user.getUsername(),"UTF-8") + "&address=" + URLEncoder.encode(user.getAddress(),"UTF-8");
}
@GetMapping("/loginPage")
@ResponseBody
public String loginPage(User user) {
    return "loginPage:" + user.getUsername() + ":" + user.getAddress();
}

这里一个注册接口,一个是登录页面,不过这里的登录页面我就简单用一个字符串代替了。然后在 consumer 中来调用注册接口,如下:

@GetMapping("/hello8")
public String hello8() {
    List<ServiceInstance> list = discoveryClient.getInstances("provider");
    ServiceInstance instance = list.get(0);
    String host = instance.getHost();
    int port = instance.getPort();
    String url = "http://" + host + ":" + port + "/register";
    MultiValueMap map = new LinkedMultiValueMap();
    map.add("username", "牧码小子");
    map.add("address", "深圳");
    URI uri = restTemplate.postForLocation(url, map);
    String s = restTemplate.getForObject(uri, String.class);
    return s;
}

这里首先调用 postForLocation 获取 Uri 地址,然后再利用 getForObject 请求 Uri。注意:postForLocation 方法返回的 Uri 实际上是指响应头的 Location 字段,所以,provider 中 register 接口的响应头必须要有 Location 字段(即请求的接口实际上是一个重定向的接口),否则 postForLocation 方法的返回值为null

关于URL和URI:URI是:协议+主机(不含端口)+具体路径(不一定含文件)URL是:协议+主机(默认是80,或是别的端口)+具体文件路径(还必须是对应到路径下面的具体文件名称)。 ​ 所以URI是粗框架,URL是详细定位。比如URI:http://www.123.com/123/,该目录下可能有index.html和index.htm(两个文件)。。。比如URL:http://www.123.com/123/index.html(唯一的文件)

OKHttp

首先需要添加依赖(实际版本可以按照需求选择)

< dependency >
  < groupId >com.squareup.okhttp</ groupId >
  < artifactId >okhttp</ artifactId >
  < version >2.5.0</ version >
</ dependency >

http连接知识

虽然在使用 OkHttp 发送 HTTP 请求时只需要提供 URL 即可,OkHttp 在实现中需要综合考虑 3 种不同的要素来确定与 HTTP 服务器之间实际建立的 HTTP 连接。这样做的目的是为了达到最佳的性能。

首先第一个考虑的要素是 URL 本身。URL 给出了要访问的资源的路径。比如 URL http://www.baidu.com 所对应的是百度首页的 HTTP 文档。在 URL 中比较重要的部分是访问时使用的模式,即 HTTP 还是 HTTPS。这会确定 OkHttp 所建立的是明文的 HTTP 连接,还是加密的 HTTPS 连接。

第二个要素是 HTTP 服务器的地址,如 baidu.com。每个地址都有对应的配置,包括端口号,HTTPS 连接设置和网络传输协议。同一个地址上的 URL 可以共享同一个底层 TCP 套接字连接。通过共享连接可以有显著的性能提升。OkHttp 提供了一个连接池来复用连接。

第三个要素是连接 HTTP 服务器时使用的路由。路由包括具体连接的 IP 地址(通过 DNS 查询来发现)和所使用的代理服务器。对于 HTTPS 连接还包括通讯协商时使用的 TLS 版本。对于同一个地址,可能有多个不同的路由。OkHttp 在遇到访问错误时会自动尝试备选路由。

当通过 OkHttp 来请求某个 URL 时,OkHttp 首先从 URL 中得到地址信息,再从连接池中根据地址来获取连接。如果在连接池中没有找到连接,则选择一个路由来尝试连接。尝试连接需要通过 DNS 查询来得到服务器的 IP 地址,也会用到代理服务器和 TLS 版本等信息。当实际的连接建立之后,OkHttp 发送 HTTP 请求并获取响应。当连接出现问题时,OkHttp 会自动选择另外的路由进行尝试。这使得 OkHttp 可以自动处理可能出现的网络问题。当成功获取到 HTTP 请求的响应之后,当前的连接会被放回到连接池中,提供给后续的请求来复用。连接池会定期把闲置的连接关闭以释放资源。

请求、响应和调用知识

HTTP 客户端所要执行的任务很简单,接受 HTTP 请求并返回响应。每个 HTTP 请求包括 URL,HTTP 方法(如 GET 或 POST),HTTP 头和请求的主体内容等。HTTP 请求的响应则包含状态代码(如 200 或 500),HTTP 头和响应的主体内容等。虽然请求和响应的交互模式很简单,但在实现中仍然有很多细节要考虑。OkHttp 会对收到的请求进行一定的处理,比如增加额外的 HTTP 头。同样的,OkHttp 也可能在返回响应之前对响应做一些处理。例如,OkHttp 可以启用 GZIP 支持。在发送实际的请求时,OkHttp 会加上 HTTP 头 Accept-Encoding。在接收到服务器的响应之后,OkHttp 会先做解压缩处理,再把结果返回。如果 HTTP 响应的状态代码是重定向相关的,OkHttp 会自动重定向到指定的 URL 来进一步处理。OkHttp 也会处理用户认证相关的响应。

OkHttp 使用调用(Call)来对发送 HTTP 请求和获取响应的过程进行抽象。代码清单 2 中给出了使用 OkHttp 发送 HTTP 请求的基本示例。首先创建一个 OkHttpClient 类的对象,该对象是使用 OkHttp 的入口。接着要创建的是表示 HTTP 请求的 Request 对象。通过 Request.Builder 这个构建帮助类可以快速的创建出 Request 对象。这里指定了 Request 的 url 为 http://www.baidu.com。接着通过 OkHttpClient 的 newCall 方法来从 Request 对象中创建一个 Call 对象,再调用 execute 方法来执行该调用,所得到的结果是表示 HTTP 响应的 Response 对象。通过 Response 对象中的不同方法可以访问响应的不同内容。如 headers 方法来获取 HTTP 头,body 方法来获取到表示响应主体内容的 ResponseBody 对象。

//清单2、OKHttp最基本的http请求
public  class  SyncGet {
    public  static  void  main(String[] args)  throws  IOException {
     OkHttpClient client =  new  OkHttpClient();
 
     Request request =  new  Request.Builder()
             .url( "http://www.baidu.com" )
             .build();
 
     Response response = client.newCall(request).execute();
     if  (!response.isSuccessful()) {
         throw  new  IOException( "服务器端错误: "  + response);
     }
 
     Headers responseHeaders = response.headers();
     for  ( int  i =  0 ; i < responseHeaders.size(); i++) {
         System.out.println(responseHeaders.name(i) +  ": "  + responseHeaders.value(i));
     }
 
     System.out.println(response.body().string());
    }
}

HTTP头处理

HTTP 头是 HTTP 请求和响应中的重要组成部分。在创建 HTTP 请求时需要设置一些 HTTP 头。在得到 HTTP 的响应之后,也会需要对其中包含的 HTTP 头进行解析。从代码的角度来说,HTTP 头的数据结构是 Map<String, List<String>>类型。也就是说,对于每个 HTTP 头,可能有多个值。但是大部分 HTTP 头都只有一个值,只有少部分 HTTP 头允许多个值。OkHttp 采用了简单的方式来区分这两种类型,使得对 HTTP 头的使用更加简单。

在设置 HTTP 头时,使用 header(name, value) 方法来设置 HTTP 头的唯一值。对同一个 HTTP 头,多次调用该方法会覆盖之前设置的值。使用 addHeader(name, value) 方法来为 HTTP 头添加新的值。在读取 HTTP 头时,使用 header(name) 方法来读取 HTTP 头的最近出现的值。如果该 HTTP 头只有单个值,则返回该值;如果有多个值,则返回最后一个值。使用 headers(name) 方法来读取 HTTP 头的所有值。

代码清单 3 中使用 header 方法设置了 User-Agent 头的值,并添加了一个 Accept 头的值。在进行解析时,通过 header 方法来获取 Server 头的单个值,通过 headers 方法来获取 Set-Cookie 头的所有值。

//HTTP头设置和读取的示例
public  class  Headers {
    public  static  void  main(String[] args)  throws  IOException {
     OkHttpClient client =  new  OkHttpClient();
 
     Request request =  new  Request.Builder()
             .url( "http://www.baidu.com" )
             .header( "User-Agent" ,  "My super agent" )
             .addHeader( "Accept" ,  "text/html" )
             .build();
 
     Response response = client.newCall(request).execute();
     if  (!response.isSuccessful()) {
         throw  new  IOException( "服务器端错误: "  + response);
     }
 
     System.out.println(response.header( "Server" ));
     System.out.println(response.headers( "Set-Cookie" ));
    }
}

POST请求

HTTP POST 和 PUT 请求可以包含要提交的内容。只需要在创建 Request 对象时,通过 post 和 put 方法来指定要提交的内容即可。代码清单 4中通过 RequestBody 的 create 方法来创建媒体类型为 text/plain 的内容并提交。

//HTTP POST 请求的基本示例
public  class  PostString {
    public  static  void  main(String[] args)  throws  IOException {
     OkHttpClient client =  new  OkHttpClient();
     MediaType MEDIA_TYPE_TEXT = MediaType.parse( "text/plain" );
     String postBody =  "Hello World" ;
 
     Request request =  new  Request.Builder()
             .url( "http://www.baidu.com" )
             .post(RequestBody.create(MEDIA_TYPE_TEXT, postBody))
             .build();
 
     Response response = client.newCall(request).execute();
     if  (!response.isSuccessful()) {
         throw  new  IOException( "服务器端错误: "  + response);
     }
 
     System.out.println(response.body().string());
    }
}

以 String 类型提交内容只适用于内容比较小的情况。当请求内容较大时,应该使用流来提交。代码清单 5 中给了使用流方式来提交内容的示例。这里创建了 RequestBody 的一个匿名子类。该子类的 contentType 方法需要返回内容的媒体类型,而 writeTo 方法的参数是一个 BufferedSink 对象。我们需要做的就是把请求的内容写入到 BufferedSink 对象即可。

//使用流方法提交 HTTP POST 请求的示例
public  class  PostStream {
    public  static  void  main(String[] args)  throws  IOException {
     OkHttpClient client =  new  OkHttpClient();
     final  MediaType MEDIA_TYPE_TEXT = MediaType.parse( "text/plain" );
     final  String postBody =  "Hello World" ;
 
     RequestBody requestBody =  new  RequestBody() {
         @Override
         public  MediaType contentType() {
             return  MEDIA_TYPE_TEXT;
         }
 
         @Override
         public  void  writeTo(BufferedSink sink)  throws  IOException {
             sink.writeUtf8(postBody);
         }
 
  @Override
         public  long  contentLength()  throws  IOException {
             return  postBody.length();
         }
     };
 
     Request request =  new  Request.Builder()
             .url( "http://www.baidu.com" )
             .post(requestBody)
             .build();
 
     Response response = client.newCall(request).execute();
     if  (!response.isSuccessful()) {
         throw  new  IOException( "服务器端错误: "  + response);
     }
 
     System.out.println(response.body().string());
    }
}

如果所要提交的内容来自本地文件,则不需要额外的流操作,只需要通过 RequestBody 的 create 方法,并把 File 类型的对象作为参数传入即可。

如果需要模拟 HTML 中的表单提交,可以通过 FormEncodingBuilder 来创建请求内容,见代码清单 6

//表单提交示例
RequestBody formBody =  new  FormEncodingBuilder()
             .add( "query" ,  "Hello" )
             .build();

如果需要模拟 HTML 中的文件上传功能,可以通过 MultipartBuilder 来创建 multipart 请求内容。代码清单 7 中的 multipart 请求添加了一个表单属性 title 和一个文件 file。

//提交 multipart 请求的示例
MediaType MEDIA_TYPE_TEXT = MediaType.parse( "text/plain" );
RequestBody requestBody =  new  MultipartBuilder()
     .type(MultipartBuilder.FORM)
     .addPart(
             Headers.of( "Content-Disposition" ,  "form-data; name=\"title\"" ),
             RequestBody.create( null ,  "测试文档" ))
     .addPart(
             Headers.of( "Content-Disposition" ,  "form-data; name=\"file\"" ),
             RequestBody.create(MEDIA_TYPE_TEXT,  new  File( "input.txt" )))
     .build();

okhttp部分后续内容参考博客:https://blog.csdn.net/tanga842428/article/details/78713089