SpringBoot如何优雅实现远程调用

废话

实在不知道做什么,还是摸会鱼写个博客吧,接下来是纯干货,个人不是很喜欢理论知识,干脆直接上手就干。

不行看这文章的文末有代码,自己下载去看去

故事

问:你有两个系统A和B,这个时候A系统需要B系统的数据或者B系统需要调用A系统数据,你应该怎么办

答:常用的方式无非是远程调用和多数据源。

多数据源:大概意思就是一套系统,通常来说我们会创建一个数据库进行存储数据,但有时候我们可以在系统中可以看见有多数据源的配置,多数据源可以在同一套系统中进行多数据库的访问,它可以在你的系统中访问其他系统配置的数据库,虽然这样做也有优点也有缺点就不一一说了,毕竟今天重点不在这。

远程调用:这个东西应该都熟悉,因为在微服务不断使用下,这玩意非常常见。大概说一下就是A系统需要用到B系统的数据,这个时候B系统写一个接口给A系统提供数据,A系统调用B系统的接口进行获取数据。大概就是这样,接下来上干货。

事先准备

做这玩意首先,先开始一个SpringBoot的项目,就构建一个空的springbootweb项目就行,按照个人喜欢可以添加点自己需要的工具。接下来开始搞。

RestTemplate实现

RestTemplate是什么看看别人怎么说吧

RestTemplate 是从 Spring3.0 开始支持的一个 HTTP 请求工具,它提供了常见的REST请求方案的模版,例如 GET 请求、POST 请求、PUT 请求、DELETE 请求以及一些通用的请求执行方法 exchange 以及 execute。RestTemplate 继承自 InterceptingHttpAccessor 并且实现了 RestOperations 接口,其中 RestOperations 接口定义了基本的 RESTful 操作,这些操作在 RestTemplate 中都得到了实现。

怎么用直接上代码:

1、先写一下RestTemplate的配置统一处理一下。新建RestTemplateConfig

package com.example.transfercloud.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.StandardCharsets;

/**
 * @author user
 */
@Configuration
public class RestTemplateConfig {
    @Bean
    public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
        RestTemplate restTemplate = new RestTemplate(factory);
        // 支持中文编码
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }

    @Bean
    public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
        SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
        //单位为ms
        factory.setReadTimeout(5000);
        factory.setConnectTimeout(5000);
        return factory;
    }
}

2、写个Test文件测试一下,写入以下代码

package com.example.transfercloud;

import com.example.transfercloud.entity.TestPOJO;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.*;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

@SpringBootTest
@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
class TestRestTemplate {
    @Autowired
    private RestTemplate restTemplate;

    @Test
    void testRestTemplateOfGet() {
        ResponseEntity<String> responseEntity = restTemplate.getForEntity("https://test.wslhome.top/api/getRecommendedGoods/v1?size=10", String.class);
        log.info("\n code:{}\n header:{}\n body:{}\n",responseEntity.getStatusCodeValue(),responseEntity.getHeaders(),responseEntity.getBody());

//        TestPOJO responseEntity0 = restTemplate.getForObject("https://test.wslhome.top/api/getRecommendedGoods/v1?size=10", TestPOJO.class);
//        log.info("\n 实体{}",responseEntity0);
//需要创建对应的实体类,这里可以采用GesonFormat逆向生成


        ResponseEntity<String> responseEntity1 = restTemplate.getForEntity("https://test.wslhome.top/api/getRecommendedGoods/v1?size={1}", String.class,2);
        log.info("\n code:{}\n header:{}\n body:{}\n",responseEntity1.getStatusCodeValue(),responseEntity1.getHeaders(),responseEntity1.getBody());

        Map<String, String> params = new HashMap<>(4);
        params.put("size","3");
        ResponseEntity<String> responseEntity2 = restTemplate.getForEntity("https://test.wslhome.top/api/getRecommendedGoods/v1?size={size}", String.class,params);
        log.info("\n code:{}\n header:{}\n body:{}\n",responseEntity2.getStatusCodeValue(),responseEntity2.getHeaders(),responseEntity2.getBody());
    }

    @Test
    void testRestTemplateOfPost(){
        LinkedMultiValueMap<String, Object> paramMap = new LinkedMultiValueMap<>();
        paramMap.add("password","123");
        paramMap.add("name","234");
        paramMap.add("code","2131");
        HttpEntity<LinkedMultiValueMap<String, Object>> param = new HttpEntity<>(paramMap, null);
        ResponseEntity<String> responseEntity2 = restTemplate.postForEntity("https://test.wslhome.top/login/user/v1",param,String.class);
        log.info("\n code:{}\n header:{}\n body:{}\n",responseEntity2.getStatusCodeValue(),responseEntity2.getHeaders(),responseEntity2.getBody());
    }

    @Test
    void testRestTemplateOfDelete(){
        Map<String,String> param = new HashMap<>(4);
        param.put("id","34");
        restTemplate.delete("https://test.wslhome.top/user/delCartByIds/v1", param);

        ResponseEntity<String> result = restTemplate.exchange("https://test.wslhome.top/user/delCartByIds/v1" , HttpMethod.DELETE, null, String.class, param);
        log.info("result{}",result);

        //设置请求头
        HttpHeaders headers = new HttpHeaders();
        //如果发送的参数数据是json数据的话,需要添加如下特殊的请求头
        headers.setContentType(MediaType.APPLICATION_JSON);
        //或者headers.set("Content-Type", "application/json");
        headers.add("Authorization", "shoppingkilleyJhbGciOiJIUzI1NiJ9.eyJ2ZXJpZnk6dXNlcjppZCI6MSwidmVyaWZ5OnVzZXI6ZmxhZyI6MTAsImV4cCI6MTYyMzc0Mjg4M30.joTLCSPy9gXlp0ABzbDlDr58hB1_wRToZaAMRVA4FqY");
        HttpEntity<Map<String, String>> httpEntity = new HttpEntity<>(param, headers);
        ResponseEntity<String> result1 = restTemplate.exchange("https://test.wslhome.top/user/delCartByIds/v1" , HttpMethod.DELETE, httpEntity, String.class);
        log.info("result{}",result1);
    }
}

运行结果大致如下:

image.png

PS:上述代码中注释部分需要创建对应的实体类

 TestPOJO responseEntity0 = restTemplate.getForObject("https://test.wslhome.top/api/getRecommendedGoods/v1?size=10", TestPOJO.class);

HttpClient实现

httpClient是什么,这玩意你就理解成一个工具包,你可以使用它使得用代码能够模拟浏览器发出请求

  • HttpClient库实现了所有可用的HTTP方法。
  • HttpClient库提供API以使用安全套接字层协议保护请求。
  • 使用HttpClient,可以使用代理建立连接。

因此httpClient也被用于接口之间的调用

使用时候先引入依赖

<dependency>
       <groupId>org.apache.httpcomponents</groupId>
       <artifactId>httpclient</artifactId>
</dependency>

然后直接上代码

package com.example.transfercloud;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Objects;

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
class TestHttpClient {

    /**
     * GET---无参测试
     *
     * @date 2018年7月13日 下午4:18:50
     */
    @Test
    void httpClientOfGet() {
       //创建客户端
        CloseableHttpClient httpClient = HttpClientBuilder.create().build();
        // 创建Get请求
        //HttpPost httpPost = new HttpPost("https://test.wslhome.top/api/getAdvertiseForView/v1");
        HttpGet httpGet = new HttpGet("https://test.wslhome.top/api/getAdvertiseForView/v1");
        // 响应模型
        CloseableHttpResponse response = null;
        try {
            // 执行Get请求
            response = httpClient.execute(httpGet);
            // 从响应模型中获取响应实体
            HttpEntity responseEntity = response.getEntity();
            //编码
            final String s = EntityUtils.toString(responseEntity, StandardCharsets.UTF_8);
            log.info("\n text{}",s);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                // 释放资源
                if (Objects.nonNull(httpClient)) {
                    httpClient.close();
                }
                if (Objects.nonNull(response)) {
                    response.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

实现效果如图:

image.png

PS:因为在httpClient中get和post方法差别不大,这里为了方便就只用get来举例说明,需要POST的方法,照葫芦画瓢就行

consul+feign实现

在微服务中,也常用consul+feign+hystrix来实现服务之间的调用、熔断降级,然后我们来看看怎么使用。

1、在创建一个springboot项目,然后写个controller为另一个服务提供接口
package com.example.cloudemo.controller;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/test")
public class TestController {

    @GetMapping("/getNum")
    public String getNum() throws InterruptedException {
        Thread.sleep(10000L);
        return "测试get请求远程调用";
    }

    @PostMapping("/postNum")
    public String PostNum(){
        return "测试POST请求远程调用";
    }

    @PostMapping("/postTest")
    public String PostNums(@RequestParam Integer num){
        if (num == 0){
            return "远程调用测试,参数0";
        }else if (num == 10){
            return "远程调用测试,参数 10";
        }else {
            return "远程调用参数只支持0、10";
        }
    }
}

2、在两个项目中都引入依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-config</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-consul-discovery</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>2.2.1.RELEASE</version>
</dependency>
3、两个项目中新建bootstrap.yml文件,文件中写入下面内容
spring:
  application:
    name: feginTest

  cloud:
    consul:
      host: 127.0.0.1
      port: 8500
      #是否启用consul
      enabled: true
      discovery:
        # 启用服务发现
        enabled: true
        # 启用服务注册
        register: true
        # 服务停止时取消注册
        deregister: true
        # 表示注册时使用IP而不是hostname
        prefer-ip-address: true
        # 执行监控检查的频率
        health-check-interval: 30s
        # 设置健康检查失败多长时间后,取消注册
        health-check-critical-timeout: 30s
        # 开启心跳检测
        heartbeat:
          enabled: true
        # 健康检查的路径
        health-check-path: /test
        # 服务注册标识,格式为:应用名称+服务器IP+端口
        instance-id: ${spring.application.name}:${spring.cloud.client.ipaddress}:${server.port}
        config:
          # 启用consul的配置中心功能,默认是true
          enabled: true
          # 表示consul上面文件的格式 有四种 YAML PROPERTIES KEY-VALUE FILES,默认是KEY-VALUE
          format: YAML
          #配置基本文件,默认值config
          prefix: ${spring.application.name}
          #表示开发环境:dev/test/prepped,生产环境独立部署consul服务器
          default-context: dev
          #表示consul上面的配置文件名,每个开发人员各自管理自己的配置文件
          data-key: infokey
          # watch选项为配置监视功能,主要监视配置的改变
          watch:
            enabled: true
            delay: 10000
            wait-time: 30


feign:
  hystrix:
    enabled: true

hystrix:
  shareSecurityContext: true
  command:
    default:
      circuitBreaker:
        requestVolumeThreshold: 1
        sleepWindowInMilliseconds: 15000
        forceOpen: false
        forceClosed: false
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 5000

ribbon:
  ConnectTimeout: 4000
  ReadTimeout: 4000

PS1:spring.application.name是服务名称,两个服务应该采用不通的名称。对了application.yml中的端口号也应该改为不同的端口号。

PS:我当时因为没有新建,直接在application.yml中写,然后访问注册中心一直都是localhost,无论填什么地址都不会变,然后无奈了,也找不到解决办法,只有新建的bootstrap.yml然后就正常了。

4、修改启动类

在启动类Application.java的类上加入注解

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
5、依次启动两个项目,然后去你的consul看看服务启动是否正常。你会看到这个截图的样子就是正常的。

image.png

6、开始写消费端的接口调用

(1)创建一个接口类用于远程调用,内容如下

package com.example.transfercloud.rpc;

import com.example.transfercloud.rpc.callBack.TestCloudCallBackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

@FeignClient(value = "TestDemo",fallbackFactory = TestCloudCallBackFactory.class)
public interface TestCloud {

    @GetMapping("/test/getNum")
    String getTestNum();

    @PostMapping("/test/postNum")
    String getTestNumByPost();

    @PostMapping("/test/postTest")
    String getTestNumByPostParam(@RequestParam Integer num);

}

(2)创建callBack文件实现接口

package com.example.transfercloud.rpc.callBack;

import com.example.transfercloud.rpc.TestCloud;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class TestCloudCallBack implements TestCloud {
    @Override
    public String getTestNum() {
        log.error("调用失败,服务降级");
        return null;
    }

    @Override
    public String getTestNumByPost() {
        log.error("调用失败,服务降级");
        return null;
    }

    @Override
    public String getTestNumByPostParam(Integer num) {
        log.error("调用失败,服务降级");
        return null;
    }
}

(3)创建FallbackFactory文件处理服务不可用情况

package com.example.transfercloud.rpc.callBack;

import com.example.transfercloud.rpc.TestCloud;
import feign.hystrix.FallbackFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class TestCloudCallBackFactory implements FallbackFactory<TestCloud> {

    @Override
    public TestCloud create(Throwable throwable) {
        return new TestCloud() {
            @Override
            public String getTestNum() {
                log.error("回调失败0");
                return null;
            }

            @Override
            public String getTestNumByPost() {
                log.error("回调失败1");
                return null;
            }

            @Override
            public String getTestNumByPostParam(Integer num) {
                log.error("回调失败2");
                return null;
            }
        };
    }
}

6、编写测试类看看是否成功
package com.example.transfercloud;

import com.example.transfercloud.rpc.TestCloud;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.annotation.Resource;

@Slf4j
@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class TestColud {

    @Resource
    private TestCloud testCloud;
    @Test
    public void test(){
        final String testNum = testCloud.getTestNum();
        final String testNumByPost = testCloud.getTestNumByPost();
        final String testNumByPostParam = testCloud.getTestNumByPostParam(10);
        log.info("\n{}\n{}\n{}",testNum,testNumByPost,testNumByPostParam);
    }
}

7、结果

image.png

可以看到第一个接口被降级因为Thread.sleep(10000L);的原因,第二三个接口调用成功

文中代码

点击下载 demo.zip

点击下载 demo.zip

点击下载 demo.zip

彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋彩蛋

文章来自个人博客[hhtps://www.wslhome.top]
转载请注明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

sirwsl

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值