2024年9月

OAuth2.0授权验证-gitee授权码模式

本文主要介绍如何笔者自己是如何使用gitee提供的OAuth2.0协议完成授权验证并登录到自己的系统,完整模式如图
image

1、创建应用

打开gitee个人中心->第三方应用->创建应用

创建应用后在我的应用界面,查看已创建应用的Client ID和Client Secret

2、对接

如何对接呢?gitee提供了开放文档,打开任意一个gitee页面,到页脚部分,找到
OpenAPI
,这里就是gitee提供的开放文档了,下面开始根据文档进行对接

  • 1、回调地址

根据协议,我们必须设置一个回调地址,来接收认证中心反馈给我们的信息,由于我是使用了vue+.net前后端分离的架构。所以需要再vue项目中创建一个页面,配置路由,并放开访问权限,这个页面的主要作用是为了让用户感知授权过程、授权成功后,回调页面会接收到state和code并转发给callback接口,页面显示授权成功,并随后关闭页面

{
    path: "/oauth-callback",
    name: "Callback",
    component: () => import("@/views/login/Callback.vue"),
    meta: {
      title: "认证中心回调页面",
      showLink: false,
      rank: 101
    }
  }
  • 2、callback接口

接收由步骤1传递过来的参数,并根据规则解析state,根据code参数,调用官方提供的api获取token,当拿到token后,就可以调用其他api了,也可以对token进行保存等其他操作。

var giteeTokenResult = await "https://gitee.com/oauth/token"
    .SetQueryParams(new { grant_type = "authorization_code", code = input.Code, client_id = authorizationCenter.ClientId, redirect_uri = authorizationCenter.RedirectUri, client_secret = authorizationCenter.ClientSecret })
    .PostAsync()
    .ReceiveJson<GiteeTokenResult>();

我们这里调用了获取用户信息的接口,根据反馈的用户信息,去后面的逻辑

oAuth2UserInfo = await "https://gitee.com/api/v5/user"
    .WithHeader("User-Agent", "purest-admin")
    .SetQueryParams(new { access_token = giteeTokenResult.AccessToken })
    .GetJsonAsync<OAuth2UserInfo>();
oAuth2UserInfo.Type = OAuth2TypeConst.GITEE;
  • 3 用户注册以及绑定

当检测到系统中存在此认证用户后,检查是否有绑定本项目的用户,如果有直接返回token。登录到首页,如果没有则去绑定或注册

  var oAuth2User = await _oAuth2UserManager.GetOAuth2UserPersistenceIdAsync(oAuth2UserInfo);
  if (oAuth2User.UserId.HasValue)
  {
      var (accessToken, userInfo) = await GetTokenAndUserInfoAsync(oAuth2User.UserId.Value);
      await _hubContext.Clients.Client(stateInfo.ConnectionId).NoticeRedirect(accessToken, userInfo);
  }
  else
      await _hubContext.Clients.Client(stateInfo.ConnectionId).NoticeRegister(oAuth2User.PersistenceId);

3、测试

image

4、总结

gitee接入没遇到什么特别的问题,按部就班就完成了。接口如果调用异常,基本都是因为访问权限的问题,酌情处理吧

最后附上项目地址,如果您觉得还行,麻烦给个star吧。您的鼓励是我坚持更新的动力!

gitee:
https://gitee.com/dymproject/purest-admin

github:
https://github.com/dymproject/purest-admin

场景

目前一个项目中数据持久化采用EF Core + MySQL,使用CodeFirst模式开发,并且对数据进行了分库,按照目前颗粒度分完之后,大概有一两百个库,每个库的数据都是相互隔离的。
借鉴了Github上一个开源的仓库
arch/UnitOfWork
实现UnitOfWork,核心操作就是每个api请求的时候带上库名,在执行CRUD之前先将DbContext切换到目标数据库,我们在切换数据库的时候加了一些操作,如检查数据库是否已创建、检查连接是否可用、判断是否需要
表结构迁移

/// <summary>
/// 切换数据库 这要求数据库在同一台机器上 注意:这只适用于MySQL。
/// </summary>
/// <param name="database">目标数据库</param>
public void ChangeDatabase(string database)
{
    // 检查连接
    ......

    // 检查数据库是否创建
    ......

    var connection = _context.Database.GetDbConnection();
    if (connection.State.HasFlag(ConnectionState.Open))
    {
        connection.ChangeDatabase(database);
    }
    else
    {
        var connectionString = Regex.Replace(connection.ConnectionString.Replace(" ", ""), @"(?<=[Dd]atabase=)\w+(?=;)", database, RegexOptions.Singleline);
        connection.ConnectionString = connectionString;
    }

    // 判断是否需要执行表结构迁移
    if(_context..Database.GetPendingMigrations().Any())
    {
        //自定义的迁移的一些逻辑
        _context.Database.Migrate(_context);
    }
}       

但是当多个操作同时对一个库进行Migrate的时候,就会出现问题,比如“新增一张表”的操作已经被第一个迁移执行过了,第二个执行的迁移并不知道已经执行过了Migrate,就会报错表已存在。
于是考虑在执行Migrate的时候,加入一个锁的机制,对当前数据库执行Migrate之前先获取锁,然后再来决定接下来的操作。由于这边有的服务无法访问Redis,这里使用数据库来实现锁的机制,当然用Redis来实现更好,加入锁的机制只是一种解决问题的思路。

利用数据库实现迁移锁

1. 新增
MigrationLocks
表来实现迁移锁

  • 锁的操作不依赖DbContext实例
  • 在执行Migrate之前,尝试获取一个锁,在获取锁之前,如果表不存在则创建
    CREATE TABLE IF NOT EXISTS MigrationLocks (
        LockName VARCHAR(255) PRIMARY KEY,
        LockedAt DATETIME NOT NULL
    );
    
  • 成功往表中插入一条记录,视为获取锁成功,主键为需要迁移的库的名称
    INSERT INTO MigrationLocks (LockName, LockedAt) VALUES (@database, NOW());
    
  • 迁移完成后,删除这条记录,视为释放锁成功;
    DELETE FROM MigrationLocks WHERE LockName = @database;
    
  • 为防止 “死锁” 发生,每次尝试获取锁之前,会对锁的状态进行检查,释放超过5分钟的锁(正常来说,上一个迁移的执行时间不会超过5分钟)。
    SELECT COUNT(*) FROM MigrationLocks WHERE LockName = @database AND LockedAt > NOW() - INTERVAL 5 MINUTE;
    

2. 封装一下MigrateLock的实现

/// <summary>
/// 迁移锁
/// </summary>
public interface IMigrateLock
{
    /// <summary>
    /// 尝试获取锁
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    bool TryAcquireLock(IDbConnection connection);

    /// <summary>
    /// 尝试获取锁
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    Task<bool> TryAcquireLockAsync(IDbConnection connection);

    /// <summary>
    /// 释放锁
    /// </summary>
    void ReleaseLock(IDbConnection connection);

    /// <summary>
    /// 释放锁
    /// </summary>
    /// <returns></returns>
    Task ReleaseLockAsync(IDbConnection connection);
}

/// <summary>
/// 迁移锁
/// </summary>
public class MigrateLock : IMigrateLock
{
    private readonly ILogger<MigrateLock> _logger;

    public MigrateLock(ILogger<MigrateLock> logger)
    {
        _logger = logger;
    }

    private const string CreateTableSql = @"
        CREATE TABLE IF NOT EXISTS MigrationLocks (
            LockName VARCHAR(255) PRIMARY KEY,
            LockedAt DATETIME NOT NULL
        );";

    private const string CheckLockedSql = "SELECT COUNT(*) FROM MigrationLocks WHERE LockName = @database AND LockedAt > NOW() - INTERVAL 5 MINUTE;";

    private const string AcquireLockSql = "INSERT INTO MigrationLocks (LockName, LockedAt) VALUES (@database, NOW());";

    private const string ReleaseLockSql = "DELETE FROM MigrationLocks WHERE LockName = @database;";

    /// <summary>
    /// 尝试获取锁
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    public bool TryAcquireLock(IDbConnection connection)
    {
        try  
        {
            CheckLocked(connection);

            var result = connection.Execute(AcquireLockSql, new { database = connection.Database });
            if (result == 1)
            {
                _logger.LogInformation("Lock acquired: {LockName}", connection.Database);

                return true;
            }

            _logger.LogWarning("Failed to acquire lock: {LockName}", connection.Database);

            return false;
        }
        catch (Exception ex)
        {
            if (ex.Message.StartsWith("Duplicate"))
            {
                _logger.LogWarning("Failed acquiring lock due to duplicate entry: {LockName}", connection.Database);
            }
            else
            {
                _logger.LogError(ex, "Error acquiring lock: {LockName}", connection.Database);
            }

            return false;
        }
    }

    /// <summary>
    /// 尝试获取锁
    /// </summary>
    /// <param name="connection"></param>
    /// <returns></returns>
    public async Task<bool> TryAcquireLockAsync(IDbConnection connection)
    {
        try
        {
            await CheckLockedAsync(connection);

            var result = await connection.ExecuteAsync(AcquireLockSql, new { database = connection.Database });
            if (result == 1)
            {
                _logger.LogInformation("Lock acquired: {LockName}", connection.Database);

                return true;
            }

            _logger.LogWarning("Failed to acquire lock: {LockName}", connection.Database);

            return false;
        }
        catch (Exception ex)
        {
            if (ex.Message.StartsWith("Duplicate"))
            {
                _logger.LogWarning("Failed acquiring lock due to duplicate entry: {LockName}", connection.Database);
            }
            else
            {
                _logger.LogError(ex, "Error acquiring lock: {LockName}", connection.Database);
            }

            return false;
        }
    }

    /// <summary>
    /// 释放锁
    /// </summary>
    public void ReleaseLock(IDbConnection connection)
    {
        try
        {
            connection.ExecuteAsync(ReleaseLockSql, new { database = connection.Database });
            _logger.LogInformation("Lock released: {LockName}", connection.Database);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error releasing lock: {LockName}", connection.Database);
        }
    }

    /// <summary>
    /// 释放锁
    /// </summary>
    public async Task ReleaseLockAsync(IDbConnection connection)
    {
        try
        {
            await connection.ExecuteAsync(ReleaseLockSql, new { database = connection.Database });
            _logger.LogInformation("Lock released: {LockName}", connection.Database);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error releasing lock: {LockName}", connection.Database);
        }
    }

    /// <summary>
    /// 检查锁
    /// </summary>
    private void CheckLocked(IDbConnection connection)
    {
        connection.Execute(CreateTableSql); 

        var databaseParam = new
        {
            database = connection.Database
        };

        var lockExists = connection.QueryFirstOrDefault<int>(CheckLockedSql, databaseParam);
        if (lockExists <= 0)
        {
            return;
        }

        _logger.LogWarning("Lock exists and is older than 5 minutes. Releasing old lock.");
        connection.Execute(ReleaseLockSql, databaseParam);
    }

    /// <summary>
    /// 检查锁
    /// </summary>
    private async Task CheckLockedAsync(IDbConnection connection)
    {
        await connection.ExecuteAsync(CreateTableSql);
         
        var databaseParam = new
        {
            database = connection.Database
        };

        var lockExists = await connection.QueryFirstOrDefaultAsync<int>(CheckLockedSql, databaseParam);
        if (lockExists <= 0)
        {
            return;
        }

        _logger.LogWarning("Lock exists and is older than 5 minutes. Releasing old lock.");
        await connection.ExecuteAsync(ReleaseLockSql, databaseParam);
    }
}

3. 封装一下MigrateExecutor的实现

/// <summary>
/// 数据库迁移执行器
/// </summary>
public interface IMigrateExcutor
{
    /// <summary>
    /// 执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    void Migrate(DbContext dbContext);

    /// <summary>
    /// 执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <returns></returns>
    Task MigrateAsync(DbContext dbContext);

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    void ConcurrentMigrate(DbContext dbContext, bool wait = true);
     
    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    /// <returns></returns>
    Task ConcurrentMigrateAsync(DbContext dbContext, bool wait = true);

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="connection"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    void ConcurrentMigrate(DbContext dbContext, IDbConnection connection, bool wait = true);

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="connection"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    Task ConcurrentMigrateAsync(DbContext dbContext, IDbConnection connection, bool wait = true);
}

/// <summary>
/// 数据库迁移执行器
/// </summary>
public class MigrateExcutor : IMigrateExcutor
{
    private readonly IMigrateLock _migrateLock;
    private readonly ILogger<MigrateExcutor> _logger;

    public MigrateExcutor(
        IMigrateLock migrateLock,
        ILogger<MigrateExcutor> logger)
    {
        _migrateLock = migrateLock;
        _logger = logger;
    }

    /// <summary>
    /// 执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <returns></returns>
    public void Migrate(DbContext dbContext)
    {
        try
        {
            if (dbContext.Database.GetPendingMigrations().Any())
            {
                dbContext.Database.Migrate();
            }
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Migration failed");

            HandleError(dbContext, e);
        }
    }

    /// <summary>
    /// 执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <returns></returns>
    public async Task MigrateAsync(DbContext dbContext)
    {
        try
        {
            if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
            {
                await dbContext.Database.MigrateAsync();
            }
        }
        catch (Exception e)
        {
            _logger.LogError(e, "Migration failed");

            await HandleErrorAsync(dbContext, e);
        }
    }

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    /// <returns></returns>
    public void ConcurrentMigrate(DbContext dbContext, bool wait = true)
    {
        if (!dbContext.Database.GetPendingMigrations().Any())
        {
            return;
        }

        using var connection = MySqlConnectionHelper.CreateConnection(dbContext.Database.GetDbConnection().Database);

        ConcurrentMigrate(dbContext, connection, wait);
    }

    /// <summary>
    /// 并发场景执行迁移
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    /// <returns></returns>
    public async Task ConcurrentMigrateAsync(DbContext dbContext, bool wait = true)
    {
        if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
        {
            return;
        }

        await using var connection = await MySqlConnectionHelper.CreateConnectionAsync(dbContext.Database.GetDbConnection().Database);

        await ConcurrentMigrateAsync(dbContext, connection, wait);
    }

    /// <summary>
    /// 并发场景执行迁移(供数据同步相关服务使用,”迁移锁“ 使用传入的 <see cref="IDbConnection"/> 对象来完成)
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="connection"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    public void ConcurrentMigrate(DbContext dbContext, IDbConnection connection, bool wait = true)
    {
        if (!dbContext.Database.GetPendingMigrations().Any())
        {
            return;
        }

        while (true)
        {
            if (_migrateLock.TryAcquireLock(connection))
            {
                try
                {
                    Migrate(dbContext);

                    break;
                }
                finally
                {
                    _migrateLock.ReleaseLock(connection);
                }
            }

            if (wait)
            {
                _logger.LogWarning("Migration is locked, wait for 2 seconds");
                Thread.Sleep(20000);

                continue;
            }

            _logger.LogInformation("Migration is locked, skip");
        }
    }

    /// <summary>
    /// 并发场景执行迁移(供数据同步相关服务使用,”迁移锁“ 使用传入的 <see cref="IDbConnection"/> 对象来完成)
    /// </summary>
    /// <param name="dbContext"></param>
    /// <param name="connection"></param>
    /// <param name="wait">是否等待至正在进行中的迁移完成</param>
    public async Task ConcurrentMigrateAsync(DbContext dbContext, IDbConnection connection, bool wait = true)
    {
        if ((await dbContext.Database.GetPendingMigrationsAsync()).Any())
        {
            return;
        }

        while (true)
        {
            if (await _migrateLock.TryAcquireLockAsync(connection))
            {
                try
                {
                    await MigrateAsync(dbContext);
                    break;
                }
                finally
                {
                    await _migrateLock.ReleaseLockAsync(connection);
                }
            }

            if (wait)
            {
                _logger.LogWarning("Migration is locked, wait for 2 seconds");
                Thread.Sleep(20000);

                continue;
            }

            _logger.LogInformation("Migration is locked, skip");

            break;
        }
    }

    private void HandleError(DbContext dbContext, Exception e)
    {
        var needChangeList = dbContext.Database.GetPendingMigrations().ToList();
        var allChangeList = dbContext.Database.GetMigrations().ToList();
        var hasChangeList = dbContext.Database.GetAppliedMigrations().ToList();

        if (needChangeList.Count + hasChangeList.Count > allChangeList.Count)
        {
            int errIndex = allChangeList.Count - needChangeList.Count;

            if (hasChangeList.Count - 1 == errIndex && hasChangeList[errIndex] != needChangeList[0])
            {
                int index = needChangeList[0].IndexOf("_", StringComparison.Ordinal);
                string errSuffix = needChangeList[0].Substring(index, needChangeList[0].Length - index);
                if (hasChangeList[errIndex].EndsWith(errSuffix))
                {
                    dbContext.Database.ExecuteSqlRaw($"Update __EFMigrationsHistory set MigrationId = '{needChangeList[0]}' where MigrationId = '{hasChangeList[errIndex]}'");
                    dbContext.Database.Migrate();
                }
                else
                {
                    throw e;
                }
            }
            else
            {
                throw e;
            }
        }
        else
        {
            throw e;
        }

        _logger.LogInformation("Migration failed, but success on second try.");
    }

    private async Task HandleErrorAsync(DbContext dbContext, Exception e)
    {
        var needChangeList = (await dbContext.Database.GetPendingMigrationsAsync()).ToList();
        var allChangeList = dbContext.Database.GetMigrations().ToList();
        var hasChangeList = (await dbContext.Database.GetAppliedMigrationsAsync()).ToList();

        if (needChangeList.Count + hasChangeList.Count > allChangeList.Count)
        {
            int errIndex = allChangeList.Count - needChangeList.Count;

            if (hasChangeList.Count - 1 == errIndex && hasChangeList[errIndex] != needChangeList[0])
            {
                int index = needChangeList[0].IndexOf("_", StringComparison.Ordinal);
                string errSuffix = needChangeList[0].Substring(index, needChangeList[0].Length - index);
                if (hasChangeList[errIndex].EndsWith(errSuffix))
                {
                    await dbContext.Database.ExecuteSqlRawAsync($"Update __EFMigrationsHistory set MigrationId = '{needChangeList[0]}' where MigrationId = '{hasChangeList[errIndex]}'");
                    await dbContext.Database.MigrateAsync();
                }
                else
                {
                    throw e;
                }
            }
            else
            {
                throw e;
            }
        }
        else
        {
            throw e;
        }

        _logger.LogInformation("Migration failed, but success on second try.");
    }
}

为什么感觉到卷?可能的一个原因是大家都在进步,用户和竞争对手也在进步,而自己却没有进步,也谈不上思维模式的改变。

我们不谈理论、不谈理想、不谈市场环境不好,就谈与用户接触过程的案例,这是最有说服力的。

1.一个集团的生产单位,要做4个分厂的集中管控平台,我们对分厂的业务进行了充分调研,经过多次交流,也做了详细的方案。但是集团的大领导一句话,项目给了集团做信息化的子公司。现在不管是私有集团、国有集团、合资集团都有自己的信息化公司,他们自己当然有在自动化、信息化和智能化领域做大做强的合理化诉求。市场环境越不好,诉求越强烈,大家都要生存。所以,我们的宿命可能只能给集团信息化公司做分包,人家吃肉、自己喝汤。

2.一个集团的运维公司,他们自己的技术人员做了一个B/S结构的运维管理系统,实现下发运维任务、运维操作规范、运维时长、KPI考核与工资挂钩等,尽管界面不是太友好和系统运行较慢,但是毕竟自主实现了和业务强关联的管理系统。他们找我们想重构和完善这套系统,可想而知的是极低的报价,尽管这样可能他们都会嫌贵。

3.一个集团的计量部门,统管集团所有计量设备,我们给他们进行设备改造和升级进行现场调研,我们提议:应该把所有设备的数据统一接入到计量部门监测和管理。我们无非是想挖掘需求,多做一些业务。他们回复:自己已经开发一套这样的系统,能看到所有计量设备的数据。后来参观了他们的计量监控室,空间很大,整面墙全是监控视频,对面是两排监控电脑,他们自己开发的计量监控系统是WinForm C/S结构,定制开发了数据分析功能。在前期交流的过程中,我感觉他们没有开发能力,还是夸赞他们做的非常不错。

集团有自己的信息化公司、职能类型的子公司有自己的开发能力,请问广大软件信息化类公司的生存空间在哪?是不是在逐步缩小?

最近试用了Cursor开发工具,类似VSCODE,集成的AI功能非常方便,大的思路规划好,AI提示的代码再进行集成和整合。将来传统软件是否都具备AI能力?

我们应该好好审视自己,给用户随便吹能做这、能做那、能降本增效智能化,但是我们自己的产品化率不高、还在用excel传递信息等,降低不了技术开发成本、实施成本和管理成本,吹的越狠反而对自己的伤害越大。


物联网&大数据技术 QQ群:54256083

物联网&大数据项目 QQ群:727664080

QQ:504547114

微信:wxzz0151

博客:https://www.cnblogs.com/lsjwq

微信公众号:iNeuOS

Java SE 23 新增特性

作者:
Grey

原文地址:

博客园:Java SE 23 新增特性

CSDN:Java SE 23 新增特性

源码

源仓库:
Github:java_new_features

Primitive Types in Patterns, instanceof, and switch (预览功能)

通过 instanceof 和 switch,我们可以检查对象是否属于特定类型,如果是,则将该对象绑定到该类型的变量,执行特定的程序路径,并在该程序路径中使用新变量。

public class PrimitiveTypesTest {
    void main() {
        test1("hello world");
        test2("hello world");
        test1(56);
        test2(56);
        test1(java.time.LocalDate.now());
        test2(java.time.LocalDate.now());
    }

    private static void test1(Object obj) {
        if (obj instanceof String s && s.length() >= 5) {
            System.out.println(s.toUpperCase());
        } else if (obj instanceof Integer i) {
            System.out.println(i * i);
        } else {
            System.out.println(obj);
        }
    }

    private static void test2(Object obj) {
        switch (obj) {
            case String s when s.length() >= 5 -> System.out.println(s.toUpperCase());
            case Integer i -> System.out.println(i * i);
            case null, default -> System.out.println(obj);
        }
    }
}

JEP 455
在 Java 23 中引入了两项变更:

  • 可以在 switch 表达式和语句中使用所有基元类型,包括 long、float、double 和 boolean。

  • 其次,我们还可以在模式匹配中使用所有基元类型,包括 instanceof 和 switch。

在这两种情况下,即通过 long、float、double 和布尔类型进行 switch 以及使用基元变量进行模式匹配时,与所有新的 switch 功能一样,switch 必须要涵盖所有可能的情况。

private static void test3(int x) {
    switch (x) {
        case 1, 2, 3 -> System.out.println("Low");
        case 4, 5, 6 -> System.out.println("Medium");
        case 7, 8, 9 -> System.out.println("High");
    }
}

Module Import Declarations (模块导入声明,预览功能)

通过简洁地导入模块导出的所有包的功能来增强 Java 编程语言。这简化了模块库的重复使用,但不要求导入代码本身必须在模块中。这是一项预览语言功能。

自 Java 1.0 起,
java.lang
包中的所有类都会自动导入到每个 .java 文件中。这就是为什么我们无需导入语句就能使用
Object

String

Integer

Exception

Thread
等类的原因。

我们还可以导入完整的包。例如,导入
java.util.*
意味着我们不必单独导入
List

Set

Map

ArrayList

HashSet

HashMap
等类。

JEP 467
现在允许我们导入完整的模块,更准确地说,是导入模块导出的包中的所有类。

例如,我们可以按如下方式导入完整的
java.base
模块,然后使用该模块中的类(例如
List

Map

Collectors

Stream
),而无需进一步导入:

package git.snippets.jdk23;

import module java.base;

public class ModuleImportDeclarationsTest {
    void main() {
        System.out.println(groupByFirstLetter("a", "abc", "bcd", "ddd", "dddc", "dfc", "bc"));
    }

    public static Map<Character, List<String>> groupByFirstLetter(String... values) {
        return Stream.of(values).collect(Collectors.groupingBy(s -> Character.toUpperCase(s.charAt(0))));
    }
}

如果有两个同名的导入类,例如下面示例中的 Date,编译器就会出错:

import module java.base;
import module java.sql;

如果一个导入模块临时导入了另一个模块,那么我们也可以使用临时导入模块导出包中的所有类,而无需显式导入。

例如,java.sql 模块转义导入了 java.xml 模块:

module java.sql {
  . . .
  requires transitive java.xml;
  . . .
}

因此,在下面的示例中,我们不需要显式导入 SAXParserFactory 和 SAXParser,也不需要显式导入 java.xml 模块:

SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

Flexible Constructor Bodies (二次预览)

在 JDK 23 之前,下述代码中,Child1的构造函数,只能先通过super构造父类,然后才能初始化子类的 b 这个变量。

public class FlexibleConstructorBodies {
    void main() {
        new Child1(1, 2);
    }
}


class Parent {
    private final int a;

    public Parent(int a) {
        this.a = a;
        printMe();
    }

    void printMe() {
        System.out.println("a = " + a);
    }
}

// JDK 23 之前
class Child1 extends Parent {
    private final int b;

    public Child1(int a, int b) {
        super(verifyParamsAndReturnA(a, b));
        this.b = b;
    }

    @Override
    void printMe() {
        super.printMe();
        System.out.println("b = " + b);
    }

    private static int verifyParamsAndReturnA(int a, int b) {
        if (a < 0 || b < 0) throw new IllegalArgumentException();
        return a;
    }
}

当我们执行

new Child1(1,2);

这段代码的时候,本来我们期待返回的是

a = 1
b = 2

但是由于父类在构造时候调用了
printMe()
,且这个调用是在 b 变量初始化之前调用的,所以导致程序执行的结果是

a = 1
b = 0

今后,在使用 super(...) 调用超级构造函数之前,以及在使用 this(...) 调用替代构造函数之前,我们可以执行任何不访问当前构造实例(即不访问其字段)的代码

此外,我们还可以初始化正在构造的实例的字段。详见
JEP 482

在 JDK 23 上,上述代码可以调整为:

class Child2 extends Parent {
    private final int b;

    public Child2(int a, int b) {
        if (a < 0 || b < 0) throw new IllegalArgumentException();  // ⟵ Now allowed!
        this.b = b;                                                // ⟵ Now allowed!
        super(a);
    }

    @Override
    void printMe() {
        super.printMe();
        System.out.println("b = " + b);
    }
}

其中构造函数中,a和b的初始化和判断,都可以在super(...)函数调用之前,
执行

new Child2(1,2);

打印结果为预期结果

a = 1
b = 2

Implicitly Declared Classes and Instance Main Methods (第三次预览)

最早出现在 JDK 21 中,见
Java SE 21 新增特性

原来我们写一个main方法,需要

public class UnnamedClassesAndInstanceMainMethodsTest {

    public static void main(String[] args) {
        System.out.println("Hello World!");
    }

}

而且Java文件的名称需要和UnnamedClassesAndInstanceMainMethodsTest保持一致,到了JDK 23,上述代码可以简化成

void main() {
    System.out.println("hello world");
}

甚至连 public class ... 这段也不需要写。

更多

Java SE 7及以后各版本新增特性,持续更新中...

参考资料

Java Language Changes for Java SE 23

JDK 23 Release Notes

JAVA 23 FEATURES(WITH EXAMPLES

不知道你在开发过程中有没有遇到过这样的困惑:这个变量怎么值被改?这个值怎么没变?

今天就来和大家分享可能导致这个问题的根本原因值传递 vs 引用传递。

在此之前我们先回顾两组基本概念:

值类型 vs 引用类型

值类型:
直接存储数据,数据存储在栈上;

引用类型:
存储数据对象的引用,数据实际存储在堆上。

形参 vs 实参

形参:
即形式参数,表示调用方法时,方法需要你传递的值。方法声明定义了其形参。也就是说在定义方法时,紧跟在方法名后面括号中的参数列表就是形参。

实参:
即实际参数,表示调用方法时,你传递给方法形参的值。调用代码在调用过程时提供实参。也就是说在调用方法时,紧跟在方法名后面括号中的参数列表就是实参。

再来回顾一下值类型和引用类型在内存中是怎么存储的呢?

对于值类型变量的值直接存储在栈中,如下图的int a=10,10就直接存在栈空间中,而其栈空间对应的内存地址为0x66666668;对于引用类型变量本身存储的是实例对象的引用,即实例对象在堆中的实际内存地址,因此引用类型变量是存储其实例对象的引用于栈上,如下图中变量Test a在栈中实际存储的是实例对象Test a在堆中的内存地址0x88888880,而栈空间对应的内存地址为0x66666668。

栈也是有内存地址的,这一点很重要,无论栈空间上存储的是值还是引用地址,这个栈空间本身也有自己对应的内存地址。

什么是值传递?什么是引用传递?

值传递
:如果变量按值传递给方法,则会把变量的副本传递给方法。对于值类型则把
变量的副本
传递给方法,对于引用类型则把
变量的引用的副本
传递给方法。因此被调用方法参数会创建一个新的内存地址用于接收存储变量,因此在方法内部对变量修改并不会影响原来的值。

引用传递
:如果变量按引用传递给方法,则会把变量的引用传递给方法,对于值类型则把
变量的栈空间地址
传递给方法,对于引用类型则把
变量的引用的栈空间地址
传递给方法。因此被调用方法参数不会创建一个新的内存地址用于接收存储变量,意味着形参与实参共同指向相同的内存地址,因此在方法内部修对变量修改会影响原来的值。

上面的描述可能有点拗口,下面我们在基于值类型、引用类型、值传递、引用传递各种组合进行一个详细说明。

01
、值类型按值传递

当值类型按值传递时,调用者会把值类型变量的副本传递给方法,因此被调用方法参数会创建一个新的内存地址用于接收存储变量,因此当在方法内部对参数进行修改时并不会影响调用者调用处的值类型变量。

传递值类型变量的副本就是相当于在栈上,又复制了一个同样的值,而且内存地址还不一样,所以互不影响。如下图把a赋值给b,则b直接新开辟了一个栈空间,虽然a和b都是10,但是它们在不同的地址空间中,因此如果他们各自被修改了,也互不影响。

下面我们写个例子演示一下,这个例子就是定义个变量a并赋值,然后调用一个方法此方法内对传进来的参数a进行加1,具体代码如下:

public static void ValueByValueRun()
{
    var a = 10;
    Console.WriteLine($"调用者-调用方法前 a 值:{a}");
    ChangeValueByValue(a);
    Console.WriteLine($"调用者-调用方法后 a 值:{a}");
}
public static void ChangeValueByValue(int a)
{
    Console.WriteLine($"    被调用方法-接收到 a 值:{a}");
    a = a + 1;
    Console.WriteLine($"    被调用方法-修改后 a 值:{a}");
}

运行结果如下:

通过代码执行结果可以发现,方法内对变量的修改已经生效,但是不没有影响到调用者调用处的变量值。

02
、引用类型按值传递

当引用类型按值传递时,调用者会把引用类型变量的引用副本传递给方法,因此被调用方法参数会创建一个新的内存地址用于接收存储变量,而对于一个引用类型变量来说其本身存储的就是引用类型实例对象的引用副本,而方法接收到的也是此变量引用的副本,所以调用者参数和被调用方法参数是引用了同一个实例对象的两个引用副本。如下图Test a可以理解为调用者传的实参,Test b可以理解为被调用方法定义的形参,这两个参数都只是指向堆中Test a的引用副本。

因此可以得出两个结论:

1、变量a和b都是指向实例对象Test a的引用,所以无论变量a或b,只要有一个更新了实例成员则另一个变量也会同步发生变化。

2、虽然变量a和b都是指向实例对象Test a的引用,但是他们存储在栈上的内存地址却不同,因此如果他们各种重新分配实例也就是new一个新对象,则另一个变量无法感知到还是保持原因状态不变。

我们先用代码说明第一个结论:

public static void ChangeReferenceByValueRun()
{
    var a = new Test
    {
        Age = 10
    };
    Console.WriteLine($"调用者-调用方法前 a.Age 值:{a.Age}");
    ChangeReferenceByValue(a);
    Console.WriteLine($"调用者-调用方法后 a.Age 值:{a.Age}");
}
public static void ChangeReferenceByValue(Test a)
{
    Console.WriteLine($"    被调用方法-接收到 a.Age 值:{a.Age}");
    a.Age = a.Age + 1;
    Console.WriteLine($"    被调用方法-修改后 a.Age 值:{a.Age}");
}

运行结果如下:

可以看到被调用方法中a实例对象的Age属性发生变化后,调用者中变量也同步发生了变化。

对于第二个结论我们这样论证,在方法中直接对参数new一个新对象,看看原变量是否发生变化,代码如下:

public static void NewReferenceByValueRun()
{
    var a = new Test
    {
        Age = 10
    };
    Console.WriteLine($"调用者-调用方法前 a.Age 值:{a.Age}");
    NewReferenceByValue(a);
    Console.WriteLine($"调用者-调用方法后 a.Age 值:{a.Age}");
}
public static void NewReferenceByValue(Test a)
{
    Console.WriteLine($"    被调用方法-接收到 a.Age 值:{a.Age}");
    a = new Test
    {
        Age = 100
    };
    Console.WriteLine($"    被调用方法-new后 a.Age 值:{a.Age}");
}

执行结果如下:

可以发现当在方法中对变量执行new操作后,调用者处的变量并没有发生变化。

为什么会这样呢?因为对于引用类型来说,形参和实参是对引用类型的实例对象引用的两个副本,而这两个副本存储在栈上又分别在不同的内存地址空间上,而new主要就是重新分配内存,这就导致形参变量a=new后,栈上形参变量a指向了Test新的实例对象的引用,而实参变量a还是保持原有实例对象引用不变。

如下图所示。

03
、值类型按引用传递

当值类型按引用传递时,调用者会把值类型变量对应的栈空间地址传递给方法,因此被调用方法参数不会创建一个新的内存地址用于接收存储变量,因此当在方法内部对参数进行修改时并同样会影响调用者调用处的值类型变量。

传递值类型变量对应的栈空间地址就意味着形参与实参共同指向相同的内存地址,所以才导致对形参修改时,实参也会同步发生变化。

我们用一个小例子演示一下:

public static void ValueByReferenceRun()
{
    Console.WriteLine($"值类型按引用传递");
    var a = 10;
    Console.WriteLine($"调用者-调用方法前 a 值:{a}");
    ChangeValueByReference(ref a);
    Console.WriteLine($"调用者-调用方法后 a 值:{a}");
}
public static void ChangeValueByReference(ref int a)
{
    Console.WriteLine($"    被调用方法-接收到 a 值:{a}");
    a = a + 1;
    Console.WriteLine($"    被调用方法-修改后 a 值:{a}");
}

执行结果如下:

可以发现调用者处的值类型变量已经发生改变。

04
、引用类型按引用传递

当引用类型按引用传递时,调用者会把引用类型变量对应的栈空间地址传递给方法,因此被调用方法参数不会创建一个新的内存地址用于接收存储变量,因此当在方法内部对参数进行修改时并同样会影响调用者调用处的引用类型变量。

传递引用类型变量对应的栈空间地址就意味着形参与实参共同指向相同的内存地址,因此对形参修改时,实参也会同步发生变化,而且这个里的修改不单单指修改实例成员,还包括new一个新实例对象。

下面我们看一个修改实例成员的例子:

public static void ChangeReferenceByReferenceRun()
{
    Console.WriteLine($"引用类型按引用传递 - 修改实例成员");
    var a = new Test
    {
        Age = 10
    };
    Console.WriteLine($"调用者-调用方法前 a.Age 值:{a.Age}");
    ChangeReferenceByReference(ref a);
    Console.WriteLine($"调用者-调用方法后 a.Age 值:{a.Age}");
}
public static void ChangeReferenceByReference(ref Test a)
{
    Console.WriteLine($"    被调用方法-接收到 a.Age 值:{a.Age}");
    a.Age = a.Age + 1;
    Console.WriteLine($"    被调用方法-修改后 a.Age 值:{a.Age}");
}

执行结果如下:

再看看new一个新对象的例子:

public static void NewReferenceByReferenceRun()
{
    Console.WriteLine($"引用类型按引用传递 - new 新实例");
    var a = new Test
    {
        Age = 10
    };
    Console.WriteLine($"调用者-调用方法前 a.Age 值:{a.Age}");
    NewReferenceByReference(ref a);
    Console.WriteLine($"调用者-调用方法后 a.Age 值:{a.Age}");
}
public static void NewReferenceByReference(ref Test a)
{
    Console.WriteLine($"    被调用方法-接收到 a.Age 值:{a.Age}");
    a = new Test
    {
        Age = 100
    };
    Console.WriteLine($"    被调用方法-new后 a.Age 值:{a.Age}");
}

执行结果如下:

另外string是一个特殊的引用类型,string类型变量的按值传递和按引用传递和值类型是一致的,也就是要把string类型当值类型一样看待就行。string类型的特殊性我们后面会单独具体介绍。

在C#中以下修饰符可应用与参数声明,并且会使得参数按引用传递:ref、out、readonly ref、in。对于每个修饰符具体怎么使用就不再这里细说了。

相信到这里你应该就可以回答我之前在《LeetCode题集-2 - 两数相加》最后提的问题了。


:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。
https://gitee.com/hugogoos/Planner