2024年2月

开启OneAPI服务

OneAPI介绍

OpenAI 接口管理 & 分发系统,支持 Azure、Anthropic Claude、Google PaLM 2 & Gemini、智谱 ChatGLM、百度文心一言、讯飞星火认知、阿里通义千问、360 智脑以及腾讯混元,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用. OpenAI key management & redistribution system, using a single API for all LLMs, and features an English UI.

项目地址:
https://github.com/songquanpeng/one-api

image-20240227105438373

使用OneAPI

基于docker部署:

# 使用 SQLite 的部署命令:
docker run --name one-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api
# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数,不清楚如何修改请参见下面环境变量一节。
# 例如:
docker run --name one-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/one-api:/data justsong/one-api

部署完成后,打开本地3000端口,如下所示:

image-20240227110126395

初始账户为root,密码为123456。

登录之后,会提示修改密码。

点击渠道,创建新的渠道:

image-20240227112714344

填入自己的大模型密钥。

添加令牌:

image-20240227112824812

测试OneAPI服务是否可用

使用Postman查看接口是否可用:

image-20240227113413504

注意事项

接口地址
:http://<你的IP地址>:3000/v1/chat/completions

ip地址可通过cmd输入ipconfig查到。

在请求中加入令牌

image-20240227113639334

在红框位置输入OneAPI中的令牌。

测试的json

{
    "model":"SparkDesk",
    "messages":[
        {
            "role":"user",
            "content":"你是谁"
        }
    ],
      "temperature":0.7    
}

星火大模型的响应

{
    "id": "",
    "object": "chat.completion",
    "created": 1709004732,
    "choices": [
        {
            "index": 0,
            "message": {
                "role": "assistant",
                "content": "您好,我是科大讯飞研发的认知智能大模型,我的名字叫讯飞星火认知大模型。我可以和人类进行自然交流,解答问题,高效完成各领域认知智能需求。"
            },
            "finish_reason": "stop"
        }
    ],
    "usage": {
        "prompt_tokens": 2,
        "completion_tokens": 40,
        "total_tokens": 42
    }
}

创建WPF项目

SemanticKernel简介

Semantic Kernel 是一个开源 SDK,可让您轻松构建可以调用现有代码的代理。作为高度可扩展的 SDK,可以将语义内核与 OpenAI、Azure OpenAI、Hugging Face 等模型一起使用!通过将现有的 C#、Python 和 Java 代码与这些模型相结合,可以生成用于回答问题和自动执行流程的代理。

image-20240227141312234

安装SemanticKernel

image-20240227141440713

在SemanticKernel中使用星火大模型

创建一个OpenAIHttpClientHandler类

OpenAIHttpClientHandler类代码:

 public class OpenAIHttpClientHandler : HttpClientHandler
 {
     protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
     {
         UriBuilder uriBuilder;
         switch (request.RequestUri?.LocalPath)
         {
             case "/v1/chat/completions":
                 uriBuilder = new UriBuilder(request.RequestUri)
                 {
                     // 这里是你要修改的 URL
                     Scheme = "http",
                     Host = "你的ip地址",
                     Port = 3000,
                     Path = "v1/chat/completions",
                 };
                 request.RequestUri = uriBuilder.Uri;
                 break;
         }

         // 接着,调用基类的 SendAsync 方法将你的修改后的请求发出去
         HttpResponseMessage response = await base.SendAsync(request, cancellationToken);

         int n = 0;
         while ((int)response.StatusCode == 500 && n < 10)
         {
             response = await base.SendAsync(request, cancellationToken);
             n++;
         }

         return response;
     }
 }

使用dotenv.net存储敏感数据

在dotenv.net.dll同一路径下,创建一个.env文件:

image-20240227153748317

在.env文件中存储敏感数据:

image-20240227154101538

模型ID要写SparkDesk,注意不要有空格,试过了有空格会报错。

APIKey就是写之前在OneAPI中复制的令牌。

测试能不能用

测试代码如下:

 
 // 加载环境变量
 DotEnv.Load();

 // 读取环境变量
 var envVars = DotEnv.Read();

// Create kernel
 var builder = Kernel.CreateBuilder();

 var handler = new OpenAIHttpClientHandler();

 builder.AddOpenAIChatCompletion(
     modelId: envVars["ModeId"],
     apiKey: envVars["APIKey"],
     httpClient: new HttpClient(handler));

 var kernel = builder.Build();

 // Create chat history
 ChatHistory history = [];

 // Get chat completion service
 var chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();

 // Start the conversation                                                  
     history.AddUserMessage("你是谁?");

 // Enable auto function calling
 OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
  {
      ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
   };

     // Get the response from the AI
     var result = await chatCompletionService.GetChatMessageContentAsync(
         history,
         executionSettings: openAIPromptExecutionSettings,
         kernel: kernel);

     // Print the results
     Console.WriteLine("Assistant > " + result);

     // Add the message from the agent to the chat history
     history.AddMessage(result.Role, result.Content);
 }

查看结果:

image-20240227154656502

第一次请求失败,为了解决这个问题,我们加了下面这段代码:

  int n = 0;
  while ((int)response.StatusCode == 500 && n < 10)
  {
      response = await base.SendAsync(request, cancellationToken);
      n++;
  }

image-20240227154922453

再请求一遍就成功了。

image-20240227155010554

收到了星火认知大模型的回答。

使用HandyControl构建页面

xaml如下:

<Window x:Class="SK_Wpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"  
        xmlns:hc="https://handyorg.github.io/handycontrol"
        xmlns:local="clr-namespace:SK_Wpf"    
        mc:Ignorable="d"
        Title="SK_WPF" Height="450" Width="800"
        Loaded="Window_Loaded">
    <StackPanel Margin="32">
        <hc:Row Margin="0,20,0,0">
            <hc:Col Span="11">
                <StackPanel>
                    <Button Style="{StaticResource ButtonPrimary}" Content="问AI" Width="80"
                            Click="Button_Click_1"/>
                    <hc:TextBox x:Name="textBox1"
                                Margin="0,20,0,0"
                                Width="300" Height="200"
                                AcceptsReturn="True"/>

                </StackPanel>
            </hc:Col>
            <hc:Col Span="2">
                <Grid >
                    <hc:LoadingCircle x:Name="loading1" 
                                      HorizontalAlignment="Center" 
                                      VerticalAlignment="Center"
                                      Visibility="Hidden"/>
                </Grid>
            </hc:Col>
            <hc:Col Span="11">
                <StackPanel>
                    <hc:Tag ShowCloseButton="False" Content="AI回答"/>
                    <RichTextBox x:Name="richTextBox2"  
                                 VerticalAlignment="Center" 
                                 HorizontalAlignment="Center" 
                                 Margin="0,20,0,0"
                                 Width="300" Height="200">
                    </RichTextBox>
                </StackPanel>
            </hc:Col>
        </hc:Row>
    </StackPanel>
</Window>

实现效果如下:

image-20240227181715497

在WPF中集成SK+OneAPI+星火认知大模型

cs如下:

using dotenv.net;
using HandyControl.Controls;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using SK_Wpf.Plugins;
using System.Net.Http;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace SK_Wpf
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : System.Windows.Window
    {
        IDictionary<string, string>? envVars;      
        Kernel? kernel;
        ChatHistory history = [];
        IChatCompletionService chatCompletionService;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // 加载环境变量
            DotEnv.Load();

            // 读取环境变量
            envVars = DotEnv.Read();



            // Create kernel
            var builder = Kernel.CreateBuilder();

            var handler = new OpenAIHttpClientHandler();

            builder.AddOpenAIChatCompletion(
                modelId: envVars["ModeId"],
                apiKey: envVars["APIKey"],
                httpClient: new HttpClient(handler));
            builder.Plugins.AddFromType<HelloPlugin>("helloPlugin");

            var kernel = builder.Build();

            // Get chat completion service
            chatCompletionService = kernel.GetRequiredService<IChatCompletionService>();


        }
      
        private async void Button_Click_1(object sender, RoutedEventArgs e)
        {
            loading1.Visibility = Visibility.Visible;

            string question = textBox1.Text;
          
            // Get user input
            history.AddUserMessage(question);

            // Enable auto function calling
            OpenAIPromptExecutionSettings openAIPromptExecutionSettings = new()
            {
                ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions
            };

            // Get the response from the AI
            var result = await chatCompletionService.GetChatMessageContentAsync(
                history,
                executionSettings: openAIPromptExecutionSettings,
                kernel: kernel);

            // Print the results           
            richTextBox2.AppendText(result.ToString());

            // Add the message from the agent to the chat history
            history.AddMessage(result.Role, result.Content);

            loading1.Visibility = Visibility.Hidden;
         
        }
    }     
    }

实现效果如下所示:

实现效果

总结

本文是一次在WPF使用SemanticKernel基于OneAPI集成讯飞星火认知大模型的实践,没有申请OpenAIAPIKey的可以使用讯飞星火认知大模型,现在个人身份认证有送200万token,个人使用可以用很久了。但是效果上肯定和OpenAI还有差别,经过测试,自动本地函数调用,用OpenAI可以用星火认知大模型不行。下期可以写一下两个模型回答的对比。

image-20240228122947902

最后感谢大佬们的分享,见参考。

参考

1、
想学Semantic Kernel,没有OpenAI接口该怎么办? (qq.com)

2、
实战教学:用Semantic Kernel框架集成腾讯混元大模型应用 (qq.com)

3、
Create AI agents with Semantic Kernel | Microsoft Learn

4、
songquanpeng/one-api: OpenAI 接口管理 & 分发系统,支持 Azure、Anthropic Claude、Google PaLM 2 & Gemini、智谱 ChatGLM、百度文心一言、讯飞星火认知、阿里通义千问、360 智脑以及腾讯混元,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用. OpenAI key management & redistribution system, using a single API for all LLMs, and features an English UI. (github.com)

5、
microsoft/semantic-kernel: Integrate cutting-edge LLM technology quickly and easily into your apps (github.com)

分享一个我自己写的 Windows 定时任务计划(Task Scheduler) 动态创建代码,没做太多封装,留个实现笔记

首先封装一个简单配置项的类


1 public classTaskSchedulerConfig2 {3     /// <summary>
4     ///引用程序路径5     /// </summary>
6     public string ApplicationPath { get; set; }7     /// <summary>
8     ///引用程序参数9     /// </summary>
10     //public string ApplicationParameter { get; set; }
11     /// <summary>
12     ///任务名称13     /// </summary>
14     public string TaskName { get; set; }15     /// <summary>
16     ///任务说明17     /// </summary>
18     public string TaskDescription { get; set; }19     /// <summary>
20     ///任务执行周期 0 每日执行 1 每周执行 2 每月执行21     /// </summary>
22     public int TaskExecutionCycle { get; set; }23     /// <summary>
24     ///开始执行时间范围 yyyy/MM/dd HH:mm:ss25     /// </summary>
26     public DateTime StartBoundary { get; set; }27     /// <summary>
28     ///每月的第几号执行29     /// </summary>
30     public int DaysOfMonth { get; set; }31     /// <summary>
32     ///每周的周几执行33     /// </summary>
34     public int DaysOfWeek { get; set; }35 
36 
37 }

View Code


1 /// <summary>
2         ///根据数字1-7来设置周一 - 周日3         /// </summary>
4         /// <param name="num"></param>
5         /// <returns></returns>
6         static DaysOfTheWeek GetDaysOfTheWeek(intnum)7 {8             switch(num)9 {10                 case 1:11                     returnDaysOfTheWeek.Monday;12                 case 2:13                     returnDaysOfTheWeek.Tuesday;14                 case 3:15                     returnDaysOfTheWeek.Wednesday;16                 case 4:17                     returnDaysOfTheWeek.Thursday;18                 case 5:19                     returnDaysOfTheWeek.Friday;20                 case 6:21                     returnDaysOfTheWeek.Saturday;22                 case 7:23                     returnDaysOfTheWeek.Sunday;24                 default:25                     throw new ArgumentException("无效的数字。请提供1到7之间的数字。");26 }27 }28         /// <summary>
29         ///限定每月只能在1-28号之间执行任务30         /// </summary>
31         /// <param name="num"></param>
32         /// <returns></returns>
33         static int GetDaysOfMonth(intnum)34 {35             if (num < 1 || num > 28)36 {37                 throw new ArgumentException("无效的数字。请提供1到28之间的数字。");38 }39             returnnum;40         }

两个范围判定的方法


1 static void Main(string[] args)2 {3 
4 TaskSchedulerConfig taskSchedulerConfig;5             try
6 {7                 var taskSchedulerConfigJson = args[0];8                 taskSchedulerConfig = JsonConvert.DeserializeObject<TaskSchedulerConfig>(taskSchedulerConfigJson);9 }10             catch
11 {12                 string demo = @"{13 ""ApplicationPath"": ""C:\\YourApplication\\YourExecutable.exe"",14 ""TaskName"": ""DailyTask"",15 ""TaskDescription"": ""This is a daily task"",16 ""TaskExecutionCycle"": 0,17 ""StartBoundary"": ""2024/02/28 08:00:00"",18 ""DaysOfMonth"": 28,19 ""DaysOfWeek"": 320 }";21 
22                 Console.WriteLine($"参数错误,正确示例:{demo}");23                 return;24 }25 
26 
27             try
28 {29                 using (TaskService taskService = newTaskService())30 {31                     //创建一个新任务
32                     TaskDefinition taskDefinition =taskService.NewTask();33                     taskDefinition.Principal.UserId = "SYSTEM"; //设置为SYSTEM以在系统级别运行34                     //设置任务的基本信息
35                     taskDefinition.RegistrationInfo.Description =taskSchedulerConfig.TaskDescription;36 
37                     switch(taskSchedulerConfig.TaskExecutionCycle)38 {39                         case 0:40                             DailyTrigger dailyTrigger = newDailyTrigger();41                             dailyTrigger.StartBoundary =taskSchedulerConfig.StartBoundary;42                             dailyTrigger.DaysInterval = 1; //每天执行
43 taskDefinition.Triggers.Add(dailyTrigger);44                             break;45                         case 1:46                             WeeklyTrigger weeklyTrigger = newWeeklyTrigger();47                             weeklyTrigger.StartBoundary =taskSchedulerConfig.StartBoundary;48                             weeklyTrigger.DaysOfWeek =GetDaysOfTheWeek(taskSchedulerConfig.DaysOfWeek);49                             weeklyTrigger.WeeksInterval = 1; //每隔1周执行
50 taskDefinition.Triggers.Add(weeklyTrigger);51                             break;52                         case 2:53                             MonthlyTrigger monthlyTrigger = newMonthlyTrigger();54                             monthlyTrigger.StartBoundary =taskSchedulerConfig.StartBoundary;55                             monthlyTrigger.DaysOfMonth = new[] { GetDaysOfMonth(taskSchedulerConfig.DaysOfMonth) };56                             monthlyTrigger.MonthsOfYear = MonthsOfTheYear.AllMonths; //每个月执行
57 taskDefinition.Triggers.Add(monthlyTrigger);58                             break;59                         default:60                             Console.WriteLine("任务执行周期参数错误!正确示例:0 每日执行 1 每周执行 2 每月执行");61                             break;62 }63 
64 
65 
66                     //设置要执行的程序路径
67                     taskDefinition.Actions.Add(new ExecAction(taskSchedulerConfig.ApplicationPath, null, null));68 
69                     //将任务注册到 Windows 任务计划程序
70 taskService.RootFolder.RegisterTaskDefinition(taskSchedulerConfig.TaskName, taskDefinition);71                     Console.WriteLine("任务计划创建成功!");72 }73 }74             catch(Exception ex)75 {76                 Console.WriteLine($"创建{taskSchedulerConfig.TaskName} 任务错误,错误原因是:{ex.Message}");77 }78 
79            
80         }

Main

大家好,我是三友~~

在很久之前,我写过两篇关于OpenFeign和Ribbon这两个SpringCloud核心组件架构原理的文章

但是说实话,从我现在的角度来看,这两篇文章的结构和内容其实还可以更加完善

刚好我最近打算整个SpringCloud各个组件架构原理的小册子

所以趁着这个机会,我就来重新写一下这两篇文章,弥补之前文章的不足

这一篇文章就先来讲一讲OpenFeign的核心架构原理

整篇文章大致分为以下四个部分的内容:

第一部分,脱离于SpringCloud,原始的Feign是什么样的?

第二部分,Feign的核心组件有哪些,整个执行链路是什么样的?

第三部分,SpringCloud是如何把Feign融入到自己的生态的?

第四部分,OpenFeign有几种配置方式,各种配置方式的优先级是什么样的?

好了,话不多说,接下来就直接进入主题,来探秘OpenFeign核心架构原理


原始Feign是什么样的?

在日常开发中,使用Feign很简单,就三步

第一步:引入依赖

 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
     <version>2.2.5.RELEASE</version>
</dependency>

第二步:在启动引导类加上
@EnableFeignClients
注解

@SpringBootApplication
@EnableFeignClients
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.classargs);
    }

}

第三步:写个FeignClient接口

@FeignClient(name = "order")
@RequestMapping("/order")
public interface OrderApiClient {

    @GetMapping
    Order queryOrder(@RequestParam("orderId") Long orderId);

}

之后当我们要使用时,只需要注入
OrderApiClient
对象就可以了

虽然使用方便,但这并不是Feign最原始的使用方式,而是SpringCloud整合Feign之后的使用方式

Feign最开始是由Netflix开源的

后来SpringCloud就将Feign进行了一层封装,整合到自己的生态,让Feign使用起来更加简单

并同时也给它起了一个更高级的名字,OpenFeign

接下来文章表述有时可能并没有严格区分Feign和OpenFeign的含义,你知道是这么个意思就行了。

Feign本身有自己的使用方式,也有类似Spring MVC相关的注解,如下所示:

public interface OrderApiClient {

    @RequestLine("GET /order/{orderId}")
    Order queryOrder(@Param("orderId") Long orderId);

}

OrderApiClient对象需要手动通过
Feign.builder()
来创建

public class FeignDemo {

    public static void main(String[] args) {
        OrderApiClient orderApiClient = Feign.builder()
                .target(OrderApiClient.class, "http://localhost:8088");
        orderApiClient.queryOrder(9527L);
    }

}


Feign的本质:动态代理 + 七大核心组件

相信稍微了解Feign的小伙伴都知道,Feign底层其实是基于JDK动态代理来的

所以
Feign.builder()
最终构造的是一个代理对象

Feign在构建动态代理的时候,会去解析方法上的注解和参数

获取Http请求需要用到基本参数以及和这些参数和方法参数的对应关系

比如Http请求的url、请求体是方法中的第几个参数、请求头是方法中的第几个参数等等

之后在构建Http请求时,就知道请求路径以及方法的第几个参数对应是Http请求的哪部分数据

当调用动态代理方法的时候,Feign就会将上述解析出来的Http请求基本参数和方法入参组装成一个Http请求

然后发送Http请求,获取响应,再根据响应的内容的类型将响应体的内容转换成对应的类型

这就是Feign的大致原理

在整个Feign动态代理生成和调用过程中,需要依靠Feign的一些核心组件来协调完成

如下图所示是Feign的一些核心组件

这些核心组件可以通过
Feign.builder()
进行替换

由于组件很多,这里我挑几个重要的跟大家讲一讲


1、Contract

前面在说Feign在构建动态代理的时候,会去解析方法上的注解和参数,获取Http请求需要用到基本参数

而这个Contract接口的作用就是用来干解析这件事的

Contract的默认实现是解析Feign自己原生注解的

解析时,会为每个方法生成一个MethodMetadata对象

MethodMetadata就封装了Http请求需要用到基本参数以及这些参数和方法参数的对应关系

SpringCloud在整合Feign的时候,为了让Feign能够识别Spring MVC的注解,所以就自己实现了Contract接口


2、Encoder

通过名字也可以看出来,这个其实用来编码的

具体的作用就是将请求体对应的方法参数序列化成字节数组

Feign默认的Encoder实现只支持请求体对应的方法参数类型为String和字节数组

如果是其它类型,比如说请求体对应的方法参数类型为
AddOrderRequest.class
类型,此时就无法对
AddOrderRequest
对象进行序列化

这就导致默认情况下,这个Encoder的实现很难用

于是乎,Spring就实现了Encoder接口

可以将任意请求体对应的方法参数类型对象序列化成字节数组


3、Decoder

Decoder的作用恰恰是跟Encoder相反

Encoder是将请求体对应的方法参数序列化成字节数组

而Decoder其实就是将响应体由字节流反序列化成方法返回值类型的对象

Decoder默认情况下跟Encoder的默认情况是一样的,只支持反序列化成字节数组或者是String

所以,Spring也同样实现了Decoder,扩展它的功能

可以将响应体对应的字节流反序列化成任意返回值类型对象


4、Client

从接口方法的参数和返回值其实可以看出,这其实就是动态代理对象最终用来执行Http请求的组件

默认实现就是通过JDK提供的HttpURLConnection来的

除了这个默认的,Feign还提供了基于HttpClient和OkHttp实现的

在项目中,要想替换默认的实现,只需要引入相应的依赖,在构建
Feign.builder()
时设置一下就行了

SpringCloud环境底下会根据引入的依赖自动进行设置

除了上述的三个实现,最最重要的当然是属于它基于
负载均衡
的实现

如下是OpenFeign用来整合Ribbon的核心实现

这个Client会根据服务名,从Ribbon中获取一个服务实例的信息,也就是ip和端口

之后会通过ip和端口向服务实例发送Http请求


5、InvocationHandlerFactory

InvocationHandler我相信大家应该都不陌生

对于JDK动态代理来说,必须得实现InvocationHandler才能创建动态代理

InvocationHandler的invoke方法实现就是动态代理走的核心逻辑

而InvocationHandlerFactory其实就是创建InvocationHandler的工厂

所以,这里就可以猜到,通过InvocationHandlerFactory创建的InvocationHandler应该就是Feign动态代理执行的核心逻辑

InvocationHandlerFactory默认实现是下面这个

SpringCloud环境下默认也是使用它的这个默认实现

所以,我们直接去看看InvocationHandler的实现类FeignInvocationHandler

从实现可以看出,除了Object类的一些方法,最终会调用方法对应的MethodHandler的invoke方法

所以注意注意,这个MethodHandler就
封装了Feign执行Http调用的核心逻辑
,很重要,后面还会提到

虽然说默认情况下SpringCloud使用是默认实现,最终使用FeignInvocationHandler

但是当其它框架整合SpringCloud生态的时候,为了适配OpenFeign,有时会自己实现InvocationHandler

比如常见的限流熔断框架Hystrix和Sentinel都实现了自己的InvocationHandler

这样就可以对MethodHandler执行前后,也就是Http接口调用前后进行限流降级等操作。


6、RequestInterceptor

RequestInterceptor它其实是一个在发送请求前的一个拦截接口

通过这个接口,在发送Http请求之前再对Http请求的内容进行修改

比如我们可以设置一些接口需要的公共参数,如鉴权token之类的

@Component
public class TokenRequestInterceptor implements RequestInterceptor {

    @Override
    public void apply(RequestTemplate template) {
        template.header("token""token值");
    }

}


7、Retryer

这是一个重试的组件,默认实现如下

默认情况下,最大重试5次

在SpringCloud下,并没有使用上面那个实现,而使用的是下面这个实现

所以,SpringCloud下默认是不会进行重试


小总结

这一节主要是介绍了7个Feign的核心组件以及Spring对应的扩展实现

为了方便你查看,我整理了如下表格

接口 作用 Feign默认实现 Spring实现
Contract 解析方法注解和参数,将Http请求参数和方法参数对应 Contract.Default SpringMvcContract
Encoder 将请求体对应的方法参数序列化成字节数组 Encoder.Default SpringEncoder
Decoder 将响应体的字节流反序列化成方法返回值类型对象 Decoder.Default SpringDecoder
Client 发送Http请求 Client.Default LoadBalancerFeignClient
InvocationHandlerFactory InvocationHandler工厂,动态代理核心逻辑 InvocationHandlerFactory.Default
RequestInterceptor 在发送Http请求之前,再对Http请求的内容进行拦截修改
Retryer 重试组件 Retryer.Default

除了这些之外,还有一些其它组件这里就没有说了

比如日志级别Logger.Level,日志输出Logger,有兴趣的可以自己查看


Feign核心运行原理分析

上一节说了Feign核心组件,这一节我们来讲一讲Feign核心运行原理,主要分为两部分内容:

  • 动态代理生成原理
  • 一次Feign的Http调用执行过程


1、动态代理生成原理

这里我先把上面的Feign原始使用方式的Demo代码再拿过来

public class FeignDemo {

    public static void main(String[] args) {
        OrderApiClient orderApiClient = Feign.builder()
                .target(OrderApiClient.class, "http://localhost:8088");
        orderApiClient.queryOrder(9527L);
    }

}

通过Demo可以看出,最后是通过
Feign.builder().target(xx)
获取到动态代理的

而上述代码执行逻辑如下所示:

最终会调用ReflectiveFeign的newInstance方法来创建动态代理对象

而ReflectiveFeign内部设置了前面提到的一些核心组件

接下我们来看看newInstance方法

这个方法主要就干两件事:

第一件事首先解析接口,构建每个方法对应的MethodHandler

MethodHandler在前面讲InvocationHandlerFactory特地提醒过

动态代理(FeignInvocationHandler)最终会调用MethodHandler来处理Feign的一次Http调用

在解析接口的时候,就会用到前面提到的Contract来解析方法参数和注解,生成MethodMetadata,这里我代码我就不贴了

第二件事通过InvocationHandlerFactory创建InvocationHandler

然后再构建出接口的动态代理对象

ok,到这其实就走完了动态代理的生成过程

所以动态代理生成逻辑很简单,总共也没几行代码,画个图来总结一下


2、一次Feign的Http调用执行过程

前面说了,调用接口动态代理的方式时,通过InvocationHandler(FeignInvocationHandler),最终交给MethodHandler的invoke方法来执行

MethodHandler是一个接口,最终会走到它的实现类SynchronousMethodHandler的invoke方法实现

SynchronousMethodHandler中的属性就是我们前面提到的一些组件

由于整个代码调用执行链路比较长,这里我就不截代码了,有兴趣的可以自己翻翻

不过这里我画了一张图,可以通过这张图来大致分析整个Feign一次Http调用的过程

  • 首先就是前面说的,进入FeignInvocationHandler,找到方法对应的SynchronousMethodHandler,调用invoke方法实现
  • 之后根据MethodMetadata和方法的入参,构造出一个RequestTemplate,RequestTemplate封装了Http请求的参数,在这个过程中,如果有请求体,那么会通过Encoder序列化
  • 然后调用RequestInterceptor,通过RequestInterceptor对RequestTemplate进行拦截扩展,可以对请求数据再进行修改
  • 再然后将RequestTemplate转换成Request,Request其实跟RequestTemplate差不多,也是封装了Http请求的参数
  • 接下来通过Client去根据Request中封装的Http请求参数,发送Http请求,得到响应Response
  • 最后根据Decoder,将响应体反序列化成方法返回值类型对象,返回

这就是Feign一次Http调用的执行过程

如果有设置重试,那么也是在这个阶段生效的


SpringCloud是如何整合Feign的?

SpringCloud在整合Feign的时候,主要是分为两部分

  • 核心组件重新实现,支持更多SpringCloud生态相关的功能
  • 将接口动态代理对象注入到Spring容器中

第一部分核心组件重新实现前面已经都说过了,这里就不再重复了

至于第二部分我们就来好好讲一讲,Spring是如何将接口动态代理对象注入到Spring容器中的


1、将FeignClient接口注册到Spring中

使用OpenFeign时,必须加上
@EnableFeignClients

这个注解就是OpenFeign的发动机

@EnableFeignClients
最后通过
@Import
注解导入了一个
FeignClientsRegistrar

FeignClientsRegistrar
实现了
ImportBeanDefinitionRegistrar

所以最终Spring在启动的时候会调用
registerBeanDefinitions
方法实现

之所以会调用
registerBeanDefinitions
方法,是
@Import
注解的作用,不清楚的同学可以看一下
扒一扒Bean注入到Spring的那些姿势,你会几种?

最终会走到
registerFeignClients
这个方法

这个方法虽然比较长,主要是干了下面这个2件事:

第一件事,扫描
@EnableFeignClients
所在类的包及其子包(如果有指定包就扫指定包),找出所有加了
@FeignClient
注解的接口,生成一堆BeanDefinition

这个BeanDefinition包含了这个接口的信息等信息

第二件事,将扫描到的这些接口注册到Spring容器中

在注册的时候,并非直接注册接口类型,而是
FeignClientFactoryBean
类型

好了,到这整个
@EnableFeignClients
启动过程就结束了

虽然上面写的很长,但是整个
@EnableFeignClients
其实也就只干了一件核心的事

扫描到所有的加了
@FeignClient
注解的接口

然后为每个接口生成一个Bean类型为
FeignClientFactoryBean
的BeanDefinition

最终注册到Spring容器中


2、FeignClientFactoryBean的秘密

上一节说到,每个接口都对应一个class类型为
FeignClientFactoryBean
的BeanDefinition

如上所示,
FeignClientFactoryBean
是一个FactoryBean

并且
FeignClientFactoryBean
的这些属性,是在生成BeanDefinition的时候设置的

并且这个type属性就是代表的接口类型

由于实现了FactoryBean,所以Spring启动过程中,一定为会调用
getObject
方法获取真正的Bean对象

FactoryBean的作用就不说了,不清楚的小伙伴还是可以看看
扒一扒Bean注入到Spring的那些姿势,你会几种?
这篇文章

getObject
最终会走到
getTarget()
方法

从如上代码其实可以看出来,最终还是会通过
Feign.builder()
来创建动态代理对象

只不过不同的是,SpringCloud会替换Feign默认的组件,改成自己实现的

总的来说,Spring是通过FactoryBean的这种方式,将Feign动态代理对象添加到Spring容器中


OpenFeign的各种配置方式以及对应优先级

既然Feign核心组件可以替换,那么在SpringCloud环境下,我们该如何去配置自己的组件呢?

不过在说配置之前,先说一下FeignClient配置隔离操作

在SpringCloud环境下,为了让每个不同的FeignClient接口配置相互隔离

在应用启动的时候,会为每个FeignClient接口创建一个Spring容器,接下来我就把这个容器称为FeignClient容器

这些FeignClient的Spring容器有一个相同的父容器,那就是项目启动时创建的容器

SpringCloud会给
每个
FeignClient容器添加一个默认的配置类
FeignClientsConfiguration
配置类

这个配置类就声明了各种Feign的组件

所以,默认情况下,OpenFeign就使用这些配置的组件构建代理对象

知道配置隔离之后,接下来看看具体的几种方式配置以及它们之间的优先级关系


1、通过@EnableFeignClients注解的defaultConfiguration属性配置

举个例子,比如我自己手动声明一个
Contract
对象,类型为
MyContract

public class FeignConfiguration {
    
    @Bean
    public Contract contract(){
        return new MyContract();
    }
    
}

注意注意,这里
FeignConfiguration
我没加
@Configuration
注解,原因后面再说

此时配置如下所示:

@EnableFeignClients(defaultConfiguration = FeignConfiguration.class)

之后这个配置类会被加到
每个FeignClient容器
中,所以这个配置是对所有的FeignClient生效

并且优先级大于默认配置的优先级

比如这个例子就会使得FeignClient使用我声明的
MyContract
,而不是
FeignClientsConfiguration
中声明的
SpringMvcContract


2、通过@FeignClient注解的configuration属性配置

还以上面的
FeignConfiguration
配置类举例,可以通过
@FeignClient
注解配置

@FeignClient(name = "order", configuration = FeignConfiguration.class)

此时这个配置类会被加到
自己FeignClient容器
中,注意是自己FeignClient容器

所以这种配置的作用范围是
自己的这个FeignClient

并且
这种配置的优先级是大于
@EnableFeignClients
注解配置的优先级


3、在项目启动的容器中配置

前面提到,由于所有的FeignClient容器的父容器都是项目启动的容器

所以可以将配置放在这个项目启动的容器中

还以
FeignConfiguration
为例,加上
@Configuration
注解,让项目启动的容器的扫描到就成功配置了

这种配置的
优先级大于前面提到的所有配置优先级

并且
是对所有的FeignClient生效

所以,这就是为什么使用注解配置时为什么配置类不能加
@Configuration
注解的原因,因为一旦被项目启动的容器扫描到,这个配置就会作用于所有的FeignClient,并且优先级是最高的,就会导致你其它的配置失效,当然你也可以加
@Configuration
注解,但是一定不能被项目启动的容器扫到


4、配置文件

除了上面3种编码方式配置,OpenFeign也是支持通过配置文件的方式进行配置

并且也同时支持对所有FeignClient生效和对单独某个FeignClient生效

对所有FeignClient生效配置:

feign:
  client:
    config:
      default: # default 代表对全局生效
        contract: com.sanyou.feign.MyContract

对单独某个FeignClient生效配置:

feign:
  client:
    config:
      order: # 具体的服务名
        contract: com.sanyou.feign.MyContract

在默认情况下,这种配置文件方式优先级最高

但是如果你在配置文件中将配置项
feign.client.default-to-properties
设置成
false
的话,配置文件的方式优先级就是最低了

feign:
  client:
    default-to-properties: false


小总结

这一节,总共总结了4种配置OpenFeign的方式以及它们优先级和作用范围

画张图来总结一下

如果你在具体使用的时候,还是遇到了一些优先级的问题,可以debug这部分源码,看看到底生效的是哪个配置


总结

到这,总算讲完了OpenFeign的核心架构原理了

这又是一篇洋洋洒洒的万字长文

由于OpenFeign它只是一个框架,并没有什么复杂的机制

所以整篇文章还是更多偏向源码方面

不知道你看起来感觉如何

如果你感觉还不错,欢迎点赞、在看、收藏、转发分享给其他需要的人

你的支持就是我更新的最大动力,感谢感谢!

更多SpringCloud系列的文章,可以在公众号后台菜单栏中查看。

好了,本文就讲到这里,让我们下期再见,拜拜!


往期热门文章推荐

如何去阅读源码,我总结了18条心法

如何写出漂亮代码,我总结了45个小技巧

三万字盘点Spring/Boot的那些常用扩展点

三万字盘点Spring 9大核心基础功能

两万字盘点那些被玩烂了的设计模式

万字+20张图探秘Nacos注册中心核心实现原理

万字+20张图剖析Spring启动时12个核心步骤

1.5万字+30张图盘点索引常见的11个知识点

扫码或者搜索关注公众号
三友的java日记
,及时干货不错过,公众号致力于通过画图加上通俗易懂的语言讲解技术,让技术更加容易学习,回复 面试 即可获得一套面试真题。

一、测试运行python项目

1.1 Flask项目

说明1:当我们直接用编译器运行Flask项目的时候,会有一个提示:意思就是:这是开发环境的服务器,不能用于生产环境的部署,请使用WSGI的服务器替换

1.2 Django项目

说明2:当我们直接用编译器运行Django项目的时候,同样有个提示,这是一个开发环境的服务器,意思就是同样不能用于生产环境

1.3 总结

    • Flask自带一个服务器,主要用在开发环境。默认情况下一次只能处理一个请求,处理能力比较有限。
    • Django 运行 runserver 命令可以直接作为服务器使用,但是也是单线程的。性能差,如果出现错误挂掉。那么整个进程都挂掉了
    • 所以我们在生产环境要,就要使用安全稳定效率高,性能强的服务器。
    • 之所以Flask和Django直接运行不能用于生产环境是因为Flask和Django是web开发框架,而不是web服务器,web框架开发的时候,只是为了方便测试,提供了一个测试服务器而已。

二、web服务器

通过上面的例子可以看出,如果让我们开发出来的项目能得到成千上万,几十万,甚至几百万几千万的用户访问的话,我们需要一个强劲有力的web服务器为我们保驾护航。
web服务器即用来接受客户端请求,建立连接,转发响应的程序。至于转发的内容是什么,交由web框架来处理,即处理这些业务逻辑。如查询数据库、生成实时信息等。我常用的Nginx就是一个web服务器,
Django

flask
就是web框架。

web服务器的特点:

    • 接受客户端(浏览器,app,小程序等)发送的请求
    • web服务器通常都会支持高并发(Nginx上万级别的并发)

所以我们在部署python项目的时候,都需要再额外部署一个web服务器,主要作用就是给我们的应用提供一个高并发安全的互联网访问环境,通常我们使用Nginx作为我们项目的web服务器。

三、Nginx服务器

nginx是一款轻量级的web 服务器,反向代理服务器及电子邮件(imap/pop3)代理服务器。其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好,大部分网站都有使用nginx。

nginx的有点:

  1. 支持高并发,能够支持高达 50,000 个并发连接数的响应,这样我们的网站并发性就很棒。
  2. 负载均衡,当访问量还是多的时候,可以同时开启多个应用服务器,进行负责均衡
  3. 提供静态服务,动静分离,减少静态资源访问对应用服务器的压力
  4. 反向代理

nginx在部署的时候可以给我的应用服务器,提供一个高并发,高可靠性,安全的网络访问环境,通过Nginx可以接收到用户通过浏览器或者app等前端的方式访问到我们的网站。

注意:这里Nginx只能帮助我们接收客户端(浏览器,app,小程序等)的请求,Nginx并不具备处理请求的能力。例如:用户发送了一个获取用户信息的请求,Nginx并没有用户的信息,所以Nginx并不能直接处理用户的请求,所以Nginx需要将用户的这个请求转发给我们的应用服务器(Flask或者Django程序等),但是,这里注意!注意!注意!,Nginx和我们Web框架(Flask/Django)也不能直接通信,他们的协议不一样(通俗的讲就是一个说英语的,一个说汉语的,谁也听不懂谁),这是就需要找一个翻译,既能读懂Nginx的协议,也能读懂python的web框架协议的,而且还要高效,这个东西就是WSGI。

四、WSGI和uWSGI

WSGI
是一种通信协议。WSGI 不是框架,也不是一个模块,而是介于 Web应用程序(Flask/Django)与 Web Server (Nginx)之间交互的一种规范。

uWSGI
是一个Web服务器,它实现了WSGI协议、uwsgi、http等协议。Nginx中HttpUwsgiModule的作用是与uWSGI服务器进行交换。

所以我们使用uWSGI充当Nginx和我们的程序(Flask/Django)之间的桥梁,这也就是为什么我们部署python项目需要uWSGI和Nginx的原因了

五、通过Nginx和uWSGi请求响应全过程

介绍

Java内部类是一种特殊的类,它定义在另一个类的内部。内部类提供了许多有用的特性,包括访问外部类的私有成员、隐藏实现细节以及实现回调接口等。以下是Java内部类的一些常用场景及其举例说明:

回调接口

当某个类需要一个对象来执行其方法时,通常可以使用回调接口。这时,内部类可以作为一个匿名实现类,简化代码。
在这个例子中,ActionListener是一个回调接口,Button类通过ActionListener来执行某个动作。在Main类中,我们使用了一个匿名内部类来实现ActionListener接口。

interface ActionListener {  
    void actionPerformed();  
}  
  
class Button {  
    private ActionListener listener;  
  
    public void setActionListener(ActionListener listener) {  
        this.listener = listener;  
    }  
  
    public void click() {  
        if (listener != null) {  
            listener.actionPerformed();  
        }  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        Button button = new Button();  
        button.setActionListener(new ActionListener() {  
            @Override  
            public void actionPerformed() {  
                System.out.println("Button clicked!");  
            }  
        });  
        button.click();  
    }  
}

访问外部类的私有成员

内部类可以访问外部类的私有成员,这使得内部类成为了一种封装和隐藏实现细节的有效手段。在这个例子中,Inner类可以访问Outer类的私有成员secret。

class Outer {  
    private int secret = 42;  
  
    class Inner {  
        void revealSecret() {  
            System.out.println("The secret is: " + secret);  
        }  
    }  
  
    public Inner getInner() {  
        return new Inner();  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        Outer outer = new Outer();  
        Outer.Inner inner = outer.getInner();  
        inner.revealSecret();  // 输出 "The secret is: 42"  
    }  
}

实现多继承

Java不支持多继承,但内部类可以帮助我们实现类似的效果。

interface A {  
    void methodA();  
}  
  
interface B {  
    void methodB();  
}  
  
class MyClass implements A {  
    private class InnerB implements B {  
        @Override  
        public void methodB() {  
            System.out.println("Method B from InnerB");  
        }  
    }  
  
    private InnerB innerB = new InnerB();  
  
    @Override  
    public void methodA() {  
        System.out.println("Method A from MyClass");  
    }  
  
    public void methodB() {  
        innerB.methodB();  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        MyClass myClass = new MyClass();  
        myClass.methodA();  // 输出 "Method A from MyClass"  
        myClass.methodB();  // 输出 "Method B from InnerB"  
    }  
}

在这个例子中,MyClass实现了接口A,并通过内部类InnerB实现了接口B。这样,MyClass就具有了A和B两个接口的行为。

数据隐藏

内部类可以将实现细节隐藏在外部类内部,从而提供一个更简洁、更易于使用的API。

class DataHolder {  
    private int data;  
  
    public DataHolder(int data) {  
        this.data = data;  
    }  
  
    class DataProcessor {  
        void processData() {  
            // 在这里可以对data进行复杂的处理  
            System.out.println("Processed data: " + data);  
        }  
    }  
  
    public DataProcessor getProcessor() {  
        return new DataProcessor();  
    }  
}  
  
public class Main {  
    public static void main(String[] args) {  
        DataHolder holder = new DataHolder(42);  
        DataHolder.DataProcessor processor = holder.getProcessor();  
        processor.processData();  // 输出 "Processed data: 42"  
    }  
}

在这个例子中,DataProcessor内部类隐藏了对data的处理细节,外部类只需要调用getProcessor()方法即可。

总之,Java内部类在回调接口、访问外部类私有成员、实现多继承和数据隐藏等场景中非常有用。