简单使用spring cloud 服务注册做一个请求转发中心

背景

上篇文章 记录多项目共用一个公众号逻辑修改, 实现了多个项目共用一个公众号。
但是也存在几点问题,比如:

  • 中间服务器拦截了微信的请求,虽然方便了项目不再需要写微信授权的代码,但如果以后需要再拓展新的微信请求,就需要再去中间服务器写api
  • 需要搭建前台,手动维护各个项目的地址和key。

我们可以让中间服务器只是简单地转发请求,不再拦截,解决第一点问题。

同时利用 服务发现与注册 解决第二个问题。

用服务注册的好处就是,我们不需要再手动地维护项目信息、各个项目的ip,只需要让新来的项目向服务中心注册一下就好了。

服务发现与注册

服务注册是指向服务注册中心注册一个服务实例,服务提供者将自己的服务信息(如IP地址等)告知服务注册中心。

简单使用spring cloud 服务注册做一个请求转发中心_第1张图片

服务发现是指当服务消费者需要消费另外一个服务时, 服务注册中心能够告知服务消费者它所要消费服务的实例信息(如服务名、IP 地址等)。

通常情况下,一个服务既是服务提供者,也是服务消费者。

所以,如下图方式,每个项目都向注册中心注册,就不需要建表维护各个项目的ip地址了。

简单使用spring cloud 服务注册做一个请求转发中心_第2张图片

常见的注册中心

能够实现服务注册的组件很多,比如ZooKeeper、Eureka、Consul、Nacos等。

简单使用spring cloud 服务注册做一个请求转发中心_第3张图片

这里选用比较常见的 Eureka。

Eureka

Eureka是Netflix开发的服务发现框架,SpringCloud将它集成在自己的子项目spring-cloud-netflix中, 实现SpringCloud的服务发现功能。

简单使用spring cloud 服务注册做一个请求转发中心_第4张图片

上图简要描述了Eureka的基本架构,由3个角色组成:

  1. Eureka Server 提供服务注册和发现
  2. Service Provider 服务提供方 将自身服务注册到Eureka,从而使服务消费方能够找到
  3. Service Consumer 服务消费方 从Eureka获取注册服务列表,从而能够消费服务

Eureka基本概念

Register——服务注册

当 Eureka Client 向 Eureka Server 注册时,Eureka Client 提供自身的元数据,比如 IP 地址、 端口、运行状况指标的 Url、主页地址等信息。

Renew——服务续约

  Eureka Client 在默认的情况下会每隔 30 秒发送一次心跳来进行服务续约。通过服务续约 来告知 Eureka Server 该 Eureka Client 仍然可用,没有出现故障。正常情况下,如果 Eureka Server 在 90 秒内没有收到 Eureka Client 的心跳,Eureka Server 会将 Eureka Client 实例从注册列表中删除。注意:官网建议不要更改服务续约的间隔时间。

Eviction——服务剔除

在默认情况下(当然我们可以修改) ,当 Eureka Client 连续 90 秒没有向 Eureka Server 发送服务续约(即心跳) 时,Eureka Server 会将该服务实例从服务注册列表删除,即服务剔除。

Fetch Registries——获取服务注册列表信息

   
Eureka Client 从 Eureka Server 获取服务注册表信息,并将其缓存在本地。Eureka Client 会使用服务注册列表信息查找其他服务的信息,从而进行远程调用。该注册列表信息定时(每 30 秒)更新一次,每次返回注册列表信息可能与 Eureka Client 的缓存信息不同,Eureka Client 会自己处理这些信息。如果由于某种原因导致注册列表信息不能及时匹配,Eureka Client 会重新获取整个注册表信息。

下面就来用Eureka实现服务的注册与发现

角色如下

简单使用spring cloud 服务注册做一个请求转发中心_第5张图片

注册中心

1.引入依赖


        1.8
        2021.0.5
    

    
        
            org.springframework.cloud
            spring-cloud-starter-netflix-eureka-server
            2.2.10.RELEASE
        


        
            
                org.springframework.cloud
                spring-cloud-dependencies
                2021.0.5
                pom
                import
            
        
    

这里有个比较深的坑,就是spring cloud的版本一定要和spring boot的版本相对应.

否则就会报异常

java.lang.ClassNotFoundException: org.springframework.boot.context.properties.ConfigurationBeanFactoryMetadata

首先 pom.xml 里面是否有 dependencyManagement 版本管理的设置,因为这块是会识别并加载所需要的依赖版本,比如我要加载 spring-cloud-starter-netflix-eureka-client ,首先确定好你的 SpringBoot 版本是否兼容依赖的 SpringCloud 版本。

其次是否设置了 spring-cloud.version ,接着确认是否设置了 dependencyManagement 下面的 spring-cloud-dependencies 依赖,最后确认好要加载的 spring-cloud-starter-netflix-eureka-client ,这样最终保证你所需要的依赖包能够争取无误的加载下来。

可以去https://spring.io/projects/spring-cloud 查看两者对应的版本

简单使用spring cloud 服务注册做一个请求转发中心_第6张图片

2.配置application.yml(全局配置)

spring:
  application:
    name: wechat-service #配置注册的名字

server:
  port: 8761

eureka:
  client:
    register-with-eureka: true #是否将自己注册到eureka-server中
    fetch-registry: true #是否从eureka-server中获取服务注册信息
    service-url:
      defaultZone: http://localhost:${server.port}/eureka/  #设置服务注册中心地址
  datacenter: cloud
  environment: product

3. 配置启动类

@SpringBootApplication
@EnableEurekaServer
public class WechatServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(WechatServiceApplication.class, args);
    }

}

之后打开 localhost:8761, 看到如下界面说明注册中心启动成功, 可以看到目前注册了一个服务 wechat-service, 即该项目本身。
简单使用spring cloud 服务注册做一个请求转发中心_第7张图片

服务注册到Eureka注册中心

另起一个spring boot项目作为服务。

1.引入依赖


  1.8
  2020.0.4



        
            
                org.springframework.cloud
                spring-cloud-dependencies
                ${spring-cloud.version}
                pom
                import
            
        


  
      org.springframework.cloud
      spring-cloud-dependencies
      Hoxton.SR8
      pom
      import
  

  
            org.springframework.cloud
            2.2.5.RELEASE
            spring-cloud-starter-netflix-eureka-client
        

这里也要注意spring boot的版本和 spring cloud的对应。

2.配置 application.yml 文件

spring:
  application:
    name: schedule  # 注册名称

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/  #注册到 eureka
  instance:
    preferIpAddress: true

3.修改启动类

添加 @EnableEurekaClient 注解

@SpringBootApplication
@EnableEurekaClient
public class ScheduleApplication {
  public static void main(String[] args) {
    SpringApplication.run(ScheduleApplication.class, args);
  }

之后启动该项目, 然后去注册中心查看,可以看到 schedule 项目成功注册。

简单使用spring cloud 服务注册做一个请求转发中心_第8张图片

那么注册成功之后,我们怎么获取呢?

获取服务

获取所有服务:

如果注册中心想获取所有服务可以使用 eurekaClient 提供的 getRegisteredApplications 方法获取所有服务。

@Controller
@RequestMapping("/eurekacenter")
public class EuServiceController {

  @Autowired
  private EurekaClient eurekaClient;

  @GetMapping("/services")
  @ResponseBody
  public List getServices() {
    return eurekaClient.getApplications().getRegisteredApplications();
  }
}

获取指定服务:

一般来说,我们更常用的是获取某一个服务。可以使用 discoveryClient.getInstances("服务的name"),来获取一个服务。

@Autowired
private DiscoveryClient discoveryClient;

// 根据服务名称获取服务
List serviceInstances = discoveryClient.getInstances(
"wechat-service");
if (CollectionUtils.isEmpty(serviceInstances)) {
  return null;
}
ServiceInstance si = serviceInstances.get(0);

String requestUrl = "http://" + si.getHost() + ":" + si.getPort() + "/request/getQrCode";

转发请求功能实现

现在我们知道了,可以根据服务的name来获取一个服务,那么作为一个请求转发中心,怎么知道一个请求要转发给哪个服务呢?

简单使用spring cloud 服务注册做一个请求转发中心_第9张图片

上一篇文章实现第三方登陆:微信扫码登录 (spring boot) 了解到,请求微信服务器需要生成一个场景值, 并且微信返回的请求 也会返回这个场景值。

简单使用spring cloud 服务注册做一个请求转发中心_第10张图片

所以,我们只要简单地在场景值前面加一个服务的关键字就可以了.逻辑如下

简单使用spring cloud 服务注册做一个请求转发中心_第11张图片

注册中心转发请求逻辑

接收微信请求后,转发请求的逻辑如下:

  1. 获取请求源客户端
  2. 设置转发请求的参数,转发给客户端
  3. 获取客户端响应数据
  4. 返回响应数据给微信

代码如下:

/**
   * 当设置完微信公众号的接口后,微信会把用户发送的消息,扫码事件等推送过来
   *
   * @param signature 微信加密签名,signature结合了开发者填写的 token 参数和请求中的 timestamp 参数、nonce参数。
   * @param encType 加密类型(暂未启用加密消息)
   * @param msgSignature 加密的消息
   * @param timestamp 时间戳
   * @param nonce 随机数
   * @throws IOException
   */
  @PostMapping(produces = "text/xml; charset=UTF-8")
  public void api(HttpServletRequest httpServletRequest,
                  HttpServletResponse httpServletResponse,
                  @RequestParam("signature") String signature,
                  @RequestParam(name = "encrypt_type", required = false) String encType,
                  @RequestParam(name = "msg_signature", required = false) String msgSignature,
                  @RequestParam("timestamp") String timestamp,
                  @RequestParam("nonce") String nonce) throws IOException {
    if (!this.weChatMpService.checkSignature(timestamp, nonce, signature)) {
      this.logger.warn("接收到了未通过校验的微信消息,这可能是token配置错了,或是接收了非微信官方的请求");
      return;
    }

    // 获取客户端url
    String targetUrl = this.wechatService.selectClientUrl(httpServletRequest);

    UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(targetUrl + RequestUrl)
        .queryParam("signature", signature)
        .queryParam("timestamp", timestamp)
        .queryParam("nonce", nonce);

    if (msgSignature != null) {
      builder.queryParam("msg_signature", msgSignature);
    }
    if (encType != null) {
      builder.queryParam("encrypt_type", encType);
    }

    URI uri = builder.build().encode().toUri();
    // 设置转发请求的参数
    HttpURLConnection connection = (HttpURLConnection) uri.toURL().openConnection();
    connection.setRequestMethod("POST");
    connection.setRequestProperty("Content-Type", "text/xml; charset=UTF-8");
    connection.setDoOutput(true);
    connection.setDoInput(true);
    if (msgSignature != null) {
      connection.setRequestProperty("msg_signature", msgSignature);
    }
    if (encType != null) {
      connection.setRequestProperty("encrypt_type", encType);
    }

    // 将 httpServletRequest 中的数据写入 转发请求中
    OutputStream outputStream = connection.getOutputStream();
    String requestBody =  new RequestWrapper(httpServletRequest).getBodyString();
    outputStream.write(requestBody.getBytes(StandardCharsets.UTF_8));
    outputStream.flush();
    outputStream.close();

    // 获取client 的响应数据
    InputStream inputStream = connection.getInputStream();
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = inputStream.read(buffer)) != -1) {
      byteArrayOutputStream.write(buffer, 0, bytesRead);
    }
    byte[] responseBytes = byteArrayOutputStream.toByteArray();
    inputStream.close();

    // 将响应数据返回给微信
    httpServletResponse.setContentType("text/xml; charset=UTF-8");
    httpServletResponse.setContentLength(responseBytes.length);
    httpServletResponse.getOutputStream().write(responseBytes);
    httpServletResponse.getOutputStream().flush();
    httpServletResponse.getOutputStream().close();
  }

效果图和以前一样:
简单使用spring cloud 服务注册做一个请求转发中心_第12张图片

源代码:https://github.com/weiweiyi189/weChatServiceCenter

参考:

https://juejin.cn/post/6910031138048180238
https://blog.51cto.com/u_10401840/5179710
https://juejin.cn/post/6910031138048180238#heading-8

你可能感兴趣的:(简单使用spring cloud 服务注册做一个请求转发中心)