前言

之前看到技术群里有同学讨论说对于MinimalApi能接入到Swagger中感到很神奇,加上Swagger的数据本身是支持OpenApi2.0OpenApi3.0使得swagger.json成为了许多接口文档管理工具的标准数据源。ASP.NET Core能够轻松快速的集成Swagger得益于微软对OpenApi的大力支持,大部分情况下几乎是添加默认配置,就能很好的工作了。这一切都是得益于ASP.NET Core底层提供了对接口元数据的描述和对终结点的相关描述。本文我们就通过MinimalApi来了解一下ASP.NET Core为何能更好的集成Swagger。

使用方式

虽然我们讨论的是MInimalApi与Swagger数据源的关系,但是为了使得看起来更清晰,我们还是先看一下MinimalApi如何集成到Swagger,直接上代码

var builder = WebApplication.CreateBuilder(args);

//这是重点,是ASP.NET Core自身提供的
builder.Services.AddEndpointsApiExplorer();
//添加swagger配置
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new()
{
Title = builder.Environment.ApplicationName,
Version = "v1"
});
}); var app = builder.Build(); if (app.Environment.IsDevelopment())
{
//swagger终结点
app.UseSwagger();
app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json",
$"{builder.Environment.ApplicationName} v1"));
} app.MapGet("/swag", () => "Hello Swagger!"); app.Run();

上面我们提到了AddEndpointsApiExplorer是ASP.NET Core自身提供的,但是如果使得MinimalApi能在Swagger中展示就必须要添加这个服务。所以Swagger还是那个Swagger,变的是ASP.NET Core本身,但是变化是如何适配数据源的问题,Swagger便是建立在这个便利基础上。接下来咱们就通过源码看一下它们之间的关系。

源码探究

想了解它们的关系就会涉及到两个主角,一个是swagger的数据源来自何处,另一个是ASP.NET Core是如何提供这个数据源的。首先我们来看一下Swagger的数据源来自何处。

swagger的数据源

熟悉Swashbuckle.AspNetCore的应该知道它其实是由几个程序集一起构建的,也就是说Swashbuckle.AspNetCore本身是一个解决方案,不过这不是重点,其中生成Swagger.json的是在Swashbuckle.AspNetCore.SwaggerGen程序集中,直接找到位置在SwaggerGenerator类中[点击查看源码]只摘要我们关注的地方即可

public class SwaggerGenerator : ISwaggerProvider
{
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider;
private readonly ISchemaGenerator _schemaGenerator;
private readonly SwaggerGeneratorOptions _options; public SwaggerGenerator(
SwaggerGeneratorOptions options,
IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
ISchemaGenerator schemaGenerator)
{
_options = options ?? new SwaggerGeneratorOptions();
_apiDescriptionsProvider = apiDescriptionsProvider;
_schemaGenerator = schemaGenerator;
} /// <summary>
/// 获取Swagger文档的核心方法
/// </summary>
public OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null)
{
if (!_options.SwaggerDocs.TryGetValue(documentName, out OpenApiInfo info))
throw new UnknownSwaggerDocument(documentName, _options.SwaggerDocs.Select(d => d.Key)); //组装OpenApiDocument核心数据源源来自_apiDescriptionsProvider
var applicableApiDescriptions = _apiDescriptionsProvider.ApiDescriptionGroups.Items
.SelectMany(group => group.Items)
.Where(apiDesc => !(_options.IgnoreObsoleteActions && apiDesc.CustomAttributes().OfType<ObsoleteAttribute().Any()))
.Where(apiDesc => _options.DocInclusionPredicate(documentName, apiDesc)); var schemaRepository = new SchemaRepository(documentName); var swaggerDoc = new OpenApiDocument
{
Info = info,
Servers = GenerateServers(host, basePath),
// Paths组装是来自applicableApiDescriptions
Paths = GeneratePaths(applicableApiDescriptions, schemaRepository),
Components = new OpenApiComponents
{
Schemas = schemaRepository.Schemas,
SecuritySchemes = new Dictionary<string, OpenApiSecurityScheme>(_options.SecuritySchemes)
},
SecurityRequirements = new List<OpenApiSecurityRequirement>(_options.SecurityRequirements)
}; //省略其他代码
return swaggerDoc;
}
}

如果你比较了解Swagger.json的话那么对OpenApiDocument这个类的结构一定是一目了然,不信的话你可以自行看看它的结构

{
"openapi": "3.0.1",
"info": {
"title": "MyTest.WebApi",
"description": "测试接口",
"version": "v1"
},
"paths": {
"/": {
"get": {
"tags": [
"MyTest.WebApi"
],
"responses": {
"200": {
"description": "Success",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
}
}
}
}
},
"components": {}
}

这么看清晰了吧OpenApiDocument这个类就是返回Swagger.json的模型类,而承载描述接口信息的核心字段paths正是来自IApiDescriptionGroupCollectionProvider。所以小结一下,Swagger接口的文档信息的数据源来自于IApiDescriptionGroupCollectionProvider

ASP.Net Core如何提供

通过上面在Swashbuckle.AspNetCore.SwaggerGen程序集中,我们看到了真正组装Swagger接口文档部分的数据源来自于IApiDescriptionGroupCollectionProvider,但是这个接口并非来自Swashbuckle而是来自ASP.NET Core。这就引入了另一个主角,也是我们上面提到的AddEndpointsApiExplorer方法。直接在dotnet/aspnetcore仓库里找到方法位置[点击查看源码]看一下方法实现

public static IServiceCollection AddEndpointsApiExplorer(this IServiceCollection services)
{
services.TryAddSingleton<IActionDescriptorCollectionProvider, DefaultActionDescriptorCollectionProvider>();
//swagger用到的核心操作IApiDescriptionGroupCollectionProvider
services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApiDescriptionProvider, EndpointMetadataApiDescriptionProvider>());
return services;
}

看到了AddEndpointsApiExplorer方法相信就明白了为啥要添加这个方法了吧,那你就有疑问了为啥不使用MinimalApi的时候就不用引入AddEndpointsApiExplorer这个方法了,况且也能使用swagger。这是因为在AddControllers方法里添加了AddApiExplorer方法,这个方法里包含了针对Controller的接口描述信息,这里就不过多说了,毕竟这种的核心是MinimalApi。接下来就看下IApiDescriptionGroupCollectionProvider接口的默认实现ApiDescriptionGroupCollectionProvider类里的实现[点击查看源码]

public class ApiDescriptionGroupCollectionProvider : IApiDescriptionGroupCollectionProvider
{
private readonly IActionDescriptorCollectionProvider _actionDescriptorCollectionProvider;
private readonly IApiDescriptionProvider[] _apiDescriptionProviders;
private ApiDescriptionGroupCollection? _apiDescriptionGroups; public ApiDescriptionGroupCollectionProvider(
IActionDescriptorCollectionProvider actionDescriptorCollectionProvider,
IEnumerable<IApiDescriptionProvider> apiDescriptionProviders)
{
_actionDescriptorCollectionProvider = actionDescriptorCollectionProvider;
_apiDescriptionProviders = apiDescriptionProviders.OrderBy(item => item.Order).ToArray();
} public ApiDescriptionGroupCollection ApiDescriptionGroups
{
get
{
var actionDescriptors = _actionDescriptorCollectionProvider.ActionDescriptors;
if (_apiDescriptionGroups == null || _apiDescriptionGroups.Version != actionDescriptors.Version)
{
//如果_apiDescriptionGroups为null则使用GetCollection方法返回的数据
_apiDescriptionGroups = GetCollection(actionDescriptors);
}
return _apiDescriptionGroups;
}
} private ApiDescriptionGroupCollection GetCollection(ActionDescriptorCollection actionDescriptors)
{
var context = new ApiDescriptionProviderContext(actionDescriptors.Items); //这里使用了_apiDescriptionProviders
foreach (var provider in _apiDescriptionProviders)
{
provider.OnProvidersExecuting(context);
} for (var i = _apiDescriptionProviders.Length - 1; i >= 0; i--)
{
_apiDescriptionProviders[i].OnProvidersExecuted(context);
} var groups = context.Results
.GroupBy(d => d.GroupName)
.Select(g => new ApiDescriptionGroup(g.Key, g.ToArray()))
.ToArray();
return new ApiDescriptionGroupCollection(groups, actionDescriptors.Version);
}
}

这里我们看到了IApiDescriptionProvider[]通过上面的方法我们可以知道IApiDescriptionProvider默认实现是EndpointMetadataApiDescriptionProvider类[点击查看源码]看一下相实现

internal class EndpointMetadataApiDescriptionProvider : IApiDescriptionProvider
{
private readonly EndpointDataSource _endpointDataSource;
private readonly IHostEnvironment _environment;
private readonly IServiceProviderIsService? _serviceProviderIsService;
private readonly ParameterBindingMethodCache ParameterBindingMethodCache = new(); public EndpointMetadataApiDescriptionProvider(
EndpointDataSource endpointDataSource,
IHostEnvironment environment,
IServiceProviderIsService? serviceProviderIsService)
{
_endpointDataSource = endpointDataSource;
_environment = environment;
_serviceProviderIsService = serviceProviderIsService;
} public void OnProvidersExecuting(ApiDescriptionProviderContext context)
{
//核心数据来自EndpointDataSource类
foreach (var endpoint in _endpointDataSource.Endpoints)
{
if (endpoint is RouteEndpoint routeEndpoint &&
routeEndpoint.Metadata.GetMetadata<MethodInfo>() is { } methodInfo &&
routeEndpoint.Metadata.GetMetadata<IHttpMethodMetadata>() is { } httpMethodMetadata &&
routeEndpoint.Metadata.GetMetadata<IExcludeFromDescriptionMetadata>() is null or { ExcludeFromDescription: false })
{
foreach (var httpMethod in httpMethodMetadata.HttpMethods)
{
context.Results.Add(CreateApiDescription(routeEndpoint, httpMethod, methodInfo));
}
}
}
} private ApiDescription CreateApiDescription(RouteEndpoint routeEndpoint, string httpMethod, MethodInfo methodInfo)
{
//实现代码省略
}
}

这个类里还有其他方法代码也非常多,都是在组装ApiDescription里的数据,通过名称可以得知,这个类是为了描述API接口信息用的,但是我们了解到的是它的数据源都来自EndpointDataSource类的实例。我们都知道MinimalApi提供的操作方法就是MapGetMapPostMapPutMapDelete等等,这些方法的本质都是在调用Map方法[点击查看源码],看一下核心实现

private static RouteHandlerBuilder Map(this IEndpointRouteBuilder endpoints,
RoutePattern pattern, Delegate handler, bool disableInferBodyFromParameters)
{
//省略部分代码
var requestDelegateResult = RequestDelegateFactory.Create(handler, options);
var builder = new RouteEndpointBuilder(requestDelegateResult.RequestDelegate,pattern,defaultOrder)
{
//路由名称
DisplayName = pattern.RawText ?? pattern.DebuggerToString(),
}; //获得httpmethod
builder.Metadata.Add(handler.Method); if (GeneratedNameParser.TryParseLocalFunctionName(handler.Method.Name, out var endpointName)
|| !TypeHelper.IsCompilerGeneratedMethod(handler.Method))
{
endpointName ??= handler.Method.Name;
builder.DisplayName = $"{builder.DisplayName} => {endpointName}";
} var attributes = handler.Method.GetCustomAttributes(); foreach (var metadata in requestDelegateResult.EndpointMetadata)
{
builder.Metadata.Add(metadata);
} if (attributes is not null)
{
foreach (var attribute in attributes)
{
builder.Metadata.Add(attribute);
}
} // 添加ModelEndpointDataSource
var dataSource = endpoints.DataSources.OfType<ModelEndpointDataSource>().FirstOrDefault();
if (dataSource is null)
{
dataSource = new ModelEndpointDataSource();
endpoints.DataSources.Add(dataSource);
}
//将RouteEndpointBuilder添加到ModelEndpointDataSource
return new RouteHandlerBuilder(dataSource.AddEndpointBuilder(builder));
}

通过Map方法我们可以看到每次添加一个MinimalApi终结点都会给ModelEndpointDataSource实例添加一个EndpointBuilder实例,EndPointBuilder里承载着MinimalApi终结点的信息,而ModelEndpointDataSource则是继承了EndpointDataSource类,这个可以看它的定义[点击查看源码]

internal class ModelEndpointDataSource : EndpointDataSource
{
}

这就和上面提到的EndpointMetadataApiDescriptionProvider里的EndpointDataSource联系起来了,但是我们这里看到的是IEndpointRouteBuilderDataSources属性,从名字看这明显是一个集合,我们可以找到定义的地方看一下[点击查看源码]

public interface IEndpointRouteBuilder
{
IApplicationBuilder CreateApplicationBuilder();
IServiceProvider ServiceProvider { get; }
//这里是一个EndpointDataSource的集合
ICollection<EndpointDataSource> DataSources { get; }
}

这里既然是一个集合那如何和EndpointDataSource联系起来呢,接下来我们就得去看EndpointDataSource是如何被注册的即可,找到EndpointDataSource注册的地方[点击查看源码]查看一下注册代码

var dataSources = new ObservableCollection<EndpointDataSource>();
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, ConfigureRouteOptions>(
serviceProvider => new ConfigureRouteOptions(dataSources))); services.TryAddSingleton<EndpointDataSource>(s =>
{
return new CompositeEndpointDataSource(dataSources);
});

通过这段代码我们可以得到两点信息

  • 一是EndpointDataSource这个抽象类,系统给他注册的是CompositeEndpointDataSource这个子类,看名字可以看出是组合的EndpointDataSource
  • 二是CompositeEndpointDataSource是通过ObservableCollection<EndpointDataSource>这么一个集合来初始化的

我们可以简单的来看下CompositeEndpointDataSource传递的dataSources是如何被接收的[点击查看源码]咱们只关注他说如何被接收的

public sealed class CompositeEndpointDataSource : EndpointDataSource
{
private readonly ICollection<EndpointDataSource> _dataSources = default!;
internal CompositeEndpointDataSource(ObservableCollection<EndpointDataSource> dataSources) : this()
{
_dataSources = dataSources;
} public IEnumerable<EndpointDataSource> DataSources => _dataSources;
}

通过上面我们可以看到,系统默认为EndpointDataSource抽象类注册了CompositeEndpointDataSource实现类,而这个实现类是一个组合类,它组合了一个EndpointDataSource的集合。那么到了这里就只剩下一个问题了,那就是EndpointDataSource是如何和IEndpointRouteBuilderDataSources属性关联起来的。现在有了提供数据源的IEndpointRouteBuilder,有承载数据的EndpointDataSource。这个地方呢大家也比较熟悉那就是UseEndpoints中间件里,我们来看下是如何实现的[点击查看源码]

public static IApplicationBuilder UseEndpoints(this IApplicationBuilder builder, Action<IEndpointRouteBuilder> configure)
{
// 省略一堆代码 //得到IEndpointRouteBuilder实例
VerifyEndpointRoutingMiddlewareIsRegistered(builder, out var endpointRouteBuilder);
//获取RouteOptions
var routeOptions = builder.ApplicationServices.GetRequiredService<IOptions<RouteOptions>>();
//遍历IEndpointRouteBuilder的DataSources
foreach (var dataSource in endpointRouteBuilder.DataSources)
{
if (!routeOptions.Value.EndpointDataSources.Contains(dataSource))
{
//dataSource放入RouteOptions的EndpointDataSources集合
routeOptions.Value.EndpointDataSources.Add(dataSource);
}
} return builder.UseMiddleware<EndpointMiddleware>();
} private static void VerifyEndpointRoutingMiddlewareIsRegistered(IApplicationBuilder app, out IEndpointRouteBuilder endpointRouteBuilder)
{
if (!app.Properties.TryGetValue(EndpointRouteBuilder, out var obj))
{
throw new InvalidOperationException();
} endpointRouteBuilder = (IEndpointRouteBuilder)obj!; if (endpointRouteBuilder is DefaultEndpointRouteBuilder defaultRouteBuilder && !object.ReferenceEquals(app, defaultRouteBuilder.ApplicationBuilder))
{
throw new InvalidOperationException();
}
}

这里我们看到是获取的IOptions<RouteOptions>里的EndpointDataSources,怎么和预想的剧本不一样呢?并非如此,你看上面咱们说的这段代码

var dataSources = new ObservableCollection<EndpointDataSource>();
services.TryAddEnumerable(ServiceDescriptor.Transient<IConfigureOptions<RouteOptions>, ConfigureRouteOptions>(
serviceProvider => new ConfigureRouteOptions(dataSources)));

上面的dataSources同时传递给了CompositeEndpointDataSourceConfigureRouteOptions,而ConfigureRouteOptions则正是IConfigureOptions<RouteOptions>类型的,所以获取IOptions<RouteOptions>就是获取的ConfigureRouteOptions的实例,咱们来看一下ConfigureRouteOptions类的实现[点击查看源码]

internal class ConfigureRouteOptions : IConfigureOptions<RouteOptions>
{
private readonly ICollection<EndpointDataSource> _dataSources; public ConfigureRouteOptions(ICollection<EndpointDataSource> dataSources)
{
if (dataSources == null)
{
throw new ArgumentNullException(nameof(dataSources));
} _dataSources = dataSources;
} public void Configure(RouteOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
} options.EndpointDataSources = _dataSources;
}
}

它的本质操作就是对RouteOptions的EndpointDataSources的属性进行操作,因为ICollection<EndpointDataSource>是引用类型,所以这个集合是共享的,因此IEndpointRouteBuilderDataSourcesIConfigureOptions<RouteOptions>本质是使用了同一个ICollection<EndpointDataSource>集合,所以上面的UseEndpoints里获取RouteOptions选项的本质正是获取的EndpointDataSource集合。

每次对IEndpointRouteBuilderDataSources集合Add的时候其实是在为ICollection<EndpointDataSource>集合添加数据,而IConfigureOptions<RouteOptions>也使用了这个集合,所以它们的数据是互通的。

许多同学都很好强,默认并没在MinimalApi看到注册UseEndpoints,但是在ASP.NET Core6.0之前还是需要注册UseEndpoints中间件的。这其实是ASP.NET Core6.0进行的一次升级优化,因为很多操作默认都得添加,所以把它统一封装起来了,这个可以在WebApplicationBuilder类中看到[点击查看源码]在ConfigureApplication方法中的代码

private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app)
{
// 省略部分代码 // 注册UseDeveloperExceptionPage全局异常中间件
if (context.HostingEnvironment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.Properties.Add(WebApplication.GlobalEndpointRouteBuilderKey, _builtApplication); if (_builtApplication.DataSources.Count > 0)
{
// 注册UseRouting中间件
if (!_builtApplication.Properties.TryGetValue(EndpointRouteBuilderKey, out var localRouteBuilder))
{
app.UseRouting();
}
else
{
app.Properties[EndpointRouteBuilderKey] = localRouteBuilder;
}
} app.Use(next =>
{
//调用WebApplication的Run方法
_builtApplication.Run(next);
return _builtApplication.BuildRequestDelegate();
}); // 如果DataSources集合有数据则注册UseEndpoints
if (_builtApplication.DataSources.Count > 0)
{
app.UseEndpoints(_ => { });
} // 省略部分代码
}

相信大家通过ConfigureApplication这个方法大家就了解了吧,之前我们能看到的熟悉方法UseDeveloperExceptionPageUseRoutingUseEndpoints方法都在这里,毕竟之前这几个方法几乎也成了新建项目时候必须要添加的,所以微软干脆就在内部统一封装起来了。

源码小结

上面咱们分析了相关的源码,整理起来就是这么一个思路。

  • Swashbuckle.AspNetCore.SwaggerGen用来生成swagger的数据源来自IApiDescriptionGroupCollectionProvider
  • IApiDescriptionGroupCollectionProvider实例的数据来自EndpointDataSource
  • 因为EndpointDataSourceDataSourcesIConfigureOptions<RouteOptions>本质是使用了同一个ICollection<EndpointDataSource>集合,所以它们是同一份数据
  • 每次使用MinimalApi的Map相关的方法的是会给IEndpointRouteBuilderDataSources集合添加数据
  • UseEndpoints中间件里获取IEndpointRouteBuilderDataSources数据给RouteOptions选项的EndpointDataSources集合属性添加数据,本质则是给ICollection<EndpointDataSource>集合赋值,自然也就是给EndpointDataSourceDataSources属性赋值

这也给我们提供了一个思路,如果你想自己去适配swagger数据源的话完全也可以参考这个思路,想办法把你要提供的接口信息放到EndpointDataSource的DataSources集合属性里即可,或者直接适配IApiDescriptionGroupCollectionProvider里的数据,有兴趣的同学可以自行研究一下。

使用扩展

我们看到了微软给我们提供了IApiDescriptionGroupCollectionProvider这个便利条件,所以如果以后有获取接口信息的时候则可以直接使用了,很多时候比如写监控程序或者写Api接口调用的代码生成器的时候都可以考虑一下,咱们简单的示例一下如何使用,首先定义个模型类来承载接口信息

public class ApiDoc
{
/// <summary>
/// 接口分组
/// </summary>
public string Group { get; set; } /// <summary>
/// 接口路由
/// </summary>
public string Route { get; set; } /// <summary>
/// http方法
/// </summary>
public string HttpMethod { get; set; }
}

这个类非常简单只做演示使用,然后我们在IApiDescriptionGroupCollectionProvider里获取信息来填充这个集合,这里我们写一个htt接口来展示

app.MapGet("/apiinfo", (IApiDescriptionGroupCollectionProvider provider) => {
List<ApiDoc> docs = new List<ApiDoc>();
foreach (var group in provider.ApiDescriptionGroups.Items)
{
foreach (var apiDescription in group.Items)
{
docs.Add(new ApiDoc
{
Group = group.GroupName,
Route = apiDescription.RelativePath,
HttpMethod = apiDescription.HttpMethod
});
}
}
return docs;
});

这个时候当你在浏览器里请求/apiinfo路径的时候会返回你的webapi包含的接口相关的信息。咱们的示例是非常简单的,实际上IApiDescriptionGroupCollectionProvider包含的接口信息是非常多的包含请求参数信息、输出返回信息等很全面,这也是swagger可以完全依赖它的原因,有兴趣的同学可以自行的了解一下,这里就不过多讲解了。

总结

本文咱们主要通过MinimalApi如何适配swagger的这么一个过程来讲解了ASP.NET Core是如何给Swagger提供了数据的。本质是微软在ASP.NET Core本身提供了IApiDescriptionGroupCollectionProvider这么一个数据源,Swagger借助这个数据源生成了swagger文档,IApiDescriptionGroupCollectionProvider来自声明终结点的时候往EndpointDataSourceDataSources集合里添加的接口信息等。其实它内部比这个还要复杂一点,不过如果我们用来获取接口信息的话,大部分时候使用IApiDescriptionGroupCollectionProvider应该就足够了。

    分享一段我个人比较认可的话,与其天天钻头觅缝、找各种机会,不如把这些时间和金钱投入到自己的能力建设上。机会稍纵即逝,而且别人给你的机会,没准儿反而是陷阱。而投资个人能力就是积累一个资产账户,只能越存越多,看起来慢,但是你永远在享受时间带来的复利,其实快得很,收益也稳定得多。有了能力之后,机会也就来了。

欢迎扫码关注我的公众号

深入探究MinimalApi是如何在Swagger中展示的的更多相关文章

  1. 如何在Word中排出漂亮的代码

    引言 学数学和计算机,当然还是用LaTeX排版技术文章更方便.但有时候还是迫不得已需要用Word写作,另外Word其实也有Word的好处,比如细节上的修改要比LaTeX方便. 从Matlab高亮代码复 ...

  2. 浅析如何在Nancy中使用Swagger生成API文档

    前言 上一篇博客介绍了使用Nancy框架内部的方法来创建了一个简单到不能再简单的Document.但是还有许许多多的不足. 为了能稍微完善一下这个Document,这篇引用了当前流行的Swagger, ...

  3. 浅析如何在Nancy中生成API文档

    前言 前后端分离,或许是现如今最为流行开发方式,包括UWP.Android和IOS这样的手机客户端都是需要调用后台的API来进行数据的交互. 但是这样对前端开发和APP开发就会面临这样一个问题:如何知 ...

  4. 服务化改造实践 | 如何在 Dubbo 中支持 REST

    什么是 REST REST 是 Roy Thomas Fielding [[1]](#fn1) 在 2000 年他的博士论文 [[2]](#fn2) “架构风格以及基于网络的软件架构设计” 中提出来的 ...

  5. 详解如何在Laravel中增加自定义全局函数

    http://www.php.cn/php-weizijiaocheng-383928.html 如何在Laravel中增加自定义全局函数?在我们的应用里经常会有一些全局都可能会用的函数,我们应该怎么 ...

  6. 我是如何在SQLServer中处理每天四亿三千万记录的

    首先声明,我只是个程序员,不是专业的DBA,以下这篇文章是从一个问题的解决过程去写的,而不是一开始就给大家一个正确的结果,如果文中有不对的地方,请各位数据库大牛给予指正,以便我能够更好的处理此次业务. ...

  7. 如何在SpringBoot中使用JSP ?但强烈不推荐,果断改Themeleaf吧

    做WEB项目,一定都用过JSP这个大牌.Spring MVC里面也可以很方便的将JSP与一个View关联起来,使用还是非常方便的.当你从一个传统的Spring MVC项目转入一个Spring Boot ...

  8. 如何在latex 中插入EPS格式图片

    如何在latex 中插入EPS格式图片 第一步:生成.eps格式的图片 1.利用visio画图,另存为pdf格式的图片 利用Adobe Acrobat裁边,使图片大小合适 另存为.eps格式,如下图所 ...

  9. 如何正确的使用json?如何在.Net中使用json?

    什么是json json是一种轻量级的数据交换格式,由N组键值对组成的字符串,完全独立于语言的文本格式. 为什么要使用json 在很久很久以前,调用第三方API时,我们通常是采用xml进行数据交互,但 ...

随机推荐

  1. Idea中配置Tomcat以及运行maven项目

    maven安装和详细配置 提示:下面是Tomcat9.0版本的下载链接,需要其他版本的去官方网站下载. 链接:https://pan.baidu.com/s/1CONf8KVXM4gyJj4pxjFB ...

  2. Ubuntu安装docker-compose(摘自官网,自用)

    安装 Docker Compose 预计阅读时间:8分钟 加速 Docker 桌面中的新功能 Docker Desktop 可帮助您在 Mac 和 Windows 上轻松构建.共享和运行容器,就像在 ...

  3. 「实践篇」解决微前端 single-spa 项目中 Vue 和 React 路由跳转问题

    前言 本文介绍的是在做微前端 single-spa 项目过程中,遇到的 Vue 子应用和 React 子应用互相跳转路由时遇到的问题. 项目情况:single-spa 项目,基座用的是 React,目 ...

  4. linux升级Nginx1.6到Nginx1.12.2

    我的升级环境: 旧版Nginx:1.6 新版Nginx:1.12.2 系统:Redhat 5.5 64位     前期准备 1.查看Nginx的安装位置 ps -ef |grep nginx  --如 ...

  5. Win下GCC报错 error: ‘for’ loop initial declarations are only allowed in C99 mode

    ##报错## 用GCC编译for循环会出现以下错误 error: 'for' loop initial declarations are only allowed in C99 mode 如图所示: ...

  6. Codeforces Round #710 (Div. 3) Editorial 1506A - Strange Table

    题目链接 https://codeforces.com/contest/1506/problem/A 原题 1506A - Strange Table Example input 5 1 1 1 2 ...

  7. 新手入门C语言第八章:C循环

    一.C 循环 有的时候,我们可能需要多次执行同一块代码.一般情况下,语句是按顺序执行的:函数中的第一个语句先执行,接着是第二个语句,依此类推.编程语言提供了更为复杂执行路径的多种控制结构.循环语句允许 ...

  8. 2021.07.18 P2290 树的计数(prufer序列、组合数学)

    2021.07.18 P2290 树的计数(prufer序列.组合数学) [P2290 HNOI2004]树的计数 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 重点: 1.pru ...

  9. jsp第一周作业

    环境搭建,运行出来一个JSP页面,显式hello 英文字母表 <%@ page language="java" import="java.util.*" ...

  10. JavaScript函数中的arguments对象

    ECMAScript标准中,每个函数都有一个特殊的内置对象arguments.arguments对象是一个类Array对象(object),用以保存函数接收到的实参副本. 一.内置特性 说它是一个内置 ...