一、简言
RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,也有的称之为网络框架,说白了就是Java版本的一个postman。专门用于在Java当中服务与服务之间远程调用接口的时候用。
举例A服务,需要从B服务获取到数据,假如不经过前端页面只是后端A服务获取B服务的数据的话,这时候就需要通过Java的HTTP请求工具来远程调用。
类似的还有Apache 的 HttpClient 以及OKHttp3,都是Java当中常用的HTTP 请求工具。
关于HttpClient用法:https://www.jb51.net/article/256131.htm
RestTemplate优点:
- RestTemplate属于spring的,假如springboot项目的话,完全不用引入任何其他依赖,直接可以用。
- RestTemplate可以和cloud当中的ribbon进行配合使用,只需要一个注解就可以完成负载均衡调用。
官网api:https://docs.spring.io/spring-framework/docs/5.0.9.RELEASE/javadoc-api/
二、注入容器
既然是远程调用,那么我们系统肯定不会只调用一次,通常情况我们会将RestTemplate注入到容器当中,让他保持单例,当我们哪个类要使用的时候直接从容器里面获取即可。这样可以避免每调用一次创建一个RestTemplate对象。
2.1、普通配置
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Configuration public class ApplicationContextBean { @Bean public RestTemplate restTemplate() { return new RestTemplate(); } }
使用的时候直接通过容器注入即可。
@Autowired private RestTemplate restTemplate;
2.2、详细配置
对于一个请求来说,连接超时时间,请求超时时间等都是可以设置的参数,为了更好的适应业务需求,所以可以自己修改restTemplate的配置。如下利用@Bean注解的参数特性,形成了一个实例化链条。(我实际开发当中一般没有详细设置过,包括我涉及到的项目也是,一般都是直接像上面的简单配置一下就开始使用了)
@Bean注解修饰的方法,假如带有参数,其实就是代表@Bean所标注的对象的实例化依赖于参数中的类,需要先将参数中的类实例化,才能实例化@Bean所标注的对象。
import org.apache.http.Header; import org.apache.http.client.HttpClient; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.impl.client.DefaultHttpRequestRetryHandler; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.message.BasicHeader; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; import java.util.ArrayList; import java.util.List; @Configuration public class RestTemplateConfig { /** * http连接管理器 * * @return */ @Bean public HttpClientConnectionManager poolingHttpClientConnectionManager() { /* 注册http和https请求 Registryregistry = RegistryBuilder. create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", SSLConnectionSocketFactory.getSocketFactory()) .build(); PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(registry);*/ PoolingHttpClientConnectionManager poolingHttpClientConnectionManager = new PoolingHttpClientConnectionManager(); // 最大连接数 poolingHttpClientConnectionManager.setMaxTotal(500); // 同路由并发数(每个主机的并发) poolingHttpClientConnectionManager.setDefaultMaxPerRoute(100); return poolingHttpClientConnectionManager; } /** * HttpClient * * @param poolingHttpClientConnectionManager * @return */ @Bean public HttpClient httpClient(HttpClientConnectionManager poolingHttpClientConnectionManager) { HttpClientBuilder httpClientBuilder = HttpClientBuilder.create(); // 设置http连接管理器 httpClientBuilder.setConnectionManager(poolingHttpClientConnectionManager); // 设置重试次数 httpClientBuilder.setRetryHandler(new DefaultHttpRequestRetryHandler(3, true)); // 设置默认请求头(具体可根据自己业务场景进行设置) List headers = new ArrayList<>(); headers.add(new BasicHeader("atoken", "WAKJDWAJLDKWAKDLKWALKDALKW")); httpClientBuilder.setDefaultHeaders(headers); return httpClientBuilder.build(); } /** * 请求连接池配置 * * @param httpClient * @return */ @Bean public ClientHttpRequestFactory clientHttpRequestFactory(HttpClient httpClient) { HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(); // httpClient创建器 clientHttpRequestFactory.setHttpClient(httpClient); // 连接超时时间/毫秒(连接上服务器(握手成功)的时间,超出抛出connect timeout) clientHttpRequestFactory.setConnectTimeout(5 * 1000); // 数据读取超时时间(socketTimeout)/毫秒(务器返回数据(response)的时间,超过抛出read timeout) clientHttpRequestFactory.setReadTimeout(10 * 1000); // 连接池获取请求连接的超时时间,不宜过长,必须设置/毫秒(超时间未拿到可用连接,会抛出org.apache.http.conn.ConnectionPoolTimeoutException: Timeout waiting for connection from pool) clientHttpRequestFactory.setConnectionRequestTimeout(10 * 1000); return clientHttpRequestFactory; } /** * rest模板 * * @return */ @Bean public RestTemplate restTemplate(ClientHttpRequestFactory clientHttpRequestFactory) { // boot中可使用RestTemplateBuilder.build创建 RestTemplate restTemplate = new RestTemplate(); // 配置请求工厂 restTemplate.setRequestFactory(clientHttpRequestFactory); // 添加拦截器(这块可以通过拦截器来实现日志打印,既然是拦截器,那肯定会丢失一定的性能,所以可根据情况使用,如果计划在拦截器当中打印request当中的日志,一定要注意request是流,不能读两次,这块要做的话,需要想办法将流取出然后再替换一个新的流) // List interceptors = new ArrayList<>(); // interceptors.add(restTemplateLog); // restTemplate.setInterceptors(interceptors); return restTemplate; } }
三、GET请求
这里的方法一共有两类,getForEntity
和 getForObject
,每一类有三个重载方法,下面我们分别予以介绍,首先我们先了解一下传参:
- String url:请求接口地址
- URI url:使用 Uri 对象时,参数可以直接拼接在地址中
- Class
responseType:返回的response当中body的对象类型,相当于只要这块写好了,我们就不用将返回值 再进行序列化成对象了。 - Map
uriVariables:map类型的key value参数,这个key需要和地址当中的key相对应的 - Object... uriVariables:这是一个可变长参数,地址栏当中可以用1,2数字占位符当中参数,传参的时候,只要这块的可变长参数能按顺序和占位符 一 一对上即可。
3.1、getForEntity
把这个接口当做要远程调用的接口:
@GetMapping("/getTest2") public String getTest2(String name) { return "hello " + name + " !"; }
这种写法实际是就相当于是这样的:http://localhost:8006/getTest2?name=222
那么远程调用怎么调用呢?下面一共提供了三种写法!
@GetMapping("/test") public void test() throws UnsupportedEncodingException { // 第一种方案(这种方案有点类似hibernate占位符取值,getForEntity第三个参数就是一个可变长参数,当get请求有多个参数的时候也是可以用这种方式的) String url1 = "http://127.0.0.1:8006/getTest2?name={1}"; // 这块的String.class就是ResponseEntity当中的body要序列化成的java对象类型 ResponseEntityresponseEntity1 = restTemplate.getForEntity(url1, String.class, "aaa"); System.out.println(responseEntity1.getStatusCode()); System.out.println(responseEntity1.getBody()); System.out.println(responseEntity1.getHeaders()); // 第二种方案,将参数放入到一个 map 中。map 中的 key 和占位符的 key 相对应,map 中的 value 就是参数的具体值 Map map = new HashMap<>(); String url2 = "http://127.0.0.1:8006/getTest2?name={name}"; map.put("name", "bbb"); ResponseEntity responseEntity2 = restTemplate.getForEntity(url2, String.class, map); System.out.println(responseEntity2.getStatusCode()); System.out.println(responseEntity2.getBody()); System.out.println(responseEntity2.getHeaders()); // 第三种方案,使用 Uri 对象时,参数可以直接拼接在地址中 String url = "http://127.0.0.1:8006/getTest2?name=" + URLEncoder.encode("ccc", "UTF-8"); URI uri = URI.create(url); ResponseEntity responseEntity3 = restTemplate.getForEntity(uri, String.class); System.out.println(responseEntity3.getStatusCode()); System.out.println(responseEntity3.getBody()); System.out.println(responseEntity3.getHeaders()); }
RestTemplate 发送的是 HTTP 请求,那么在响应的数据中必然也有响应头,如果开发者需要获取响应头的话,那么就需要使用 getForEntity
来发送 HTTP 请求,此时返回的对象是一个 ResponseEntity
的实例。通过ResponseEntity
我们可以获取请求的响应详细信息,例如:body请求体,header请求头,status状态码
ResponseEntity对象,他实际上就是包含了例如网页访问时候的响应信息。
3.2、getForObject
getForObject
方法和 getForEntity
方法类似,getForObject
方法也有三个重载的方法,参数和 getForEntity
一样,因此这里我就不重复介绍参数了,这里主要说下 getForObject
和 getForEntity
的差异,这两个的差异主要体现在返回值的差异上, getForObject
的返回值就是服务提供者返回的数据,使用 getForObject
无法获取到响应头。
把下面接口当做要远程调用的接口:
// 没有参数的时候调用 @GetMapping("/getTest1") public String getTest1() { return "getTest1"; } // ?拼接的参数 @GetMapping("/getTest2") public String getTest2(String name) { return "hello " + name + " !"; } // 斜杠地址栏/ 拼接的参数 @GetMapping("/getTest3/{name}") public String getTest3(@PathVariable(value = "name") String name) { return "hello " + name + " !"; }
那么远程调用怎么调用呢?下面提供多个示例供参考!
@GetMapping("/test1") public void test1() throws UnsupportedEncodingException { // 无参调用 String result = restTemplate.getForObject("http://127.0.0.1:8006/getTest1", String.class); System.out.println(result); String url2 = "http://127.0.0.1:8006/getTest2?name={1}"; String result1 = restTemplate.getForObject(url2, String.class, "aaa"); System.out.println(result1); // 第一种方案 调用地址栏/拼接 String url3 = "http://127.0.0.1:8006/getTest3/{1}"; String result2 = restTemplate.getForObject(url3, String.class, "bbb"); System.out.println(result2); // 第二种方案 调用地址栏/拼接 String url4 = "http://127.0.0.1:8006/getTest3/" + URLEncoder.encode("ccc", "UTF-8"); URI uri = URI.create(url4); String result3 = restTemplate.getForObject(uri, String.class); System.out.println(result3); // 第三种方案 调用地址栏/拼接 Mapmap = new HashMap<>(); String url5 = "http://127.0.0.1:8006/getTest3/{name}"; map.put("name", "ddd"); String result4 = restTemplate.getForObject(url5, String.class, map); System.out.println(result4); }
通过以上练习后会发现一个致命问题,提供的get请求的API当中并没有header传参这一项,假如请求的别的服务接口有认证,那我们该怎么办呢?
我们可以使用RestTemplate 当中的 exchange自定义请求。
四、POST 请求
可以看到,post 请求的方法类型除了 postForEntity
和 postForObject
之外,还有一个 postForLocation
。这里的方法类型虽然有三种,但是这三种方法重载的参数基本是一样的。其余参数都和get一样,就多了一个request参数,如下:
Object request
: 该参数可以是一个普通对象, 也可以是一个HttpEntity对象。
- 如果是一个普通对象, 而非HttpEntity对象的时候, RestTemplate会将请求对象转换为一个HttpEntity对象来处理, 其中Object就是 request请求体(body)的类型, request内容会被视作完整的body来处理;
- 如果 request 是一个HttpEntity对象, 那么就会被当作一个完成的HTTP请求对象来处理, 这个 request 中不仅包含了body的内容, 也包含了header的内容。
如下源码是对request参数进行转换调用的:
什么情况下要使用HttpEntity?
假如我们远程调用的接口是有token认证的,我们请求的时候就需要在header请求头当中添加token,这时候我们就需要通过HttpEntity对象来携带请求头。
4.1、postForEntity
示例一:下面接口是JSON传参,然后还获取token了,把它当做要远程调用的接口:
@PostMapping("/postTest2") public User test2(HttpServletRequest request, @RequestBody User user1) { System.out.println("接受的参数:" + user1); // 正常情况我们是需要通过拦截器来拦截request获取请求头当中的token来进行认证,这块只是演示 System.out.println("接受的请求头:" + request.getHeader("token")); User user = new User(); user.setId(1); user.setName("张三"); user.setSex("男性"); return user; }
那么远程调用怎么调用呢?下面示例供参考!
示例使用的是如下图API,request 可以是一个普通对象,也可以是HttpEntity。
@GetMapping("/hello2") public User hello5() { String url = "http://127.0.0.1:8006/postTest2"; // 请求示例一:携带token请求头 // 请求体 User user = new User(); user.setId(1); user.setName("李四"); user.setSex("女性"); // 请求头(这里有一点需要注意,HttpHeaders和HttpEntity都是引入的spring的包) HttpHeaders headers = new HttpHeaders(); headers.add("token", "WDWADAWDWADAWSAXZCXZCXZEDF"); // 请求 HttpEntityrequst = new HttpEntity<>(user, headers); ResponseEntity responseEntity = restTemplate.postForEntity(url, requst, User.class); System.out.println("响应的结果:" + responseEntity.getBody()); // 请求示例二:不携带请求头,直接传普通对象,这里直接传的user,map也是可以的,他都可以转换成json,切记不能使用LinkedMultiValueMap,使用他不会转换成json ResponseEntity responseEntity1 = restTemplate.postForEntity(url, user, User.class); System.out.println("响应的结果:" + responseEntity1.getBody()); return responseEntity1.getBody(); }
调用结果:
示例二:下面接口是key/value传参,然后还获取token了,把它当做要远程调用的接口:
@PostMapping("/postTest1") public String test1(HttpServletRequest request, String name) { System.out.println("接受的参数:" + name); System.out.println("接受的请求头:" + request.getHeader("token")); return "Hello " + name + " !"; }
那么远程调用怎么调用呢?下面示例供参考!
下面每个示例都使用了request参数,正常情况下,假如第三方接口没有token认证,我们是不需要使用request参数的,直接传null即可。
@GetMapping("/hello1") public void hello1() throws UnsupportedEncodingException { // 请求头(这里有一点需要注意,HttpHeaders和HttpEntity都是引入的spring的包) HttpHeaders headers = new HttpHeaders(); headers.add("token", "WDWADAWDWADAWSAXZCXZCXZEDF"); HttpEntity
调用示例:
前三种方式都是携带了header,最后一种没有带。
4.2、postForObject
postForObject 和 postForEntity 基本一致,就是返回类型不同而已,这里不再赘述。
4.3、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() { Listlist = 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 实际上是指响应头的 Location 字段,所以,provider 中 register 接口的响应头必须要有 Location 字段(即请求的接口实际上是一个重定向的接口),否则 postForLocation 方法的返回值为null,初学者很容易犯这个错误。
五、PUT请求
PUT 请求和POST请求传参基本上一模一样,他也有request参数,唯一区别就是PUT请求是没有返回值的,PUT 请求本身方法也比较少,只有三个,如下:
六、DELETE请求
DELETE 请求和GET请求传参基本上一模一样,同样都没有request参数,唯一区别就是DELETE 请求是没有返回值的,DELETE 请求本身方法也比较少,只有三个,如下:
七、LinkedMultiValueMap源码分析
1、项目中在使用RestTemplate进行http请求时,会用到LinkedMultiValueMap,现在简单分析一下LinkedMultiValueMap的数据结构。
2、打开LinkedMultiValueMap的源码,可以看到里面封装的是一个Map,再看构造方法,最终创建的是一个LinkedHashMap。这个Map的value有点特别,他不能放任何值,必须是一个List。
其实LinkedMultiValueMap可以看作是一个表单,在表单中,一个name可以对应很多值,比如根据id批量删除,一个key需要对应很多需要删除的id
3、add、put、set
public static void main(String[] args) { MultiValueMap params = new LinkedMultiValueMap(); params.add("username", "张三"); params.add("username", "李四"); System.out.println(params); params.put("password", Arrays.asList("1234")); System.out.println(params); params.set("username", "王五"); System.out.println(params); }
运行结果:
4、通过源码分析
add是把值放到list中,若map中没有key对应的list,先new一个LinkedList。
put直接接收list
set不管原来怎么样,都会重新new一个LinkedList,再把值放进去
5、什么时候使用LinkedMultiValueMap,答案就是模拟普通的表单提交时。
HttpEntity的body里面使用LinkedMultiValueMap,那么就是key/value传参,如果是其他的bean类型,就会格式化成json。
八、通用方法 exchange
为什么说它通用呢?因为这个方法需要你在调用的时候去指定请求类型,即它既能做 GET 请求,也能做 POST 请求,也能做其它各种类型的请求。如果开发者需要对请求进行封装,使用它再合适不过了。这个方法重载的方法非常多,其中参数也多了很多个,如下:
HttpMethod method
:请求的方法(GET、POST、PUT等)HttpEntity> requestEntity
:HttpEntity对象,封装了请求头和请求体,在上面post的时候应该掌握的差不多了。
8.1、用法示例
使用如下API演示两个示例:
- post的key/value传参
- post的json传参
示例一:post的key/value传参
@PostMapping("/exchangeTest1") public String test1(HttpServletRequest request, String name) { System.out.println("接受的参数:" + name); System.out.println("接受的请求头:" + request.getHeader("token")); return "Hello " + name + " !"; }
调用示例:
@GetMapping("/exchange1") public void hello1() { String url = "http://127.0.0.1:8006/exchangeTest1"; // 请求体,一定要用LinkedMultiValueMap,用hashmap或者javabean会直接转换成json的 MultiValueMap map = new LinkedMultiValueMap(); map.add("name", "萨瓦迪卡"); // 请求头(这里有一点需要注意,HttpHeaders和HttpEntity都是引入的spring的包) HttpHeaders headers = new HttpHeaders(); headers.add("token", "WDWADAWDWADAWSAXZCXZCXZEDF"); // 请求 HttpEntity
调用结果:
示例二:post的json传参
@PostMapping("/postTest2") public User test2(HttpServletRequest request, @RequestBody User user1) { System.out.println("接受的参数:" + user1); System.out.println("接受的请求头:" + request.getHeader("token")); User user = new User(); user.setId(1); user.setName("张三"); user.setSex("男性"); return user; }
调用示例:
@GetMapping("/exchange2") public void hello2() { String url = "http://127.0.0.1:8006/postTest2"; // 请求体,不要用LinkedMultiValueMap,否则会变成key/value传参 User user = new User(); user.setId(1); user.setName("李四"); user.setSex("女性"); // 请求头(这里有一点需要注意,HttpHeaders和HttpEntity都是引入的spring的包) HttpHeaders headers = new HttpHeaders(); headers.add("token", "WDWADAWDWADAWSAXZCXZCXZEDF"); // 请求 HttpEntityrequst = new HttpEntity<>(user, headers); ResponseEntity exchange = restTemplate.exchange(url, HttpMethod.POST, requst, User.class); System.out.println("响应的结果:" + exchange.getBody()); }
调用结果:
8.2、封装通用util
可以写一个通用的工具类,根据传入的参数来确定请求的路径和内容。工具类肯定是想通过静态方式来直接调用,而不是通过new Util()的方式来使用。但是静态变量/类变量不是对象的属性,而是一个类的属性,spring则是基于对象层面上的依赖注入。所以我们不能@Autowired或者@resource一个静态变量。但是静态方法又不能调用非静态的属性,那么我们该怎么办呢?
@PostContruct是Java自带的注解,在方法上加该注解会在项目启动的时候执行该方法,也可以理解为在spring容器初始化的时候执行该方法。
@Component public class HttpUtil { private static Logger logger = LoggerFactory.getLogger(HttpUtil.class); @Resource private RestTemplate restTemplate; private static HttpUtil httpUtil; @PostConstruct public void init(){ httpUtil = this; httpUtil.restTemplate = this.restTemplate; } public staticString httpRequest(String url, HttpMethod method, HttpEntity entity,Class responseType){ try { //发起一个POST请求 ResponseEntity result = httpUtil.restTemplate.exchange(url, method, entity, responseType); return result.getBody(); } catch (Exception e) { logger.error("请求失败: " + e.getMessage()); } return null; } }
这个时候我们就需要维护一个工具类的静态实例,初始化的时候把restTemplate传进来,这样就可以直接调用HttpUtil.httpRequest()方法。
到此这篇关于详解RestTemplate 用法的文章就介绍到这了,更多相关RestTemplate 用法内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!