使用 Fabric8 在 CDI 托管 Bean 中注入 Kubernetes 服务

一则或许对你有用的小广告

欢迎加入小哈的星球 ,你将获得:专属的项目实战 / Java 学习路线 / 一对一提问 / 学习打卡/ 赠书活动

目前,正在 星球 内带小伙伴们做第一个项目:全栈前后端分离博客项目,采用技术栈 Spring Boot + Mybatis Plus + Vue 3.x + Vite 4手把手,前端 + 后端全栈开发,从 0 到 1 讲解每个功能点开发步骤,1v1 答疑,陪伴式直到项目上线,目前已更新了 204 小节,累计 32w+ 字,讲解图:1416 张,还在持续爆肝中,后续还会上新更多项目,目标是将 Java 领域典型的项目都整上,如秒杀系统、在线商城、IM 即时通讯、权限管理等等,已有 870+ 小伙伴加入,欢迎点击围观

Kubernetes 中,我最喜欢的是发现服务的方式。为什么?

主要是因为用户代码不必处理注册、查找服务,也因为没有网络意外 (如果您曾经尝试过基于注册表的方法,您就会明白我在说什么)

这篇文章将介绍如何使用 Fabric8 以使用 CDI 在 Java 中注入 Kubernetes 服务。

Kubernetes 服务

深入介绍 Kubernetes 服务 超出了本文的范围,但我将尝试对它们进行非常简短的概述。

在 Kubernetes 中,应用程序被打包为 Docker 容器。通常,将应用程序拆分成单独的部分是个不错的主意,因此您将拥有多个很可能需要相互通信的 Docker 容器。一些容器可以通过将它们放在同一个 Pod 中并置在一起,而其他容器可能是远程的并且需要一种方式来相互交谈。这就是 服务 发挥作用的地方。

一个容器可以绑定到一个或多个端口,为其他容器提供一个或多个“服务”。例如:

  • 数据库服务器。
  • 消息代理。
  • 休息服务。

问题是 其他容器如何知道如何访问这些服务?

因此, Kubernetes 允许您“标记”每个 Pod ,并使用这些标签来“选择”提供逻辑服务的 Pod 。这些标签是简单的键值对。

这是一个示例,说明我们如何通过指定具有键 和值 mysql 的 标签来“标记”一个 pod。


 {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

这是一个示例,说明我们如何定义一个公开 mysql 端口的 服务 。服务选择器使用我们上面指定的键/值对来定义提供服务的 pod。


 {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

Kubernetes 服务 信息作为环境变量传递给每个容器。对于创建的每个容器, Kubernetes 将确保为容器可见的 所有 服务传递适当的环境变量。

对于上面示例的 mysql 服务,环境变量将为:

  • MYSQL_SERVICE_HOST
  • MYSQL_SERVICE_PORT

Fabric8 提供了一个 CDI 扩展,可用于通过提供 Kubernetes 资源注入来简化 Kubernetes 应用程序的开发。

Fabric8 CDI 扩展入门

要使用 cdi 扩展,第一步是将依赖项添加到项目中。


 {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

下一步是决定要将哪个服务注入到哪个字段,然后向其添加 @ServiceName 注释。


 {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

在上面的示例中,我们有一个类需要 JDBC 连接到 mysql 数据库,该数据库可通过 Kubernetes Services 获得。

注入的 serivceUrl 将采用以下形式:[tcp|udp]://[host]:[port]。这是一个非常好的 url,但它不是一个正确的 jdbc url。所以我们需要一个实用程序来转换它。这就是 toJdbcUrl 的目的。

尽管可以在定义服务时指定协议,但只能指定核心传输协议,例如 TCP 或 UDP,而不能指定 http、jdbc 等协议。

@Protocol 注解

必须用应用程序协议查找并替换“tcp”或“udp”值,这很臭,而且很快就会变旧。为了删除该样板, Fabric8 提供了 @Protocol 注释。此注释允许您在注入的服务 URL 中选择所需的应用程序协议。在前面的例子中就是“jdbc:mysql”。所以代码可能看起来像:


 {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

毫无疑问,这样就干净多了。它仍然不包含有关实际数据库的信息或通常作为 JDBC Url 的一部分传递的任何参数,因此这里还有改进的余地。

人们会期望以同样的精神使用@Path 或@Parameter 注释,但这两者都是属于配置数据的东西,不适合硬编码到代码中。此外,Fabric8 的 CDI 扩展并不希望成为 URL 转换框架。因此,相反,它允许您直接实例化客户端以访问任何给定服务并将其注入源,从而使事情更上一层楼。

使用@Factory 注解为服务创建客户端

在前面的示例中,我们看到了如何获取服务的 url 并使用它创建 JDBC 连接。任何需要 JDBC 连接的项目都可以复制该片段并且它会很好地工作,只要用户记得他需要设置实际的数据库名称。

如果不是复制和粘贴该片段而是可以将其组件化并重用,那不是很好吗?这里是工厂注解的用武之地。您可以使用@Factory 对任何接受服务URL 作为参数并返回使用该URL 创建的对象(例如服务的客户端)的方法进行注解。所以对于前面的例子,我们可以有一个 MysqlConnectionFactory:


 {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

然后可以直接注入连接而不是注入 URL,如下所示。


 {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

这里发生了什么?

当 CDI 应用程序启动时,Fabric8 扩展将接收有关所有注释方法的事件。它将跟踪所有可用的工厂,因此对于使用@ServiceName 注释的任何非字符串注入点,它将创建一个在引擎盖下使用匹配的@Factory 的生产者。

在上面的示例中,首先 MysqlConnectionFactory 将被注册,当检测到带有 @ServiceName 限定符的 Connection 实例时,将创建一个委托给 MysqlConnectionFactory 的 Producer (将考虑所有限定符)

这很棒,但也很 简单 。为什么?

因为很少有这样的工厂只需要服务的 url。在大多数情况下,还需要其他配置参数,例如:

  • 认证信息
  • 连接超时
  • 更多的 ....

将@Factory 与@Configuration 一起使用

在下一节中,我们将看到使用配置数据的工厂。我将使用 mysql jdbc 示例并添加对指定可配置凭据的支持。但在此之前我要问一个反问?

“怎么样,能配置一个容器化的应用吗?”

最短的答案是“使用环境变量”。

因此,在此示例中,我将假设使用以下环境变量将凭据传递给需要访问 mysql 的容器:

  • MYSQL_用户名
  • MYSQL_密码

现在我们需要看看我们的@Factory 如何使用它们。

我过去曾想在 CDI 中使用环境变量,很可能您使用过 Apache DeltaSpike 。除其他项目外,该项目还提供了 @ConfigProperty 注释,它允许您将环境变量注入 CDI bean (它实际上做的比这更多)


 {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

这个 bean 可以与 @Factory 方法结合使用,这样我们就可以将配置传递给工厂本身。

但是,如果我们有 多个 数据库服务器,配置了一组不同的凭据或多个数据库怎么办?在这种情况下,我们可以使用服务名称作为前缀,让 Fabric8 确定它应该为每个 @Configuration 实例查找哪些环境变量。


 {
        "apiVersion" : "v1beta3",
        "kind" : "ReplicationController",
        "metadata" : {
          "labels" : {
            "name" : "mysql"
          },
          "name" : "mysql"
        },
        "spec" : {
          "replicas" : 1,
          "selector" : {    
            "name" : "mysql"
          },
          "template" : {
            "metadata" : {
              "labels" : {
                "name" : "mysql"
              }
            },
            "spec" : {
              "containers" : [ {
                "image" : "mysql",
                "imagePullPolicy" : "IfNotPresent",
                "name" : "mysql",
                "ports" : [ {
                  "containerPort" : 3306,
                  "name" : "mysql"
                    } ]                  
            }]
          }
        }
      }
    }

现在,我们有一个可重用的组件,可以与在 kubernetes 中运行的任何 mysql 数据库一起使用,并且是完全可配置的。

Fabric8 CDI 扩展中还有其他功能,但由于这篇文章已经太长,它们将在以后的文章中介绍。