- 使用 Spring Initializr 创建 Spring Boot 应用程序
- 在Spring Boot中配置Cassandra
- 在 Spring Boot 上配置 Tomcat 连接池
- 将Camel消息路由到嵌入WildFly的Artemis上
最近在写包, 一开始封装了仓储Repository
用于操作数据库, 然后为了快速开发一些业务简单的接口, 通过QueryController
, ModifyController
, CrudController
提供默认实现, 在添加接口的时候只需要新建一个 Controller, 然后继承
public class TestController : QueryRepController<int?, TestEntity, TestEntityGet>
{
public TestController(IQueryRepository<int?, TestEntity> repository) : base(repository)
{
}
}
即可实现简单的增删改查功能
看到 TestController
这单薄的实现, 我突然有个想法
"既然这个controller写得这么简单, 为什么我不能尝试靠代码去生成呢!?!"
虽然这个功能不一定有什么用, 但我还是开始了踩坑
经过简单的思考, 我认为第一步应该是创建 Type
最开始尝试注册一堆 typeof(QueryRepController<int?, TestEntity, TestEntityGet>)
, 然后动态创建路由
但我搞了半天也没发现asp.net里面有相关的功能, 也不能确定这样生成的 Type
是正常的, 感觉这里面能让我栽进去的坑有很多
虽然可以自己重新实现一套路由......后面还得搞日志, 拦截器什么的 ?!?
我废那劲干嘛, 于是放弃
之前就听说C#有 Source Generator, 可以在编译时直接生成代码
还听说 AutoMapper
就用了这种技术(也不知道是真是假)
然后决定研究一下......
一个周末的时间让我了解到, 这东西好像没多少人用啊, 相关资料少得可怜, 网上逛了两天, 除了说这东西很有用, 很香, 没找着多少对我有用的资料, 也可能是我太菜了不会用
虽然最后生成了一个可以正常使用的 Controller
, 但是与我的预期有极大的差距
我期望的使用方式类似下面这种
services.AddQueryRepController<int?, TestEntity, TestEntityGet>("Test");
在使用的时候可以主动通过注册的方式添加 Controller, 然后可以自由更改路由(比如把Test改为WTF)
搞了两天感觉方向不对, 虽然 Source Generator 确实挺有意思的, 也有可以发挥的场景, 但至少不太符合我这时的需要
从 Source Generator 中抽身后, 我又开始大海捞针式地寻找方案
然后在 万能的stackoverflow 上找到了可能的方案
说实话在这之前我从来没有听说过 dotnet 中的 Emit
, 平时使用的反射也只是 GetValue``SetValue
这样的, 这鬼东西真是让我 大 开 眼 界
经过一番"艰苦"奋战后, 磕磕绊绊憋出了类似下面的代码
public static IServiceCollection AddQueryRepController<TKey, T, GetT>(this IServiceCollection services, string route)
where T : class, IBaseEntity<TKey> where GetT : IBaseGet<T>
{
// 建一个 Assembly
AssemblyBuilder Ass = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("NewController"), AssemblyBuilderAccess.Run);
ModuleBuilder MB = Ass.DefineDynamicModule("NewController");
// 起个好听的名字
var typeName = $"{route}Controller";
// 使用QueryRepController<TKey, T, GetT>整一个builder
var typeBuilder = MB.DefineType(typeName, TypeAttributes.Class | TypeAttributes.Public, typeof(QueryRepController<TKey, T, GetT>), null);
// 添加一个构造函数,
var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, new[] { typeof(IQueryRepository<TKey, T>) });
// 给这个构造函数编IL
var ilGenerator = ctor.GetILGenerator();
// 通过ILSpy反编译,然后抄il
ilGenerator.Emit(OpCodes.Ldarg, 0);
ilGenerator.Emit(OpCodes.Ldarg, 1);
ilGenerator.Emit(OpCodes.Call, typeof(QueryRepController<TKey, T, GetT>).GetConstructors()[0]);
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Nop);
ilGenerator.Emit(OpCodes.Ret);
// 创建这个新的 type
var type = typeBuilder.CreateType();
// 根据自己的情况注册到容器中
services.AddTransient(typeof(IQueryController<TKey, T, GetT>), type);
return services;
}
以我的水平和能力, 做到这样已经是极限, 靠ILSpy反编译上面的 TestController
, 抄了点代码(我抄我自己)
现在可以使用
services.AddQueryRepController<int?, TestEntity, TestEntityGet>("Test")
生成并注册一个 TestController 到容器中, 也可以正常获取实例
但是程序就是无法感知到代码的变化, swagger 中也看不到新加的 Controller
尝试进行请求, 最后也以 404 Not Found
失败告终
于是再次陷入僵局
之前在逛园子的时候看到 Artech大佬的 文章 , 当时看的时候感觉云里雾里的, 不知所云
也尝试硬着头皮写, 但是没有能够坚持下去, 但我在完成以上步骤并且被卡住后, 再次看了大佬的文章, 豁然开朗!
为了让这些程序集成为应用的一个有效组成部分,程序集需要封装成ApplicationPart对象并利用ApplicationPartManager进行注册
参考大佬的文章, 写了如下的实现
AddControllerChangeProvider
public class AddControllerChangeProvider : IActionDescriptorChangeProvider
{
public static AddControllerChangeProvider Instance { get; } = new AddControllerChangeProvider();
public CancellationTokenSource TokenSource { get; private set; }
public bool HasChanged { get; set; }
public IChangeToken GetChangeToken()
{
TokenSource = new CancellationTokenSource();
return new CancellationChangeToken(TokenSource.Token);
}
}
又有一个 HostedService
在注册完成后通过 ApplicationPartManager
更新注册信息
ChangeActionService
public class ChangeActionService : IHostedService
{
private readonly ApplicationPartManager Part;
public ChangeActionService(IServiceScopeFactory scope)
{
Part = scope.CreateScope().ServiceProvider.GetService<ApplicationPartManager>();
}
public async Task StartAsync(CancellationToken cancellationToken)
{
Part.ApplicationParts.Add(new AssemblyPart( <可以直接使用之前的AssemblyBuilder> ));
AddControllerChangeProvider.Instance.HasChanged = true;
AddControllerChangeProvider.Instance.TokenSource.Cancel();
await Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
await Task.CompletedTask;
}
}
之后使用时注册 AddControllerChangeProvider
和 ChangeActionService
services.AddSingleton<IActionDescriptorChangeProvider>(AddControllerChangeProvider.Instance);
services.AddHostedService<ChangeActionService>();
程序运行后会启动 ChangeActionService
, 读取我之前生成controller时使用的 AssemblyBuilder, 注册生成的新的controller
这时就已经可以在 swagger 中看到创建的 TestController 了, 并且也能正常进行访问
之后经过一系列过度封装, 简单的代码如下(用了很多自己的封装, 看看就好...)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMysql<TestDbContext>("localhost", 3306, "test", "root", "pwd")
// 将 TestDbContext 注册为默认的 DbContext
.AddDefaultDbContext<TestDbContext>()
.AddControllers();
builder.Services
// 注册一个 TestController
.AddQueryRepController<long?, TestEntity, TestEntityGet>("Test")
// 带注释的 Swagger
.AddSwaggerWithComments();
var app = builder.Build();
app.UseSwagger().UseSwaggerUI();
app.MapControllers();
app.Run();
public class TestDbContext : DbContext
{
public DbSet<TestEntity> Tests { get; set; }
public TestDbContext(DbContextOptions<TestDbContext> options) : base(options)
{ }
}
// 对应数据库中的 Test 表
public class TestEntity : BaseEntity<long?>
{
public string Code { get; set; }
public int? Number { get; set; }
public bool? IsTest { get; set; }
}
// 对应 TestEntity 的 TestEntityGet, 决定接口的查询规则
public class TestEntityGet : BaseGet<TestEntity>
{
public string? Code { get; set; }
public int? Number { get; set; }
public bool? IsTest { get; set; }
}
虽然没啥卵用, 但是写出这段代码的那一刻, 我自己是爽了, 有没有用已经不重要的
自己写包, 最重要的就是让自己开心!
我正在尝试使用以下 keytool 命令为我的应用程序生成 keystore : keytool -genkey -alias tomcat -keystore tomcat.keystore -ke
编辑:在西里尔正确解决问题后,我注意到只需将生成轴的函数放在用于生成标签的函数下面就可以解决问题。 我几乎读完了 O'Reilly 书中关于 D3.js 的教程,并在倒数第二页上制作了散点图,但是当添
虽然使用 GraphiQL 效果很好,但我的老板要求我实现一个用户界面,用户可以在其中通过 UI 元素(例如复选框、映射关系)检查呈现给他们的元素并获取数据,这样做将为该人生成 graphql 输入,
我尝试在 Netbean 6.8 中使用 ws-import 生成 Java 类。我想重新生成 jax-ws,因为在 ebay.api.paypalapi 包中发现了一个错误(我认为该错误是由于 Pa
我有一个 perl 脚本,它获取系统日期并将该日期写入文件名。 系统日期被分配给 TRH1 变量,然后它被设置为一个文件名。 $TRH1 =`date + %Y%m%d%H%M`; print "TR
我是 Haskell 的新手,需要帮助。我正在尝试构建一种必须具有某种唯一性的新数据类型,因此我决定使用 UUID 作为唯一标识符: data MyType = MyType { uuid ::
我制作了一个脚本,它可以根据 Mysql 数据库中的一些表生成 XML。 该脚本在 PHP 中运行。 public function getRawMaterials($apiKey, $format
所以这是我的项目中的一个问题。 In this task, we will use OpenSSL to generate digital signatures. Please prepare a f
我在 SAS LIFEREG 中有一个加速故障时间模型,我想绘制它。因为 SAS 在绘图方面非常糟糕,我想实际重新生成 R 中曲线的数据并将它们绘制在那里。 SAS 提出了一个尺度(在指数分布固定为
我正在为 Django 后端制作一个样板,并且我需要能够使它到达下一个下载它的人显然无法访问我的 secret key 的地方,或者拥有不同的 key 。我一直在研究一些选项,并在这个过程中进行了实验
我正在创建一个生成采购订单的应用程序。我可以根据用户输入的详细信息创建文本文件。我想生成一个看起来比普通文本文件好得多的 Excel。有没有可以在我的应用程序中使用的开源库? 最佳答案 目前还没有任何
我正在尝试使用 ScalaCheck 为 BST 创建一个 Gen,但是当我调用 .sample 方法时,它给了我 java.lang.NullPointerException。我哪里错了? seal
已关闭。此问题需要 debugging details 。目前不接受答案。 编辑问题以包含 desired behavior, a specific problem or error, and the
我尝试编写一些代码,例如(在verilog中): parameter N = 128; if (encoder_in[0] == 1) begin 23 binary_out = 1;
我正忙于在 Grails 项目中进行从 MySQL 到 Postgres 的相当复杂的数据迁移。 我正在使用 GORM 在 PostGres 中生成模式,然后执行 MySQL -> mysqldump
如何使用纯 XSLT 生成 UUID?基本上是寻找一种使用 XSLT 创建独特序列的方法。该序列可以是任意长度。 我正在使用 XSLT 2.0。 最佳答案 这是一个good example 。基本上,
我尝试安装.app文件,但是当我安装并单击“同步”(在iTunes中)时,我开始在设备上开始安装,然后停止,这是一个问题,我不知道在哪里,但我看到了我无法解决的奇怪的事情: 最佳答案 似乎您没有在Xc
自从我生成 JavaDocs 以来已经有一段时间了,我确信这些选项在过去 10 年左右的时间里已经得到了改进。 我能否得到一些有关生成器的建议,该生成器将输出类似于 .Net 文档结构的 JavaDo
我想学习如何生成 PDF,我不想使用任何第三方工具,我想自己用代码创建它。到目前为止,我所看到的唯一示例是我通过在第 3 方 dll 上打开反射器查看的代码,以查看发生了什么。不幸的是,到目前为止我看
我正在从 Epplus 库生成 excel 条形图。 这是我成功生成的。 我的 table 是这样的 Mumbai Delhi Financial D
我是一名优秀的程序员,十分优秀!