C#反射实现插件式开发 - 码农阿亮 - 博客园

mikel阅读(391)

来源: C#反射实现插件式开发 – 码农阿亮 – 博客园

前言

插件式架构,一种全新的、开放性的、高扩展性的架构体系。插件式架构设计好处很多,把扩展功能从框架中剥离出来,降低了框架的复杂度,让框架更容易实现。扩展功能与框架以一种很松的方式耦合,两者在保持接口不变的情况下,可以独立变化和发布。基于插件设计并不神秘,相反它比起一团泥的设计更简单,更容易理解。

项目介绍

书写4个插件类库,分别传参实现“加减乘除”运算,调用插件的客户端采用Winform窗体程序。

目标框架:.NET Framework 4.6.1

项目架构和窗体布局:

image

客户端程序:

  • PluginApp:反射调用插件

插件描述:

  • PluginBase:规范插件的基类,定义抽象类,开发的插件的类需要继承此类,代表遵守这个规范。
  • CustomPlugInA:实现加法的插件
  • CustomPlugInB:实现减法的插件
  • CustomPlugInC:实现乘法的插件
  • CustomPlugInD:实现除法的插件

代码实现

插件基类
 /// <summary>
    ///插件基类
    /// </summary>
    public abstract class Base
    {
        /// <summary>
        /// 插件名称
        /// </summary>
        /// <returns></returns>
        public abstract string Name();
       /// <summary>
       /// 插件描述
       /// </summary>
       /// <returns></returns>
        public abstract string Desc();
        /// <summary>
        /// 执行方法
        /// </summary>
        /// <param name="param1">参数1</param>
        /// <param name="param2">参数2</param>
        /// <returns></returns>
        public abstract string Run(int param1, int param2);
        /// <summary>
        /// 版本 
        /// </summary>
        public string Version
        {
            get { return "1.0.0"; }
        }
    }
PlugInA
    public class PlugInA: Base
    {

        public override string Name()
        {
            return "PlugInA";
        }

        public override string Desc()
        {
            return "加法";
        }

        public override string Run(int param1,int param2)
        {
            return (param1 + param2) + "";
        }
    }
}
PlugInB
    public class PlugInB : Base
    {

        public override string Name()
        {
            return "PlugInB";
        }

        public override string Desc()
        {
            return "减法";
        }

        public override string Run(int param1, int param2)
        {
            return (param1 - param2) + "";
        }
    }
PlugInC
  public class PlugInC : Base
    {

        public override string Name()
        {
            return "PlugInC";
        }

        public override string Desc()
        {
            return "乘法";
        }

        public override string Run(int param1, int param2)
        {
            return (param1 * param2) + "";
        }
    }
PlugInD
 public class PlugInD : Base
    {

        public override string Name()
        {
            return "PlugInD";
        }

        public override string Desc()
        {
            return "除法";
        }

        public override string Run(int param1, int param2)
        {
            return (param1 / param2) + "";
        }
    }
客户端核心代码:
   public partial class FrmMain : Form
    {
        public FrmMain()
        {
            InitializeComponent();
            dgrvPlugins.AutoGenerateColumns = false;
        }

        List<PluginModel> List = new List<PluginModel>();

        readonly string PlugInPath = Application.StartupPath + "\\PlugIns";

        /// <summary>
        /// 载入插件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btLoadPlugins_Click(object sender, EventArgs e)
        {

            if (!Directory.Exists(PlugInPath))
            {
                Directory.CreateDirectory(PlugInPath);
            }
            List.Clear();
            string[] files = Directory.GetFiles(PlugInPath);
            foreach (string file in files)
            {
                if (file.ToLower().EndsWith(".dll"))
                {
                    try
                    {
                        Assembly assembly = Assembly.LoadFrom(file);
                        Type[] types = assembly.GetTypes();
                        foreach (Type type in types)
                        {
                            if (type.BaseType.FullName == "PlugInBase.Base")
                            {
                                object obj = assembly.CreateInstance(type.FullName);
                                string name = type.GetMethod("Name").Invoke(obj, null).ToString();
                                string desc = type.GetMethod("Desc").Invoke(obj, null).ToString();
                                string version = type.GetProperty("Version").GetValue(obj).ToString();

                                List.Add(new PluginModel
                                {
                                    Name = name,
                                    Desc = desc,
                                    Version = version,
                                    type = type,
                                    Obj = obj
                                });
                            }
                        }
                    }
                    catch (Exception ex)
                    {
                        throw ex;
                    }
                }
            }

            dgrvPlugins.DataSource = new BindingList<PluginModel>(List);
        }

        /// <summary>
        /// 打开插件目录
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btOpenPluginDir_Click(object sender, EventArgs e)
        {
            Process.Start(PlugInPath);
        }


        /// <summary>
        /// 执行选中插件
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btExcute_Click(object sender, EventArgs e)
        {
            //获取选择的插件信息
            int index = dgrvPlugins.CurrentRow.Index;
            object obj = List[index].Obj;
            Type type = List[index].type;
            //参数
            object[] inParams = new object[2];
            inParams[0] =Convert.ToInt32( dgrvPlugins.CurrentRow.Cells[2].Value);
            inParams[1] = Convert.ToInt32(dgrvPlugins.CurrentRow.Cells[3].Value);
            object value = type.GetMethod("Run").Invoke(obj, inParams);
            MessageBox.Show(Convert.ToString(value),"结果",MessageBoxButtons.OK);
        }
    }

项目配置

插件生成配置

编译生成项目的时候需要注意,此处的调用插件是通过反射调用.dll中类和方法,所以首先要找到这个.dll的文件,所以此处我们在Winform客户端程序下建立一个存放类库dll的文件PlugIns,在插件类库项目生成后事件命令中,填入如下命令:

copy /Y "$(TargetDir)$(ProjectName).dll" "$(SolutionDir)\PlugIns"

以上命令代表,在项目的类库生成后,将类库copy到解决方案的路径子文件夹PlugIns,也就是我们建立存放自定义插件的文件夹。当然,如果不怕麻烦,每次生成后,手动复制到此文件夹也可以,直接复制到客户端程序的..\bin\PlugIns文件夹下。

image

插件路径配置

全选这些类库,把这些类库设置为”如果较新则复制”,这样每次在编译客户端程序,如果自定义插件有更新,则同步会复制到bin目录下

image

插件基类配置

插件基类提供了规范,需要在类库的生成后事件,添加命令:

copy /Y "$(TargetDir)$(ProjectName).dll" "$(SolutionDir)\bin\Debug"

将生成的dll文件,拷贝到客户端程序的bin路径下

image

调用演示

CustomPlugInA

image

CustomPlugInB

image

CustomPlugInC

image

CustomPlugInD

image

插件开发优缺点

优点
  • 把扩展功能从框架中剥离出来,降低了框架的复杂度,让框架更容易实现
  • 宿主中可以对各个模块解析,完成插件间、插件和主程序间的通信。
  • 插件开发的可扩展性,灵活性比较高,而且可以进行定制化开发。
缺点
  • 每一个插件被编译成了dll,各模块无法单独运行,必须依托于主程序。
  • 修改插件时,由于生成的是dll,无法快速直观的查看修改以及调试。
  • 每一个插件必须依赖于某一个规范。

源码

百度网盘

链接:https://pan.baidu.com/s/1Ev1s14IvggrCVBvbu-NW7g?pwd=mnal
提取码:mnal

码云

https://gitee.com/mingliang_it/PlugInsApp.git

Llama2-Chinese项目:2.1-Atom-7B预训练 - 扫地升 - 博客园

mikel阅读(328)

来源: Llama2-Chinese项目:2.1-Atom-7B预训练 – 扫地升 – 博客园

虽然Llama2的预训练数据相对于第一代LLaMA扩大了一倍,但是中文预训练数据的比例依然非常少,仅占0.13%,这也导致了原始Llama2的中文能力较弱。为了能够提升模型的中文能力,可以采用微调和预训练两种路径,其中:

  • 微调需要的算力资源少,能够快速实现一个中文Llama的雏形。但缺点也显而易见,只能激发基座模型已有的中文能力,由于Llama2的中文训练数据本身较少,所以能够激发的能力也有限,治标不治本。
  • 基于大规模中文语料进行预训练,成本高,不仅需要大规模高质量的中文数据,也需要大规模的算力资源。但是优点也显而易见,就是能从模型底层优化中文能力,真正达到治本的效果,从内核为大模型注入强大的中文能力。

下面从主要目标、训练数据、权重更新、数据转换和预处理、任务类型、示例应用和典型场景7个方面进行比较,如下所示(ChatGPT):

特征 Pretraining(预训练) Continuous Pretraining(继续预训练) Fine-tuning(微调) Post-Pretrain(预训练之后)
主要目标 学习通用的表示 在通用表示上继续学习 在特定任务上调整模型 在预训练之后的额外学习和任务
训练数据 大规模文本数据集 额外的文本数据集 特定任务的数据集 额外的优化、领域适应或任务迁移
权重更新 固定模型参数,不进行权重更新 继续更新模型参数 在任务数据上进行权重更新 针对特定需求进行权重更新
数据转换和预处理 通常包括数据标准化、掩码预测等 与预训练相似的预处理 根据任务需求进行调整 针对特定需求进行数据处理和优化
任务类型 无监督学习、自监督学习 通常是自监督学习 监督学习 可以包括优化、领域适应、任务迁移等多种任务
示例应用 BERT、GPT等 额外的预训练 文本分类、命名实体识别等 模型优化、领域适应、多任务学习等
典型场景 语言理解和生成 继续模型学习 特定文本任务 后续步骤和任务,用于定制和优化模型

说明:本文环境为Windows10,Python3.10,CUDA 11.8,GTX 3090(24G),内存24G。

一.模型预训练脚本
模型预训练脚本中的参数较多,只能在实践中来消化了。因为用的Windows10系统,所以运行shell脚本较麻烦,这部分不做过多介绍。如下所示:

train/pretrain/pretrain.sh  
output_model=/mnt/data1/atomgpt                                  # output_model:输出模型路径
if [ ! -d ${output_model} ];then                                 # -d:判断是否为目录,如果不是目录则创建
    mkdir ${output_model}                                        # mkdir:创建目录
fi
cp ./pretrain.sh ${output_model}                                 # cp:复制文件pretrain.sh到output_model目录下
cp ./ds_config_zero*.json ${output_model}                        # cp:复制文件ds_config_zero*.json到output_model目录下

deepspeed --num_gpus 1 pretrain_clm.py \                         # deepspeed:分布式训练,num_gpus:使用的gpu数量,pretrain_clm.py:训练脚本
    --model_name_or_path L:/20230903_Llama2/Llama-2-7b-hf \      # model_name_or_path:模型名称或路径
    --train_files ../../data/train_sft.csv \                     # train_files:训练数据集路径
                ../../data/train_sft_sharegpt.csv \
    --validation_files  ../../data/dev_sft.csv \                 # validation_files:验证数据集路径
                         ../../data/dev_sft_sharegpt.csv \
    --per_device_train_batch_size 10 \                           # per_device_train_batch_size:每个设备的训练批次大小
    --per_device_eval_batch_size 10 \                            # per_device_eval_batch_size:每个设备的验证批次大小
    --do_train \                                                 # do_train:是否进行训练
    --output_dir ${output_model} \                               # output_dir:输出路径
    --evaluation_strategy  steps \                               # evaluation_strategy:评估策略,steps:每隔多少步评估一次
    --use_fast_tokenizer false \                                 # use_fast_tokenizer:是否使用快速分词器
    --max_eval_samples 500 \                                     # max_eval_samples:最大评估样本数,500:每次评估500个样本
    --learning_rate 3e-5 \                                       # learning_rate:学习率
    --gradient_accumulation_steps 4 \                            # gradient_accumulation_steps:梯度累积步数
    --num_train_epochs 3 \                                       # num_train_epochs:训练轮数
    --warmup_steps 10000 \                                       # warmup_steps:预热步数
    --logging_dir ${output_model}/logs \                         # logging_dir:日志路径
    --logging_strategy steps \                                   # logging_strategy:日志策略,steps:每隔多少步记录一次日志
    --logging_steps 2 \                                          # logging_steps:日志步数,2:每隔2步记录一次日志
    --save_strategy steps \                                      # save_strategy:保存策略,steps:每隔多少步保存一次
    --preprocessing_num_workers 10 \                             # preprocessing_num_workers:预处理工作数
    --save_steps 500 \                                           # save_steps:保存步数,500:每隔500步保存一次
    --eval_steps 500 \                                           # eval_steps:评估步数,500:每隔500步评估一次
    --save_total_limit 2000 \                                    # save_total_limit:保存总数,2000:最多保存2000个
    --seed 42 \                                                  # seed:随机种子
    --disable_tqdm false \                                       # disable_tqdm:是否禁用tqdm
    --ddp_find_unused_parameters false \                         # ddp_find_unused_parameters:是否找到未使用的参数
    --block_size 4096 \                                          # block_size:块大小
    --overwrite_output_dir \                                     # overwrite_output_dir:是否覆盖输出目录
    --report_to tensorboard \                                    # report_to:报告给tensorboard
    --run_name ${output_model} \                                 # run_name:运行名称
    --bf16 \                                                     # bf16:是否使用bf16
    --bf16_full_eval \                                           # bf16_full_eval:是否使用bf16进行完整评估
    --gradient_checkpointing \                                   # gradient_checkpointing:是否使用梯度检查点
    --deepspeed ./ds_config_zero3.json \                         # deepspeed:分布式训练配置文件
    --ignore_data_skip true \                                    # ignore_data_skip:是否忽略数据跳过
    --ddp_timeout 18000000 \                                     # ddp_timeout:ddp超时时间,18000000:18000000毫秒
    | tee -a ${output_model}/train.log                           # tee:将标准输出重定向到文件,-a:追加到文件末尾
    
    # --resume_from_checkpoint ${output_model}/checkpoint-20400 \# resume_from_checkpoint:从检查点恢复训练

 

二.预训练实现代码
Llama中文社区供了Llama模型的预训练代码,以及中文语料(参考第六部分)。本文在meta发布的Llama-2-7b基础上进行预训练,pretrain_clm.py代码的中文注释参考[0],执行脚本如下所示:

python pretrain_clm.py --output_dir ./output_model  --model_name_or_path L:/20230903_Llama2/Llama-2-7b-hf  --train_files ../../data/train_sft.csv ../../data/train_sft_sharegpt.csv  --validation_files  ../../data/dev_sft.csv ../../data/dev_sft_sharegpt.csv --do_train --overwrite_output_dir  

说明:使用GTX 3090 24G显卡,还是报了OOM错误,但是并不影响调试学习,输出日志参考[2]。
1.代码结构

(1)ModelArguments:模型参数类
(2)DataTrainingArguments:数据训练参数类
(3)TrainingArguments:训练参数类
2.model_args, data_args, training_args = parser.parse_args_into_dataclasses()
解析:加载模型参数、数据训练参数和训练参数,如下所示:

3.raw_datasets = load_dataset(...)
解析:加载原始数据集,如下所示:

4.config = AutoConfig.from_pretrained(model_args.model_name_or_path, **config_kwargs)
解析:加载config,如下所示:

5.tokenizer = AutoTokenizer.from_pretrained(model_args.model_name_or_path, **tokenizer_kwargs)
解析:加载tokenizer,如下所示:

6.model = AutoModelForCausalLM.from_pretrained()
解析:加载model,这一步非常耗时,如下所示:

7.tokenized_datasets = raw_datasets.map()
解析:原始数据集处理,比如编码等,如下所示:

8.trainer = Trainer()
解析:实例化一个trainer,用于后续的训练或评估。跑到这一步的时候就报OOM了。如下所示:

trainer = Trainer( # 训练器
    model=model, # 模型
    args=training_args, # 训练参数
    train_dataset= IterableWrapper(train_dataset) if training_args.do_train else None, # 训练数据集
    eval_dataset= IterableWrapper(eval_dataset) if training_args.do_eval else None, # 评估数据集
    tokenizer=tokenizer, # 分词器
    # Data collator will default to DataCollatorWithPadding, so we change it.
    # 翻译:数据收集器将默认为DataCollatorWithPadding,因此我们将其更改。
    data_collator=default_data_collator, # 默认数据收集器
    compute_metrics=compute_metrics if training_args.do_eval and not is_torch_tpu_available() else None, # 计算指标
    preprocess_logits_for_metrics=preprocess_logits_for_metrics if training_args.do_eval and not is_torch_tpu_available() else None, # 为指标预处理logits
    # callbacks=([SavePeftModelCallback] if isinstance(model, PeftModel) else None),
)

说过:阅读pretrain_clm.py代码有几个疑问:中文词表是什么扩展的?上下文长度如何扩增?如何预训练text数据?后续再写文章分享。

三.DeepSpeed加速
DeepSpeed是一个由微软开发的开源深度学习优化库,旨在提高大规模模型训练的效率和可扩展性。它通过多种技术手段来加速训练,包括模型并行化、梯度累积、动态精度缩放、本地模式混合精度等。DeepSpeed还提供了一些辅助工具,如分布式训练管理、内存优化和模型压缩等,以帮助开发者更好地管理和优化大规模深度学习训练任务。此外,deepspeed基于pytorch构建,只需要简单修改即可迁移。DeepSpeed已经在许多大规模深度学习项目中得到了应用,包括语言模型、图像分类、目标检测等等。
DeepSpeed主要包含三部分:

  • Apis:提供易用的api接口,训练模型、推理模型只需要简单调用几个接口即可。其中最重要的是initialize接口,用来初始化引擎,参数中配置训练参数及优化技术等。配置参数一般保存在config.json文件中。
  • Runtime:运行时组件,是DeepSpeed管理、执行和性能优化的核心组件。比如部署训练任务到分布式设备、数据分区、模型分区、系统优化、微调、故障检测、checkpoints保存和加载等。该组件使用Python语言实现。
  • Ops:用C++和CUDA实现底层内核,优化计算和通信,比如ultrafast transformer kernels、fuse LAN kernels、cusomary deals等。

1.Windows10安装DeepSpeed
解析:管理员启动cmd:

build_win.bat
python setup.py bdist_wheel

2.安装编译工具
在Visual Studio Installer中勾选”使用C++的桌面开发”,如下所示:

3.error C2665: torch::empty: 没有重载函数可以转换所有参数类型

解决办法如下所示:

4.元素”1″: 从”size_t”转换为”_Ty”需要收缩转换
解析:具体错误如下所示:

csrc/transformer/inference/csrc/pt_binding.cpp(536): error C2398: 元素"1": 从"size_t"转换为"_Ty"需要收缩转换


解析方案如下所示:

536:hidden_dim * (unsigned)InferenceContext
537:k * (int)InferenceContext
545:hidden_dim * (unsigned)InferenceContext
546:k * (int)InferenceContext
1570input.size(1), (int)mlp_1_out_neurons


编译成功如下所示:

5.安装类库

PS L:\20230903_Llama2\whl文件\DeepSpeed\dist> pip3 install .\deepspeed-0.10.4+180dd397-cp310-cp310-win_amd64.whl

说明:由于DeepSpeed在Windows操作不友好,这部分只做学习使用。
6.单卡训练和多卡训练
(1)对于单卡训练,可以采用ZeRO-2的方式,参数配置见train/pretrain/ds_config_zero2.json

{
    "fp16": {                      // 混合精度训练
        "enabled": "auto",         // 是否开启混合精度训练
        "loss_scale": 0,           // 损失缩放
        "loss_scale_window": 1000, // 损失缩放窗口
        "initial_scale_power": 16, // 初始损失缩放幂
        "hysteresis": 2,           // 滞后
        "min_loss_scale": 1        // 最小损失缩放
    },
    "optimizer": {                 // 优化器
        "type": "AdamW",           // 优化器类型
        "params": {                // 优化器参数
            "lr": "auto",          // 学习率
            "betas": "auto",       // 衰减因子
            "eps": "auto",         // 除零保护
            "weight_decay": "auto" // 权重衰减
        }
    },

    "scheduler": {                       // 学习率调度器
        "type": "WarmupDecayLR",         // 调度器类型
        "params": {                      // 调度器参数
            "last_batch_iteration": -1,  // 最后批次迭代
            "total_num_steps": "auto",   // 总步数
            "warmup_min_lr": "auto",     // 最小学习率
            "warmup_max_lr": "auto",     // 最大学习率
            "warmup_num_steps": "auto"   // 热身步数
        }
    },

    "zero_optimization": {              // 零优化
        "stage": 2,                     // 零优化阶段
        "offload_optimizer": {          // 优化器卸载
            "device": "cpu",            // 设备
            "pin_memory": true          // 锁页内存
        },
        "offload_param": {              // 参数卸载
            "device": "cpu",            // 设备
            "pin_memory": true          // 锁页内存
        },
        "allgather_partitions": true,   // 全收集分区
        "allgather_bucket_size": 5e8,   // 全收集桶大小
        "overlap_comm": true,           // 重叠通信
        "reduce_scatter": true,         // 减少散射
        "reduce_bucket_size": 5e8,      // 减少桶大小
        "contiguous_gradients": true    // 连续梯度
    },
    "activation_checkpointing": {                   // 激活检查点
        "partition_activations": false,             // 分区激活
        "cpu_checkpointing": false,                 // CPU检查点
        "contiguous_memory_optimization": false,    // 连续内存优化
        "number_checkpoints": null,                 // 检查点数量
        "synchronize_checkpoint_boundary": false,   // 同步检查点边界
        "profile": false                            // 档案
    },
    "gradient_accumulation_steps": "auto",          // 梯度累积步骤
    "gradient_clipping": "auto",                    // 梯度裁剪
    "steps_per_print": 2000,                        // 每次打印步骤
    "train_batch_size": "auto",                     // 训练批次大小
    "min_lr": 5e-7,                                 // 最小学习率
    "train_micro_batch_size_per_gpu": "auto",       // 每个GPU的训练微批次大小
    "wall_clock_breakdown": false                   // 墙上时钟分解
}

(2)对于多卡训练,可以采用ZeRO-3的方式,参数配置见train/pretrain/ds_config_zero3.json

{
    "fp16": {                         // 混合精度训练
        "enabled": "auto",            // 是否开启混合精度训练
        "loss_scale": 0,              // 损失缩放
        "loss_scale_window": 1000,    // 损失缩放窗口
        "initial_scale_power": 16,    // 初始缩放幂
        "hysteresis": 2,              // 滞后
        "min_loss_scale": 1,          // 最小损失缩放
        "fp16_opt_level": "O2"        // 混合精度优化级别
    },

    "bf16": {                         // 混合精度训练
        "enabled": "auto"             // 是否开启混合精度训练
    }, 

    "optimizer": {                    // 优化器
        "type": "AdamW",              // 优化器类型
        "params": {                   // 优化器参数
            "lr": "auto",             // 学习率
            "betas": "auto",          // 衰减因子
            "eps": "auto",            // 除零保护
            "weight_decay": "auto"    // 权重衰减
        }
    },

    "scheduler": {                       // 学习率调度器
        "type": "WarmupDecayLR",         // 学习率调度器类型
        "params": {                      // 学习率调度器参数
            "last_batch_iteration": -1,  // 最后批次迭代
            "total_num_steps": "auto",   // 总步数
            "warmup_min_lr": "auto",     // 最小学习率
            "warmup_max_lr": "auto",     // 最大学习率
            "warmup_num_steps": "auto"   // 热身步数
        }
    },

    "zero_optimization": {                                 // 零优化
        "stage": 3,                                        // 零优化阶段
        "overlap_comm": true,                              // 重叠通信
        "contiguous_gradients": true,                      // 连续梯度
        "sub_group_size": 1e9,                             // 子组大小
        "reduce_bucket_size": "auto",                      // 减少桶大小
        "stage3_prefetch_bucket_size": "auto",             // 阶段3预取桶大小
        "stage3_param_persistence_threshold": "auto",      // 阶段3参数持久性阈值
        "stage3_max_live_parameters": 1e9,                 // 阶段3最大活动参数
        "stage3_max_reuse_distance": 1e9,                  // 阶段3最大重用距离
        "gather_16bit_weights_on_model_save": true         // 在模型保存时收集16位权重
    },
    "gradient_accumulation_steps": "auto",                 // 梯度累积步数
    "gradient_clipping": "auto",                           // 梯度裁剪
    "steps_per_print": 2000,                               // 每次打印步数
    "train_batch_size": "auto",                            // 训练批次大小
    "train_micro_batch_size_per_gpu": "auto",              // 训练每个GPU的微批次大小
    "wall_clock_breakdown": false                          // 墙上时钟分解
}

ZeRO-2和ZeRO-3间的比较如下所示(ChatGPT):

特征 Zero2(0.2版本) Zero3(0.3版本)
内存占用优化
动态计算图支持 不支持 支持
性能优化 一般 更好
模型配置选项 有限 更多
分布式训练支持
具体应用 非动态计算图模型 动态计算图模型

 

四.训练效果度量指标
accuracy.py代码的中文注释参考[1],主要在预训练评估的时候用到了该文件,如下所示:

train/pretrain/accuracy.py  
metric = evaluate.load("accuracy.py") # 加载指标

 

五.中文测试语料
中文测试语料数据格式如下所示:

<s>Human: 问题</s><s>Assistant: 答案</s>

多轮语料将单轮的拼接在一起即可,如下所示:

<s>Human: 内容1\n</s><s>Assistant: 内容2\n</s><s>Human: 内容3\n</s><s>Assistant: 内容4\n</s>


Llama2-Chinese项目中提供的train和dev文件共有3个,如下所示:

data\dev_sft.csv  
data\dev_sft_sharegpt.csv  
data\train_sft.csv  

更多的语料可从Llama中文社区(https://llama.family/)链接下载:

六.中文语料
Atom-7B是一个基于Llama2架构的预训练语言模型,Llama中文社区将基于大规模中文语料,从预训练开始对Llama2模型进行中文能力的持续迭代升级。通过以下数据来优化Llama2的中文能力:

类型 描述
网络数据 互联网上公开的网络数据,挑选出去重后的高质量中文数据,涉及到百科、书籍、博客、新闻、公告、小说等高质量长文本数据。
Wikipedia 中文Wikipedia的数据
悟道 中文悟道开源的200G数据
Clue Clue开放的中文预训练数据,进行清洗后的高质量中文长文本数据
竞赛数据集 近年来中文自然语言处理多任务竞赛数据集,约150个
MNBVC MNBVC 中清洗出来的部分数据集

说明:除了网络数据和竞赛数据集这2个没有提供链接,其它的4个都提供了数据集的链接。

参考文献:
[0]https://github.com/ai408/nlp-engineering/blob/main/20230916_Llama2-Chinese/train/pretrain/pretrain_clm.py
[1]https://github.com/ai408/nlp-engineering/blob/main/20230916_Llama2-Chinese/train/pretrain/accuracy.py
[2]https://github.com/ai408/nlp-engineering/blob/main/20230916_Llama2-Chinese/train/pretrain/pretrain_log/pretrain_log
[3]https://huggingface.co/meta-llama/Llama-2-7b-hf/tree/main
[4]https://huggingface.co/spaces/ysharma/Explore_llamav2_with_TGI
[5]https://huggingface.co/meta-llama/Llama-2-70b-chat-hf
[6]https://huggingface.co/blog/llama2
[7]https://developer.nvidia.com/rdp/cudnn-download
[8]https://github.com/jllllll/bitsandbytes-windows-webui
[9]https://github.com/langchain-ai/langchain
[10]https://github.com/AtomEcho/AtomBulb
[11]https://github.com/huggingface/peft
[12]全参数微调时,报没有target_modules变量:https://github.com/FlagAlpha/Llama2-Chinese/issues/169
[13]https://huggingface.co/FlagAlpha
[14]Win10安装DeepSpeed:https://zhuanlan.zhihu.com/p/636450918

SQL查询中的小技巧:SELECT 1 和 LIMIT 1 替代 count(*) - 程序员济癫 - 博客园

mikel阅读(341)

来源: SQL查询中的小技巧:SELECT 1 和 LIMIT 1 替代 count(*) – 程序员济癫 – 博客园

前言

在写SQL查询时,常规做法是使用SELECT count(*)来统计符合条件的记录数。

然而,在某些情况下,我们只关心是否存在符合条件的记录,而不需要知道具体的记录数。

为了优化性能,可以改用使用SELECT 1LIMIT 1的方式查询。

在业务代码中,直接判断查询结果是否非空即可,不再需要使用count来获取记录数。

实战

我们使用Java和MyBatis演示优化方案的代码示例。

假设我们有一个名为User的数据库表,其中包含idnameage字段。我们想要检查是否存在年龄大于等于18岁的用户。

  1. 创建一个UserMapper接口,定义一个方法来执行查询操作:
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
    Integer existUsersWithAgeGreaterThan(int age);
}

  1. UserMapper.xml中实现这个方法,使用优化的SQL语句:
<?xml version="1.0" encoding="UTF-8"?>
<mapper namespace="com.example.mapper.UserMapper">

    <select id="existUsersWithAgeGreaterThan" resultType="java.lang.Integer">
        SELECT 1 FROM users WHERE age >= #{age} LIMIT 1
    </select>

</mapper>

  1. 然后,在业务代码中调用existUsersWithAgeGreaterThan方法进行判断:
import com.example.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private final UserMapper userMapper;

    @Autowired
    public UserService(UserMapper userMapper) {
        this.userMapper = userMapper;
    }

    public void checkUsersWithAgeGreaterThan(int age) {
        Integer exist = userMapper.existUsersWithAgeGreaterThan(age);
        if (exist != null) {
            // 当存在满足条件的用户时,执行这里的代码
            System.out.println("存在符合条件的用户");
        } else {
            // 当不存在满足条件的用户时,执行这里的代码
            System.out.println("不存在符合条件的用户");
        }
    }
}

通过调用existUsersWithAgeGreaterThan方法获取查询结果,并根据结果是否为空来判断是否存在满足条件的用户。

优化

既然讲到这里,也就顺便提一下数据库查询的一般性能调优做法,在数据量十分庞大的情况下,这里给出几点建议:

  1. 索引优化:确保在查询条件列上存在适当的索引,以加快查询速度。比如当前的示例,可以在age列上创建索引;
  2. 分页查询:如果只关心是否存在满足条件的记录,但不需要具体的记录内容,可以考虑使用分页查询的方式进行优化。通过限制返回结果的数量,可以减少查询的开销;
  3. 缓存机制:如果查询结果相对稳定或者经常被重复查询,可以考虑使用缓存来避免重复的数据库查询操作,从而提高响应速度,比如把前几页缓存下来,因为很多用户可能不会一直往后点;
  4. 数据库调优:对数据库进行性能调优,包括优化查询计划、调整内存设置、合理配置数据库连接池等,可以提升整体查询性能。

总结

其实案例中的优化方案在查询结果集较大时已经非常有效,可以说是肉眼可见的性能提升,在某些情况下还可以减少联合索引的创建。

总而言之,通过使用SELECT 1LIMIT 1代替SELECT count(*),可以提高查询性能并简化业务代码,特别适用于仅需判断是否存在符合条件的记录的场景。

好了,今天这个小知识,你学会了吗?

每日一练:无感刷新页面(附可运行的前后端源码,前端vue,后端node) - 菜鸟小何 - 博客园

mikel阅读(315)

来源: 每日一练:无感刷新页面(附可运行的前后端源码,前端vue,后端node) – 菜鸟小何 – 博客园

1、前言

想象下,你正常在网页上浏览页面。突然弹出一个窗口,告诉你登录失效,跳回了登录页面,让你重新登录。你是不是很恼火。这时候无感刷新的作用就体现出来了。

2、方案

2.1 redis设置过期时间

在最新的技术当中,token一般都是在Redis服务器存着,设置过期时间。只要在有效时间内,重新发出请求,Redis中的过期时间会去更新,这样前只需要一个token。这个方案一般是后端做。

2.2 双token模式

2.21 原理

  • 用户登录向服务端发送账号密码,登录失败返回客户端重新登录。登录成功服务端生成 accessToken 和 refreshToken,返回生成的 token 给客户端。
  • 在请求拦截器中,请求头中携带 accessToken 请求数据,服务端验证 accessToken 是否过期。token 有效继续请求数据,token 失效返回失效信息到客户端。
  • 客户端收到服务端发送的请求信息,在二次封装的 axios 的响应拦截器中判断是否有 accessToken 失效的信息,没有返回响应的数据。有失效的信息,就携带 refreshToken 请求新的 accessToken。
  • 服务端验证 refreshToken 是否有效。有效,重新生成 token, 返回新的 token 和提示信息到客户端,无效,返回无效信息给客户端。
  • 客户端响应拦截器判断响应信息是否有 refreshToken 有效无效。无效,退出当前登录。有效,重新存储新的 token,继续请求上一次请求的数据。

2.22 上代码

后端:node.js、koa2服务器、jwt、koa-cors等(可使用koa脚手架创建项目,本项目基于koa脚手架创建。完整代码可见文章末尾github地址)

  1. 新建utils/token.js (双token)
const jwt=require('jsonwebtoken')

const secret='2023F_Ycb/wp_sd'  // 密钥
/*
expiresIn:5 过期时间,时间单位是秒
也可以这么写 expiresIn:1d 代表一天
1h 代表一小时
*/
// 本次是为了测试,所以设置时间 短token5秒 长token15秒
const accessTokenTime=5
const refreshTokenTime=15

// 生成accessToken
const accessToken=(payload={})=>{  // payload 携带用户信息
    return jwt.sign(payload,secret,{expiresIn:accessTokenTime})
}
//生成refreshToken
const refreshToken=(payload={})=>{
    return jwt.sign(payload,secret,{expiresIn:refreshTokenTime})
}

module.exports={
    secret,
    accessToken,
    refreshToken
}
  1. router/index.js 创建路由接口
const router = require('koa-router')()
const jwt = require('jsonwebtoken')
const { accessToken, refreshToken, secret }=require('../utils/token')
router.get('/', async (ctx, next) => {
  await ctx.render('index', {
    title: 'Hello Koa 2!'
  })
})

router.get('/string', async (ctx, next) => {
  ctx.body = 'koa2 string'
})

router.get('/json', async (ctx, next) => {
  ctx.body = {
    title: 'koa2 json'
  }
})
/*登录接口*/
router.get('/login',(ctx)=>{
  let code,msg,data=null
  code=2000
  msg='登录成功,获取到token'
  data={
    accessToken:accessToken(),
    refreshToken:refreshToken()
  }
  ctx.body={
    code,
    msg,
    data
  }
})

/*用于测试的获取数据接口*/
router.get('/getTestData',(ctx)=>{
  let code,msg,data=null
  code=2000
  msg='获取数据成功'
  ctx.body={
    code,
    msg,
    data
  }
})

/*验证长token是否有效,刷新短token
  这里要注意,在刷新短token的时候回也返回新的长token,延续长token,
  这样活跃用户在持续操作过程中不会被迫退出登录。长时间无操作的非活
  跃用户长token过期重新登录
*/
router.get('/refresh',(ctx)=>{

  let code,msg,data=null
  //获取请求头中携带的长token
  let r_tk=ctx.request.headers['pass']
  //解析token 参数 token 密钥 回调函数返回信息
  jwt.verify(r_tk,secret,(error)=>{
    if(error){
      code=4006,
      msg='长token无效,请重新登录'
    }
    else{
      code = 2000,
      msg = '长token有效,返回新的token'
      data = {
        accessToken: accessToken(),
        refreshToken: refreshToken()
      }
    }
    ctx.body={
      code,
      msg:msg?msg:null,
      data
    }
  })
})



module.exports = router

3.新建utils/auth.js (中间件)

const { secret } = require('./token')
const jwt = require('jsonwebtoken')

/*白名单,登录、刷新短token不受限制,也就不用token验证*/
const whiteList=['/login','/refresh']
const isWhiteList=(url,whiteList)=>{
    return whiteList.find(item => item === url) ? true : false
}

/*中间件
 验证短token是否有效
*/
const auth = async (ctx,next)=>{
    let code, msg, data = null
    let url = ctx.path
    if(isWhiteList(url,whiteList)){
        // 执行下一步
        return await next()
    } else {
        // 获取请求头携带的短token
        const a_tk=ctx.request.headers['authorization']
        if(!a_tk){
            code=4003
            msg='accessToken无效,无权限'
            ctx.body={
                code,
                msg,
                data
            }
        } else{
            // 解析token
            await jwt.verify(a_tk,secret,async (error)=>{
                if(error){
                    code=4003
                    msg='accessToken无效,无权限'
                    ctx.body={
                        code,
                        msg,
                        data
                    }
                } else {
                    // token有效
                    return await next()
                }
            })
        }
    }
}
module.exports=auth
  1. app.js
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
const cors=require('koa-cors')

const index = require('./routes/index')
const users = require('./routes/users')
const auth=require('./utils/auth')

// error handler
onerror(app)

// middlewares
app.use(bodyparser({
  enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(cors())
app.use(auth)

app.use(views(__dirname + '/views', {
  extension: 'pug'
}))

// logger
app.use(async (ctx, next) => {
  const start = new Date()
  await next()
  const ms = new Date() - start
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})

// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())

// error-handling
app.on('error', (err, ctx) => {
  console.error('server error', err, ctx)
});

module.exports = app

前端:vite、vue3、axios等 (完整代码可见文章末尾github地址)

  1. 新建config/constants.js
export const ACCESS_TOKEN = 'a_tk' // 短token字段
export const REFRESH_TOKEN = 'r_tk' // 短token字段
export const AUTH = 'Authorization'  // header头部 携带短token
export const PASS = 'pass' // header头部 携带长token
  1. 新建config/storage.js
import * as constants from "./constants"

// 存储短token
export const setAccessToken = (token) => localStorage.setItem(constants.ACCESS_TOKEN, token)
// 存储长token
export const setRefreshToken = (token) => localStorage.setItem(constants.REFRESH_TOKEN, token)
// 获取短token
export const getAccessToken = () => localStorage.getItem(constants.ACCESS_TOKEN)
// 获取长token
export const getRefreshToken = () => localStorage.getItem(constants.REFRESH_TOKEN)
// 删除短token
export const removeAccessToken = () => localStorage.removeItem(constants.ACCESS_TOKEN)
// 删除长token
export const removeRefreshToken = () => localStorage.removeItem(constants.REFRESH_TOKEN)

3.新建utils/refresh.js

export {REFRESH_TOKEN,PASS} from '../config/constants.js'
import { getRefreshToken, removeRefreshToken, setAccessToken, setRefreshToken} from '../config/storage'
import server from "./server";

let subscribes=[]
let flag=false // 设置开关,保证一次只能请求一次短token,防止客户多此操作,多次请求

/*把过期请求添加在数组中*/
export const addRequest = (request) => {
    subscribes.push(request)
}

/*调用过期请求*/
export const retryRequest = () => {
    console.log('重新请求上次中断的数据');
    subscribes.forEach(request => request())
    subscribes = []
}

/*短token过期,携带token去重新请求token*/
export const refreshToken=()=>{
    console.log('flag--',flag)
    if(!flag){
        flag = true;
        let r_tk = getRefreshToken() // 获取长token
        if(r_tk){
            server.get('/refresh',Object.assign({},{
                headers:{PASS : r_tk}
            })).then((res)=>{
                //长token失效,退出登录
                if(res.code===4006){
                    flag = false
                    removeRefreshToken(REFRESH_TOKEN)
                } else if(res.code===2000){
                    // 存储新的token
                    setAccessToken(res.data.accessToken)
                    setRefreshToken(res.data.refreshToken)
                    flag = false
                    // 重新请求数据
                    retryRequest()
                }
            })
        }
    }
}

4.新建utils/server.js

import axios from "axios";
import * as storage from "../config/storage"
import * as constants from '../config/constants'
import { addRequest, refreshToken } from "./refresh";

const server = axios.create({
    baseURL: 'http://localhost:3000', // 你的服务器
    timeout: 1000 * 10,
    headers: {
        "Content-type": "application/json"
    }
})

/*请求拦截器*/
server.interceptors.request.use(config => {
    // 获取短token,携带到请求头,服务端校验
    let aToken = storage.getAccessToken(constants.ACCESS_TOKEN)
    config.headers[constants.AUTH] = aToken
    return config
})

/*响应拦截器*/
server.interceptors.response.use(
    async response => {
        // 获取到配置和后端响应的数据
        let { config, data } = response
        console.log('响应提示信息:', data.msg);
        return new Promise((resolve, reject) => {
            // 短token失效
            if (data.code === 4003) {
                // 移除失效的短token
                storage.removeAccessToken(constants.ACCESS_TOKEN)
                // 把过期请求存储起来,用于请求到新的短token,再次请求,达到无感刷新
                addRequest(() => resolve(server(config)))
                // 携带长token去请求新的token
                refreshToken()
            } else {
                // 有效返回相应的数据
                resolve(data)
            }

        })

    },
    error => {
        return Promise.reject(error)
    }
)
export default  server

5.新建apis/index.js

import server from "../utils/server.js";
/*登录*/
export const login = () => {
    return server({
        url: '/login',
        method: 'get'
    })
}
/*请求数据*/
export const getList = () => {
    return server({
        url: '/getTestData',
        method: 'get'
    })
}

6.修改App.vue

<script setup>
  import {login,getList} from "./apis";
  import {setAccessToken,setRefreshToken} from "./config/storage";
  const getToken=()=>{
    login().then(res=>{
      setAccessToken(res.data.accessToken)
      setRefreshToken(res.data.refreshToken)
    })
  }
  const getData = ()=>{
    getList()
  }
</script>

<template>
    <button @click="getToken">登录</button>
    <button @click="getData">请求数据</button>

</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
  transition: filter 300ms;
}
.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

2.23 效果图

3、完整项目代码

3.1 地址

https://github.com/heyu3913/doubleToken

3.2 运行

后端:

cd server
pnpm i
pnpm start

前端

cd my-vue-app
pnpm i
pnpm dev

4 PS: 这里附送大家一个免费的gpt地址,自己搭的,不收费。注册即用:

https://www.hangyejingling.cn

.NET Core 实现Excel的导入导出 - TomLucas - 博客园

mikel阅读(312)

来源: .NET Core 实现Excel的导入导出 – TomLucas – 博客园

 

前言

我们在日常开发中对Excel的操作可能会比较频繁,好多功能都会涉及到Excel的操作。在.Net Core中大家可能使用Npoi比较多,这款软件功能也十分强大,而且接近原始编程。但是直接使用Npoi大部分时候我们可能都会自己封装一下,毕竟根据二八原则,我们百分之八十的场景可能都是进行简单的导入导出操作,这里就引出我们的主角Npoi。

NPOI简介

NPOI是指构建在POI 3.x版本之上的一个程序,NPOI可以在没有安装Office的情况下对Word或Excel文档进行读写操作。NPOI是一个开源的C#读写Excel、WORD等微软OLE2组件文档的项目。

一、安装相对应的程序包

在 .Net Core 中使用NPOI首先必须先安装NPOI;如下图所示:
右键项目,选择“管理 NuGet 程序包”菜单

1.1、在 “管理NuGet程序包” 中的浏览搜索:“NPOI”

相关安装包的安装
点击安装以上两个即可,安装完成之后最好重新编译一下项目以防出错

二、新建Excel帮助类

在项目中新建“ExcelHelper”类;此类用于封装导入导出以及其他配置方法。代码如下:

using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using NPOI;
using System.Text;
using NPOI.HSSF.UserModel;
using NPOI.XSSF.UserModel;
using NPOI.SS.Formula.Eval;
using NPOI.SS.UserModel;
using NPOI.SS.Util;
using System.Text.RegularExpressions;
using System.Reflection;
using System.Collections;
using NPOI.HSSF.Util;

namespace WebApplication1 //命名空间依据自己的项目进行修改
{
    /// <summary>
    /// Excel帮助类
    /// 功能:
    ///   1、导出数据到Excel文件中
    ///   2、将Excel文件的数据导入到List<T>对象集合中
    /// </summary>
    public static class ExcelHelper
    {
        /// <summary>
        /// 导出列名
        /// </summary>
        public static SortedList ListColumnsName;

        #region 从DataTable导出到excel文件中,支持xls和xlsx格式

        #region 导出为xls文件内部方法

        /// <summary>
        /// 从DataTable 中导出到excel
        /// </summary>
        /// <param name="strFileName">excel文件名</param>
        /// <param name="dtSource">datatabe源数据</param>
        /// <param name="strHeaderText">表名</param>
        /// <param name="sheetnum">sheet的编号</param>
        /// <returns></returns>
        static MemoryStream ExportDT(string strFileName, DataTable dtSource, string strHeaderText, Dictionary<string, string> dir, int sheetnum)
        {
            //创建工作簿和sheet
            IWorkbook workbook = new HSSFWorkbook();
            using (Stream writefile = new FileStream(strFileName, FileMode.OpenOrCreate, FileAccess.Read))
            {
                if (writefile.Length > 0 && sheetnum > 0)
                {
                    workbook = WorkbookFactory.Create(writefile);
                }
            }
            ISheet sheet = null;
            ICellStyle dateStyle = workbook.CreateCellStyle();
            IDataFormat format = workbook.CreateDataFormat();
            dateStyle.DataFormat = format.GetFormat("yyyy-mm-dd");
            int[] arrColWidth = new int[dtSource.Columns.Count];
            foreach (DataColumn item in dtSource.Columns)
            {
                arrColWidth[item.Ordinal] = Encoding.GetEncoding(936).GetBytes(Convert.ToString(item.ColumnName)).Length;
            }
            for (int i = 0; i < dtSource.Rows.Count; i++)
            {
                for (int j = 0; j < dtSource.Columns.Count; j++)
                {
                    int intTemp = Encoding.GetEncoding(936).GetBytes(Convert.ToString(dtSource.Rows[i][j])).Length;
                    if (intTemp > arrColWidth[j])
                    {
                        arrColWidth[j] = intTemp;
                    }
                }
            }
            int rowIndex = 0;
            foreach (DataRow row in dtSource.Rows)
            {
                #region 新建表,填充表头,填充列头,样式
                if (rowIndex == 0)
                {
                    string sheetName = strHeaderText + (sheetnum == 0 ? "" : sheetnum.ToString());
                    if (workbook.GetSheetIndex(sheetName) >= 0)
                    {
                        workbook.RemoveSheetAt(workbook.GetSheetIndex(sheetName));
                    }
                    sheet = workbook.CreateSheet(sheetName);
                    #region 表头及样式
                    {
                        sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, dtSource.Columns.Count - 1));
                        IRow headerRow = sheet.CreateRow(0);
                        headerRow.HeightInPoints = 25;
                        headerRow.CreateCell(0).SetCellValue(strHeaderText);
                        ICellStyle headStyle = workbook.CreateCellStyle();
                        headStyle.Alignment = HorizontalAlignment.Center;
                        IFont font = workbook.CreateFont();
                        font.FontHeightInPoints = 20;
                        font.Boldweight = 700;
                        headStyle.SetFont(font);
                        headerRow.GetCell(0).CellStyle = headStyle;

                        rowIndex = 1;
                    }
                    #endregion

                    #region 列头及样式

                    if (rowIndex == 1)
                    {
                        IRow headerRow = sheet.CreateRow(1);//第二行设置列名
                        ICellStyle headStyle = workbook.CreateCellStyle();
                        headStyle.Alignment = HorizontalAlignment.Center;
                        IFont font = workbook.CreateFont();
                        font.FontHeightInPoints = 10;
                        font.Boldweight = 700;
                        headStyle.SetFont(font);
                        //写入列标题
                        foreach (DataColumn column in dtSource.Columns)
                        {
                            headerRow.CreateCell(column.Ordinal).SetCellValue(dir[column.ColumnName]);
                            headerRow.GetCell(column.Ordinal).CellStyle = headStyle;
                            //设置列宽
                            sheet.SetColumnWidth(column.Ordinal, (arrColWidth[column.Ordinal] + 1) * 256 * 2);
                        }
                        rowIndex = 2;
                    }
                    #endregion
                }
                #endregion

                #region 填充内容

                IRow dataRow = sheet.CreateRow(rowIndex);
                foreach (DataColumn column in dtSource.Columns)
                {
                    NPOI.SS.UserModel.ICell newCell = dataRow.CreateCell(column.Ordinal);
                    string drValue = row[column].ToString();
                    switch (column.DataType.ToString())
                    {
                        case "System.String": //字符串类型
                            double result;
                            if (isNumeric(drValue, out result))
                            {
                                //数字字符串
                                double.TryParse(drValue, out result);
                                newCell.SetCellValue(result);
                                break;
                            }
                            else
                            {
                                newCell.SetCellValue(drValue);
                                break;
                            }

                        case "System.DateTime": //日期类型
                            DateTime dateV;
                            DateTime.TryParse(drValue, out dateV);
                            newCell.SetCellValue(dateV);

                            newCell.CellStyle = dateStyle; //格式化显示
                            break;
                        case "System.Boolean": //布尔型
                            bool boolV = false;
                            bool.TryParse(drValue, out boolV);
                            newCell.SetCellValue(boolV);
                            break;
                        case "System.Int16": //整型
                        case "System.Int32":
                        case "System.Int64":
                        case "System.Byte":
                            int intV = 0;
                            int.TryParse(drValue, out intV);
                            newCell.SetCellValue(intV);
                            break;
                        case "System.Decimal": //浮点型
                        case "System.Double":
                            double doubV = 0;
                            double.TryParse(drValue, out doubV);
                            newCell.SetCellValue(doubV);
                            break;
                        case "System.DBNull": //空值处理
                            newCell.SetCellValue("");
                            break;
                        default:
                            newCell.SetCellValue(drValue.ToString());
                            break;
                    }
                }
                #endregion
                rowIndex++;
            }
            using (MemoryStream ms = new MemoryStream())
            {
                workbook.Write(ms, true);
                ms.Flush();
                ms.Position = 0;
                return ms;
            }
        }

        #endregion

        #region 导出为xlsx文件内部方法

        /// <summary>
        /// 从DataTable 中导出到excel
        /// </summary>
        /// <param name="dtSource">DataTable数据源</param>
        /// <param name="strHeaderText">表名</param>
        /// <param name="fs">文件流</param>
        /// <param name="readfs">内存流</param>
        /// <param name="sheetnum">sheet索引</param>
        static void ExportDTI(DataTable dtSource, string strHeaderText, FileStream fs, MemoryStream readfs, Dictionary<string, string> dir, int sheetnum)
        {
            IWorkbook workbook = new XSSFWorkbook();
            if (readfs.Length > 0 && sheetnum > 0)
            {
                workbook = WorkbookFactory.Create(readfs);
            }
            ISheet sheet = null;
            ICellStyle dateStyle = workbook.CreateCellStyle();
            IDataFormat format = workbook.CreateDataFormat();
            dateStyle.DataFormat = format.GetFormat("yyyy-mm-dd");

            //取得列宽
            int[] arrColWidth = new int[dtSource.Columns.Count];
            foreach (DataColumn item in dtSource.Columns)
            {
                arrColWidth[item.Ordinal] = Encoding.GetEncoding(936).GetBytes(Convert.ToString(item.ColumnName)).Length;
            }
            for (int i = 0; i < dtSource.Rows.Count; i++)
            {
                for (int j = 0; j < dtSource.Columns.Count; j++)
                {
                    int intTemp = Encoding.GetEncoding(936).GetBytes(Convert.ToString(dtSource.Rows[i][j])).Length;
                    if (intTemp > arrColWidth[j])
                    {
                        arrColWidth[j] = intTemp;
                    }
                }
            }
            int rowIndex = 0;

            foreach (DataRow row in dtSource.Rows)
            {
                #region 新建表,填充表头,填充列头,样式

                if (rowIndex == 0)
                {
                    #region 表头及样式
                    {
                        string sheetName = strHeaderText + (sheetnum == 0 ? "" : sheetnum.ToString());
                        if (workbook.GetSheetIndex(sheetName) >= 0)
                        {
                            workbook.RemoveSheetAt(workbook.GetSheetIndex(sheetName));
                        }
                        sheet = workbook.CreateSheet(sheetName);
                        sheet.AddMergedRegion(new CellRangeAddress(0, 0, 0, dtSource.Columns.Count - 1));
                        IRow headerRow = sheet.CreateRow(0);
                        headerRow.HeightInPoints = 25;
                        headerRow.CreateCell(0).SetCellValue(strHeaderText);

                        ICellStyle headStyle = workbook.CreateCellStyle();
                        headStyle.Alignment = HorizontalAlignment.Center;
                        IFont font = workbook.CreateFont();
                        font.FontHeightInPoints = 20;
                        font.Boldweight = 700;
                        headStyle.SetFont(font);
                        headerRow.GetCell(0).CellStyle = headStyle;
                    }
                    #endregion

                    #region 列头及样式
                    {
                        IRow headerRow = sheet.CreateRow(1);
                        ICellStyle headStyle = workbook.CreateCellStyle();
                        headStyle.Alignment = HorizontalAlignment.Center;
                        IFont font = workbook.CreateFont();
                        font.FontHeightInPoints = 10;
                        font.Boldweight = 700;
                        headStyle.SetFont(font);
                        foreach (DataColumn column in dtSource.Columns)
                        {
                            headerRow.CreateCell(column.Ordinal).SetCellValue(dir[column.ColumnName]);
                            headerRow.GetCell(column.Ordinal).CellStyle = headStyle;
                            //设置列宽
                            sheet.SetColumnWidth(column.Ordinal, (arrColWidth[column.Ordinal] + 1) * 256 * 2);
                        }
                    }

                    #endregion

                    rowIndex = 2;
                }
                #endregion

                #region 填充内容
                IRow dataRow = sheet.CreateRow(rowIndex);
                foreach (DataColumn column in dtSource.Columns)
                {
                    NPOI.SS.UserModel.ICell newCell = dataRow.CreateCell(column.Ordinal);
                    string drValue = row[column].ToString();
                    switch (column.DataType.ToString())
                    {
                        case "System.String": //字符串类型
                            double result;
                            if (isNumeric(drValue, out result))
                            {
                                double.TryParse(drValue, out result);
                                newCell.SetCellValue(result);
                                break;
                            }
                            else
                            {
                                newCell.SetCellValue(drValue);
                                break;
                            }
                        case "System.DateTime": //日期类型
                            DateTime dateV;
                            DateTime.TryParse(drValue, out dateV);
                            newCell.SetCellValue(dateV);

                            newCell.CellStyle = dateStyle; //格式化显示
                            break;
                        case "System.Boolean": //布尔型
                            bool boolV = false;
                            bool.TryParse(drValue, out boolV);
                            newCell.SetCellValue(boolV);
                            break;
                        case "System.Int16": //整型
                        case "System.Int32":
                        case "System.Int64":
                        case "System.Byte":
                            int intV = 0;
                            int.TryParse(drValue, out intV);
                            newCell.SetCellValue(intV);
                            break;
                        case "System.Decimal": //浮点型
                        case "System.Double":
                            double doubV = 0;
                            double.TryParse(drValue, out doubV);
                            newCell.SetCellValue(doubV);
                            break;
                        case "System.DBNull": //空值处理
                            newCell.SetCellValue("");
                            break;
                        default:
                            newCell.SetCellValue(drValue.ToString());
                            break;
                    }
                }
                #endregion
                rowIndex++;
            }
            workbook.Write(fs,true);
            fs.Close();
        }

        #endregion

        #region 导出excel表格

        /// <summary>
        ///  DataTable导出到Excel文件,xls文件
        /// </summary>
        /// <param name="dtSource">数据源</param>
        /// <param name="strHeaderText">表名</param>
        /// <param name="strFileName">excel文件名</param>
        /// <param name="dir">DataTable和excel列名对应字典</param>
        /// <param name="sheetRow">每个sheet存放的行数</param>
        public static void ExportDTtoExcel(DataTable dtSource, string strHeaderText, string strFileName, Dictionary<string, string> dir, bool isNew, int sheetRow = 50000)
        {
            int currentSheetCount = GetSheetNumber(strFileName);//现有的页数sheetnum
            if (sheetRow <= 0)
            {
                sheetRow = dtSource.Rows.Count;
            }
            string[] temp = strFileName.Split('.');
            string fileExtens = temp[temp.Length - 1];
            int sheetCount = (int)Math.Ceiling((double)dtSource.Rows.Count / sheetRow);//sheet数目
            if (temp[temp.Length - 1] == "xls" && dtSource.Columns.Count < 256 && sheetRow < 65536)
            {
                if (isNew)
                {
                    currentSheetCount = 0;
                }
                for (int i = currentSheetCount; i < currentSheetCount + sheetCount; i++)
                {
                    DataTable pageDataTable = dtSource.Clone();
                    int hasRowCount = dtSource.Rows.Count - sheetRow * (i - currentSheetCount) < sheetRow ? dtSource.Rows.Count - sheetRow * (i - currentSheetCount) : sheetRow;
                    for (int j = 0; j < hasRowCount; j++)
                    {
                        pageDataTable.ImportRow(dtSource.Rows[(i - currentSheetCount) * sheetRow + j]);
                    }

                    using (MemoryStream ms = ExportDT(strFileName, pageDataTable, strHeaderText, dir, i))
                    {
                        using (FileStream fs = new FileStream(strFileName, FileMode.Create, FileAccess.Write))
                        {

                            byte[] data = ms.ToArray();
                            fs.Write(data, 0, data.Length);
                            fs.Flush();
                        }
                    }
                }
            }
            else
            {
                if (temp[temp.Length - 1] == "xls")
                    strFileName = strFileName + "x";
                if (isNew)
                {
                    currentSheetCount = 0;
                }
                for (int i = currentSheetCount; i < currentSheetCount + sheetCount; i++)
                {
                    DataTable pageDataTable = dtSource.Clone();
                    int hasRowCount = dtSource.Rows.Count - sheetRow * (i - currentSheetCount) < sheetRow ? dtSource.Rows.Count - sheetRow * (i - currentSheetCount) : sheetRow;
                    for (int j = 0; j < hasRowCount; j++)
                    {
                        pageDataTable.ImportRow(dtSource.Rows[(i - currentSheetCount) * sheetRow + j]);
                    }
                    FileStream readfs = new FileStream(strFileName, FileMode.OpenOrCreate, FileAccess.Read);
                    MemoryStream readfsm = new MemoryStream();
                    readfs.CopyTo(readfsm);
                    readfs.Close();
                    using (FileStream writefs = new FileStream(strFileName, FileMode.Create, FileAccess.Write))
                    {

                        ExportDTI(pageDataTable, strHeaderText, writefs, readfsm, dir, i);
                    }
                    readfsm.Close();
                }
            }
        }

        /// <summary>
        /// 导出Excel(//超出10000条数据 创建新的工作簿)
        /// </summary>
        /// <param name="dtSource">数据源</param>
        /// <param name="dir">导出Excel表格的字段名和列名的字符串字典实例;例如:dir.Add("IllegalKeywords", "姓名");</param>
        public static XSSFWorkbook ExportExcel(DataTable dtSource, Dictionary<string, string> dir)
        {
            XSSFWorkbook excelWorkbook = new XSSFWorkbook();

            //int columnsCount = columnsNames.GetLength(0);
            int columnsCount = dir.Count;
            if (columnsCount > 0)
            {
                ListColumnsName = new SortedList(new NoSort());
                //for (int i = 0; i < columnsCount; i++)
                //{
                //    ListColumnsName.Add(columnsNames[i, 0], columnsNames[i, 1]);
                //}
                foreach (KeyValuePair<string,string> item in dir)
                {
                    ListColumnsName.Add(item.Key, item.Value);
                }
                if (ListColumnsName == null || ListColumnsName.Count == 0)
                {
                    throw (new Exception("请对ListColumnsName设置要导出的列明!"));
                }
                else
                {
                    excelWorkbook = InsertRow(dtSource);
                }
            }
            else
            {
                throw (new Exception("请对ListColumnsName设置要导出的列明!"));
            }
            return excelWorkbook;
        }

        #endregion

        /// <summary>
        /// 创建Excel文件
        /// </summary>
        /// <param name="filePath"></param>
        private static XSSFWorkbook CreateExcelFile()
        {
            XSSFWorkbook xssfworkbook = new XSSFWorkbook();

            //右击文件“属性”信息
            #region 文件属性信息
            {
                POIXMLProperties props = xssfworkbook.GetProperties();
                props.CoreProperties.Creator = "Joy";//Excel文件的创建作者
                props.CoreProperties.Title = "";//Excel文件标题
                props.CoreProperties.Description = "";//Excel文件备注
                props.CoreProperties.Category = "";//Excel文件类别信息
                props.CoreProperties.Subject = "";//Excel文件主题信息
                props.CoreProperties.Created = DateTime.Now;//Excel文件创建时间
                props.CoreProperties.Modified = DateTime.Now;//Excel文件修改时间
                props.CoreProperties.SetCreated(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));
                props.CoreProperties.LastModifiedByUser = "Joy";//Excel文件最后一次保存者
                props.CoreProperties.SetModified(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"));//Excel文件最后一次保存日期
            }
            #endregion

            return xssfworkbook;
        }

        /// <summary>
        /// 创建excel表头
        /// </summary>
        /// <param name="dgv"></param>
        /// <param name="excelSheet"></param>
        private static void CreateHeader(XSSFSheet excelSheet, XSSFWorkbook excelWorkbook, XSSFCellStyle cellStyle)
        {
            int cellIndex = 0;
            //循环导出列
            foreach (System.Collections.DictionaryEntry de in ListColumnsName)
            {
                XSSFRow newRow = (XSSFRow)excelSheet.CreateRow(0);
                XSSFCellStyle? headTopStyle = CreateStyle(excelWorkbook, cellStyle,HorizontalAlignment.Center, VerticalAlignment.Center, 18, true, true, "宋体", true, false, false, true, FillPattern.SolidForeground, HSSFColor.Grey25Percent.Index, HSSFColor.Black.Index,FontUnderlineType.None, FontSuperScript.None, false);
                XSSFCell newCell = (XSSFCell)newRow.CreateCell(cellIndex);
                newCell.SetCellValue(de.Value.ToString());
                newCell.CellStyle = headTopStyle;
                cellIndex++;
            }
        }

        /// <summary>
        /// 插入数据行
        /// </summary>
        private static XSSFWorkbook InsertRow(DataTable dtSource)
        {
            XSSFWorkbook excelWorkbook = CreateExcelFile();
            int rowCount = 0;
            int sheetCount = 1;
            XSSFSheet newsheet = null;

            //循环数据源导出数据集
            newsheet = (XSSFSheet)excelWorkbook.CreateSheet("Sheet" + sheetCount);
            XSSFCellStyle headCellStyle = (XSSFCellStyle)excelWorkbook.CreateCellStyle(); //创建列头单元格实例样式
            CreateHeader(newsheet, excelWorkbook, headCellStyle);
            //单元格内容信息
            foreach (DataRow dr in dtSource.Rows)
            {
                rowCount++;
                //超出10000条数据 创建新的工作簿
                if (rowCount == 10000)
                {
                    rowCount = 1;
                    sheetCount++;
                    newsheet = (XSSFSheet)excelWorkbook.CreateSheet("Sheet" + sheetCount);
                    CreateHeader(newsheet, excelWorkbook, headCellStyle);
                }
                XSSFRow newRow = (XSSFRow)newsheet.CreateRow(rowCount);
                XSSFCellStyle cellStyle = (XSSFCellStyle)excelWorkbook.CreateCellStyle(); //创建单元格实例样式
                XSSFCellStyle? style = CreateStyle(excelWorkbook, cellStyle, HorizontalAlignment.Center, VerticalAlignment.Center, 14, true, false);
                InsertCell(dtSource, dr, newRow, style, excelWorkbook);
            }
            //自动列宽
            //for (int i = 0; i <= dtSource.Columns.Count; i++)
            //{
            //    newsheet.AutoSizeColumn(i, true);
            //}
            return excelWorkbook;
        }

        /// <summary>
        /// 导出数据行
        /// </summary>
        /// <param name="dtSource"></param>
        /// <param name="drSource"></param>
        /// <param name="currentExcelRow"></param>
        /// <param name="excelSheet"></param>
        /// <param name="excelWorkBook"></param>
        private static void InsertCell(DataTable dtSource, DataRow drSource, XSSFRow currentExcelRow, XSSFCellStyle cellStyle, XSSFWorkbook excelWorkBook)
        {
            for (int cellIndex = 0; cellIndex < ListColumnsName.Count; cellIndex++)
            {
                //列名称
                string columnsName = ListColumnsName.GetKey(cellIndex).ToString();
                XSSFCell newCell = null;
                System.Type rowType = drSource[columnsName].GetType();
                string drValue = drSource[columnsName].ToString().Trim();
                switch (rowType.ToString())
                {
                    case "System.String"://字符串类型
                        drValue = drValue.Replace("&", "&");
                        drValue = drValue.Replace(">", ">");
                        drValue = drValue.Replace("<", "<");
                        newCell = (XSSFCell)currentExcelRow.CreateCell(cellIndex);
                        newCell.SetCellValue(drValue);
                        newCell.CellStyle = cellStyle;
                        break;
                    case "System.DateTime"://日期类型
                        DateTime dateV;
                        DateTime.TryParse(drValue, out dateV);
                        newCell = (XSSFCell)currentExcelRow.CreateCell(cellIndex);
                        newCell.SetCellValue(dateV);
                        newCell.CellStyle = cellStyle;
                        break;
                    case "System.Boolean"://布尔型
                        bool boolV = false;
                        bool.TryParse(drValue, out boolV);
                        newCell = (XSSFCell)currentExcelRow.CreateCell(cellIndex);
                        newCell.SetCellValue(boolV);
                        newCell.CellStyle = cellStyle;
                        break;
                    case "System.Int16"://整型
                    case "System.Int32":
                    case "System.Int64":
                    case "System.Byte":
                        int intV = 0;
                        int.TryParse(drValue, out intV);
                        newCell = (XSSFCell)currentExcelRow.CreateCell(cellIndex);
                        newCell.SetCellValue(intV.ToString());
                        newCell.CellStyle = cellStyle;
                        break;
                    case "System.Decimal"://浮点型
                    case "System.Double":
                        double doubV = 0;
                        double.TryParse(drValue, out doubV);
                        newCell = (XSSFCell)currentExcelRow.CreateCell(cellIndex);
                        newCell.SetCellValue(doubV);
                        newCell.CellStyle = cellStyle;
                        break;
                    case "System.DBNull"://空值处理
                        newCell = (XSSFCell)currentExcelRow.CreateCell(cellIndex);
                        newCell.SetCellValue("");
                        newCell.CellStyle = cellStyle;
                        break;
                    default:
                        throw (new Exception(rowType.ToString() + ":类型数据无法处理!"));
                }
            }
        }

        /// <summary>
        /// 行内单元格常用样式设置
        /// </summary>
        /// <param name="workbook">Excel文件对象</param>
        /// <param name="cellStyle">Excel文件中XSSFCellStyle对象</param>
        /// <param name="hAlignment">水平布局方式</param>
        /// <param name="vAlignment">垂直布局方式</param>
        /// <param name="fontHeightInPoints">字体大小</param>
        /// <param name="isAddBorder">是否需要边框</param>
        /// <param name="boldWeight">字体加粗 (None = 0,Normal = 400,Bold = 700</param>
        /// <param name="fontName">字体(仿宋,楷体,宋体,微软雅黑...与Excel主题字体相对应)</param>
        /// <param name="isAddBorderColor">是否增加边框颜色</param>
        /// <param name="isItalic">是否将文字变为斜体</param>
        /// <param name="isLineFeed">是否自动换行</param>
        /// <param name="isAddCellBackground">是否增加单元格背景颜色</param>
        /// <param name="fillPattern">填充图案样式(FineDots 细点,SolidForeground立体前景,isAddFillPattern=true时存在)</param>
        /// <param name="cellBackgroundColor">单元格背景颜色(当isAddCellBackground=true时存在)</param>
        /// <param name="fontColor">字体颜色</param>
        /// <param name="underlineStyle">下划线样式(无下划线[None],单下划线[Single],双下划线[Double],会计用单下划线[SingleAccounting],会计用双下划线[DoubleAccounting])</param>
        /// <param name="typeOffset">字体上标下标(普通默认值[None],上标[Sub],下标[Super]),即字体在单元格内的上下偏移量</param>
        /// <param name="isStrikeout">是否显示删除线</param>
        /// <param name="dataFormat">格式化日期显示</param>
        /// <returns></returns>
        public static XSSFCellStyle CreateStyle(XSSFWorkbook workbook, XSSFCellStyle cellStyle, HorizontalAlignment hAlignment, VerticalAlignment vAlignment, short fontHeightInPoints, bool isAddBorder, bool boldWeight, string fontName = "宋体", bool isAddBorderColor = true, bool isItalic = false, bool isLineFeed = true, bool isAddCellBackground = false, FillPattern fillPattern = FillPattern.NoFill, short cellBackgroundColor = HSSFColor.Yellow.Index, short fontColor = HSSFColor.Black.Index, FontUnderlineType underlineStyle =
            FontUnderlineType.None, FontSuperScript typeOffset = FontSuperScript.None, bool isStrikeout = false,string dataFormat="yyyy-MM-dd HH:mm:ss")
        {
            cellStyle.Alignment = hAlignment; //水平居中
            cellStyle.VerticalAlignment = vAlignment; //垂直居中
            cellStyle.WrapText = isLineFeed;//自动换行

            //格式化显示
            XSSFDataFormat format = (XSSFDataFormat)workbook.CreateDataFormat();
            cellStyle.DataFormat = format.GetFormat(dataFormat);

            //背景颜色,边框颜色,字体颜色都是使用 HSSFColor属性中的对应调色板索引,关于 HSSFColor 颜色索引对照表,详情参考:https://www.cnblogs.com/Brainpan/p/5804167.html

            //TODO:引用了NPOI后可通过ICellStyle 接口的 FillForegroundColor 属性实现 Excel 单元格的背景色设置,FillPattern 为单元格背景色的填充样式

            //TODO:十分注意,要设置单元格背景色必须是FillForegroundColor和FillPattern两个属性同时设置,否则是不会显示背景颜色
            if (isAddCellBackground)
            {
                cellStyle.FillForegroundColor = cellBackgroundColor;//单元格背景颜色
                cellStyle.FillPattern = fillPattern;//填充图案样式(FineDots 细点,SolidForeground立体前景)
            }
            else
            {
                cellStyle.FillForegroundColor = HSSFColor.White.Index;//单元格背景颜色
            }

            //是否增加边框
            if (isAddBorder)
            {
                //常用的边框样式 None(没有),Thin(细边框,瘦的),Medium(中等),Dashed(虚线),Dotted(星罗棋布的),Thick(厚的),Double(双倍),Hair(头发)[上右下左顺序设置]
                cellStyle.BorderBottom = BorderStyle.Thin;
                cellStyle.BorderRight = BorderStyle.Thin;
                cellStyle.BorderTop = BorderStyle.Thin;
                cellStyle.BorderLeft = BorderStyle.Thin;
            }

            //是否设置边框颜色
            if (isAddBorderColor)
            {
                //边框颜色[上右下左顺序设置]
                cellStyle.TopBorderColor = XSSFFont.DEFAULT_FONT_COLOR;//DarkGreen(黑绿色)
                cellStyle.RightBorderColor = XSSFFont.DEFAULT_FONT_COLOR;
                cellStyle.BottomBorderColor = XSSFFont.DEFAULT_FONT_COLOR;
                cellStyle.LeftBorderColor = XSSFFont.DEFAULT_FONT_COLOR;
            }

            /**
             * 设置相关字体样式
             */
            var cellStyleFont = (XSSFFont)workbook.CreateFont(); //创建字体

            //假如字体大小只需要是粗体的话直接使用下面该属性即可
            //cellStyleFont.IsBold = true;

            cellStyleFont.IsBold = boldWeight; //字体加粗
            cellStyleFont.FontHeightInPoints = fontHeightInPoints; //字体大小
            cellStyleFont.FontName = fontName;//字体(仿宋,楷体,宋体 )
            cellStyleFont.Color = fontColor;//设置字体颜色
            cellStyleFont.IsItalic = isItalic;//是否将文字变为斜体
            cellStyleFont.Underline = underlineStyle;//字体下划线
            cellStyleFont.TypeOffset = typeOffset;//字体上标下标
            cellStyleFont.IsStrikeout = isStrikeout;//是否有删除线

            cellStyle.SetFont(cellStyleFont); //将字体绑定到样式
            return cellStyle;
        }

        #endregion

        #region 从excel文件中将数据导出到List<T>对象集合

        /// <summary>
        /// 将制定sheet中的数据导出到DataTable中
        /// </summary>
        /// <param name="sheet">需要导出的sheet</param>
        /// <param name="HeaderRowIndex">列头所在行号,-1表示没有列头</param>
        /// <param name="dir">excel列名和DataTable列名的对应字典</param>
        /// <returns></returns>
        static DataTable ImportDt(ISheet sheet, int HeaderRowIndex, Dictionary<string, string> dir)
        {
            DataTable table = new DataTable();
            IRow headerRow;
            int cellCount;
            try
            {
                //没有标头或者不需要表头用excel列的序号(1,2,3..)作为DataTable的列名
                if (HeaderRowIndex < 0)
                {
                    headerRow = sheet.GetRow(0);
                    cellCount = headerRow.LastCellNum;

                    for (int i = headerRow.FirstCellNum; i <= cellCount; i++)
                    {
                        DataColumn column = new DataColumn(Convert.ToString(i));
                        table.Columns.Add(column);
                    }
                }
                //有表头,使用表头做为DataTable的列名
                else
                {
                    headerRow = sheet.GetRow(HeaderRowIndex);
                    cellCount = headerRow.LastCellNum;
                    for (int i = headerRow.FirstCellNum; i <cellCount; i++)
                    {
                        //如果excel某一列列名不存在:以该列的序号作为DataTable的列名,如果DataTable中包含了这个序列为名的列,那么列名为重复列名+序号
                        if (headerRow.GetCell(i) == null)
                        {
                            if (table.Columns.IndexOf(Convert.ToString(i)) > 0)
                            {
                                DataColumn column = new DataColumn(Convert.ToString("重复列名" + i));
                                table.Columns.Add(column);
                            }
                            else
                            {
                                DataColumn column = new DataColumn(Convert.ToString(i));
                                table.Columns.Add(column);
                            }

                        }
                        //excel中的某一列列名不为空,但是重复了:对应的DataTable列名为“重复列名+序号”
                        else if (table.Columns.IndexOf(headerRow.GetCell(i).ToString()) > 0)
                        {
                            DataColumn column = new DataColumn(Convert.ToString("重复列名" + i));
                            table.Columns.Add(column);
                        }
                        else
                        //正常情况,列名存在且不重复:用excel中的列名作为DataTable中对应的列名
                        {
                            string aaa = headerRow.GetCell(i).ToString();
                            string colName = dir.Where(s => s.Value == headerRow.GetCell(i).ToString()).First().Key;
                            DataColumn column = new DataColumn(colName);
                            table.Columns.Add(column);
                        }
                    }
                }
                int rowCount = sheet.LastRowNum;
                for (int i = (HeaderRowIndex + 1); i <= sheet.LastRowNum; i++)//excel行遍历
                {
                    try
                    {
                        IRow row;
                        if (sheet.GetRow(i) == null)//如果excel有空行,则添加缺失的行
                        {
                            row = sheet.CreateRow(i);
                        }
                        else
                        {
                            row = sheet.GetRow(i);
                        }

                        DataRow dataRow = table.NewRow();

                        for (int j = row.FirstCellNum; j <= cellCount; j++)//excel列遍历
                        {
                            try
                            {
                                if (row.GetCell(j) != null)
                                {
                                    switch (row.GetCell(j).CellType)
                                    {
                                        case CellType.String://字符串
                                            string str = row.GetCell(j).StringCellValue;
                                            if (str != null && str.Length > 0)
                                            {
                                                dataRow[j] = str.ToString();
                                            }
                                            else
                                            {
                                                dataRow[j] = default(string);
                                            }
                                            break;
                                        case CellType.Numeric://数字
                                            if (DateUtil.IsCellDateFormatted(row.GetCell(j)))//时间戳数字
                                            {
                                                dataRow[j] = DateTime.FromOADate(row.GetCell(j).NumericCellValue);
                                            }
                                            else
                                            {
                                                dataRow[j] = Convert.ToDouble(row.GetCell(j).NumericCellValue);
                                            }
                                            break;
                                        case CellType.Boolean:
                                            dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue);
                                            break;
                                        case CellType.Error:
                                            dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue);
                                            break;
                                        case CellType.Formula://公式
                                            switch (row.GetCell(j).CachedFormulaResultType)
                                            {
                                                case CellType.String:
                                                    string strFORMULA = row.GetCell(j).StringCellValue;
                                                    if (strFORMULA != null && strFORMULA.Length > 0)
                                                    {
                                                        dataRow[j] = strFORMULA.ToString();
                                                    }
                                                    else
                                                    {
                                                        dataRow[j] = null;
                                                    }
                                                    break;
                                                case CellType.Numeric:
                                                    dataRow[j] = Convert.ToString(row.GetCell(j).NumericCellValue);
                                                    break;
                                                case CellType.Boolean:
                                                    dataRow[j] = Convert.ToString(row.GetCell(j).BooleanCellValue);
                                                    break;
                                                case CellType.Error:
                                                    dataRow[j] = ErrorEval.GetText(row.GetCell(j).ErrorCellValue);
                                                    break;
                                                default:
                                                    dataRow[j] = "";
                                                    break;
                                            }
                                            break;
                                        default:
                                            dataRow[j] = "";
                                            break;
                                    }
                                }
                            }
                            catch (Exception exception)
                            {
                                //loger.Error(exception.ToString());
                            }
                        }
                        table.Rows.Add(dataRow);
                    }
                    catch (Exception exception)
                    {
                        //loger.Error(exception.ToString());
                    }
                }
            }
            catch (Exception exception)
            {
                //loger.Error(exception.ToString());
            }
            return table;
        }

        /// <summary>
        /// DataTable 转换为List<T>对象集合
        /// </summary>
        /// <typeparam name="TResult">类型</typeparam>
        /// <param name="dt">DataTable</param>
        /// <returns></returns>
        public static List<TResult> DataTableToList<TResult>(this DataTable dt) where TResult : class, new()
        {
            //创建一个属性的列表
            List<PropertyInfo> prlist = new List<PropertyInfo>();
            //获取TResult的类型实例  反射的入口
            Type t = typeof(TResult);
            //获得TResult 的所有的Public 属性 并找出TResult属性和DataTable的列名称相同的属性(PropertyInfo) 并加入到属性列表
            Array.ForEach<PropertyInfo>(t.GetProperties(), p => { if (dt.Columns.IndexOf(p.Name) != -1) prlist.Add(p); });
            //创建返回的集合
            List<TResult> oblist = new List<TResult>();
            foreach (DataRow row in dt.Rows)
            {
                //创建TResult的实例
                TResult ob = new TResult();
                //找到对应的数据  并赋值
                prlist.ForEach(p => { if (row[p.Name] != DBNull.Value) p.SetValue(ob, row[p.Name], null); });
                //放入到返回的集合中.
                oblist.Add(ob);
            }
            return oblist;
        }

        /// <summary>
        /// DataTable转化为List集合
        /// </summary>
        /// <typeparam name="T">实体对象</typeparam>
        /// <param name="dt">datatable表</param>
        /// <param name="isStoreDB">是否存入数据库datetime字段,date字段没事,取出不用判断</param>
        /// <returns>返回list集合</returns>
        private static List<T> DataTableToList<T>(DataTable dt, bool isStoreDB = true)
        {
            List<T> list = new List<T>();
            Type type = typeof(T);
            //List<string> listColums = new List<string>();
            PropertyInfo[] pArray = type.GetProperties(); //集合属性数组
            foreach (DataRow row in dt.Rows)
            {
                T entity = Activator.CreateInstance<T>(); //新建对象实例 
                foreach (PropertyInfo p in pArray)
                {
                    if (!dt.Columns.Contains(p.Name) || row[p.Name] == null || row[p.Name] == DBNull.Value)
                    {
                        continue;  //DataTable列中不存在集合属性或者字段内容为空则,跳出循环,进行下个循环   
                    }
                    if (isStoreDB && p.PropertyType == typeof(DateTime) && Convert.ToDateTime(row[p.Name]) < Convert.ToDateTime("1753-01-01"))
                    {
                        continue;
                    }
                    try
                    {
                        var obj = Convert.ChangeType(row[p.Name], p.PropertyType);//类型强转,将table字段类型转为集合字段类型  
                        p.SetValue(entity, obj, null);
                    }
                    catch (Exception)
                    {
                        // throw;
                    }             
                }
                list.Add(entity);
            }
            return list;
        }

        /// <summary>
        /// DataSet 转换成List
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="ds"></param>
        /// <param name="tableIndext"></param>
        /// <returns></returns>
        private static List<T> DataTable2List<T>(DataTable dt)
        {
            //确认参数有效 
            if (dt == null || dt.Rows.Count <= 0)
            {
                return null;
            }

            IList<T> list = new List<T>(); //实例化一个list 
                                           // 在这里写 获取T类型的所有公有属性。 注意这里仅仅是获取T类型的公有属性,不是公有方法,也不是公有字段,当然也不是私有属性 
            PropertyInfo[] tMembersAll = typeof(T).GetProperties();

            for (int i = 0; i < dt.Rows.Count; i++)
            {
                //创建泛型对象。为什么这里要创建一个泛型对象呢?是因为目前我不确定泛型的类型。 
                T t = Activator.CreateInstance<T>();

                //获取t对象类型的所有公有属性。但是我不建议吧这条语句写在for循环里,因为没循环一次就要获取一次,占用资源,所以建议写在外面 
                //PropertyInfo[] tMembersAll = t.GetType().GetProperties();
                for (int j = 0; j < dt.Columns.Count; j++)
                {
                    //遍历tMembersAll 
                    foreach (PropertyInfo tMember in tMembersAll)
                    {
                        //取dt表中j列的名字,并把名字转换成大写的字母。整条代码的意思是:如果列名和属性名称相同时赋值 
                        if (dt.Columns[j].ColumnName.ToUpper().Equals(tMember.Name.ToUpper()))
                        {
                            //dt.Rows[i][j]表示取dt表里的第i行的第j列;DBNull是指数据库中当一个字段没有被设置值的时候的值,相当于数据库中的“空值”。 
                            if (dt.Rows[i][j] != DBNull.Value)
                            {
                                //SetValue是指:将指定属性设置为指定值。 tMember是T泛型对象t的一个公有成员,整条代码的意思就是:将dt.Rows[i][j]赋值给t对象的tMember成员,参数详情请参照http://msdn.microsoft.com/zh-cn/library/3z2t396t(v=vs.100).aspx/html
                                tMember.SetValue(t, Convert.ToString(dt.Rows[i][j]), null);
                            }
                            else
                            {
                                tMember.SetValue(t, null, null);
                            }
                            break;//注意这里的break是写在if语句里面的,意思就是说如果列名和属性名称相同并且已经赋值了,那么我就跳出foreach循环,进行j+1的下次循环 
                        }
                    }
                }
                list.Add(t);
            }
            dt.Dispose();
            return list.ToList();

        }

        /// <summary>
        /// 读取Excel文件特定名字sheet的内容到List<T>对象集合
        /// </summary>
        /// <param name="strFileName">excel文件路径</param>
        /// <param name="dir">excel列名和DataTable列名的对应字典</param>
        /// <param name="SheetName">excel表名</param>
        /// <param name="HeaderRowIndex">列头所在行号,-1表示没有列头</param>
        /// <returns></returns>
        public static List<T> ImportExceltoDt<T>(string strFileName, Dictionary<string, string> dir, string SheetName, int HeaderRowIndex = 0)
        {
            DataTable table = new DataTable();
            using (FileStream file = new FileStream(strFileName, FileMode.Open, FileAccess.Read))
            {
                if (file.Length > 0)
                {
                    IWorkbook wb = WorkbookFactory.Create(file);
                    ISheet isheet = wb.GetSheet(SheetName);
                    table = ImportDt(isheet, HeaderRowIndex, dir);
                    isheet = null;
                }
            }
            List<T> results = DataTableToList<T>(table);
            table.Dispose();
            return results;
        }

        /// <summary>
        /// 读取Excel文件某一索引sheet的内容到DataTable
        /// </summary>
        /// <param name="strFileName">excel文件路径</param>
        /// <param name="sheet">需要导出的sheet序号</param>
        /// <param name="HeaderRowIndex">列头所在行号,-1表示没有列头</param>
        /// <param name="dir">excel列名和DataTable列名的对应字典</param>
        /// <returns></returns>
        public static List<T> ImportExceltoDt<T>(string strFileName, Dictionary<string, string> dir, int HeaderRowIndex = 0, int SheetIndex = 0)
        {
            DataTable table = new DataTable();
            using (FileStream file = new FileStream(strFileName, FileMode.Open, FileAccess.Read))
            {
                if (file.Length > 0)
                {
                    IWorkbook wb = WorkbookFactory.Create(file);
                    ISheet isheet = wb.GetSheetAt(SheetIndex);
                    table = ImportDt(isheet, HeaderRowIndex, dir);
                    isheet = null;
                }
            }
            List<T> results = DataTableToList<T>(table);
            table.Dispose();
            return results;
        }

        #endregion

        /// <summary>
        /// 获取excel文件的sheet数目
        /// </summary>
        /// <param name="outputFile"></param>
        /// <returns></returns>
        public static int GetSheetNumber(string outputFile)
        {
            int number = 0;
            using (FileStream readfile = new FileStream(outputFile, FileMode.OpenOrCreate, FileAccess.Read))
            {
                if (readfile.Length > 0)
                {
                    IWorkbook wb = WorkbookFactory.Create(readfile);
                    number = wb.NumberOfSheets;
                }
            }
            return number;
        }

        /// <summary>
        /// 判断内容是否是数字
        /// </summary>
        /// <param name="message"></param>
        /// <param name="result"></param>
        /// <returns></returns>
        public static bool isNumeric(String message, out double result)
        {
            Regex rex = new Regex(@"^[-]?\d+[.]?\d*$");
            result = -1;
            if (rex.IsMatch(message))
            {
                result = double.Parse(message);
                return true;
            }
            else
                return false;
        }

        /// <summary>
        /// 验证导入的Excel是否有数据
        /// </summary>
        /// <param name="excelFileStream"></param>
        /// <returns></returns>
        public static bool HasData(Stream excelFileStream)
        {
            using (excelFileStream)
            {
                IWorkbook workBook = new HSSFWorkbook(excelFileStream);
                if (workBook.NumberOfSheets > 0)
                {
                    ISheet sheet = workBook.GetSheetAt(0);
                    return sheet.PhysicalNumberOfRows > 0;
                }
            }
            return false;
        }

    }

    /// <summary>
    /// 排序实现接口 不进行排序 根据添加顺序导出
    /// </summary>
    public class NoSort : IComparer
    {
        public int Compare(object x, object y)
        {
            return -1;
        }
    }

}

三、调用

3.1、增加一个“keywords”模型类,用作导出

public class keywords
{
    [Column("姓名")]
    public string IllegalKeywords { get; set; }
}

3.2、添加一个控制器

3.3、编写导入导出的控制器代码

3.3.1、重写“Close”函数

在导出时,为了防止MemoryStream无法关闭从而报错,所以我们继承MemoryStream;代码如下:

namespace WebApplication1    //命名空间依据自己的项目进行修改
{
    public class NpoiMemoryStream : MemoryStream
    {
        public NpoiMemoryStream()
        {
            AllowClose = true;
        }

        public bool AllowClose { get; set; }

        public override void Close()
        {
            if (AllowClose)
                base.Close();
        }
    }
}

3.3.2、添加控制器代码

/// <summary>
/// 本地环境
/// </summary>
private IHostingEnvironment _hostingEnv;


/// <summary>
/// Excel导入的具体实现
/// </summary>
/// <returns></returns>
public IActionResult import_excel()
{
    string filepath = _hostingEnv.WebRootPath + "/在线用户20230324.xlsx";//导入的文件地址路径,可动态传入
    Dictionary<string, string> dir = new Dictionary<string, string>();//申明excel列名和DataTable列名的对应字典
    dir.Add("IllegalKeywords","姓名");
    List<keywords> keyWordsList = ExcelHelper.ImportExceltoDt<keywords>(filepath, dir,"Sheet1",0);
    #region 将List动态添加至数据库
    //……
    #endregion
    return Json(new { code = 200, msg = "导入成功" });
}

/// <summary>
/// Excel导出的具体实现
/// </summary>
/// <returns></returns>
public IActionResult export_excel()
{
    #region 添加测试数据
    List<keywords> keys = new List<keywords>();
    for (int i = 0; i < 6; i++)
    {
        keywords keyword = new keywords();
        keyword.IllegalKeywords = "测试_" + i;
        keys.Add(keyword);
    }
    #endregion
    #region 实例化DataTable并进行赋值
    DataTable dt = new DataTable();
    dt = listToDataTable(keys);//List<T>对象集合转DataTable
    #endregion
    string filename = DateTime.Now.ToString("在线用户yyyyMMdd") + ".xlsx";
    Dictionary<string, string> dir = new Dictionary<string, string>();
    dir.Add("IllegalKeywords", "姓名");
    XSSFWorkbook book= ExcelHelper.ExportExcel(dt,  dir);
    dt.Dispose();//释放DataTable所占用的数据资源

    NpoiMemoryStream ms = new NpoiMemoryStream();
    ms.AllowClose = false;
    book.Write(ms, true);
    ms.Flush();
    ms.Position = 0;
    ms.Seek(0, SeekOrigin.Begin);
    ms.AllowClose = true;
    book.Dispose();//使用由XSSFWorkbook所占用的资源

    return File(ms, "application/vnd.ms-excel", Path.GetFileName(filename));//进行浏览器下载
}

3.3.3、Excel导出效果

Excel导出效果

3.3.4、Excel导入效果

导入后的List再根据需求调用添加方法实现数据的添加
Excel导入效果

MVVM模式于MVP模式 - 温暖的人 - 博客园

mikel阅读(444)

来源: MVVM模式于MVP模式 – 温暖的人 – 博客园

MVC、MVP、MVVM这些模式是为了解决开发过程中的实际问题而提出来的,目前作为主流的几种架构模式而被广泛使用。

一.MVP模式(Model-View-Presenter):传统的开发是MVP模式(例如JQuery)

MVP是把MVC中的Controller换成了Presenter(呈现),目的就是为了完全切断View跟Model之间的联系,由Presenter充当桥梁,做到View-Model之间通信的完全隔离。

.NET程序员熟知的ASP.NET webform、winform基于事件驱动的开发技术就是使用的MVP模式。控件组成的页面充当View,实体数据库操作充当Model,而View和Model之间的控件数据绑定操作则属于Presenter。控件事件的处理可以通过自定义的IView接口实现,而View和IView都将对Presenter负责。

二. MVC(MOdel-View-Controller)

MVC是比较直观的架构模式,用户操作->View(负责接收用户的输入操作)->Controller(业务逻辑处理)->Model(数据持久化)->View(将结果反馈给View)。

MVC使用非常广泛,比如JavaEE中的SSH框架(Struts/Spring/Hibernate),Struts(View, STL)-Spring(Controller, Ioc、Spring MVC)-Hibernate(Model, ORM)以及ASP.NET中的ASP.NET MVC框架,xxx.cshtml-xxxcontroller-xxxmodel。(实际上后端开发过程中是v-c-m-c-v,v和m并没有关系,下图仅代表经典的mvc模型)

三. MVVM(Model-View-ViewModek)

如果说MVP是对MVC的进一步改进,那么MVVM则是思想的完全变革。它是将“数据模型数据双向绑定”的思想作为核心,因此在View和Model之间没有联系,通过ViewModel进行交互,而且Model和ViewModel之间的交互是双向的,因此视图的数据的变化会同时修改数据源,而数据源数据的变化也会立即反应到View上。

这方面典型的应用有.NET的WPF,js框架Knockout、AngularJS,Vue等。

 

.NET Core 3.0之深入源码理解Host(二) - 艾心❤ - 博客园

mikel阅读(335)

来源: .NET Core 3.0之深入源码理解Host(二) – 艾心❤ – 博客园

写在前面

停了近一个月的技术博客,随着正式脱离996的魔窟,接下来也正式恢复了。本文从源码角度进一步讨论.NET Core 3.0 中关于Host扩展的一些技术点,主要讨论Long Run Program的创建与守护。

关于Host,我们最容易想到的就是程序的启动与停止,而其中隐藏着非常关键的功能,就是Host的初始化,我们所需要的所有资源都必须而且应该在程序启动过程中初始化完成,本文的主要内容并不是Host初始化,前文已经累述。为了更好的守护与管理已经启动的Host,.NET Core 3.0将程序的生命周期事件的订阅开放给开发者,也包括自定义的Host Service对象。

注:本文代码基于.NET Core 3.0 Preview9

host2

.NET Core 3.0中创建Long Run Program

IHost与IHostBuilder

当我们创建Long Run Program时,会首先关注程序的启动与停止,.NET Core 3.0为此提供了一个接口IHost,该接口位于Microsoft.Extensions.Hosting类库中,其源码如下:

   1:  /// <summary>
   2:  /// A program abstraction.
   3:  /// </summary>
   4:  public interface IHost : IDisposable
   5:  {
   6:      /// <summary>
   7:      /// The programs configured services.
   8:      /// </summary>
   9:      IServiceProvider Services { get; }
  10:  
  11:      /// <summary>
  12:      /// Start the program.
  13:      /// </summary>
  14:      /// <param name="cancellationToken">Used to abort program start.</param>
  15:      /// <returns>A <see cref="Task"/> that will be completed when the <see cref="IHost"/> starts.</returns>
  16:      Task StartAsync(CancellationToken cancellationToken = default);
  17:  
  18:      /// <summary>
  19:      /// Attempts to gracefully stop the program.
  20:      /// </summary>
  21:      /// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
  22:      /// <returns>A <see cref="Task"/> that will be completed when the <see cref="IHost"/> stops.</returns>
  23:      Task StopAsync(CancellationToken cancellationToken = default);
  24:  }

该接口含有一个只读属性:IServiceProvider Services { get; },通过该属性,我们可以拿到所有Host初始化时所注入的对象信息。

IHostBuilder接口所承担的核心功能就是程序的初始化,通过:IHost Build()来完成,当然只需要运行一次即可。其初始化内容一般包括以下几个功能:

host3

另外需要说明的是,以上功能的初始化,是通过IHostBuilder提供的接口获取用户输入的信息后,通过调用Build()方法来完成初始化。以下为IHostBuilder的部分源代码:

   1:  /// <summary>
   2:  /// Set up the configuration for the builder itself. This will be used to initialize the <see cref="IHostEnvironment"/>
   3:  /// for use later in the build process. This can be called multiple times and the results will be additive.
   4:  /// </summary>
   5:  /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used
   6:  /// to construct the <see cref="IConfiguration"/> for the host.</param>
   7:  /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
   8:  public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
   9:  {
  10:      _configureHostConfigActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
  11:      return this;
  12:  }
  13:  
  14:  /// <summary>
  15:  /// Adds services to the container. This can be called multiple times and the results will be additive.
  16:  /// </summary>
  17:  /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used
  18:  /// to construct the <see cref="IConfiguration"/> for the host.</param>
  19:  /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
  20:  public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
  21:  {
  22:      _configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
  23:      return this;
  24:  }
  25:  
  26:  /// <summary>
  27:  /// Overrides the factory used to create the service provider.
  28:  /// </summary>
  29:  /// <typeparam name="TContainerBuilder">The type of the builder to create.</typeparam>
  30:  /// <param name="factory">A factory used for creating service providers.</param>
  31:  /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
  32:  public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory)
  33:  {
  34:      _serviceProviderFactory = new ServiceFactoryAdapter<TContainerBuilder>(factory ?? throw new ArgumentNullException(nameof(factory)));
  35:      return this;
  36:  }
  37:  
  38:  /// <summary>
  39:  /// Enables configuring the instantiated dependency container. This can be called multiple times and
  40:  /// the results will be additive.
  41:  /// </summary>
  42:  /// <typeparam name="TContainerBuilder">The type of the builder to create.</typeparam>
  43:  /// <param name="configureDelegate">The delegate for configuring the <see cref="IConfigurationBuilder"/> that will be used
  44:  /// to construct the <see cref="IConfiguration"/> for the host.</param>
  45:  /// <returns>The same instance of the <see cref="IHostBuilder"/> for chaining.</returns>
  46:  public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderContext, TContainerBuilder> configureDelegate)
  47:  {
  48:      _configureContainerActions.Add(new ConfigureContainerAdapter<TContainerBuilder>(configureDelegate
  49:          ?? throw new ArgumentNullException(nameof(configureDelegate))));
  50:      return this;
  51:  }

IHostService

文章开头有说过自定义Host Service对象,那么我们如何自定义呢,其实很简单只需要实现IHostService,并在ConfigureServices中调用services.AddHostedService<MyServiceA>()即可,以下是IHostService的源码:

   1:  /// <summary>
   2:  /// Defines methods for objects that are managed by the host.
   3:  /// </summary>
   4:  public interface IHostedService
   5:  {
   6:      /// <summary>
   7:      /// Triggered when the application host is ready to start the service.
   8:      /// </summary>
   9:      /// <param name="cancellationToken">Indicates that the start process has been aborted.</param>
  10:      Task StartAsync(CancellationToken cancellationToken);
  11:  
  12:      /// <summary>
  13:      /// Triggered when the application host is performing a graceful shutdown.
  14:      /// </summary>
  15:      /// <param name="cancellationToken">Indicates that the shutdown process should no longer be graceful.</param>
  16:      Task StopAsync(CancellationToken cancellationToken);
  17:  }

根据源码我们可以知道,该接口只有两个方法,即代码程序开始与停止的方法。具体的实现可以参考如下:

   1:  public class MyServiceA : IHostedService, IDisposable
   2:  {
   3:      private bool _stopping;
   4:      private Task _backgroundTask;
   5:  
   6:      public MyServiceA(ILoggerFactory loggerFactory)
   7:      {
   8:          Logger = loggerFactory.CreateLogger<MyServiceB>();
   9:      }
  10:  
  11:      public ILogger Logger { get; }
  12:  
  13:      public Task StartAsync(CancellationToken cancellationToken)
  14:      {
  15:          Logger.LogInformation("MyServiceB is starting.");
  16:          _backgroundTask = BackgroundTask();
  17:          return Task.CompletedTask;
  18:      }
  19:  
  20:      private async Task BackgroundTask()
  21:      {
  22:          while (!_stopping)
  23:          {
  24:              await Task.Delay(TimeSpan.FromSeconds(7));
  25:              Logger.LogInformation("MyServiceB is doing background work.");
  26:          }
  27:  
  28:          Logger.LogInformation("MyServiceB background task is stopping.");
  29:      }
  30:  
  31:      public async Task StopAsync(CancellationToken cancellationToken)
  32:      {
  33:          Logger.LogInformation("MyServiceB is stopping.");
  34:          _stopping = true;
  35:          if (_backgroundTask != null)
  36:          {
  37:              // TODO: cancellation
  38:              await _backgroundTask;
  39:          }
  40:      }
  41:  
  42:      public void Dispose()
  43:      {
  44:          Logger.LogInformation("MyServiceB is disposing.");
  45:      }
  46:  }

IHostService是我们自定义Host管理对象的入口,所有需要压入到Host托管的对象都必须要实现此接口。

Host生命周期的管理

该接口提供了一种我们可以在程序运行期间进行管理的功能,如程序的启动与停止事件的订阅,关于Host生命周期的管理,主要由IHostApplicationLifetime和IHostLifetime这两个接口来完成。

以下是IHostApplicationLifetime的源码​

   1:  public interface IHostApplicationLifetime
   2:  {
   3:      /// <summary>
   4:      /// Triggered when the application host has fully started.
   5:      /// </summary>
   6:      CancellationToken ApplicationStarted { get; }
   7:  
   8:      /// <summary>
   9:      /// Triggered when the application host is performing a graceful shutdown.
  10:      /// Shutdown will block until this event completes.
  11:      /// </summary>
  12:      CancellationToken ApplicationStopping { get; }
  13:  
  14:      /// <summary>
  15:      /// Triggered when the application host is performing a graceful shutdown.
  16:      /// Shutdown will block until this event completes.
  17:      /// </summary>
  18:      CancellationToken ApplicationStopped { get; }
  19:  
  20:      /// <summary>
  21:      /// Requests termination of the current application.
  22:      /// </summary>
  23:      void StopApplication();
  24:  }

IHostLifetime源码如下:

   1:  public interface IHostLifetime
   2:  {
   3:      /// <summary>
   4:      /// Called at the start of <see cref="IHost.StartAsync(CancellationToken)"/> which will wait until it's complete before
   5:      /// continuing. This can be used to delay startup until signaled by an external event.
   6:      /// </summary>
   7:      /// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
   8:      /// <returns>A <see cref="Task"/>.</returns>
   9:      Task WaitForStartAsync(CancellationToken cancellationToken);
  10:  
  11:      /// <summary>
  12:      /// Called from <see cref="IHost.StopAsync(CancellationToken)"/> to indicate that the host is stopping and it's time to shut down.
  13:      /// </summary>
  14:      /// <param name="cancellationToken">Used to indicate when stop should no longer be graceful.</param>
  15:      /// <returns>A <see cref="Task"/>.</returns>
  16:      Task StopAsync(CancellationToken cancellationToken);
  17:  }

具体的使用可以参考如下代码:

   1:  public class MyLifetime : IHostLifetime, IDisposable
   2:  {
   3:      .........
   4:  
   5:      private IHostApplicationLifetime ApplicationLifetime { get; }
   6:  
   7:      public ConsoleLifetime(IHostApplicationLifetime applicationLifetime)
   8:      {
   9:          ApplicationLifetime = applicationLifetime ?? throw new ArgumentNullException(nameof(applicationLifetime));
  10:      }
  11:  
  12:      public Task WaitForStartAsync(CancellationToken cancellationToken)
  13:      {
  14:          _applicationStartedRegistration = ApplicationLifetime.ApplicationStarted.Register(state =>
  15:          {
  16:              ((ConsoleLifetime)state).OnApplicationStarted();
  17:          },
  18:          this);
  19:          _applicationStoppingRegistration = ApplicationLifetime.ApplicationStopping.Register(state =>
  20:          {
  21:              ((ConsoleLifetime)state).OnApplicationStopping();
  22:          },
  23:          this);
  24:  
  25:          .......
  26:  
  27:          return Task.CompletedTask;
  28:      }
  29:  
  30:      private void OnApplicationStarted()
  31:      {
  32:          Logger.LogInformation("Application started. Press Ctrl+C to shut down.");
  33:          Logger.LogInformation("Hosting environment: {envName}", Environment.EnvironmentName);
  34:          Logger.LogInformation("Content root path: {contentRoot}", Environment.ContentRootPath);
  35:      }
  36:  
  37:      private void OnApplicationStopping()
  38:      {
  39:          Logger.LogInformation("Application is shutting down...");
  40:      }
  41:  
  42:      ........
  43:  }

总结

至此,我们知道了创建Long Run Program所需要关注的几个点,分别是继承IHostService、订阅程序的生命周期时间以及Host的初始化过程。相对来说这段内容还是比较简单的,但是开发过程中,依然会遇到很多的问题,比如任务的定时机制、消息的接入、以及程序的性能优化等等,这些都需要我们在实践中进一步总结完善。

.NET Core 3.0之深入源码理解Host(一) - 艾心❤ - 博客园

mikel阅读(284)

来源: .NET Core 3.0之深入源码理解Host(一) – 艾心❤ – 博客园

写在前面

ASP .NET Core中的通用主机构建器是在v2.1中引入的,应用在启动时构建主机,主机作为一个对象用于封装应用资源以及应用程序启动和生存期管理。其主要功能包括配置初始化(包括加载配置以及配置转换为通用的键值对格式),创建托管环境和Host通用上下文、依赖注入等。

在.NET Core 3.0中采用了IHostBuilder用于创建Host,同时也不再建议使用Web主机,而建议使用泛型主机,主要原因是原有的通用主机仅适用于非HTTP负载,为了提供更加广泛的主机方案,需要将HTTP管道与Web主机的接口分离出来。但Web主机仍会向后兼容。

host-update

.NET Core 3.0中创建通用主机

以下代码是V3.0中提供的模板代码,可以看到在创建主机的过程中,已经摒弃了WebHostBuilder的创建方式

   1:  public class Program
   2:  {
   3:      public static void Main(string[] args)
   4:      {
   5:          CreateHostBuilder(args).Build().Run();
   6:      }
   7:  
   8:      public static IHostBuilder CreateHostBuilder(string[] args) =>
   9:          Host.CreateDefaultBuilder(args)
  10:              .ConfigureWebHostDefaults(webBuilder =>
  11:              {
  12:                  webBuilder.UseStartup<Startup>();
  13:              });
  14:  }

而在.NET Core 2.X中

   1:  public class Program
   2:  {
   3:     public static void Main(string[] args)
   4:     {
   5:        CreateWebHostBuilder(args).Build().Run();
   6:     }
   7:  
   8:     public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
   9:        WebHost.CreateDefaultBuilder(args)
  10:           .UseStartup<Startup>();
  11:  }

V3.0模板中提供的CreateHostBuilder()方法看起来非常类似于V2.X中的CreateWebHostBuilder()。

其主要区别在于对WebHost.CreateDefaultBuilder()由Host.CreateDefaultBuilder()替换。使用CreateDefaultBuilder()辅助方法可以非常轻松地从v2.x切换到v3.0。

另一个区别是关于ConfigureWebHostDefaults()的调用。由于新的主机构建器是通用主机构建器,因此我们必须让它知道我们打算为Web主机配置默认设置。这些默认配置我们可以在ConfigureWebHostDefaults()方法中实现

CreateDefaultBuilder

该方法Microsoft.Extensions.Hosting.Host中,它是一个静态类,里面有两个方法,一个有参的CreateDefaultBuilder(string[] args),一个是无参的。

无参方法源码如下,

   1:  public static IHostBuilder CreateDefaultBuilder() =>
   2:              CreateDefaultBuilder(args: null);

可以看到该方法实际上是设置了默认值。

IHostBuilder CreateDefaultBuilder(string[] args)方法主要有以下功能:

创建HostBuilder对象

   1:  var builder = new HostBuilder();

指定Host要使用的内容根目录

   1:  builder.UseContentRoot(Directory.GetCurrentDirectory());

配置初始化(环境变量、appsettings.json、User Secrets)

   1:  builder.ConfigureHostConfiguration(config =>
   2:  {
   3:      config.AddEnvironmentVariables(prefix: "DOTNET_");
   4:      if (args != null)
   5:      {
   6:          config.AddCommandLine(args);
   7:      }
   8:  });
   9:  
  10:  builder.ConfigureAppConfiguration((hostingContext, config) =>
  11:  {
  12:      var env = hostingContext.HostingEnvironment;
  13:  
  14:      config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
  15:            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true);
  16:  
  17:      if (env.IsDevelopment() && !string.IsNullOrEmpty(env.ApplicationName))
  18:      {
  19:          var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName));
  20:          if (appAssembly != null)
  21:          {
  22:              config.AddUserSecrets(appAssembly, optional: true);
  23:          }
  24:      }
  25:  
  26:      config.AddEnvironmentVariables();
  27:  
  28:      if (args != null)
  29:      {
  30:          config.AddCommandLine(args);
  31:      }
  32:  })

日志

   1:  .ConfigureLogging((hostingContext, logging) =>
   2:  {
   3:      logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
   4:      logging.AddConsole();
   5:      logging.AddDebug();
   6:      logging.AddEventSourceLogger();
   7:  })

在开发环境模式下启用作用域验证

   1:  .UseDefaultServiceProvider((context, options) =>
   2:  {
   3:      var isDevelopment = context.HostingEnvironment.IsDevelopment();
   4:      options.ValidateScopes = isDevelopment;
   5:      options.ValidateOnBuild = isDevelopment;
   6:  });

Build

Build()方法是Microsoft.Extensions.Hosting中,并且该方法只会执行一次,当然这种一次只是在同一个实例里面

   1:  public IHost Build()
   2:  {
   3:      if (_hostBuilt)
   4:      {
   5:          throw new InvalidOperationException("Build can only be called once.");
   6:      }
   7:      _hostBuilt = true;
   8:  
   9:      BuildHostConfiguration();
  10:      CreateHostingEnvironment();
  11:      CreateHostBuilderContext();
  12:      BuildAppConfiguration();
  13:      CreateServiceProvider();
  14:  
  15:      return _appServices.GetRequiredService<IHost>();
  16:  }

该方法主要是包括以下功能:

创建HostingEnvironment

创建HostBuilderContext

配置初始化及格式标准化

DI(创建IHostEnvironment、IHostApplicationLifetime、IHostLifetime、IHost)

Run

Run方法运行应用程序并阻止调用线程,直到主机关闭

   1:  public static void Run(this IHost host)
   2:  {
   3:      host.RunAsync().GetAwaiter().GetResult();
   4:  }

以下是RunAsync的源码,此处可以通过设置CancellationToken的值,使应用程序自动关闭

   1:  public static async Task RunAsync(this IHost host, CancellationToken token = default)
   2:  {
   3:      try
   4:      {
   5:          await host.StartAsync(token);
   6:  
   7:          await host.WaitForShutdownAsync(token);
   8:      }
   9:      finally
  10:      {
  11:  #if DISPOSE_ASYNC
  12:          if (host is IAsyncDisposable asyncDisposable)
  13:          {
  14:              await asyncDisposable.DisposeAsync();
  15:          }
  16:          else
  17:  #endif
  18:          {
  19:              host.Dispose();
  20:          }
  21:  
  22:      }
  23:  }

c# 匿名方法(函数) 匿名委托 内置泛型委托 lamada - 伊一线天 - 博客园

mikel阅读(297)

来源: c# 匿名方法(函数) 匿名委托 内置泛型委托 lamada – 伊一线天 – 博客园

匿名方法:通过匿名委托 、lamada表达式定义的函数具体操作并复制给委托类型;

匿名委托:委托的一种简单化声明方式通过delegate关键字声明;

内置泛型委托:系统已经内置的委托类型主要是不带返回值的Action<T1,,,,Tn>和带返回值的Func<T1,,,Tn,Tresult>

实例代码(运行环境netcoreapp3.1)

复制代码
class demoFunc
        {
            /// <summary>
            /// 定义函数单条语句直接用lamada表达式
            /// </summary>
            /// <param name="x"></param>
           public void funcA( string x)=> Console.WriteLine("this is funcA!{0}",x);
           /// <summary>
           /// 使用内置泛型委托action(返回值为void) 定义委托类型成员变量,并通过那lamada定义匿名函数
           /// </summary>
           public Action<string> funcB= x => Console.WriteLine("this is funcB!{0}", x);
           /// <summary>
           /// 使用内置泛型委托action(返回值为void) 定义委托类型成员变量,
           /// 并通过匿名委托定义匿名函数
           /// </summary>
           public Action<string> FuncB_1= delegate(string s)
           {
               Console.WriteLine("this is funcB_1!{0}", s);
           };
           /// <summary>
           /// 定义委托类型
           /// </summary>
           /// <param name="s"></param>
           public  delegate  void TFuccB_2(string s);
           /// <summary>
           /// 使用匿名函数声明委托
           /// </summary>
           public TFuccB_2 FuncB_2= delegate(string s) {
               Console.WriteLine("this is funcB_2!{0}", s);
           };

           /// <summary>
           /// 使用内置泛型委托func(返回值不可以为void,参数列表中最后一个时返回值),
           /// 定义委托类型成员变量,并通过lamada定义单含带返回值的匿名函数
           /// 单行表达式的返回值就是此匿名函数的返回值
           /// </summary>
           public Func<string, string> funcC=x=>  string.Format("this is funcC!{0}", x);

           /// <summary>
           /// 使用内置泛型委托func(返回值不可以为void),定义委托类型成员变量,
           /// 并通过lamada定义多行代码的匿名函数
           /// </summary>
           public Func<string,string, string> funcD= (x1, x2) =>
           {
               Console.WriteLine("this is funcd!{0}{1}", x1, x2);
               return string.Format("this is funcd!{0}{1}", x1, x2);
           };

        }
复制代码

运行测试代码

复制代码
static void Main(string[] args)
        {
            demoFunc demo=new demoFunc();
            demo.funcA("a");
            demo.funcB("b");
            demo.FuncB_1("b");
            demo.FuncB_2("b");
            Console.WriteLine(demo.funcC("c"));
            Console.WriteLine(demo.funcD("d1","d2"));
            
            Console.WriteLine("Hello World!");
        }
复制代码

运行结果

复制代码
  1 "C:\Program Files\dotnet\dotnet.exe" C:/Users/edzjx/RiderProjects/testDemo/testDemo/bin/Debug/netcoreapp3.1/testDemo.dll
  2 this is funcA!a
  3 this is funcB!b
  4 this is funcB_1!b
  5 this is funcB_2!b
  6 this is funcC!c
  7 this is funcd!d1d2
  8 this is funcd!d1d2
  9 Hello World!
 10 
 11 Process finished with exit code 0.
 12
复制代码

asp.net core 3.1 入口:Program.cs中的Main函数_伊一线天的博客-CSDN博客

mikel阅读(328)

来源: asp.net core 3.1 入口:Program.cs中的Main函数_伊一线天的博客-CSDN博客

本文分析Program.cs 中Main()函数中代码的运行顺序分析ASP.NET core程序的启动,重点不是剖析源码,而是理清程序开始时执行的顺序。到底用了哪些实例,哪些法方。

ASP.NET core 3.1 的程序入口在项目Program.cs文件里,如下。

ususing System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace WebDemo
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}

public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
1. Program类
program类是定义在项目根目录Program.cs文件中,所有.net core程序的入口,包括asp.net core 程序。这很有意思,就有点类似控制端程序。相比之前的web项目没有任何程序入口只有web.config、global这类配置文件和全局文件这种没有程序入口web项目,我觉得这类更容易理解asp.net core是怎么工作的。

在asp.net core 3.1中 的Program类里,定义了2个方法:Main() 和CreateHostBuilder()

2. 主程序入口Program.Main()方法
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
Main() 方法是整个项目的入口方法,就如同C系列语言,所有的程序入口都是。这里main()只有一行代码,但是实际上执行了三个函数C:

1 IHostBuilder builder= CreateHostBuilder(args);
2 IHost host=builder.Build();
3 host.Run();
第一行,是定义在Program类的CreateHostBuilder 它主要产生一个IhostBuilder实例builder。

第二行,通过builder.Build()法方产生一个Ihost实例 host。

第三含,通过host.Run()方法,开始运行web项目,这时候就可以响应各种请求了。

而这个过程里最繁琐的就是创建builder,本文重点篇幅就是在这里。

3. 创建并配置主机Builder:Program.CreateHostBuilder(args)法方
分解CreateHostBuilder(args) 的定义
此法方通过lamada表达式定义在Pogram类中。因为它就执行了一句化,所以可以用这种简便的方式定义(关于匿名函数、lamada、内置委托可以参考我之前的文章C# 匿名方法(函数) 匿名委托 内置泛型委托 lamada1)。为了方便阅读按照上面刨析Main()法方,所以它实际的定义如下:

1 public static IHostBuilder CreateHostBuilder(string[] args)
2 {
3 IHostBuilder builder = Host.CreateDefaultBuilder(args);
4 Action < IWebHostBuilder > configAction = delegate(IWebHostBuilder webBuilder)
5 {
6 webBuilder.UseStartup<Startup>();
7 };
8 builder=builder.ConfigureWebHostDefaults(configAction);
9 return builder;
10 }
3.1. builder诞生 :生成IHostBuilder
将从Host.CreateHostBuilder()法方分析入手
CreateHostBuilder()法方的第一行代码是调用Host.CreateDefaultBuilder(args)法方,来创建IBuilder 对象实例。

IHostBuilder builder = Host.CreateDefaultBuilder(args);
IHostBuilder是一个非常重要的实例,有了这个实例才可以继续后续的加载更多的配置操作。通过IHostBuilder.Build()生产IHost实例。最终通过IHost.Run()运行项目。

3.1.1 Host类——用于产生初始的builder静态类

Host类是定义在Microsoft.Extensions.Hosting命名空间(Program.cs中引用了该命名公开)下的静态类.在Program.cs里引入。

声明如下:

1 using Microsoft.Extensions.Hosting;
2
3 namespace Microsoft.Extensions.Hosting
4 {
5 public static class Host
6 {
7 public static IHostBuilder CreateDefaultBuilder();
8 public static IHostBuilder CreateDefaultBuilder(string[] args);
9 }
10 }
Host静态类就一个法方就是CreateDefaultBuilder() 。合计2个重载声明:一个带参数,一个不带参数。参考源码(.NET Core 3.0之深入源码理解Host(一)2)不带参数的重载实际在是将null作为参数调用带参数形式,如下:

1 public static IHostBuilder CreateDefaultBuilder() =>CreateDefaultBuilder(args: null);
3.1.2 Host.CreateDefaultBuilder(args)方法
官方说明是用预先配置的默认值初始化一个Microsoft.Extensions.Hosting.HostBuilder实例(Initializes a new instance of the Microsoft.Extensions.Hosting.HostBuilder class with pre-configured defaults,详细见参考官方文档3)。
相关操作有:设置根目录ContentRootPath ;给Host.IConfiguration 加载:环境变量EnvironmentName,命令行参数args,配置文件IConfiguration.[EnvironmentName] json;还有加载日志模块等等。
详细见参考文档2、3,其中.NET Core 3.0之深入源码理解Host(一),作者从源码角度剖析了此法方,官方文档Host.CreateDefaultBuilder 方法官方说明了法方操作那些内容。

3.2. IHostBuilder转变成IWebHostBuilder
继续对Host.CreateHostBuilder()法方分析,在3.1建立了IHostBuilder实例后,将通过builder.ConfigureWebHostDefaults(Action <IWebHostBuilder >)方法将IHostBuilder实例转变为一个web主机性质的webbuilder(IWebHostBuilder)
IHoustBuilder.ConfigureWebHostDefaults(Action <IWebHostBuilder>)方法定义在Microsoft.Extensions.Hosting.GenericHostBuilderExtensions静态类中(GenericHostBuilderExtensions.cs),此静态类就定义这一个静态方法。

3.2.1. ConfigureWebHostDefaults()参数
此处参数configure是一个内置委托Action <IWebHostBuilder>,这个委托的定义就执行一行代码:
webBuilder.UseStartup<Startup>();

3.2.2. ConfigureWebHostDefaults()方法定义
其实ConfigureWebHostDefaults(Action <IWebHostBuilder >)方法是一个IHostBuilder的扩展方法(所以之前的写法并不准确,缺少了this builder参数,之前省略了)。
为了方便阅读法,用3.中的方式码剖析如下:
1 public static IHostBuilder ConfigureWebHostDefaults(this IHostBuilder builder, Action<IWebHostBuilder> configure)
2 {
3 Action<IWebHostBuilder> webconfigure = delegate(IWebHostBuilder webHostBuilder)
4 {
5 WebHost.ConfigureWebDefaults(webHostBuilder);
6 configure(webHostBuilder);
7 };
8 return builder.ConfigureWebHost(webconfigure);
9 }
其实就调取了IHostBuilder的另一个扩展类ConfigureWebHost(Action<IWebHostBuilder>)(此扩展法方定义在Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions静态类中)。
3.2.2.1 把IHostBuilder转变为IWebHostBuilder: builder.ConfigureWebHost(webconfigure)法方
3.2.2.1.1 Action<IWebHostBuilder> webconfigure参数
内置委托,在这里对委托进行了定义,主要两行代码:
配置IWebHostBuilder的默认web设置。
WebHost.ConfigureWebDefaults(webHostBuilder);
在core2.1中该方法是在CreateHostBuilder()中WebHost.CreateDefaultBuilder(arg)中调用,core3.1,改为Host.ConfigureDefualts(arg)后,通过委托在ConfigureWebHost中调用。因为此方法在委托里调用,在下面章节降到委托执行的时候再详细说明。
调用参数传入的委托
configure(webHostBuilder);
就是3.2.1中的
webBuilder.UseStartup<Startup>();
具体在下面章节讲解委托执行时在详细说明。
3.2.2.1.2 builder.ConfigureWebHost(webconfigure)法方定义
定义在Microsoft.Extensions.Hosting.GenericHostWebHostBuilderExtensions(GenericHostWebHostBuilderExtensions.cs)
1 using System;
2 using Microsoft.AspNetCore.Hosting;
3 using Microsoft.AspNetCore.Hosting.Internal;
4
5 namespace Microsoft.Extensions.Hosting
6 {
7 public static class GenericHostWebHostBuilderExtensions
8 {
9 public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure)
10 {
11 var webhostBuilder = new GenericWebHostBuilder(builder);
12 configure(webhostBuilder);
13 return builder;
14 }
15 }
16 }

Microsoft.AspNetCore.Hosting.Internal.GenericWebHostBuilder
在asp.net core 2.1 里IWebHostBuilder 在asp.net core 3.1里是怎么把泛型IHostBuilder生产webhost的builder的了,终于扒到它了。
通过Microsoft.AspNetCore.Hosting.Internal.GenericWebHostBuilder类实例。GenericWebHostBuilder是一个内部类。这个类网上资源极少,甚至在官方文档里找不到说明,但可以参考其源码GenericWebHostBuilder.cs源代码。此类声明和其构造函数声明如下:
internal class GenericWebHostBuilder : IWebHostBuilder, ISupportsStartup, ISupportsUseDefaultServiceProvider
{
public GenericWebHostBuilder(IHostBuilder builder);
}
此类非常重要,它是Asp.net core 3.1 项目中IHostBuilder转变成IWebHostBuilder的基石或者源头 一个IHostBuilder实例最早就是通过此类构造函数实例化后变成了2.1里的IWebHostBuilder。实际干的就是注入了一些web有关的服务,详细建议查看源码(参考文档4)
3.2.2.2 给IWebHostBuilder配置Web相关参数委托参数:终于执行委托了
configure(webhostBuilder);
在传入了那么多层委托后,终于我们扒到底了,在实现了IHostBuilder转变为IWebHostBuilder后,所有config有关的委托终于得以执行
1 //在 builder.ConfigureWebHostDefaults()(–>在program.GreateHostBuilder()中)
2 //和下一句代码一同作为委托参数传入builder.ConfigureWebHost()
3 WebHost.ConfigureWebDefaults(webHostBuilder);
4 //Program.CreateHostBuilder()里的作为委托参数传入builder.ConfigureWebHostDefaults()
5 //再经过builder.ConfigureWebHostDefaults()合上一句一同作为委托传入builder.ConfigureWebHost()
6 webBuilder.UseStartup<Startup>();
3.2.2.2.1 WebHost.ConfigureWebDefualts(IWebHostBuilder)方法:
此方法在Asp.net core 2.1中 是WebHost.CreateDefaultBuilder(args) 里最后调用的方法,而在Asp.net core 3.1中,把IWebHostBuilder该为泛型IHostBuilder后,在执行IHostBuilder.ConfigureWebHost()时回调委托执行。此方法作用类似与Host.CreateDefaultBuilder(args),使用预配置配置一些关于Web方式的参数。主要是注入一些web相关的服务,配置主机地址等。此方法没有官方文档说明,建议直接查看源码WebHost.cs源代码(参考文档5)。
3.2.2.2.2 webBuilder.UseStartup<Startup>()法方:
UseStartup()是webbuilder的扩展发放,定义在Microsoft.AspNetCore.Hosting.WebHostBuilderExtensions静态类中(WebHostBuilderExtensions.cs),此方法申明如下
public static IWebHostBuilder UseStartup(this IWebHostBuilder hostBuilder, Type startupType)
从方法名就可以看出是使用Startup类,这个方法主要使用反射技术,反射Startup类,响应startup类中的配置的信息

3.3. 最后梳理一下builder核心的代码
1.IHostBuilder builder = Host.CreateDefaultBuilder(args);
创建一个基础builder(读取app.json)
2.IWebHostBuilder webHostBuilder= new GenericWebHostBuilder(builder);
转换为webbuilder
3.WebHost.ConfigureWebDefaults(webHostBuilder);
给webbuilder使用预先的默认值配置
4.webBuilder.UseStartup<Startup>();
读取startup类配置信息

4.总结
aps.net core 3.1的程序启动代码分析下来。基本3个概念创建builder ,通过builder生成host ,最后使用host执行起来。其中builder及builder配置 ,host主机都是重要的概念。core 3.1 和 core2.1的区别,就是把IWebHostBuilder上再抽象一个IHostBuilder,可以是core 3.1的代码更加的灵活,以后的服务可以不单单是web服务。
另外asp.net core 的builder 配置中可以看到很多非常依赖,控制反转技术也就是注入依赖,好处就是想要什么服务就注册什么服务,更加灵活。
如果想深入学习,建议参考如下文章:

1. .NET Core 3.0之深入源码理解Host(一)
2. .NET Core 3.0之深入源码理解Host(二)
作者的这个2个文章从源码角度简介了Ihostbuilder的相关知识剖析深度更深
3. 官方文档:.NET 通用主机 讲解IHostBuilder相关知识
4. 官方文档:ASP.NET Core 中的应用启动 介绍startup类相关知识

参考文档:
1. C# 匿名方法(函数) 匿名委托 内置泛型委托 lamada 作者:edzjx
2. .NET Core 3.0之深入源码理解Host(一) 作者:艾心
3. 官方文档Host.CreateDefaultBuilder 方法 作者:Micrsoft官方文档
4. GenericWebHostBuilder.cs源代码 作者:Asp.net@github
5. WebHost.cs源代码 作者:Asp.net@github
6. .NET Core 3.0之深入源码理解Host(二) 作者:艾心
7. 官方文档:.NET 通用主机 作者:Micrsoft官方文档
8. 官方文档:ASP.NET Core 中的应用启动 作者:Micrsoft官方文档
————————————————
版权声明:本文为CSDN博主「伊一线天」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/edzjx/article/details/104257596