解决MySql布尔型新旧版本兼容问题,采用枚举来表示布尔型的数据表。由正向工程赋值
大石头 authored at 2018-05-15 21:21:05
10.18 KiB
X
using System.Collections.Concurrent;
using NewLife.Configuration;
using NewLife.Log;
using NewLife.Model;

namespace NewLife.Remoting;

/// <summary>服务解析器。根据服务名获取通信客户端,用于服务发现场景</summary>
/// <remarks>
/// 三层架构设计:
/// <list type="number">
/// <item><description>NewLife.Core 定义接口与基于配置的默认实现 <see cref="ConfigServiceResolver"/>,仅支持 http/https</description></item>
/// <item><description>NewLife.Remoting 提供 RemotingServiceResolver,扩展支持 tcp/udp/ws/wss 等长连接协议</description></item>
/// <item><description>Stardust 提供完整实现(通过 IRegistry),从注册中心动态发现服务地址并支持权重、自动更新</description></item>
/// </list>
/// 在类库项目中仅引用 NewLife.Core 即可通过 DI 获取该接口,
/// 由上层应用(如 Stardust)提供具体实现,支持注册中心的服务发现与动态地址更新。
/// </remarks>
public interface IServiceResolver
{
    /// <summary>为指定服务获取客户端,自动从注册中心订阅服务地址并支持动态更新</summary>
    /// <remarks>同名服务(含 tag)复用同一实例,推荐在应用生命周期内长持客户端</remarks>
    /// <param name="serviceName">服务名。用于在配置中心或注册中心定位服务地址</param>
    /// <param name="tag">特性标签。用于区分同一服务的不同环境或分组,为空时不区分</param>
    /// <returns>返回与服务通信的客户端实例</returns>
    Task<IApiClient> GetClientAsync(String serviceName, String? tag = null);

    /// <summary>解析服务的地址列表,供调用方自行创建客户端</summary>
    /// <remarks>
    /// 与 <see cref="GetClientAsync"/> 的区别:
    /// <list type="bullet">
    /// <item><description><see cref="GetClientAsync"/> 返回托管客户端(内置负载均衡、故障转移、自动更新)</description></item>
    /// <item><description><see cref="ResolveAddressesAsync"/> 仅返回地址字符串,适合需要自定义协议或客户端类型的场景</description></item>
    /// </list>
    /// </remarks>
    /// <param name="serviceName">服务名</param>
    /// <param name="tag">特性标签。为空时返回所有地址</param>
    /// <returns>地址列表,未找到时返回空数组</returns>
    Task<String[]> ResolveAddressesAsync(String serviceName, String? tag = null);
}

/// <summary>基于配置的服务解析器。从配置中心或本地配置文件读取服务地址,仅支持 http/https 协议</summary>
/// <remarks>
/// 以服务名为键从 <see cref="IConfigProvider"/> 读取地址(优先 DI 中的配置提供者,其次本地 appsettings.json),
/// 支持逗号或分号分隔的多地址(轮询负载均衡)。
/// 同名服务(含 tag)复用同一 <see cref="IApiClient"/> 实例。
/// 子类可重写 <see cref="BuildClient"/> 以支持更多协议(如 NewLife.Remoting 中的 RemotingServiceResolver)。
/// <para>地址解析优先级:<see cref="Servers"/> 属性 → DI 中的 IConfigProvider → 本地 appsettings.json</para>
/// </remarks>
public class ConfigServiceResolver : DisposeBase, IServiceResolver
{
    private readonly IServiceProvider? _serviceProvider;

    /// <summary>实例化,指定服务提供者,用于解析 IConfigProvider、ITracer、ILog 等依赖</summary>
    /// <param name="serviceProvider">服务提供者(DI 容器)</param>
    public ConfigServiceResolver(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    /// <summary>实例化,指定配置提供者,直接使用指定的配置源读取服务地址</summary>
    /// <param name="configProvider">配置提供者</param>
    public ConfigServiceResolver(IConfigProvider configProvider)
    {
        _configProvider = configProvider;
    }

    /// <summary>服务地址。直接指定所有服务统一使用的地址,优先级高于配置文件,支持逗号或分号分隔的多地址</summary>
    public String? Servers { get; set; }

    private IConfigProvider? _configProvider;
    private readonly ConcurrentDictionary<String, IApiClient> _clients = new(StringComparer.OrdinalIgnoreCase);

    /// <summary>销毁,释放已缓存的所有客户端</summary>
    /// <param name="disposing">true 表示从 Dispose 调用;false 表示终结器调用</param>
    protected override void Dispose(Boolean disposing)
    {
        base.Dispose(disposing);

        foreach (var client in _clients.Values)
        {
            client.TryDispose();
        }
        _clients.Clear();
    }

    /// <summary>为指定服务获取客户端,同名服务(含 tag)复用同一实例</summary>
    /// <param name="serviceName">服务名。不能为空</param>
    /// <param name="tag">特性标签。用于区分同一服务的不同分组</param>
    /// <returns>与服务通信的 IApiClient 实例</returns>
    public virtual Task<IApiClient> GetClientAsync(String serviceName, String? tag = null)
    {
        if (serviceName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(serviceName));

        var key = tag.IsNullOrEmpty() ? serviceName : $"{serviceName}#{tag}";

        if (_clients.TryGetValue(key, out var client)) return Task.FromResult(client);

        // 客户端持有 HttpClient/连接池,不能走 GetOrAdd 的“工厂可多跑”语义(败者会被静默丢弃而泄漏);
        // 先创建再 CAS,败者就地释放(同 ServiceEndpoint.TryGetClient 口径)
        var newClient = CreateClient(serviceName, tag);
        client = _clients.GetOrAdd(key, newClient);
        if (!ReferenceEquals(client, newClient)) newClient.TryDispose();

        return Task.FromResult(client);
    }

    /// <summary>解析服务的地址列表,供调用方自行创建客户端</summary>
    /// <param name="serviceName">服务名。不能为空</param>
    /// <param name="tag">特性标签(本实现不使用,由注册中心实现用于过滤)</param>
    /// <returns>地址列表,未找到时返回空数组</returns>
    public virtual Task<String[]> ResolveAddressesAsync(String serviceName, String? tag = null)
    {
        if (serviceName.IsNullOrEmpty()) throw new ArgumentNullException(nameof(serviceName));

        var address = ReadAddress(serviceName);
        if (address.IsNullOrEmpty()) return Task.FromResult(new String[0]);

        var addrs = address.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries);
        for (var i = 0; i < addrs.Length; i++)
        {
            addrs[i] = addrs[i].Trim();
        }
        return Task.FromResult(addrs);
    }

    /// <summary>根据服务名读取地址字符串。按优先级:Servers 属性 → IConfigProvider → appsettings.json</summary>
    /// <param name="serviceName">服务名</param>
    /// <returns>地址字符串,未找到时返回 null</returns>
    private String? ReadAddress(String serviceName)
    {
        var address = Servers;
        if (!address.IsNullOrEmpty()) return address;

        var config = _configProvider;
        config ??= _serviceProvider?.GetService<IConfigProvider>();
        config ??= JsonConfigProvider.LoadAppSettings(null);
        return config?[serviceName];
    }

    /// <summary>根据服务名创建客户端。按优先级读取地址配置并调用 BuildClient</summary>
    /// <remarks>
    /// 地址解析优先级:
    /// <list type="number">
    /// <item><description><see cref="Servers"/> 属性(直接指定)</description></item>
    /// <item><description>DI 容器中的 <see cref="IConfigProvider"/></description></item>
    /// <item><description>本地 appsettings.json 文件</description></item>
    /// </list>
    /// 当地址来自配置(非 <see cref="Servers"/> 属性)时,自动将 <see cref="IConfigProvider"/> 绑定到客户端,
    /// 配置变更时客户端服务地址随之自动更新。
    /// </remarks>
    /// <param name="serviceName">服务名</param>
    /// <param name="tag">特性标签</param>
    /// <returns>新建的 IApiClient 实例</returns>
    protected virtual IApiClient CreateClient(String serviceName, String? tag)
    {
        var address = ReadAddress(serviceName);
        if (address.IsNullOrEmpty()) throw new InvalidOperationException($"配置中未找到服务[{serviceName}]的地址");

        var client = BuildClient(serviceName, address);

        // 地址来自配置时绑定 IConfigProvider,配置变更时自动更新客户端地址
        // Servers 属性为静态指定,无需绑定
        if (Servers.IsNullOrEmpty())
        {
            var config = _configProvider ?? _serviceProvider?.GetService<IConfigProvider>();
            if (config != null && client is IConfigMapping map)
                config.Bind(map, true, serviceName);
        }

        return client;
    }

    /// <summary>根据地址字符串构建客户端。仅支持 http/https,子类可重写以支持更多协议</summary>
    /// <param name="name">服务名,用作节点标识</param>
    /// <param name="address">地址,支持逗号或分号分隔的多地址</param>
    /// <returns>构建的 ApiHttpClient 实例</returns>
    protected virtual IApiClient BuildClient(String name, String address)
    {
        var addrs = address.Split([',', ';'], StringSplitOptions.RemoveEmptyEntries);
        if (addrs.Length == 0) throw new InvalidOperationException($"服务[{name}]地址不能为空");

        foreach (var addr in addrs)
        {
            var a = addr.Trim();
            if (!a.StartsWith("http://", StringComparison.OrdinalIgnoreCase) &&
                !a.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
                throw new NotSupportedException($"服务[{name}]地址协议不支持(仅支持 http/https):{a}");
        }

        var http = new ApiHttpClient
        {
            LoadBalanceMode = LoadBalanceMode.RoundRobin,
            ServiceProvider = _serviceProvider,
            Tracer = _serviceProvider?.GetService<ITracer>(),
        };
        var log = _serviceProvider?.GetService<ILog>();
        if (log != null) http.Log = log;

        // AddServer 仅支持逗号分隔,统一替换分号
        http.AddServer(name, address.Replace(';', ','));

        return http;
    }
}