一次 .NET 性能优化之旅:将 GC 压力降低 99% - 智汇IOT - 博客园

mikel阅读(196)

来源: 一次 .NET 性能优化之旅:将 GC 压力降低 99% – 智汇IOT – 博客园

一次 .NET 性能优化之旅:将 GC 压力降低 99%

前言:问题的浮现

最近,我使用 ScottPlot 库开发一个频谱分析应用。应用的核心功能之一是实时显示频谱图,这可以看作是一个高频刷新热力图(Heatmap)。然而,在程序运行一段时间后,我注意到整体性能开始逐渐下降,界面也出现了卡顿。直觉告诉我,这背后一定隐藏着性能瓶颈。

分析:探寻性能瓶颈

面对性能问题,我首先打开了 Visual Studio 的诊断工具,重点关注计数器(Counters)的变化。

图片

VS 诊断工具

上图揭示了几个严重的问题:

  1. 1. GC 频繁:进程内存图表中,GC(垃圾回收)标记几乎连成一片,表明垃圾回收异常频繁。
  2. 2. GC 耗时过长:% Time in GC since last GC 的值非常高,说明 GC 占用了大量的 CPU 时间。
  3. 3. 高内存分配率:Allocation Rate 居高不下,意味着程序在以极高的速率分配内存。

显然,问题出在 GC 上。但究竟是哪部分代码导致了如此巨大的 GC 压力呢?

定位:追踪 GC 的“元凶”

为了找出问题的根源,我使用了 Visual Studio 的性能探查器(Performance Profiler),并选择了 .NET 对象分配跟踪(.NET Object Allocation Tracking)模式。

在程序运行一段时间后,我停止了分析,并查看了分配(Allocations)选项卡。结果令人震惊:System.Double 类型的分配次数和字节数都异常巨大。这正是导致 GC 频繁的“元凶”。

通过调用堆栈,我迅速定位到了问题代码:

图片

调用堆栈

函数名                                          分配        字节          模块名称
+ ScottPlot.NumericConversion.Clamp<T>(T, T, T)    3,592,245    86,213,880    scottplot

所有的矛头都指向了 ScottPlot.NumericConversion.Clamp<T>(T, T, T) 这个函数。

探究:泛型与装箱的“陷阱”

为了弄清真相,我翻阅了 ScottPlot 的源代码,并梳理了整个调用流程:

  1. 1. 在绘制热力图时,程序会调用 NumericConversion.Clamp 函数,将数据归一化到 0-1 的范围内。
  2. 2. 接着,程序会根据归一化后的值,从颜色映射表(ColorMap)中获取对应的颜色。
public Color GetColor(double position)
{
    position = NumericConversion.Clamp(position, 01);
    int index = (int)((Colors.Length - 1) * position);
    return Colors[index];
}

问题就出在 NumericConversion.Clamp 函数的实现上:

public static T Clamp<T>(T input, T min, T max) where T : IComparable
{
    if (input.CompareTo(min) < 0) return min;
    if (input.CompareTo(max) > 0) return max;
    return input;
}

这是一个泛型方法,并且 double 是值类型。当 double 作为参数传递给这个泛型方法时,会发生装箱(boxing),即 double 被转换为 IComparable 接口。在每秒数万次的调用下,这会导致频繁的堆分配,从而引发巨大的 GC 压力。

深究:为何会发生装箱?

首先感谢两位大神的指出,问题的根源在于 Clamp<T> 方法的泛型约束 where T : IComparable,修改为使用 where T : IComparable<T>就可以避免装箱的问题。但为什么这个约束会导致装箱呢?

答案隐藏在 IComparable 接口的定义之中。让我们来看一下它的 CompareTo 方法:

// 非泛型版本
public interface IComparable
{
    int CompareTo(object? obj);
}

正如你所见,CompareTo 方法接受一个 object 类型的参数。当我们将像 double 这样的值类型传递给它时,CLR 为了匹配方法签名,必须将其转换为引用类型。这个从值类型到 object 的转换过程,就是装箱。每一次装箱都会在托管堆上分配一小块内存,在高频调用的场景下,这会迅速累积成巨大的内存压力,迫使 GC 频繁介入。

.NET 同时为我们提供了泛型版本的 IComparable<T> 接口:

// 泛型版本
public interface IComparable<in T>
{
    int CompareTo(T? other);
}

看到区别了吗?这个版本的 CompareTo 方法接受的是一个类型为 T 的参数。由于 double 等基础值类型已经实现了 IComparable<double>,编译器可以进行类型匹配,从而直接调用,完全避免了装箱操作。

因此,如果 ScottPlot 的源代码将约束改为 where T : IComparable<T>,就可以从根本上解决装箱导致的这个性能问题。不过,直接使用对应值类型的重载的性能还是会大幅的高于IComparable的版本,具体原因这里就不展开讲了。

优化:小改动,大提升

找到了问题的根源,解决方案也就水到渠成了。我为 Clamp 函数添加了一个 double 类型的重载版本,从而避免了装箱操作:

public static double Clamp(double input, double min, double max)
{
    if (input < minreturn min;
    if (input > maxreturn max;
    return input;
}

测试:验证优化效果

为了验证优化效果,我使用 LinqPad 和 BenchmarkDotNet 进行了性能测试。

#load "BenchmarkDotNet"

void Main()
{
    RunBenchmark();
}

privatedoublevalue = 0.75;
privatedouble min = 0.0;
privatedouble max = 1.0;

[Benchmark]
public double Clamp_Double()
    => NumericConversion.Clamp(value, min, max);

[Benchmark]
public double Clamp_Generic()
    => NumericConversion.Clamp<double>(value, min, max);

publicstaticclassNumericConversion
{
    public static double Clamp(double valuedouble min, double max)
        => value < min ? min : (value > max ? max : value);

    public static T Clamp<T>(T input, T min, T maxwhere T : IComparable
    {
        if (input.CompareTo(min) < 0return min;
        if (input.CompareTo(max) > 0return max;
        return input;
    }
}

测试结果如下:

图片

性能测试结果

从上图可以看出,新添加的 Clamp_Double 方法在性能上远超泛型版本。

再次打开 Visual Studio 的诊断工具,GC 压力几乎消失了:

图片

优化后诊断工具

总结:性能优化的启示

通过对 GC 压力的分析和优化,我成功解决了程序中的性能瓶颈。这次优化的核心在于,通过为 NumericConversion.Clamp 函数添加 double 类型的重载,避免了高频调用下的装箱操作,从而显著提升了性能,并将 GC 压力降低了 99% 以上。

这次经历不仅提升了程序的运行效率,也为我未来的性能调优工作积累了宝贵的经验。

目前,我已经将针对 ScottPlot 源码的修改提交了 PR:https://github.com/ScottPlot/ScottPlot/pull/4985

C#.Net筑基-优雅LINQ的查询艺术 - 安木夕 - 博客园

mikel阅读(260)

来源: C#.Net筑基-优雅LINQ的查询艺术 – 安木夕 – 博客园

Linq(Language Integrated Query,集成查询语言),顾名思义就是用来查询数据的一种语言(可以看作是一组功能、框架特性的集合)。在.NETFramework3.5(大概2007年)引入C#,用统一的C#语言快速查询各种数据,如数据库、XML文档、对象集合等等。Linq的诞生对 C# 编程领域产生了深远而持久的影响,改变了开发人员对查询的思考方式。

  • 使用简单:统一语法(链式方法语法、类似SQL的查询语法),智能提示。
  • 类型安全:编译时强类型检查,减少运行时错误。
  • 延迟执行,查询本身只是构建了一个表达式,在真正使用的时候(foreach、ToList、查询数据库)才会执行。
  • 支持多种数据源:内存中的集合,以及各种外部数据库。

Linq支持查询任何实现了IEnumerable<T>接口的集合类型,基本上所有集合数据都支持Linq查询。如下示例:大于 5 的偶数,并倒叙排列取前三名

//方法链语法
var query = arr.Where(n => n > 5 && n % 2 == 0).OrderByDescending(n => n).Take(3);

01、Linq 基础概括

1.1、Linq语法:链式方法、查询表达式

Linq 有两种语法风格,如下实例代码,一种是常规C#方法调用方式,另外一种是类似SQL的查询表达式。这两种语法其本质是一样的,编译后的中间语言(IL)是一样的,确实仅仅只是语法形式不同而已

🔸链式方法:就是字面意思,函数式方法调用。这些方法都来自 IEnumerable 接口或 IQueryable 接口的扩展方法,这些方法提供了过滤、聚合、排序等多种查询功能。

🔸查询表达式:查询表达式由一组用类似于 SQL 的声明性语法所编写的子句组成。 每个子句依次包含一个或多个 C# 表达式,而这些表达式可能本身就是查询表达式,或者包含查询表达式。查询表达式必须以 from 子句开头,且必须以 select 或 group 子句结尾。

//方法链语法
var query = arr.Where(n => n > 5 && n % 2 == 0).OrderByDescending(n => n).Take(3);
//查询表达式语法,类似数据库SQL语言+C#的语法风格
var query2 = (from n in arr
where n > 5 && n % 2 == 0
orderby n descending
select n).Take(3);
比较 链式方法 查询表达式(SQL)
特点 链式方法调用,函数式编程 类似SQL语句,自然语言,容易掌握
语法形式 点点点链式方法调用,Where().Select().Order() from开头:from...where...select
常用方法/语法 System.Linq 上提供的扩展方法或第三方扩展:Where、OrderBy、Select、Skip、Take、Union 仅支持编译器识别的关键字:from、where、orderby、group、join、let、select、into、in、on等
本质 System.Linq 提供的扩展方法调用 编译为标准查询运算符方法调用,编译结果和链式方法一样
功能完整性 完整的Linq功能 有些能力没有对应语法(如Max),需要结合链式方法使用

📢 两种编写方式编译后生成的IL代码实际上是一样的,也可以混合使用,因此他们并没有性能差异。

查询表达式并不能实现获取前3个元素,此时就需要两者混合使用,

var query = from u in list
where u.Age>14
group u by u.Address into gu
orderby gu.Count() descending
select (gu.Key,gu.Count());
query = query.Take(3);

1.2、Linq执行:本地查询、解释型查询

LINQ 提供了两种用途的架构:针对本地(内存)对象的本地查询,以及针对远程数据源(数据库)的解释性查询。两者的语法形式基本一样,都支持链式方法、查询表达式。

🔸本地查询:实现了针对IEnumerable的内存集合(数组、List)的查询,其Linq的扩展方法都在 System.Linq.Enumerable 类中。查询只是构建了一个可枚举的迭代装饰器序列,延迟在使用(消费)数据时执行。

🔸解释查询:解释查询是描述性的,实现了针对IQueryable(Table、DbSet)的远程数据查询,对应扩展方法都在 System.Linq.Queryable 类中。他们在运行时生成表达式树,并进行解释为SQL语句,在数据库中执行该SQL语句并获取数据。

比较 本地查询 Enumerable 解释查询 Queryable
操作对象 内存中的集合(IEnumerable<T> 外部数据源的查询接口(IQueryable<T>
延迟执行 支持,真正使用(消费)数据时才执行,如 foreach、ToList 支持,消费数据时才翻译成SQL并在数据库中执行获取数据
执行原理 参数为委托方法,C#内部执行委托、迭代器 参数为表达式树,LINQ Provider 在运行时遍历该树转换为目标语言(如 SQL)
谁来执行 CLR本地执行,数据在内存中 数据库执行SQL,数据在数据库中
执行过程 本地逐个元素迭代调用委托 数据库中执行SQL,返回查询结果
使用场景 List、Array、普通内存数据 Entity Framework、LINQ to SQL、MongoDB 查询
语法 都支持链式方法、表达式查询 同样支持链式方法、表达式查询
Linq方法在哪里? System.Linq.Enumerable 静态类 System.Linq.Queryable 类,方法和 Enumerable 大部分对应。有些方法并不能生成数据库兼容的SQL语法。
扩展性 内存查询支持任意C#方法,扩展性强 受限,只能使用数据库兼容的方法。如正则表达式SQLServer就不支持。
结合使用 本地数据只能用本地查询 远程数据可以结合本地查询混用。

IQueryable 继承自 IEnumerable,因此解释查询可以转换为本地查询,query.AsEnumerable(),不过需谨慎使用,会将数据库的相应数据都加载到内存中。

public interface IQueryable<out T> : IEnumerable<T>, IEnumerable, IQueryable
{
}

📢表达式树是一个微型的代码DOM结构,树中的节点是Expression类型的节点,涵盖各种语法形式,如参数、变量、常量、赋值、比较、循环等等。表达式树可以转换(Compile)为委托,反之则不能。

1.3、延迟执行的注意事项

延迟执行是指查询代码不会立刻执行,而是在正真取数的时候才会执行。他是Linq最主要的特点,是优点,也不全是,有些需要注意的地方。

  • 并不是所有的Linq方法都是延迟的,如:First()、Last()、ToArray()、ToList(),及Count、Max等聚合计算方法会立即执行。
  • 如果数据源变了,结果也会变化。
List<int> list = [2,3,9,4,5];
var query = list.Where(s=>s>5);
Console.WriteLine(query.Sum()); //9
list.Add(6);
Console.WriteLine(query.Sum()); //15
  • 重复取数时,查询也会重复执行,可能会浪费性能,特别是复杂、耗时的查询。避免的方式就是query.ToList() 一次性立即获取数据。
  • Lambda变量捕获,变量的值在真正执行查询的时候才会获取,这是方法闭包的特点。
List<int> list = [2,3,9,4,5];
int n = 5;
var query = list.Where(s=>s>n);
n = 4;
Console.WriteLine(query.Sum()); //14 //使用的n=4

1.4、迭代装饰器序列

为了支持延迟执行,Linq内部封装了很多迭代装饰器,偷偷看了下源码,如 WhereIteratorSelectEnumerableIteratorReverseIteratorUnionIterator 等,都是Linq内部的迭代装饰器。迭代装饰器会保留输入序列的引用及其他相关参数,仅当枚举结果时才会执行。

迭代序列装饰器本身继承自IEnumerable,因此就支持装饰器之间的嵌套。下面为迭代装饰器序列基类的源码 Iterator.cs

internal abstract class Iterator<TSource> : IEnumerable<TSource>, IEnumerator<TSource>
{
private readonly int _threadId;
internal int _state;
internal TSource _current = default!;
}


2、Enumerable扩展方法汇总⭐

内存集合的Linq扩展方法,基本都来自Enumerable类,参考官方 Enumerable 类。用于数据库的解释性查询方法在 System.Linq.Queryable 类中,方法和 Enumerable 基本上都是对应的。基本上所有的Linq方法都在这里汇总:

方法 说明
Chunk(Int32) 分块拆分为多个固定大小的数组,返回IEnumerable<TSource[]>,内部每次迭代会构建一个数组new TSource[arraySize]
Append(T) 末尾追加一个元素,原理是内部构建了一个新的迭代器AppendPrepend1Iterator实现返回这个元素。
Prepend(T) 在前面追加一个元素,原理同上,是同一个AppendPrepend1Iterator
🔸聚合计算,立即执行
Count() 获取集合中元素的数量,可指定条件参数Func。arr.Count(),内部原理比较简单,如果集合是ICollection等,则直接获取Count,否则只能e.MoveNext()一个一个的数了。
TryGetNonEnumeratedCount 获取元素数量,在不真正遍历(不枚举)集合的情况下,尽量尝试快速拿到集合元素的数量
Max() 返回最大的那个元素。截止.NET8,整数类型用了Vector提升性,其他循环比较,性能一般😒。
Min() 返回最小的那个元素,性能原理同Max
Average() 计算平均值,对于数值类型,内部用到了Vector,性能还是不错的。var a = arr.Average()
Sum() 求和,arr1.Sum()
Aggregate(Func) 执行累加器函数,函数的的输出为作为下一轮迭代的输入,依次迭代执行。
示例,计算序列最大值:var max = arr.Aggregate((acc,n)=>acc>n?acc:n)
🔸条件判断
Contains(T) 判断是否包含指定元素,返回bool,可指定比较器。bool f = arr.Contains(6)
Any() 集合是否包含元素,判断集合是否不为空。if(arr.Any()){}
Any(Func) 集合是否包含指定条件的元素,示例:是否有人考试满分,bool flag = arr.All(n=>n==100)
All(Func) 所有元素是否满足条件,示例:是否所有同学都及格了,bool flag = arr.All(n=>n>=60)
SequenceEqual(IEnumerable) 序列相等比较,比较两个序列是否相同,长度相同、每个元素相等则返回True
🔸元素选择
First() 返回第一个元素,如果一个都没有抛出异常,arr1.First()
FirstOrDefault() 返回第一元素,如果一个都没有则返回默认值,arr1.FirstOrDefault()
Last() 返回最后一个元素,如果一个都没有抛出异常。如果不是常规集合,会foreach循环所有😒。
LastOrDefault() 同上,如果一个都木有则返回默认值
Single()SingleOrDefault() 获取唯一元素,如果元素数量大于1则抛出异常。这个方法在数据库按主键查询时比较有用。
ElementAt(Index) 返回指定索引Index位置的元素,arr.ElementAt(0)。还有个更安全的 ElementAtOrDefault
DefaultIfEmpty(defaultT) 如果集合为空(集合中没有元素)返回含一个默认值的IEnumerable,否则返回原序列。
🔸筛选查询
Where(Func) 条件查询,最常用的Linq函数了,arr1.Where(s=>s>5)
Select(selector) 返回指定Key(元素选择处理器结果)的集合,list.Select(s=>s.Name+s.Age)
SelectMany() 将每个元素的“内部集合”展开合并为一个大集合,list.SelectMany(s=>s.Name.Split('-'))
Distinct() 去重,arr.Distinct(),内部使用HashSet<TSource>来去重。DistinctBy>可指定键Key。
OfType() 根据类型T筛选集合,源码中用obj is TResult来筛选,不符合的丢弃。list.OfType<double>()
Skip(int count) 跳过指定数量的元素,返回剩余的元素,arr1.Skip(5)
SkipLast(int count) 忽略后面的元素,返回前面剩余的元素。arr1.SkipLast(3)
SkipWhile(Func) 从开头跳过符合条件的元素,直到遇到不符合条件时停下,返回剩下的元素。
Take(int count) 返回前n个元素,Skip的逆运算,Take(3)
TakeLast(int count) 返回最后n个元素,arr1.TakeLast(3)
TakeWhile(Func) 从开头返回符合条件的元素,直到遇到不符合条件时停下,与SkipWhile相反arr1.TakeWhile(s=>s<5)
🔸排序分组
Order() 升序排列集合,arr2.Order()
OrderBy(TKey) 指定Key键升序排列集合,list.OrderBy(s=>s.Age)
OrderByDescending(TKey) 指定Key键降序排列集合,list.OrderByDescending(s=>s.Age)
ThenByThenByDescending 二次排序,跟着OrderBy使用,设置第二排序键。list.OrderBy(s=>s.Grade).ThenBy(s=>s.Age)
Reverse() 反转序列中元素的顺序,arr2.Reverse()。内部源码是创建了一个数组来实现翻转,性能不佳😒,数组推荐使用Array.Reverse(),原地翻转,不会创建额外对象。
GroupBy 按指定的Key分组,返回一个分组集合IGrouping<TKey, TSource>list.GroupBy(s=>s.Name)
GroupJoin 带分组的连接(Join)操作,类似Sql中的Left Join + 分组,每个「左边元素」对应到「右边的一组元素」
🔸多集合操作
Union(IEnumerable) 并集,合并两个集合并去重arr1.Union(arr2)
Intersect(IEnumerable) 交集(Intersect /ˌɪntəˈsekt/ 相交),返回两个集合都包含的元素。IntersectBy 可指定键Key。
Except(IEnumerable) 移除(Except /ɪkˈsept/ 除外)arr1.Except(arr2)移除arr2中也存在的元素。ExceptBy可指定键Key。
Concat(IEnumerable) “合并”两个序列集合(),内部由私有的ConcatIterator实现的连接迭代,arr.Concat([3])
Join(arr2, k2,k1,Func) 两个“表”内连接,类似Sql中的 Inner Join,用于两个不同类型元素的的连接,两个表Key匹配的元素合并
Zip 就像拉链(zipper)一样,把两个序列一对一地配对合并成一个新序列,arr1.Zip(arr2,(n1,n2)=>n1+n2)
🔸转换,ToXX立即执行 谨慎使用,会创建新的集合对象
Cast() 强制类型转换,内部使用强制转换“(TResult)obj
ToArray() 从 IEnumerable 创建新数组,慎用。var narr = arr1.Order().ToArray()
ToList() 从 IEnumerable 创建新Listarr1.Take(3).ToList()
ToHashSet 从 IEnumerable 创建新HashSet(不可重复集合,自动去重),arr1.ToHashSet()
ToDictionary() 从 IEnumerable 创建新字典Dictionary<TK,TV>list.ToDictionary(s=>s.Name,s=>s.Age)
ToLookup() 从 IEnumerable 创建新 Lookup分组的字典),arr1.ToLookup(s=>s%2)
🔸其他
Range(start, end) 静态方法,创建一个连续的序列,可用来创建测试数据,Enumerable.Range(1,10).ToArray()
Repeat(T, count) 静态方法,创建一个重复值的序列,Enumerable.Repeat(18,10)
Empty() 静态方法,获得一个空的序列,Enumerable.Empty<int>().Any(); //false
AsEnumerable() 返回自己,什么也不干。在Linq to SQL中可以强制让后续操作在本地内存中进行,而不会翻译成SQL。

2.1、示例:构建动态查询

根据用户输入条件,构建动态查询条件,使用 Skip 和 Take 实现分页

var query = list.AsEnumerable();
if (!string.IsNullOrWhiteSpace(name))
query = query.Where(s => s.Name.Contains(name, StringComparison.OrdinalIgnoreCase));
if (age.HasValue)
query = query.Where(s => s.Age == age);
if (!string.IsNullOrWhiteSpace(address))
query = query.Where(s => s.Address.Contains(address));
//使用 Skip 和 Take 实现分页
query = query
.Skip((pageNumber – 1) * pageSize)
.Take(pageSize);
//执行查询,获取结果
var result = query.ToArray();

2.2、自定义扩展

本地查询扩展是很容易的,基于IEnumerable<T>实现扩展方法即可。IQueryable扩展则要考虑数据库的支持和映射,一般无需自定义扩展。

//交替获取元素集合
public static IEnumerable<T> AlternateElements<T>(this IEnumerable<T> source)
{
int index = 0;
foreach (T element in source)
{
if (index % 2 == 0)
{
yield return element;
}
index++;
}
}
//使用
var query = list.AlternateElements();

参考资料

Spring用到的10种设计模式,真巧妙! - 苏三说技术 - 博客园

mikel阅读(221)

来源: Spring用到的10种设计模式,真巧妙! – 苏三说技术 – 博客园

前言

作为一名有多年开发经验的老司机,每次翻看Spring源码都让我感叹:”这哪是框架,分明是设计模式的百科全书!”

有些小伙伴在工作中可能只会用@Autowired,却不知背后藏着多少精妙设计。

今天这篇文章跟大家一起聊聊Spring中最常用的10种设计模式,希望对你会有所帮助。

1 模板方法模式:流程骨架大师

场景:处理重复流程但允许细节变化
Spring应用JdbcTemplateRestTemplate

// 伪代码展示模板方法核心
public abstract class JdbcTemplate {
    // 定义算法骨架(不可重写)
    public final Object execute(String sql) {
        Connection conn = getConnection(); // 抽象方法
        Statement stmt = conn.createStatement();
        ResultSet rs = stmt.executeQuery(sql);
        Object result = mapResult(rs);     // 抽象方法
        releaseResources(conn, stmt, rs);
        return result;
    }
    
    // 留给子类实现的钩子方法
    protected abstract Connection getConnection();
    protected abstract Object mapResult(ResultSet rs);
}

为什么用

  1. 复用资源管理(连接获取/释放)等通用逻辑
  2. 允许子类只关注业务差异(如结果映射)
    思考:当你写重复流程时,想想能否抽出模板骨架

2 工厂模式:对象出生管理局

场景:解耦对象创建与使用
Spring应用BeanFactory核心接口

public interface BeanFactory {
    Object getBean(String name);
    <T> T getBean(Class<T> requiredType);
}

// 实现类:DefaultListableBeanFactory
public class UserService {
    // 使用者无需关心Bean如何创建
    @Autowired 
    private OrderService orderService; 
}

设计精髓

  • 隐藏复杂的对象初始化过程(如循环依赖处理)
  • 统一管理对象生命周期(单例/原型等作用域)
    类比:就像点外卖不需要知道厨师怎么做菜

3 代理模式:隐形护卫

场景:无侵入增强对象功能

Spring应用:AOP动态代理

// JDK动态代理示例
public class LogProxy implements InvocationHandler {
    private Object target;
    
    public Object createProxy(Object target) {
        this.target = target;
        return Proxy.newProxyInstance(
            target.getClass().getClassLoader(),
            target.getClass().getInterfaces(),
            this);
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
        System.out.println("【日志】调用方法: " + method.getName());
        return method.invoke(target, args); // 执行原方法
    }
}

// Spring中通过@Aspect实现类似功能
@Aspect
@Component
public class LogAspect {
    @Before("execution(* com.example.service.*.*(..))")
    public void logMethodCall(JoinPoint jp) {
        System.out.println("调用方法: " + jp.getSignature().getName());
    }
}

动态代理两板斧

  1. JDK代理:基于接口(要求目标类实现接口)
  2. CGLIB代理:基于继承(可代理普通类)
    价值:业务逻辑与横切关注点(日志/事务等)彻底解耦

4 单例模式:全局唯一指挥官

场景:减少资源消耗,保证全局一致性

Spring实现:Bean默认作用域

// 源码片段:AbstractBeanFactory
public Object getBean(String name) {
    Object bean = getSingleton(name); // 先查缓存
    if (bean == null) {
        bean = createBean(name);      // 不存在则创建
        addSingleton(name, bean);     // 放入缓存
    }
    return bean;
}

关键设计

  • 三级缓存解决循环依赖(singletonObjects, earlySingletonObjects, singletonFactories)
  • 并发安全通过synchronized+双重检查锁定实现
    警示:切忌在单例Bean中保存状态变量!

5 观察者模式:事件广播网

场景:解耦事件生产者和消费者

Spring应用ApplicationEvent机制

// 1. 定义事件
public class OrderCreatedEvent extends ApplicationEvent {
    public OrderCreatedEvent(Order source) {
        super(source);
    }
}

// 2. 发布事件
@Service
public class OrderService {
    @Autowired ApplicationEventPublisher publisher;
    
    public void createOrder(Order order) {
        // 业务逻辑...
        publisher.publishEvent(new OrderCreatedEvent(order));
    }
}

// 3. 监听事件
@Component
public class EmailListener {
    @EventListener
    public void handleOrderEvent(OrderCreatedEvent event) {
        // 发送邮件通知
    }
}

优势

  • 事件源与监听器完全解耦
  • 支持异步处理(加@Async注解即可)

6 策略模式:算法切换器

场景:动态选择算法实现

Spring应用Resource资源加载

// 资源加载策略族
Resource res1 = new ClassPathResource("config.xml"); // 类路径策略
Resource res2 = new UrlResource("http://config.com");// 网络策略
Resource res3 = new FileSystemResource("/opt/config");// 文件系统策略

// 统一调用接口
InputStream is = res1.getInputStream();

源码设计亮点

  • Resource接口统一抽象
  • 通过ResourceLoader自动选择策略
    应用场景:支付方式切换(微信/支付宝/银联)

7 适配器模式:接口转换器

场景:兼容不兼容的接口

Spring应用:Spring MVC的HandlerAdapter

// 伪代码:处理多种Controller
public class RequestMappingHandlerAdapter implements HandlerAdapter {
    
    public boolean supports(Object handler) {
        return handler instanceof Controller;
    }
    
    public ModelAndView handle(HttpRequest req, HttpResponse res, Object handler) {
        Controller controller = (Controller) handler;
        return controller.handleRequest(req, res); // 统一适配调用
    }
}

// 实际Spring源码中处理了:
// 1. @Controller注解类 2. HttpRequestHandler 3. Servlet实现等

价值

  • 让DispatcherServlet无需关心Controller具体类型
  • 新增Controller类型只需扩展适配器

8 装饰器模式:功能增强包

场景:动态添加功能

Spring应用HttpServletRequest包装

// 典型应用:缓存请求体
ContentCachingRequestWrapper wrappedRequest = 
    new ContentCachingRequestWrapper(rawRequest);

// 可在filter中多次读取body
byte[] body = wrappedRequest.getContentAsByteArray();

源码实现

public class ContentCachingRequestWrapper extends HttpServletRequestWrapper {
    private ByteArrayOutputStream cachedContent;
    
    @Override
    public ServletInputStream getInputStream() {
        // 装饰原方法:缓存流数据
    }
}

设计本质:通过包装器在不修改原对象基础上增强功能

9 建造者模式:复杂对象组装工

场景:分步构建复杂对象

Spring应用BeanDefinitionBuilder

// 构建复杂的Bean定义
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(UserService.class);
builder.addPropertyValue("maxRetry", 3);
builder.setInitMethodName("init");
builder.setScope(BeanDefinition.SCOPE_SINGLETON);

// 注册到容器
registry.registerBeanDefinition("userService", builder.getBeanDefinition());

对比传统构造

  • 解决多参数构造的混乱(尤其可选参数多时)
  • 构建过程更加清晰可读

10 责任链模式:拦截器的骨架设计

场景:解耦多步骤处理流程

Spring应用HandlerInterceptor拦截器链

// Spring MVC核心执行链
public class HandlerExecutionChain {
    private final List<HandlerInterceptor> interceptors = new ArrayList<>();
    
    // 执行前置处理(责任链核心)
    public boolean applyPreHandle(HttpServletRequest request, 
                                 HttpServletResponse response) {
        for (int i = 0; i < interceptors.size(); i++) {
            HandlerInterceptor interceptor = interceptors.get(i);
            // 任意拦截器返回false则中断链条
            if (!interceptor.preHandle(request, response, this.handler)) {
                triggerAfterCompletion(request, response, i); // 清理已完成
                return false;
            }
        }
        return true;
    }
}

实战配置

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 构建责任链
        registry.addInterceptor(new LogInterceptor()).order(1);
        registry.addInterceptor(new AuthInterceptor()).order(2);
        registry.addInterceptor(new RateLimitInterceptor()).order(3);
    }
}

// 独立拦截器实现
public class AuthInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        if (!checkToken(req.getHeader("Authorization"))) {
            res.sendError(401); // 认证失败
            return false; // 中断链
        }
        return true; // 放行
    }
}

设计价值

  1. 开闭原则:新增拦截器无需修改现有代码
  2. 单一职责:每个拦截器只关注单一功能
  3. 动态编排:通过order()灵活调整执行顺序
  4. 流程控制:任意节点可中断或继续传递

典型反模式:在拦截器中注入其他拦截器,这将破坏责任链独立性,导致循环依赖!

总结

  1. 解耦的艺术
    工厂模式解耦创建/使用,观察者模式解耦事件/处理
  2. 扩展性的智慧
    策略模式支持算法扩展,装饰器模式支持功能扩展
  3. 复杂性的封装
    模板方法封装流程,建造者模式封装构建
  4. 性能的权衡
    单例模式减少资源消耗,代理模式按需增强

最后送给小伙伴们的建议:不要为了用模式而用模式

就像Spring的作者Rod Johnson说的:”优雅的代码不是模式的堆砌,而是恰到好处的抽象。”

当你下次写代码感到别扭时,不妨想想这些经典模式,或许能豁然开朗。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,我的所有文章都会在公众号上首发,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 - 但风偏偏,雨渐渐 - 博客园

mikel阅读(497)

来源: 基于Docker+DeepSeek+Dify :搭建企业级本地私有化知识库超详细教程 – 但风偏偏,雨渐渐 – 博客园

 

    本次演示部署环境:Windows 10专业版,转载请说明出处

下载安装Docker

  Docker官网:https://www.docker.com/

自定义Docker安装路径

Docker默认安装在C盘,大小大概2.9G,做这行最忌讳的就是安装软件全装C盘,所以我调整了下安装路径。

新建安装目录:E:\MySoftware\Docker并将Docker安装包放在目录内,这里有个小细节,安装包名称一定要改下,官网下载下来的名称叫:Docker Desktop Installer.exe,一定要修改一下,不能用这个名字,否则等下在CMD命令安装的时候就会报错说被资源占用,因为Docker在安装时会解压一个一模一样名称的exe程序,重名就会导致安装失败,所以一定要改下名字。

  在文件路径输入cmd回车

输入:


.\"Docker.exe" install --installation-dir=E:\MySoftware\Docker
语法:.\”安装程序名称” install --installation-dir=指定Docker安装的路径

   安装完成后会提示Installation sueceeded

  桌面会出现Docker图标

  启动Docker这里很多人会报这个错,这个是因为电脑没有WSL导致无法启动Docker容器。

下载WSL

  进入微软官网按步骤执行即解决,几分钟完成。

https://learn.microsoft.com/zh-cn/windows/wsl/install-manual#step-4—download-the-linux-kernel-update-package

  用管理员身份打开Powershell窗口,粘贴微软官网的命令执行下载即可

粘贴执行:

dism.exe /online /enable-feature /featurename:Microsoft-Windows-Subsystem-Linux /all /norestart
dism.exe /online /enable-feature /featurename:VirtualMachinePlatform /all /norestart
wsl --set-default-version 2

  下载完后会启动一个这个页面,可以关闭。

  注:完成内核更新包安装后需重启系统(微软官方指南),再启动下Docker,出现这个界面就代表WSL和Docker都启动成功了。

Docker镜像存储迁移

这时进入Docker设置中,将Docker的镜像资源存放路径改一下,不然都会下载都C盘。

Docker镜像源加速配置

至关重要的地方来了,打开Docker的设置中的–>Docker Engine,然后把国内的镜像源复制进去保存,我这里提供一些,如果失效了就百度找新的。

复制代码
{
"registry-mirrors": [
"https://docker.m.daocloud.io",
"https://noohub.ru",
"https://huecker.io",
"https://dockerhub.timeweb.cloud",
"https://0c105db5188026850f80c001def654a0.mirror.swr.myhuaweicloud.com",
"https://5tqw56kt.mirror.aliyuncs.com",
"https://docker.1panel.live",
"http://mirrors.ustc.edu.cn/",
"http://mirror.azure.cn/",
"https://hub.rat.dev/",
"https://docker.ckyl.me/",
"https://docker.chenby.cn",
"https://docker.hpcloud.cloud",
"https://docker.m.daocloud.io"
]
}
复制代码

保存镜像源后就可以试一下拉取镜像,如果拉取不成功也可以重启下Docker,还是不行提示超时就说明镜像源失效了,就在网上搜索下新的镜像源。

测试拉取镜像

在cmd命令窗口输入:

docker pull hello-world

出现这个Status: Dowloaded newer image for hello-world:latest就代表镜像源没有问题。

安装Dify

下载Dify代码包

进入github下载Dify代码包:https://github.com/langgenius/dify

解压代码包后,把压缩后的文件夹复制到自己想要安装的目录下,这里复制一下.env.example文件,然后重命名一下改成.env

  在当前文件路径下输入CMD回车

拉取Dify依赖镜像资源

  粘贴以下命令回车,会自动下载一些依赖资源。如果你的下载失败就是镜像源失效了,换一个镜像源,重新拉取镜像。

docker-compose up -d

下载完成

  回到Docker可以都看到已经下载好的镜像全部都显示了,并且都在运行。

进入Dify后台

输入http://127.0.0.1/会自动打开Dify的页面,有人会遇到这个Internal Server Error报错,这是因为镜像下载来后,有部分镜像还在启动中或未启动,这时候将所有镜像重启一次才可以。

  重启所有镜像

创建管理员用户

  重新进入Dify管理后台,首次进入需创建管理员用户。

创建管理员用户后,将进入登录界面。

登录成功

添加AI模型

点击右上角头像-设置

成员这里可以创建企业内成员进行登录使用。

选择模型供应商

这里我本地已经安装部署好了Ollama和Deepseek R1和BGE-M3模型,如果没有部署好的请看我这篇文章本地电脑部署DeepSeek 大模型AI

  由于我本地已经安装好了Ollama,所以就找到Ollama,点击安装插件,其他供应商选择对应安装。下载可能稍慢,请耐心等待。

添加模型时,若不知模型名称,可在CMD中输入Ollama list查看本地模型名称并复制。

模型类型:

推理模型 → LLM
嵌入模型 → Text Embedding

模型名称就把刚刚复制下来的粘贴上去就可以了。

这里IP地址要注意了,由于我没有用Docker容器里部署Ollama,而是在本地电脑安装的Ollama,这里对IP就要进行特殊处理下了,需要改为:http://host.docker.internal:11434。

网络通信原理:
host.docker.internal为Docker内置域名解析,实现容器与宿主机服务的桥接。简单说就是Docker自己和我们电脑主机网络做了一个映射关系。

模型添加完成

创建应用

其实我也不太会使用,就简单粗糙的做个示范吧,要深入研究需要找下资料学习下。

这里我选择创建聊天助手(每个不同应用的作用不一样,选择与自己相符的就行)

添加一些提示词、变量、知识库、模型,设置好后在右边可以调试预览效果,调试完成后就可以发布应用了。

这里提一句,由于我自己的电脑资源很一般,所以每次一提问的时候资源就占比很高,不过等AI思考完毕后资源占用会下降。

测试结果,虽然回答是错误的。

知识库测试

我这里测试了下知识库检索,上传了6个本地文档。

然后我简单的定义了提示词后,对模型提出问题:结合知识库帮我找出住在向XXX街道人员的电话和姓名。

 

 

然后真的回答对了,全体起立!

这是源文件里的内容。(虚拟信息,如有雷同纯属巧合)

 

WSL资源控制

由于我是针对个人学习,在学习完后我发现我的电脑内存占比一直居高不下,在任务管理器查到了是一个Vmmem的进程占用,大概也知道应该是虚拟机类的占用。

搜索了下网上资源了解到vmmem是一个由WSL(Windows Subsystem for Linux)创建的虚拟进程,主要用于管理WSL2的内存和CPU资源。当WSL2运行时,Vmmem进程会占用大量的内存和CPU资源,以确保虚拟机的最佳性能。然而,这可能会导致主机系统的其他应用程序运行缓慢或无法正常运行‌。

关闭WSL服务

所以如果不用的时候可以关闭掉WSL服务。

在cmd里输入:

wsl --shutdown

关闭后电脑资源就回到正常状态了。

启动WSL服务

那如果我们后再用的时候就重新启动WSL服务就可以。

在cmd输入:

wsl

最后的最后

关于Dify的作用文中提到的只是冰山一角,它真正的厉害之处是它的工作流,由于博主知识有限,只能教大家部署应用,具体的功能开发使用还要大家自行学习,后续博主也会去学习Dify的相关知识,有机会的话就再开一贴。如有讲的不对的地方,敬请指正。

附上Dify的官方操作手册地址:https://docs.dify.ai/zh-hans

这是我整个学习过程中遇到的问题,最后结合百度和AI最后都完成解决了。

总结几个小坑:

1、WSL2的安装。

2、Docker容器镜像源的设置。

3、Dify依赖镜像的拉取。

4、Dify添加模型时IP映射设置。

C#.NET Rsa私钥加密公钥解密 - runliuv - 博客园

mikel阅读(308)

来源: C#.NET Rsa私钥加密公钥解密 – runliuv – 博客园

C#中,RSA私钥只能签名,不能加密,如果要加密,要借助BouncyCastle库。

nuget 中引用 Portable.BouncyCastle。

工具类:

RsaEncryptUtil

复制代码
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using System;
using System.Security.Cryptography;
using System.Text;

namespace CommonUtils
{
    public static class RsaEncryptUtil
    {
        /// <summary>
        /// 私钥加密 .Net平台默认是使用公钥进行加密,私钥进行解密。私钥加密需要自己实现或者使用第三方dll
        /// </summary>
        /// <param name="data"></param>
        /// <param name="key">私钥,格式:PKCS8</param>
        /// <returns></returns>
        public static byte[] encryptByPrivateKey(String data, String key)
        {
            String priKey = key.Trim();
            String xmlPrivateKey = RSAPrivateKeyJava2DotNet(priKey);
            //加载私钥  
            RSACryptoServiceProvider privateRsa = new RSACryptoServiceProvider();
            privateRsa.FromXmlString(xmlPrivateKey);
            //转换密钥  
            AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetKeyPair(privateRsa);
            IBufferedCipher c = CipherUtilities.GetCipher("RSA/ECB/PKCS1Padding");// 参数与Java中加密解密的参数一致       
            c.Init(true, keyPair.Private); //第一个参数为true表示加密,为false表示解密;第二个参数表示密钥 
            byte[] DataToEncrypt = Encoding.UTF8.GetBytes(data);
            byte[] outBytes = c.DoFinal(DataToEncrypt);//加密  
            return outBytes;

        }

        /// <summary>
        /// 私钥加密
        /// </summary>
        /// <param name="data">明文</param>
        /// <param name="key">私钥</param>
        /// <param name="keyFormat">私钥格式:PKCS1,PKCS8</param>
        /// <returns></returns>
        public static byte[] encryptByPrivateKey(String data, String key,string keyFormat)
        {
            String priKey = key.Trim();
             
            //加载私钥  
            RSACryptoServiceProvider privateRsa = RsaUtil.LoadPrivateKey(key,keyFormat);
             
            //转换密钥  
            AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetKeyPair(privateRsa);
            IBufferedCipher c = CipherUtilities.GetCipher("RSA/ECB/PKCS1Padding");// 参数与Java中加密解密的参数一致       
            c.Init(true, keyPair.Private); //第一个参数为true表示加密,为false表示解密;第二个参数表示密钥 
            byte[] DataToEncrypt = Encoding.UTF8.GetBytes(data);
            byte[] outBytes = c.DoFinal(DataToEncrypt);//加密  
            return outBytes;

        }

        /// <summary>
        /// RSA私钥格式转换,java->.net
        /// </summary>
        /// <param name="privateKey"></param>
        /// <returns></returns>
        private static string RSAPrivateKeyJava2DotNet(string privateKey)
        {
            RsaPrivateCrtKeyParameters privateKeyParam = (RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKey));

            return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
                Convert.ToBase64String(privateKeyParam.Modulus.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.PublicExponent.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.P.ToByteArrayUnsigned()), Convert.ToBase64String(privateKeyParam.Q.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.DP.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.DQ.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.QInv.ToByteArrayUnsigned()),
                Convert.ToBase64String(privateKeyParam.Exponent.ToByteArrayUnsigned()));
        }

        /// <summary>
        /// 用公钥解密
        /// </summary>
        /// <param name="data"></param>
        /// <param name="key"></param>
        /// <returns></returns>
        public static byte[] decryptByPublicKey(String data, String key)
        {
            String pubKey = key.Trim();
            String xmlPublicKey = RSAPublicKeyJava2DotNet(pubKey);

            RSACryptoServiceProvider publicRsa = new RSACryptoServiceProvider();
            publicRsa.FromXmlString(xmlPublicKey);

            AsymmetricKeyParameter keyPair = DotNetUtilities.GetRsaPublicKey(publicRsa);
            //转换密钥  
            // AsymmetricCipherKeyPair keyPair = DotNetUtilities.GetRsaKeyPair(publicRsa);
            IBufferedCipher c = CipherUtilities.GetCipher("RSA/ECB/PKCS1Padding");// 参数与Java中加密解密的参数一致       
            c.Init(false, keyPair); //第一个参数为true表示加密,为false表示解密;第二个参数表示密钥 
            byte[] DataToEncrypt = Convert.FromBase64String(data);
            byte[] outBytes = c.DoFinal(DataToEncrypt);//解密  
            return outBytes;
        }

        /// <summary>
        /// RSA公钥格式转换,java->.net
        /// </summary>
        /// <param name="publicKey"></param>
        /// <returns></returns>
        private static string RSAPublicKeyJava2DotNet(string publicKey)
        {
            RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKey));
            return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>",
                Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()),
                Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned()));
        }


    }
}
复制代码

RsaUtil

复制代码
using Org.BouncyCastle.Crypto.Parameters;
using Org.BouncyCastle.Security;
using System;
using System.IO;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;

namespace CommonUtils
{
    public static class RsaUtil
    {
        #region 加载私钥


        /// <summary>
        /// 转换私钥字符串为RSACryptoServiceProvider
        /// </summary>
        /// <param name="privateKeyStr">私钥字符串</param>
        /// <param name="keyFormat">PKCS8,PKCS1</param>
        /// <param name="signType">RSA 私钥长度1024 ,RSA2 私钥长度2048</param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPrivateKey(string privateKeyStr, string keyFormat)
        {
            string signType = "RSA";
            if (privateKeyStr.Length > 1024)
            {
                signType = "RSA2";
            }
            //PKCS8,PKCS1
            if (keyFormat == "PKCS1")
            {
                return LoadPrivateKeyPKCS1(privateKeyStr, signType);
            }
            else
            {
                return LoadPrivateKeyPKCS8(privateKeyStr);
            }
        }

        /// <summary>
        /// PKCS1 格式私钥转 RSACryptoServiceProvider 对象
        /// </summary>
        /// <param name="strKey">pcsk1 私钥的文本内容</param>
        /// <param name="signType">RSA 私钥长度1024 ,RSA2 私钥长度2048 </param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPrivateKeyPKCS1(string privateKeyPemPkcs1, string signType)
        {
            try
            {
                privateKeyPemPkcs1 = privateKeyPemPkcs1.Replace("-----BEGIN RSA PRIVATE KEY-----", "").Replace("-----END RSA PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();
                privateKeyPemPkcs1 = privateKeyPemPkcs1.Replace("-----BEGIN PRIVATE KEY-----", "").Replace("-----END PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();

                byte[] data = null;
                //读取带

                data = Convert.FromBase64String(privateKeyPemPkcs1);


                RSACryptoServiceProvider rsa = DecodeRSAPrivateKey(data, signType);
                return rsa;
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return null;
        }

        private static RSACryptoServiceProvider DecodeRSAPrivateKey(byte[] privkey, string signType)
        {
            byte[] MODULUS, E, D, P, Q, DP, DQ, IQ;

            // --------- Set up stream to decode the asn.1 encoded RSA private key ------
            MemoryStream mem = new MemoryStream(privkey);
            BinaryReader binr = new BinaryReader(mem);  //wrap Memory Stream with BinaryReader for easy reading
            byte bt = 0;
            ushort twobytes = 0;
            int elems = 0;
            try
            {
                twobytes = binr.ReadUInt16();
                if (twobytes == 0x8130) //data read as little endian order (actual data order for Sequence is 30 81)
                    binr.ReadByte();    //advance 1 byte
                else if (twobytes == 0x8230)
                    binr.ReadInt16();    //advance 2 bytes
                else
                    return null;

                twobytes = binr.ReadUInt16();
                if (twobytes != 0x0102) //version number
                    return null;
                bt = binr.ReadByte();
                if (bt != 0x00)
                    return null;


                //------ all private key components are Integer sequences ----
                elems = GetIntegerSize(binr);
                MODULUS = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                E = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                D = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                P = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                Q = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                DP = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                DQ = binr.ReadBytes(elems);

                elems = GetIntegerSize(binr);
                IQ = binr.ReadBytes(elems);


                // ------- create RSACryptoServiceProvider instance and initialize with public key -----
                CspParameters CspParameters = new CspParameters();
                CspParameters.Flags = CspProviderFlags.UseMachineKeyStore;

                int bitLen = 1024;
                if ("RSA2".Equals(signType))
                {
                    bitLen = 2048;
                }

                RSACryptoServiceProvider RSA = new RSACryptoServiceProvider(bitLen, CspParameters);
                //RSACryptoServiceProvider RSA = new RSACryptoServiceProvider();

                RSAParameters RSAparams = new RSAParameters();
                RSAparams.Modulus = MODULUS;
                RSAparams.Exponent = E;
                RSAparams.D = D;
                RSAparams.P = P;
                RSAparams.Q = Q;
                RSAparams.DP = DP;
                RSAparams.DQ = DQ;
                RSAparams.InverseQ = IQ;
                RSA.ImportParameters(RSAparams);
                return RSA;
            }
            catch (Exception ex)
            {
                throw ex;
                // return null;
            }
            finally
            {
                binr.Close();
            }
        }

        private static int GetIntegerSize(BinaryReader binr)
        {
            byte bt = 0;
            byte lowbyte = 0x00;
            byte highbyte = 0x00;
            int count = 0;
            bt = binr.ReadByte();
            if (bt != 0x02)        //expect integer
                return 0;
            bt = binr.ReadByte();

            if (bt == 0x81)
                count = binr.ReadByte();    // data size in next byte
            else
                if (bt == 0x82)
            {
                highbyte = binr.ReadByte(); // data size in next 2 bytes
                lowbyte = binr.ReadByte();
                byte[] modint = { lowbyte, highbyte, 0x00, 0x00 };
                count = BitConverter.ToInt32(modint, 0);
            }
            else
            {
                count = bt;     // we already have the data size
            }

            while (binr.ReadByte() == 0x00)
            {    //remove high order zeros in data
                count -= 1;
            }
            binr.BaseStream.Seek(-1, SeekOrigin.Current);        //last ReadByte wasn't a removed zero, so back up a byte
            return count;
        }

        /// <summary>
        /// PKCS8 文本转RSACryptoServiceProvider 对象
        /// </summary>
        /// <param name="privateKeyPemPkcs8"></param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPrivateKeyPKCS8(string privateKeyPemPkcs8)
        {

            try
            {
                //PKCS8是“BEGIN PRIVATE KEY”
                privateKeyPemPkcs8 = privateKeyPemPkcs8.Replace("-----BEGIN RSA PRIVATE KEY-----", "").Replace("-----END RSA PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();
                privateKeyPemPkcs8 = privateKeyPemPkcs8.Replace("-----BEGIN PRIVATE KEY-----", "").Replace("-----END PRIVATE KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();

                //pkcs8 文本先转为 .NET XML 私钥字符串
                string privateKeyXml = RSAPrivateKeyJava2DotNet(privateKeyPemPkcs8);

                RSACryptoServiceProvider publicRsa = new RSACryptoServiceProvider();
                publicRsa.FromXmlString(privateKeyXml);
                return publicRsa;
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// PKCS8 私钥文本 转 .NET XML 私钥文本
        /// </summary>
        /// <param name="privateKeyPemPkcs8"></param>
        /// <returns></returns>
        public static string RSAPrivateKeyJava2DotNet(string privateKeyPemPkcs8)
        {
            RsaPrivateCrtKeyParameters privateKeyParam = (RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateKeyPemPkcs8));
            return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
            Convert.ToBase64String(privateKeyParam.Modulus.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.PublicExponent.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.P.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.Q.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.DP.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.DQ.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.QInv.ToByteArrayUnsigned()),
            Convert.ToBase64String(privateKeyParam.Exponent.ToByteArrayUnsigned()));
        }

        #endregion


        /// <summary>
        /// 加载公钥证书
        /// </summary>
        /// <param name="publicKeyCert">公钥证书文本内容</param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPublicCert(string publicKeyCert)
        {

            publicKeyCert = publicKeyCert.Replace("-----BEGIN CERTIFICATE-----", "").Replace("-----END CERTIFICATE-----", "").Replace("\r", "").Replace("\n", "").Trim();

            byte[] bytesCerContent = Convert.FromBase64String(publicKeyCert);
            X509Certificate2 x509 = new X509Certificate2(bytesCerContent);
            RSACryptoServiceProvider rsaPub = (RSACryptoServiceProvider)x509.PublicKey.Key;
            return rsaPub;

        }

        /// <summary>
        /// pem 公钥文本 转  .NET RSACryptoServiceProvider。
        /// </summary>
        /// <param name="publicKeyPem"></param>
        /// <returns></returns>
        public static RSACryptoServiceProvider LoadPublicKey(string publicKeyPem)
        {

            publicKeyPem = publicKeyPem.Replace("-----BEGIN PUBLIC KEY-----", "").Replace("-----END PUBLIC KEY-----", "").Replace("\r", "").Replace("\n", "").Trim();

            //pem 公钥文本 转  .NET XML 公钥文本。
            string publicKeyXml = RSAPublicKeyJava2DotNet(publicKeyPem);

            RSACryptoServiceProvider publicRsa = new RSACryptoServiceProvider();
            publicRsa.FromXmlString(publicKeyXml);
            return publicRsa;


        }

        /// <summary>
        /// pem 公钥文本 转  .NET XML 公钥文本。
        /// </summary>
        /// <param name="publicKey"></param>
        /// <returns></returns>
        private static string RSAPublicKeyJava2DotNet(string publicKey)
        {
            RsaKeyParameters publicKeyParam = (RsaKeyParameters)PublicKeyFactory.CreateKey(Convert.FromBase64String(publicKey));
            return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent></RSAKeyValue>",
                Convert.ToBase64String(publicKeyParam.Modulus.ToByteArrayUnsigned()),
                Convert.ToBase64String(publicKeyParam.Exponent.ToByteArrayUnsigned()));
        }


    }
}
复制代码

 

使用:

这个方法只能加密 私钥长度/8 -11 个字符,分段加密的代码要自己处理了。

私钥加密:

复制代码
private void btnPrivateKeyEncrypt_Click(object sender, EventArgs e)
        {
            try
            {
                //byte[] rst = RsaEncryptUtil.encryptByPrivateKey(txtMingWen.Text, txtPrivateKey.Text);
                byte[] rst = RsaEncryptUtil.encryptByPrivateKey(txtMingWen.Text, txtPrivateKey.Text,cbxPrivateKeyFormat.Text);

                //加密后一般转Base64String ,Base64FormattingOptions.InsertLineBreaks.
                string base64str = Convert.ToBase64String(rst, Base64FormattingOptions.InsertLineBreaks);

                txtJiaMiHou.Text = base64str;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
复制代码

公钥解密:

复制代码
private void btnPubKeyDecrypt_Click(object sender, EventArgs e)
        {
            try
            {
                byte[] rst = RsaEncryptUtil.decryptByPublicKey(txtJiaMiHou.Text, txtPubKey.Text);
                 
                string strRst = Encoding.UTF8.GetString(rst);
                txtJieMiHou.Text = strRst;
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }

Java 实现微信小程序不同人员生成不同小程序码并追踪扫码来源 - VipSoft - 博客园

mikel阅读(247)

来源: Java 实现微信小程序不同人员生成不同小程序码并追踪扫码来源 – VipSoft – 博客园

下面我将详细介绍如何使用Java后台实现这一功能。

一、整体架构设计

  1. 前端:微信小程序
  2. 后端:Java (Spring Boot)
  3. 数据库:MySQL/其他
  4. 微信接口:调用微信小程序码生成API

二、数据库设计

1. 推广人员表(promoter)

CREATE TABLE `promoter` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL COMMENT '推广人员姓名',
  `mobile` varchar(20) COMMENT '联系电话',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

2. 用户-推广关系表(user_promoter_relation)

CREATE TABLE `user_promoter_relation` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `user_id` varchar(64) NOT NULL COMMENT '小程序用户openid',
  `promoter_id` bigint NOT NULL COMMENT '推广人员ID',
  `first_scan_time` datetime NOT NULL COMMENT '首次扫码时间',
  `last_scan_time` datetime NOT NULL COMMENT '最近扫码时间',
  `scan_count` int NOT NULL DEFAULT '1' COMMENT '扫码次数',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_user_promoter` (`user_id`,`promoter_id`),
  KEY `idx_promoter` (`promoter_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

三、Java后端实现

1. 添加微信小程序Java SDK依赖

<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-miniapp</artifactId>
    <version>4.1.0</version>
</dependency>

2. 配置微信小程序参数

@Configuration
public class WxMaConfiguration {
    
    @Value("${wx.miniapp.appid}")
    private String appid;
    
    @Value("${wx.miniapp.secret}")
    private String secret;
    
    @Bean
    public WxMaService wxMaService() {
        WxMaDefaultConfigImpl config = new WxMaDefaultConfigImpl();
        config.setAppid(appid);
        config.setSecret(secret);
        
        WxMaService service = new WxMaServiceImpl();
        service.setWxMaConfig(config);
        return service;
    }
}

3. 生成带参数的小程序码

@RestController
@RequestMapping("/api/qrcode")
public class QrCodeController {
    
    @Autowired
    private WxMaService wxMaService;
    
    @Autowired
    private PromoterService promoterService;
    
    /**
     * 生成推广二维码
     * @param promoterId 推广人员ID
     * @return 二维码图片字节流
     */
    @GetMapping("/generate")
    public void generatePromoterQrCode(@RequestParam Long promoterId, 
                                     HttpServletResponse response) throws IOException {
        // 验证推广人员是否存在
        Promoter promoter = promoterService.getById(promoterId);
        if (promoter == null) {
            throw new RuntimeException("推广人员不存在");
        }
        
        // 生成小程序码
        String scene = "promoterId=" + promoterId;
        WxMaQrcodeService qrcodeService = wxMaService.getQrcodeService();
        File qrCodeFile = qrcodeService.createWxaCodeUnlimit(scene, "pages/index/index", 430, true, null, false);
        
        // 返回图片流
        response.setContentType("image/jpeg");
        try (InputStream in = new FileInputStream(qrCodeFile);
             OutputStream out = response.getOutputStream()) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = in.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        }
    }
}

4. 处理扫码进入事件

@RestController
@RequestMapping("/api/track")
public class TrackController {
    
    @Autowired
    private UserPromoterRelationService relationService;
    
    /**
     * 记录用户扫码行为
     * @param dto 包含用户信息和推广信息
     * @return 操作结果
     */
    @PostMapping("/scan")
    public Result trackScan(@RequestBody ScanTrackDTO dto) {
        // 解析scene参数
        String scene = dto.getScene();
        Map<String, String> sceneParams = parseScene(scene);
        String promoterIdStr = sceneParams.get("promoterId");
        
        if (StringUtils.isBlank(promoterIdStr)) {
            return Result.fail("缺少推广人员参数");
        }
        
        try {
            Long promoterId = Long.parseLong(promoterIdStr);
            relationService.recordUserScan(dto.getOpenid(), promoterId);
            return Result.success();
        } catch (NumberFormatException e) {
            return Result.fail("推广人员参数格式错误");
        }
    }
    
    private Map<String, String> parseScene(String scene) {
        Map<String, String> params = new HashMap<>();
        if (StringUtils.isBlank(scene)) {
            return params;
        }
        
        String[] pairs = scene.split("&");
        for (String pair : pairs) {
            String[] kv = pair.split("=");
            if (kv.length == 2) {
                params.put(kv[0], kv[1]);
            }
        }
        return params;
    }
}

5. 用户-推广关系服务

@Service
public class UserPromoterRelationServiceImpl implements UserPromoterRelationService {
    
    @Autowired
    private UserPromoterRelationMapper relationMapper;
    
    @Override
    @Transactional
    public void recordUserScan(String openid, Long promoterId) {
        // 查询是否已有记录
        UserPromoterRelation relation = relationMapper.selectByUserAndPromoter(openid, promoterId);
        
        Date now = new Date();
        if (relation == null) {
            // 新建关系记录
            relation = new UserPromoterRelation();
            relation.setUserId(openid);
            relation.setPromoterId(promoterId);
            relation.setFirstScanTime(now);
            relation.setLastScanTime(now);
            relation.setScanCount(1);
            relationMapper.insert(relation);
        } else {
            // 更新已有记录
            relation.setLastScanTime(now);
            relation.setScanCount(relation.getScanCount() + 1);
            relationMapper.updateById(relation);
        }
    }
}

四、小程序前端处理

在小程序的app.js中处理扫码进入的场景:

App({
  onLaunch: function(options) {
    // 处理扫码进入的情况
    if (options.scene === 1047 || options.scene === 1048 || options.scene === 1049) {
      // 这些scene值表示是通过扫码进入
      const scene = decodeURIComponent(options.query.scene);
      
      // 上报扫码信息到后端
      wx.request({
        url: 'https://yourdomain.com/api/track/scan',
        method: 'POST',
        data: {
          scene: scene,
          openid: this.globalData.openid // 需要先获取用户openid
        },
        success: function(res) {
          console.log('扫码记录成功', res);
        }
      });
    }
  }
})

五、数据统计接口实现

@RestController
@RequestMapping("/api/stat")
public class StatController {
    
    @Autowired
    private UserPromoterRelationMapper relationMapper;
    
    /**
     * 获取推广人员业绩统计
     * @param promoterId 推广人员ID
     * @param startDate 开始日期
     * @param endDate 结束日期
     * @return 统计结果
     */
    @GetMapping("/promoter")
    public Result getPromoterStats(@RequestParam Long promoterId,
                                 @RequestParam(required = false) @DateTimeFormat(pattern="yyyy-MM-dd") Date startDate,
                                 @RequestParam(required = false) @DateTimeFormat(pattern="yyyy-MM-dd") Date endDate) {
        
        // 构建查询条件
        QueryWrapper<UserPromoterRelation> query = new QueryWrapper<>();
        query.eq("promoter_id", promoterId);
        
        if (startDate != null) {
            query.ge("first_scan_time", startDate);
        }
        if (endDate != null) {
            query.le("first_scan_time", endDate);
        }
        
        // 执行查询
        int totalUsers = relationMapper.selectCount(query);
        List<Map<String, Object>> dailyStats = relationMapper.selectDailyStatsByPromoter(promoterId, startDate, endDate);
        
        // 返回结果
        Map<String, Object> result = new HashMap<>();
        result.put("totalUsers", totalUsers);
        result.put("dailyStats", dailyStats);
        
        return Result.success(result);
    }
}

六、安全注意事项

  1. 参数校验:所有传入的promoterId需要验证是否存在
  2. 防刷机制:限制同一用户频繁上报扫码记录
  3. HTTPS:确保所有接口使用HTTPS协议
  4. 权限控制:推广数据统计接口需要添加权限验证
  5. 日志记录:记录所有二维码生成和扫码行为

七、扩展功能建议

  1. 二级分销:可以扩展支持多级推广关系
  2. 奖励机制:根据扫码用户的活动情况给推广人员奖励
  3. 实时通知:当有新用户扫码时,实时通知推广人员
  4. 数据分析:提供更详细的数据分析报表

通过以上Java实现,你可以完整地构建一个支持不同人员生成不同小程序码并能追踪扫码来源的系统。

Web性能优化:从 2 秒到200毫秒 - ASER_1989 - 博客园

mikel阅读(214)

来源: Web性能优化:从 2 秒到200毫秒 – ASER_1989 – 博客园

前不久发布了个人笔记软件 Nebula Note 的Web预览版(传送门),整体开发体验和使用效果都很满意。但作为Web工程师的我习惯性的打开了浏览器开发者工具的Network面板,主要想观察首次加载时间。2 秒+!显然,这个加载速度无法接受。于是便开始了一轮深入优化,目标是:将首页加载时间控制在 1 秒内,真正的实现秒开。

 

性能瓶颈分析

从浏览器开发者工具的Network面板上可以很明显的观察到是首屏资源体积过大所致。项目技术栈为:

  • 前端框架:React
  • 服务端框架:NodeJsKoa
  • 构建工具:Vite
  • UI 组件:自研的 Nebula UI,由于功能过于简单,所以没有用主流的UI库。

排除自研代码后,问题可能出在集成的第三方组件上。使用打包分析工具检查产物体积,结果如下:

  • react-codemirror在线代码编辑器:体积最大
  • toast-ui Markdown 编辑器:第二大
  • 自研逻辑:占比极小

使用source-map-explorer对构架结果进行分析,截图如下:

打包分析图

 

基础优化:开启 Brotli 压缩

当前服务部署在 99 元云服务器 上的 Kubernetes 环境中,所有服务通过自定义模板的 Deployment 文件部署(配置传送门),IngressRoute 中默认启用了Gzip 压缩。Gzip还是有点温柔,考虑进一步压榨传输体积,于是启用了Brotli压缩。构建结果对比测试效果如下:

压缩对比图

Brotli的实力毋庸置疑,比Gzip多压缩了近200KB。而且这次是在打包的时候就对资源进行了压缩,理论上应该能有效缩减服务器的响应时间。但即便如此页面加载时间仍未突破 1 秒。

 

深度优化:移除冗余语言包(AST静态裁剪)

根据打包分析结果,react-codemirror 是最大“重量级选手”。主要原因是其默认引入了大量编程语言的语法高亮支持,而目前 Nebula Note 实际仅使用少数几种。因此,静态分析源码后,通过自定义 Vite 插件在构建阶段识别未使用的语言包,然后再利用 AST(抽象语法树)移除无关代码,最后打包体积减少1MB+。效果是相当的炸裂。优化后打包体积对比图如下:

优化后图1

构建分析结果:

优化后图2

此轮优化后react-codemirror 从第一名降至第二,首屏加载时间也成功挤进 1 秒以内。最终成果如下:

秒开效果

 

更近一步:延迟加载非首屏组件

虽然“秒开”目标已实现,但从打包占比来看 react-codemirror 与 toast-ui 两大组件仍占据 近 80% 体积,并且这两个包在第一屏中是非必需的,或只需其一。于是采用 React 的 Suspense + lazy 机制,针对这两个组件实现延迟加载:

import React, { Suspense, lazy } from 'react';
import SuspenseLoading from '@client/components/suspenseLoading';
import type { Props as IProps } from './codeMirror';

const Editor = lazy(() => import('./codeMirror'));

export type Props = IProps;
const CodeEditor = (props: IProps) => {
    return (
        <Suspense fallback={<SuspenseLoading />}>
            <Editor {...props} />
        </Suspense>
    );
};
export default CodeEditor;

使用Suspense后,懒加载的模块在构建的时候会被拆成独立的包,这对于首屏的加载非常的友好。通过对比可以看到不仅是JS文件从262KB降到了93KB,首次加载的CSS文件更是从83.5KB降到了2.1KB。

 

最后

有一个很奇怪的现象,CSS的TTFB很不稳定,在约40ms和100ms间反复横跳。其他资源,尤其是Http请求相关的资源表现很稳定。有知道原因的朋友,还请在评论区分享一下。最后附上博客所述内容资源,欢迎点赞支持~✌️。

Nebula Note预览版:https://note.aser1989.cn/
Nebula Note源代码: https://github.com/ASER1989/nebula-note

【拥抱鸿蒙】Flutter+Cursor轻松打造HarmonyOS应用(一) - 郑知鱼 - 博客园

mikel阅读(269)

来源: 【拥抱鸿蒙】Flutter+Cursor轻松打造HarmonyOS应用(一) – 郑知鱼 – 博客园

前言

在移动应用开发领域,Flutter以其出色的跨平台能力和高效的开发体验赢得了众多开发者的青睐,是许多移动开发者混合开发的首选。

随着HarmonyOS的崛起,许多开发者开始探索如何将Flutter应用迁移到鸿蒙生态。本文将带你从零开始,使用Flutter开发HarmonyOS应用,并借助强大的AI编程助手Cursor来加速UI开发过程。

这是“【拥抱鸿蒙】Flutter+Cursor轻松打造HarmonyOS应用”系列的第一篇。

一、环境准备:搭建Flutter for HarmonyOS开发环境

1.1 基础工具安装

首先确保你的开发机器已安装以下基础工具:

  • Flutter SDK(推荐3.13.0或更高版本)
    # 下载Flutter SDK
    git clone https://github.com/flutter/flutter.git -b stable
    
    # 添加环境变量
    export PATH="$PATH:`pwd`/flutter/bin"
    
    # 运行flutter doctor检查依赖
    flutter doctor
    
  • HarmonyOS开发工具
    API12, deveco-studio-5.0 或 command-line-tools-5.0 (推荐使用5.0.0 Release或更新版本)
  • JDK17
  • 配置环境变量
 export TOOL_HOME=/Applications/DevEco-Studio.app/Contents # mac环境
 export DEVECO_SDK_HOME=$TOOL_HOME/sdk # command-line-tools/sdk
 export PATH=$TOOL_HOME/tools/ohpm/bin:$PATH # command-line-tools/ohpm/bin
 export PATH=$TOOL_HOME/tools/hvigor/bin:$PATH # command-line-tools/hvigor/bin
 export PATH=$TOOL_HOME/tools/node/bin:$PATH # command-line-tools/tool/node/bin

📢📢📢注意
这里有一个问题需要注意,支持鸿蒙的Flutter SDK版本目前是3.22.0版本。在开发适配iOS和Android时,我们使用的Flutter版本可能高于3.22.0。这时,因此需要使用fvm对Flutter版本进行管理,可以同时维护和切换多个Flutter版本。同时也建议,Flutter的鸿蒙应用仓库最好和其他端仓库分开,因为目前支持鸿蒙的Flutter版本是滞后于Flutter官方版本的。

使用fvm维护多个版本示例:

4001

1.2 配置Flutter鸿蒙支持

目前Flutter对HarmonyOS的支持主要通过开源项目OpenHarmony-TPC/flutter_flutter实现。

该仓库是基于Flutter官方仓库的3.22.0版本,对于OpenHarmony平台的兼容拓展。可支持IDE或者终端使用Flutter Tools指令编译和构建OpenHarmony应用程序。

Build前需使用fvm切换Flutter版本:

fvm use custom_3.22.0

1.3 环境验证

创建新项目并检查鸿蒙支持:

flutter create my_harmony_app
cd my_harmony_app
flutter run -d harmony

如果看到鸿蒙模拟器或真机上运行着Flutter的默认启动页面,说明环境配置成功!

二、Flutter鸿蒙开发框架解析

2.1 架构概览

Flutter在HarmonyOS上的运行架构分为三层:

  1. 框架层:Dart实现的Flutter框架
  2. 引擎层:Skia渲染引擎+鸿蒙适配层
  3. 系统层:HarmonyOS的ACE(Ability Cross-platform Environment)

2.2 关键差异点

Android/iOS平台相比,Flutter在鸿蒙上需要注意:

  • 页面导航:使用HarmonyOS的Page Ability而非Activity/ViewController
  • 权限系统:鸿蒙特有的权限声明方式
  • 原生交互:通过ffi与鸿蒙的Native API通信

2.3 常用适配组件

import 'package:flutter_harmony/harmony.dart';

// 鸿蒙特色的组件
HarmonyApp(
  config: HarmonyConfig(
    abilityName: 'MainAbility', // 对应的鸿蒙Ability名称
  ),
  home: MyHomePage(),
);

三、Flutter与鸿蒙原生交互

3.1 导入原生插件

与其他端类似,ohos工程中需要GeneratedPluginRegistrant.ets文件导入Flutter生成的原生插件。

3.2 实现FlutterPlugin

定义一个类作为FlutterPlugin的实现,并在onAttachedToEngine(binding: FlutterPluginBinding): void方法中使用MethodChannel监听Flutter调用的方法。

export default class HMFlutterPlugin implements FlutterPlugin {
  private channel?: MethodChannel;
  private basicChannel?: BasicMessageChannel<Any>;
  private context?: common.UIAbilityContext;

  setContext(context: common.UIAbilityContext) {
    this.context = context;
  }

  onAttachedToEngine(binding: FlutterPluginBinding): void {
    this.channel = new MethodChannel(binding.getBinaryMessenger(), "com.xxx.app/message");
    this.channel.setMethodCallHandler({
      onMethodCall: (call: MethodCall, result: MethodResult) => {
        if (!this.context) {
          result.error("CONTEXT_NOT_INITIALIZED", "Context is not initialized", null);
        }

        const argsRec = call.args as Map<string, Object>;
        LogUtil.info(`[flutter-call-ohos]\nmethod: ${call.method}\nargs: ${JSONUtil.toJSONString(argsRec)}`);
        switch (call.method) {
          case "xxx": {
            break;
          }
          default: break;
        }
}

3.3 配置FlutterEngine

在EntryAbility.ets中实现configureFlutterEngine(flutterEngine: FlutterEngine)方法。

configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    GeneratedPluginRegistrant.registerWith(flutterEngine)
    const plugin = new HMFlutterPlugin();
    plugin.setContext(this.context);
    this.addPlugin(plugin);
  }

3.4 回调返回给Flutter端

使用result.success(res);回调执行成功的数据;使用result.error(errorCode, error.message, null);回调执行失败的错误信息。

try {
    // xxx接口功能实现
    result.success(res);
} catch (err) {
    let error = err as BusinessError;
    result.error(errorCode, error.message, null);
}

3.5 Flutter调用鸿蒙原生方法并接收回调数据

try {
    final res = await _channel.invokeMethod('method_ohos');
    return res.toString().toLowerCase() == 'true' || res.toString() == '1';
} catch (e) {
    printError('get network status error: $e');
}

总结

本篇主要介绍了Flutter开发鸿蒙应用的环境搭建和交互,我们将在下篇介绍如何借助Cursor提高开发效率让我们的开发之旅轻松而有趣。

我是郑知鱼🐳,欢迎大家讨论与指教。
如果你觉得有所收获,也请点赞👍🏻收藏⭐️关注🔍我吧~~

Dify、n8n、Coze、Fastgpt和Ragflow对比分析,如何选择? | AI工具集

mikel阅读(1467)

来源: Dify、n8n、Coze、Fastgpt和Ragflow对比分析,如何选择? | AI工具集

以下文章来源于袋鼠帝AI客栈,作者袋鼠帝

原文链接:Dify、n8n、Coze、Fastgpt、Ragflow到底该怎么选?超详细指南~

一直以来,我们分享了不少关于工作流平台、LLM应用平台的文章。

主要包含:Dify、Coze、n8n、Fastgpt、Ragflow。

但是几乎每一篇文章的评论区都有小伙伴问,xxx平台和xxx平台比怎么样,该怎么选?



确实,面对日新月异的AI技术,还有飞速发展的各种LLM平台,我们很容易患上选择困难症

但我想说的是,每个平台各有优势,需要根据自身需求,选择合适的即可。

这篇文章会从实用角度出发,通过详细的功能对比、真实的使用体验和具体的应用场景,帮助你在Dify、Coze、n8n、FastGPT和RAGFlow这五款主流平台中找到最适合自己的那一个。

无论你是AI开发者、企业用户,还是刚接触AI的新手,这篇对比分析都能为你提供清晰的选择指南。

本篇文章5000字,干货满满,建议收藏

首先我们要明确一下

LLM应用平台有:Dify、Coze、Fastgpt、ragflow

n8n比较特殊一点,它是以工作流为主的LLM平台。

LLM应用平台的核心价值在于大大降低了AI应用的开发门槛,加速从概念到产品的落地过程,并为开发者提供整合、管理和优化AI能力的工具集(插件、MCP工具等等)。

通过这些平台,咱们可以更专注于业务逻辑和用户体验创新,而非重复性的底层技术构建

先简单了解一下这几个平台的特点

n8n: 以其强大的通用工作流自动化能力著称,近年来积极拥抱AI,允许用户将LLM节点嵌入复杂的自动化流程中。

Coze (扣子): 由字节跳动推出,主打低代码/无代码的AI Agent开发,强调快速构建和部署对话式AI应用。

FastGPT: 一个开源的AI Agent构建平台,专注于知识库问答系统的构建,提供数据处理、模型调用和可视化工作流编排能力。

Dify: 开源的LLM应用开发平台,融合BaaS和LLMOps理念,旨在提供一站式的AI应用快速开发与运营能力,包括Agent工作流、RAG Pipeline等。

RAGFlow: 基于深度文档理解的开源RAG引擎,专注于解决复杂格式文档的知识提取与高质量问答。

各平台详细介绍

Dify:LLM平台中的瑞士军刀

先给Dify 3个关键词吧

#开源  #LLMOps  #生产就绪

一句话: Dify 是个23年4月开源的LLM应用开发平台,如果想整点专业的、能上生产的AI应用,还想把后端、模型运维的事全搞定?用它就OK了。

地址:dify.ai

Dify 主打“Backend-as-a-Service”和“LLMOps”,目标是让开发者和不懂技术的创新者都能轻松上手,快速鼓捣出实用的AI解决方案。

它把 RAG(检索增强生成)管道、AI工作流、监控工具、模型管理,MCP这些功能都塞进一个平台里。

确实像瑞士军刀一样,想要什么功能基本都有。

主打一个“你只管创新,其他交给Dify”。

顺便插播一下,Dify最近做了一下品牌焕新。

支持使用Docker私有化部署,运行起来的服务器最低配置是2核4G

社区活跃度也不错,目前在Github已经有98.3K Star

但是总给我一种样样通,”样样松”的感觉,好像没有特别突出的地方。

还有一个缺点就是Dify里面创建的Bot,如果想对外提供服务的话,其API没有兼容OpenAI API,就会导致外部应用想要对接会相对困难。

另外,对于只想快速实现一些小功能的用户来说是有点重了

大型企业集成的话,应该还是需要自己在上面二次开发的。

适合人群: 有一定技术的开发者、追求专业、效率的团队、需要定制化AI解决方案的企业。

Coze:LLM平台界的“乐高”

#无代码 #智能体构建 #多平台发布

先来一句话总结:Coze(扣子)是字节跳动旗下的,主打一个“人人都是AI开发者”,内置上千款工具插件,让你像搭积木一样简单地创建和发布AI Agent。

地址:coze.cn

不管你懂不懂编程,Coze都能让你把脑洞里的AI智能体快速实现。

可视化搭建、丰富的插件、知识库、工作流一应俱全,还支持一键发布到抖音、飞书、微信公众号、小程序、Discord、Telegram等各大平台。

有海外版(Coze)和国内版(扣子)

Coze是闭源的,但它的功能比Dify更丰富。

我比较中意的有代码插件,零代码小程序、web页面,定时任务等功能。

适合人群: AI入门用户、产品经理、运营人员、想快速搭建个性化AI Agent的创作者、以及预算、技术有限的个人和小型团队等。

FastGPT:知识库小能手

#开源 #RAG知识库

一句话: FastGPT是个免费开源的AI知识库平台,让AI根据你的私有数据精准回答问题,是你的第二个”大脑”

地址:tryfastgpt.ai

FastGPT 提供数据处理、模型调用、RAG检索和可视化AI工作流,MCP一条龙服务。

你可以导入各种格式的文档(Word、PDF、网页链接等),用最短的时间打造出特定领域的AI问答助手。

Fastgpt的RAG效果是相当不错的,它能够简单、快速构建一个高质量知识库,我之前用它做我的微信AI助理产品的客服,挺棒的。

一些企业级客户我也是帮助他们用fastgpt来构建知识库,轻量,简单,好用。

它还提供与OpenAI兼容的API,可以非常方便的把它集成到现有的其他应用里。

支持Docker私有化部署,最好用2核4G的服务器来跑。

相比Dify来说,优点在于更轻量、知识库效果更好、API兼容OpenAI API,更方便集成到其他应用。

但是在功能的丰富度、和一些体验上是不如Dify的,社区也不如Dify活跃,目前在Github是24.2K Star

但是如果你是想快速打造知识库为主的AI应用,我都推荐先试试Fastgpt。

适合人群:需要构建企业内部知识库、AI客服、的开发者或企业,以及对RAG技术感兴趣的AI爱好者。

RAGFlow:知识库专家

标签:#开源  #RAG引擎  #深度文档理解

一句话: RAGFlow 是个开源的RAG引擎

地址:ragflow.io

RAGFlow的核心竞争力在于“深度文档理解”,比如能从合同里提取条款、总结长篇报告。以及支持10多种类型的数据预处理,不管是在RAG的知识库构建,还是问答阶段都有非常丰富的参数去调整。还支持知识图谱功能。

RAG的颗粒度细,知识库效果上限很高。

如果说Fastgpt是知识库小能手,那么Ragflow就是知识库专家(从它的名字里面就能看出来)。

支持Docker部署,但是比较重,需要至少4核16G配置的服务器才能流畅使用。目前在Github有53.1K Star

适合对答案准确性和可追溯性有高要求的行业(如法律、医疗、金融)、需要处理大量复杂文档的企业、以及RAG技术的研究者和开发者。

n8n:最强开源工作流平台

#开源 #工作流自动化 #低代码

一句话总结: n8n 是一个开源的低代码工作流自动化工具,专注于将各种应用和服务连接起来,形成自动化的业务流程。

地址:n8n.io

n8n 的核心是通过可视化节点(Node)来构建自动化流程,同时每个节点所提供的配置参数丰富,定制化程度高。

它提供了超过400个预置集成,覆盖各类SaaS服务和数据库。既可以通过简单的拖拽操作构建工作流,也可以通过js或Python代码进行更复杂的定制。

它包含Agent节点,能够快速接入各种大模型,同样支持了MCP。

在实际业务中,n8n能极大提高工作效率

比如Delivery Hero使用n8n每月节省了200多小时的工作时间

https://n8n.io/case-studies/delivery-hero/

StepStone也靠它运行了200多个关键任务流程

https://n8n.io/case-studies/stepstone/

虽然n8n有很多优点,但毕竟是工作流平台,主打工作流。在LLM这块丝滑程度还是比不上其他专业的LLM应用平台,LLM这块该有的也都有,就是用起来感觉更麻烦一些。

同时上手难度也是这些个平台里面最大的了,需要一些逻辑思维,和前期的学习成本,但上手之后效率将会极大的提升。

也支持Docker私有化部署,完全不吃配置,1核1G的服务器应该都能跑。

适合人群: 需要高度定制自动化流程的团队、开发者、以及追求效率最大化的中小企业。

5大平台功能横向对比分析

为了帮助大家更清晰地了解这五个平台的区别和优势,我整理了一张详细的对比表,从多个维度进行客观分析:

其中Coze目前不是免费的了

平台选择实用建议:

从我的实际体验来看,如果你是刚接触AI应用开发,希望快速看到成果,Coze是最容易上手的选择。

如果你的工作或者业务涉及多个系统和服务之间的数据流转,需要自动化处理,n8n的强大自动化工作流会为你节省超多时间。

想搭建企业内部智能知识库或者Q&A系统,FastGPT、Ragflow可以优先考虑,它们在RAG方面都比较强,FastGPT更轻量、Ragflow更重(但上限更高)

对于有长期规划、需要构建可扩展企业级AI应用的团队,Dify的完整生态系统和企业级功能是好的选择。

为了更直观,基于我的实际使用体验和各平台特点,我整理了下面这张”用户适用性评分图”(满分5分),希望可以帮助大家快速定位自己的需求对应哪个平台:

还有下面这个图,也可以参考参考

选型考量要素

在最终做出选择之前,建议大家考虑以下几个关键要素,它们会直接影响你的使用体验和长期效果:

预算:

开源平台可以免费自托管使用,但需要考虑服务器和维护成本;云服务则是按使用量或订阅付费,前期成本低但长期可能更高。根据你的资源状况和业务规模选择合适的方案。

技术能力:

评估你或团队的技术背景、学习意愿。如果技术实力有限,选择像Coze这样的无代码平台会更合适;如果有较强的技术团队,就可以考虑Dify或n8n等提供更多定制能力的平台。

部署:

考虑是否需要数据本地私有化。自托管方案提供更高的数据安全性和隐私保护,但需要更多的技术支持;云服务则提供快速部署和低维护成本,但可能存在数据安全风险。

核心功能需求:

详细列出你最核心的需求,看哪个平台能够最好地满足这些关键点。比如如果RAG能力是最重要的,那么FastGPT或RAGFlow可能比Coze更合适;如果需要复杂工作流,n8n或Dify会是更好的选择。

平台可持续性:

评估平台的更新频率、社区活跃度和长期支持情况。开源项目要看社区活跃度和贡献者数量;商业产品看公司背景和市场表现。这直接关系到你选择的平台能否长期发展并跟上技术变化。

数据安全与合规方面:

特别是对企业用户来说,数据隐私保护、访问控制和合规性至关重要。开源自托管平台在数据安全方面更有优势,因为数据可以完全保留在自己的环境中;商业平台则需要仔细阅读其隐私政策和数据处理协议等等。

通过认真评估上面这些因素,结合前面的对比分析,相信大家应该能够找到最符合自身需求的LLM应用平台了吧。

「最后」

经过这次全方位的对比分析

希望大家对Dify、Coze、n8n、FastGPT和RAGFlow这五个平台有了更清晰的认识。

没有绝对完美的工具,只有最适合当前需求和发展阶段的选择。

我的建议是:

如果可能的话,可以先从使用门槛较低的平台(如:Coze)开始尝试,熟悉LLM应用开发的基本概念和流程;

后面需求越来越复杂,技术也有一定提升之后,再逐步过渡到更专业的平台(如Dify或n8n)。

AI Agent是一个快速发展的领域,各平台也在飞速进化和完善。

希望这篇分析能为大家提供一个基础的参考框架

帮助大家在这个充满机遇和挑战的AI时代找到适合自己的工具和方向。

Agent Neo - Flowith推出的AI Agent,能持续不断地执行任务 | AI工具集

mikel阅读(470)

来源: Agent Neo – Flowith推出的AI Agent,能持续不断地执行任务 | AI工具集

Agent Neo是什么

Agent Neo是Flowith推出的创新 AI Agent。Agent Neo具备无限步骤,无限上下文,无限工具的核心能力,能持续执行复杂任务、处理海量信息和调用多种大模型与工具。Agent Neo 结合 Flowith 的知识库功能,用户上传知识库,能快速构建数字分身或生成高质量内容。Flowith 提供Agent社区,用户能将自己的工作流做成 Recipe 分享到社区里。Agent Neo交互界面美观且富有创意,提供透明化的工作流程,适用于复杂任务自动化、知识管理与分享等场景。Agent Neo目前为邀请制,需激活码使用。

Agent Neo

Agent Neo的主要功能

  • 无限步骤(Unlimited Steps):Agent Neo能进行无限深度的推理,支持持续不断地工作,能执行需要长时间运行的复杂任务。
  • 多步骤优化(Multi-step Refinement):基于多个步骤优化网页,提供最佳的结果。
  • 24/7云端执行(24/7 Cloud Execution):支持全天候云端执行任务,用户的设备处于休眠状态,任务也能不间断地运行。
  • 无限输出长度(Unlimited Output Length):支持生成任意长度的响应,不会出现内容截断的情况。
  • 超智能重新规划(Super-Intelligent Re-Planning):在执行过程中,根据最终目标智能地调整计划。

Agent Neo的官方示例

  • Prompt:Please generate a detailed ‘The Hunger Games’ setting collection, and draw rich and detailed illustrations based on the content of the book. The final presentation form is an immersive experience website with rich animation effects. Please ensure that all key content has correct diagrams. You need to generate relevant content in batches to ensure that each major element has a relevant visual image and picture.(请生成详细的《饥饿游戏》背景集,根据书中的内容绘制丰富而详细的插图。最终呈现形式是一个具有丰富动画效果的沉浸式体验网站。请确保所有关键内容都有正确的图表。你需要批量生成相关内容,确保每个主要元素都有相关的视觉图像和图片。”)

Agent Neo

  • Prompt:introduce flowith 2.0.(介绍 Flowith 2.0)

Agent Neo

Agent Neo的性能表现

Agent Neo 在通用 AI Agent 能力测试 GAIA 中表现出色,刷新所有难度级别的最新最佳性能评分。

Agent Neo

如何使用Agent Neo

  • 获取邀请码:Agent Neo目前为邀请制,需获取激活码后使用。
  • 注册并登录:访问 Flowith 官方网站,完成注册,用邀请码登录。
  • 进入 Agent Neo 模式:在 Flowith 平台中找到打开 Agent Mode,调用 Agent Neo。
  • 设置任务
    • 输入任务描述:告诉 Agent Neo 想要完成的任务,例如生成报告、创建网页、续写故事等。
    • 选择或上传知识库:如果任务需要特定的知识背景,选择已有的知识库或上传相关文档,让 Agent Neo 从中获取信息。
  • 任务规划:Agent Neo 自动规划任务的工作流,包括信息搜集、内容生成、工具调用等。
  • 实时交互:在任务执行过程中,Agent Neo 根据需要与用户交互,例如确认信息、获取反馈或调整任务方向。
  • 查看结果:任务完成后,将结果呈现给用户,例如生成的网页、文档或报告。
  • 修改结果:用户根据需要对生成的内容进行修改或优化,Agent Neo 支持用户直接在可视化界面或代码层面进行调整。
  • 保存工作流:用户将完成的任务保存为工作流(Recipe),方便后续复用或分享给其他用户。
  • 社区分享:将工作流发布到 Flowith 的 Agent 社区,与其他用户共享经验和创意

Agent Neo的应用场景

  • 自动化任务执行:自动执行重复性任务,如数据收集、报告生成和监控任务,提高效率和准确性。
  • 复杂项目管理:基于无限步骤和深度推理来规划和管理项目,直至完成。
  • 内容创作与编辑:续写故事、生成文章或优化网页内容,支持创意写作和多步骤内容精炼。
  • 知识库构建与应用:用户上传和分析知识库,提高任务执行的精准性和效率。
  • 数字分身创建:创建具有专业知识和历史记忆的数字分身,模拟对话或自动化客户服务。