-
经过年复一年的开发积累,企业的代码仓逐渐变得臃肿,甚至变成屎山代码。这些屎山代码,往往经过N个程序员之手,他们水平参差不起,风格不一。如何对这些屎山代码进行统一的管理,让它们可以被监控、评价和批量改造?建立“代码管理系统”的第一个难点在于,如何在庞大的代码仓中,快速的查找出具有某些特征的代码段。由于我们需要查找的是代码段,而不是代码行,用传统的正则表达式难以实现,需要通过语法解析器进行自定义语法配置,然后进行代码查找。 以小实例说明 : ### 实例1: 找出JAVA代码中,入参数量超过4个的函数:# 配置查找规则(Code_manage.syn)如下所示:__DEF_CASE_SENSITIVE__ Y __DEF_FUZZY__ Y __DEF_DEBUG__ N __DEF_LINE_COMMENT__ // __DEF_LINES_COMMENT__ /* */ __DEF_STR__ __NAME__ <1,200> [1,1]ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_$?? [0,199]ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_?? [NO] import if else for while break continue class return try except finally final static public private __DEF_PATH__ __FUNCTION_DEF__ 0101 : x1 @ | public : x2 @ + private 0 0 : x3 @ CAN_SKIP | static 1 1 : x4 @ | __NAME__ : x5 @ | __NAME__ : x6 @ | ( 1111 : p1 @ CAN_SKIP | final : p11 @ | __NAME__ : p111 @ | __NAME__ : p2 @ | , : p22 @ CAN_SKIP | final : p222 @ | __NAME__ : p2222 @ | __NAME__ : p3 @ | , : p33 @ CAN_SKIP | final : p333 @ | __NAME__ : p3333 @ | __NAME__ NNNN : p4 @ | , : p44 @ CAN_SKIP | final : x444 @ | __NAME__ : x4444 @ | __NAME__ 1111 : xx @ | )# 假设java代码(MyCode.java) 如下所示:private int alreadyBufferedSize = 0; // The index in the byte[] found at buffers.getLast() to be written next private int index = 0; // Is the stream closed? private boolean closed = false; public FastByteArrayOutputStream(int initialBlockSize) { Assert.isTrue(initialBlockSize > 0, "Initial block size must be greater than 0"); this.initialBlockSize = initialBlockSize; this.nextBlockSize = initialBlockSize; } @Override public void applyBeanPropertyValues(Object existingBean, String beanName, int autowireMode, boolean dependencyCheck, int initSize) throws BeansException { markBeanAsCreated(beanName); BeanDefinition bd = getMergedBeanDefinition(beanName); BeanWrapper bw = new BeanWrapperImpl(existingBean); initBeanWrapper(bw); applyPropertyValues(beanName, bd, bw, bd.getPropertyValues()); } @Override public Object initializeBean(Object existingBean, String beanName) { return initializeBean(beanName, existingBean, null); }根据配置规则,执行查找命令: ZGLanguage -e Code_manage.syn -f MyCode.java可以得到结果:C:\>ZGLanguage -e Code_manage.syn -f MyCode.java Run type : Find Syntax file : Code_manage.syn code file : MyCode.java Output file : out.zgl -------------------------------------------------------------------- ### Found code by : __FUNCTION_DEF__ | Lines : 17 ~ 17 : -------------------------------------------------------------------- public void applyBeanPropertyValues(Object existingBean, String beanName, int autowireMode, boolean dependencyCheck, int initSize)可以看出,查找结果只输出了函数 applyBeanPropertyValues,它的入参数量为5个,其他2个函数的入参均不超过4个,因此被忽略。 ### 实例2: 提取SQL代码中的关联(on)和筛选(where)代码段:# 配置查找规则(Code_manage.syn)如下所示:__DEF_DEBUG__ N __DEF_FUZZY__ Y __DEF_CASE_SENSITIVE__ N __DEF_LINE_COMMENT__ -- __DEF_LINES_COMMENT__ /* */ __DEF_PATH__ __WHERE__ 1 : x1 | where : x2 | __PATH_4_EXPR__ __DEF_PATH__ __ON__ 1 : x1 | __\b__ : x2 + __\t__ : x3 + __\n__ : x4 | on : x5 | __PATH_4_EXPR__ __DEF_SUB_PATH__ __PATH_4_EXPR__ 1 : x1 | __SUB_PATH_EXPR__ : x2 + __ONE_PATH_EXPR__ __DEF_SUB_PATH__ __SUB_PATH_EXPR__ 1 : x1 | ( : x2 | __ONE_PATH_EXPR__ : x3 | ) __DEF_SUB_PATH__ __ONE_PATH_EXPR__ NN : @ | __NAME__ : @ + __INT__ : @ + __FLOAT__ : @ + __CASE_WHEN__ : @ + __STRING__ : @ + __CAST_AS__ : @ + __FUNCTION__ : @ + __SUB_PATH_EXPR__ : @ + = : @ + <> : @ + != : @ + > : @ + >= : @ + < : @ + <= : @ + . : @ + , : @ + + : @ + - : @ + * : @ + / : @ + || : @ + null : @ + between : @ + and : @ + or : @ + like : @ + in : @ STRING + not in : @ STRING + is null : @ STRING + is not null __DEF_STR__ __NAME__ <1,100> [1,1]ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_?? [0,100]ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_?? [NO] select inner left join on from where group order by having union all with as table date time __DEF_STR__ __FLOAT__ <1,100> [1,50]0123456789 [1,1]. [1,50]0123456789 __DEF_STR__ __INT__ <1,100> [1,100]0123456789 __DEF_SUB_PATH__ __STRING__ 1 : x1 | ' : x2 | __ANY__ : x3 | ' __DEF_SUB_PATH__ __DECIMAL__ 111 : x1 | decimal 0 : x2 | ( 01 : x3 | __INT__ 00 : x4 | , 00 : x5 | __INT__ 01 : x6 | ) __DEF_SUB_PATH__ __VAR_NAME__ 1 : x1 | $ : x2 | { : x3 | __NAME__ : x4 | } __DEF_SUB_PATH__ __CASE_WHEN__ 1 : x1 @ | case N : x2 @ | when : x3 @ | __PATH_4_EXPR__ : x4 @ | then : x5 @ | __PATH_4_EXPR__ 1 : x6 @ CAN_SKIP | else : x7 @ CAN_SKIP | __PATH_4_EXPR__ : x8 @ | end __DEF_SUB_PATH__ __CAST_AS__ 1 : x1 | cast : x2 | ( 1 : x3 | __PATH_4_EXPR__ : x4 | as : x5 | date : x6 + int : n1 + double : n2 + float : n3 + bigint : x8 + __DECIMAL__ 1 : xx | ) __DEF_SUB_PATH__ __FUNCTION__ 1 : x1 @ | __NAME__ : x2 @ | ( N : x3 @ CAN_SKIP | __PATH_4_EXPR__ e : x4 @ CAN_SKIP | , 1 : x5 @ | )# 假设SQL代码(myproc.sql) 如下所示:CREATE OR REPLACE PROCEDURE PROC_F_CWWS_LOAN ( P_AS_OF_DATE IN DATE, RET_FLG OUT VARCHAR2, RET_MSG OUT VARCHAR2 ) IS -- 声明变量并初始化 V_COUNT NUMBER := 0; V_PROC_NAME VARCHAR2(200) := 'PROC_F_CWWS_LOAN'; V_PROC_DESC VARCHAR2(100) := 'xxxx业务数据ETL处理'; V_P_FREQ VARCHAR2(4) := ''; BEGIN --写入初始日志 INSERT INTO M_RUNLOG VALUES (SYSDATE, V_PROC_NAME, 'it is 1'); COMMIT; --设置会话日期格式 EXECUTE IMMEDIATE ' ALTER SESSION SET NLS_DATE_FORMAT = ''YYYY-MM-DD'''; --查询参数表中,该程序对应的频率值 SELECT P_FREQ INTO V_P_FREQ FROM ETL_PROC_STATUS_DEF WHERE PROC_NAME = V_PROC_NAME; --判断是调度频率 ETL.ETL_ADD_PARTITION('MA_F_LOAN', P_AS_OF_DATE, 'ETL'); --从还款计划表中取每笔账户最近一次小于等于数据日期还款日,作为上次还款日 INSERT INTO ETL.TMP_XD_LAST_PAYDATE (OBJECTNO, LAST_PAYDATE) SELECT OBJECTNO, LAST_PAYDATE FROM (SELECT T.OBJECTNO, MAX(TO_DATE(PAYDATE, 'YYYY-MM-DD')) LAST_PAYDATE FROM NYBDP.O_CWWS_ACCT_PAYMENT_SCHEDULE T WHERE T.AS_OF_DATE = P_AS_OF_DATE AND T.SEQID <> '999' AND TO_DATE(T.PAYDATE, 'YYYY-MM-DD') < P_AS_OF_DATE GROUP BY T.OBJECTNO); INSERT INTO M_RUNLOG VALUES (SYSDATE, V_PROC_NAME, 'it is 3'); COMMIT; MERGE INTO ETL.MA_F_LOAN A USING (SELECT /*+PARALLEL(8)*/ T.ACCOUNT_NUMBER, T.GL_ACCOUNT_ID, T.INT_GL_ACCOUNT_ID FROM ETL.MA_F_LOAN T INNER JOIN ETL.MA_D_GL_SUBJECT T1 ON T.INT_GL_ACCOUNT_ID = T1.SUBJECT_NO3 AND T1.SUBJECT_NAME3 LIKE '%已减值%' AND T1.AS_OF_DATE = P_AS_OF_DATE WHERE T.AS_OF_DATE = P_AS_OF_DATE AND T.ACCOUNT_NUMBER IN (SELECT ACCOUNT_NUMBER FROM (SELECT /*+PARALLEL(8)*/ T2.ACCOUNT_NUMBER, COUNT(1) FROM ETL.MA_F_LOAN T2 WHERE T2.AS_OF_DATE = P_AS_OF_DATE GROUP BY T2.ACCOUNT_NUMBER HAVING COUNT(1) > 1))) B ON (A.ACCOUNT_NUMBER = B.ACCOUNT_NUMBER AND A.AS_OF_DATE = P_AS_OF_DATE AND A.GL_ACCOUNT_ID = B.GL_ACCOUNT_ID AND A.INT_GL_ACCOUNT_ID = B.INT_GL_ACCOUNT_ID) WHEN MATCHED THEN UPDATE SET A.CUR_BOOK_BAL = 0, A.OVERDUE_BAL = 0; COMMIT; RET_FLG := '0'; RET_MSG := '执行成功'; EXCEPTION WHEN OTHERS THEN --写入异常日志 ETL.PROC_ETL_LOG(P_AS_OF_DATE,V_PROC_NAME,V_PROC_DESC,V_COUNT,-1,SQLCODE,SQLERRM); RET_MSG := SQLCODE || ':' || SQLERRM; END; /根据配置规则,执行查找命令: ZGLanguage -e Code_manage.syn -f myproc.sql可以得到结果:C:\>ZGLanguage -e Code_manage.syn -f myproc.sql Run type : Find Syntax file : Code_manage.syn code file : myproc.sql Output file : out.zgl -------------------------------------------------------------------- ### Found code by : __WHERE__ | Lines : 27 ~ 27 : -------------------------------------------------------------------- WHERE PROC_NAME = V_PROC_NAME -------------------------------------------------------------------- ### Found code by : __WHERE__ | Lines : 39 ~ 41 : -------------------------------------------------------------------- WHERE T.AS_OF_DATE = P_AS_OF_DATE AND T.SEQID <> '999' AND TO_DATE(T.PAYDATE, 'YYYY-MM-DD') < P_AS_OF_DATE -------------------------------------------------------------------- ### Found code by : __ON__ | Lines : 52 ~ 54 : -------------------------------------------------------------------- ON T.INT_GL_ACCOUNT_ID = T1.SUBJECT_NO3 AND T1.SUBJECT_NAME3 LIKE '%宸插噺鍊?' AND T1.AS_OF_DATE = P_AS_OF_DATE -------------------------------------------------------------------- ### Found code by : __WHERE__ | Lines : 55 ~ 56 : -------------------------------------------------------------------- WHERE T.AS_OF_DATE = P_AS_OF_DATE AND T.ACCOUNT_NUMBER IN -------------------------------------------------------------------- ### Found code by : __WHERE__ | Lines : 61 ~ 61 : -------------------------------------------------------------------- WHERE T2.AS_OF_DATE = P_AS_OF_DATE -------------------------------------------------------------------- ### Found code by : __ON__ | Lines : 64 ~ 66 : -------------------------------------------------------------------- ON (A.ACCOUNT_NUMBER = B.ACCOUNT_NUMBER AND A.AS_OF_DATE = P_AS_OF_DATE AND A.GL_ACCOUNT_ID = B.GL_ACCOUNT_ID AND A.INT_GL_ACCOUNT_ID = B.INT_GL_ACCOUNT_ID) WHEN MATCHED THEN UPDATE SET A.CUR_BOOK_BAL = 0, A.OVERDUE_BAL = 0可以看出,查找结果将 myproc.sql 代码中的 where 和 on 代码块及其所在行号提取出来。
-
作为后端开发者,API 是系统的门面,也是前后端协作的基石。一份规范、优雅、可扩展的 RESTful API,不仅能降低协作成本,更能支撑系统长期迭代。今天用实战视角,把 RESTful 设计规范与落地最佳实践一次性讲透。 一、先搞懂:什么是 RESTfulREST(Representational State Transfer)是一种架构风格,而非强制标准。 满足以下核心原则,才能叫 RESTful:面向资源(Resource),一切皆资源使用 HTTP 方法表达行为无状态(Stateless):每个请求独立完整统一接口:URI、Method、Status、Response 标准化目标:直观、自解释、易维护、易扩展。 二、URI 设计规范(最容易踩坑的地方) 1. 使用名词,而非动词/getUser/deleteOrder/createProduct错误:/getUser/deleteOrder/createProduct正确:/users/orders/products2. 复数形式 资源是集合,统一用复数: /users 而非 /user/items 而非 /item3. 层级关系用 / 表示/users/{id}/orders/users/{userId}/addresses/{addressId}4. 禁止大写、下划线,用中横线/userInfo ❌/user-info ✅5. 不要暴露文件后缀/api/users.json ❌/api/users ✅三、HTTP 方法语义(必须严格遵守)方法作用幂等安全GET查询资源✅✅POST新增资源❌❌PUT全量更新 / 替换✅❌PATCH局部更新✅❌DELETE删除✅❌实战示例:获取用户列表:GET /users获取单个用户:GET /users/{id}新建用户:POST /users全量更新:PUT /users/{id}局部更新:PATCH /users/{id}删除用户:DELETE /users/{id} 四、状态码使用(别再只返回 200/404 了)成功类200 OK:查询 / 修改成功201 Created:创建成功204 No Content:删除成功(无返回体)客户端错误400 Bad Request:参数错误401 Unauthorized:未登录 / Token 无效403 Forbidden:已登录但无权限404 Not Found:资源不存在405 Method Not Allowed:方法不支持422 Unprocessable Entity:校验失败服务端错误500 Internal Server Error:服务器异常502 Bad Gateway:网关错误503 Service Unavailable:服务不可用 五、统一响应体结构(前后端必约定)推荐通用结构:{ "code": 200, // 业务码 "message": "success", "data": {}, // 主体数据 "requestId": "xxx" // 方便排查问题}成功:code=200失败:使用具体业务码,如 1001、4001禁止:成功失败混用结构六、分页、排序、过滤(标准写法)1. 分页GET /users?pageNum=1&pageSize=102. 排序GET /users?sort=createTime,desc3. 过滤GET /users?gender=male&status=active原则: 查询条件全部放 query不要放 body(GET 不规范) 七、版本管理(API 必做)三种主流方式,任选其一并统一:URL 路径(最常用)/api/v1/users/api/v2/users请求头Accept-version: v1参数/users?version=1建议:新项目直接用 v1 起步。八、安全与最佳实践必须 HTTPSToken 放 Header:Authorization: Bearer {token}敏感参数不暴露在 URL请求参数必校验接口限流防刷日志记录 requestId、入参、耗时、异常跨域使用 CORS,不要乱用 JSONP提供 API 文档:Swagger / Knife4j / OpenAPI 九、反例 vs 正例(一眼看懂差距)❌ 混乱不规范:/getUserInfo?id=1/api/delete_order/1/userList?type=1&page=1✅ RESTful 规范:GET /api/v1/users/1DELETE /api/v1/orders/1GET /api/v1/users?type=1&pageNum=1&pageSize=10十、总结一句话RESTful 不是花架子,而是团队协作的通用语言:资源用名词复数行为用 HTTP 方法状态码表达结果结构统一、版本必加、安全必做
-
Prometheus+Grafana+Alertmanager 监控体系搭建与告警优化实战在微服务、容器化普及的当下,一套高效、可靠的监控告警体系,是保障系统稳定运行的“生命线”。Prometheus负责数据采集与存储,Grafana负责可视化展示,Alertmanager负责告警分发与处理,三者联动形成“采集-展示-告警”全链路监控闭环,也是当前企业级监控的主流方案。本文从实战出发,手把手教你搭建Prometheus+Grafana+Alertmanager监控体系,同时分享高频告警优化技巧,解决“监控不准、告警泛滥、漏报误报”等核心痛点,适合运维、开发、架构师参考,可直接用于生产环境落地。一、监控体系核心架构:三者各司其职,联动闭环在动手搭建前,先明确三者的核心分工,避免搭建过程中混淆角色,确保体系高效运转:Prometheus(普罗米修斯):核心采集器,负责从目标设备(服务器、容器、微服务、数据库等)采集指标数据(如CPU使用率、内存占用、接口QPS),并以时序数据的形式存储,支持灵活的查询语句(PromQL)。Grafana(格拉法纳):可视化面板,对接Prometheus数据源,通过拖拽式操作生成直观的监控仪表盘,支持折线图、柱状图、仪表盘等多种展示形式,轻松实现“一眼看清系统状态”。Alertmanager(告警管理器):告警处理器,接收Prometheus触发的告警规则,进行去重、分组、路由,然后分发到指定渠道(邮件、企业微信、钉钉、Slack),同时支持告警抑制、静默等高级功能,避免告警泛滥。核心流程:Prometheus采集数据 → 存储时序数据 → Grafana可视化展示 → 触发告警规则 → Alertmanager处理并分发告警 → 运维人员响应处理。二、实战搭建:从0到1部署监控体系(Docker部署,最简单易落地)推荐使用Docker部署,无需复杂的环境配置,快速完成搭建,适合新手和生产环境快速落地。提前准备:服务器(推荐2核4G以上)、Docker、Docker Compose(简化部署命令)。1. 环境准备(前置操作)# 1. 安装Docker(略,已有Docker可跳过)# 2. 安装Docker Composecurl -L "https://github.com/docker/compose/releases/download/v2.20.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-composechmod +x /usr/local/bin/docker-compose# 3. 创建工作目录,统一管理配置文件mkdir -p /data/monitor/{prometheus,grafana,alertmanager}cd /data/monitor 2. 编写Docker Compose配置文件(核心)创建docker-compose.yml文件,整合三者服务,一键启动,配置如下(可直接复制修改):version: '3.8'services: # Prometheus服务 prometheus: image: prom/prometheus:v2.45.0 container_name: prometheus ports: - "9090:9090" # Prometheus访问端口 volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml # 核心配置文件 - prometheus_data:/prometheus # 数据持久化 restart: always networks: - monitor_net # Grafana服务 grafana: image: grafana/grafana:10.2.0 container_name: grafana ports: - "3000:3000" # Grafana访问端口 volumes: - grafana_data:/var/lib/grafana # 数据持久化 restart: always depends_on: - prometheus # 依赖Prometheus,确保先启动 networks: - monitor_net # Alertmanager服务 alertmanager: image: prom/alertmanager:v0.26.0 container_name: alertmanager ports: - "9093:9093" # Alertmanager访问端口 volumes: - ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml # 告警配置 restart: always depends_on: - prometheus networks: - monitor_netvolumes: prometheus_data: grafana_data:networks: monitor_net: driver: bridge3. 配置Prometheus(数据采集核心)创建Prometheus核心配置文件prometheus.yml,定义数据采集目标、存储策略、告警规则关联,关键配置如下global: scrape_interval: 15s # 采集间隔,默认15秒,可根据需求调整 evaluation_interval: 15s # 告警规则评估间隔# 告警规则配置,关联Alertmanageralerting: alertmanagers: - static_configs: - targets: - alertmanager:9093 # 对接Alertmanager服务# 告警规则文件(可拆分,便于维护)rule_files: - "alert_rules.yml" # 后续创建,定义具体告警规则# 采集目标配置(核心:监控哪些设备/服务)scrape_configs: # 1. 监控Prometheus自身 - job_name: 'prometheus' static_configs: - targets: ['localhost:9090'] # 2. 监控服务器(需在服务器安装node_exporter,下文说明) - job_name: 'server' static_configs: - targets: ['服务器IP:9100'] # node_exporter端口 # 3. 监控Docker容器(需安装cadvisor) - job_name: 'docker' static_configs: - targets: ['服务器IP:8080'] # cadvisor端口 # 4. 监控微服务(以Spring Boot为例,需集成micrometer) - job_name: 'spring-boot' metrics_path: '/actuator/prometheus' static_configs: - targets: ['微服务IP:8080'] 4. 配置Alertmanager(告警分发核心)创建Alertmanager配置文件alertmanager.yml,定义告警路由、接收渠道(以企业微信为例,最常用),配置如下:global: resolve_timeout: 5m # 告警解决后,5分钟内不再重复发送# 告警路由:默认路由,所有告警先进入这里route: group_by: ['alertname'] # 按告警名称分组,同一类型告警合并发送 group_wait: 10s # 组内第一个告警触发后,等待10秒,收集同组告警一起发送 group_interval: 10s # 同一组告警,两次发送间隔 repeat_interval: 1h # 同一告警,重复发送间隔(避免频繁骚扰) receiver: 'wechat' # 默认接收者# 接收者配置(企业微信)receivers:- name: 'wechat' wechat_configs: - corp_id: '你的企业微信corp_id' to_user: '@all' # 接收人,@all表示全员 agent_id: '你的应用agent_id' api_secret: '你的应用api_secret'# 告警抑制:避免关联告警泛滥(如CPU高告警触发后,不再发送内存高告警)inhibit_rules:- source_match: severity: 'critical' # 源告警级别(严重) target_match: severity: 'warning' # 目标告警级别(警告) equal: ['instance'] # 相同实例的告警,严重告警抑制警告告警5. 配置告警规则(触发告警的条件)在Prometheus配置目录下创建alert_rules.yml,定义常用告警规则(服务器、容器、服务),可根据实际需求扩展:groups:- name: 服务器监控告警 rules: # CPU使用率超过80%,持续5分钟,触发警告;超过90%,持续3分钟,触发严重告警 - alert: CPU使用率过高 expr: avg(rate(node_cpu_seconds_total{mode!='idle'}[5m])) by (instance) * 100 > 80 for: 5m labels: severity: warning annotations: summary: "CPU使用率过高" description: "实例 {{ $labels.instance }} CPU使用率超过80%,当前值:{{ $value | humanizePercentage }}" - alert: CPU使用率危急 expr: avg(rate(node_cpu_seconds_total{mode!='idle'}[5m])) by (instance) * 100 > 90 for: 3m labels: severity: critical annotations: summary: "CPU使用率危急" description: "实例 {{ $labels.instance }} CPU使用率超过90%,当前值:{{ $value | humanizePercentage }}" # 内存使用率超过85%,触发警告 - alert: 内存使用率过高 expr: (node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) / node_memory_MemTotal_bytes * 100 > 85 for: 5m labels: severity: warning annotations: summary: "内存使用率过高" description: "实例 {{ $labels.instance }} 内存使用率超过85%,当前值:{{ $value | humanizePercentage }}"- name: 容器监控告警 rules: # 容器CPU使用率超过90%,持续3分钟 - alert: 容器CPU使用率过高 expr: avg(rate(container_cpu_usage_seconds_total[3m])) by (container_name) * 100 > 90 for: 3m labels: severity: warning annotations: summary: "容器CPU使用率过高" description: "容器 {{ $labels.container_name }} CPU使用率超过90%,当前值:{{ $value | humanizePercentage }}"- name: 微服务监控告警 rules: # 服务接口错误率超过5%,持续2分钟 - alert: 微服务接口错误率过高 expr: sum(rate(http_server_requests_seconds_count{status=~"5.."}[2m])) / sum(rate(http_server_requests_seconds_count[2m])) * 100 > 5 for: 2m labels: severity: critical annotations: summary: "微服务接口错误率过高" description: "微服务接口错误率超过5%,当前值:{{ $value | humanizePercentage }}" 6. 启动服务,完成搭建# 启动所有服务docker-compose up -d# 查看服务状态,确保所有服务正常运行docker-compose ps# 查看日志,排查启动异常docker-compose logs -f启动成功后,访问对应端口验证:Prometheus:http://服务器IP:9090 (可查看采集目标、告警规则)Grafana:http://服务器IP:3000 (默认账号密码admin/admin,首次登录需修改)Alertmanager:http://服务器IP:9093 (可查看告警记录、静默规则)7. Grafana可视化配置(关键步骤)Grafana默认无监控面板,需对接Prometheus数据源,导入预制面板(无需手动拖拽):登录Grafana,点击左侧「Configuration」→「Data Sources」,点击「Add data source」,选择「Prometheus」;在URL中输入「http://prometheus:9090」,点击「Save & Test」,提示“Data source is working”即为成功;导入预制面板:点击左侧「Dashboards」→「Import」,输入面板ID(服务器监控用8919,Docker监控用893,Spring Boot监控用12856),选择已配置的Prometheus数据源,点击「Import」;导入完成后,即可在Grafana中查看直观的监控面板,支持自定义调整面板样式、指标展示。三、告警优化:解决漏报、误报、泛滥三大痛点很多人搭建完监控体系后,会陷入“告警泛滥”“误报频发”“关键告警漏报”的困境,核心原因是告警规则、路由配置不合理。以下是生产环境高频优化技巧,直接复用即可。1. 优化告警规则:避免误报,精准触发设置合理的「for」时间:避免瞬时波动触发告警(如CPU瞬时冲高到80%,持续5分钟再触发,而非立即触发);细化告警阈值:按服务/设备重要性区分阈值(如核心服务器CPU阈值80%,非核心服务器90%);使用「rate()」函数:计算一段时间内的平均指标,避免瞬时值误触发(如rate(node_cpu_seconds_total[5m]));添加「annotations」描述:告警信息中包含实例、当前值、处理建议,便于运维人员快速响应(如上文告警规则中的description)。2. 优化Alertmanager:避免告警泛滥,精准分发合理分组(group_by):按「alertname、instance、service」分组,同一类型、同一实例的告警合并发送,避免多条重复告警;设置重复发送间隔(repeat_interval):核心告警1小时重复一次,非核心告警2-4小时重复一次,避免频繁骚扰;启用告警抑制(inhibit_rules):关联告警抑制(如CPU高告警触发后,抑制内存高告警),减少无效告警;按 severity 分渠道分发:critical(严重)告警发送到企业微信+电话,warning(警告)告警仅发送企业微信,避免过度告警。3. 优化数据采集:提升监控准确性,避免漏报调整采集间隔(scrape_interval):核心服务/设备采集间隔设为10-15s,非核心设为30s-1min,平衡性能与准确性;增加采集超时时间:避免网络波动导致采集失败,在scrape_configs中添加「scrape_timeout: 10s」;监控采集目标状态:在Prometheus中添加「up」指标告警(up==0表示采集失败),避免因采集失败导致漏报。4. 实战优化案例(可直接复用)场景:核心微服务接口错误率告警,避免瞬时错误触发误报,同时确保严重错误及时通知:四、生产环境落地避坑指南踩坑1:数据未持久化,容器重启后监控数据丢失 → 解决方案:配置Docker volumes挂载,如本文Docker Compose中的prometheus_data、grafana_data;踩坑2:告警规则过于简单,导致误报频发 → 解决方案:添加for时间、细化阈值、使用rate()函数,避免瞬时波动触发;踩坑3:Alertmanager未配置告警抑制,关联告警泛滥 → 解决方案:按severity配置抑制规则,避免同一实例的关联告警重复发送;踩坑4:Grafana面板导入后无数据 → 解决方案:检查Prometheus数据源配置是否正确,采集目标是否正常(Prometheus的Targets页面查看);踩坑5:采集间隔过短,导致Prometheus资源占用过高 → 解决方案:核心服务15s,非核心30s以上,避免过度采集;踩坑6:告警渠道配置错误,导致告警无法接收 → 解决方案:先测试告警渠道(如企业微信发送测试消息),再配置到Alertmanager。
-
随着微服务规模从几十到上百甚至上千,传统的 “代码埋点 + 配置中心” 的治理方式早已力不从心 —— 服务间调用链路混乱、流量管控粒度粗、安全认证散落在各服务、问题排查全靠日志大海捞针… 而Istio作为当前最主流的服务网格(Service Mesh)框架,通过 “数据面 + 控制面” 的无侵入架构,完美解决了微服务治理的痛点。本文从实战角度,带你掌握 Istio 核心能力:流量治理、服务安全、可观测性,零基础也能落地。随着微服务规模从几十到上百甚至上千,传统的 “代码埋点 + 配置中心” 的治理方式早已力不从心 —— 服务间调用链路混乱、流量管控粒度粗、安全认证散落在各服务、问题排查全靠日志大海捞针… 而Istio作为当前最主流的服务网格(Service Mesh)框架,通过 “数据面 + 控制面” 的无侵入架构,完美解决了微服务治理的痛点。本文从实战角度,带你掌握 Istio 核心能力:流量治理、服务安全、可观测性,零基础也能落地。 一、先搞懂:Istio 到底是什么? Service Mesh(服务网格)是微服务的 “网络中间件”,Istio 则是它的 “标杆实现”: 🧩 架构分层:控制面(Istiod):负责配置管理、服务发现、策略下发,核心大脑;数据面(Envoy 代理):以 Sidecar 模式注入到每个服务 Pod 中,拦截所有进出流量,无需修改业务代码。 ✨ 核心价值:无侵入实现流量管控、安全认证、监控追踪,让业务开发专注业务,治理交给 Istio。🎯 适用场景:K8s 环境下的微服务集群(这也是 Istio 的主要落地场景)。 二、实战 1:流量治理 —— 精准控制服务间流量 流量治理是 Istio 最核心的能力,覆盖灰度发布、故障注入、流量分流、熔断限流等场景,以下是高频实战场景: 1. 灰度发布(金丝雀发布) 场景:将 10% 的流量路由到新版本服务(v2),90% 保留在 v1,验证新版本稳定性。 # 定义目标规则:区分v1和v2版本 apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: order-service spec: host: order-service # 服务名 subsets: - name: v1 labels: version: v1 - name: v2 labels: version: v2 # 定义虚拟服务:流量分流规则 apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: order-service spec: hosts: - order-service http: - route: - destination: host: order-service subset: v1 weight: 90 # 90%流量到v1 - destination: host: order-service subset: v2 weight: 10 # 10%流量到v2部署后,仅需修改 weight 参数,即可平滑将流量切到 100% v2,全程无服务中断。 2. 故障注入 —— 模拟服务异常 场景:测试下游服务超时 / 错误时,上游服务的容错能力(无需修改代码)。 apiVersion: networking.istio.io/v1beta1 kind: VirtualService metadata: name: payment-service spec: hosts: - payment-service http: - fault: abort: httpStatus: 500 # 注入500错误 percentage: value: 30 # 30%的请求返回500 route: - destination: host: payment-service subset: v1 通过该配置,可模拟 30% 的支付请求失败,验证订单服务的重试 / 降级逻辑是否生效。 3. 熔断限流 —— 保护服务不被打垮 当 user-service 出现异常时,Istio 会自动熔断,避免故障扩散到整个集群。apiVersion: networking.istio.io/v1beta1 kind: DestinationRule metadata: name: user-service spec: host: user-service trafficPolicy: connectionPool: tcp: maxConnections: 100 # 最大TCP连接数 http: http1MaxPendingRequests: 100 # 最大挂起请求数 maxRequestsPerConnection: 10 # 每个连接最大请求数 outlierDetection: consecutiveErrors: 5 # 连续5次错误触发熔断 interval: 30s # 检测间隔 baseEjectionTime: 30s # 熔断后剔除时间三、实战 2:服务安全 —— 零代码实现认证与加密 微服务间的 “通信安全” 是刚需,Istio 提供了开箱即用的安全能力,无需在业务代码中写认证逻辑: 1. 双向 TLS(mTLS)—— 服务间加密通信 一键开启集群内所有服务的双向 TLS 认证,所有流量自动加密: apiVersion: security.istio.io/v1beta1 kind: PeerAuthentication metadata: name: default namespace: default spec: mtls: mode: STRICT # 强制开启mTLS开启后:只有携带合法 Istio 证书的服务才能通信,杜绝非法服务接入;所有服务间流量自动通过 TLS 加密,无需配置 HTTPS 证书。2. 授权策略 —— 精细化控制访问权限场景:仅允许 order-service 访问 payment-service,拒绝其他服务调用: apiVersion: security.istio.io/v1beta1 kind: AuthorizationPolicy metadata: name: payment-service-auth namespace: default spec: selector: matchLabels: app: payment-service action: ALLOW rules: - from: - source: principals: ["cluster.local/ns/default/sa/order-service-sa"] # 仅允许order-service的服务账号 to: - operation: methods: ["POST"] # 仅允许POST方法四、实战 3:可观测性 —— 全方位监控追踪Istio 内置了对 Prometheus、Grafana、Jaeger 的原生支持,无需手动埋点,即可实现:1. 指标监控(Metrics)部署 Istio 后,自动采集以下核心指标:服务调用成功率、延迟、QPS;流量分布、错误率、熔断次数;Sidecar 代理的资源占用。通过 Grafana Istio 仪表盘,可一键查看集群级 / 服务级 / 接口级的监控数据,快速定位性能瓶颈。2. 分布式追踪(Tracing)仅需在服务中传递x-request-id头,Istio 即可自动生成全链路追踪数据,对接 Jaeger 后:可视化展示请求从入口网关到各微服务的完整链路;定位链路中耗时最长的服务 / 接口;关联日志和指标,快速排查 “偶发超时 / 错误” 问题。3. 访问日志(Logging) 配置 Sidecar 输出详细访问日志,包含:[%START_TIME%] "%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% "%REQ(X-FORWARDED-FOR)%" "%REQ(USER-AGENT)%" "%REQ(X-REQUEST-ID)%" "%REQ(:AUTHORITY)%" "%UPSTREAM_HOST%" 日志包含请求方法、路径、响应码、耗时、上游服务等关键信息,可接入 ELK 进行检索分析。五、落地建议 & 避坑指南✅ 落地步骤(循序渐进)先在测试环境部署 Istio,仅注入核心服务(如网关、订单、支付);先启用监控和日志能力,熟悉 Istio 的基础运维;逐步接入流量治理规则(先灰度,再熔断,最后故障注入);最后启用 mTLS 和授权策略,完成安全加固。🚫 避坑点不要一次性给所有服务注入 Sidecar,避免资源占用突增;控制面(Istiod)需单独配置资源限制,避免成为性能瓶颈;虚拟服务规则优先级需理清,避免规则冲突导致流量异常;监控数据需设置保留策略,避免 Prometheus 磁盘占满。总结Istio 通过 “无侵入” 的方式,为微服务架构提供了流量治理、安全、可观测性三大核心能力,解决了传统微服务治理的痛点:流量治理:支持灰度发布、故障注入、熔断限流,无需修改业务代码;服务安全:一键开启 mTLS 加密和精细化授权,降低安全开发成本;可观测性:自动采集指标、追踪、日志,实现全链路问题排查。对于中大型微服务集群,Istio 不是 “可选项” 而是 “必选项”—— 它让微服务治理从 “零散的代码埋点” 升级为 “平台化的统一管控”,大幅提升运维效率和系统稳定性。
-
作为 Java 生态中最主流的开发框架,Spring Boot 3.2 的正式发布带来了诸多令人振奋的更新,其中对 JDK 21 虚拟线程(Virtual Threads)的原生支持 无疑是最值得关注的核心特性之一。虚拟线程彻底重构了 Java 应用的并发模型,让高并发场景下的开发和性能优化变得前所未有的简单。 一、先搞懂:什么是虚拟线程? 在传统 Java 开发中,我们使用的是 “平台线程(Platform Thread)”—— 它与操作系统内核线程一一映射,创建和销毁成本高、上下文切换开销大,且受限于内核线程数量(通常单机几千个就到瓶颈)。 而虚拟线程是 JVM 层面实现的轻量级线程,由 JVM 管理而非操作系统:🌱 极致轻量:单个 JVM 可创建数百万个虚拟线程,内存占用仅几 KB🚀 超低开销:创建、切换、销毁几乎无性能损耗🎯 无感知迁移:完全兼容现有 Thread API,业务代码无需大幅改造⚡ 自动挂起:遇到 IO 阻塞(如数据库查询、网络请求)时,虚拟线程会自动挂起,释放底层平台线程,避免资源浪费二、Spring Boot 3.2 对虚拟线程的核心支持 Spring Boot 3.2 基于 Spring Framework 6.1 的能力,为虚拟线程提供了 “零侵入式” 集成,主要体现在这几个核心场景: 1. 配置层面:一键启用虚拟线程 无需复杂编码,仅需在application.properties/yaml中添加配置,即可让 Spring 容器的核心线程池切换为虚拟线程:# 全局启用虚拟线程 spring.threads.virtual.enabled=true # Web容器(Tomcat/Undertow)使用虚拟线程处理请求 server.tomcat.threads.virtual.enabled=true2. Web 请求处理:超高并发支撑 传统 Tomcat 容器默认使用平台线程池(默认 200 线程),高并发下易出现线程耗尽;启用虚拟线程后,Tomcat 会为每个 HTTP 请求分配独立的虚拟线程: 单实例可轻松支撑数万 QPS 的请求处理彻底解决 “线程池满导致请求排队 / 拒绝” 问题无需手动调整线程池参数,JVM 自动适配 3. 异步任务:@Async 无缝兼容 原有使用@Async注解的异步任务,只需指定虚拟线程池即可:@Configuration @EnableAsync public class AsyncConfig { // 定义虚拟线程池 @Bean public Executor virtualThreadExecutor() { // Spring提供的虚拟线程构建器 return Executors.newVirtualThreadPerTaskExecutor(); } } // 业务层使用 @Service public class OrderService { // 指定使用虚拟线程池执行异步任务 @Async("virtualThreadExecutor") public CompletableFuture<OrderVO> processOrder(Long orderId) { // 模拟IO密集型操作(数据库/远程调用) return CompletableFuture.supplyAsync(() -> { // 业务逻辑 return orderDao.queryById(orderId); }); } }4. 数据访问层:自动适配对于 JDBC、JPA、Redis 等 IO 密集型操作,Spring Data 会自动利用虚拟线程的特性: 数据库查询等待结果时,虚拟线程自动挂起连接池无需过度扩容,减少资源占用实测:相同数据库连接数下,QPS 提升 3-5 倍三、适用场景 & 性能对比 ✅ 最适合的场景 IO 密集型应用:微服务接口、电商订单系统、消息消费、数据同步高并发短任务:API 网关、秒杀接口、短信 / 邮件发送批量处理任务:数据导出、日志分析、批量入库 📊 性能实测(单机 8 核 16G)场景平台线程虚拟线程提升幅度HTTP 请求处理 QPS2000150007.5 倍异步任务处理耗时800ms120ms6.7 倍最大并发连接数20005000025 倍 四、避坑指南:这些情况不适合用虚拟线程🚫 CPU 密集型任务:虚拟线程无法提升 CPU 密集型任务性能(如大数据计算、加密解密),反而可能增加调度开销🚫 长时间阻塞的本地代码:如死循环、本地耗时计算,会占用平台线程导致无法释放🚫 依赖 ThreadLocal 的代码:虚拟线程切换时 ThreadLocal 会失效,需改用 InheritableThreadLocal🚫 旧版原生库:依赖 JNI 的本地库可能不兼容虚拟线程五、升级建议基础环境:升级 JDK 至 21(LTS 版本),Spring Boot 3.2 最低要求 JDK 17平滑迁移:先在非核心链路启用虚拟线程,观察监控指标(线程数、CPU、内存)监控适配:使用 Spring Boot Actuator 监控虚拟线程状态,需升级 Micrometer 至 1.12+代码改造:移除手动创建的线程池,优先使用 Spring 提供的Executors.newVirtualThreadPerTaskExecutor()
-
在大模型席卷全球的技术浪潮下,Java 开发者们迫切需要一款贴合自身生态、低门槛接入 AI 能力的框架。Spring AI 的出现,恰好填补了这一空白 —— 它并非简单移植 Python 生态的现有方案,而是深度遵循 Spring 设计哲学,为 Java 和 Spring 开发者打造了原生的 AI 开发框架。本文将从 Spring AI 的核心概念、核心特性出发,结合实际环境搭建与首个对话案例,带大家快速上手这款框架,解锁 Java 生态与 AI 融合的全新可能。一、什么是 Spring AI?Spring AI 是面向 Java 和 Spring 生态的原生人工智能框架,其核心设计理念完全传承自 Spring:依赖注入、POJO 编程、模块化架构与可配置性。它重构了 AI 应用的全开发流程,让开发者无需关注底层模型的适配细节,就能像调用数据库、Web API 一样轻松集成聊天、文本嵌入、图像生成、语音处理等 AI 能力。更重要的是,Spring AI 完美解决了多 AI 供应商适配的痛点 —— 通过 “一套接口,多种实现” 的统一抽象,开发者可以无缝切换 OpenAI、Anthropic、Bedrock、Hugging Face、Vertex AI、Ollama 等主流 AI 服务,无需修改核心业务代码。同时,它还支持企业内部数据与 AI 模型的快速关联,这正是检索增强生成(RAG)等高级场景的核心需求。官网地址官网地址:https://spring.io/projects/spring-ai官方文档:https://docs.spring.io/spring-ai/reference/index.html中文文档:https://spring-ai.spring-doc.cn/docs/1.0.0/index.html二、Spring AI 核心特性:为什么值得选?Spring AI 覆盖了 AI 应用开发的全流程,其核心特性可以总结为以下 7 点,每一点都精准命中开发者的实际需求:1. 全栈多供应商模型适配,覆盖主流 AI 能力深度对接Anthropic、OpenAI等主流服务商,覆盖聊天交互、文本嵌入、多模态生成、语音交互、内容安全等核心能力。聊天交互(文本对话、多轮上下文对话)文本嵌入(语义向量转换,支撑语义搜索)多模态生成(文生图、图生文)语音交互(音频转录/语音转文字、文本转语音)内容安全(敏感信息检测与审核)所有模型统一接口封装,切换模型无需修改业务代码,提升项目灵活性与可扩展性。2. 标准化抽象 API,统一调用体验Spring AI 提供了 ChatClient、EmbeddingModel、ImageModel 等标准化接口,开发者无需关心底层模型的差异。支持同步/流式调用及模型高级功能(如OpenAI Function Calling),聚焦业务逻辑,提升开发效率。3. 原生集成 Spring Boot,开箱即用遵循Spring Boot设计理念,通过Starter依赖与自动装配实现AI组件一键集成,Spring Initializr可快速生成项目骨架,开箱即用,贴合Java开发者习惯。4. 结构化输出与类型安全,降低维护成本支持将AI非结构化响应自动解析映射到Java POJO,保障类型安全,避免繁琐的字符串解析与类型转换,降低维护成本。5. 内置向量存储与 RAG 支持,激活私有知识库Spring AI 集成了 PostgreSQL/pgvector、Pinecone、Qdrant、Redis、Weaviate 等主流向量数据库,提供元数据过滤、相似度检索能力;同时内置ETL流程,可快速搭建RAG系统,解决大模型“知识过期”“不懂私有数据”痛点。6. 工具调用能力,打通业务系统闭环原生支持模型驱动的工具调用,可将Spring Bean注册为AI可调用工具,实现AI自动调用业务接口、查询数据库等操作,例如:调用天气 API 获取实时气象数据查询企业 CRM 系统获取客户详情执行业务数据统计与分析操作实现AI与业务流程深度融合,让AI从“对话工具”升级为“业务智能入口”,打通需求到执行的全闭环。三、快速上手:环境准备与 Deepseek 对话案例理论再多不如实际动手,下面我们将通过一个完整的案例,教大家搭建 Spring AI 环境,并实现与 Deepseek 模型的对话交互。3.1 环境要求Spring AI 构建在 Spring Boot 3.x 之上,对环境有明确要求:JDK:必须为 17 及以上版本(不支持 Java 8/11/16)Maven:3.6 及以上版本Spring Boot:3.x 系列(本文使用 3.5.0 版本)JDK 17 安装步骤下载地址:https://www.oracle.com/cn/java/technologies/downloads/#java17安装路径建议:D:\Program Files\Java\jdk17\jdk(可自定义)安装成功后,配置环境变量,也可以在 Spring Boot 项目中指定 JDK 版本。3.2 创建 Spring Boot 项目Step 1:访问 Spring Initializr:https://start.spring.io/,或在 IDEA 中直接创建 New Module。项目配置如下:Name:Weiz-SpringAI-QuickStartGroup:com.exampleArtifact:Weiz-SpringAI-QuickStartPackage name:com.example.weizspringaiLanguage:JavaJDK:17Spring Boot:3.5.3Packaging:JarStep 2:Spring Boot版本选择3.5.x,依赖选择:仅需添加「Spring Web」依赖(后续通过 Maven 引入 Spring AI 相关依赖)。创建完成后的项目结构如下:Weiz-SpringAI-QuickStart/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── example/ │ │ │ └── weizspringai/ │ │ │ └── WeizSpringAiQuickStartApplication.java │ │ └── resources/ │ │ └── application.properties │ └── test/ └── pom.xmlStep 3:配置 pom.xml 依赖在 pom.xml 中添加 Spring AI 相关依赖,核心是导入 Spring AI BOM 统一管理版本,并引入 Deepseek 模型 starter:<?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>com.example</groupId> <artifactId>Weiz-SpringAI</artifactId> <version>0.0.1-SNAPSHOT</version> </parent> <artifactId>Weiz-SpringAI-QuickStart</artifactId> <name>Weiz-SpringAI-QuickStart</name> <description>Weiz-SpringAI-QuickStart</description> <properties> <java.version>17</java.version> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-deepseek</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>Step 4:配置 application.properties在 src/main/resources/application.properties 中配置 Deepseek 模型的基础信息:spring.application.name=Weiz-SpringAI-QuickStart server.port=8080 # Deepseek URL spring.ai.deepseek.base-url=https://api.deepseek.com spring.ai.deepseek.api-key=你的Deepseek appkey spring.ai.deepseek.chat.options.model=deepseek-chat 注意:Deepseek API 密钥需要在 Deepseek 官网注册获取,替换配置中的占位符。Step 5:编写 ChatController创建 com.example.weizspringai.controller 包,并编写 ChatController 类,实现与 Deepseek 模型的交互:import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/ai") public class ChatController { @Autowired private DeepSeekChatModel chatModel; @PostMapping("/chat") public ChatResponse chat(@RequestBody ChatRequest chatRequest){ String resp = chatModel.call(chatRequest.getMessage()); return new ChatResponse(resp); } }Step 6:启动并测试运行 WeizSpringAiEmbeddingApplication.java 启动项目。浏览器访问:http://localhost:8080/ai/chat?message=你是谁,即可看到模型响应:你好!我是DeepSeek,由深度求索公司创造的AI助手!😊 我是一个纯文本模型,虽然不支持多模态识别功能,但我有文件上传功能,可以帮你处理图像、txt、pdf、ppt、word、excel等文件,并从中读取文字信息进行分析处理。我完全免费使用,拥有128K的上下文长度,还支持联网搜索(需要你在Web/App中手动点开联网搜索按键)。 你可以通过官方应用商店下载我的App来使用。我很乐意帮助你解答问题、处理文档、进行对话交流等等! 有什么我可以帮你的吗?无论是学习、工作还是日常生活中的问题,我都很愿意协助你!✨四、实战进阶:流式响应与可视化界面优化前面使用SpringAI 快速整合DeepSeek,实现了与大模型对话的功能,但是,这个项目存在两个不足:1. 无可视化交互界面;2. AI 响应为一次性返回,缺乏实时感。下面对项目进行优化升级。Step 1:实现流式返回接口在ChatController 中,创建新接口/ai/chatStream接口。@PostMapping("/chatStream") public SseEmitter chatStream(@RequestBody ChatRequest chatRequest) { SseEmitter emitter = new SseEmitter(Long.MAX_VALUE); Flux<String> stream = chatModel.stream(chatRequest.getMessage()); stream.subscribe( chunk -> { try { ChatResponse chatResponse = new ChatResponse(); chatResponse.setResponse(chunk); chatResponse.setCode(200); chatResponse.setMessage("streaming"); String json = objectMapper.writeValueAsString(chatResponse); emitter.send(SseEmitter.event() .data(json) .build()); } catch (IOException e) { emitter.completeWithError(e); } }, error -> { emitter.completeWithError(error); }, () -> { emitter.complete(); } ); return emitter; }Step 2:创建可视化聊天界面为了更直观地与模型交互,我们可以在 src/main/resources/static 目录下创建 index.html,实现简单的聊天界面:<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>AI 聊天助手(流式响应)</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); height: 100vh; display: flex; justify-content: center; align-items: center; } .chat-container { width: 800px; height: 600px; background: white; border-radius: 16px; box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); display: flex; flex-direction: column; overflow: hidden; } .chat-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 20px; color: white; } .chat-header h1 { font-size: 24px; font-weight: 600; } .chat-header p { font-size: 14px; opacity: 0.9; margin-top: 5px; } .chat-messages { flex: 1; padding: 20px; overflow-y: auto; background: #f5f5f5; } .message { margin-bottom: 16px; display: flex; align-items: flex-start; } .message.user { justify-content: flex-end; } .message.assistant { justify-content: flex-start; } .message-content { max-width: 70%; padding: 12px 16px; border-radius: 12px; line-height: 1.5; word-wrap: break-word; } .message.user .message-content { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-bottom-right-radius: 4px; } .message.assistant .message-content { background: white; color: #333; border-bottom-left-radius: 4px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .chat-input-container { padding: 20px; background: white; border-top: 1px solid #e0e0e0; } .chat-input-form { display: flex; gap: 12px; } #messageInput { flex: 1; padding: 12px 16px; border: 2px solid #e0e0e0; border-radius: 24px; font-size: 16px; outline: none; transition: border-color 0.3s; } #messageInput:focus { border-color: #667eea; } #sendButton { padding: 12px 32px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; border-radius: 24px; font-size: 16px; font-weight: 600; cursor: pointer; transition: transform 0.2s, box-shadow 0.2s; } #sendButton:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); } #sendButton:active { transform: translateY(0); } #sendButton:disabled { background: #ccc; cursor: not-allowed; transform: none; } .typing { display: inline-block; padding: 8px 12px; } .typing::after { content: '...'; animation: typing 1.5s steps(4, end) infinite; } @keyframes typing { 0%, 100% { content: ''; } 25% { content: '.'; } 50% { content: '..'; } 75% { content: '...'; } } .welcome-message { text-align: center; color: #666; padding: 40px 20px; } .welcome-message h2 { font-size: 20px; margin-bottom: 10px; color: #333; } .welcome-message p { font-size: 14px; } </style> </head> <body> <div class="chat-container"> <div class="chat-header"> <h1>AI 聊天助手</h1> <p>基于 Spring AI 和 DeepSeek 的智能对话系统(流式响应)</p> </div> <div class="chat-messages" id="chatMessages"> <div class="welcome-message"> <h2>欢迎使用 AI 聊天助手!</h2> <p>请在下方输入您的问题,我会尽力为您解答。</p> </div> </div> <div class="chat-input-container"> <form class="chat-input-form" id="chatForm"> <input type="text" id="messageInput" placeholder="输入您的问题..." autocomplete="off" required> <button type="submit" id="sendButton">发送</button> </form> </div> </div> <script> const chatMessages = document.getElementById('chatMessages'); const chatForm = document.getElementById('chatForm'); const messageInput = document.getElementById('messageInput'); const sendButton = document.getElementById('sendButton'); const welcomeMessage = document.querySelector('.welcome-message'); // 移除欢迎消息 function removeWelcomeMessage() { if (welcomeMessage) { welcomeMessage.remove(); } } // 添加消息到聊天界面 function addMessage(content, isUser = false) { removeWelcomeMessage(); const messageDiv = document.createElement('div'); messageDiv.className = `message ${isUser ? 'user' : 'assistant'}`; const messageContent = document.createElement('div'); messageContent.className = 'message-content'; messageContent.textContent = content; messageDiv.appendChild(messageContent); chatMessages.appendChild(messageDiv); // 滚动到底部 chatMessages.scrollTop = chatMessages.scrollHeight; } // 添加加载消息 function addLoadingMessage() { removeWelcomeMessage(); const messageDiv = document.createElement('div'); messageDiv.className = 'message assistant'; messageDiv.id = 'loadingMessage'; const messageContent = document.createElement('div'); messageContent.className = 'message-content typing'; messageContent.textContent = 'AI 正在思考'; messageDiv.appendChild(messageContent); chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; } // 移除加载消息 function removeLoadingMessage() { const loadingMessage = document.getElementById('loadingMessage'); if (loadingMessage) { loadingMessage.remove(); } } // 发送消息(流式响应) async function sendMessage(message) { addMessage(message, true); addLoadingMessage(); sendButton.disabled = true; messageInput.disabled = true; try { const response = await fetch('/ai/chatStream', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ message: message }) }); if (!response.ok) { throw new Error('网络请求失败'); } removeLoadingMessage(); // 创建一个新的消息容器用于显示流式响应 const messageDiv = document.createElement('div'); messageDiv.className = 'message assistant'; messageDiv.id = 'currentStreamMessage'; const messageContent = document.createElement('div'); messageContent.className = 'message-content'; messageContent.textContent = ''; messageDiv.appendChild(messageContent); chatMessages.appendChild(messageDiv); chatMessages.scrollTop = chatMessages.scrollHeight; // 处理流式响应 const reader = response.body.getReader(); const decoder = new TextDecoder(); let fullText = ''; while (true) { const { done, value } = await reader.read(); if (done) break; // 解码并解析数据 const chunk = decoder.decode(value, { stream: true }); // 尝试解析 JSON 行 const lines = chunk.split('\n').filter(line => line.trim()); for (const line of lines) { try { // 移除可能的数据前缀(如 "data:") const cleanLine = line.replace(/^data:\s*/, '').trim(); if (!cleanLine) continue; const jsonStr = cleanLine.replace(/,$/, '').trim(); const data = JSON.parse(jsonStr); if (data.response) { fullText += data.response; messageContent.textContent = fullText; chatMessages.scrollTop = chatMessages.scrollHeight; } else if (data.reply) { fullText += data.reply; messageContent.textContent = fullText; chatMessages.scrollTop = chatMessages.scrollHeight; } } catch (e) { console.warn('解析 JSON 失败:', e, line); // 如果解析失败,尝试直接显示文本 if (chunk.trim()) { fullText += chunk; messageContent.textContent = fullText; chatMessages.scrollTop = chatMessages.scrollHeight; } } } } // 移除当前消息的 ID 标记 if (messageDiv) { messageDiv.removeAttribute('id'); } } catch (error) { removeLoadingMessage(); addMessage('抱歉,发生了错误:' + error.message, false); } finally { sendButton.disabled = false; messageInput.disabled = false; messageInput.focus(); } } // 表单提交 chatForm.addEventListener('submit', async (e) => { e.preventDefault(); const message = messageInput.value.trim(); if (!message) return; messageInput.value = ''; await sendMessage(message); }); // 页面加载时聚焦输入框 messageInput.focus(); </script> </body> </html>Step 3:启动并测试重启项目后,访问 http://localhost:8080,即可通过可视化界面与 AI 聊天,例如输入 “你好”,模型会流式返回。总结本文我们从 Spring AI 的核心概念出发,详细介绍了它的 7 大核心特性,并通过一个完整的 Deepseek 对话案例,带大家完成了环境搭建、依赖配置、代码编写与测试的全流程。Spring AI 的核心优势在于 “原生集成 Spring 生态” 与 “统一抽象接口”,让 Java 开发者无需跨生态就能快速接入 AI 能力,极大降低了 AI 应用的开发门槛。版权声明:本文为CSDN博主「小马不敲代码」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/weixin_46619605/article/details/157511534
-
1. 按照任务类型对线程池进行分类在讨论线程数之前,首先需要明确一点:线程数的配置和任务类型是强相关的。 使用标准构造器 ThreadPoolExecutor 创建线程池时,会涉及线程数的配置,而线程数的配置与异步任务类型是分不开的。这里将线程池的异步任务大致分为以下三类:IO 密集型任务此类任务主要是执行 IO 操作。由于执行 IO 操作的时间较长,导致 CPU 的利用率不高,这类任务 CPU 常处于空闲状态。Netty 的 IO 读写操作为此类任务的典型例子。CPU 密集型任务此类任务主要是执行计算任务。由于响应时间很快,CPU 一直在运行,这种任务 CPU 的利用率很高。混合型任务此类任务既要执行逻辑计算,又要进行 IO 操作(如 RPC 调用、数据库访问)。相对来说,由于执行 IO 操作的耗时较长(一次网络往返往往在数百毫秒级别),这类任务的 CPU 利用率也不是太高。Web 服务器的 HTTP 请求处理操作为此类任务的典型例子。一般情况下,针对以上不同类型的异步任务需要创建不同类型的线程池,并进行针对性的参数配置。2. 为 IO 密集型任务确定线程数由于 IO 密集型任务的 CPU 使用率较低,导致线程空余时间很多,因此通常需要开 CPU 核心数两倍的线程。当 IO 线程空闲时,可以启用其他线程继续使用 CPU,以提高 CPU 的使用率。接下来为 IO 密集型任务创建了一个简单的参考线程池,具体代码如下:12345678910111213141516171819202122232425import java.util.concurrent.LinkedBlockingQueue;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit; public class ThreadUtil { private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int THREAD_COUNT = Math.max(2, CPU_COUNT); private static final int QUEUE_COUNT = 128; private static final int KEEP_ALIVE_SECONDS = 30; private static class ThreadPoolExecutorDemo{ private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor( THREAD_COUNT, THREAD_COUNT, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, new LinkedBlockingQueue<>(QUEUE_COUNT), new ThreadPoolExecutor.AbortPolicy() ); }}3. 为 CPU 密集型任务确定线程数CPU 密集型任务也叫计算密集型任务,其特点是要进行大量计算而需要消耗 CPU 资源,比如计算圆周率、对视频进行高清解码等。CPU 密集型任务虽然也可以并行完成,但是并行的任务越多,花在任务切换的时间就越多 CPU 执行任务的效率就越低,所以要最高效地利用 CPU,CPU 密集型任务并行执行的数量应当等于 CPU 的核心数。比如说 4 个核心的 CPU,通过 4 个线程并行执行 4 个 CPU 密集型任务,此时的效率是最高的。但是如果线程数远远超出 CPU 核心数量,就需要频繁地切换线程,线程上下文切换时需要消耗时间,反而会使得任务效率下降。因此,对于 CPU 密集型的任务来说,线程数等于 CPU 数就行。接下来为 CPU 密集型任务创建了一个简单的参考线程池,具体代码如下:1234567891011121314151617181920212223import java.util.concurrent.*; public class ThreadUtil { private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final int THREAD_COUNT = CPU_COUNT; private static final int QUEUE_COUNT = 128; private static final int KEEP_ALIVE_SECONDS = 30; private static class ThreadPoolExecutorDemo{ private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor( THREAD_COUNT, THREAD_COUNT, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS, new LinkedBlockingQueue<>(QUEUE_COUNT), new ThreadPoolExecutor.AbortPolicy() ); }}4. 为混合型任务确定线程数混合型任务既要执行逻辑计算,又要进行大量非CPU 耗时操作(如 RPC 调用、数据库访问、网络通信等),所以混合型任务 CPU 利用率不是太高,非 CPU 耗时往往是 CPU 耗时的数倍。比如在 Web 应用处理 HTTP 请求处理时,一次请求处理会包括 DB 操作、RPC 操作、缓存操作等多种耗时操作。一般来说,一次 Web 请求的 CPU 计算耗时往往较少,大致在 100 - 500 毫秒,而其他耗时操作会占用 500 - 1000 毫秒,甚至更多的时间。在为混合型任务创建线程池时,如何确定线程数呢?在工程实践中,通常会通过 线程等待时间和 CPU 计算时间的比例 来估算线程数,常见的计算思路如下:最佳线程数 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU核数经过简单的换算,以上公式可进一步转换为:最佳线程数目 =(线程等待时间与线程CPU时间之比 + 1)* CPU核数通过公式可以看出:等待时间所占比例越高,需要的线程就越多;CPU 耗时所占比例越高,需要的线程就越少。下面举一个例子:比如在 Web 服务器处理 HTTP 请求时,假设平均线程 CPU 运行时间为 100 毫秒,而线程等待时间(比如包括 DB 操作、RPC操作、缓存操作等)为 900 毫秒,如果 CPU 核数为 8,那么根据上面这个公式,估算如下:(900ms+100ms)/100ms8= 108 = 8经过计算,以上案例中需要的线程数为 80。很多人认为,线程数越高越好。那么,使用很多线程是否就一定比单线程高效呢?答案是否定的,比如大名鼎鼎的 Redis 就是单线程的,但它却非常高效,基本操作都能达到十万量级/秒。由于 Redis 基本都是内存操作,在这种情况下单线程可以高效地利用 CPU,多线程反而不是太适用。多线程适用场景一般是:存在相当比例非 CPU 耗时操作,如 IO、网络操作,需要尽量提高并行化比率以提升 CPU 的利用率。总体来说,线程池线程数并不存在一个放之四海而皆准的固定值。不同类型的任务,其 CPU 使用情况和等待时间差异很大,直接决定了线程数配置的侧重点。对于 IO 密集型、CPU 密集型以及混合型任务,本文给出的配置思路和估算公式可以作为一个起点,但在真实的生产环境中,仍然需要结合具体的业务特性、硬件条件以及压测结果进行不断调整。实际上,线程池真正“难”的地方,往往不止是线程数本身,还包括队列大小、拒绝策略以及运行时的监控和调优。这些问题在复杂系统中同样容易被忽视,后续也值得单独展开讨论。
-
最近使用分布式锁比较多,发现大家使用分布式锁,都各有各的用法,想着用搞一个加锁模板,方便大家使用,就选择声明一个函数式接口,去实现加锁的模板函数式接口是一种特殊的接口,只包含一个抽象方法。函数式接口的目的是为了支持函数式编程,使开发者能够以更简洁的方式定义单一抽象方法的接口,从而可以使用Lambda表达式来实现这个接口的抽象方法。可以通过lambda表达式进行传递,简单理解就是java8对函数式编程的一种支持实战我们一般加锁是这样,尝试获取锁,失败则抛异常,成功则执行业务代码,每次都要写一大段逻辑1234567891011121314151617181920RLock lock = redissonClient.getLock(lockKey); try { if (!lock.tryLock(waitTime, leaseTime, unit)) { throw new CommonException(ErrorCodeEnum.TRY_LOCK_ERROR).detailMessage("操作频繁,请稍后再试!"); } 。。。。业务代码 }catch (CommonException commonException) { } catch (Exception e) { log.error("LockTemplate >> 获取锁异常",e); } finally { try { // 锁不为空 是否还是锁定状态 当前执行线程的锁 if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); } } catch (Exception e) { log.error("LockTemplate >> 获取锁异常",e); } } }所以可以采用函数是接口,定义一套加锁模板,开发者就可以只关注业务代码,而不用去写一大段加锁逻辑,123456789101112131415161718192021222324252627282930313233343536373839404142434445package com.dept.common.template; import cn.hutool.core.util.StrUtil;import com.dept.common.enums.ErrorCodeEnum;import com.dept.common.exception.CommonException;import lombok.AllArgsConstructor;import lombok.extern.slf4j.Slf4j;import org.redisson.api.RLock;import org.redisson.api.RedissonClient;import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service@AllArgsConstructor@Slf4jpublic class LockTemplate { private RedissonClient redissonClient; public void templateLock(String prefix,String key,Runnable target,long waitTime, long leaseTime, TimeUnit unit) { String lockKey = StrUtil.format(prefix,key); // 判断是否正在使用 RLock lock = redissonClient.getLock(lockKey); try { if (!lock.tryLock(waitTime, leaseTime, unit)) { throw new CommonException(ErrorCodeEnum.TRY_LOCK_ERROR).detailMessage("操作频繁,请稍后再试!"); } // 执行业务代码 target.run(); }catch (CommonException e){ throw e; } catch (Exception e) { log.error("LockTemplate >> 获取锁异常",e); } finally { try { // 锁不为空 是否还是锁定状态 当前执行线程的锁 if (lock != null && lock.isLocked() && lock.isHeldByCurrentThread()) { lock.unlock(); } } catch (Exception e) { log.error("LockTemplate >> 获取锁异常",e); } } }}重点就是将Runnable做为入参,Runnable是一个函数式接口,所以我们可以直接传入业务实现,当然我们也可以自己定义一个函数式接口123lockTemplate.templateLock(SystemConstants.STAR_TASK_MINA_ORDER_LOCK, order.getOrderSn(), () -> { 。。。业务实现 }, RedisConstants.ONE_SECOND, RedisConstants.TEN_SECOND, TimeUnit.MINUTES);这样我们就可以不用去关注加锁逻辑,只需要写自己的业务代码接口总结通过函数式接口,我们可以定义很多模板代码,让开发者只需要传入对应的业务实现即可,TransactionTemplate的execute的方法也是类似的实现
-
Java判断时间间隔是否超限的方法?
-
Redis是一款高性能的内存数据库,支持字符串、哈希、列表、集合等多种数据结构,广泛应用于缓存、分布式锁、消息队列等场景。本文基于Spring Boot框架,集成Redis,实现缓存管理(查询缓存、缓存更新、缓存失效)和分布式锁两大核心功能,结合实际业务场景编写完整代码,详解Redis在Java项目中的落地应用。一、环境准备与依赖配置1. 开发环境JDK 1.8、Spring Boot 2.7.x、Redis 6.2.x、Spring Data Redis 2.7.x、Maven 3.6.x、IDEA2. Maven依赖配置在pom.xml中引入Spring Boot Redis依赖、Spring Boot Web依赖等: <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Data Redis(Spring Boot集成Redis的核心依赖) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Redis客户端(默认使用Lettuce,可替换为Jedis) --> <dependency> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency><!-- Spring Boot Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> 3. 配置文件设置在application.yml中配置Redis连接信息(主机、端口、密码、数据库索引)、缓存配置、Lettuce客户端配置等: spring: # Redis配置 redis: host: localhost # Redis服务器地址 port: 6379 # Redis端口 password: 123456 # Redis密码(若未设置则省略) database: 0 # 连接的数据库索引(默认0) timeout: 3000ms # 连接超时时间 # Lettuce客户端配置(连接池) lettuce: pool: max-active: 8 # 连接池最大活跃连接数 max-idle: 8 # 连接池最大空闲连接数 min-idle: 2 # 连接池最小空闲连接数 max-wait: -1ms # 连接池最大阻塞等待时间(-1表示无限制) # 缓存配置(使用Redis作为缓存管理器) cache: type: redis # 缓存类型为Redis redis: time-to-live: 3600000ms # 缓存默认过期时间(1小时) cache-null-values: false # 是否缓存null值(避免缓存穿透,根据业务需求设置) key-prefix: "cache:" # 缓存key前缀 4. Redis配置类创建RedisConfig类,配置RedisTemplate(自定义序列化方式,避免key和value序列化乱码)、缓存管理器等: import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; @Configuration @EnableCaching // 开启缓存注解支持 public class RedisConfig { // 配置RedisTemplate,自定义序列化方式 @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); // 设置key序列化器(String序列化,避免key乱码) StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); redisTemplate.setKeySerializer(stringRedisSerializer); redisTemplate.setHashKeySerializer(stringRedisSerializer); // 设置value序列化器(JSON序列化,支持对象序列化) GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); redisTemplate.setValueSerializer(jsonRedisSerializer); redisTemplate.setHashValueSerializer(jsonRedisSerializer); // 初始化RedisTemplate redisTemplate.afterPropertiesSet(); return redisTemplate; } // 配置缓存管理器,自定义缓存配置 @Bean public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) { // 基础缓存配置(默认过期时间1小时) RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(1)) // 缓存过期时间 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); // 不缓存null值 // 创建缓存管理器 RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory) .cacheDefaults(defaultCacheConfig) // 可为不同缓存设置不同过期时间(如user缓存过期时间30分钟) .withCacheConfiguration("user", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.of
-
IO流是Java核心基础知识点,用于处理文件、网络数据等输入输出操作,在实际开发中应用广泛(如文件上传下载、日志读写、数据导入导出等)。本文将围绕文件上传、文件下载、批量读取文件内容三个核心场景,结合Spring Boot框架,实现完整的IO流实战案例,包含详细代码与场景说明,帮助开发者快速掌握IO流的实际应用。一、IO流核心概念梳理Java IO流按操作方向分为输入流(InputStream/Reader)和输出流(OutputStream/Writer),按操作数据类型分为字节流(InputStream/OutputStream)和字符流(Reader/Writer):1. 字节流:以字节为单位处理数据,适用于所有文件类型(如图片、视频、文档等),核心类包括FileInputStream、FileOutputStream、BufferedInputStream、BufferedOutputStream;2. 字符流:以字符为单位处理数据,适用于文本文件(如txt、java文件等),核心类包括FileReader、FileWriter、BufferedReader、BufferedWriter;3. 缓冲流:基于基础流封装,增加缓冲区功能,减少IO操作次数,提升读写效率(如BufferedInputStream比FileInputStream读写速度更快)。实际开发中,推荐使用缓冲流结合try-with-resources语法(自动关闭流资源,避免资源泄漏),简化代码并提升安全性。二、实战场景一:文件上传(单文件+多文件)文件上传是Web开发常见场景,本文基于Spring Boot+IO流实现单文件和多文件上传功能,限制文件大小和类型,将上传文件保存到本地指定目录。1. 配置文件设置在application.yml中配置文件上传相关参数(上传文件保存路径、最大文件大小、请求大小): spring: servlet: multipart: max-file-size: 10MB # 单个文件最大大小 max-request-size: 50MB # 单次请求最大文件大小 # 自定义文件上传路径(Windows系统可改为D:/upload/files) file: upload: path: /Users/xxx/upload/files/ 2. 工具类:文件上传工具创建FileUploadUtils类,封装文件上传核心逻辑(校验文件类型、生成唯一文件名、写入文件到本地): import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.*; import java.util.UUID; @Component public class FileUploadUtils { // 注入文件上传保存路径 @Value("${file.upload.path}") private String uploadPath; // 允许上传的文件类型(后缀名) private static final String[] ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png", "gif", "txt", "doc", "docx", "pdf"}; // 校验文件类型是否合法 private boolean isAllowedExtension(String originalFilename) { if (originalFilename == null || originalFilename.isEmpty()) { return false; } // 获取文件后缀名 String extension = getFileExtension(originalFilename); for (String allowedExtension : ALLOWED_EXTENSIONS) { if (allowedExtension.equalsIgnoreCase(extension)) { return true; } } return false; } // 获取文件后缀名 private String getFileExtension(String originalFilename) { int lastIndexOfDot = originalFilename.lastIndexOf("."); if (lastIndexOfDot == -1) { return ""; } return originalFilename.substring(lastIndexOfDot + 1); } // 生成唯一文件名(避免文件名重复覆盖) private String generateUniqueFilename(String originalFilename) { String extension = getFileExtension(originalFilename); return UUID.randomUUID().toString() + "." + extension; } // 单文件上传 public String uploadFile(MultipartFile file) throws IOException { // 校验文件是否为空 if (file.isEmpty()) { throw new IOException("上传文件不能为空"); } // 校验文件类型 String originalFilename = file.getOriginalFilename(); if (!isAllowedExtension(originalFilename)) { throw new IOException("不允许上传该类型文件,允许的类型:" + String.join(",", ALLOWED_EXTENSIONS)); } // 生成唯一文件名 String uniqueFilename = generateUniqueFilename(originalFilename); // 创建上传目录(若目录不存在则创建) File uploadDir = new File(uploadPath); if (!uploadDir.exists()) { boolean mkdirs = uploadDir.mkdirs(); if (!mkdirs) { throw new IOException("创建上传目录失败"); } } // 拼接文件完整路径 String filePath = uploadPath + File.separator + uniqueFilename; // 使用IO流将文件写入本地(try-with-resources自动关闭流) try (InputStream inputStream = file.getInputStream(); OutputStream outputStream = new FileOutputStream(filePath); BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) { byte[] buffer = new byte[1024 * 8]; // 8KB缓冲区 int len; while ((len = bufferedInputStream.read(buffer)) != -1) { bufferedOutputStream.write(buffer, 0, len); } // 刷新缓冲区,确保数据全部写入 bufferedOutputStream.flush(); } // 返回文件保存路径(可存储到数据库,用于后续下载) return filePath; } // 多文件上传(批量上传) public String[] uploadFiles(MultipartFile[] files) throws IOException { if (files == null || files.length == 0) { throw new IOException("上传文件不能为空"); } String[] filePaths = new String[files.length]; for (int i = 0; i < files.length; i++) { MultipartFile file = files[i]; // 调用单文件上传方法 String filePath = uploadFile(file); filePaths[i] = filePath; } return filePaths; } } 3. 控制器:文件上传接口创建FileUploadController,提供单文件和多文件上传接口: import com.example.iodemo.utils.FileUploadUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; @RestController @RequestMapping("/api/file") public class FileUploadController { @Autowired private FileUploadUtils fileUploadUtils; // 单文件上传 @PostMapping("/upload/single") public String uploadSingleFile(@RequestParam("file") MultipartFile file) { try { String filePath = fileUploadUtils.uploadFile(file); return "单文件上传成功,文件保存路径:" + filePath; } catch (IOException e) { return "单文件上传失败:" + e.getMessage(); } } // 多文件上传 @PostMapping("/upload/multiple") public String uploadMultipleFiles(@RequestParam("files") MultipartFile[] files) { try { String[] filePaths = fileUploadUtils.uploadFiles(files); return "多文件上传成功,文件保存路径:" + String.join(";", filePaths); } catch (IOException e) { return "多文件上传失败:" + e.getMessage(); } } } 三、实战场景二:文件下载文件下载功能通过IO流读取本地文件,将文件数据写入响应流,返回给前端。本文实现根据文件路径下载文件,并支持设置下载文件名。1. 工具类:文件下载工具创建FileDownloadUtils类,封装文件下载核心逻辑(校验文件是否存在、读取文件数据、写入响应流): import javax.servlet.http.HttpServletResponse; import java.io.*; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; public class FileDownloadUtils { // 文件下载核心方法 public static void downloadFile(String filePath, String downloadFilename, HttpServletResponse response) throws IOException { // 校验文件路径 File file = new File(filePath); if (!file.exists()) { throw new FileNotFoundException("文件不存在,路径:" + filePath); } if (!file.isFile()) { throw new IOException("路径对应的不是文件:" + filePath); } // 设置响应头,告知浏览器以附件形式下载 response.setContentType("application/octet-stream"); response.setContentLength((int) file.length()); // 编码文件名,避免中文乱码 String encodedFilename = URLEncoder.encode(downloadFilename, StandardCharsets.UTF_8.name()); response.setHeader("Content-Disposition", "attachment; filename=\"" + encodedFilename + "\""); // 使用IO流读取文件并写入响应流(try-with-resources自动关闭流) try (InputStream inputStream = new FileInputStream(file); BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream); OutputStream outputStream = response.getOutputStream(); BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream)) { byte[] buffer = new byte[1024 * 8]; int len; while ((len = bufferedInputStream.read(buffer)) != -1) { bufferedOutputStream.write(buffer, 0, len); } bufferedOutputStream.flush(); } } } 2. 控制器:文件下载接口创建FileDownloadController,提供文件下载接口,接收文件路径和下载文件名参数: import com.example.iodemo.utils.FileDownloadUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @RestController @RequestMapping("/api/file") public class FileDownloadController { @GetMapping("/download") public void downloadFile( @RequestParam("filePath") String filePath, @RequestParam("filename") String filename, HttpServletResponse response) { try { // 调用文件下载工具类 FileDownloadUtils.downloadFile(filePath, filename, response); } catch (IOException e) { // 处理异常,设置响应状态 response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); try { response.getWriter().write("文件下载失败:" + e.getMessage()); } catch (IOException ex) { ex.printStackTrace(); } } } } 四、实战场景三:批量读取文本文件内容批量读取文本文件内容是数据处理常见场景(如读取日志文件、导入CSV数据等),本文实现批量读取指定目录下所有txt文件的内容,并统计文件总数量和总字符数。1. 工具类:文件读取工具创建FileReadUtils类,封装批量读取文本文件核心逻辑(遍历目录下文件、读取文件内容、统计数据): import java.io.*; import java.util.ArrayList; import java.util.List; public class FileReadUtils { // 批量读取指定目录下所有txt文件内容 public static FileReadResult batchReadTxtFiles(String dirPath) throws IOException { File dir = new File(dirPath); if (!dir.exists()) { throw new FileNotFoundException("目录不存在:" + dirPath); } if (!dir.isDirectory()) { throw new IOException("路径对应的不是目录:" + dirPath); } // 存储所有文件内容 List<String> allFileContents = new ArrayList<>(); // 统计文件总数量和总字符数 int fileCount = 0; long totalCharCount = 0; // 遍历目录下所有文件 File[] files = dir.listFiles(); if (files == null) { throw new IOException("读取目录文件失败:" + dirPath); } for (File file : files) { // 只处理txt文件 if (file.isFile() && file.getName().endsWith(".txt")) { fileCount++; // 读取单个txt文件内容 String fileContent = readSingleTxtFile(file); allFileContents.add(fileContent); // 统计字符数(去除空格和换行符) totalCharCount += fileContent.replaceAll("\\s+", "").length(); } } // 返回读取结果 return new FileReadResult(fileCount, totalCharCount, allFileContents); } // 读取单个txt文件内容(字符流,避免中文乱码) private static String readSingleTxtFile(File file) throws IOException { StringBuilder contentBuilder = new StringBuilder(); // 使用BufferedReader读取文本文件,指定编码为UTF-8 try (Reader reader = new FileReader(file, java.nio.charset.StandardCharsets.UTF_8); BufferedReader bufferedReader = new BufferedReader(reader)) { String line; while ((line = bufferedReader.readLine()) != null) { contentBuilder.append(line).append("\n"); } } return contentBuilder.toString(); } // 定义文件读取结果实体类 public static class FileReadResult { private int fileCount; // 文件总数量 private long totalCharCount; // 总字符数(去除空格和换行) private List<String> allFileContents; // 所有文件内容 public FileReadResult(int fileCount, long totalCharCount, List<String> allFileContents) { this.fileCount = fileCount; this.totalCharCount = totalCharCount; this.allFileContents = allFileContents; } // getter和setter方法 public int getFileCount() { return fileCount; } public void setFileCount(int fileCount) { this.fileCount = fileCount; } public long getTotalCharCount() { return totalCharCount; } public void setTotalCharCount(long totalCharCount) { this.totalCharCount = totalCharCount; } public List<String> getAllFileContents() { return allFileContents; } public void setAllFileContents(List<String> allFileContents) { this.allFileContents = allFileContents; } } } 2. 控制器:文件读取接口创建FileReadController,提供批量读取文本文件接口: import com.example.iodemo.utils.FileReadUtils; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; @RestController @RequestMapping("/api/file") public class FileReadController { @GetMapping("/read/batch") public FileReadUtils.FileReadResult batchReadTxtFiles(@RequestParam("dirPath") String dirPath) { try { return FileReadUtils.batchReadTxtFiles(dirPath); } catch (IOException e) { throw new RuntimeException("批量读取文件失败:" + e.getMessage()); } } } 五、核心注意事项1. 资源关闭:IO流属于稀缺资源,必须确保关闭,推荐使用try-with-resources语法(JDK7+),自动关闭实现AutoCloseable接口的资源;2. 编码问题:读取文本文件时需指定编码(如UTF-8),避免中文乱码;文件下载时需对文件名进行URLEncoder编码,适配不同浏览器;3. 缓冲区优化:使用缓冲流(BufferedInputStream/BufferedOutputStream等),设置合适的缓冲区大小(一般8KB或16KB),减少IO操作次数,提升性能;4. 异常处理:IO操作易出现异常(如文件不存在、权限不足等),需捕获异常并友好提示,避免程序崩溃;5. 文件大小限制:文件上传时需限制文件大小,避免超大文件占用过多服务器资源,可通过Spring Boot配置或手动校验实现。本文通过三个核心场景实现了IO流的实际应用,代码可直接落地到文件管理、数据导入导出等业务场景,帮助开发者快速掌握IO流的使用技巧与优化方法。
-
MyBatis-Plus是MyBatis的增强工具,在MyBatis基础上简化了CRUD操作,提供了分页插件、条件构造器、代码生成器等强大功能,能够大幅减少开发工作量。本文将基于Spring Boot框架,集成MyBatis-Plus,实现一个用户管理系统,覆盖实体类映射、CRUD操作、分页查询、条件查询等核心功能,包含完整代码与详细说明。一、环境准备与依赖配置1. 开发环境JDK 1.8、Spring Boot 2.7.x、MyBatis-Plus 3.5.x、MySQL 8.0、Maven 3.6.x、IDEA2. Maven依赖配置在pom.xml中引入Spring Boot Web、MyBatis-Plus、MySQL驱动等依赖: <dependencies> <!-- Spring Boot Web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency><!-- MyBatis-Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Lombok(简化实体类编写) --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Spring Boot Test --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> 3. 配置文件设置在application.yml中配置数据库连接信息、MyBatis-Plus相关配置(如Mapper扫描路径、日志打印): spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mybatis_plus_demo?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8&useSSL=false username: root password: root # MyBatis-Plus配置 mybatis-plus: # Mapper接口扫描路径 mapper-locations: classpath:mapper/**/*.xml # 实体类扫描路径(可选,若使用@TableName注解可省略) type-aliases-package: com.example.mybatisplusdemo.entity # 日志打印(开发环境开启,方便调试) configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 数据库表下划线转驼峰(默认开启,可省略) map-underscore-to-camel-case: true 二、数据库表设计创建用户表(sys_user),包含id、username、password、nickname、age、email、create_time、update_time等字段,SQL语句如下: CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加密存储)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `age` int DEFAULT NULL COMMENT '年龄', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; 三、核心代码实现1. 实体类(Entity)创建SysUser实体类,使用Lombok的@Data注解简化getter/setter方法,使用MyBatis-Plus的@TableName、@TableId、@TableField等注解实现实体类与数据库表的映射: import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("sys_user") // 关联数据库表名 public class SysUser { // 主键ID,使用雪花算法自动生成(MyBatis-Plus默认策略) @TableId(type = IdType.AUTO) private Long id; // 用户名,对应表中username字段(若字段名与属性名一致,可省略@TableField) @TableField("username") private String username; // 密码 private String password; // 昵称 private String nickname; // 年龄 private Integer age; // 邮箱 private String email; // 创建时间,自动填充(新增时触发) @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; // 更新时间,自动填充(新增和修改时触发) @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; } 2. 自动填充处理器针对createTime和updateTime字段,实现MyBatis-Plus的元对象处理器,实现字段自动填充(无需手动设置时间): import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import org.apache.ibatis.reflection.MetaObject; import org.springframework.stereotype.Component; import java.time.LocalDateTime; @Component public class MyMetaObjectHandler implements MetaObjectHandler { // 新增时自动填充 @Override public void insertFill(MetaObject metaObject) { // 填充createTime字段 this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); // 填充updateTime字段 this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } // 修改时自动填充 @Override public void updateFill(MetaObject metaObject) { // 填充updateTime字段 this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); } } 3. Mapper接口创建SysUserMapper接口,继承MyBatis-Plus的BaseMapper接口,BaseMapper已封装了CRUD核心方法,无需编写XML即可实现基础操作: import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.mybatisplusdemo.entity.SysUser; import org.apache.ibatis.annotations.Mapper; // @Mapper注解:标识该接口为MyBatis Mapper接口,Spring Boot会自动扫描 @Mapper public interface SysUserMapper extends BaseMapper<SysUser> { // 基础CRUD方法已由BaseMapper提供,如需自定义SQL可在此添加方法 } 4. 服务层(Service)创建SysUserService接口及其实现类,继承MyBatis-Plus的IService和ServiceImpl,ServiceImpl已实现IService接口的核心方法,可直接使用或重写。 // 服务接口 import com.baomidou.mybatisplus.extension.service.IService; import com.example.mybatisplusdemo.entity.SysUser; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; public interface SysUserService extends IService<SysUser> { // 分页查询用户 IPage<SysUser> pageQuery(IPage<SysUser> page, QueryWrapper<SysUser> queryWrapper); // 根据用户名查询用户(自定义方法) SysUser getByUsername(String username); } // 服务实现类 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.mybatisplusdemo.entity.SysUser; import com.example.mybatisplusdemo.mapper.SysUserMapper; import org.springframework.stereotype.Service; @Service public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService { @Override public IPage<SysUser> pageQuery(IPage<SysUser> page, QueryWrapper<SysUser> queryWrapper) { // 调用BaseMapper的selectPage方法实现分页查询 return baseMapper.selectPage(page, queryWrapper); } @Override public SysUser getByUsername(String username) { // 使用QueryWrapper构建查询条件 QueryWrapper<SysUser> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("username", username); // 调用BaseMapper的selectOne方法查询单个结果 return baseMapper.selectOne(queryWrapper); } } 5. 控制器(Controller)创建SysUserController,提供RESTful接口,实现用户的新增、修改、删除、查询、分页查询等功能: import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.mybatisplusdemo.entity.SysUser; import com.example.mybatisplusdemo.service.SysUserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/api/user") public class SysUserController { @Autowired private SysUserService sysUserService; // 新增用户 @PostMapping public String addUser(@RequestBody SysUser sysUser) { boolean save = sysUserService.save(sysUser); return save ? "新增用户成功" : "新增用户失败"; } // 修改用户 @PutMapping("/{id}") public String updateUser(@PathVariable Long id, @RequestBody SysUser sysUser) { sysUser.setId(id); boolean update = sysUserService.updateById(sysUser); return update ? "修改用户成功" : "修改用户失败"; } // 删除用户(逻辑删除可配置,本文为物理删除) @DeleteMapping("/{id}") public String deleteUser(@PathVariable Long id) { boolean remove = sysUserService.removeById(id); return remove ? "删除用户成功" : "删除用户失败"; } // 根据ID查询用户 @GetMapping("/{id}") public SysUser getUserById(@PathVariable Long id) { return sysUserService.getById(id); } // 查询所有用户 @GetMapping("/list") public List<SysUser> getAllUser() { return sysUserService.list(); } // 分页查询用户(支持条件查询,如按昵称、年龄筛选) @GetMapping("/page") public IPage<SysUser> pageUser( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String nickname, @RequestParam(required = false) Integer age) { // 构建分页对象 IPage<SysUser> page = new Page<>(pageNum, pageSize); // 构建查询条件 QueryWrapper<SysUser> queryWrapper = Wrappers.query(); if (nickname != null && !nickname.isEmpty()) { queryWrapper.like("nickname", nickname); // 模糊查询 } if (age != null) { queryWrapper.eq("age", age); // 精确查询 } // 调用分页查询方法 return sysUserService.pageQuery(page, queryWrapper); } // 根据用户名查询用户 @GetMapping("/username/{username}") public SysUser getUserByUsername(@PathVariable String username) { return sysUserService.getByUsername(username); } } 6. 分页插件配置MyBatis-Plus的分页功能需要配置分页插件,创建MyBatisPlusConfig类: import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); return interceptor; } } 四、接口测试与效果验证启动Spring Boot应用,使用Postman或浏览器调用接口进行测试:1. 新增用户:POST /api/user,请求体为JSON格式用户数据,返回“新增用户成功”即表示成功;2. 分页查询:GET /api/user/page?pageNum=1&pageSize=5&nickname=张,返回包含总条数、总页数、当前页数据的分页结果;3. 修改用户:PUT /api/user/1,请求体为修改后的用户数据,返回“修改用户成功”;4. 删除用户:DELETE /api/user/1,返回“删除用户成功”;5. 根据用户名查询:GET /api/user/username/admin,返回对应用户名的用户信息。五、核心功能总结本文基于Spring Boot+MyBatis-Plus实现了用户管理系统,核心优势的在于:1. 简化CRUD操作:通过继承BaseMapper和IService,无需编写大量XML和SQL语句,即可实现基础增删改查;2. 强大的条件构造器:QueryWrapper支持多条件组合查询(精确、模糊、范围等),无需手动拼接SQL;3. 便捷的分页功能:配置分页插件后,只需传入分页参数即可实现分页查询,无需手动处理分页逻辑;4. 自动填充功能:通过元对象处理器,实现创建时间、更新时间等字段的自动填充,减少重复代码。MyBatis-Plus还提供了代码生成器、逻辑删除、乐观锁等功能,可根据业务需求进一步拓展,大幅提升Java项目的开发效率。
-
在Java开发中,并发场景无处不在,线程池作为管理线程的核心组件,能够有效避免频繁创建和销毁线程带来的性能损耗,提升系统并发处理能力。本文将从线程池核心原理出发,结合实际业务场景,实现一个基于线程池的异步任务处理系统,并详解核心代码与优化要点。一、线程池核心原理梳理Java线程池核心类为ThreadPoolExecutor,其构造方法包含7个核心参数,决定了线程池的运行机制: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { // 构造逻辑省略 } 各参数核心作用:corePoolSize:核心线程数,线程池长期保留的线程数量,即使处于空闲状态也不会销毁(除非设置allowCoreThreadTimeOut为true);maximumPoolSize:最大线程数,线程池允许创建的最大线程数量,当任务队列满且核心线程都在工作时,会创建临时线程直至达到该数量;keepAliveTime:临时线程空闲存活时间,超过该时间未处理任务则销毁临时线程;workQueue:任务阻塞队列,用于存储等待执行的任务,常见实现有ArrayBlockingQueue、LinkedBlockingQueue等;threadFactory:线程创建工厂,用于自定义线程名称、优先级等属性;handler:拒绝策略,当线程池与任务队列都满时,对新提交任务的处理方式,常见策略有AbortPolicy(直接抛异常)、CallerRunsPolicy(调用者线程执行)等。线程池核心运行流程:提交任务时,先判断核心线程是否空闲,若空闲则直接执行;若核心线程已满,将任务加入阻塞队列;若队列已满,创建临时线程执行任务;若临时线程达到最大线程数,触发拒绝策略。二、实际业务场景:异步任务处理系统假设业务需求:电商平台订单支付成功后,需要异步执行一系列后续任务(发送支付成功短信、更新订单状态、同步库存、生成交易日志),要求这些任务并行执行,且不影响支付接口的响应速度。此时使用线程池实现异步处理,既能提升接口性能,又能保证任务有序可控。三、完整代码实现1. 线程池配置类通过@Configuration注解创建线程池实例,自定义线程工厂和拒绝策略,适配业务场景需求(核心线程数8,最大线程数16,队列容量100,临时线程空闲时间30秒)。 import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicInteger; @Configuration public class ThreadPoolConfig { // 核心线程数 private static final int CORE_POOL_SIZE = 8; // 最大线程数 private static final int MAX_POOL_SIZE = 16; // 队列容量 private static final int QUEUE_CAPACITY = 100; // 临时线程空闲时间(秒) private static final long KEEP_ALIVE_TIME = 30; @Bean("orderThreadPool") public ThreadPoolExecutor orderThreadPool() { // 自定义线程工厂,设置线程名称前缀 ThreadFactory threadFactory = new ThreadFactory() { private final AtomicInteger threadNum = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("order-handle-thread-" + threadNum.getAndIncrement()); thread.setPriority(Thread.NORM_PRIORITY); return thread; } }; // 拒绝策略:当线程池和队列都满时,由调用者线程执行任务,避免任务丢失 RejectedExecutionHandler rejectedHandler = new ThreadPoolExecutor.CallerRunsPolicy(); // 创建线程池实例 return new ThreadPoolExecutor( CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, new LinkedBlockingQueue<>(QUEUE_CAPACITY), threadFactory, rejectedHandler ); } } 2. 任务接口与实现类定义任务接口OrderTask,包含各异步任务的执行方法,分别实现短信发送、订单状态更新、库存同步、日志生成功能。 import org.springframework.stereotype.Component; // 任务接口 public interface OrderTask { // 发送支付成功短信 void sendPaySms(Long orderId, String phone); // 更新订单状态为已支付 void updateOrderStatus(Long orderId); // 同步库存(减少对应商品库存) void syncStock(Long orderId); // 生成交易日志 void generateTradeLog(Long orderId, BigDecimal amount); } // 任务实现类 @Component public class OrderTaskImpl implements OrderTask { @Override public void sendPaySms(Long orderId, String phone) { // 模拟短信发送耗时操作 try { Thread.sleep(200); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("订单" + orderId + ":向手机号" + phone + "发送支付成功短信"); } @Override public void updateOrderStatus(Long orderId) { // 模拟数据库操作耗时 try { Thread.sleep(150); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("订单" + orderId + ":状态更新为已支付"); } @Override public void syncStock(Long orderId) { // 模拟库存同步耗时 try { Thread.sleep(300); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("订单" + orderId + ":库存同步完成"); } @Override public void generateTradeLog(Long orderId, BigDecimal amount) { // 模拟日志生成耗时 try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } System.out.println("订单" + orderId + ":交易日志生成完成,金额:" + amount); } } 3. 业务服务类创建OrderService类,在支付成功方法中,通过线程池提交异步任务,实现多任务并行执行。 import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import java.math.BigDecimal; import java.util.concurrent.ThreadPoolExecutor; @Service public class OrderService { @Autowired private OrderTask orderTask; @Autowired @Qualifier("orderThreadPool") private ThreadPoolExecutor threadPoolExecutor; // 支付成功处理方法 public void paySuccess(Long orderId, String phone, BigDecimal amount) { // 提交异步任务:发送短信 threadPoolExecutor.submit(() -> orderTask.sendPaySms(orderId, phone)); // 提交异步任务:更新订单状态 threadPoolExecutor.submit(() -> orderTask.updateOrderStatus(orderId)); // 提交异步任务:同步库存 threadPoolExecutor.submit(() -> orderTask.syncStock(orderId)); // 提交异步任务:生成交易日志 threadPoolExecutor.submit(() -> orderTask.generateTradeLog(orderId, amount)); // 支付接口直接返回成功,无需等待异步任务完成 System.out.println("订单" + orderId + ":支付成功,异步任务已提交"); } } 4. 测试类编写测试类,模拟订单支付成功场景,验证线程池异步任务执行效果。 import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.math.BigDecimal; @SpringBootTest @RunWith(SpringRunner.class) public class OrderServiceTest { @Autowired private OrderService orderService; @Test public void testPaySuccess() { // 模拟订单数据 Long orderId = 10001L; String phone = "13800138000"; BigDecimal amount = new BigDecimal("999.00"); // 调用支付成功方法 orderService.paySuccess(orderId, phone, amount); // 等待异步任务执行完成(测试用,实际业务无需此操作) try { Thread.sleep(2000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } 四、核心优化与注意事项1. 线程池参数优化:核心线程数和最大线程数需根据CPU核心数和业务并发量调整,一般核心线程数=CPU核心数+1,最大线程数=2*CPU核心数+1;队列容量需避免过大(导致任务堆积)或过小(频繁触发拒绝策略)。2. 任务异常处理:异步任务中若发生异常,线程池会直接销毁该线程且不抛出异常,需在任务中捕获异常并处理(如记录日志、重试机制),避免线程泄漏。3. 线程池监控:可通过ThreadPoolExecutor的getActiveCount()、getQueue().size()等方法监控线程池运行状态,及时发现任务堆积、线程泄漏等问题。4. 避免使用Executors默认线程池:Executors创建的线程池(如FixedThreadPool、CachedThreadPool)存在队列无界、线程数无上限等问题,易导致OOM,推荐自定义ThreadPoolExecutor。本文通过线程池实现了异步任务处理系统,核心代码可直接落地到电商、支付等并发场景,有效提升系统性能和稳定性。
-
题⽬描述在⼀个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表 1->2->3->3->4->4->5 处理后为 1->2->5示例1输⼊:{1,2,3,3,4,4,5}返回值:{1,2,5}思路及解答hash统计第一次遍历统计频率,第二次遍历删除重复节点javaimport java.util.HashMap;public class Solution { public ListNode deleteDuplication(ListNode head) { if (head == null || head.next == null) { return head; } // 第一次遍历:统计每个节点值出现的次数 HashMap<Integer, Integer> countMap = new HashMap<>(); ListNode current = head; while (current != null) { countMap.put(current.val, countMap.getOrDefault(current.val, 0) + 1); current = current.next; } // 第二次遍历:删除重复节点 ListNode dummy = new ListNode(-1); // 哑节点简化边界处理 dummy.next = head; ListNode prev = dummy; current = head; while (current != null) { if (countMap.get(current.val) > 1) { // 当前节点重复,跳过 prev.next = current.next; } else { // 当前节点不重复,移动prev指针 prev = prev.next; } current = current.next; } return dummy.next; }}时间复杂度:O(n)空间复杂度:O(n)直接遍历(推荐)注意,题目已经提到是排序的节点,那么就可以直接原地删除对⽐前后两个元素,如果相同的情况下,接着遍历后⾯的元素,直到元素不相等的时候,将前⾯的指针指向最后⼀个相同的元素的后⾯,相当于跳过了相同的元素。javapublic class Solution { public ListNode deleteDuplication(ListNode pHead) { //遍历链表,直接删除 if(pHead == null || pHead.next == null) return pHead; ListNode head = new ListNode(0); head.next = pHead; ListNode cur = head.next; ListNode pre = head; while(cur != null){ //将重复的结点都遍历过,然后将后面节点复制给pre结点后面 if(cur.next != null && cur.val == cur.next.val){ while(cur.next != null && cur.val == cur.next.val){ cur = cur.next; } pre.next = cur.next; cur = cur.next; }else{ pre = pre.next; cur = cur.next; } } return head.next; }}空间复杂度为 O(1) ,没有借助额外的空间时间复杂度为 O(n) ,只遍历了⼀次链表递归将大问题分解为当前节点+剩余链表的子问题java/** * 递归法:分治思想解决子问题 * 思路:将大问题分解为当前节点+剩余链表的子问题 * */public class Solution { public ListNode deleteDuplication(ListNode head) { // 递归终止条件:空链表或单节点链表 if (head == null || head.next == null) { return head; } // 情况1:当前节点与下一节点重复 if (head.val == head.next.val) { // 跳过所有重复节点,找到第一个不重复的节点 ListNode node = head.next; while (node != null && head.val == node.val) { node = node.next; } // 递归处理剩余部分 return deleteDuplication(node); } // 情况2:当前节点不重复 else { head.next = deleteDuplication(head.next); return head; } }}时间复杂度:O(n)空间复杂度:O(n) ,递归栈空间三指针法使用pre、cur、next三个指针精确控制删除范围javapublic class Solution { public ListNode deleteDuplication(ListNode head) { if (head == null || head.next == null) { return head; } ListNode dummy = new ListNode(-1); dummy.next = head; ListNode pre = dummy; // 前驱指针 ListNode cur = head; // 当前指针 ListNode next = null; // 后继指针 while (cur != null && cur.next != null) { next = cur.next; // 发现重复节点 if (cur.val == next.val) { // 移动next直到找到不重复的节点 while (next != null && cur.val == next.val) { next = next.next; } // 跳过所有重复节点 pre.next = next; cur = next; } // 没有重复,正常移动指针 else { pre = cur; cur = cur.next; } } return dummy.next; }}时间复杂度:O(n)空间复杂度:O(1)转载自https://www.cnblogs.com/sevencoding/p/19411014
-
题⽬描述给定⼀棵⼆叉搜索树,请找出其中的第 k ⼩的 TreeNode 结点。示例1输⼊:{5,3,7,2,4,6,8},3返回值:{4}思路及解答二叉搜索树的关键性质二叉搜索树具有一个重要特性:中序遍历(左-根-右)BST会得到一个升序排列的节点值序列。因此,寻找第k小的节点本质上就是获取中序遍历序列中的第k个元素。理解这一点是掌握所有解法的基石。递归中序遍历(直观版)算法思路:进行递归中序遍历将遍历到的节点值依次加入一个列表。遍历完成后,列表中的元素就是升序排列的。从列表中取出第k-1个元素(索引从0开始)即为答案。javaclass TreeNode { int val; TreeNode left; TreeNode right; TreeNode(int x) { val = x; }}public class Solution { public int kthSmallest(TreeNode root, int k) { // 用于存储中序遍历结果的列表 List<Integer> inorderList = new ArrayList<>(); // 执行中序遍历 inorderTraversal(root, inorderList); // 返回第k小的元素(列表索引从0开始,所以是k-1) return inorderList.get(k - 1); } /** * 递归中序遍历二叉树 * @param node 当前遍历的节点 * @param list 存储遍历结果的列表 */ private void inorderTraversal(TreeNode node, List<Integer> list) { if (node == null) { return; // 递归终止条件:遇到空节点则返回 } inorderTraversal(node.left, list); // 递归遍历左子树 list.add(node.val); // 访问当前节点,将值加入列表 inorderTraversal(node.right, list); // 递归遍历右子树 }}时间复杂度:O(n)。需要遍历树中的所有n个节点。空间复杂度:O(n)。主要取决于递归调用栈的深度(最坏情况为O(n),树退化成链表)和存储遍历结果的列表(O(n))。迭代中序遍历(提前终止)方法一需要遍历完整棵树,即使答案在很早就已确定。我们可以利用迭代中序遍历实现提前终止,找到第k小的节点后立即返回,提升效率。算法思路:使用一个栈来模拟递归过程。从根节点开始,将所有左子节点压入栈,直到最左边的节点。弹出栈顶元素,这将是当前最小的节点。每弹出一个节点,计数器k减1。当k减到0时,当前节点就是第k小的节点,直接返回。如果k不为0,则转向当前节点的右子树,重复步骤2-4。javapublic class Solution { public int kthSmallest(TreeNode root, int k) { Deque<TreeNode> stack = new LinkedList<>(); TreeNode current = root; while (current != null || !stack.isEmpty()) { // 将当前节点及其所有左子节点压入栈 while (current != null) { stack.push(current); current = current.left; } // 弹出栈顶节点,即当前最小的节点 current = stack.pop(); k--; // 计数器减1 // 如果k减到0,说明找到了第k小的节点 if (k == 0) { return current.val; } // 转向右子树 current = current.right; } // 如果k超出节点总数,返回-1(根据题目保证k有效,此情况可不处理) return -1; }}时间复杂度:最坏情况O(n)(当k=n时仍需遍历大部分节点),平均情况优于O(n),因为可能提前返回。空间复杂度:O(h),其中h是树的高度。栈的深度最大为树高,在平衡BST中为O(log n)。记录子节点数的递归(进阶优化)如果BST结构频繁变动(插入、删除),但需要频繁查询第k小的值,前两种方法每次查询都可能需要O(n)时间。我们可以通过扩展树节点结构,记录以每个节点为根的子树中的节点个数,来优化查询效率。算法思路:修改树节点结构,增加一个字段(如size)表示以该节点为根的子树的总节点数。在插入、删除节点时,维护每个节点的size信息。查询第k小的节点时:从根节点开始。计算左子树的节点数leftSize。如果k <= leftSize,说明目标节点在左子树,递归地在左子树中寻找第k小的节点。如果k == leftSize + 1,说明当前根节点就是目标节点。如果k > leftSize + 1,说明目标节点在右子树,递归地在右子树中寻找第k - (leftSize + 1)小的节点。javaclass TreeNodeWithSize { int val; TreeNodeWithSize left; TreeNodeWithSize right; int size; // 以该节点为根的子树包含的节点总数 TreeNodeWithSize(int x) { val = x; size = 1; // 初始时只有自身 } // 假设插入操作会更新size,这里省略具体的树结构维护代码}public class Solution { public int kthSmallest(TreeNodeWithSize root, int k) { if (root == null) { return -1; } // 计算左子树的节点数(如果左子树为空,则节点数为0) int leftSize = (root.left != null) ? root.left.size : 0; if (k <= leftSize) { // 第k小的节点在左子树 return kthSmallest(root.left, k); } else if (k == leftSize + 1) { // 当前节点就是第k小的节点 return root.val; } else { // 第k小的节点在右子树,在右子树中寻找第 (k - (leftSize + 1)) 小的节点 return kthSmallest(root.right, k - (leftSize + 1)); } }}转载自https://www.cnblogs.com/sevencoding/p/19468628
上滑加载中
推荐直播
-
HDC深度解读系列 - Serverless与MCP融合创新,构建AI应用全新智能中枢2025/08/20 周三 16:30-18:00
张昆鹏 HCDG北京核心组代表
HDC2025期间,华为云展示了Serverless与MCP融合创新的解决方案,本期访谈直播,由华为云开发者专家(HCDE)兼华为云开发者社区组织HCDG北京核心组代表张鹏先生主持,华为云PaaS服务产品部 Serverless总监Ewen为大家深度解读华为云Serverless与MCP如何融合构建AI应用全新智能中枢
回顾中 -
关于RISC-V生态发展的思考2025/09/02 周二 17:00-18:00
中国科学院计算技术研究所副所长包云岗教授
中科院包云岗老师将在本次直播中,探讨处理器生态的关键要素及其联系,分享过去几年推动RISC-V生态建设实践过程中的经验与教训。
回顾中 -
一键搞定华为云万级资源,3步轻松管理企业成本2025/09/09 周二 15:00-16:00
阿言 华为云交易产品经理
本直播重点介绍如何一键续费万级资源,3步轻松管理成本,帮助提升日常管理效率!
回顾中
热门标签