Web框架 --- 依赖注入实例的生命周期

2023-12-28 12:11:54

什么是生命周期

  • 通过依赖注入的实例都有一个生命周期, 主要有三种生命周期

Singleton

  • 整个程序运行过程中, 只产生一个实例, 应用程序结束后会被销毁
  • controller默认为singleton, 大部分情况下service和repository的实例也都为单例

当使用 Singleton 生命周期时,一个典型的例子是在一个应用程序中管理全局状态或共享资源,确保整个应用程序只有一个实例。

// 全局状态管理服务接口
public interface IGlobalStateService
{
    int GetTotalUsers();
    void IncrementUserCount();
}

// 具体的全局状态管理服务实现
public class GlobalStateService : IGlobalStateService
{
    private int totalUsers;

    public GlobalStateService()
    {
        totalUsers = 0;
    }

    public int GetTotalUsers()
    {
        return totalUsers;
    }

    public void IncrementUserCount()
    {
        totalUsers++;
    }
}

// 在 Startup.cs 中进行注册
public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IGlobalStateService, GlobalStateService>();
}

在这个示例中,GlobalStateService 被注册为 IGlobalStateService 接口的实现,并且使用 Singleton 生命周期。这意味着在整个应用程序生命周期内,只有一个 GlobalStateService 实例存在,即使多个组件或服务需要访问全局状态,它们都会共享同一个实例。

这种生命周期适用于那些需要在整个应用程序中共享状态或资源的服务,如全局配置、计数器、全局缓存等。

Scoped in C# or Request in Java

  • 在同一次请求中只产生一个实例, 请求结束后实例会被销毁. 如果在不同的请求中,将会创建一个新的实例

当使用 ASP.NET Core 中的 Scoped 生命周期时,一个典型的例子是在 Web 应用程序中管理用户的登录状态。假设有一个用于管理用户身份验证和授权的服务,我们将其注册为 Scoped 生命周期,以便在每个 HTTP 请求期间维护相同的实例状态。

// 用户管理服务接口
public interface IUserManagementService
{
    bool IsUserLoggedIn();
    void Login(string username);
    void Logout();
}

// 具体的用户管理服务实现
public class UserManagementService : IUserManagementService
{
    private bool isLoggedIn;
    private string loggedInUsername;

    public UserManagementService()
    {
        isLoggedIn = false;
        loggedInUsername = null;
    }

    public bool IsUserLoggedIn()
    {
        return isLoggedIn;
    }

    public void Login(string username)
    {
        isLoggedIn = true;
        loggedInUsername = username;
        // 实现登录逻辑
    }

    public void Logout()
    {
        isLoggedIn = false;
        loggedInUsername = null;
        // 实现登出逻辑
    }
}

// 在 Startup.cs 中进行注册
public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IUserManagementService, UserManagementService>();
}

在这个例子中,UserManagementService实现了 IUserManagementService 接口,并且通过 Scoped 生命周期注册到依赖注入容器中。在一个 HTTP 请求过程中,每次需要用户管理服务时,都会返回同一个实例。

使用 Scoped 生命周期对于在同一 HTTP 请求期间共享用户状态或身份验证信息非常有用。例如,在用户登录过程中,可以在整个请求期间持续维护用户登录状态,并在请求结束时使其失效。

Transient in C# or Prototype in Java

  • 每次使用都会创建一个新的实例, 使用结束后会被销毁

一个典型的例子是创建一个服务来生成随机数。

// 生成随机数服务接口
public interface IRandomNumberService
{
    int GetRandomNumber();
}

// 具体的随机数服务实现
public class RandomNumberService : IRandomNumberService
{
    private readonly Random random;

    public RandomNumberService()
    {
        random = new Random();
    }

    public int GetRandomNumber()
    {
        return random.Next();
    }
}

// 在 Startup.cs 中进行注册
public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IRandomNumberService, RandomNumberService>();
}

在这个示例中,RandomNumberService 被注册为 IRandomNumberService 接口的实现,并且使用 Transient 生命周期。每次通过依赖注入容器请求 IRandomNumberService 时,都会创建一个新的 RandomNumberService 实例,从而每次调用 GetRandomNumber() 方法都会返回一个新的随机数。

这种生命周期适合那些不需要共享状态或数据,而只是需要在每次使用时都获取新实例的服务。例如,生成随机数、执行临时任务等场景都可以使用 Transient 生命周期。

session only in Java

  • 首次http请求创建一个实例,作用域是浏览器首次访问直至浏览器关闭。
    同一个HTTP Session共享一个Bean,不同Session使用不通的Bean,仅适用于WebApplicationContext环境。

使用不同生命周期 — Example

ExampleTransientService class

using System;
using DependencyInjectionAndServiceLifetimes.Interfaces;

namespace DependencyInjectionAndServiceLifetimes.Services
{
    public class ExampleTransientService : IExampleTransientService
    {
        private readonly Guid Id;

        public ExampleTransientService()
        {
            Id = Guid.NewGuid();
        }

        public string GetGuid() => Id.ToString();
    }
}

ExampleScopedService class

using System;
using DependencyInjectionAndServiceLifetimes.Interfaces;

namespace DependencyInjectionAndServiceLifetimes.Services
{
    public class ExampleScopedService : IExampleScopedService
    {
        private readonly Guid Id;

        public ExampleScopedService()
        {
            Id = Guid.NewGuid();
        }

        public string GetGuid() => Id.ToString();
    }
}

ExampleSingletonService class

using System;
using DependencyInjectionAndServiceLifetimes.Interfaces;

namespace DependencyInjectionAndServiceLifetimes.Services
{
    public class ExampleSingletonService : IExampleSingletonService
    {
        private readonly Guid Id;

        public ExampleSingletonService()
        {
            Id = Guid.NewGuid();
        }

        public string GetGuid() => Id.ToString();
    }
}

config Service

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

    services.AddTransient<IExampleTransientService, ExampleTransientService>();
    services.AddScoped<IExampleScopedService, ExampleScopedService>();
    services.AddSingleton<IExampleSingletonService, ExampleSingletonService>();

    services.AddControllersWithViews();
}

Inject Service

using Microsoft.AspNetCore.Mvc;
using System.Text;
using DependencyInjectionAndServiceLifetimes.Interfaces;

namespace DependencyInjectionAndServiceLifetimes.Controllers
{
    public class HomeController : Controller
    {
        private readonly IExampleTransientService _exampleTransientService1;
        private readonly IExampleTransientService _exampleTransientService2;

        private readonly IExampleScopedService _exampleScopedService1;
        private readonly IExampleScopedService _exampleScopedService2;

        private readonly IExampleSingletonService _exampleSingletonService1;
        private readonly IExampleSingletonService _exampleSingletonService2;

        public HomeController(IExampleTransientService exampleTransientService1,
            IExampleTransientService exampleTransientService2,
            IExampleScopedService exampleScopedService1,
            IExampleScopedService exampleScopedService2,
            IExampleSingletonService exampleSingletonService1,
            IExampleSingletonService exampleSingletonService2)
        {
            _exampleTransientService1 = exampleTransientService1;
            _exampleTransientService2 = exampleTransientService2;

            _exampleScopedService1 = exampleScopedService1;
            _exampleScopedService2 = exampleScopedService2;

            _exampleSingletonService1 = exampleSingletonService1;
            _exampleSingletonService2 = exampleSingletonService2;
        }

        public IActionResult Index()
        {
            var exampleTransientServiceGuid1 = _exampleTransientService1.GetGuid();
            var exampleTransientServiceGuid2 = _exampleTransientService2.GetGuid();

            var exampleScopedServiceGuid1 = _exampleScopedService1.GetGuid();
            var exampleScopedServiceGuid2 = _exampleScopedService2.GetGuid();

            var exampleSingletonServiceGuid1 = _exampleSingletonService1.GetGuid();
            var exampleSingletonServiceGuid2 = _exampleSingletonService2.GetGuid();

            StringBuilder messages = new StringBuilder();
            messages.Append($"Transient 1: {exampleTransientServiceGuid1}\n");
            messages.Append($"Transient 2: {exampleTransientServiceGuid2}\n\n");

            messages.Append($"Scoped 1: {exampleScopedServiceGuid1}\n");
            messages.Append($"Scoped 2: {exampleScopedServiceGuid2}\n\n");

            messages.Append($"Singleton 1: {exampleSingletonServiceGuid1}\n");
            messages.Append($"Singleton 2: {exampleSingletonServiceGuid2}");

            return Ok(messages.ToString());
        }
    }
}
  • This is the result on the page:
    在这里插入图片描述
  • Transient 因为每次使用都会产生新的实例, 所以reference不一样
  • Scoped因为同一个请求内是一个实例, 所以reference一样
  • Singleton因为只有一个实例, 所以reference一样
  • If we refresh the page, this is the new result:
    在这里插入图片描述
  • Transient 因为每次使用都会产生新的实例, 所以reference不一样
  • Scoped因为发出了新的请求, 所以reference和之前的不一样
  • Singleton因为只有一个实例, 所以reference一样

生命周期和并发安全

  • 在Web开发中, 需要注意多请求情况下的并发安全问题

Singleton的并发问题

  • 采用单例模式的最大好处,就是可以在高并发场景下极大地节省内存资源,提高服务抗压能力。
  • 单例模式容易出现的问题是:在单例中定义的实例变量,在多个请求并发时会出现竞争访问,所以单例中的实例变量不是线程安全的, 所以尽量不在单例中使用实例变量
  • 比如Controller不是线程安全的. 正因为Controller默认是单例,所以不是线程安全的。如果用SpringMVC 的 Controller时,尽量不在 Controller中使用实例变量,否则会出现线程不安全性的情况,导致数据逻辑混乱。

举一个简单的例子,在一个Controller中定义一个非静态成员变量 num 。通过Controller成员方法来对 num 增加

@Controller
public class TestController {
    private int num = 0;

    @RequestMapping("/addNum")
    public void addNum() {
        System.out.println(++num);
    }
}

在本地运行后:
首先访问 http:// localhost:8080 / addNum,得到的答案是1;
再次访问 http:// localhost:8080 / addNum,得到的答案是2, 但是期望返回的还是1

单例中的静态成员变量

  • 如果在单例中设置了静态成员变量, 而代码中又根据请求对静态成员变量进行了赋值, 则在并发请求的情况下会导致混乱
@Controller
public class TestController {
    private static int num = 0;
    
    @RequestMapping("/addNum")
    public void addNum(Request request) {
       //在并发请求的情况下, num会被覆盖重写
        num = request.header.studentId
    }
}


文章来源:https://blog.csdn.net/weixin_38803409/article/details/135258150
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。