22.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--增加公共代码
在拆分服务之前,我们需要先提取一些公共代码。本篇将重点新增日志记录、异常处理以及Redis的通用代码。这些组件将被整合到一个共享类库中,便于在微服务架构中高效复用。
Tip:在后续的教程中我们会穿插多篇提取公共代码的文章,帮助大家更好地理解如何将单体应用拆分为微服务。
在创建通用代码前,我们需要创建通用代码类库。首先,我们需要在当前git库中新建一个基于单体应用的分支 Microservices,并切换到这个分支,后续的操作都在这个分支上进行。具体操作这里就不赘述了,不清楚的可以去翻阅我的另一个专栏《GIT版本控制》。接着,在解决方案下新建一个类库项目,命名为 SP.Common,这个类库将用于存放我们提取的公共代码。最后在 SP.Common 类库中创建三个文件夹,分别命名为 Redis、ExceptionHandling 和 Logger,用于存放Redis相关的代码、异常处理相关的代码和日志记录相关的代码。
在创建完文件夹后,我们就可以开始编写代码了。我们将从Redis相关的代码开始,接着是异常处理相关的代码,最后是日志记录相关的代码。每个部分的代码都将包含详细的注释和说明,以便于大家理解和使用。
一、Redis代码
在 Redis 文件夹中我们要实现Redis相关的通用代码,代码结构采用接口与实现分离的设计模式,通过IRedisService
接口定义操作,RedisService
类实现具体功能,同时使用依赖注入和选项模式简化配置和使用。整体设计封装了底层StackExchange.Redis
库的操作,提供异常处理和日志记录,支持多种Redis功能包括字符串、对象操作、哈希表、分布式锁和发布订阅。
1.1 Redis配置类
首先,我们需要创建Redis配置类,用于存储Redis连接的相关配置信息。这个类将作为选项模式(Options Pattern)的一部分,便于在依赖注入系统中配置和使用Redis服务。代码很简单,这里就不讲解了。
namespace SP.Common.Redis
{/// <summary>/// Redis配置选项/// </summary>public class RedisOptions{/// <summary>/// Redis连接字符串/// </summary>public string ConnectionString { get; set; } = "localhost:6379";/// <summary>/// 默认数据库索引/// </summary>public int DefaultDatabase { get; set; } = 0;/// <summary>/// 连接空闲超时时间(秒)/// </summary>public int ConnectionIdleTimeout { get; set; } = 180;/// <summary>/// 连接超时时间(毫秒)/// </summary>public int ConnectTimeout { get; set; } = 5000;/// <summary>/// 默认缓存过期时间(秒)/// </summary>public int DefaultExpireSeconds { get; set; } = 3600;}
}
Tip:选项模式(Options Pattern)是.NET Core中一种推荐的配置管理方式,它允许我们将应用程序的配置分离到一个或多个类中,并通过依赖注入将这些配置类注入到需要它们的服务中,这样可以使代码更加清晰和可维护。
1.2 Redis服务接口
接下来,我们需要创建Redis服务接口IRedisService
,这个接口定义了我们需要的Redis操作方法,包括字符串、对象、哈希表、分布式锁和发布订阅等操作。代码如下:
namespace SP.Common.Redis
{/// <summary>/// Redis服务接口/// </summary>public interface IRedisService{/// <summary>/// 获取字符串值/// </summary>/// <param name="key">键</param>/// <returns>字符串值</returns>Task<string?> GetStringAsync(string key);/// <summary>/// 设置字符串值/// </summary>/// <param name="key">键</param>/// <param name="value">值</param>/// <param name="expirySeconds">过期时间(秒),默认使用配置中的默认过期时间</param>/// <returns>是否成功</returns>Task<bool> SetStringAsync(string key, string value, int? expirySeconds = null);/// <summary>/// 获取对象/// </summary>/// <typeparam name="T">对象类型</typeparam>/// <param name="key">键</param>/// <returns>对象</returns>Task<T?> GetAsync<T>(string key) where T : class;/// <summary>/// 设置对象/// </summary>/// <typeparam name="T">对象类型</typeparam>/// <param name="key">键</param>/// <param name="value">值</param>/// <param name="expirySeconds">过期时间(秒),默认使用配置中的默认过期时间</param>/// <returns>是否成功</returns>Task<bool> SetAsync<T>(string key, T value, int? expirySeconds = null) where T : class;/// <summary>/// 删除键/// </summary>/// <param name="key">键</param>/// <returns>是否成功</returns>Task<bool> RemoveAsync(string key);/// <summary>/// 键是否存在/// </summary>/// <param name="key">键</param>/// <returns>是否存在</returns>Task<bool> ExistsAsync(string key);/// <summary>/// 设置过期时间/// </summary>/// <param name="key">键</param>/// <param name="expirySeconds">过期时间(秒)</param>/// <returns>是否成功</returns>Task<bool> SetExpiryAsync(string key, int expirySeconds);/// <summary>/// 批量获取/// </summary>/// <param name="keys">键集合</param>/// <returns>值字典</returns>Task<Dictionary<string, string>> GetAllStringAsync(IEnumerable<string> keys);/// <summary>/// 获取所有匹配的键/// </summary>/// <param name="pattern">匹配模式</param>/// <returns>键集合</returns>Task<IEnumerable<string>> GetKeysAsync(string pattern);/// <summary>/// 获取Hash值/// </summary>/// <param name="key">Hash键</param>/// <param name="field">字段</param>/// <returns>值</returns>Task<string?> HashGetAsync(string key, string field);/// <summary>/// 设置Hash值/// </summary>/// <param name="key">Hash键</param>/// <param name="field">字段</param>/// <param name="value">值</param>/// <returns>是否成功</returns>Task<bool> HashSetAsync(string key, string field, string value);/// <summary>/// 获取所有Hash值/// </summary>/// <param name="key">Hash键</param>/// <returns>字段值字典</returns>Task<Dictionary<string, string>> HashGetAllAsync(string key);/// <summary>/// 发布消息/// </summary>/// <param name="channel">频道</param>/// <param name="message">消息</param>/// <returns>接收到消息的客户端数量</returns>Task<long> PublishAsync(string channel, string message);/// <summary>/// 获取分布式锁/// </summary>/// <param name="key">锁键</param>/// <param name="expiry">锁过期时间</param>/// <returns>是否成功获取锁</returns>Task<bool> LockAsync(string key, TimeSpan expiry);/// <summary>/// 释放分布式锁/// </summary>/// <param name="key">锁键</param>/// <returns>是否成功释放锁</returns>Task<bool> UnlockAsync(string key);}
}
我们在前述代码中可以看到,所有方法都是异步的,这样可以提高性能,避免阻塞线程。并且我们使用了Task
作为返回类型,这样可以方便地与异步编程模型结合使用。
Tip:在.NET中,异步编程是一种重要的编程模型,它允许我们在等待某些操作完成时继续执行其他操作,从而提高应用程序的响应性和性能。使用
async
和await
关键字可以轻松实现异步编程。
1.3 Redis服务实现
我们还需要实现IRedisService
接口的实现类RedisService
。这个类将实现所有的Redis操作方法,并使用StackExchange.Redis
库与Redis进行交互,因此需要在类库中安装StackExchange.Redis
包。我们可以使用NuGet包管理器或者命令行工具安装这个包,命令如下:
dotnet add package StackExchange.Redis
安装完成后,我们就可以在RedisService
类中使用StackExchange.Redis
库了。这个类的实现不是很复杂,主要是对Redis的操作进行了封装,并添加了异常处理和日志记录。
using System.Text.Json;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StackExchange.Redis;namespace SP.Common.Redis
{/// <summary>/// Redis服务实现/// </summary>public class RedisService : IRedisService{private readonly ILogger<RedisService> _logger;private readonly RedisOptions _options;private readonly Lazy<ConnectionMultiplexer> _connectionMultiplexer;private readonly string _lockValuePrefix;/// <summary>/// 构造函数/// </summary>/// <param name="options">Redis配置选项</param>/// <param name="logger">日志器</param>public RedisService(IOptions<RedisOptions> options, ILogger<RedisService> logger){_logger = logger;_options = options.Value;_lockValuePrefix = $"lock:{Environment.MachineName}:{Guid.NewGuid()}:";_connectionMultiplexer = new Lazy<ConnectionMultiplexer>(() =>{var configOptions = ConfigurationOptions.Parse(_options.ConnectionString);configOptions.DefaultDatabase = _options.DefaultDatabase;configOptions.ConnectTimeout = _options.ConnectTimeout;configOptions.AbortOnConnectFail = false;return ConnectionMultiplexer.Connect(configOptions);});}/// <summary>/// 获取Redis连接/// </summary>private ConnectionMultiplexer Connection => _connectionMultiplexer.Value;/// <summary>/// 获取Redis数据库/// </summary>private IDatabase Database => Connection.GetDatabase();/// <summary>/// 获取字符串值/// </summary>public async Task<string?> GetStringAsync(string key){try{var value = await Database.StringGetAsync(key);return value.HasValue ? value.ToString() : null;}catch (Exception ex){_logger.LogError(ex, "Redis获取字符串值失败,Key: {Key}", key);return null;}}/// <summary>/// 设置字符串值/// </summary>public async Task<bool> SetStringAsync(string key, string value, int? expirySeconds = null){try{var expiry = TimeSpan.FromSeconds(expirySeconds ?? _options.DefaultExpireSeconds);return await Database.StringSetAsync(key, value, expiry);}catch (Exception ex){_logger.LogError(ex, "Redis设置字符串值失败,Key: {Key}", key);return false;}}/// <summary>/// 获取对象/// </summary>public async Task<T?> GetAsync<T>(string key) where T : class{try{var value = await GetStringAsync(key);if (string.IsNullOrEmpty(value)){return null;}return JsonSerializer.Deserialize<T>(value);}catch (Exception ex){_logger.LogError(ex, "Redis获取对象失败,Key: {Key}, Type: {Type}", key, typeof(T).Name);return null;}}/// <summary>/// 设置对象/// </summary>public async Task<bool> SetAsync<T>(string key, T value, int? expirySeconds = null) where T : class{if (value == null){throw new ArgumentNullException(nameof(value));}try{var json = JsonSerializer.Serialize(value);return await SetStringAsync(key, json, expirySeconds);}catch (Exception ex){_logger.LogError(ex, "Redis设置对象失败,Key: {Key}, Type: {Type}", key, typeof(T).Name);return false;}}/// <summary>/// 删除键/// </summary>public async Task<bool> RemoveAsync(string key){try{return await Database.KeyDeleteAsync(key);}catch (Exception ex){_logger.LogError(ex, "Redis删除键失败,Key: {Key}", key);return false;}}/// <summary>/// 键是否存在/// </summary>public async Task<bool> ExistsAsync(string key){try{return await Database.KeyExistsAsync(key);}catch (Exception ex){_logger.LogError(ex, "Redis检查键是否存在失败,Key: {Key}", key);return false;}}/// <summary>/// 设置过期时间/// </summary>public async Task<bool> SetExpiryAsync(string key, int expirySeconds){try{return await Database.KeyExpireAsync(key, TimeSpan.FromSeconds(expirySeconds));}catch (Exception ex){_logger.LogError(ex, "Redis设置过期时间失败,Key: {Key}", key);return false;}}/// <summary>/// 批量获取/// </summary>public async Task<Dictionary<string, string>> GetAllStringAsync(IEnumerable<string> keys){try{var keyArray = keys.ToArray();var redisKeys = keyArray.Select(k => (RedisKey)k).ToArray();var values = await Database.StringGetAsync(redisKeys);var result = new Dictionary<string, string>();for (var i = 0; i < keyArray.Length; i++){if (values[i].HasValue){result.Add(keyArray[i], values[i].ToString());}}return result;}catch (Exception ex){_logger.LogError(ex, "Redis批量获取失败");return new Dictionary<string, string>();}}/// <summary>/// 获取所有匹配的键/// </summary>public async Task<IEnumerable<string>> GetKeysAsync(string pattern){try{var keys = new List<string>();var endpoints = Connection.GetEndPoints();foreach (var endpoint in endpoints){var server = Connection.GetServer(endpoint);var serverKeys = server.Keys(pattern: pattern).Select(k => (string)k).ToList();keys.AddRange(serverKeys);}return await Task.FromResult(keys);}catch (Exception ex){_logger.LogError(ex, "Redis获取匹配键失败,Pattern: {Pattern}", pattern);return Enumerable.Empty<string>();}}/// <summary>/// 获取Hash值/// </summary>public async Task<string?> HashGetAsync(string key, string field){try{var value = await Database.HashGetAsync(key, field);return value.HasValue ? value.ToString() : null;}catch (Exception ex){_logger.LogError(ex, "Redis获取Hash值失败,Key: {Key}, Field: {Field}", key, field);return null;}}/// <summary>/// 设置Hash值/// </summary>public async Task<bool> HashSetAsync(string key, string field, string value){try{return await Database.HashSetAsync(key, field, value);}catch (Exception ex){_logger.LogError(ex, "Redis设置Hash值失败,Key: {Key}, Field: {Field}", key, field);return false;}}/// <summary>/// 获取所有Hash值/// </summary>public async Task<Dictionary<string, string>> HashGetAllAsync(string key){try{var entries = await Database.HashGetAllAsync(key);return entries.ToDictionary(entry => entry.Name.ToString(),entry => entry.Value.ToString());}catch (Exception ex){_logger.LogError(ex, "Redis获取所有Hash值失败,Key: {Key}", key);return new Dictionary<string, string>();}}/// <summary>/// 发布消息/// </summary>public async Task<long> PublishAsync(string channel, string message){try{return await Connection.GetSubscriber().PublishAsync(channel, message);}catch (Exception ex){_logger.LogError(ex, "Redis发布消息失败,Channel: {Channel}", channel);return 0;}}/// <summary>/// 获取分布式锁/// </summary>public async Task<bool> LockAsync(string key, TimeSpan expiry){try{var lockKey = $"lock:{key}";var lockValue = $"{_lockValuePrefix}{DateTime.UtcNow.Ticks}";// SET命令的NX选项确保键不存在时才设置值return await Database.StringSetAsync(lockKey, lockValue, expiry, When.NotExists);}catch (Exception ex){_logger.LogError(ex, "Redis获取分布式锁失败,Key: {Key}", key);return false;}}/// <summary>/// 释放分布式锁/// </summary>public async Task<bool> UnlockAsync(string key){try{var lockKey = $"lock:{key}";return await Database.KeyDeleteAsync(lockKey);}catch (Exception ex){_logger.LogError(ex, "Redis释放分布式锁失败,Key: {Key}", key);return false;}}}
}
在RedisService
类中,我们使用了Lazy<T>
来延迟初始化ConnectionMultiplexer
,这样可以避免在应用程序启动时就连接Redis,提高性能。并且我们使用了IOptions<T>
来获取配置选项,这样可以方便地在依赖注入系统中配置和使用Redis服务。
1.4 Redis扩展方法
最后,我们还需要创建一个扩展方法类RedisServiceExtensions
,用于将Redis服务注册到依赖注入系统中,方便我们后续在应用程序中使用。代码如下:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;namespace SP.Common.Redis
{/// <summary>/// Redis服务扩展方法/// </summary>public static class RedisServiceExtensions{/// <summary>/// 添加Redis服务/// </summary>/// <param name="services">服务集合</param>/// <param name="configuration">配置</param>/// <returns>服务集合</returns>public static IServiceCollection AddRedisService(this IServiceCollection services, IConfiguration configuration){// 从配置中获取Redis节点var redisSection = configuration.GetSection("Redis");// 注册RedisOptionsservices.Configure<RedisOptions>(options =>{// 将配置节点中的值绑定到options对象if (redisSection["ConnectionString"] != null)options.ConnectionString = redisSection["ConnectionString"];if (int.TryParse(redisSection["DefaultDatabase"], out int defaultDb))options.DefaultDatabase = defaultDb;if (int.TryParse(redisSection["ConnectionIdleTimeout"], out int idleTimeout))options.ConnectionIdleTimeout = idleTimeout;if (int.TryParse(redisSection["ConnectTimeout"], out int connectTimeout))options.ConnectTimeout = connectTimeout;if (int.TryParse(redisSection["DefaultExpireSeconds"], out int expireSeconds))options.DefaultExpireSeconds = expireSeconds;});// 注册Redis服务services.AddSingleton<IRedisService, RedisService>();return services;}/// <summary>/// 添加Redis服务/// </summary>/// <param name="services">服务集合</param>/// <param name="connectionString">连接字符串</param>/// <returns>服务集合</returns>public static IServiceCollection AddRedisService(this IServiceCollection services, string connectionString){// 注册RedisOptionsservices.Configure<RedisOptions>(options => { options.ConnectionString = connectionString; });// 注册Redis服务services.AddSingleton<IRedisService, RedisService>();return services;}}
}
在这个扩展方法中,我们使用了IConfiguration
来获取Redis的配置选项,并将其绑定到RedisOptions
对象中。然后,我们将IRedisService
注册为单例服务,这样在应用程序中就可以通过依赖注入来使用Redis服务了。
二、日志记录
在 Logger 文件夹中,我们要实现日志记录的通用代码。我们将集成 Serilog 日志框架来实现日志记录,并提供了多种日志级别的支持,包括信息、警告、错误等。我们还将创建一个扩展方法AddLoggerService
,用于将日志记录器注册到依赖注入系统中,方便我们后续在应用程序中使用。
2.1 日志服务接口
我们需要创建一个日志服务接口ILoggerService
,这个接口定义了我们需要的日志操作方法,包括信息、警告、错误等操作。代码如下:
namespace SP.Common.Logger;/// <summary>
/// 日志服务接口
/// </summary>
public interface ILoggerService
{/// <summary>/// 记录信息日志/// </summary>/// <param name="message">日志消息</param>/// <param name="args">格式化参数</param>void LogInformation(string message, params object[] args);/// <summary>/// 记录警告日志/// </summary>/// <param name="message">日志消息</param>/// <param name="args">格式化参数</param>void LogWarning(string message, params object[] args);/// <summary>/// 记录错误日志/// </summary>/// <param name="message">日志消息</param>/// <param name="args">格式化参数</param>void LogError(string message, params object[] args);/// <summary>/// 记录错误日志/// </summary>/// <param name="exception">异常</param>/// <param name="message">日志消息</param>/// <param name="args">格式化参数</param>void LogError(Exception exception, string message, params object[] args);/// <summary>/// 记录调试日志/// </summary>/// <param name="message">日志消息</param>/// <param name="args">格式化参数</param>void LogDebug(string message, params object[] args);/// <summary>/// 记录关键错误日志/// </summary>/// <param name="message">日志消息</param>/// <param name="args">格式化参数</param>void LogCritical(string message, params object[] args);/// <summary>/// 记录关键错误日志/// </summary>/// <param name="exception">异常</param>/// <param name="message">日志消息</param>/// <param name="args">格式化参数</param>void LogCritical(Exception exception, string message, params object[] args);
}
在这个接口中,我们定义了多个日志记录方法,包括信息、警告、错误、调试和关键错误等方法。每个方法都接受一个消息参数和可选的格式化参数,这样可以方便地记录不同类型的日志。
2.2 日志服务实现
接下来,我们需要实现ILoggerService
接口的实现类LoggerService
。这个类将实现所有的日志操作方法,并使用 Serilog 日志框架与日志进行交互,因此需要在类库中安装 Serilog 包。我们可以使用NuGet包管理器或者命令行工具安装这个包,命令如下:
dotnet add package Serilog
dotnet add package Serilog.AspNetCore
dotnet add package Serilog.Sinks.Grafana.Loki
安装完成后,我们就可以在LoggerService
类中使用 Serilog 日志框架了。
using Microsoft.Extensions.Logging;
using ILogger = Microsoft.Extensions.Logging.ILogger;namespace SP.Common.Logger
{/// <summary>/// 日志服务实现/// </summary>public class LoggerService : ILoggerService{private readonly ILogger _logger;/// <summary>/// 构造函数/// </summary>/// <param name="logger">日志记录器</param>public LoggerService(ILogger<LoggerService> logger){_logger = logger;}/// <summary>/// 记录信息级别的日志/// </summary>/// <param name="message"></param>/// <param name="args"></param>public void LogInformation(string message, params object[] args){_logger.LogInformation(message, args);}/// <summary>/// 记录警告级别的日志/// </summary>/// <param name="message"></param>/// <param name="args"></param>public void LogWarning(string message, params object[] args){_logger.LogWarning(message, args);}/// <summary>/// 记录错误级别的日志/// </summary>/// <param name="message"></param>/// <param name="args"></param>public void LogError(string message, params object[] args){_logger.LogError(message, args);}/// <summary>/// 记录错误级别的日志/// </summary>/// <param name="exception"></param>/// <param name="message"></param>/// <param name="args"></param>public void LogError(Exception exception, string message, params object[] args){_logger.LogError(exception, message, args);}/// <summary>/// 记录调试级别的日志/// </summary>/// <param name="message"></param>/// <param name="args"></param>public void LogDebug(string message, params object[] args){_logger.LogDebug(message, args);}/// <summary>/// 记录关键错误级别的日志/// </summary>/// <param name="message"></param>/// <param name="args"></param>public void LogCritical(string message, params object[] args){_logger.LogCritical(message, args);}/// <summary>/// 记录关键错误级别的日志/// </summary>/// <param name="exception"></param>/// <param name="message"></param>/// <param name="args"></param>public void LogCritical(Exception exception, string message, params object[] args){_logger.LogCritical(exception, message, args);}}
}
在LoggerService
类中,我们使用了 Serilog 日志框架来记录日志。我们将日志记录器注入到构造函数中,并在各个日志方法中调用相应的日志记录方法。代码很简单,这里不再详细讲解。
2.3 Loki日志配置服务
然后创建一个Loki日志配置服务LokiLoggerConfiguration
,用于配置Loki日志的相关信息。首先创建Loki日志配置类LokiOptions
,用于存储Loki日志的相关配置信息,代码如下:
namespace SP.Common.Logger
{/// <summary>/// Loki日志配置选项/// </summary>public class LokiOptions{/// <summary>/// Loki服务器地址,例如:http://loki:3100/// </summary>public string Url { get; set; } = string.Empty;/// <summary>/// 应用名称,用于标识日志来源/// </summary>public string AppName { get; set; } = "SporeAccounting";/// <summary>/// 环境名称,如development、production等/// </summary>public string Environment { get; set; } = "development";/// <summary>/// 用户名(如果Loki配置了基本认证)/// </summary>public string? Username { get; set; }/// <summary>/// 密码(如果Loki配置了基本认证)/// </summary>public string? Password { get; set; }}
}
在这个类中,我们定义了Loki日志的相关配置信息,包括Loki服务器地址、应用名称、环境名称、用户名和密码等信息。接下来,我们需要创建一个Loki日志配置服务接口ILokiLoggerConfigService
,代码如下:
namespace SP.Common.Logger;/// <summary>
/// Loki日志配置服务接口
/// </summary>
public interface ILokiLoggerConfigService
{/// <summary>/// 配置并返回Serilog日志记录器/// </summary>/// <returns>已配置的Serilog日志记录器</returns>Serilog.Core.Logger ConfigureLogger();
}
在这个接口中,我们定义了一个方法LokiLoggerConfigService
,用于配置并返回Serilog日志记录器。接下来,我们需要实现这个接口的实现类LokiLoggerConfigService
,代码如下:
using Microsoft.Extensions.Options;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.Grafana.Loki;namespace SP.Common.Logger
{/// <summary>/// Loki日志配置服务实现/// </summary>public class LokiLoggerConfigService : ILokiLoggerConfigService{private readonly LokiOptions _options;/// <summary>/// 构造函数/// </summary>/// <param name="options">Loki配置选项</param>public LokiLoggerConfigService(IOptions<LokiOptions> options){_options = options.Value;}/// <summary>/// 配置并返回Serilog日志记录器/// </summary>/// <returns>配置的Serilog日志记录器</returns>public Serilog.Core.Logger ConfigureLogger(){// 创建基本标签var labels = new List<LokiLabel>(){new LokiLabel(){Key = "app",Value = _options.AppName},new LokiLabel(){Key = "environment",Value = _options.Environment}};// 创建Loki配置var credentials = string.IsNullOrEmpty(_options.Username)? null: new LokiCredentials{Login = _options.Username,Password = _options.Password};// 配置Serilogvar configuration = new LoggerConfiguration().MinimumLevel.Debug().MinimumLevel.Override("Microsoft", LogEventLevel.Information).Enrich.FromLogContext().WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Debug,outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}").WriteTo.GrafanaLoki(uri: _options.Url,credentials: credentials,textFormatter: null,batchPostingLimit: 100,queueLimit: 10000,period: TimeSpan.FromSeconds(2),labels: labels,restrictedToMinimumLevel: LogEventLevel.Information);return configuration.CreateLogger();}}
}
在LokiLoggerConfigService
类中,我们使用了Loki日志的相关配置信息来配置Serilog日志记录器。我们创建了基本标签和Loki配置,并使用这些信息来配置Serilog日志记录器,最后返回已配置的Serilog日志记录器。其中,基本标签的作用是区分不同的日志来源,例如应用名称和环境名称等信息。Loki配置则是用于连接Loki服务器的相关信息,包括用户名和密码等信息,同时配置配置日志输出到控制台,并调用
.WriteTo.GrafanaLoki方法来将日志同时输出到Loki服务器。
2.4 日志服务扩展方法
最后创建一个扩展方法类LoggerServiceExtensions
,用于将日志服务注册到依赖注入系统中,方便我们后续在应用程序中使用。代码如下:
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Serilog;namespace SP.Common.Logger
{/// <summary>/// 日志服务扩展方法/// </summary>public static class LoggerServiceExtensions{/// <summary>/// 添加日志服务/// </summary>/// <param name="services">服务集合</param>/// <param name="configuration">配置</param>/// <returns>服务集合</returns>public static IServiceCollection AddLoggerService(this IServiceCollection services, IConfiguration configuration){// 从配置中获取Loki选项services.Configure<LokiOptions>(configuration.GetSection("Loki"));// 注册Loki日志配置服务services.AddSingleton<ILokiLoggerConfigService, LokiLoggerConfigService>();// 配置Serilog并设置为默认日志提供程序var sp = services.BuildServiceProvider();var lokiConfigService = sp.GetRequiredService<ILokiLoggerConfigService>();Log.Logger = lokiConfigService.ConfigureLogger();// 添加Serilogservices.AddLogging(loggingBuilder =>{loggingBuilder.ClearProviders();loggingBuilder.AddSerilog(dispose: true);});// 注册日志服务services.AddScoped<ILoggerService, LoggerService>();return services;}}
}
在这个扩展方法中,我们使用了IConfiguration
来获取Loki的配置选项,并将其绑定到LokiOptions
对象中。然后,我们将ILokiLoggerConfigService
注册为单例服务,并配置Serilog日志记录器。最后,我们将ILoggerService
注册为作用域服务,这样在应用程序中就可以通过依赖注入来使用日志服务了。
三、异常处理代码
在 ExceptionHandling 文件夹中,我们要实现异常处理的通用代码。我们将创建一个中间件ExceptionHandlingMiddleware
,用于捕获应用程序中的未处理异常,并记录日志。这个中间件将会在请求管道中被调用,当发生异常时,它会将异常信息记录到日志中,并返回一个友好的错误响应。异常处理涉及到的代码比较多,但大部分代码类似,因此在这一小节我们只展示关键代码,其他的代码可以参考Github上的完整代码。
3.1 异常基类
当前项目中,所有请求的异常类都继承自AppException
类,这个类是一个自定义的异常基类,包含了错误码、错误信息以及内部异常信息,代码如下:
using System.Net;namespace SP.Common.ExceptionHandling.Exceptions
{/// <summary>/// 应用程序自定义异常基类/// </summary>public class AppException : Exception{/// <summary>/// HTTP状态码/// </summary>public HttpStatusCode StatusCode { get; }/// <summary>/// 创建一个应用程序异常实例/// </summary>/// <param name="message">错误消息</param>/// <param name="statusCode">HTTP状态码</param>public AppException(string message, HttpStatusCode statusCode = HttpStatusCode.InternalServerError): base(message){StatusCode = statusCode;}/// <summary>/// 创建一个应用程序异常实例/// </summary>/// <param name="message">错误消息</param>/// <param name="innerException">内部异常</param>/// <param name="statusCode">HTTP状态码</param>public AppException(string message, Exception innerException, HttpStatusCode statusCode = HttpStatusCode.InternalServerError): base(message, innerException){StatusCode = statusCode;}}
}
在这个类中,我们定义了一个StatusCode
属性,用于存储HTTP状态码。我们还提供了两个构造函数,一个用于传入错误消息和状态码,另一个用于传入错误消息、内部异常和状态码。我们可以在应用程序中抛出这个异常类的实例,来表示应用程序中的错误,但是在项目中我们一般不会直接使用这个类,而是使用它的派生类,在当前代码中,已经定义了五个异常类,分别是BadRequestException
、ForbiddenException
、NotFoundException
、UnauthorizedException
和ValidationException
,这些异常类都继承自AppException
类,并提供了不同的HTTP状态码。
3.2 异常中间件
与前面的Redis和日志服务类似,我们还需要创建一个中间件ExceptionHandlingMiddleware
,用于捕获应用程序中的未处理异常,并记录日志。这个中间件将会在请求管道中被调用,当发生异常时,它会将异常信息记录到日志中,并返回一个友好的错误响应。代码如下:
using System.Net;
using System.Text.Json;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using SP.Common.ExceptionHandling.Exceptions;
using SP.Common.Logger;namespace SP.Common.ExceptionHandling
{/// <summary>/// 全局异常处理中间件/// </summary>public class ExceptionHandlingMiddleware{private readonly RequestDelegate _next;private readonly ILogger<ExceptionHandlingMiddleware> _logger;private readonly ILoggerService _loggerService;/// <summary>/// 构造函数/// </summary>/// <param name="next">请求委托</param>/// <param name="logger">日志记录器</param>/// <param name="loggerService">日志服务</param>public ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger,ILoggerService loggerService){_next = next;_logger = logger;_loggerService = loggerService;}/// <summary>/// 处理请求/// </summary>/// <param name="context">HTTP上下文</param>/// <returns>Task</returns>public async Task Invoke(HttpContext context){try{await _next(context);}catch (Exception ex){await HandleExceptionAsync(context, ex);}}private async Task HandleExceptionAsync(HttpContext context, Exception exception){// 记录详细异常信息到日志_logger.LogError(exception, "处理请求时发生未处理的异常: {Message}", exception.Message);// 记录到Loki日志(包含更详细的信息)LogExceptionToLoki(context, exception);// 设置响应内容类型context.Response.ContentType = "application/json";// 获取状态码和错误消息HttpStatusCode statusCode = HttpStatusCode.InternalServerError;string errorMessage = "服务器内部错误,请稍后再试";object errors = null;// 根据异常类型设置不同的状态码和错误消息if (exception is ValidationException validationException){statusCode = validationException.StatusCode;errorMessage = validationException.Message;errors = validationException.Errors;}else if (exception is AppException appException){statusCode = appException.StatusCode;errorMessage = appException.Message;}else if (exception is ArgumentException){statusCode = HttpStatusCode.BadRequest;errorMessage = exception.Message;}else if (exception is UnauthorizedAccessException){statusCode = HttpStatusCode.Unauthorized;errorMessage = "未授权访问";}// 可以根据需要添加更多的异常类型处理// 设置响应状态码context.Response.StatusCode = (int)statusCode;// 创建异常响应对象var response = new ExceptionResponse{StatusCode = statusCode,ErrorMessage = errorMessage};// 如果有验证错误,添加到响应中if (errors != null){var jsonResponse = JsonSerializer.Serialize(new{response.StatusCode,response.ErrorMessage,Errors = errors}, new JsonSerializerOptions{PropertyNamingPolicy = JsonNamingPolicy.CamelCase});await context.Response.WriteAsync(jsonResponse);return;}// 在开发环境下可以返回详细的异常信息,生产环境下只返回友好消息#if DEBUGif (!(exception is AppException)){response.ErrorMessage = exception.Message;}response.StackTrace = exception.StackTrace;#endif// 序列化响应对象var jsonResponseDefault = JsonSerializer.Serialize(response, new JsonSerializerOptions{PropertyNamingPolicy = JsonNamingPolicy.CamelCase});// 写入响应await context.Response.WriteAsync(jsonResponseDefault);}/// <summary>/// 记录异常到Loki日志/// </summary>/// <param name="context">HTTP上下文</param>/// <param name="exception">异常</param>private void LogExceptionToLoki(HttpContext context, Exception exception){try{// 收集请求信息var request = context.Request;var requestPath = request.Path;var requestMethod = request.Method;var requestQuery = request.QueryString.ToString();var requestHeaders = SerializeHeaders(request.Headers);string requestBody = "未捕获";try{// 如果请求是可重置的,尝试读取bodyif (request.Body.CanSeek){var position = request.Body.Position;request.Body.Position = 0;using var reader = new StreamReader(request.Body, leaveOpen: true);requestBody = reader.ReadToEndAsync().GetAwaiter().GetResult();request.Body.Position = position;}}catch{// 忽略读取请求体的错误}// 构建详细的日志消息var errorLogModel = new{RequestInfo = new{Url = requestPath,Method = requestMethod,QueryString = requestQuery,Headers = requestHeaders,Body = requestBody},ExceptionInfo = new{Message = exception.Message,ExceptionType = exception.GetType().FullName,StackTrace = exception.StackTrace,InnerException = exception.InnerException?.Message},User = GetUserInfo(context),Timestamp = DateTime.UtcNow};// 序列化为JSON以便于在Loki中查看var errorLogJson = JsonSerializer.Serialize(errorLogModel, new JsonSerializerOptions{WriteIndented = true,PropertyNamingPolicy = JsonNamingPolicy.CamelCase});// 记录到Loki_loggerService.LogError(exception, "处理请求发生异常: {ErrorDetails}", errorLogJson);}catch (Exception ex){// 如果记录日志本身出错,使用标准日志记录_logger.LogError(ex, "记录异常到Loki时发生错误");}}/// <summary>/// 序列化请求头/// </summary>/// <param name="headers">请求头集合</param>/// <returns>序列化后的请求头</returns>private Dictionary<string, string> SerializeHeaders(IHeaderDictionary headers){var result = new Dictionary<string, string>();foreach (var header in headers){// 排除敏感信息,如Authorization、Cookie等if (!header.Key.Equals("Authorization", StringComparison.OrdinalIgnoreCase) &&!header.Key.Equals("Cookie", StringComparison.OrdinalIgnoreCase)){result[header.Key] = header.Value.ToString();}}return result;}/// <summary>/// 获取用户信息/// </summary>/// <param name="context">HTTP上下文</param>/// <returns>用户信息</returns>private object GetUserInfo(HttpContext context){try{var userId = context.User?.Identity?.Name;var isAuthenticated = context.User?.Identity?.IsAuthenticated ?? false;return new{UserId = userId,IsAuthenticated = isAuthenticated,IpAddress = context.Connection.RemoteIpAddress?.ToString()};}catch{return new { IpAddress = context.Connection.RemoteIpAddress?.ToString() };}}}
}
在ExceptionHandlingMiddleware
类中,我们实现了一个Invoke
方法,用于处理请求。在这个方法中,我们调用下一个中间件,并捕获任何未处理的异常。如果发生异常,我们调用HandleExceptionAsync
方法来处理异常。在这个方法中,我们记录异常信息到日志,并返回一个友好的错误响应。我们还提供了一个LogExceptionToLoki
方法,用于将异常信息记录到Loki日志中。
3.3 异常处理扩展方法
最后创建一个扩展方法类ExceptionHandlingMiddlewareExtensions
,用于将异常处理中间件注册到请求管道中。代码如下:
using Microsoft.AspNetCore.Builder;namespace SP.Common.ExceptionHandling
{/// <summary>/// 异常处理中间件扩展/// </summary>public static class ExceptionHandlingMiddlewareExtensions{/// <summary>/// 使用全局异常处理中间件/// </summary>/// <param name="builder">应用程序构建器</param>/// <returns>应用程序构建器</returns>public static IApplicationBuilder UseExceptionHandling(this IApplicationBuilder builder){return builder.UseMiddleware<ExceptionHandlingMiddleware>();}/// <summary>/// 启用请求缓冲,使请求体可以被多次读取/// </summary>/// <param name="builder">应用程序构建器</param>/// <returns>应用程序构建器</returns>public static IApplicationBuilder UseRequestBuffering(this IApplicationBuilder builder){return builder.UseMiddleware<EnableRequestBufferingMiddleware>();}/// <summary>/// 使用全局异常处理(包括请求缓冲)/// </summary>/// <param name="builder">应用程序构建器</param>/// <returns>应用程序构建器</returns>public static IApplicationBuilder UseFullExceptionHandling(this IApplicationBuilder builder){return builder.UseRequestBuffering().UseExceptionHandling();}}
}
在这个扩展方法中,我们提供了一个UseExceptionHandling
方法,用于将异常处理中间件注册到请求管道中。我们还提供了一个UseRequestBuffering
方法,用于启用请求缓冲,使请求体可以被多次读取。最后,我们提供了一个UseFullExceptionHandling
方法,用于同时注册这两个中间件。
3.4 启用请求缓冲中间件
在前面代码中,我们看到了EnableRequestBufferingMiddleware
中间件,这个中间件的作用是启用请求缓冲,使请求体可以被多次读取。启用请求缓冲中间件的主要原因是在ASP.NET Core中,请求体(Request Body)默认是一个单向流,只能被读取一次。如果在管道的某个中间件中读取了请求体,那么后续的中间件将无法再次读取相同的内容。其次当发生异常时,我们希望在异常处理中间件中能够记录完整的请求信息,包括请求体内容。如果没有启用请求缓冲,当异常发生时,请求体可能已经被之前的中间件或控制器读取过,导致异常处理中间件无法获取请求体内容。对于复杂错误的调试,完整的请求上下文信息非常重要。通过启用请求缓冲,我们可以确保在Loki日志中记录完整的请求信息,包括请求体数据,这对于排查问题特别有价值。并且在某些场景下,应用可能需要多次读取请求体数据,例如先进行请求验证,然后进行请求处理,最后可能还需要记录请求日志。启用请求缓冲可以满足这些多次读取的需求。
具体实现是EnableRequestBufferingMiddleware
通过调用context.Request.EnableBuffering()
方法,将请求体内容加载到内存中并允许多次读取。这样,在请求处理过程中的任何地方,包括异常处理中间件,都可以读取到完整的请求体内容。
Tip:启用请求缓冲会占用额外的内存资源,特别是对于大型请求体。因此,在配置请求缓冲时,可能需要考虑设置缓冲大小限制,以防止潜在的内存问题。
using Microsoft.AspNetCore.Http;namespace SP.Common
{/// <summary>/// 启用请求缓冲中间件,使请求体可以被多次读取/// </summary>public class EnableRequestBufferingMiddleware{private readonly RequestDelegate _next;/// <summary>/// 构造函数/// </summary>/// <param name="next">请求委托</param>public EnableRequestBufferingMiddleware(RequestDelegate next){_next = next;}/// <summary>/// 处理请求/// </summary>/// <param name="context">HTTP上下文</param>/// <returns>Task</returns>public async Task Invoke(HttpContext context){// 启用请求缓冲,使请求体可以被多次读取context.Request.EnableBuffering();await _next(context);}}
}
在EnableRequestBufferingMiddleware
类中,我们实现了一个Invoke
方法,用于处理请求。在这个方法中,我们调用context.Request.EnableBuffering()
方法来启用请求缓冲,然后调用下一个中间件。
四、总结
到这里,我们已经完成了Redis、日志记录和异常处理的通用代码实现。我们创建了Redis服务、日志服务和异常处理中间件,并提供了相应的扩展方法来注册这些服务到依赖注入系统中。通过这些通用代码,我们可以在应用程序中方便地使用Redis、日志记录和异常处理功能,提高了代码的可维护性和可读性。在实际应用中,我们可以根据需要扩展这些服务的功能,例如添加更多的日志级别、支持不同的日志输出格式、支持更多的异常类型等。同时,我们还可以根据项目的需求,进一步优化这些服务的性能和可靠性。
相关文章:
22.【.NET8 实战--孢子记账--从单体到微服务--转向微服务】--单体转微服务--增加公共代码
在拆分服务之前,我们需要先提取一些公共代码。本篇将重点新增日志记录、异常处理以及Redis的通用代码。这些组件将被整合到一个共享类库中,便于在微服务架构中高效复用。 Tip:在后续的教程中我们会穿插多篇提取公共代码的文章,帮助…...
EasyOps®5月热力焕新:三大核心模块重构效能边界
在应用系统管理中,我们将管理对象从「服务实例」优化为「部署实例」,这一改变旨在提升管理效率与数据展示清晰度。 此前,系统以 “IP Port” 组合定义服务实例。当同一 IP 下启用多个进程或端口时,会产生多个服务实例。比如一台…...
基于深度学习的工业OCR数字识别系统架构解析
一、项目场景 春晖数字识别视觉检测系统专注于工业自动化生产监控、设备运行数据记录等关键领域。系统通过高精度OCR算法,能够实时识别设备上显示的关键数据(如温度、压力、计数等),并定时存储至Excel文件中。这些数据对于生产过…...
R语言绘图 | 渐变火山图
客户要求绘制类似文章中的这种颜色渐变火山图,感觉挺好看的。网上找了一圈,发现有别人已经实现的类似代码,拿来修改后即可使用,这里做下记录,以便后期查找。 简单实现 library(tidyverse)library(ggrepel)library(ggf…...
Go语言——docker-compose部署etcd以及go使用其服务注册
一、docker-compsoe.yml文件如下 version: "3.5"services:etcd:hostname: etcdimage: bitnami/etcd:latestdeploy:replicas: 1restart_policy:condition: on-failureprivileged: truevolumes:# 持久化 etcd 数据到宿主机- "/app/apisix/etcd/data:/bitnami/etc…...
Tomcat的调优
目录 一. JVM 1.1 JVM的组成 1.2 运行时数据区域的组成 二. 垃圾回收 2.1 如何确认垃圾 1. 引用计数法 2. 根搜索算法 2.2 垃圾回收基本算法 1. 标记-清除算法(Mark-Sweep) 2. 标记-压缩算法(Mark-Compact) 3. 复制算法…...
Tomcat和Nginx的主要区别
1、功能定位 Nginx:核心是高并发HTTP服务器和反向代理服务器,擅长处理静态资源(如HTML、图片)和负载均衡。Tomcat:是Java应用服务器,主要用于运行动态内容(如JSP、Servlet)…...
Python训练营打卡——DAY24(2025.5.13)
目录 一、元组 1. 通俗解释 2. 元组的特点 3. 元组的创建 4. 元组的常见用法 二、可迭代对象 1. 定义 2. 示例 3. 通俗解释 三、OS 模块 1. 通俗解释 2. 目录树 四、作业 1. 准备工作 2. 实战代码示例 3. 重要概念解析 一、元组 是什么:一种…...
【TDengine源码阅读】DLL_EXPORT
2025年5月13日,周二清晨 #ifdef WINDOWS #define DLL_EXPORT __declspec(dllexport) #else #define DLL_EXPORT #endif为啥Linux和MacOS平台时宏为空,难道Linux和mac不用定义导出函数吗? 这段代码是一个跨平台的宏定义,用于处理不…...
电子科技浪潮下的华秋电子:慕尼黑上海电子展精彩回顾
为期3天的2025慕尼黑上海电子展(electronica China 2025)于17日在上海新国际博览中心落下帷幕。 展会那规模,真不是吹的!本届展会汇聚了1,794家国内外行业知名品牌企业的展商来 “摆摊”,展览面积大得像个超级大迷宫&…...
TDengine编译成功后的bin目录下的文件的作用
2025年5月13日,周二清晨 以下是TDengine工具集中各工具的功能说明: 核心工具 taosd • TDengine的核心服务进程,负责数据存储、查询和集群管理。 taos • 命令行客户端工具,用于连接TDengine服务器并执行SQL操作。 taosBenchma…...
spark sql基本操作
Spark SQL 是 Apache Spark 的一个模块,用于处理结构化数据。它允许用户使用标准的 SQL 语法来查询数据,并且可以无缝地与 Spark 的其他功能(如 DataFrame、Dataset 和 RDD)结合使用。以下是 Spark SQL 的基本使用方法和一些常见操…...
采购流程规范化如何实现?日事清流程自动化助力需求、采购、财务高效协作
采购审批流程全靠人推进,内耗严重,效率低下? 花重金上了OA,结果功能有局限、不灵活? 问题出在哪里?是我们的要求太多、太苛刻吗?NO! 流程名称: 采购审批管理 流程功能…...
影刀RPA开发-CSS选择器介绍
影刀RPA网页自动化开发,很多时候需要我们查看页面源码,查找相关的元素属性,这就需要我们有必要了解CSS选择器。本文做了些简单的介绍。希望对大家有帮助! 1. CSS选择器概述 1.1 定义与作用 CSS选择器是CSS(层叠样式…...
DeepSeek、B(不是百度)AT、科大讯飞靠什么坐上中国Ai牌桌?
在国产AI舞台上,DeepSeek、阿里、字节、腾讯、讯飞群雄逐鹿,好不热闹。 这场堪称“军备竞赛”的激烈角逐,绝非简单的市场竞争,而是一场关乎技术、创新与未来布局的深度博弈。在竞赛中,五大模型各显神通,以…...
MySQL全局优化
目录 1 硬件层面优化 1.1 CPU优化 1.2 内存优化 1.3 存储优化 1.4 网络优化 2 系统配置优化 2.1 操作系统配置 2.2 MySQL服务配置 3 库表结构优化 4 SQL及索引优化 mysql可以从四个层面考虑优化,分别是 硬件系统配置库表结构SQL及索引 从成本和优化效果来看…...
【github】主页显示star和fork
数据收集:定期(例如每天)获取你所有仓库的 Star 和 Fork 总数。数据存储:将收集到的数据(时间戳、总 Star 数、总 Fork 数)存储起来。图表生成:根据存储的数据生成变化曲线图(通常是…...
网站遭受扫描攻击,大量爬虫应对策略
网站的日志里突然有很多访问路径不存在的,有些ip地址也是国外的,而且访问是在深夜且次数非常频繁紧密。判定就是不怀好意的扫描网站寻找漏洞。也有些是爬虫,且是国外的爬虫,有的也是不知道的爬虫爬取网站。网站的真实流量不多&…...
【 Redis | 实战篇 秒杀实现 】
目录 前言: 1.全局ID生成器 2.秒杀优惠券 2.1.秒杀优惠券的基本实现 2.2.超卖问题 2.3.解决超卖问题的方案 2.4.基于乐观锁来解决超卖问题 3.秒杀一人一单 3.1.秒杀一人一单的基本实现 3.2.单机模式下的线程安全问题 3.3.集群模式下的线程安全问题 前言&…...
手搓传染病模型(SEIARW)
在传染病传播的研究中,水传播途径是一个重要的考量因素。SEAIRW 模型(易感者 S - 暴露者 E - 感染者 I - 无症状感染者 A - 康复者 R - 水中病原体 W)综合考虑了人与人接触传播以及水传播的双重机制,为分析此类传染病提供了全面的…...
【C++】深入理解 unordered 容器、布隆过滤器与分布式一致性哈希
【C】深入理解 unordered 容器、布隆过滤器与分布式一致性哈希 在日常开发中,无论是数据结构优化、缓存设计,还是分布式架构搭建,unordered_map、布隆过滤器和一致性哈希都是绕不开的关键工具。它们高效、轻量,在性能与扩展性方面…...
第五天——贪心算法——射气球
1.题目 有一些球形气球贴在一个表示 XY 平面的平坦墙壁上。气球用一个二维整数数组 points 表示,其中 points[i] [xstart, xend] 表示第 i 个气球的水平直径范围从 xstart 到 xend。你并不知道这些气球的具体 y 坐标。 可以从 x 轴上的不同位置垂直向上࿰…...
麦肯锡110页PPT企业组织效能提升调研与诊断分析指南
“战略清晰、团队拼命、资源充足,但业绩就是卡在瓶颈期上不去……”这是许多中国企业面临的真实困境。表面看似健康的企业,往往隐藏着“组织亚健康”问题——跨部门扯皮、人才流失、决策迟缓、市场反应滞后……麦肯锡最新研究揭示:组织健康度…...
BFS算法篇——从晨曦到星辰,BFS算法在多源最短路径问题中的诗意航行(上)
文章目录 引言一、多源BFS的概述二、应用场景三、算法步骤四、代码实现五、代码解释六、总结 引言 在浩渺的图论宇宙中,图的每一条边、每一个节点都是故事的组成部分。每当我们站在一个复杂的迷宫前,开始感受它的深邃时,我们往往不再局限于从…...
理解 C# 中的各类指针
前言 变量可以理解成是一块内存位置的别名,访问变量也就是访问对应内存中的数据。 指针是一种特殊的变量,它存储了一个内存地址,这个内存地址代表了另一块内存的位置。 指针指向的可以是一个变量、一个数组元素、一个对象实例、一块非托管内存…...
MySQL 事务(二)
文章目录 事务隔离性理论理解隔离性隔离级别 事务隔离级别的设置和查看事务隔离级别读未提交读提交(不可重复读) 事务隔离性理论 理解隔离性 MySQL服务可能会同时被多个客户端进程(线程)访问,访问的方式以事务方式进行一个事务可能由多条SQL…...
【HarmonyOS】ArkTS开发应用的横竖屏切换
文章目录 1、简介2、静态 — 横竖屏切换2.1、效果2.2、实现原理2.3、module.json5 源码 3、动态 — 横竖屏切换3.1、应用随系统旋转切换横竖屏3.2、setPreferredOrientation 原理配置3.3、锁定旋转的情况下,手动设置横屏状态 1、简介 在完成全屏网页嵌套应用开发后…...
Linux中find命令用法核心要点提炼
大家好,欢迎来到程序视点!我是你们的老朋友.小二! 以下是针对Linux中find命令用法的核心要点提炼: 基础语法结构 find [路径] [选项] [操作]路径:查找目录(.表当前目录,/表根目录)…...
专栏项目框架介绍
项目整体实现框图 如下图所示,是该项目的整体框图,项目的功能概括为:PC端下发数据文件,FPGA板卡接收数据文件,缓存至DDR中,待数据文件发送完毕,循环读取DDR有效写区域数据,将DDR数据…...
WSL 安装 Debian 12 后,Linux 如何安装 vim ?
在 WSL 的 Debian 12 中安装 Vim 非常简单,只需使用 apt 包管理器即可。以下是详细步骤: 1. 更新软件包列表 首先打开终端,确保系统包列表是最新的: sudo apt update2. 安装 Vim 直接通过 apt 安装 Vim: sudo apt …...
【SpringBoot】从零开始全面解析Spring MVC (一)
本篇博客给大家带来的是SpringBoot的知识点, 本篇是SpringBoot入门, 介绍Spring MVC相关知识. 🐎文章专栏: JavaEE初阶 🚀若有问题 评论区见 ❤ 欢迎大家点赞 评论 收藏 分享 如果你不知道分享给谁,那就分享给薯条. 你们的支持是我不断创作的动力 . 王子…...
C++—特殊类设计设计模式
目录 C—特殊类设计&设计模式1.设计模式2.特殊类设计2.1设计一个无法被拷贝的类2.2设计一个只能在堆上创建对象的类2.3设计一个只能在栈上创建对象的类2.4设计一个类,无法被继承2.5设计一个类。这个类只能创建一个对象【单例模式】2.5.1懒汉模式实现2.5.2饿汉模…...
初入OpenCV
OpenCV简介 OpenCV是一个开源的跨平台计算机视觉库,它实现了图像处理和计算机视觉方面的很多通用算法。 应用场景: 目标识别:人脸、车辆、车牌、动物; 自动驾驶;医学影像分析; 视频内容理解分析ÿ…...
霍夫圆变换全面解析(OpenCV)
文章目录 一、霍夫圆变换基础1.1 霍夫圆变换概述1.2 圆的数学表达与参数化 二、霍夫圆变换算法实现2.1 标准霍夫圆变换算法流程2.2 参数空间的表示与优化 三、关键参数解析3.1 OpenCV中的HoughCircles参数3.2 参数调优策略 四、Python与OpenCV实现参考4.1 基本实现代码4.2 改进…...
互联网大厂Java求职面试:优惠券服务架构设计与AI增强实践-4
互联网大厂Java求职面试:优惠券服务架构设计与AI增强实践-4 场景设定 面试官:某互联网大厂技术总监,拥有超过10年大型互联网企业一线技术管理经验,擅长分布式架构、微服务治理、云原生等领域。 候选人:郑薪苦&#…...
项目中会出现的css样式
1.重复渐变边框 思路: 主要是用重复的背景渐变实现的 如图: <div class"card"><div class"container">全面收集中医癌毒临床医案,建立医案共享机制,构建癌毒病机知识图谱,便于医疗人…...
LeetCode[101]对称二叉树
思路: 对称二叉树是左右子树对称,而不是左右子树相等,所以假设一个树只有3个节点,那么判断这个数是否是对称二叉树,肯定是先判断左右两个树,然后再看根节点,这样递归顺序我们就确认了࿰…...
黑马k8s(四)
1.资源管理介绍 本章节主要介绍yaml语法和kubernetes的资源管理方式 2.YAML语言介绍 3.资源管理方式 命令式对象管理 dev下删除了pod,之后发现还有pod,把原来的pod删除了,重新启动了一个 命令式对象配置 声明式对象配置 命令式对象配置&…...
华为ensp实现跨vlan通信
要在网络拓扑中实现主机192.168.1.1、192.168.1.2和192.168.2.1之间的互相通信,需要正确配置交换机(S5700)和路由器(AR3260),以确保不同网段之间的通信(即VLAN间路由)。 网络拓扑分析…...
TCPIP详解 卷1协议 十 用户数据报协议和IP分片
10.1——用户数据报协议和 IP 分片 UDP是一种保留消息边界的简单的面向数据报的传输层协议。它不提供差错纠正、队列管理、重复消除、流量控制和拥塞控制。它提供差错检测,包含我们在传输层中碰到的第一个真实的端到端(end-to-end)校验和。这…...
Java笔记4
第一章 static关键字 2.1 概述 以前我们定义过如下类: public class Student {// 成员变量public String name;public char sex; // 男 女public int age;// 无参数构造方法public Student() {}// 有参数构造方法public Student(String a) {} }我们已经知道面向…...
Matlab 垂向七自由度轨道车辆开关型半主动控制
1、内容简介 Matlab 229-垂向七自由度轨道车辆开关型半主动控制 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略...
Matlab 短时交通流预测AR模型
1、内容简介 Matlab 230-短时交通流预测AR模型 可以交流、咨询、答疑 2、内容说明 略 3、仿真分析 略 4、参考论文 略城市道路短时交通流预测.pdf...
MYSQL之表的约束
表中真正约束字段的是数据类型, 但是只有数据类型约束就很单一, 也需要有一些额外的约束, 从而更好的保证数据的合法性, 从业务逻辑角度保证数据的正确性. 比如有一个字段是email, 要求是唯一的. 为什么要有表的约束? 表的约束: 表中一定要有各种约束, 通过约束, 让我们未来…...
使用ACE-Step在本地生成AI音乐
使用ACE-Step v1-3.5B开源模型从文本提示、标签和歌词创建完整的AI生成歌曲 — 无需云服务,无需API,仅需您的GPU。 这是由ACE Studio和StepFun开发的开源音乐生成模型。 在对数据隐私和云服务依赖性日益增长的担忧时代,ACE-Step将强大的文本转音乐生成完全离线,使其成为A…...
web 自动化之 Unittest 四大组件
文章目录 一、如何开展自动化测试1、项目需求分析,了解业务需求 web 功能纳入自动化测试2、选择何种方式实现自动化测试 二、Unittest 框架三、TestCase 测试用例四、TestFixture 测试夹具 执行测试用例前的前置操作及后置操作五、TestSuite 测试套件 & TestLoa…...
2025年渗透测试面试题总结-渗透测试红队面试七(题目+回答)
网络安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 渗透测试红队面试七 一百八十一、Shiro漏洞类型,721原理,721利用要注意什么&am…...
Mysql的索引,慢查询和数据库表的设计以及乐观锁和悲观锁
设计高性能数据表的原则 数据库设计经验和技巧 单张数据表的字段不宜过多(20个),如果确实存在大量field,考虑拆成多张表或json text存储 数据表字段都是not null的,即使没有数据,最好也使用无意义的值填充,…...
day012-软件包管理专题
文章目录 1. 生成随机密码2. 软件包管理2.1 类红帽系统2.1.1 安装软件包2.1.2 查找软件包2.1.3 查看软件包内容2.1.4 查看命令或文件属于哪个软件包2.1.5 重新安装软件包2.1.6 删除软件包2.1.7 升级2.1.8 rpm安装软件包2.1.9 rpm升级软件包2.1.10 rpm检查软件包文件是否改变 3.…...
学习黑客5 分钟深入浅出理解Windows Firewall
5 分钟深入浅出理解Windows Firewall 🔥 大家好!今天我们将探索Windows防火墙——这是Windows操作系统中的核心安全组件,负责控制进出计算机的网络流量。无论你是计算机初学者,还是在TryHackMe等平台上学习网络安全的爱好者&…...