ASP.NET Core Blazor 核心功能三:Blazor与JavaScript互操作——让Web开发更灵活

thbcm阅读(142)

ASP.NET Core Blazor 核心功能三:Blazor与JavaScript互操作——让Web开发更灵活

嗨,大家好!我是码农刚子。今天我们来聊聊Blazor中C#与JavaScript互操作。我知道很多同学在听到”Blazor”和”JavaScript”要一起工作时会有点懵,但别担心,我会用最简单的方式带你掌握这个技能!

为什么要学JavaScript互操作?

想象一下:你正在用Blazor开发一个超棒的应用,但突然需要用到某个只有JavaScript才能实现的炫酷效果,或者要集成一个超好用的第三方JS库。这时候,JavaScript互操作就是你的救星!

简单来说,它让Blazor和JavaScript可以”握手合作”,各展所长。下面我们就从最基础的部分开始。

1. IJSRuntime – 你的JavaScript通行证

在Blazor中,IJSRuntime是与JavaScript沟通的桥梁。获取它超级简单:

@inject IJSRuntime JSRuntime

<button @onclick="ShowAlert">点我弹窗!</button>
@code {
    private async Task ShowAlert()
    {
        await JSRuntime.InvokeVoidAsync("alert", "Hello from Blazor!");
    }
}

就两行关键代码:

  • @inject IJSRuntime JSRuntime – 拿到通行证
  • InvokeVoidAsync – 调用不返回值的JS函数

实际场景:比如用户完成某个操作后,你想显示一个提示,用这种方式就特别方便。

2. 调用JavaScript函数 – 不只是简单弹窗

当然,我们不会只满足于弹窗。来看看更实用的例子:

首先,在wwwroot/index.html中添加我们的JavaScript工具函数:

<script>
    // 创建命名空间避免全局污染
    window.myJsHelpers = {
        showNotification: function (message, type) {
            // 模拟显示一个漂亮的提示框
            const notification = document.createElement('div');
            notification.style.cssText = `
                position: fixed;
                top: 20px;
                right: 20px;
                padding: 15px 20px;
                border-radius: 5px;
                color: white;
                z-index: 1000;
                transition: all 0.3s ease;
            `;
            
            if (type === 'success') {
                notification.style.backgroundColor = '#28a745';
            } else if (type === 'error') {
                notification.style.backgroundColor = '#dc3545';
            } else {
                notification.style.backgroundColor = '#17a2b8';
            }
            
            notification.textContent = message;
            document.body.appendChild(notification);
            
            // 3秒后自动消失
            setTimeout(() => {
                notification.remove();
            }, 3000);
        },
        
        getBrowserInfo: function () {
            return {
                userAgent: navigator.userAgent,
                language: navigator.language,
                platform: navigator.platform
            };
        },
        
        // 带参数的计算函数
        calculateDiscount: function (originalPrice, discountPercent) {
            return originalPrice * (1 - discountPercent / 100);
        }
    };
</script>

然后在Blazor组件中使用:

@inject IJSRuntime JSRuntime

<div class="demo-container">
    <h3>JavaScript函数调用演示</h3>
    
    <button @onclick="ShowSuccessNotification" class="btn btn-success">
        显示成功提示
    </button>
    
    <button @onclick="ShowErrorNotification" class="btn btn-danger">
        显示错误提示
    </button>
    
    <button @onclick="GetBrowserInfo" class="btn btn-info">
        获取浏览器信息
    </button>
    
    <button @onclick="CalculatePrice" class="btn btn-warning">
        计算折扣价格
    </button>
    @if (!string.IsNullOrEmpty(browserInfo))
    {
        <div class="alert alert-info mt-3">
            <strong>浏览器信息:</strong> @browserInfo
        </div>
    }

    @if (discountResult > 0)
    {
        <div class="alert alert-success mt-3">
            <strong>折扣价格:</strong> ¥@discountResult
        </div>
    }
</div>
@code {
    private string browserInfo = "";
    private decimal discountResult;

    private async Task ShowSuccessNotification()
    {
        await JSRuntime.InvokeVoidAsync("myJsHelpers.showNotification", 
            "操作成功!数据已保存。", "success");
    }

    private async Task ShowErrorNotification()
    {
        await JSRuntime.InvokeVoidAsync("myJsHelpers.showNotification", 
            "出错了!请检查网络连接。", "error");
    }

    private async Task GetBrowserInfo()
    {
        var info = await JSRuntime.InvokeAsync<BrowserInfo>("myJsHelpers.getBrowserInfo");
        browserInfo = $"语言: {info.Language}, 平台: {info.Platform}";
    }

    private async Task CalculatePrice()
    {
        discountResult = await JSRuntime.InvokeAsync<decimal>(
            "myJsHelpers.calculateDiscount", 1000, 20); // 原价1000,8折
    }

    // 定义接收复杂对象的类
    private class BrowserInfo
    {
        public string UserAgent { get; set; }
        public string Language { get; set; }
        public string Platform { get; set; }
    }
}

Note

  • 使用InvokeVoidAsync调用不返回值的函数
  • 使用InvokeAsync<T>调用有返回值的函数,记得指定返回类型
  • 复杂对象会自动序列化/反序列化

3. 把.NET方法暴露给JavaScript – 双向操作

有时候,我们也需要让JavaScript能调用我们的C#方法。这就用到[JSInvokable]特性了。

@inject IJSRuntime JSRuntime
@implements IDisposable

<div class="demo-container">
    <h3>.NET方法暴露演示</h3>
    
    <div class="mb-3">
        <label>消息内容:</label>
        <input @bind="message" class="form-control" />
    </div>
    
    <div class="mb-3">
        <label>重复次数:</label>
        <input type="number" @bind="repeatCount" class="form-control" />
    </div>
    
    <button @onclick="RegisterDotNetMethods" class="btn btn-primary">
        注册.NET方法给JavaScript使用
    </button>
    
    <div id="js-output" class="mt-3 p-3 border rounded">
        <!-- JavaScript会在这里输出内容 -->
    </div>
</div>
@code {
    private string message = "Hello from .NET!";
    private int repeatCount = 3;
    private DotNetObjectReference<MyComponent> dotNetHelper;

    protected override void OnInitialized()
    {
        dotNetHelper = DotNetObjectReference.Create(this);
    }

    private async Task RegisterDotNetMethods()
    {
        await JSRuntime.InvokeVoidAsync("registerDotNetHelper", dotNetHelper);
    }

    [JSInvokable]
    public string GetFormattedMessage()
    {
        return string.Join(" ", Enumerable.Repeat(message, repeatCount));
    }

    [JSInvokable]
    public async Task<string> ProcessDataAsync(string input)
    {
        // 模拟一些异步处理
        await Task.Delay(500);
        return $"处理后的数据: {input.ToUpper()} (处理时间: {DateTime.Now:HH:mm:ss})";
    }

    [JSInvokable]
    public void ShowAlert(string alertMessage)
    {
        // 这个方法会被JavaScript调用
        // 在实际应用中,你可能会更新组件状态或触发其他操作
        Console.WriteLine($"收到JavaScript的警告: {alertMessage}");
    }

    public void Dispose()
    {
        dotNetHelper?.Dispose();
    }
}

对应的JavaScript代码:

// 在index.html中添加
function registerDotNetHelper(dotNetHelper) {
    // 存储.NET引用供后续使用
    window.dotNetHelper = dotNetHelper;
    
    // 演示调用.NET方法
    callDotNetMethods();
}

async function callDotNetMethods() {
    if (!window.dotNetHelper) {
        console.error('.NET helper 未注册');
        return;
    }
    
    try {
        // 调用无参数的.NET方法
        const message = await window.dotNetHelper.invokeMethodAsync('GetFormattedMessage');
        
        // 调用带参数的异步.NET方法
        const processed = await window.dotNetHelper.invokeMethodAsync('ProcessDataAsync', 'hello world');
        
        // 调用void方法
        window.dotNetHelper.invokeMethodAsync('ShowAlert', '这是从JS发来的消息!');
        
        // 在页面上显示结果
        const output = document.getElementById('js-output');
        output.innerHTML = `
            <strong>来自.NET的消息:</strong> ${message}<br>
            <strong>处理后的数据:</strong> ${processed}
        `;
        
    } catch (error) {
        console.error('调用.NET方法失败:', error);
    }
}

Note

  • 记得使用DotNetObjectReference来创建引用
  • 使用Dispose()及时清理资源
  • 异步方法要返回TaskTask<T>

4. 使用JavaScript库 – 集成第三方神器

这是最实用的部分!让我们看看如何集成流行的JavaScript库。

示例:集成Chart.js图表库

首先引入Chart.js:

<!-- 在index.html中 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

创建图表辅助函数:

// 在index.html中或单独的JS文件
window.chartHelpers = {
    createChart: function (canvasId, config) {
        const ctx = document.getElementById(canvasId).getContext('2d');
        return new Chart(ctx, config);
    },
    
    updateChart: function (chart, data) {
        chart.data = data;
        chart.update();
    },
    
    destroyChart: function (chart) {
        chart.destroy();
    }
};

Blazor组件:

@inject IJSRuntime JSRuntime
@implements IDisposable

<div class="chart-demo">
    <h3>销售数据图表</h3>
    <canvas id="salesChart" width="400" height="200"></canvas>
    
    <div class="mt-3">
        <button @onclick="LoadSalesData" class="btn btn-primary">加载销售数据</button>
        <button @onclick="SwitchToProfitChart" class="btn btn-secondary">切换到利润图表</button>
    </div>
</div>
@code {
    private IJSObjectReference chartInstance;
    private bool isSalesData = true;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await InitializeChart();
        }
    }

    private async Task InitializeChart()
    {
        var config = new
        {
            type = "bar",
            data = new
            {
                labels = new[] { "一月", "二月", "三月", "四月", "五月" },
                datasets = new[]
                {
                    new
                    {
                        label = "销售额",
                        data = new[] { 65, 59, 80, 81, 56 },
                        backgroundColor = "rgba(54, 162, 235, 0.5)",
                        borderColor = "rgba(54, 162, 235, 1)",
                        borderWidth = 1
                    }
                }
            },
            options = new
            {
                responsive = true,
                plugins = new
                {
                    title = new
                    {
                        display = true,
                        text = "月度销售数据"
                    }
                }
            }
        };

        chartInstance = await JSRuntime.InvokeAsync<IJSObjectReference>(
            "chartHelpers.createChart", "salesChart", config);
    }

    private async Task LoadSalesData()
    {
        var newData = new
        {
            labels = new[] { "一月", "二月", "三月", "四月", "五月", "六月" },
            datasets = new[]
            {
                new
                {
                    label = "销售额",
                    data = new[] { 65, 59, 80, 81, 56, 75 },
                    backgroundColor = "rgba(54, 162, 235, 0.5)"
                }
            }
        };

        await JSRuntime.InvokeVoidAsync("chartHelpers.updateChart", 
            chartInstance, newData);
    }

    private async Task SwitchToProfitChart()
    {
        isSalesData = !isSalesData;
        
        var newData = isSalesData ? 
            new { 
                labels = new[] { "Q1", "Q2", "Q3", "Q4" },
                datasets = new[] {
                    new {
                        label = "销售额",
                        data = new[] { 100, 120, 110, 130 },
                        backgroundColor = "rgba(54, 162, 235, 0.5)"
                    }
                }
            } :
            new {
                labels = new[] { "Q1", "Q2", "Q3", "Q4" },
                datasets = new[] {
                    new {
                        label = "利润",
                        data = new[] { 30, 45, 35, 50 },
                        backgroundColor = "rgba(75, 192, 192, 0.5)"
                    }
                }
            };

        await JSRuntime.InvokeVoidAsync("chartHelpers.updateChart", 
            chartInstance, newData);
    }

    public async void Dispose()
    {
        if (chartInstance != null)
        {
            await JSRuntime.InvokeVoidAsync("chartHelpers.destroyChart", chartInstance);
        }
    }
}

常见问题与解决方案

问题1:JS互操作调用失败

症状:控制台报错,函数未定义

解决

try 
{
    await JSRuntime.InvokeVoidAsync("someFunction");
}
catch (JSException ex)
{
    Console.WriteLine($"JS调用失败: {ex.Message}");
    // 回退方案
    await JSRuntime.InvokeVoidAsync("console.warn", "功能不可用");
}

问题2:性能优化

对于频繁调用的JS函数,可以使用IJSInProcessRuntime

@inject IJSRuntime JSRuntime

@code {
    private IJSInProcessRuntime jsInProcess;

    protected override void OnInitialized()
    {
        jsInProcess = (IJSInProcessRuntime)JSRuntime;
    }

    private void HandleInput(ChangeEventArgs e)
    {
        // 同步调用,更高效
        jsInProcess.InvokeVoidAsync("handleInput", e.Value.ToString());
    }
}

问题3:组件销毁时资源清理

@implements IDisposable

@code {
    private DotNetObjectReference<MyComponent> dotNetRef;
    private IJSObjectReference jsModule;

    protected override async Task OnInitializedAsync()
    {
        dotNetRef = DotNetObjectReference.Create(this);
        jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
            "import", "./js/myModule.js");
    }

    public async void Dispose()
    {
        dotNetRef?.Dispose();
        
        if (jsModule != null)
        {
            await jsModule.DisposeAsync();
        }
    }
}

Blazor的JavaScript互操作其实没那么难的。记住这几个关键点:

  • IJSRuntime 是你的通行证
  • InvokeVoidAsyncInvokeAsync 是主要工具
  • [JSInvokable] 让.NET方法对JS可见
  • 及时清理资源 很重要

现在你已经掌握了Blazor与JavaScript互操作的核心技能!试着在自己的项目中实践一下,示例源码更放在仓库:https://github.com/shenchuanchao/BlazorApp/tree/master/BlazorAppWasm/Pages

​以上就是《ASP.NET Core Blazor 核心功能二:Blazor与JavaScript互操作——让Web开发更灵活》的全部内容,希望你有所收获。关注、点赞,持续分享 posted @
2025-11-05 21:34 
码农刚子  阅读(
149)  评论(
0)   
收藏 
举报

【Kubernetes】入门-部署Spring应用

thbcm阅读(177)

【Kubernetes】入门-部署Spring应用

Kubernetes

基本概念

Pod

是什么: Kubernetes 中最小的、最简单的部署和管理单元。

类比: 一台“逻辑主机”或一个“虚拟机实例”。

关键点

  • 一个 Pod 可以包含一个或多个紧密相关的容器(例如,一个应用容器和一个日志收集 sidecar 容器)。
  • 这些容器共享网络命名空间(拥有相同的 IP 地址和端口空间)、存储卷和其他资源。
  • Pod 是短暂的,会被频繁地创建和销毁

Deployment

是什么: 用于部署和管理 Pod 副本集的声明式定义。

解决的问题: 如何确保指定数量的、完全相同的 Pod 副本始终在运行?如何无缝更新应用(滚动更新)?如何在更新出问题时快速回滚?

关键点

  • 你定义一个“期望状态”(例如,需要运行 3 个 Nginx Pod),Deployment 控制器会确保实际状态始终匹配这个期望状态。

  • 它是实现无状态应用部署的首选控制器。

Service

是什么: 一个稳定的网络端点,用于访问一组 Pod。

解决的问题: Pod 是短暂的,IP 地址会变。客户端如何稳定地找到并访问这些动态变化的 Pod?

关键点

  • Service 提供一个固定的 虚拟 IP (ClusterIP) 和 DNS 名称。

  • 通过 标签选择器 (Label Selector) 来匹配一组 Pod,并将流量负载均衡到这些 Pod 上。

  • 常见的类型:

    • ClusterIP: 默认类型,仅在集群内部可访问。

    • NodePort: 在每个节点上开放一个静态端口,从而可以从集群外部访问。

    • LoadBalancer: 使用云提供商的负载均衡器,向外部公开服务

Volume (存储卷)

是什么: 允许 Pod 中的容器持久化存储数据。

解决的问题: 容器内部的文件系统是临时的,容器重启后数据会丢失。如何持久化保存数据(如数据库文件、日志)?

关键点

  • 存储卷的生命周期独立于 Pod。即使 Pod 被销毁,存储卷中的数据依然存在。

  • 支持多种后端存储类型:本地存储、云存储(如 AWS EBS、GCE PD)、NFS、分布式存储(如 Ceph)等。

Namespace (命名空间)

是什么: 在物理集群内部创建的虚拟集群,用于实现资源隔离和分组。

解决的问题: 在有多个团队或项目的大型集群中,如何避免资源(如 Pod、Service 名称)冲突?如何实现资源配额管理?

关键点

  • 默认有 default, kube-system (系统组件), kube-public 等命名空间。

  • 可以为每个命名空间设置资源配额,限制其能使用的 CPU、内存等资源总量。

ConfigMap & Secret

  • ConfigMap: 用于将非机密的配置数据(如配置文件、环境变量、命令行参数)与容器镜像解耦。让你可以不重写镜像就能改变应用配置。

  • Secret: 与 ConfigMap 类似,但专门用于存储敏感信息,如密码、OAuth 令牌、SSH 密钥。数据会以 Base64 编码(非加密,需额外措施保障安全)存储。

使用K8S部署Spring Boot应用

前置条件,docker和kubernetes已安装配置

参考:macOS上优雅运行Docker容器

准备一个Spring Boot应用

SpringK8sDemoApplication

@SpringBootApplication
@RestController
public class SpringK8sDemoApplication {

	private static Date firstTime;
	private static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

	public static void main(String[] args) {
		SpringApplication.run(SpringK8sDemoApplication.class, args);
	}

	@GetMapping("/")
	public String hello() {
		if (firstTime == null) {
			firstTime = new Date();
		}
		// 容器里看日志是否有负载均衡
		System.out.println("request in " + formatter.format(new Date()));
		return "Hello from Spring Boot on Kubernetes! first time: " + formatter.format(firstTime);
	}

	@GetMapping("/health")
	public String health() {
		return "OK";
	}
}

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.5.7</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>spring-k8s-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-k8s-demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>21</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>
	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

构建 Docker 镜像

Dockerfile

FROM eclipse-temurin:21-jre-alpine AS runtime

# 创建应用目录
WORKDIR /app

# 复制jar文件
COPY target/spring-k8s-demo-0.0.1-SNAPSHOT.jar app.jar

# 暴露端口
EXPOSE 8080

# 启动应用
ENTRYPOINT ["java", "-jar", "app.jar"]

build.sh

# 切换指定JDK版本(可选)
export JAVA_HOME=`/usr/libexec/java_home -v 21`

# 可替换mvn
mvnd clean package -DskipTests

# 构建镜像
docker build -t spring-k8s-demo:latest .

编写 k8s Deployment 文件

k8s-deployment.yaml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-app
spec:
  replicas: 2  # 运行 2 个 Pod 副本
  selector:
    matchLabels:
      app: spring-app
  template:
    metadata:
      labels:
        app: spring-app
    spec:
      containers:
      - name: spring-app
        image: spring-k8s-demo:latest
        ports:
        - containerPort: 8080   # 容器内暴露的端口
        livenessProbe:          # 存活探针
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 300
          periodSeconds: 10
        readinessProbe:         # 就绪探针
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 300
          periodSeconds: 10
        resources:              # 分配资源
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"

k8s-services.yaml

# service.yaml
apiVersion: v1
kind: Service
metadata:
  name: sprint-service
  labels:
    app: sprint-service
spec:
  type: NodePort
  selector:
    app: spring-app
  ports:
    - protocol: TCP
      port: 80
      targetPort: 8080
      nodePort: 30080

配置文件解释

  • Deployment:

    • replicas: 2:确保任何时候都有 2 个 Pod 在运行。
    • selector & labels:Deployment 通过 app: spring-app 这个标签来管理它创建的 Pod。
    • image:指定从哪个镜像运行容器。
    • resources:为容器设置 CPU 和内存的资源请求与限制,这是生产环境的最佳实践。
  • Service:

    • type: NodePort: 在每个节点上开放一个静态端口,从而可以从集群外部访问
    • selector: app: spring-app:Service 通过这个标签找到要代理的 Pod。
    • ports:将 Service 的 80 端口流量转发到 Pod 的 8080 端口。

部署到集群

应用配置

kubectl apply -f k8s-deployment.yaml
kubectl apply -f k8s-services.yaml

测试

  1. 查看pod状态
kubectl get pods

Pod 状态变为 Running

  1. 查看Deployment状态
kubectl get deployment
  1. 查看 Service
kubectl get service
  1. curl
curl http://192.168.64.3:30080/

根据时间,可以看到每次轮询到不同pod,也就是不同容器

注意:因为我使用是colima所以需要使用colima list命令查出来IP是192.168.64.3

k8s 常用命令

# 查看所有节点
kubectl get nodes

# 查看节点详情
kubectl describe node <node-name>

# 查看Pod
kubectl get pods

# 查看Pod详情
kubectl describe pod <pod-name>

# 查看Pod日志
kubectl logs <pod-name> -n <namespace>

# 查看单个 Pod 的日志
kubectl logs <pod-name>

# 查看部署
kubectl get deployments

# 查看部署详情
kubectl describe deployment <deployment-name> 

# 查看由 Deployment 创建的所有 Pod 的日志
kubectl logs -l app=spring-app

# 转发端口(用于本地测试)
kubectl port-forward svc/sprint-service 8080:80

# 水平扩缩容(将副本数从 2 增加到 3)
kubectl scale deployment spring-app --replicas=3

# 查看部署历史
kubectl rollout history deployment/<deployment-name>

# 滚动更新(例如,更新到新版本的镜像)
kubectl set image deployment/spring-app java-app=spring-k8s-demo:2.0.0

# 回滚到上一个版本
kubectl rollout undo deployment/<deployment-name>

# 删除部署
kubectl delete -f k8s-deployment.yaml
kubectl delete -f k8s-services.yaml

引用

https://github.com/WilsonPan/java-developer

例子: https://github.com/WilsonPan/java-developer/tree/main/samples/spring-demo

posted @
2025-11-06 00:03 
WilsonPan  阅读(
20)  评论(
0)   
收藏 
举报



刷新页面
返回顶部

让 AI 记住我家狗叫「十六」,原来只需要 5 分钟

thbcm阅读(136)

让 AI 记住我家狗叫「十六」,原来只需要 5 分钟

现在用 AI,最怕的不是它不会,而是它不记得

比如,我家的狗叫「十六」。

前一天,我刚手把手(打字)教会它“十六是我家狗”,还分享了三小时宠物日常。过两天打开新对话,我兴冲冲地发了句:“十六昨天终于学会游泳了!”结果,它用那种最标准的 AI 腔调回我:“很抱歉,您提到的「十六」是指什么?”

兄弟们,那一刻我的血压“噌”就上来了。这就是现实版的“赛博背叛”啊?

一、AI 为啥总是“金鱼记忆”?

虽然如今的 AI 已具备短期记忆,却难以真正理解用户——这几乎是当下所有 AI 用户的核心痛点。其根本原因,正是模型缺乏长期记忆能力。

这也是为什么业界都在卷记忆:从 OpenAI 的长期记忆实验,到各类 Memory Agent 框架,大家都在试图解决如何让模型记住用户这件事。

要理解这个问题,我们得从模型的记忆架构说起。传统 LLM 的记忆机制主要分两类:

  • 参数记忆(Parametric Memory):“焊”在模型权重里的知识,更新极其困难。
  • 激活记忆(Activation Memory):对话时临时的上下文缓存,纯“日抛”记忆,会话结束当场失忆。

这意味着,模型在生成回答时,所能“看到”的信息完全取决于上下文窗口的大小。当上下文过长时,计算量将以平方级增长。为了控制计算成本,模型会自动遗忘较早的内容。于是,LLM 能和你聊逻辑,它却记不住你家狗的名字。能总结论文,却记不住你昨天说过的喜好。

更为严重的问题是幻觉——当模型缺乏连续上下文时,会自动补全缺失的信息,从而生成看似合理但错误的回答。

过去大家用 RAG(检索增强生成)或外部数据库来缓解这个问题,但那些方案大多是临时记忆:能查能存,却不懂何时调取、何时更新,更谈不上长期记忆。

二、让记忆成为一种系统资源:MemOS 登场

直到前段时间,我在 GitHub 上刷到一个开源项目——MemOS 专为 LLM 设计的长期记忆系统。

GitHub 地址:github.com/MemTensor/MemOS

在了解 MemOS 之前,我一直认为 AI 记忆系统只是一堆“补丁”,开发者不得不反复折腾嵌入、检索和更新等繁琐的逻辑。而 MemOS 的理念,是把“记忆”抽象为系统级能力——你只需告诉它要记住的内容,其余都由系统自动处理。

这听起来就像人脑的记忆机制:我们不是刻意存储信息,而是让重要的东西自然被记住。MemOS 就是在让 AI 拥有这种类人式的长期记忆能力

三、记忆系统,如何“自己思考”?

我扒了扒它的文档,发现 MemOS 的记忆架构设计确实有点“东西”。

它并非简单地将聊天记录存入数据库,而是在每一条记忆之间建立关联——类似维基百科的超链接。它参考了操作系统的分层思想,将记忆系统划分为三层。

当我和 AI 聊到“十六”,系统不只是存下“十六 = 狗”这条信息,而是还会同时捕捉到对话的时间、情绪、上下文主题,并自动在它们之间建立语义链接。这样,下次你提“带十六去看兽医”,AI 就能主动推断出「十六」是那只宠物,而不是把它当作一个数字。

换句话说,MemOS 是让 AI “长记性”的操作系统。它不仅存数据,还能在推理中动态调度、复用和更新这些记忆。

更智能的是,系统会自动修剪和优化这张记忆图谱。当某些信息(比如「十六」)被频繁使用时,MemOS 会提高其优先级;反之则逐渐淡化。久而久之,系统会形成一张动态演化的记忆图谱,不断重塑、筛选、连接。

这意味着,AI 不再是死记硬背的机器,而是会“取舍”的伙伴。

四、从 Prompt 到 Context:记忆驱动的上下文工程

有了记性,还得会“用”才行。这就引出了上下文工程 这个核心技术。

  • 传统 Prompt 工程:依赖“人工填鸭”,手动拼接提示词模板。

  • MemOS 模式:Prompt 不再是一次性的提示词输入,而是一个动态上下文重构的过程。

模型每次生成前,MemOS 会根据任务意图自动“三连”:

  1. 检索: 从记忆库中查出相关内容;
  2. 评估: 评估上下文权重;
  3. 注入: 将结果组织成结构化上下文并注入模型输入。

比如向 AI 提问:帮我查查上次说的那个”Redis 优化方案”。MemOS 会立刻自动找到历史记录,组合为上下文。

[2024-10-10] Redis 优化建议: 
1. 使用 pipeline 批量命令;
2. 调整内存回收策略;
3. 添加监控指标。

再交给模型生成回答。这不仅极大减少了幻觉概率,也让推理更加稳健,具备了记忆连续性。

五、三步让 AI 记住「十六」

MemOS 已经为 AI 应用开发的开发者,提供了完善的 API 与 SDK,并且可以无缝集成主流智能体框架(如 Dify、LangGraph、Coze 等)。

以 Dify 为例,接入流程非常简单:

1.配置节点
在 Dify 的 ChatFlow 中添加两个 HTTP 节点:

  • /product/search:检索历史记忆
  • /product/add:写入新记忆

2.连接逻辑节点

  • search 的输出作为模型输入前的“上下文扩展”
  • 将模型的生成结果通过 add 节点写回 MemOS

3.示例代码

# 添加记忆
curl -X POST "http://127.0.0.1:8002/product/add" \
-H "Content-Type: application/json" \
-d '{
  "messages": [
    {"role": "user", "content": "十六是我家的狗"},
    {"role": "assistant", "content": "明白了,十六是你的狗狗!"}
  ],
  "mem_cube_id": "user_001"
}'

# 检索记忆
curl -X POST "http://127.0.0.1:8002/product/search" \
-H "Content-Type: application/json" \
-d '{
  "mem_cube_id":"user_001",
  "query":"十六是谁",
  "top_k":3
}'

几行代码,就能让 Dify 应用真正“记住”我家的狗「十六」!然后我还测试了一下,让它记住「十六」的生日,然后一周后再问它:“下周有什么要记得的事吗?”

它回答:“「十六」的生日要到了,别忘了准备小蛋糕。”

老实说,那一刻,我真有点被“治愈”到了。因为这不只是记忆被保存了,而是语境、情感和关系被理解并延续了

这就是记忆的力量,让 AI 从一个工具变成了伙伴

六、写在最后

人们常说“大模型的能力边界在模型之外”,而记忆,特别是模型长期记忆,就是让 AI 突破边界的关键一步

我认为,AI 不缺知识,也不缺算力,真正缺的是「连续性」——一种能跨越会话、理解上下文、记住人和关系的能力。

这,正是 MemOS 这样的 AI 记忆系统试图重塑的核心方向。

GitHub 地址:github.com/MemTensor/MemOS

或许在不久的未来,我们不会再说「和 AI 对话」,而是说「与一个真正记得你的智能体交流」。

因为到那时,AI 就不会再忘记 「十六」 的名字了。

作者:削微寒
扫描左侧的二维码可以联系到我

本作品采用署名-非商业性使用-禁止演绎 4.0 国际 进行许可。

posted @
2025-11-06 08:18 
削微寒  阅读(
273)  评论(
3)   
收藏 
举报

AI提效这么多,为什么不试试自己开发N个产品呢?

thbcm阅读(126)

AI提效这么多,为什么不试试自己开发N个产品呢?

前言

在这个AI恐怖的提效时代,很多同学应该都冒出过这样的想法:在AI的帮助下,这么快就做一个产品,为什么不尝试在业余时间自己做一堆产品,反正成本就只有自己的时间。万一有一个产品成功了,可能就不需要打工了。

加入欧阳的AI交流群

具像化需求从哪里来?

思考一下在以前,一个模糊的产品想法是如何落地为具体的产品呢?

  • 某天灵感一现有个模糊的产品idea
  • 找一个产品角色将这个模糊的产品idea具象为具体的产品需求
  • 找一个程序员将这些具体的产品需求开发出来

在以前的流程中有两个主要的角色:产品经理程序员

“一个模糊的产品idea”和“将具体的产品需求开发出来”这两个任务我们程序员都能完成,但是将模糊的产品idea具象为具体的产品需求,应该是绝大部分的程序员都做不到的。

既然我们程序员做不到,那么可以利用AI将模糊的产品idea具象为具体的产品需求吗?

答案是:可以的。

AI具象化产品需求

可以将你的模糊想法丢给AI,然后和AI进行需求讨论,我一般是这样做的:

  • 把我的模糊产品idea告诉AI
  • 和AI讨论出有哪些页面,这些页面的大概功能有哪些
  • 再来讨论每个页面里面的具体功能

这里有几个痛点:

  • AI的输出是纯文字的,太抽象了,只能通过脑子去想象AI描述的画面。如果想要看到这些UI,那么我就只有通过coding去将这些页面实现出来,但是经常实现出来后发现这并不是我想要的,导致很多次无效的coding。
  • 产品涉及到很多页面,和AI进行文字沟通产品的流程经常理解都不清楚。期望有个可视化的工作流工具直接“拖拖改改”就更直观了

欧阳最近发现一个AI工具uxbot,可以完美的解决我的这些痛点。

codex是将具体需求落地为产品的AI工具,uxbot是将模糊idea落地为具体需求的AI工具。

这个是uxbot的链接:https://www.uxbot.cn?utm_source=39cfdca6ed4c60d1&utm_medium=influencer&utm_influencer=f7fb1e9e05cf46f0

想做一个语音克隆的产品

从模糊idea到落地产品,AI工具已经可以贯穿全流程了。

比如我想做一个“语音克隆的产品”,我就只有这么一个模糊的idea,具体怎么做我也不知道。

把这个idea告诉给uxbot

1分钟后,他直接就给我生成了一个可视化的产品工作流:

这一步他直接就帮我生成了有哪些页面,这些页面的页面流转是什么样的,以及这些页面的功能是什么样的。

生成的这些页面以及页面的功能点看着还不错。

页面的功能如果不满意,直接点击卡片就可以修改页面的功能。

对页面的跳转不满意,直接通过拖拽就可以将改变页面的跳转。

想要新增页面也可以点击按钮新增一个页面就行。

理清楚了这个产品有哪些页面,以及这些页面大概有哪些功能之后,接下来就需要看看这些页面到底长什么样。

点击生成产品页面按钮就可以生成这些产品页面的预览了:

在生成页面时可以选择页面的风格,然后下方输入框就会生成风格对应的内置提示词,你也可以在这些提示词的基础上进行修改。

点击生成产品页面按钮这次就会生成最后的产品预览页面,这一步几分钟就完成了:

已经帮我生成了所有预览页面,并且这些页面就像是真实页面一样也是可以点击进行页面跳转的。

如果你对某个页面不满意,重新给他一个新的页面描述,然后点击重新生成按钮就可以生成新的页面。

还有一个AI助手功能也挺有用的,直接给他一个指令就可以让他帮你对页面进行微调,比如我这里想将“情感模拟”模块的背景色改为蓝色,直接给他说就行了:

如果你想手动微调页面也可以,选中你想修改的部分,直接改就行了,整个页面还支持可视化编辑的功能。

比如我想修改“声音的边界,从此被改写”这个文案和颜色,直接选中这个文案就可以修改了:

除了可以修改选中内容的样式,还可以设计交互,基本就是可视化编程了。

还有一个更好用的功能就是“配图”,比如生成的页面有这样的一个模块:

点击右边的“为当前界面配图”按钮,等待几分钟就帮我连页面的图都生成好了:

最后就是点击右上角的 “导出” 按钮将其导出:

这里可以选择hmtl或者Sketch进行导出,我选择的是html。

最后一步就是回到AI编辑器中,我这里是Codex:

这一步我们将uxbot生成的html文件丢改Codex,让他将这些html代码转换为nextjs代码就行了。

总结

uxbot + Codex这两个AI工具覆盖了从模糊想法到具体产品落地的全流程,根据uxbot团队透露他们最近要支持直接导出前端代码,这样连codex都可以省去了。最后附上uxbot的链接:https://www.uxbot.cn?utm_source=39cfdca6ed4c60d1&utm_medium=influencer&utm_influencer=f7fb1e9e05cf46f0

posted @
2025-11-06 08:24 
前端欧阳  阅读(
307)  评论(
1)   
收藏 
举报

关于AI编程能力对程序员的职业影响

thbcm阅读(173)

关于AI编程能力对程序员的职业影响

关于AI编程能力对程序员的职业影响

如今对于AI的很多媒体炒作(包括自媒体),在技术方面是有根本性错误的,例如,最近经常有人在嘲笑“老一辈说洗衣机不如手洗,这一辈说AI编程不如手写代码”之类的。这种行为的麻烦之处在于他们对问题本身都描述得不清楚,还要再加上几个自以为是的,不伦不类的比喻,根本是在搞乱一个旧问题的同时,制造一个新问题(制造不必要的对立)。在Twitter/X或微博上,我们确实经常会看到有人在不懂装懂嘲笑别人不了解人工神经网络,然后自己没有半个推文在科普这方面知识的,基本上是在炒作概念。贩卖焦虑。

那么在技术方面上,我们该如何分辨一个人所说的话是在炒作概念还是科普技术呢?较为简单的方法是:

  • 如果这个人说话,是在让你热血沸腾,很激动或很焦虑,不是想创业就是想放弃,wow,xxx时代来了,xxx要过时了,xxx要被淘汰了,这大概率是在炒作。
  • 如果这个人说话,能让你顺着他的用词一个一个查,且越查越知道自己要学啥,他通常是真的懂这项技术,并且在做科普。

另外,如果有人跟你说,他常常和一堆大牛线下聊AI技术。大概率情况不外乎两种:一,这堆人中只有一个大牛,他讲课来的。二,这堆人中至少有一个投资,其他人是创业找钱来的。一堆技术大牛在一起,聊啥都比聊技术可能性大。说真的,技术就不该让人热血沸腾。

对于文科生或对编程逻辑不太熟悉的人来说,学点vibe coding,确实能在产品设计阶段更好地和开发沟通,减少沟通成本,这是值得鼓励的。但作为技术人员,整天折腾提示词和大模型客户端,就敢出来炒概念、贩卖焦虑,这就太离谱了。靠几句 prompt 就想代表“AI编程能力”,这不是创新,是偷懒。技术不是拿来表演的,别把不学无术当成新趋势。

具体到技术上来说,现在大家所说的AI,大多数都属于大语言模型(LLM),这基本上是一种以自然语言处理(NLP)技术为核心的产品。虽然这类产品如今得到了多模态的扩展,多模态的模型可能在某程度上整合了自然语言处理、计算机视觉及语音处理等方面,但它在编程领域所能做到的只是将自然语言”翻译”成编程语言,而不是直接编译程序或进行解释执行。更精确地说,它所做的工作是一种统计式的语义生成,而非具备程序语义理解的编译过程。所以,它能否编程,实际上取决于自然语言是否足以描述一个产品级程序。那这可行吗?显然是不行的。在编程领域,自然语言所能起到最大的作用,是描述一个产品的需求,设计的idea。而且即便如此,也要求描述者至少要了解程序的架构,例如C/S,MVC。这些都是架构师的活,

换而言之,我们怎么能幻想让一个核心功能为NLP的系统学会如何构建软件架构?它最多只能把一个初步的设计翻译成一个demo罢了。即使是科幻,阿西莫夫也在他的小说中反复强调,机器人三大定律用自然语言来描述只需要三句话,但在机器人的正子脑中,对应的是数以亿计的量子决策分支,更何况如今的计算机离他所描述的正子脑还远得很。浪漫和信仰都可以理解,但做技术,不能哗众取宠,取悦流量,图个热闹和自嗨。

说真的,无论媒体或者那些网红把LLM吹成什么样子,作为技术人员,我们至少要始终清楚,这类产品聚焦的只是以NLP技术为核心的领域,这只能让机器“听懂”人类的idea,但不等于它能系统性地解决人类的问题。在我个人看来,AI编程能力在可以预见的短期未来,给程序员带来的直接影响大约有三个方面。

  1. 大幅度增加提示词工程师职位,用于产品的最初设计以及demo开发。
  2. 大幅度增加测试岗位,用于产品的质量保证以及快速迭代。
  3. 大幅度增加维护岗位,用于应对更为陡峭的新产品维护曲线。

最后,我想说的是:AI的编程能力不只是对不懂编程的新人不友好(如果只会一些提示词的话),对一堆想依赖ai创业的人会更不友好。因为,AI的编程能力目前最大的作用其实是把一个idea快速变成demo,但是从demo变成真正的产品,恐怕门槛是提高了,而不是降低了,因为由LLM所生成的编程语言编码通常缺乏可维护性、稳定性和架构一致性,导致后期的重构成本上升。如果创业者迷信AI,意识不到这些,他的投资是有很大风险的。

posted on 2025-11-06 11:12  凌杰  阅读(0)  评论(0)    收藏  举报

我发现很多程序员都不会打日志。。。

thbcm阅读(164)

我发现很多程序员都不会打日志。。。

日志不是写给机器看的,是写给未来的你和你的队友看的!

你是小阿巴,刚入职的低级程序员,正在开发一个批量导入数据的程序。

没想到,程序刚上线,产品经理就跑过来说:小阿巴,用户反馈你的程序有 Bug,刚导入没多久就报错中断了!

你赶紧打开服务器,看着比你发量都少的报错信息:

你一脸懵逼:只有这点儿信息,我咋知道哪里出了问题啊?!

你只能硬着头皮让产品经理找用户要数据,然后一条条测试,看看是哪条数据出了问题……

原本大好的摸鱼时光,就这样无了。

这时,你的导师鱼皮走了过来,问道:小阿巴,你是持矢了么?脸色这么难看?

你无奈地说:皮哥,刚才线上出了个 bug,我花了 8 个小时才定位到问题……

鱼皮皱了皱眉:这么久?你没打日志吗?

你很是疑惑:谁是日志?为什么要打它?

鱼皮叹了口气:唉,难怪你要花这么久…… 来,我教你打日志!

⭐️ 本文对应视频版:https://bilibili.com/video/BV1K71yBUEDv

 

什么是日志?

鱼皮打开电脑,给你看了一段代码:

@Slf4j
public class UserService {
   public void batchImport(List<UserDTOuserList) {
       log.info("开始批量导入用户,总数:{}", userList.size());
       
       int successCount 0;
       int failCount 0;
       
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在导入用户:{}", userDTO.getUsername());
               validateUser(userDTO);
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 导入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       
       log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
  }
}

你看着代码里的 log.infolog.error,疑惑地问:这些 log 是干什么的?

鱼皮:这就是打日志。日志用来记录程序运行时的状态和信息,这样当系统出现问题时,我们可以通过日志快速定位问题。

你若有所思:哦?还可以这样!如果当初我的代码里有这些日志,一眼就定位到问题了…… 那我应该怎么打日志?用什么技术呢?

 

怎么打日志?

鱼皮:每种编程语言都有很多日志框架和工具库,比如 Java 可以选用 Log4j 2、Logback 等等。咱们公司用的是 Spring Boot,它默认集成了 Logback 日志框架,你直接用就行,不用再引入额外的库了~

日志框架的使用非常简单,先获取到 Logger 日志对象。

1)方法 1:通过 LoggerFactory 手动获取 Logger 日志对象:

public class MyService {
   private static final Logger logger LoggerFactory.getLogger(MyService.class);
}

2)方法 2:使用 this.getClass 获取当前类的类型,来创建 Logger 对象:

public class MyService {
   private final Logger logger LoggerFactory.getLogger(this.getClass());
}

然后调用 logger.xxx(比如 logger.info)就能输出日志了。

public class MyService {
   private final Logger logger LoggerFactory.getLogger(this.getClass());

   public void doSomething() {
       logger.info("执行了一些操作");
  }
}

效果如图:

 

小阿巴:啊,每个需要打日志的类都要加上这行代码么?

鱼皮:还有更简单的方式,使用 Lombok 工具库提供的 @Slf4j 注解,可以自动为当前类生成日志对象,不用手动定义啦。

@Slf4j
public class MyService {
   public void doSomething() {
       log.info("执行了一些操作");
  }
}

上面的代码等同于 “自动为当前类生成日志对象”:

private static final org.slf4j.Logger log 
   org.slf4j.LoggerFactory.getLogger(MyService.class);

 

你咧嘴一笑:这个好,爽爽爽!

等等,不对,我直接用 Java 自带的 System.out.println 不也能输出信息么?何必多此一举?

System.out.println("开始导入用户" user.getUsername());

 

鱼皮摇了摇头:千万别这么干!

首先,System.out.println 是一个同步方法,每次调用都会导致耗时的 I/O 操作,频繁调用会影响程序的性能。

而且它只能输出信息到控制台,不能灵活控制输出位置、输出格式、输出时机等等。比如你现在想看三天前的日志,System.out.println 的输出早就被刷没了,你还得浪费时间找半天。

 

你恍然大悟:原来如此!那使用日志框架就能解决这些问题吗?

鱼皮点点头:没错,日志框架提供了丰富的打日志方法,还可以通过修改日志配置文件来随心所欲地调教日志,比如把日志同时输出到控制台和文件中、设置日志格式、控制日志级别等等。

在下苦心研究日志多年,沉淀了打日志的 8 大邪修秘法,先传授你 2 招最基础的吧。

 

打日志的 8 大最佳实践

1、合理选择日志级别

第一招,日志分级。

你好奇道:日志还有级别?苹果日志、安卓日志?

鱼皮给了你一巴掌:可不要乱说,日志的级别是按照重要程度进行划分的。

其中 DEBUG、INFO、WARN 和 ERROR 用的最多。

  • 调试用的详细信息用 DEBUG

  • 正常的业务流程用 INFO

  • 可能有问题但不影响主流程的用 WARN

  • 出现异常或错误的用 ERROR

log.debug("用户对象的详细信息:{}", userDTO);  // 调试信息
log.info("用户 {} 开始导入", username);  // 正常流程信息
log.warn("用户 {} 的邮箱格式可疑,但仍然导入", username);  // 警告信息
log.error("用户 {} 导入失败", username, e);  // 错误信息

 

你挠了挠头:俺直接全用 DEBUG 不行么?

鱼皮摇了摇头:如果所有信息都用同一级别,那出了问题时,你怎么快速找到错误信息?

在生产环境,我们通常会把日志级别调高(比如 INFO 或 WARN),这样 DEBUG 级别的日志就不会输出了,防止重要信息被无用日志淹没。

你点点头:俺明白了,不同的场景用不同的级别!

 

2、正确记录日志信息

鱼皮:没错,下面教你第二招。你注意到我刚才写的日志里有一对大括号 {} 吗?

log.info("用户 {} 开始导入", username);

你回忆了一下:对哦,那是啥啊?

鱼皮:这叫参数化日志。{} 是一个占位符,日志框架会在运行时自动把后面的参数值替换进去。

你挠了挠头:我直接用字符串拼接不行吗?

log.info("用户 " username " 开始导入");

鱼皮摇摇头:不推荐。因为字符串拼接是在调用 log 方法之前就执行的,即使这条日志最终不被输出,字符串拼接操作还是会执行,白白浪费性能。

 

你点点头:确实,而且参数化日志比字符串拼接看起来舒服~

 

鱼皮:没错。而且当你要输出异常信息时,也可以使用参数化日志:

try {
   // 业务逻辑
catch (Exception e) {
   log.error("用户 {} 导入失败", username, e);  // 注意这个 e
}

这样日志框架会同时记录上下文信息和完整的异常堆栈信息,便于排查问题。

你抱拳:学会了,我这就去打日志!

 

3、把控时机和内容

很快,你给批量导入程序的代码加上了日志:

@Slf4j
public class UserService {
   public BatchImportResult batchImport(List<UserDTOuserList) {
       log.info("开始批量导入用户,总数:{}", userList.size());
       int successCount 0;
       int failCount 0;
       for (UserDTO userDTO : userList) {
           try {
               log.info("正在导入用户:{}", userDTO.getUsername());   
               // 校验用户名
               if (StringUtils.isBlank(userDTO.getUsername())) {
                   throw new BusinessException("用户名不能为空");
              }
               // 保存用户
               saveUser(userDTO);
               successCount++;
               log.info("用户 {} 导入成功", userDTO.getUsername());
          } catch (Exception e) {
               failCount++;
               log.error("用户 {} 导入失败,原因:{}", userDTO.getUsername(), e.getMessage(), e);
          }
      }
       log.info("批量导入完成,成功:{},失败:{}", successCount, failCount);
       return new BatchImportResult(successCount, failCount);
  }
}

 

光做这点还不够,你还翻出了之前的屎山代码,想给每个文件都打打日志。

 

但打着打着,你就不耐烦了:每段代码都要打日志,好累啊!但是不打日志又怕出问题,怎么办才好?

鱼皮笑道:好问题,这就是我要教你的第三招 —— 把握打日志的时机。

对于重要的业务功能,我建议采用防御性编程,先多多打日志。比如在方法代码的入口和出口记录参数和返回值、在每个关键步骤记录执行状态,而不是等出了问题无法排查的时候才追悔莫及。之后可以再慢慢移除掉不需要的日志。

 

你叹了口气:这我知道,但每个方法都打日志,工作量太大,都影响我摸鱼了!

鱼皮:别担心,你可以利用 AOP 切面编程,自动给每个业务方法的执行前后添加日志,这样就不会错过任何一次调用信息了。

 

你双眼放光:这个好,爽爽爽!

 

鱼皮:不过这样做也有一个缺点,注意不要在日志中记录了敏感信息,比如用户密码。万一你的日志不小心泄露出去,就相当于泄露了大量用户的信息。

你拍拍胸脯:必须的!

 

4、控制日志输出量

一个星期后,产品经理又来找你了:小阿巴,你的批量导入功能又报错啦!而且怎么感觉程序变慢了?

你完全不慌,淡定地打开服务器的日志文件。结果瞬间呆住了……

好家伙,满屏都是密密麻麻的日志,这可怎么看啊?!

鱼皮看了看你的代码,摇了摇头:你现在每导入一条数据都要打一些日志,如果用户导入 10 万条数据,那就是几十万条日志!不仅刷屏,还会影响性能。

你有点委屈:不是你让我多打日志的么?那我应该怎么办?

鱼皮:你需要控制日志的输出量。

1)可以添加条件来控制,比如每处理 100 条数据时才记录一次:

if ((1) 100 == 0) {
   log.info("批量导入进度:{}/{}", 1, userList.size());
}

2)或者在循环中利用 StringBuilder 进行字符串拼接,循环结束后统一输出:

StringBuilder logBuilder new StringBuilder("处理结果:");
for (UserDTO userDTO : userList) {
   processUser(userDTO);
   logBuilder.append(String.format("成功[ID=%s], ", userDTO.getId()));
}
log.info(logBuilder.toString());

3)还可以通过修改日志配置文件,过滤掉特定级别的日志,防止日志刷屏:

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
   <file>logs/app.log</file>
   <!-- 只允许 INFO 级别及以上的日志通过 -->
   <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
       <level>INFO</level>
   </filter>
</appender>

 

5、统一日志格式

你开心了:好耶,这样就不会刷屏了!但是感觉有时候日志很杂很乱,尤其是我想看某一个请求相关的日志时,总是被其他的日志干扰,怎么办?

鱼皮:好问题,可以在日志配置文件中定义统一的日志格式,包含时间戳、线程名称、日志级别、类名、方法名、具体内容等关键信息。

<!-- 控制台日志输出 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
   <encoder>
       <!-- 日志格式 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
   </encoder>
</appender>

这样输出的日志更整齐易读:

 

此外,你还可以通过 MDC(Mapped Diagnostic Context)给日志添加额外的上下文信息,比如请求 ID、用户 ID 等,方便追踪。

在 Java 代码中,可以为 MDC 设置属性值:

@PostMapping("/user/import")
public Result importUsers(@RequestBody UserImportRequest request) {
   // 1. 设置 MDC 上下文信息
   MDC.put("requestId", generateRequestId());
   MDC.put("userId", String.valueOf(request.getUserId()));
   try {
       log.info("用户请求处理完成");      
       // 执行具体业务逻辑
       userService.batchImport(request.getUserList());     
       return Result.success();
  } finally {
       // 2. 及时清理MDC(重要!)
       MDC.clear();
  }
}

然后在日志配置文件中就可以使用这些值了:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
   <encoder>
       <!-- 包含 MDC 信息 -->
       <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{requestId}] [%X{userId}] %msg%n</pattern>
   </encoder>
</appender>

这样,每个请求、每个用户的操作一目了然。

 

6、使用异步日志

你又开心了:这样打出来的日志,确实舒服,爽爽爽!但是我打日志越多,是不是程序就会更慢呢?有没有办法能优化一下?

鱼皮:当然有,可以使用 异步日志

正常情况下,你调用 log.info() 打日志时,程序会立刻把日志写入文件,这个过程是同步的,会阻塞当前线程。而异步日志会把写日志的操作放到另一个线程里去做,不会阻塞主线程,性能更好。

你眼睛一亮:这么厉害?怎么开启?

鱼皮:很简单,只需要修改一下配置文件:

<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
   <queueSize>512</queueSize>  <!-- 队列大小 -->
   <discardingThreshold>0</discardingThreshold>  <!-- 丢弃阈值,0 表示不丢弃 -->
   <neverBlock>false</neverBlock>  <!-- 队列满时是否阻塞,false 表示会阻塞 -->
   <appender-ref ref="FILE" />  <!-- 引用实际的日志输出目标 -->
</appender>
<root level="INFO">
   <appender-ref ref="ASYNC" />
</root>

不过异步日志也有缺点,如果程序突然崩溃,缓冲区中还没来得及写入文件的日志可能会丢失。

所以要权衡一下,看你的系统更注重性能还是日志的完整性。

你想了想:我们的程序对性能要求比较高,偶尔丢几条日志问题不大,那我就用异步日志吧。

 

7、日志管理

接下来的很长一段时间,你混的很舒服,有 Bug 都能很快发现。

你甚至觉得 Bug 太少、工作没什么激情,所以没事儿就跟新来的实习生阿坤吹吹牛皮:你知道日志么?我可会打它了!

直到有一天,运维小哥突然跑过来:阿巴阿巴,服务器挂了!你快去看看!

你连忙登录服务器,发现服务器的硬盘爆满了,没法写入新数据。

你查了一下,发现日志文件竟然占了 200GB 的空间!

 

你汗流浃背了,正在考虑怎么甩锅,结果阿坤突然鸡叫起来:阿巴 giegie,你的日志文件是不是从来没清理过?

你尴尬地倒了个立,这样眼泪就不会留下来。

鱼皮叹了口气:这就是我要教你的下一招 —— 日志管理。

你好奇道:怎么管理?我每天登服务器删掉一些历史文件?

鱼皮:人工操作也太麻烦了,我们可以通过修改日志配置文件,让框架帮忙管理日志。

首先设置日志的滚动策略,可以根据文件大小和日期,自动对日志文件进行切分。

<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
   <maxFileSize>10MB</maxFileSize>
   <maxHistory>30</maxHistory>
</rollingPolicy>

这样配置后,每天会创建一个新的日志文件(比如 app-2025-10-23.0.log),如果日志文件大小超过 10MB 就再创建一个(比如 app-2025-10-23.1.log),并且只保留最近 30 天的日志。

还可以开启日志压缩功能,进一步节省磁盘空间:

<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
   <!-- .gz 后缀会自动压缩 -->
   <fileNamePattern>logs/app-%d{yyyy-MM-dd}.log.gz</fileNamePattern>
</rollingPolicy>

你有些激动:吼吼,这样我们就可以按照天数更快地查看日志,服务器硬盘也有救啦!

 

8、集成日志收集系统

两年后,你负责的项目已经发展成了一个大型的分布式系统,有好几十个微服务。

如今,每次排查问题你都要登录到不同的服务器上查看日志,非常麻烦。而且有些请求的调用链路很长,你得登录好几台服务器、看好几个服务的日志,才能追踪到一个请求的完整调用过程。

你简直要疯了!

于是你找到鱼皮求助:现在查日志太麻烦了,当年你还有一招没有教我,现在是不是……

鱼皮点点头:嗯,对于分布式系统,就必须要用专业的日志收集系统了,比如很流行的 ELK。

你好奇:ELK 是啥?伊拉克?

阿坤抢答道:我知道,就是 Elasticsearch + Logstash + Kibana 这套组合。

简单来说,Logstash 负责收集各个服务的日志,然后发送给 Elasticsearch 存储和索引,最后通过 Kibana 提供一个可视化的界面。

这样一来,我们可以方便地集中搜索、查看、分析日志。

你惊讶了:原来日志还能这么玩,以后我所有的项目都要用 ELK!

鱼皮摆摆手:不过 ELK 的搭建和运维成本比较高,对于小团队来说可能有点重,还是要按需采用啊。

 

结局

至此,你已经掌握了打日志的核心秘法。

只是你很疑惑,为何那阿坤竟对日志系统如此熟悉?

阿坤苦笑道:我本来就是日志管理大师,可惜我上家公司的同事从来不打日志,所以我把他们暴打了一顿后跑路了。

阿巴 giegie 你要记住,日志不是写给机器看的,是写给未来的你和你的队友看的!

你要是以后不打日志,我就打你!

 

更多编程学习资源

posted @
2025-11-06 11:29 
程序员鱼皮  阅读(
0)  评论(
0)   
收藏 
举报

AD 横向移动-LSASS 进程转储

thbcm阅读(341)

首先,我们在 RDP 的桌面环境下使用任务管理器去转储 LSASS 进程;然后,在无 GUI 的命令行环境下使用 7+2 种方式去转储 LSASS 进程。

C#/.NET/.NET Core技术前沿周刊 | 第 41 期(2025年6.1-6.8)

thbcm阅读(392)

C#/.NET/.NET Core技术前沿周刊,你的每周技术指南针!记录、追踪C#/.NET/.NET Core领域、生态的每周最新、最实用、最有价值的技术文章、社区动态、优质项目和学习资源等。让你时刻站在技术前沿,助力技术成长与视野拓宽。

现代 Python 包管理器 uv

thbcm阅读(361)

因为写一个命令行程序总是要安装的,想分享到 PYPI 也必须要打包。

联系我们