2024年11月

Chrome 浏览器 131 版本新特性

一、Chrome 浏览器 131 版本新特性

1.
在 iOS 上使用 Google Lens 搜索

自 Chrome 126 版本以来,用户可以通过 Google Lens 搜索屏幕上看到的任何图片或文字。

要使用此功能,请访问网站,并点击聚焦时出现在地址栏的 Google Lens 搜索按钮,或者点击桌面右键菜单、桌面和移动端的三点菜单。用户可以在屏幕上任意点击、选中或拖动以搜索内容,并通过在搜索框中添加关键词或问题来优化搜索。

管理员可以通过名为 LensOverlaySettings 的策略控制此功能。进行搜索时,屏幕截图会被发送到 Google 服务器,但不会关联任何 ID 或账户,且不会被任何人查看,内容信息也不会被记录。我们已在 iOS 的 Chrome 131 版本中开始逐步推出此功能,并计划在 Chrome 132 版本中全面推出。

1.1.
适用版本:

  • Chrome 126
    (适用于 ChromeOS、Linux、macOS、Windows):在 1% 的稳定版中发布该功能
  • Chrome 127
    (适用于 ChromeOS、Linux、macOS、Windows):在 100% 的稳定版中发布该功能
  • Chrome 131
    (适用于 iOS):开始推出该功能
  • Chrome 132
    (适用于 iOS):在 100% 的稳定版中发布该功能

2.
iOS 上的异步实时安全浏览检查

目前,安全浏览检查是在页面加载的阻塞路径上进行的,这意味着用户在检查完成之前无法看到页面。为了提升 Chrome 的加载速度,在 Chrome 122 版本(iOS 上为 Chrome 131 版本)之后,实时安全浏览检查将不再阻塞页面加载。

2.1.
适用版本:

  • Chrome 122:Android、ChromeOS、Linux、macOS、Windows
  • Chrome 131:iOS

3.
适用于 macOS 的 PWA Shim 临时代码签名

在 macOS 上安装渐进式 Web 应用程序(PWA)时创建的应用程序 Shim 的代码签名将更改为在应用程序安装时生成的临时代码签名。

代码签名由 macOS 用于标识应用程序。这些临时签名会让每个 PWA 应用 Shim 在 macOS 上拥有唯一的身份。

可以使用 chrome://flags/#use-adhoc-signing-for-web-app-shims 标志开启该功能进行测试。

如果该功能与现有的安全策略不兼容,可以使用 AdHocCodeSigningForPWAsEnabled 策略禁用该功能,以便管理员部署更新的端点安全策略。

3.1.
适用版本:

  • Chrome 129 on macOS
    :此功能可以通过标志(chrome://flags/#use-adhoc-signing-for-web-app-shims)开启。
  • Chrome 131 on macOS
    :该功能开始逐步向稳定版推出,初始推出率为 1%。

4.
从 Google Drive 中选择文件

从 Chrome 131 开始,iOS 版 Chrome 用户可以直接从 Google Drive 将文件上传到网页,而无需先将其下载到设备上。

5.
Chrome PDF 查看器 OCR 功能

Chrome 桌面版现在使扫描的 PDF 更加易于访问。通过设备上的光学字符识别(OCR)技术来确保隐私(不会将内容发送到 Google),Chrome 自动转换扫描的 PDF,让用户可以选择文本、使用 Ctrl+F 搜索、复制和粘贴。该功能不会绕过受保护的 PDF,仅对用户有访问权限的 PDF 使用 OCR。

  • Chrome 131
    适用于 ChromeOS、Linux、macOS 和 Windows。

6.
桌面端新标签页的 iOS 版 Chrome 推广

桌面端的新标签页中将显示 iOS 版 Chrome 的推广信息。此推广旨在提高用户对 iOS 版 Chrome 的认知,并提供简单的安装方式。

可以使用现有的 PromotionsEnabled 和 NTPMiddleSlotAnnouncementVisible 策略来控制此功能。

7.
跨配置文件的密码重用检测

之前,公司凭证的密码重用检测只能在公司配置文件中检测到。现在,在受管理的浏览器上,密码重用检测可以在所有非隐身模式的配置文件中检测公司凭证的重用。

更新了跨配置文件密码重用检测的标准,以更准确地反映受管理的企业账户。

8.
Android 版 Chrome 现已支持第三方自动填充和密码管理服务

此前,Android 版 Chrome 中的第三方自动填充和密码管理服务是通过辅助功能 API 实现的。

在 Chrome M131 版本中,增加了对 Android 自动填充的直接支持,这意味着这些第三方服务可以在不依赖辅助功能 API 的情况下与 Android 版 Chrome 配合使用,从而提升 Chrome 和第三方自动填充服务的性能。

要使用此功能,用户需要在 Android 设置中配置好第三方提供商。然后,在 Chrome 中访问设置 -> 自动填充服务,并选择“使用其他服务进行自动填充”。

如果用户未更改这两个设置,Chrome 将继续使用 Google 的服务来自动填充密码、支付和地址信息。是否允许用户使用第三方自动填充服务可由新的策略
ThirdPartyPasswordManagersAllowed
控制。

9.
弃用安全浏览扩展报告功能

Chrome 正在弃用安全浏览扩展报告功能,该功能曾通过收集参与用户的遥测信息来增强所有用户的安全性,并用于 Google 安全浏览保护。收集的数据包括访问网页的 URL、有限的系统信息和部分页面内容。

此功能现已被增强保护模式取代。建议用户切换到增强保护模式,以便在启用 Chrome 最强安全性功能的同时,继续提供对所有用户的安全保护。

image

10.
不再信任 Entrust 证书

由于 Entrust 持续存在合规问题,Chrome 将在 Chrome 131 及更高版本的 Windows、macOS、ChromeOS、Android 和 Linux 中默认更改对 Entrust 颁发的公开信任 TLS 服务器认证(网站)证书的信任方式。iOS 的政策不允许在 iOS 版 Chrome 中使用 Chrome 根存储。

具体而言,通过 Entrust 根 CA 证书验证并存储于 Chrome 根存储中的 TLS 证书,如果:

  • 签发日期为 2024 年 11 月 11 日之后
    ,将不再被默认信任。
  • 签发日期为 2024 年 11 月 11 日或之前
    ,则不受此更改的影响。
  • Chrome 131 on Android、ChromeOS、Linux、macOS、Windows
    :所有依赖 Chrome 根存储的 Chrome 131 及更高版本将执行该阻止操作,但阻止仅适用于 2024 年 11 月 11 日后签发的证书。

11.
iOS 上的不安全表单警告

自 Chrome 125 起,Chrome 浏览器在 iOS 上会阻止从安全页面提交到不安全页面的表单。当 Chrome 检测到不安全的表单提交时,会显示一个警告,要求用户确认提交。此举的目的是防止在没有用户明确批准的情况下,通过明文传输泄露表单数据。提供了一个名为
InsecureFormsWarningsEnabled
的策略,用于控制此功能。

  • Chrome 125 在 iOS 上
    :此功能开始推出。
  • Chrome 131 在 iOS 上
    :InsecureFormsWarningsEnabled 策略将被移除。

12.
具有高级检查的 PartitionAlloc (PA/AC)

PartitionAlloc (PA) 及其相关的内存安全项目包含一系列高级保护措施,这些措施默认情况下被禁用(或仅在调试版本中启用),因为它们可能会对性能产生影响。尽管立即为所有用户启用该功能可能不可行,但仍然有机会在特定的有限条件下部分启用它。

13.
简化的登录和同步体验

从 Chrome 131 开始,启用 Chrome 同步的现有用户将体验到简化和整合的登录与同步流程。

Chrome 同步不再作为设置中的独立功能显示,也不再单独列出。相反,用户只需登录 Chrome,即可在 Google 帐户中使用和保存信息,如密码、书签等,前提是遵循相关的企业政策。

与之前一样,保存和访问 Chrome 数据到 Google 帐户的功能仍然可以通过 SyncTypesListDisabled 进行控制。登录 Chrome 的功能仍然可以通过 BrowserSignin 来关闭,如同以前一样。

需要注意的是,这些更改不会影响用户在 Web 上登录 Google 服务(如 Gmail)的能力,即使他们未登录 Chrome,也不会影响他们保持 Chrome 未登录状态的能力,或者控制与 Google 帐户同步的信息。

14.
能源节省模式下的标签冻结

当启用能源节省模式时,Chrome 会冻结那些已隐藏且静默超过 5 分钟并且占用大量 CPU 的标签,除非:

  • 该标签提供音频或视频会议功能,通过麦克风、摄像头、屏幕、窗口或标签捕获,或者有一个 RTCPeerConnection 正在使用打开的 RTCDataChannel 或一个活跃的 MediaStreamTrack。
  • 该标签控制一个外部设备,通过 Web USB、Web Bluetooth、Web HID 或 Web Serial 被检测到。

此功能通过减少 CPU 使用,延长电池寿命并加速 Chrome 浏览器的运行。

该功能将在 Chrome 131 中开始向 1% 的稳定版用户逐步推出,随后会逐渐扩展到 100% 的稳定版用户。

15.
更新 Google Play 服务以修复设备上的密码问题

使用旧版 Google Play 服务的用户将会在设备上的密码功能上遇到功能减少的问题,并且密码管理器可能很快完全无法使用。这些用户需要更新 Google Play 服务,或者根据他们的状态,系统将引导他们通过其他故障排除方法进行解决。这是一个正在进行的迁移过程,仅影响使用 Google 密码管理器的 Android 用户。

16.
用于 TLS 的 X25519Kyber768 密钥封装机制

从 Chrome 124 开始,Chrome 在所有桌面平台上默认启用了基于 NIST 标准(ML-KEM)的新后量子安全 TLS 密钥封装机制 X25519Kyber768。该机制旨在保护 Chrome 与支持 ML-KEM 的服务器之间的网络流量,防止未来量子计算机解密。这一变更对于服务器运营商应当是透明的。此密码算法将用于 TLS 1.3 和 QUIC 连接。

17.
CSS 锚点定位属性
inset-area
的废弃

CSS 工作组(CSSWG)决定将 inset-area 属性重命名为 position-area。

通过此次功能更新,position-area 属性作为 inset-area 的同义词发布,标志着 inset-area 属性的废弃和移除。

这一变更将影响使用 inset-area 属性的开发者,未来应该使用新的 position-area 属性来替代旧的名称。

18.
改进
<details>

<summary>
元素的样式结构

为 <details> 和 <summary> 元素提供更多的 CSS 样式支持,使这些元素可以在更多构建披露小部件或手风琴小部件的网页中使用。

这项更改移除了以前限制设置这些元素的 display 属性的限制,并新增了 ::details-content 伪元素,用于样式化用于扩展和折叠的部分容器。

19.
键盘锁定和指针锁定权限

当网站请求使用键盘锁定或指针锁定时,浏览器可能会向用户显示权限提示,并将用户的偏好设置保存为内容设置。这些设置可以通过权限 API 进行查询。此措施有助于减少对这些 API 的滥用。

20.
移除非标准的
GPUAdapterrequestAdapterInfo()
方法

WebGPU 工作组(WG)决定,requestAdapterInfo() 方法触发权限提示的方式不切实际,因此他们移除了该选项,并用 GPUAdapter 的 info 属性替代。这样,网页开发者可以同步地获取相同的 GPUAdapterInfo 值。

21.
<select>
解析器放宽

此更改使 HTML 解析器在 <select> 元素中允许更多的标签,除了 <option>、<optgroup> 和 <hr>。

这项更改是为了支持可自定义的 <select> 功能,但由于它可以单独实现并且风险较小,因此首先推出。

此功能受临时策略 SelectParserRelaxationEnabled 控制。该策略将在 Chrome 136 版本中停止生效。

22.
支持外部 SVG 资源用于
clip-path

fill

stroke

marker-*
属性

此更改允许在 clip-path、fill、stroke 和 marker-* 属性中使用外部 SVG 资源的引用。例如,可以使用如下语法:

clip-path: url("resources.svg#myPath");

可以在 CSS 中直接引用外部 SVG 文件中的路径、标记或填充/描边服务器,从而使图形元素的样式更加灵活和可重用。

23.
支持非特殊方案 URL

Chrome 130 开始支持非特殊方案的 URL,例如 git://example.com/path。之前,Chromium 的 URL 解析器不支持非特殊 URL,且解析非特殊 URL 时会将路径视为不透明的,这与 URL 标准不一致。现在,Chromium 的 URL 解析器已正确解析非特殊 URL,遵循 URL 标准。

23.1.
适用版本:

  • Chrome 130
    在 Windows、macOS、Linux、Android 上支持非特殊方案的 URL。
  • Chrome 131
    在 Windows、macOS、Linux、Android 上继续支持。
  • Chrome 134
    在 Windows、macOS、Linux、Android 上将移除此功能标志。

24.
翻译功能与 Google Lens 搜索结合

增强现实(AR)翻译功能正在被集成到 Google Lens 搜索功能中。

25.
Chrome 浏览器中的新策略

策略名称 描述
DownloadRestrictions

允许设置下载限制。
CAPlatformIntegrationEnabled

使用用户添加的 TLS 证书来自平台信任存储进行服务器身份验证。
SelectParserRelaxationEnabled
控制是否启用 <select> 元素的新 HTML 解析器行为。
EnterpriseProfileBadgeToolbarSettings

控制工具栏中企业配置文件徽章的可见性。
WebAudioOutputBufferingEnabled

启用 Web 音频的自适应缓冲。

26.
Chrome 浏览器移除的策略

策略名称 描述
ProfileLabel

控制用于标识已登录配置文件的标签。该标签将显示在多个位置,帮助用户识别配置文件,例如在工具栏的配置文件图标旁边。
ToolbarAvatarLabelSettings

管理工具栏头像标签设置。

二、Chrome 131 版本更新日期

1. Chrome 131

1.1. Beta 版

2024 年 10 月 16 日,星期三

1.2. 稳定版本

2024 年 11 月 5 日,星期二

参考资料

.NET Conf上的介绍

在今年的.NET Conf上Steve Sanderson带来了题为“AI Building Blocks - A new, unified AI layer”的演讲。该演讲的主要内容如下:

“大多数.NET应用程序可以通过AI功能变得更加强大和高效,例如语义搜索、自动分类、摘要生成、翻译、数据提取,甚至是基于聊天的助手。但直到现在,.NET本身还没有统一的AI概念表示标准,因此开发者需要组合使用许多不相关的API。Microsoft.Extensions.AI解决了这个问题,提供了一组新的AI服务标准API,包括在本地工作站上运行或作为托管服务的大型语言模型(LLMs),并集成了文本嵌入、向量存储等功能。在本次演讲中,我们将展示这些新的标准抽象如何让你组合多个服务,并且这些服务可以随着时间的推移轻松替换和更改,以及如何在更高级的场景中接入内部机制。通过本次演讲,你将能够开始在自己的应用程序中实验新的AI功能。”

youtube地址:
https://www.youtube.com/watch?v=qcp6ufe_XYo&list=PLdo4fOcmZ0oXeSG8BgCVru3zQtw_K4ANY&index=3

Steve Sanderson介绍了以下几种应用场景:

image-20241120094958717

Microsoft.Extensions.AI介绍

2024年10月8日,Luis Quintanilla在.NET Blog上发布了题为“Introducing Microsoft.Extensions.AI Preview – Unified AI Building Blocks for .NET”的文章介绍了Microsoft.Extensions.AI Preview。

文章地址:
https://devblogs.microsoft.com/dotnet/introducing-microsoft-extensions-ai-preview/

“Microsoft.Extensions.AI 是一组由 .NET 生态系统中的开发者(包括 Semantic Kernel 团队)共同开发的核心 .NET 库。这些库提供了一层统一的 C# 抽象层,用于与 AI 服务进行交互,例如小型和大型语言模型(SLM 和 LLM)、嵌入内容以及中间件。”

img

“目前,我们的重点是创建抽象概念,这些抽象概念可以由各种服务实现,并且都遵循相同的核心理念。我们不打算发布针对任何特定服务提供商的API。我们的目标是在.NET生态系统中充当一个统一的层,使开发者能够选择他们喜欢的框架和库,同时确保在整个生态系统中的无缝集成和协作。”

Microsoft.Extensions.AI的优势

Microsoft.Extensions.AI 提供了一个统一的 API 抽象,用于 AI 服务,类似于我们在日志记录和依赖注入(DI)抽象方面的成功。我们的目标是提供标准的实现,用于缓存、遥测、工具调用和其他常见任务,这些实现可以与任何提供商兼容。

核心优势有以下几点:

统一API:为将AI服务集成到.NET应用程序提供了一致的API和约定。
灵活性:允许.NET库作者使用AI服务而无需绑定特定提供商,使其适应任何提供商。
易用性:使.NET开发人员能够使用相同的底层抽象尝试不同的包,在整个应用程序中保持单一API。
组件化:简化了添加新功能的过程,并促进了应用程序的组件化和测试。

Microsoft.Extensions.AI简单实践

使用Microsoft.Extensions.AI可以看Nuget包的介绍。

地址:
https://www.nuget.org/packages/Microsoft.Extensions.AI.Abstractions/9.0.0-preview.9.24556.5

先简单的以OpenAI为例,然后考虑到在国内使用OpenAI不便,再介绍一下如何接入兼容OpenAI格式的大语言模型提供商。

简单的对话:

string OPENAI_API_KEY = "sk-sssss...";

IChatClient client =
 new OpenAIClient(OPENAI_API_KEY)
.AsChatClient(modelId: "gpt-4o-mini");

var response = await client.CompleteAsync("你是谁?");

Console.WriteLine(response.Message);

效果:

image-20241120101114704

我比较关心的是Function Calling的功能,来简单尝试一下:

string OPENAI_API_KEY = "sk-sssss...";

[Description("Get the current time")]
 string GetCurrentTime() => DateTime.Now.ToString();

 IChatClient client = new ChatClientBuilder()
     .UseFunctionInvocation()
     .Use(new OpenAIClient(OPENAI_API_KEY).AsChatClient(modelId: "gpt-4o-mini"));

 var response = client.CompleteStreamingAsync(
     "现在几点了?",
     new() { Tools = [AIFunctionFactory.Create(GetCurrentTime)] });

 await foreach (var update in response)
 {
     Console.Write(update);
 }

效果:

image-20241120101404123

成功获取到了当前的时间。

由于在国内使用OpenAI不方便,而且国内也有很多大模型提供商都是兼容OpenAI格式的,因此现在以国内的模型提供商为例,进行说明。

我以硅基流动为例,上面还有一些额度。

简单对话:

 OpenAIClientOptions openAIClientOptions = new OpenAIClientOptions();
 openAIClientOptions.Endpoint = new Uri("https://api.siliconflow.cn/v1");

 // SiliconCloud API Key
 string mySiliconCloudAPIKey = "sk-lll...";


 OpenAIClient client = new OpenAIClient(new ApiKeyCredential(mySiliconCloudAPIKey),  openAIClientOptions);
 IChatClient chatClient = client.AsChatClient("Qwen/Qwen2.5-72B-Instruct-128K");
 var response = await chatClient.CompleteAsync("你是谁?");
 Console.WriteLine(response.Message);

效果:

image-20241120101803488

函数调用:

 OpenAIClientOptions openAIClientOptions = new OpenAIClientOptions();
 openAIClientOptions.Endpoint = new Uri("https://api.siliconflow.cn/v1");

 // SiliconCloud API Key
 string mySiliconCloudAPIKey = "sk-lll...";

  [Description("Get the current time")]
  string GetCurrentTime() => DateTime.Now.ToString();

  IChatClient client = new ChatClientBuilder()
      .UseFunctionInvocation()
      .Use(new OpenAIClient(new ApiKeyCredential(mySiliconCloudAPIKey), openAIClientOptions).AsChatClient("Qwen/Qwen2.5-72B-Instruct-128K"));

  var response = await client.CompleteAsync(
      "现在几点了?",
      new() { Tools = [AIFunctionFactory.Create(GetCurrentTime)] });

  Console.Write(response);

image-20241120102258535

也成功进行函数调用,获取到了当前的时间。

会发现其实和SemanticKernel很像,Steve Sanderson也坦言这些是从SemanticKernel“毕业”的东西,更多用例可由读者自行探索。

Spring Data Redis 为我们提供了下面的Serializer:GenericToStringSerializer、Jackson2JsonRedisSerializer、JacksonJsonRedisSerializer、JdkSerializationRedisSerializer、OxmSerializer、StringRedisSerializer。
image

序列化方式对比:

  • JdkSerializationRedisSerializer: 使用JDK提供的序列化功能。 优点是反序列化时不需要提供类型信息(class),但缺点是需要实现Serializable接口,还有序列化后的结果非常庞大,是JSON格式的5倍左右,这样就会消耗redis服务器的大量内存。
  • Jackson2JsonRedisSerializer: 使用Jackson库将对象序列化为JSON字符串。优点是速度快,序列化后的字符串短小精悍,不需要实现Serializable接口。但缺点也非常致命,那就是此类的构造函数中有一个类型参数,必须提供要序列化对象的类型信息(.class对象)。 通过查看源代码,发现其只在反序列化过程中用到了类型信息。

使用 FastJson2 来做。重写一些序列化器,并实现RedisSerializer接口。源码如下:

<dependency>
    <groupId>com.alibaba.fastjson2</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.50</version>
</dependency>

Fastjson2 序列化

FastJson2JsonRedisSerializer

package com.vipsoft.base.util;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import com.alibaba.fastjson2.filter.Filter;

import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;

/**
 * Redis使用FastJson序列化
 * 
 * @author ruoyi
 */
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
{
    /**
     * 自动识别json对象白名单配置(仅允许解析的包名,范围越小越安全)
     */
    public static final String[] JSON_WHITELIST_STR = { "org.springframework", "com.vipsoft" };

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");

    static final Filter AUTO_TYPE_FILTER = JSONReader.autoTypeFilter(JSON_WHITELIST_STR);

    private Class<T> clazz;

    public FastJson2JsonRedisSerializer(Class<T> clazz)
    {
        super();
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException
    {
        if (t == null)
        {
            return new byte[0];
        }
        return JSON.toJSONString(t, JSONWriter.Feature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException
    {
        if (bytes == null || bytes.length <= 0)
        {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return (T)JSON.parseObject(str, clazz, AUTO_TYPE_FILTER);
    }
}

Redis.config

package com.vipsoft.base.config;

import com.cuwor.base.util.FastJson2JsonRedisSerializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@EnableCaching   //开启缓存功能,作用于缓存配置类上或者作用于springboot启动类上
@Configuration
public class RedisConfig {


    /**
     * 创建一个RedisTemplate实例,用于操作Redis数据库。
     * 其中,redisTemplate是一个泛型为<String, Object>的模板对象,可以存储键值对数据;
     * @param factory   factory是一个Redis连接工厂对象,用于建立与Redis服务器的连接
     * @return
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {

        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);

        // 使用StringRedisSerializer来序列化和反序列化redis的key值
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);

        // Hash的key也采用StringRedisSerializer的序列化方式
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);

        template.afterPropertiesSet();
        return template;
    }
}

RedisUtil.java

package com.vipsoft.base.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.util.*;
import java.util.concurrent.TimeUnit;

@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 判断key是否存在
     *
     * @param key 键
     * @return true 存在 false不存在
     */
    public boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }


    /**
     * 指定缓存失效时间
     *
     * @param key  键
     * @param time 时间(秒)
     */
    public boolean expire(String key, long time) {
        try {
            redisTemplate.expire(key, time, TimeUnit.MINUTES);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }


    /**
     * 获取 Keys
     *
     * @param key 键
     */
    public Set<String> keys(String key) {
        Set<String> result = new HashSet();
        try {
            result = redisTemplate.keys(key);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return result;
    }


    /**
     * 删除缓存
     */
    public boolean del(String key) {
        try {
            redisTemplate.delete(key);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    /**
     * 删除缓存
     */
    public boolean del(Collection<String> keys) {
        try {
            redisTemplate.delete(keys);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    //region String

    public <T> T get(String key) {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    public <T> boolean set(String key, T value) {
        try {

            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    /**
     * 普通缓存放入并设置时间
     *
     * @param key   键
     * @param value 值
     * @param time  时间(分种) time要大于0 如果time小于等于0 将设置无限期
     * @return true成功 false 失败
     */
    public boolean set(String key, Object value, long time) {
        try {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.MINUTES);
            return true;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return false;
    }

    //endregion

    //region Hash

    public <T> T hget(String key, String hashKey) {
        HashOperations<String, String, T> hash = redisTemplate.opsForHash();
        return hash.get(key, hashKey);
    }

    public <T> Map<String, T> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }


    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key     键
     * @param hashKey 项
     * @param value   值
     * @return true 成功 false失败
     */
    public boolean hset(String key, String hashKey, Object value) {
        redisTemplate.opsForHash().put(key, hashKey, value);
        return true;
    }

    /**
     * 向一张hash表中放入数据,如果不存在将创建
     *
     * @param key     键
     * @param hashKey 项
     * @param value   值
     * @param time    时间(分钟) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
     * @return true 成功 false失败
     */
    public boolean hset(String key, String hashKey, Object value, long time) {
        redisTemplate.opsForHash().put(key, hashKey, value);
        if (time > 0) {
            redisTemplate.expire(key, time, TimeUnit.MINUTES);
        }
        return true;
    }

    /**
     * HashSet
     *
     * @param key 键
     * @param map 对应多个键值
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        redisTemplate.opsForHash().putAll(key, map);
        return true;
    }

    /**
     * HashSet 并设置时间
     *
     * @param key  键
     * @param map  对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        redisTemplate.opsForHash().putAll(key, map);
        if (time > 0) {
            redisTemplate.expire(key, time, TimeUnit.MINUTES);
        }
        return true;
    }


    /**
     * 删除hash表中的值
     *
     * @param key     键 不能为null
     * @param hashKey 项 可以使多个 不能为null
     */
    public void hdel(String key, Object... hashKey) {
        redisTemplate.opsForHash().delete(key, hashKey);
    }

    /**
     * 判断hash表中是否有该项的值
     *
     * @param key     键 不能为null
     * @param hashKey 项 不能为null
     * @return true 存在 false不存在
     */
    public boolean hHasKey(String key, String hashKey) {
        return redisTemplate.opsForHash().hasKey(key, hashKey);
    }


    /**
     * hash递增 如果不存在,就会创建一个 并把新增后的值返回
     *
     * @param key  键
     * @param hashKey 项
     * @param by   要增加几(大于0)
     * @return
     */
    public double hincr(String key, String hashKey, double by) {
        return redisTemplate.opsForHash().increment(key, hashKey, by);
    }

    /**
     * hash递减
     *
     * @param key  键
     * @param hashKey 项
     * @param by   要减少记(小于0)
     * @return
     */
    public double hdecr(String key, String hashKey, double by) {
        return redisTemplate.opsForHash().increment(key, hashKey, -by);
    }


    //endregion

    //region List

    /**
	 * 获取list缓存的内容
	 * @param key 键
	 * @param start 开始
	 * @param end 结束 0 到 -1代表所有值
	 * @return
	 */
	public <T> List<T> lrange(String key, long start, long end) {
		return redisTemplate.opsForList().range(key, start, end);
	}

	/**
	 * 获取list缓存的长度
	 * @param key 键
	 * @return
	 */
	public long llen(String key) {
		return redisTemplate.opsForList().size(key);
	}

	/**
	 * 通过索引 获取list中的值
	 * @param key 键
	 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
	 * @return
	 */
	public Object lindex(String key, long index) {
		return redisTemplate.opsForList().index(key, index);
	}

	/**
	 * 将一个或多个值插入到列表的尾部(最右边)。
	 * @param key 键
	 * @param value 值
	 * @return
	 */
	public boolean lrpush(String key, Object value) {
		redisTemplate.opsForList().rightPush(key, value);
		return true;
	}

	/**
	 * 将一个或多个值插入到列表的尾部(最右边)。
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒)
	 * @return
	 */
	public boolean lrpush(String key, Object value, long time) {
		redisTemplate.opsForList().rightPush(key, value);
		if (time > 0) {
			expire(key, time);
		}
		return true;
	}

	/**
	 * 将一个或多个值插入到列表的尾部(最右边)。
	 * @param key 键
	 * @param value 值
	 * @return
	 */
	public boolean lrpush(String key, List<Object> value) {
		redisTemplate.opsForList().rightPushAll(key, value);
		return true;
	}

	/**
	 * 将list放入缓存
	 * @param key 键
	 * @param value 值
	 * @param time 时间(秒)
	 * @return
	 */
	public boolean lrpush(String key, List<Object> value, long time) {
		redisTemplate.opsForList().rightPushAll(key, value);
		if (time > 0) {
			expire(key, time);
		}
		return true;
	}

	/**
	 * 通过索引来设置元素的值
	 * @param key 键
	 * @param index 索引
	 * @param value 值
	 * @return /
	 */
	public boolean lset(String key, long index, Object value) {
		redisTemplate.opsForList().set(key, index, value);
		return true;
	}

	/**
	 * 删除指定key集合中值等于value的元素(count=0, 删除所有值等于value的元素; count>0, 从头部开始删除第一个值等于value的元素; count<0, 从尾部开始删除第一个值等于value的元素)
	 * @param key 键
	 * @param count  count>0, 从头部开始删除第一个值等于value的元素; count<0, 从尾部开始删除第一个值等于value的元素
	 * @param value 值
	 * @return 移除的个数
	 */
	public long lremove(String key, long count, Object value) {
		return redisTemplate.opsForList().remove(key, count, value);
	}

    //endregion
}


fastjson和fastjson2的区别

FastJSON是一种广泛使用的JSON解析库,其高性能和简单易用的特点受到开发者的喜爱。然而,随着应用场景的复杂化和安全要求的提高,FastJSON逐渐暴露出一些问题。为了解决这些问题并进一步提升性能和安全性,阿里巴巴推出了FastJSON2。以下是FastJSON和FastJSON2的详细对比:
一、性能
FastJSON:
以其快速高效著称,可以快速地将Java对象转换为JSON字符串,也可以将JSON字符串转换为Java对象。
FastJSON2:
在性能上进行了多方面的优化,通过优化序列化算法和数据结构,提高了序列化的效率,减少了不必要的对象创建和复制操作,从而降低了CPU和内存的开销。同时,对解析器进行了重构,使其能够更快速地处理大规模JSON数据。根据官方提供的性能测试数据,FastJSON2的性能远超其他流行的JSON库,如Jackson、Gson和org.json。

二、安全性
FastJSON:
在安全性方面存在一些漏洞,特别是其AutoType功能的安全性问题较为突出。该功能在序列化的JSON字符串中带上类型信息,在反序列化时不需要传入类型,实现自动类型识别,但这也为恶意攻击提供了可能。
FastJSON2:
在安全性方面做了显著改进,针对之前版本中暴露的一些安全漏洞进行了修复。为了防止反序列化漏洞,FastJSON2引入了更严格的黑名单机制,默认禁止了某些危险类型的反序列化操作。同时,增强了类型检测机制,可以更好地防止恶意数据的注入,确保数据解析的安全性。此外,FastJSON2的AutoType功能必须显式打开才能使用,且没有任何白名单,也不包括任何Exception类的白名单,这进一步提高了其安全性。
三、功能特性
FastJSON:
支持复杂对象的处理,包括嵌套对象、集合、Map等;支持注解来指定Java对象与JSON字符串之间的映射关系;允许用户自定义实现对象的序列化和反序列化过程;提供了流式的API,可以在处理大量JSON数据时提供更好的性能。
FastJSON2:
在FastJSON的基础上增加了一系列新的功能特性。例如,对JSONPath的支持更加全面和高效,允许开发者使用JSONPath表达式方便地访问和操作JSON数据;改进了对日期和时间的处理,提供了更多的配置选项和更好的性能,支持多种日期格式和时区处理;提供了对JSON Schema的支持,增强了数据验证的能力;新增加对二进制格式JSONB的支持,进一步提升了数据处理的效率和灵活性。
四、兼容性
FastJSON:
作为早期版本的JSON处理库,已经被广泛应用于各种Java项目中。
FastJSON2:
在保持与FastJSON兼容性的同时,也进行了一些必要的改进和优化。例如,FastJSON2支持最新的JDK特性,包括JDK 11和JDK 17等;同时提供了对Kotlin语言的优化支持,使得Kotlin开发者能够更加便捷地使用FastJSON2。此外,FastJSON2还支持跨平台兼容性,包括Android 8+等平台。
五、易用性
FastJSON:
提供了简单明了的API,使用起来非常方便。
FastJSON2:
对部分API进行了调整和优化,使其更加直观和易用。同时提供了更详细和友好的错误提示信息,帮助开发者更快地定位和解决问题。
综上所述,FastJSON2在性能、安全性、功能特性、兼容性和易用性等方面相比FastJSON都有显著的提升。对于需要高性能JSON解析和序列化的应用场景,特别是在安全性要求较高的情况下,FastJSON2是一个更好的选择。

公司SaaS系统有个给客户的员工发放金币,最后计算金币老是流水和总额对不上,以前负责这块的人做过修改还是不对,后来这负责人离职,接手大数据的事情后,该客户真在用金币这块业务,而且财务用这个结算对账,2023年底客户逼急了,要彻底解决这个问题:

和负责这块的产品经理沟通这块内容,说这个金币计算有历史原因导致流水和总额不对,不确定是谁不对,程序也有bug,但现在要重新计算对上,因不懂这块业务,产品经理最后确定原则:

1,每条金币日志流水中会保留当前的金币总数,用这个2022年12月31日的总额为准,把不对的总额改成当前的流水的总额

2,在按当天总额+后面的流水重新计算得到每天的期末和期初(金币余额)

持续多少年的问题,一直没解决,一听就头疼,尽接这样的难题,这里面的坑也不小:

后来查询金币报表的计算逻辑,同时按上面的思路做发现,不能按2的来做,因为到2022年12月31日批量计算后,和报表的总额有部分还是对不上。后来找了产品经理,他说他只是提供一下解决思路,具体怎么做自己想办法了!

整体解决方法如下:

1,SQL查出截止到2022年12月31日的最新的一个金币记录数(2023-01-01前)

2,对比金币数据,不一致的改成上面总额一致

3,通过计算金币流水的存储过程,重新计算从2023-01-01到2023-12-25的每天的金币明细

4,对比金币在2023-12-25的流水总额,不一致的查看不一致原因

5,发现有几十个账号的金币还是不一致, 期初是一致的,通过计算流水到2023-12-25就不一样,不得不一个账号查原因再计算

其中发现有程序bug:2个金币明细表的数据有同一条重复的数据,修改其中的一条金币数为0,还有金币数小于0的计算。

通过这样的笨办法,一个一个看数据,再修正数据到一致,修的眼睛都看花了,程序的bug导致提交后,有2条是一样的数据,让研发同学去改程序。

没想到2周后,客户发现又有账号的金币又对不上,自己手工重新计算后,对比发现就只有1个账号不对,修正好,再加上程序的修正,到现在2024年11月18日,客户再也没说金币不对的问题。

总结:

想想为何这么多年的问题一直没有彻底解决,是真的很难?现在看来,就是数据细心核对修正和程序bug修改,就OK,
真是事上无难事,只怕有心人

Hivgor脚本文件

在构建的生命周期中Hvigor使用两个脚本文件来完成插件、任务以及生命周期hook的注册:

  • hvigorconfig.ts
    :此文件在整个项目中只有根目录下存在一份,不是构建必须的文件并且默认不存在,如有需要可自行创建,此文件被解析执行的时间较早,可用于在Hvigor生命周期刚开始时操作某些数据。
  • hvigorfile.ts
    :此文件在每个node下都有一份,是构建的必须文件,在此文件中可以注册插件、任务以及生命周期hook等操作。

任务与任务依赖图

Hvigor是基于任务对您的项目进行自动化构建的,任务(Task)是Hvigor构建过程中的基本工作单元,它定义了构建项目时需要执行的具体工作。任务可以完成多种操作,比如源码编译任务,打包任务或签名任务等。每一种任务的执行逻辑由插件(plugin)提供,插件可以是由hvigor-ohos-plugin提供的默认任务逻辑,也可由您个性化定制。

需要注意的一点是,任务是存在依赖关系的,Hvigor在执行任何任务之前会构建任务依赖图,所有任务会形成一个有向无环图(DAG),如下示例图,任务之间的依赖关系用箭头进行表示:
img1
hvigor插件(hvigor-ohos-plugin)和hvigorfile.ts文件中的构建脚本都将通过任务依赖机制对任务依赖图做出影响。

hvigor-ohos-plugin

hvigor-ohos-plugin是默认的构建插件,为任务(Task)的完成提供业务逻辑支持,比如为Hvigor提供Hap、Har和Hsp打包服务等任务,每一种任务的具体执行逻辑由本模块中不同的插件来提供。

Hvigor与hvigor-ohos-plugin的关系

概述部分提到了Hvigor提供任务注册编排以及配置管理等任务管理机制,它负责控制任务的执行流程,但是并不包含每一个任务的具体业务逻辑,具体逻辑是由hvigor-ohos-plugin提供的。

Hvigor和hvigor-ohos-plugin的关系可以通过下图来说明,Hvigor接受任务的注册并编排任务执行顺序,并按照顺序依次调用hvigor-ohos-plugin中的插件来执行任务。如果您定制了自己的任务逻辑插件并将其注册,hvigor-ohos-plugin也会调用您的个性化插件来完成编译构建流程。

在Hvigor执行构建的过程中,hvigor-ohos-plugin会向Hvigor进行任务的注册,Hvigor会根据构建的任务执行有向图依次调用对应的插件来执行相应任务,在完成编译、打包、签名等一系列任务后,Hvigor也就正式完成了构建。
img2

Hvigor生命周期

生命周期展示了Hvigor编译构建系统如何进行一次完整的编译构建流程。Hvigor的编译构建过程有三个不同的阶段,分为初始化、配置和执行,Hvigor会按顺序运行这些阶段。

  • 初始化
    :此阶段主要目的为初始化项目的编译参数,构造出项目结构的树形数据模型,每个node为一个HvigorNode对象。
  • 配置
    :此阶段开始时,所有的node都已经加载完毕,但每个node中还没有加载插件(plugin)、任务(task)和DAG图,此阶段的主要目的就是加载出这些内容。
  • 执行
    :任务之间的依赖关系决定了任务执行顺序,任务可以并行执行

生命周期及hook点

在Hvigor的生命周期中,以下多个hook点可供您使用,便于您在对应的时机调用某些逻辑。在下图中所有绿色标记的线框为可以使用的hook点
img3