• [技术干货] Java外功精要(3)——Spring配置文件和mybatis
    1.配置文件1.1 概述计算机配置文件:用于存储系统、应用程序的设置信息,通常以文本或结构化数据格式(如JSON、XML、INI等)保存。其核心功能包括但不限于:参数定制:允许用户或管理员调整软件或硬件的运行参数环境适配:根据不同设备或场景加载特定配置(如开发/生产环境)持久化存储:确保重启后设置仍生效SpringBoot配置文件:SpringBoot支持多种类型的配置文件,常见的格式包括properties、yaml和yml,主要用于集中管理应用程序的各种配置参数,简化部署和开发过程中的环境切换YAML和YML本质上是相同的文件格式,只是文件扩展名的不同,两者在功能和使用上没有区别1.2 propertiesproperties配置文件是最早期的配置⽂件格式,也是创建SpringBoot项⽬默认的配置⽂件采用常见的键值对格式(key=value)支持#开头的注释#应用程序名称spring.application.name=configuration#应用程序端口号server.port=8080#数据库连接信息spring.datasource.url=jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=falsespring.datasource.username=rootspring.datasource.password=root一键获取完整项目代码properties1.3 yml采用键值对格式(key: value),冒号后必须有空格数据序列化格式,通过缩进表示层级关系支持#开头的注释spring:  application:    #应用程序名称    name: configuration  #数据库连接信息  datasource:    url: jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=false    username: root    password: root#应用程序端口号server:  port: 8080一键获取完整项目代码yaml1.4 优缺点对比properties优点:语法简单直观,采用key=value形式,适合初学者快速上手与Java生态兼容性极强缺点:缺乏层次结构,复杂配置时容易冗余。上述配置数据库连接信息时spring.datasource前缀冗余不支持数据类型定义,所有值均为字符串,需手动转换yml优点:层次化结构清晰,通过缩进表示层级,适合复杂配置场景支持数据类型(如布尔值、数字),减少手动类型转换缺点:格式错误易导致解析失败(容易忽略冒号后空格)部分旧版工具链兼容性较差,需额外依赖解析库注:SpringBoot同时支持两种格式,混合使用时若key重复,properties优先级高于yml1.5 @Value注解作用:是Spring框架提供了一个@Value注解(org.springframework.beans.factory.annotation.Value),用于将外部配置文件中的值注入到Spring管理的Bean中示例:(properties和yml的读取方式相同)package org.example.configuration.config;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Configuration;@Configurationpublic class Config {    @Value("${spring.application.name}")    private String applicationName;    @Value("${server.port}")    private Integer port;    @Value("${spring.datasource.url}")    private String url;    @Value("${spring.datasource.username}")    private String username;    @Value("${spring.datasource.password}")    private String password;    public void print() {        System.out.println("applicationName=" + applicationName);        System.out.println("port=" + port);        System.out.println("url=" + url);        System.out.println("username=" + username);        System.out.println("password=" + password);    }}package org.example.configuration;import org.example.configuration.config.Config;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ApplicationContext;@SpringBootApplicationpublic class ConfigurationApplication {    public static void main(String[] args) {        ApplicationContext context = SpringApplication.run(ConfigurationApplication.class, args);        Config config = context.getBean(Config.class);        config.print();    }}一键获取完整项目代码java运行结果:applicationName=configurationport=8080url=jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=falseusername=rootpassword=root2.mybatis2.1 概述MyBatis是一款优秀的持久层框架,支持自定义 SQL、存储过程、高级映射以及多种配置方式。它消除了几乎所有的JDBC代码和参数的手动设置以及结果集的检索支持存储过程:指的是数据库管理系统(DBMS)允许用户创建、存储和执行存储过程的能力。存储过程是一组预编译的SQL语句,存储在数据库中,可以被应用程序调用执行支持高级映射:指通过配置或注解实现复杂SQL查询结果与Java对象之间的灵活转换。其核心目标是简化数据库关联操作,提升开发效率支持多种配置方式:mybatis支持注解和xml两种配置方式2.2 前置操作引入依赖:Spring Web,Mybatis Framework,MySQL Driver,Lombok在application.properties/yml中添加数据库连接信息:#应用程序名称spring.application.name=configuration#应用程序端口号server.port=8080#数据库连接信息spring.datasource.url=jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=falsespring.datasource.username=rootspring.datasource.password=root#自动驼峰转换mybatis.configuration.map-underscore-to-camel-case=true一键获取完整项目代码propertiesspring:  application:    #应用程序名称    name: configuration  #数据库连接信息  datasource:    url: jdbc:mysql://127.0.0.1:3306/database_name?characterEncoding=utf8&useSSL=false    username: root    password: root#应用程序端口号server:  port: 8080mybatis:  configuration:     map-underscore-to-camel-case: true #自动驼峰转换一键获取完整项目代码yamlSQL命名规范:采用下划线分隔单词(如order_detail)Java命名规范:大驼峰/小驼峰2.3 注解2.3.1 配置1.创建一个接口,并使用 @Mapper注解 修饰import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface BlogMapper {    //其他代码}一键获取完整项目代码java@Mapper注解:允许开发者直接在接口方法上通过注解配置SQL语句,无需编写XML映射文件。适用于简单SQL场景,能显著减少配置量2.初始化数据create table blog (id int primary key auto_increment,name varchar(128),age int);insert into blog values (null,'刘备',30),(null,'关羽',28),(null,'张飞',25);一键获取完整项目代码sql3.创建对应实体类import lombok.Data;@Datapublic class PersonInfo {    private Integer id;    private String name;    private Integer age;    public PersonInfo(Integer id, String name, Integer age) {        this.id = id;        this.name = name;        this.age = age;    }    public PersonInfo() {    }}一键获取完整项目代码sql2.3.2 CRUDimport com.example.spring_mybatis.model.PersonInfo;import org.apache.ibatis.annotations.*;@Mapperpublic interface BlogMapper {    @Select("select * from blog")    List<PersonInfo> getPersonInfoAll();    @Insert("insert into blog values (#{id},#{name},#{age})")    Integer addPerson(PersonInfo person);    @Update("update blog set name = #{name},age = #{age} where id = #{id}")    Integer updatePerson(PersonInfo personInfo);    @Delete("delete from blog where id = #{id}")    Integer deletePerson(Integer id);}一键获取完整项目代码sql按住alt+insert,可在test目录下生成以上方法的测试方法import com.example.spring_mybatis.model.PersonInfo;import lombok.extern.slf4j.Slf4j;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest@Slf4jclass BlogMapperTest {    private final BlogMapper blogMapper;    @Autowired    public BlogMapperTest(BlogMapper blogMapper) {        this.blogMapper = blogMapper;    }    @Test    void getPersonInfoAll() {        List<PersonInfo> personInfoAll = blogMapper.getPersonInfoAll();        log.info("查询成功,personInfoAll:{}",personInfoAll.toString());        //查询成功,personInfoAll:[PersonInfo(id=1, name=刘备, age=30),        //PersonInfo(id=2, name=关羽, age=28),        //PersonInfo(id=3, name=张飞, age=25)]    }    @Test    void addPerson() {        Integer ret = blogMapper.addPerson(new PersonInfo(null, "赵云", 25));        log.info("添加成功,影响行数:{}",ret.toString());//添加成功,影响行数:1    }    @Test    void updatePerson() {        Integer ret = blogMapper.updatePerson(new PersonInfo(1, "刘备", 35));        log.info("更新成功,影响行数:{}",ret.toString());//更新成功,影响行数:1    }    @Test    void deletePerson() {        Integer ret = blogMapper.deletePerson(4);        log.info("删除成功,影响行数:{}",ret.toString());//删除成功,影响行数:1    }}一键获取完整项目代码sql2.3.3 @Param作用:用于在Mapper接口方法中为形式参数指定名称。当方法有多个参数时,通过该注解明确SQL中引用的参数名,避免依赖参数顺序@Mapperpublic interface BlogMapper {    //@Param    @Update("update blog set name = #{name},age = #{age} where id = #{id}")    Integer updatePersonInfo(@Param("id") Integer userId,@Param("name") String userName,@Param("age") Integer userAge);}一键获取完整项目代码java@SpringBootTest@Slf4jclass BlogMapperTest {    private final BlogMapper blogMapper;    @Autowired    public BlogMapperTest(BlogMapper blogMapper) {        this.blogMapper = blogMapper;    }    @Test    void updatePersonInfo() {        Integer ret = blogMapper.updatePersonInfo(1, "刘玄德", 30);        log.info("更新成功,影响行数:{}",ret.toString());//更新成功,影响行数:1    }}一键获取完整项目代码java2.4 xml2.4.1 配置1.创建一个接口,并使用 @Mapper注解 修饰import org.apache.ibatis.annotations.Mapper;@Mapperpublic interface BlogXMLMapper {    //其他代码}一键获取完整项目代码java2.配置mybatis的xml文件路径mybatis:  mapper-locations: classpath:mybatis/**Mapper.xml #配置mybatis的xml文件路径  #标识位于resources/mybatis路径下任何以Mapper结尾的xml文件为mybatis的配置文件一键获取完整项目代码xml3.在resources/mybatis路径下创建以Mapper结尾的xml文件,并添加如下代码<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><!--namespace:用于指定该XML文件对应的Java接口或类的全限定名--><mapper namespace="com.example.spring_mybatis.mapper_blog.BlogXMLMapper"></mapper>一键获取完整项目代码xml2.4.2 示例在接口中声明方法import com.example.spring_mybatis.model_blog.PersonInfo;import org.apache.ibatis.annotations.Mapper;import java.util.List;@Mapperpublic interface BlogXMLMapper {    List<PersonInfo> getPersonInfoAll();}一键获取完整项目代码java在对应xml文件中实现接口方法id:是MyBatis映射文件中SQL语句的唯一标识符。需与Mapper接口中的方法名一致,保证映射正确resultType:指定SQL查询结果映射的Java对象类型,需为全限定类名<mapper namespace="com.example.spring_mybatis.mapper_blog.BlogXMLMapper">    <select id="getPersonInfoAll" resultType="com.example.spring_mybatis.model_blog.PersonInfo">        select * from blog    </select></mapper>一键获取完整项目代码xml按住alt+insert,可在test目录下生成以上方法的测试方法import com.example.spring_mybatis.model_blog.PersonInfo;import lombok.extern.slf4j.Slf4j;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.test.context.SpringBootTest;import java.util.List;@SpringBootTest@Slf4jclass BlogXMLMapperTest {    private final BlogXMLMapper blogXMLMapper;    @Autowired    public BlogXMLMapperTest(BlogXMLMapper blogXMLMapper) {        this.blogXMLMapper = blogXMLMapper;    }    @Test    void getPersonInfoAll() {        List<PersonInfo> personInfoAll = blogXMLMapper.getPersonInfoAll();        log.info("查询成功,personInfoAll:{}",personInfoAll.toString());    }}一键获取完整项目代码java运行结果:2.5 动态SQL动态SQL:指在程序运行时根据条件或参数动态生成的SQL语句。与静态SQL相比,动态SQL更具灵活性,适用于需要根据不同条件构建查询的场景。例如,在某些web/app进行账号注册时会出现非必填选项mybatis的注解和xml两种方式都能实现动态SQL,但xml较为方便,所以下文使用xml来实现动态SQL2.5.1 trim标签作用:用于自定义字符串截取规则。包含四个属性:prefix:最终结果添加前缀suffix:最终结果添加后缀prefixOverrides:去除首部指定内容suffixOverrides:去除尾部指定内容2.5.2 if标签作用:用于条件判断,通常在where或set语句中使用。当test表达式的值为true时,包含标签内的SQL片段    <insert id="addPersonInfo">        insert into blog        <trim prefix="(" suffix=")" suffixOverrides=",">            <if test="id != null">                id,            </if>            <if test="name != null">                name,            </if>            <if test="age != null">                age,            </if>        </trim>        values        <trim prefix="(" suffix=")" suffixOverrides=",">            <if test="id != null">                #{id},            </if>            <if test="name != null">                #{name},            </if>            <if test="age != null">                #{age},            </if>        </trim>    </insert>一键获取完整项目代码xml2.5.3 where标签作用:替代SQL中的where关键字。当if条件成立时才会加入SQL片段,并自动去除第一个子句的and/or    <select id="getPersonInfoByNameAndAge">        select * from blog        <where>            <if test="name != null">                and name = #{name}            </if>            <if test="age != null">                and age = #{age}            </if>        </where>    </select>一键获取完整项目代码xml2.5.4 set标签作用:用于update语句。当if条件成立时才会加入SQL片段,并自动去除最后一个子句的逗号、    <update id="updatePersonInfo">        update blog        <set>            <if test="name != null">                name = #{name},            </if>            <if test="age != null">                age = #{age},            </if>        </set>        <where>            and id = #{id}        </where>    </update>一键获取完整项目代码xml2.5.5 foreach标签作用:用于集合遍历。主要属性:collection:集合参数名item:当前元素变量名open/close:包围符号separator:分隔符    @Test    void getPersonInfoById() {        ArrayList<Integer> ids = new ArrayList<>();        ids.add(1);        ids.add(2);        ids.add(3);        List<PersonInfo> personInfoById = blogXMLMapper.getPersonInfoById(ids);        System.out.println(personInfoById);    }一键获取完整项目代码java    <select id="getPersonInfoById" resultType="com.example.spring_mybatis.model_blog.PersonInfo">        select * from blog        where id in        <foreach collection="ids" item="id" open="(" close=")" separator=",">            #{id}        </foreach>    </select>一键获取完整项目代码xml2.5.6 include标签作用:用于引用SQL片段,通过refid指定要引用的片段id。需配合sql标签使用,实现代码复用    <sql id="collection">        id,name,age    </sql>    <select id="getPersonInfoAll" resultType="com.example.spring_mybatis.model_blog.PersonInfo">        select        <include refid="collection">        </include>        from blog    </select>一键获取完整项目代码xml2.6 主键返回主键返回:指在数据库插入操作后,自动获取刚插入记录的主键值。在mybatis中使用注解和xml都能获取到返回的主键1.注解实现@Mapperpublic interface BlogMapper {    @Options(useGeneratedKeys = true,keyProperty = "id")    @Insert("insert into blog values (#{id},#{name},#{age})")    Integer addPerson(PersonInfo person);}@SpringBootTest@Slf4jclass BlogMapperTest {    private final BlogMapper blogMapper;    @Autowired    public BlogMapperTest(BlogMapper blogMapper) {        this.blogMapper = blogMapper;    }        @Test    void addPerson() {        PersonInfo personInfo = new PersonInfo(null, "黄忠", 60);        Integer ret = blogMapper.addPerson(personInfo);        log.info("添加成功,影响行数:{},返回的主键值:{}",ret.toString(),personInfo.getId());//添加成功,影响行数:1,返回的主键值:6    }}一键获取完整项目代码java 2.通过xml实现@SpringBootTest@Slf4jclass BlogXMLMapperTest {    private final BlogXMLMapper blogXMLMapper;    @Autowired    public BlogXMLMapperTest(BlogXMLMapper blogXMLMapper) {        this.blogXMLMapper = blogXMLMapper;    }        @Test    void addPersonInfo() {        PersonInfo personInfo = new PersonInfo();        personInfo.setAge(40);        personInfo.setName("曹操");        Integer ret = blogXMLMapper.addPersonInfo(personInfo);        log.info("添加成功,影响行数:{},返回的主键值:{}",ret.toString(),personInfo.getId());//添加成功,影响行数:1,返回的主键值:7    }}一键获取完整项目代码java    <insert id="addPersonInfo" useGeneratedKeys="true" keyProperty="id">        insert into blog        <trim prefix="(" suffix=")" suffixOverrides=",">            <if test="id != null">                id,            </if>            <if test="name != null">                name,            </if>            <if test="age != null">                age,            </if>        </trim>        values        <trim prefix="(" suffix=")" suffixOverrides=",">            <if test="id != null">                #{id},            </if>            <if test="name != null">                #{name},            </if>            <if test="age != null">                #{age},            </if>        </trim>    </insert>一键获取完整项目代码xml2.7 预编译/即时SQL预编译SQL(Prepared Statements):SQL语句在程序运行前被预先编译并存储在数据库中。执行时只需传递参数,无需重新编译SQL语句安全性高:通过参数化查询避免SQL注入攻击。参数化查询是一种将SQL语句与用户输入数据分离的数据库操作方式,查询语句中使用占位符(如?、@param等)代替直接拼接用户输入,执行时通过预编译机制将参数动态绑定到占位符位置性能优化:编译一次,多次执行,减少数据库开销即时SQL(Dynamic SQL):在程序运行时动态生成并立即编译执行,每次执行都可能涉及完整的SQL解析和编译过程灵活性高:可根据运行时条件动态拼接SQL语句潜在风险:直接拼接用户输入可能导致SQL注入性能开销:每次执行需重新编译#占位符会使用预编译机制,将参数值安全地绑定到SQL语句中,防止SQL注入攻击。MyBatis会将#替换为?,然后通过JDBC的预编译功能设置参数值$占位符直接进行字符串替换,将参数值拼接到SQL语句中,不会进行预编译或转义处理SQL注入攻击:当恶意输入" 'or 1 = '1 "时1.预编译SQL@Select("select * from blog where name= #{name}")List<UserInfo> queryByName(String name)一键获取完整项目代码java预编译SQL会根据参数的类型判断是否需要加引号,上述name参数是String类型,需要加引号,这就是参数化查询的作用。最终SQL:select * from blog where name = " 'or 1='1 "; # 整个 'or 1='1 会作为name的值一键获取完整项目代码sql2.即时SQL@Select("select * from blog where name= '${name}'")List<UserInfo> queryByName(String name)一键获取完整项目代码java即时SQL不会判断参数类型从而是否添加引号,所以需要手动加上单引号。最终SQL:select * from blog where name = ''or 1 = '1'; # 因为1=1是恒等式,所以该表的数据会被全部查询出来,这就是SQL注入————————————————原文链接:https://blog.csdn.net/2401_89167985/article/details/152804543
  • [技术干货] 用 Notion + 自动化打造你的个人知识管理系统
    你是不是也这样:看到好文章先收藏,结果再也没打开过;笔记散落在微信、网页、本地文档里,想找时怎么也找不到;学了一堆东西,却没法串联起来形成自己的知识体系?其实,问题不在你不够努力,而在缺乏一个趁手的系统。一、Notion的优点Notion 不是唯一选择,但它有几个独特优势:自由度高:数据库、看板、日历、列表随意组合双向链接:轻松建立知识点之间的关联(类似 Roam Research)全平台同步:手机、电脑、网页端实时更新免费够用:个人使用基本功能完全免费API 开放:可接入自动化工具(如 Zapier、Make)更重要的是——它足够简单,能让你坚持用下去。二、三库一体一个有效的 PKM 系统不需要复杂,关键在于输入 → 整理 → 输出的闭环。我们用三个核心数据库实现:1. 收件箱(Inbox)—— 临时存放所有碎片信息用途:快速保存灵感、链接、摘录,不求完美,只求不丢字段建议:内容(文本/URL)来源(如“知乎”、“播客”、“会议记录”)状态(待处理 / 已归档)创建时间 技巧:在手机桌面放 Notion 快捷方式,看到好内容 5 秒内存入收件箱。2. 知识库(Knowledge Base)—— 结构化沉淀核心内容用途:经过整理的、可长期复用的知识单元(类似维基)字段建议:标题标签(多选:技术/心理学/产品…)相关主题(关联到其他知识条目)原始来源(链接回 Inbox 条目)最后复习时间 关键:每条知识尽量独立、原子化。例如不要写“机器学习笔记”,而是拆成“梯度下降原理”、“过拟合解决方法”等小条目。3. 项目/行动库(Projects & Actions)—— 驱动实践与输出用途:把知识转化为具体行动(写作、做项目、分享)字段建议:任务名称关联知识(多对多链接到 Knowledge Base)截止日期状态(规划中 / 进行中 / 已完成)三、从一条微博到知识卡片假设你在微博看到一段关于“费曼学习法”的精彩解释:步骤 1:快速存入收件箱打开 Notion 手机 App点击“+”新建页面,粘贴原文 + 微博链接标记来源为“微博”,状态为“待处理”步骤 2:每日整理(10 分钟即可)打开收件箱,筛选“待处理”阅读内容,用自己的话重写核心观点(避免直接收藏)在知识库中新建页面《费曼学习法》,填写:核心步骤:1. 选择概念 2. 教给别人 3. 发现漏洞 4. 简化类比关联已有知识:《认知负荷理论》《主动回忆》添加标签:#学习方法 #教育步骤 3:驱动输出在项目库中创建任务:“写一篇费曼学习法实践心得”关联《费曼学习法》知识条目设置一周后提醒这样,一条碎片信息就完成了从“看到”到“内化”再到“输出”的完整旅程。四、减少手动操作1. 用浏览器插件一键保存网页安装Notion Web Clipper ,点击即可将当前网页保存到指定数据库(如收件箱),自动带标题、URL、截图。2. 邮件自动转 Notion(适合订阅内容)通过 Notion API + Zapier:规则:收到发件人邮件动作:自动创建 Inbox 条目,内容为邮件正文3. 微信内容自动同步(进阶)使用工具如 WeChat Notion Sync(开源项目),将微信“文件传输助手”或特定群聊消息自动转发到 Notion(需自建服务)。 知识管理不是为了囤积信息,而是构建一个能自我生长的认知外脑。Notion 只是工具,真正的价值在于你如何用它建立输入、思考、输出的正向循环。 
  • [技术干货] 如何选择日志和监控系统的集成方案?
    选择日志与监控系统的集成方案,是构建高效可观测性体系的关键一步。一个良好的集成不仅能提升故障排查效率,还能降低运维成本、避免数据孤岛。以下是系统化的选型思路和实践建议,适用于从初创团队到大型企业的不同场景。一、明确核心需求在选型前,先回答以下问题:问题决策影响业务规模:日均日志量多少?服务数量?决定是否需要分布式架构、冷热分层SLA 要求:能否容忍分钟级延迟?是否需实时告警?影响采集与查询引擎选型团队技能:是否有专职 SRE?熟悉哪些技术栈?决定自建 or 托管合规要求:数据是否必须私有化部署?排除公有云托管方案预算限制:愿意为可观测性投入多少成本?开源 vs 商业产品的权衡关键原则:不要为“未来可能的需求”过度设计,优先解决当前最痛的点(如“无法快速定位线上错误”)。二、集成架构的三种主流模式1. 统一平台模式(All-in-One)代表方案:Datadog、New Relic、阿里云 ARMS、腾讯云可观测平台特点:日志、指标、链路追踪一体化采集与展示自动关联 trace_id、service 等上下文内置智能告警、仪表盘、SLO 管理适用场景:团队规模小,希望快速上线愿意接受按量付费(通常按主机数或数据量计费)不想维护底层基础设施 优势:开箱即用,体验流畅 劣势:长期成本高,数据迁移困难2. 开源组合模式(Best-of-Breed)典型栈:采集:OpenTelemetry SDK + Fluent Bit / Vector传输:OpenTelemetry Collector存储与查询:Logs → Grafana Loki / ElasticsearchMetrics → Prometheus + Thanos / VictoriaMetricsTraces → Jaeger / Grafana Tempo可视化:Grafana(统一面板)适用场景:需要数据自主可控有运维能力,追求成本优化希望避免厂商锁定 优势:灵活、可扩展、长期成本低 劣势:需自行保障高可用、扩缩容、备份3. 混合模式(Hybrid)策略:核心系统自建(保障安全+成本),边缘或临时项目使用托管服务示例:生产环境:Loki + Prometheus + Tempo(私有部署)测试环境 / 新业务:Datadog(快速验证)优势:兼顾控制力与敏捷性三、关键技术考量点1. 上下文关联能力确保日志、指标、链路能通过共同字段(如 trace_id, service.name, pod)联动查询。推荐做法:应用使用 OpenTelemetry SDK,自动注入 trace_id 到日志日志格式为 JSON,包含标准字段(遵循 OTel Semantic Conventions)2. 采集性能与资源开销避免在应用进程内做 heavy parsing(如正则提取)使用轻量级 agent(如 Fluent Bit < Fluentd,Vector 性能更优)启用批处理和压缩(gzip)减少网络流量3. 查询语言与体验Prometheus 的 PromQL 适合指标分析Loki 的 LogQL 支持标签过滤 + 日志内容搜索Elasticsearch 的 Lucene 语法强大但学习成本高 尽量统一查询入口(如 Grafana),降低用户认知负担4. 告警闭环告警应基于指标(如错误率 > 1%),而非原始日志告警信息需包含上下文链接(如“点击查看相关 Trace”)支持自动静默、升级、通知渠道(企业微信/钉钉/Slack) 实施路线图Phase 1:基础覆盖应用接入 OTel SDK部署统一日志采集(Fluent Bit → Loki)配置核心服务指标监控(Prometheus)Phase 2:关联与告警实现日志 ↔ Trace 联动设置关键业务告警(如 API 错误率、延迟 P99)Phase 3:优化与治理引入采样、冷热分层建立日志规范(禁止随意打 INFO)接入 SLO 和用户体验监控(RUM) 日志与监控的集成,不是“选一个工具”,而是设计一套可持续演进的数据流水线。无论选择开源还是商业方案,核心目标始终是:让工程师在最短时间内,获得最多有效信息。建议从一个小而完整的闭环开始(如“一个服务的日志+指标+告警”),验证流程后再横向扩展。记住:最好的可观测性系统,是那个被团队真正用起来的系统,而不是功能最全的那个。
  • [行业前沿] 可观测性系统的设计与实践
     在复杂的分布式系统中,传统的监控手段——如简单的指标采集和日志查看——已难以满足快速定位问题、理解系统行为和保障用户体验的需求。可观测性(Observability)作为新一代系统洞察方法论,正逐步取代传统监控,成为云原生时代运维与开发的核心能力。本文将深入探讨可观测性的三大支柱(日志、指标、链路追踪)如何协同工作,分析主流技术栈的选型逻辑,并分享构建高效、低成本、可扩展可观测性系统的工程实践。什么是可观测性?可观测性源于控制理论,指通过系统外部输出(如日志、指标)推断其内部状态的能力。在软件工程中,它强调无需修改代码即可理解系统运行时行为,尤其适用于黑盒或灰盒系统。与传统监控的区别在于:监控:基于已知问题设置告警(“已知的未知”)可观测性:支持探索未知问题(“未知的未知”)例如,当用户投诉“页面加载慢”,可观测性系统应能帮助工程师快速下钻:是某个数据库查询变慢?CDN 回源异常?还是第三方 API 延迟升高?可观测性的三大支柱1. 日志(Logs):记录离散事件日志是最原始的系统输出,通常为带时间戳的文本或结构化 JSON。用途:调试、审计、错误追踪挑战:数据量大、非结构化、检索成本高2. 指标(Metrics):聚合的数值序列指标是对系统状态的量化描述,如 CPU 使用率、HTTP 请求数、错误率。特点:高压缩比、低存储成本、适合告警局限:丢失上下文,无法还原具体请求示例:http_requests_total{method="POST", status="500", path="/api/order"}3. 链路追踪(Traces):请求的全链路视图追踪记录单个请求在多个服务间的流转路径,由一系列 Span 组成。核心价值:可视化调用拓扑、定位性能瓶颈、识别异常服务标准协议:OpenTelemetry(OTLP)已成为事实标准,取代早期的 Zipkin、Jaeger 协议。技术栈选型:开源 vs 托管开源自建方案日志:Fluent Bit → Kafka → Loki / Elasticsearch指标:Prometheus + Thanos(长期存储)追踪:OpenTelemetry Collector → Jaeger / Tempo可视化:Grafana(统一面板)优势:成本可控、数据自主、灵活定制代价:运维复杂、需处理扩缩容、高可用保障云托管服务AWS:CloudWatch Logs + X-Ray + Managed PrometheusGCP:Cloud Logging + Cloud Trace + Cloud Monitoring第三方:Datadog、New Relic、Splunk Observability优势:开箱即用、自动扩缩、集成 AI 异常检测代价:按量计费成本高,存在厂商锁定风险选型建议:初创团队/成本敏感 → 开源组合(Loki + Prometheus + Tempo + Grafana)中大型企业/追求效率 → 托管服务 + OpenTelemetry 标准接入统一数据采集:OpenTelemetry 的核心作用OpenTelemetry(OTel)是 CNCF 主导的可观测性标准,提供:语言 SDK:自动注入 trace_id、采集指标(支持 Java、Go、Python 等)Collector:接收、处理、转发 Logs/Metrics/Traces语义约定:统一字段命名(如 http.method, db.statement)典型架构:App (OTel SDK) → OTel Collector → Backend (Loki/Prometheus/Jaeger)通过 OTel,应用只需一次埋点,即可将数据分发至多个后端,避免 vendor lock-in。成本优化策略可观测性数据增长迅猛,需主动控制成本:采样(Sampling)对高频 Trace 按比例采样(如 10%)错误请求 100% 采集,确保关键路径不丢失日志分级生产环境关闭 DEBUG 日志WARN/ERROR 日志强制保留冷热分层存储热数据(7天):高性能存储(Elasticsearch)冷数据(30天+):对象存储(S3)+ 查询引擎(Athena)指标预聚合在 Collector 层对高基数指标进行降维(如合并相似 label)构建一个轻量级可观测性平台假设你正在搭建一个微服务系统,预算有限但需基本可观测能力:采集层应用集成 OTel SDK,自动注入 trace_id 到日志部署 Fluent Bit DaemonSet 采集容器日志传输层使用 OTel Collector 接收数据,做简单过滤和批处理存储与查询Logs → Grafana Loki(低成本,标签索引)Metrics → Prometheus + VictoriaMetrics(高压缩比)Traces → Grafana Tempo(与 Loki 共享对象存储)可视化Grafana 统一面板:日志搜索、指标图表、Trace 下钻联动该方案总资源占用可控制在 2–4 核 CPU + 8GB 内存,适合中小规模集群。常见误区与避坑指南误区1:“日志越多越好”→ 后果:存储爆炸、检索变慢。应聚焦关键路径日志。误区2:“有了 Prometheus 就够了”→ 后果:无法定位具体错误请求。需结合 Trace 和 Logs。误区3:“可观测性只是运维的事”→ 正确做法:开发需在代码中合理埋点(如记录业务关键事件)。误区4:忽略上下文关联→ 必须确保 Logs、Metrics、Traces 共享相同标识(如 trace_id、service.name)。 可观测性不是一堆工具的堆砌,而是一种以问题为导向的系统思维。优秀的可观测性系统,能让工程师在 5 分钟内回答:“用户为什么失败?”、“系统哪里变慢了?”、“这次发布是否引入了风险?”。随着 OpenTelemetry 的普及和云原生生态的成熟,构建高效、统一、低成本的可观测性平台已不再是巨头专属。无论团队规模大小,只要坚持结构化数据、统一上下文、分层存储和成本意识,就能让系统真正“可见、可查、可信赖”。毕竟,在复杂的世界里,看得见,才安心。
  • [行业前沿] 倒排索引在AI场景中的应用有哪些?
     1. 检索增强生成(RAG, Retrieval-Augmented Generation)RAG 是当前大语言模型(LLM)落地的关键架构之一,其核心思想是:在生成答案前,先从外部知识库中检索相关上下文,再将上下文与用户问题一同输入 LLM,从而提升回答的准确性、时效性和可解释性。倒排索引的作用:对知识库文档(如 FAQ、产品手册、维基页面)进行分词并构建倒排索引用户提问时,提取关键词(或经 query expansion 后),通过倒排索引快速召回包含这些关键词的候选文档作为第一阶段粗排(Candidate Generation),高效过滤出 Top-K 相关文档(如 100 篇),供后续精排或向量重排使用优势:检索速度快,适合处理海量文本可解释性强(能明确看到命中了哪些关键词)对拼写错误、同义词可通过 Analyzer(如 IK 分词器 + 同义词库)优化2. 混合检索(Hybrid Search):关键词 + 语义的双重保障纯向量检索虽能理解语义相似性,但对精确术语、实体名、代码片段、ID 等字面匹配需求表现不佳。而倒排索引恰好擅长此类任务。混合策略:两路召回融合:一路用倒排索引做关键词匹配,一路用向量索引做语义匹配,再加权合并结果(如 Reciprocal Rank Fusion)级联过滤:先用倒排索引召回包含关键实体(如“iPhone 15”、“北京”)的文档,再在子集中做向量精排元数据过滤:利用倒排索引对文档的标签、类别、时间等结构化字段进行高效过滤应用场景:电商搜索:“红色连衣裙 2024新款” → 倒排索引确保“红色”“连衣裙”“2024”被精确命中,向量模型理解“新款”语义技术文档搜索:“如何修复 Kubernetes Pod CrashLoopBackOff” → 倒排索引精准捕获错误码,向量模型理解“修复”意图3. 多模态检索中的元数据索引在图像、视频、音频等多模态 AI 系统中,原始内容被编码为向量,但其关联的文本元数据(如标题、标签、OCR 识别结果、ASR 转录文本)仍需高效检索。倒排索引的角色:对图像的 OCR 文本、视频字幕、音频转写内容构建倒排索引用户输入文本查询时,可同时触发:向量路径:将 query 编码为向量,搜索视觉/听觉特征向量文本路径:通过倒排索引搜索元数据中的关键词二者结果融合,提升召回率案例:视频平台搜索“包含‘特斯拉发布会’字幕的视频” → 倒排索引快速定位字幕含该短语的视频片段医疗影像系统:通过报告中的“肺结节”“CT”等关键词召回相关病例,再结合影像向量比对4. 负样本挖掘与训练数据构建在训练对比学习(Contrastive Learning)或双塔模型时,需要大量难负样本(Hard Negative) 来提升模型判别能力。倒排索引辅助负采样:给定一个正样本 query,用倒排索引召回一批“看似相关但实际无关”的文档(如包含部分关键词但主题偏离)这些文档作为高质量负样本,比随机负样本更能提升模型性能5. 实时更新与高吞吐写入场景相比向量索引(如 FAISS、HNSW)通常需要批量重建或复杂增量更新,倒排索引天然支持高频写入和实时可见(如 Elasticsearch 的近实时 refresh)。AI 应用价值:用户行为日志、实时新闻、社交媒体内容等动态数据,可立即被倒排索引收录并参与检索在 RAG 系统中,新上传的文档几秒内即可被查询到,满足企业对“知识即时生效”的需求6. 可解释性与调试支持AI 系统常被视为“黑盒”,而倒排索引提供了透明的检索依据:可展示“因命中关键词‘退款政策’而推荐该文档”便于人工审核、规则干预(如屏蔽某些关键词结果)在合规场景(如金融、医疗)中满足审计要求 
  • [技术干货] 深入剖析现代数据库索引结构:B+树、LSM 树与倒排索引
     在数据驱动的时代,数据库作为信息系统的核心组件,其性能直接决定了应用的响应速度与用户体验。而索引(Index),作为加速数据检索的关键技术,堪称数据库的“高速公路”。不同的数据访问模式催生了多样化的索引结构——从关系型数据库广泛采用的 B+ 树,到 NoSQL 系统偏爱的 LSM 树(Log-Structured Merge-Tree),再到搜索引擎依赖的 倒排索引(Inverted Index)。理解这些索引的设计哲学、适用场景与实现细节,是构建高性能数据系统的基础。一、B+ 树:OLTP 场景的黄金标准1. 结构特点B+ 树是一种多路平衡查找树,专为磁盘 I/O 优化设计:所有数据存储在叶子节点,非叶子节点仅保存索引键和指针叶子节点通过链表串联,支持高效的范围查询节点大小通常设为一页(如 4KB 或 16KB),减少磁盘随机读取次数例如,在 MySQL InnoDB 中,主键索引即为聚簇 B+ 树,行数据直接存储在叶子节点;二级索引则为非聚簇 B+ 树,叶子节点存储主键值。2. 优势与适用场景点查高效:时间复杂度 O(logₙN),n 为分支因子(通常数百)范围扫描快:叶子节点链表顺序遍历,避免回溯写入稳定:每次更新仅修改少量页面,支持 WAL(Write-Ahead Logging)保证事务原子性3. 写放大与碎片问题频繁的随机插入可能导致页分裂,产生空间碎片;删除操作则留下“空洞”,需定期 OPTIMIZE TABLE 回收空间。二、LSM 树:为写密集型负载而生当写入吞吐成为瓶颈(如日志、时序数据、消息队列),B+ 树的随机写开销显得力不从心。LSM 树通过将随机写转化为顺序写,极大提升写入性能。1. 核心思想:分层合并LSM 树将数据分为多层(Level),典型结构如下:MemTable:内存中的有序结构(如 SkipList),接收新写入Immutable MemTable:写满后冻结,异步刷入磁盘SSTable(Sorted String Table):磁盘上的只读有序文件Compaction:后台合并小 SSTable 为大文件,清除过期/删除数据写入流程:数据先写入 WAL(保证持久性)插入 MemTable(内存操作,极快)MemTable 满后转为 Immutable,刷盘生成 Level 0 SSTable后台线程定期执行 Compaction,合并多层 SSTable2. 优势与代价写入吞吐极高:顺序写磁盘,接近硬件极限空间放大:Compaction 过程中临时占用额外空间读放大:需查询多层 SSTable 和 MemTable,可能触发多次 I/O写放大:同一数据在 Compaction 中被反复重写3. 优化策略布隆过滤器(Bloom Filter):快速判断 key 是否存在于某 SSTable,减少无效 I/O分层 Compaction(Leveled) vs 大小分级(Size-Tiered):权衡读写放大与空间效率三、倒排索引:全文搜索的基石在搜索引擎或需要文本匹配的场景中,传统索引无法高效支持“关键词 → 文档”的反向查找。倒排索引应运而生。1. 结构组成倒排索引由两部分构成:词典(Dictionary):所有唯一词条的有序列表,通常用 B+ 树或 FST(Finite State Transducer)存储倒排列表(Posting List):每个词条对应的文档 ID 列表,常附带位置、频率等元数据例如,对两篇文档建立索引:Doc1: "The quick brown fox"Doc2: "Jumped over the lazy dog"倒排列表:"the" → [Doc1(pos=0), Doc2(pos=2)]"fox" → [Doc1(pos=3)]...2. 高效压缩与跳表优化Delta 编码:文档 ID 通常递增,存储相邻差值(如 [100, 105, 110] → [100, 5, 5])跳表(Skip List):在长 Posting List 中设置跳跃指针,加速“AND/OR”查询的交并集计算3. 实时更新挑战传统倒排索引为静态构建。为支持实时写入,现代引擎(如 Elasticsearch)采用:分段(Segment)机制:新数据写入新 Segment,后台合并近实时(NRT)搜索:通过 refresh 操作使新数据可查(默认 1 秒)四、三大索引对比与选型指南特性B+ 树LSM 树倒排索引核心优势点查 & 范围查询快写入吞吐高文本关键词检索高效写入性能中等(随机写)极高(顺序写)中等(需分段合并)读取性能稳定低延迟可能较高(多层查找)依赖压缩与缓存事务支持强(MVCC + WAL)弱(通常最终一致)无典型系统MySQL, PostgreSQLCassandra, RocksDBElasticsearch, Lucene适用场景OLTP、高一致性业务日志、时序、IoT搜索、文本分析五、混合索引:现代数据库的融合趋势单一索引难以满足复杂需求,新一代数据库开始融合多种结构:TiDB:底层使用 LSM 树(RocksDB),上层构建分布式 B+ 树索引MongoDB:默认 B 树索引,同时支持全文检索(倒排)和地理空间索引ClickHouse:主键使用稀疏索引(类似 B+ 树),配合 MergeTree 引擎优化分析查询此外,向量索引(如 HNSW、IVF)正随着 AI 应用兴起,用于相似性搜索,进一步拓展索引的边界。六、结语索引是数据库性能的“命门”,其选择绝非技术炫技,而是对业务访问模式的深刻理解。B+ 树在事务型系统中屹立不倒,LSM 树在写密集场景大放异彩,倒排索引则撑起了整个搜索生态。作为工程师,我们不仅要知其然,更要知其所以然——理解每种索引背后的权衡(Trade-off):读写放大、空间效率、一致性模型。唯有如此,才能在面对海量数据、高并发、低延迟的挑战时,做出最合理的架构决策。未来,随着硬件演进(如持久内存、NVMe)与 AI 负载增长,索引技术将继续演化,但其核心使命始终不变:让数据触手可及。
  • [交流吐槽] FileReader/Writer 修改后缀名的避坑指南
    全网同名「悬着的心终于死了」,全网同名认证~  题主要批量修改网上下来的一些图片的后缀名,因为之前学艺不精,搞出来很多问题,这里记录一下(>_<)。之前学习操作文件的时候很草率,总结了一套文件基本操作流程:现在回来才发现这个套路并不适合所有的文件类型先上错误代码:import java.io.*;public class Main { public static void main(String[] args) throws Exception { String imgPath="D:\\文件夹/00015.webp"; File file=new File(imgPath); FileReader fileReader=new FileReader(file); FileWriter fileWriter=new FileWriter(file); String fatherPath=file.getParent(); String name=file.getName(); System.out.println(file.renameTo(new File(fatherPath,toJpg(name)))); try { fileReader.close(); fileWriter.close(); }catch (Exception e){ e.printStackTrace(); } } public static String toJpg(String oName){ int path=oName.lastIndexOf("."); return oName.substring(0,path)+".jpg"; }}操作的时候,各种错误:这里我总结为2个问题:1.FileWriter错误使用就像前面所说,我是直接套“模板”写,FileReader和FileWriter虽然没用到,但没报错就没删(这次长记性了>m<)经过排查,发现因为我多写这一句FileWriter才导致图片损坏这里是搜集到的结果如果 WebP 文件已经损坏,可以考虑使用专门的工具进行修复。例如,FabConvert 提供了一个免费的 WEBP 修复工具,可以在任何具有现代网络浏览器的系统上运行,并且没有使用限制未正确关闭流:在使用 FileWriter 写入数据后,如果没有正确关闭流,可能会导致数据未完全写入,从而损坏文件 。异常处理不当:如果在写入过程中发生异常,而异常没有被正确捕获和处理,可能会导致文件处于不一致的状态 。写入中途断电或系统崩溃:在写入过程中,如果遇到断电或系统崩溃等意外情况,可能会导致文件写入未完成,从而损坏文件。文件系统限制:某些文件系统可能有写入限制,例如最大文件大小或特定格式要求,不遵守这些限制可能会导致文件损坏。缓存问题:有时候浏览器可能会缓存旧版本的图片文件,导致新的 WebP 图片无法加载,这可能是文件损坏的一个表现 。文件损坏:检查 WebP 图片文件是否损坏或完整。有时候图片文件可能会在上传或保存过程中出现问题 。为了避免文件损坏,应该采取以下措施:确保在使用 FileWriter 后正确关闭它。使用异常处理来捕获并处理写入过程中可能发生的错误。避免在没有适当同步机制的情况下进行并发写入。使用事务或日志记录来确保写入操作的原子性和一致性。检查磁盘空间,并确保应用程序有足够的权限来写入文件。这里我并没有用FileWriter写入数据,所以应该是文件系统限制的原因,希望有懂的大佬可以在评论区解答一下(ˊ˘ˋ*)♡。2.FileReader错误使用这里通过反复测试发现,在没有FileReader这一句是file.renameTo()是可以执行的,原因是在使用FileReader时会资源锁定。以下为总结:在许多编程语言中,当你使用 FileReader 或类似的文件读取类与一个 File 对象关联后,你通常不能再直接操作这个 File 对象来读取文件。这主要是因为以下几个原因:资源锁定:一旦 FileReader 打开了一个文件,操作系统可能会锁定这个文件,防止其他进程或线程同时读取或写入,以避免数据损坏或冲突。状态管理:FileReader 可能内部维护了文件的状态信息,如当前读取位置。如果尝试用同一个 File 对象再次创建 FileReader,可能会遇到状态不一致的问题。设计模式:编程语言的设计可能鼓励使用流式操作,即一次只通过一个流(如 FileReader)来处理文件,而不是同时打开多个流。资源释放:如果 FileReader 没有被正确关闭,它可能会持续占用文件资源,导致其他操作无法进行。API限制:某些编程语言或库可能在API设计上限制了对同一个文件对象的多次使用,以简化资源管理和错误处理。如果需要在同一个程序中多次读取同一个文件,通常的做法是:在每次读取操作之后,确保关闭 FileReader 对象。如果需要再次读取,可以重新打开文件,创建一个新的 FileReader 对象。例如,在Java中,你可以这样做:File file = new File("path/to/your/file.txt");// 第一次读取FileReader reader1 = new FileReader(file);// ... 执行读取操作 ...reader1.close();// 第二次读取FileReader reader2 = new FileReader(file);// ... 执行读取操作 ...reader2.close();综上,修改了这两个问题后,修改文件后缀名就成功了import java.io.*;public class Main { public static void main(String[] args) throws Exception { String imgPath="D:\\文件夹/00015.webp"; File file=new File(imgPath); //FileReader fileReader=new FileReader(file); //FileWriter fileWriter=new FileWriter(file); String fatherPath=file.getParent(); String name=file.getName(); System.out.println(file.renameTo(new File(fatherPath,toJpg(name)))); } public static String toJpg(String oName){ int path=oName.lastIndexOf("."); return oName.substring(0,path)+".jpg"; }最后叨叨一句,自己准备的模板一定要完全弄懂口牙_(:зゝ∠)_
  • [互动交流] 跑JAVA代码时候发现JFrame这个图形化的类没办法使用
    如图,跑JAVA代码时候发现JFrame这个图形化的类没办法使用
  • [行业前沿] 如何优化 JFR(Java Flight Recorder)分析的性能?
    JFR(Java Flight Recorder)是 JDK 内置的高性能诊断工具,以极低开销记录 JVM 和应用运行时的关键事件。然而,不当配置可能导致录制开销升高、文件过大或分析困难。尤其在高并发、长时间运行的生产环境(如鲲鹏 ARM64 服务器)中,合理优化 JFR 的使用策略至关重要。本文将从 录制配置、资源控制、事件筛选、分析效率 四个维度,系统讲解如何优化 JFR 的性能表现。一、核心原则:平衡“数据完整性”与“运行开销”JFR 的默认配置(profile.jfc)已针对通用场景做了权衡,但实际业务需根据目标调整:目标推荐策略长期监控(7×24)仅启用关键事件(GC、线程、CPU 采样),降低采样频率故障复现启用详细事件(方法、异常、I/O),短时间高保真录制性能压测对比使用统一配置,确保数据可比性二、优化录制阶段的性能1. 选择合适的预设模板JDK 提供两个内置模板:default.jfc:基础事件(低开销,适合长期运行)profile.jfc:包含方法采样、锁竞争等(中等开销,适合性能分析)建议:# 长期监控用 default-XX:StartFlightRecording=settings=default,duration=1h,filename=/data/jfr/low.jfr# 深度分析用 profile-XX:StartFlightRecording=settings=profile,duration=5m,filename=/data/jfr/high.jfr 毕昇 JDK 还提供 kunpeng-optimized.jfc(如有),可进一步适配 ARM64。2. 自定义 .jfc 配置文件(推荐)复制并修改模板,关闭非必要事件:<!-- custom-low-overhead.jfc --><configuration ...> <event name="jdk.MethodSampling"> <setting name="enabled">false</setting> <!-- 关闭方法采样 --> </event> <event name="jdk.JavaMonitorEnter"> <setting name="enabled">true</setting> <setting name="threshold">10ms</setting> <!-- 仅记录 >10ms 的锁等待 --> </event> <event name="jdk.GCPhasePause"> <setting name="enabled">true</setting> </event></configuration>启动时指定:-XX:StartFlightRecording=settings=/path/to/custom-low-overhead.jfc,...3. 控制录制时长与文件大小避免无限制录制导致磁盘爆满:# 方式1:固定时长duration=10m# 方式2:循环录制(保留最近数据)maxsize=500MB,maxage=1h# 方式3:条件触发(JDK 17+ 支持)-XX:FlightRecorderOptions:repository=/tmp/jfr-cache生产建议:单文件 ≤ 1GB总录制时长 ≤ 30 分钟(除非明确需要长期趋势)4. 调整采样频率(降低 CPU 开销)关键参数:jdk.ThreadCPULoad:线程 CPU 采样间隔(默认 1s)jdk.ExecutionSample:方法栈采样间隔(默认 10ms)在自定义 .jfc 中调整:<event name="jdk.ExecutionSample"> <setting name="period">100ms</setting> <!-- 从 10ms 放宽到 100ms --></event> 注意:采样间隔越长,热点方法识别精度越低。三、减少 I/O 与内存开销1. 使用高速存储路径将 .jfr 文件写入 SSD 或内存盘:filename=/dev/shm/app.jfr # 写入 tmpfs(内存文件系统) 优势:避免磁盘 I/O 成为瓶颈 风险:重启丢失,需及时备份2. 启用压缩(JDK 17+)-XX:FlightRecorderOptions=compress=true可减少 30%~50% 文件体积。3. 避免多进程同时写同一目录每个 Java 进程应使用独立子目录,防止文件锁竞争。四、优化分析阶段的效率1. 使用命令行快速筛查(避免 GUI 开销)# 查看 GC 暂停总时间jfr print --events GCPhasePause app.jfr | grep "duration"# 统计最耗时的 10 个方法jfr summary app.jfr --category "Code" | head -n 102. 在分析机而非生产机运行 JMC将 .jfr 文件拷贝至开发机或专用分析服务器;避免在生产环境启动图形界面工具。3. 使用脚本自动化分析结合 jfr 命令 + Shell/Python 脚本,实现:自动提取关键指标生成性能报告触发告警(如 GC 暂停 > 100ms)示例脚本片段:MAX_PAUSE=$(jfr print --events GCPhasePause app.jfr | awk '/duration/ {print $2}' | sort -nr | head -1)if [ "$MAX_PAUSE" -gt 100000000 ]; then # 100ms in nanoseconds echo "ALERT: Max GC pause exceeds 100ms!"fi五、鲲鹏 ARM64 环境下的特别建议优先使用毕昇 JDK其 JFR 实现针对鲲鹏处理器的缓存、NUMA 架构优化,事件采集效率更高。关注 ARM64 特有事件如:jdk.CPULoad:ARM64 大小核调度可能影响 CPU 利用率jdk.NativeLibrary:验证 native 库是否为 ARM64 编译对比 x86 基线在相同负载下,分别录制 x86 与鲲鹏的 JFR 数据,使用 JMC 的 Compare 功能 定位架构差异点。 
  • [技术干货] 鲲鹏服务器上的 Java 性能调优利器
    什么是 JFR?Java Flight Recorder(JFR)是 Oracle 自 JDK 7 引入、并在 JDK 11+ 开源的高性能事件记录框架。它以极低的性能开销(通常 <1%)持续收集 JVM 和应用层的关键事件,包括:GC 活动(暂停时间、堆变化)线程状态(阻塞、锁竞争)方法采样(CPU 热点)I/O 操作异常抛出类加载JIT 编译行为优势:内置于 JDK,无需额外依赖支持长时间录制(小时级)事件粒度细、上下文完整官方工具 JMC(Java Mission Control)提供可视化分析启用 JFR 并录制数据方法 1:启动时开启录制(推荐用于生产)java -XX:+FlightRecorder \ -XX:StartFlightRecording=duration=300s,filename=/tmp/app.jfr,name=MyAppProfile \ -jar your-app.jar参数说明:duration=300s:录制 5 分钟后自动停止filename:输出文件路径name:录制会话名称方法 2:运行时动态开启(需 JDK 14+ 或使用 jcmd)# 查看 Java 进程 PIDjps# 启动 60 秒录制jcmd <PID> JFR.start duration=60s filename=/tmp/runtime.jfr# 手动停止(可选)jcmd <PID> JFR.stop name=1注意:鲲鹏服务器上建议将 .jfr 文件保存至高速 SSD,避免 I/O 影响录制性能。 分析 JFR 数据工具 1:Java Mission Control(JMC)下载 JMC(支持 ARM64):华为毕昇 JDK 自带 JMC或从 Adoptium 下载 ARM64 版打开 .jfr 文件:jmc /tmp/app.jfr关键分析视图:Memory → Garbage Collections:GC 频率与暂停时间Threads → Thread Dump:线程阻塞与锁竞争Code → Hot Methods:CPU 消耗最高的方法I/O → File Read/Write:磁盘 I/O 瓶颈工具 2:命令行快速查看(无需 GUI)# 使用 jfr 工具(JDK 19+ 内置)jfr print --events GCPhasePause /tmp/app.jfr# 统计方法采样jfr summary /tmp/app.jfr 在鲲鹏服务器上,JFR 是 Java 应用性能调优不可或缺的工具。结合毕昇 JDK 的 ARM64 优化,可以:精准识别 GC、锁、I/O 等瓶颈;对比 x86 与 ARM64 的性能差异;验证 JVM 参数调优效果;构建性能基线,支撑容量规划。
  • [互动交流] 鸿蒙版CodeArts多久能支持svn和maven
    鸿蒙版CodeArts IDE,进行Java开发spring boot微服务,支持svn和maven配置吗?如果不支持,有没有其他的解决办法,如果有的话,有详细步骤吗?第一次用CodeArts IDE,还不知道怎么用呢
  • [介绍/入门] Spring Boot 数组的使用详解
    在 Spring Boot 开发中,数组(Array)作为一种基础而高效的数据结构,广泛应用于参数接收、配置管理、批量处理等场景。尽管 Java 生态中更常使用 List、Set 等集合类型,但在特定场合(如性能敏感、固定长度、与前端/接口协议对齐等),原生数组仍有不可替代的价值。一、控制器中接收数组参数1.1 接收 URL 查询参数中的数组前端可通过重复参数名传递数组,Spring Boot 自动绑定为 Java 数组或 List。@RestControllerpublic class UserController { // 方式1:使用数组接收 @GetMapping("/users") public String getUsersByIds(@RequestParam("ids") Long[] ids) { return "Received IDs: " + Arrays.toString(ids); } // 方式2:使用 List 接收(推荐) @GetMapping("/users/list") public String getUsersByIdsList(@RequestParam("ids") List<Long> ids) { return "Received IDs: " + ids; }}请求示例:GET /users?ids=1&ids=2&ids=3 支持:Long[]、String[]、int[] 等基本类型及包装类数组 注意:若参数为空(如 /users),ids 将为 null,建议加 required = false 或设默认值@RequestParam(value = "ids", required = false, defaultValue = "") String[] ids1.2 接收 JSON 请求体中的数组当请求体为 JSON 格式时,可直接映射到对象字段中的数组。public class BatchUpdateRequest { private Long[] userIds; // 或 List<Long> userIds private String status; // getter/setter}@PostMapping("/batch-update")public ResponseEntity<String> batchUpdate(@RequestBody BatchUpdateRequest request) { System.out.println("User IDs: " + Arrays.toString(request.getUserIds())); return ResponseEntity.ok("Success");}请求体示例:{ "userIds": [101, 102, 103], "status": "ACTIVE"}Jackson 默认支持数组反序列化若需自定义行为,可配置 ObjectMapper二、在配置文件中使用数组2.1 application.yml 中定义数组app: supported-languages: [zh-CN, en-US, ja-JP] max-retry-attempts: 3 admin-ids: - 1001 - 1002 - 10032.2 使用 @ConfigurationProperties 绑定@Component@ConfigurationProperties(prefix = "app")@Data // Lombok 注解,自动生成 getter/setterpublic class AppProperties { private String[] supportedLanguages; private int maxRetryAttempts; private Long[] adminIds;} 支持 String[]、int[]、Long[] 等 不支持泛型数组(如 List<String> 需用 List)2.3 使用 @Value 注入数组(不推荐复杂场景)@Value("${app.supported-languages}")private String[] supportedLanguages;// 或使用 SpEL 表达式(适用于简单字符串数组)@Value("#{'${app.admin-ids}'.split(',')}")private Long[] adminIds; // 需确保配置为逗号分隔 缺点:类型转换弱、错误提示不友好,建议优先使用 @ConfigurationProperties三、数组与 JSON 序列化Spring Boot 默认使用 Jackson 处理 JSON。3.1 返回数组作为响应体@GetMapping("/languages")public String[] getSupportedLanguages() { return new String[]{"zh-CN", "en-US", "ja-JP"};}响应结果:["zh-CN","en-US","ja-JP"]3.2 自定义序列化行为(可选)若需控制空数组、null 值等行为,可在字段或类上添加注解:public class ApiResponse { @JsonInclude(JsonInclude.Include.NON_EMPTY) private String[] tags = {}; // 空数组不返回 // getter/setter}
  • [技术干货] Java进阶 —— 多线程并发-转载
    一、基础概念进程         我们知道CPU是主机上的中央核心处理器,CPU的核数代表着主机能在一个瞬间同时并行处理的任务数,单核CPU只能在内存中并发处理任务。而在现有的操作系统中,几乎都支持进程这个概念。进程是程序的在内存中的一次执行过程,具有一定的独立功能,进程是系统进行资源分配和调度的一个独立单位。线程        线程在程序中是独立的、并发的执行流,与分隔的进程相比隔离性会更小,线程之间共享内存、文件句柄和其它的进程应有的状态。线程比进程具有更高的性能,这是由于同一进程中的线程具有共性。简单理解,多线程是进程中并行执行的多个子程序。并发性和并行的区别        并行是指在同一时刻,有多条指令在多个处理器上同时执行;而并发是指在同一时刻只能执行,但是通过多进程快速轮换执行可以达到同时执行的效果。CPU主频就代表着这些进程之间频繁切换的速度。二、创建线程的三种方式2.1 通过继承Thread类来启用Java语言中JVM允许程序运行多个线程并通过java.lang.Thread类来实现。Thread类的特性        每个线程都是通过某个特定Thread对象的run()方法来完成操作的,经常把run()方法的主体称为线程体,并通过该Thread对象的start()方法来启动线程。流程:定义子类继承Thread类;子类中重写Thread类中的run方法;创建Thread子类对象,即创建了线程对象;调用线程对象start方法:启动线程,调用run方法。 具体代码示例首先构建一个继承Thread类的子类//继承Thread类的方式实现多线程public class TestThread extends Thread{    @Override    public void run(){        System.out.println("多线程运行的代码");    }}AI写代码java运行 调用线程public class Test{    public static void main(String[]args){        Thread t = new TestThread();        t.start();   //启动线程    }}AI写代码java运行2.2 实现Runnable接口来实现流程定义子类,实现Runnable接口。子类中重写Runnable接口中的run方法。通过Thread类含参构造器创建线程对象。将Runnable接口的子类对象作为实际参数传递给Thread类的构造方法中。调用Thread类的start方法:开启线程,调用Runnable子类接口的run方法。  实现Runnable接口public class TestRunnable implements Runnable{    @Override    public void run(){        System.out.println("实现Runnable接口运行多线程");    }}AI写代码java运行实现多线程public class Test{    public static void main(String[]args){        Thread t = new Thread(new TestRunnable);        //带有线程名称的实例化线程对象。可以通过Thread.currentThread().getName()获取        //Thread t = new Thread(new TestRunnable,"the FirstThread");        t.start();   //启动线程    }}AI写代码java运行与继承Thread类的区别继承Thread:线程代码存放Thread子类run方法中。重写run方法实现Runnable:线程代码存在接口的子类的run方法。实现run方法 实现Runnable接口方法的好处        实现Runnable接口方法通过继承Runnable接口避免了当继承的局限性,同时也使得多个线程可以同时共享一个接口实现类的对象,非常适合多个相同线程来处理同一份资源。 2.3 实现Callable接口        在前面通过实现Runnable接口创建多线程时,Thread类的作用就是把run方法包装成线程的执行体。而从Java5以后,Java提供了一个Callable接口中的call()方法作为线程执行体,同时call()方法可以有返回值,也可以抛出异常。public class Test{    public static void main(String[]args){        //创建callable对象        ThirdThread tt = new ThirdThread();        //使用FutureTask来包装Callable对象        FutureTask<Integer> task = new FutureTask<Integer>((Callable<Integer>)()->{            ...            ...        });        new Thread(task,"有返回值的线程").start();        try{            //获取线程返回值            System.out.println("子线程的返回值" + task.get());        }catch (EXception ex){            ex.printStackTrace();            }    }}AI写代码java运行Callable接口实现类和Runnable接口实现类的区别在于是否有参数返回! 三、Thread类的相关方法常用方法如下:void start():启动线程,并执行对象的run(0方法run():线程在被调度时执行的操作String getName():返回线程的名称void setName(String name):设置该线程名称static currentThread():返回当前线程 public class Test{    public static void main(String[]args){        TestRun r1 = new TestRun();        Thread t1 = new Thread(r1);        //为线程设置名称        t1.setName("线程t1");         t1.start();   //启动线程        System.out.println(t1.getName()); //若没指定,系统默认给出的线程名称是Thread-0....    }}public class TestRun implements Runnable{    @Override    public void run(){        System.out.println("实现Runnable接口运行多线程");    }}AI写代码java运行线程优先级线程的优先级设置增加了线程的执行顺序靠前的概率,是用一个数组1-10来表示的,默认的优先级是5。涉及的方法有:getPriority()和setPriority()//获取优先级t1.getPriority();//设置优先级t1.setPriority(10);AI写代码java运行线程让步static void yield()线程让步,即暂停当前正在执行的线程,把执行机会让给优先级相同或更高的线程,若队列中没有同优先级的线程,则跳过。Thread.yield();AI写代码java运行线程阻塞join():当某个程序执行流中调用其他线程的join()方法时,调用线程将被阻塞,直到join()方法加入的join线程执行完为止。try{    //获取线程返回值    t1.join();}catch (EXception ex){    ex.printStackTrace();    }AI写代码java运行线程睡眠try{    Thread.sleep(1000);//当前线程睡眠1000毫秒}catch(InterruptedException e)(    e.printStackTrace();}AI写代码java运行线程生命结束t1.stop();AI写代码java运行判断当前线程是否存活t1.isAlive();AI写代码java运行四、生命周期线程从创建、启动到死亡经历了一个完整的生命周期,在线程的生命周期中一般要经历五种状态:新建——就绪——运行——阻塞——死亡。新建:当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态;就绪:处于新建状态的线程被start()后,将进入线程队列等待CPU时间片,此时它已具备了运行的条件,也就是在执行.start()方法后;运行:当就绪的线程被调度并获得处理器资源时,便进入运行状态,run()方法定义了线程的操作和功能,此时run()方法的代码开始执行;阻塞:在某种特殊情况下,被人为挂起或执行输入输出操作时,让出CPU并临时中止自己的执行,进入阻塞状态;死亡:线程完成了它的全部工作或线程被提前强制性地中止 。 ​​​​​线程可能以如下三种方法结束:run或call方法执行完成后线程抛出一个未捕获的Exception或Error直接调用了stop()方法五、同步锁和死锁5.1 同步锁         多线程模式的提出势必就会带来线程同步的问题,在保证数据一致性上,我们需要为线程加上同步锁。Java中对于多线程安全的问题提出了同步机制,即在方法声明的时候加入synchronized关键字来修饰或者直接使用synchronized来锁一个demo5.1.1 synchronized加锁的两种方式 synchronized同步锁关键字修饰 //使用synchronized同步锁关键字修饰需要同步执行的方法体public synchronized void drawing(int money){    需要同步执行的代码}AI写代码java运行注意:        在普通方法上加同步锁synchronized,锁的是整个对象,不是某一个方法。如果是不同对象的话那么就是不同的锁。静态的方法加synchronized对于所有的对象都是同一个锁!synchronized锁一段demo使用这种方法来锁指向this的代码块使用的都是同一个同步锁。如果改成方法对象的话比如Account对象的话就是不同的同步锁。synchronized(this){ //表示当前的对象的代码块被加了synchronized同步锁    demo...}AI写代码java运行5.1.2 Lock        相比于上面的synchronized相应的锁操作,Lock提供了更为广泛的锁操作。其中包括ReadWriteLock(读写锁)和ReentrantLock(可重入锁),ReadWriteLock提供了ReentrantReadWriteLock的实现类。在Java8中引入了一个新的StampedLock类替代了传统的ReentrantReadWriteLock并给出了三种锁模式:Write、ReadOptimistic和Reading。ReentrantLock 实现demo class x{    //定义锁对象    private final ReentrantLock lock = new ReentrantLock();    //...    //定义需要保证线程安全的方法    public void m(){        lock.lock();        try{            //需要保证线程安全的demo        }        finally{            lock.unlock();        }    }}AI写代码java运行5.2 死锁不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。解决方法专门的算法、原则,比如加锁顺序一致尽量减少同步资源的定义,尽量避免锁未释放的场景六、线程通信        当我们手动开启并在控制台中输出两个线程的运行过程的时候,程序并不能每次都准确的控制两个线程的轮换执行的先后次序,所以Java中也提供了一些机制来保证线程的协调运行。在传统的Java中,基于同步锁synchronized关键字提供了借助于Object类的wait()、notify()和notifyAll()方法来控制线程的阻塞情况,而之后也出现了基于Condition和阻塞队列BlockingQueue来控制线程阻塞的情况。6.1 传统的线程通信Object类中提供的wait()、notify()和notifyAll()方法必须由一个同步监视器对象来调用,所以这三种方法必须基于同步锁synchronized关键字。wait():该方法会导致当前线程进入等待状态,直到其它的线程调用notify()或notifyAll()方法来唤醒该线程,wait方法有三种形式:不带时间参数(等待唤醒)、带毫秒时间参数(时间到自动唤醒)和带毫微秒的时间参数(时间到自动唤醒)。调用wait方法当前线程会释放对同步监视器的锁定。notify():唤醒该同步监视器上等待的单个线程,这种选择是按照优先级最高的来唤醒结束其等待状态。notifyAll():唤醒等待的所有线程。//使用时直接调用方法就行,但必须是在有synchronized修饰的方法内去调用才可wait();notify();notifyAll();AI写代码6.2 使用Condition来控制线程通信        对于程序不使用synchronized关键字来保证同步锁,而是采用Lock对象来保证同步,Java中提供了Condition类来保证线程通信。Contidion类中提供了类似于synchronized关键字中的三种方法:await()、signal()和signalAll(),替代了同步监视器的功能。await():类似于wait方法,会使得当前线程进入等待状态,直到其它线程调用signal()或signalAll()来唤醒。signal():唤醒单个线程。signalAll():唤醒多个线程。//显示定义Lock对象Lock lock = new ReentrantLock();//获取ConditionCondition cond = lock.newCondition();//需要同步的方法中加锁public void fun(){    //加锁过程    lock.lock();    try{        if(条件) cond.await(); //线程进入等待        else{            //唤醒其他线程            cond.signalAll();        }    }catch(InterruptedException e){        e.printStrackTrace();    }finally{        //锁的释放        lock.unlock();    }}AI写代码java运行6.3 使用阻塞队列来控制线程通信        除了上述两种方法,Java5中还提供了BlockingQueue接口来作为线程同步的工具。它的工作原理是这样滴:当生产者往BlockingQueue接口中放入元素直至接口队列满了,线程阻塞;消费者从BlockingQueue接口队列中取元素直至队列空了,线程阻塞。BlockingQueue接口继承了Queue接口并提供了如下三组方法。 在队列尾部添加元素:add(E e)、offer(E e)、put(E e),当队列已满的时候,这三个方法分别会抛出异常、返回false和阻塞线程。在队列头部删除并返回删除元素:remove()、poll()、take()方法。当该队列已空时,这三个方法分别会抛出异常、返回false和阻塞线程。在队列头部取出但不删除元素:element()和peek(),当该队列已空时,分别会抛出异常和返回false在Java7之后,阻塞队列出现了新增,分别是:ArrayBlockingQueue、LinkedBlockingQueue、priorityBlockingQueue、SynchornizedQueue和DelayQueue这五个类。 七、线程池        系统启动一个新线程的成本是比较高的,尤其是当系统本身已经有大量的并发线程时,会导致系统性能急剧下降,甚至会导致JVM崩溃,因此我们通常采用线程池来维护系统的并发线程。与数据库连接池类似的时,线程池在系统启动时即创建大量空闲的线程,程序将一个Runnable对象或Callable对象传给线程池,线程池就会启动后一个空闲的线程来执行它们的run()或call()方法,当运行结束后,该线程不会死亡而是返回线程池中进入空闲等待状态。        ExecutorService代表尽快执行线程的线程池,程序只需要将一个Runnable对象或Callable对象传给线程池,就会尽快执行线程任务;ScheduledExecutorService代表可在指定延迟后或周期性地执行线程任务的线程池。7.1 ExecutorService类使用示例使用线程池的步骤如下:调用Executors类的静态工厂方法调用创建一个ExecutorService对象,该对象就代表着一个线程池;创建Runnable实现类或Callable实现类的实例,作为线程执行的任务;调用ExecutorService对象的submit()方法来提交Runnable或者Callable对象实例;结束任务时,调用ExecutorService对象的shutdown()方法来关闭线程池;//开启6个线程的线程池ExecutorService pool = Executors.newFixedThreadPool(6);//创建Runnable实现类Runnable target = ()->{...} //提交线程任务到线程池pool.submit(); //关闭线程pool.shutdown();AI写代码java运行        用完一个线程池后,应该调用该线程池的shutdown()方法,该方法将启动线程池的关闭序列并不再接受新任务,线程池中的任务依次执行完毕后线程死亡;或者调用线程池的shutdownNow()方法来直接停止所有正在执行的活动任务。7.2 Java8中的ForkJoinPool        计算机发展到现在其实基本的硬件都支持多核CPU,为了更好地利用硬件设备的资源,Java中提供了一个ForkJoinPool来支持将一个任务拆分成多个小任务并行计算。ForkJoinPool是ExecutorService的实现类,是一个特殊的线程池。构造器的两种方法ForkJoinPool(int num):创建一个包含num个并行线程的ForkJoinPool;ForkJoinPool():以Runtime.availableProcessors()方法的返回值作为parallelism参数(上面我写成了num)来创建改线程池实现通用池的两个静态方法ForkJoinPool commonPool():改方法返回一个通用池,通用池的状态不会受到shutdown()等方法的影响,System.exit(0)除外。int getCommonPoolParallelism():该方法返回通用池的并行级别注意:        ForkJoinPool.submit(ForkJoinTask task) ,其中ForkJoinTask代表着一个可以并行和合并的任务,他有两个抽象的子类:RecursiveAction和RecursiveTask,分别代表着有返回值和无返回值的任务。class PrintTask extends RecursiveAction{    ...    @Override    protected void compute(){        ......        //分割任务        PrintTask t1 = new PrintTask(start,middle);        PrintTask t2 = new PrintTask(middle,end);        //并行执行子任务        t1.fork();        t2.fork();    }} public class Test{    public static void main(String[]args) throws Exception{        //实例化通用池对象        ForkJoinPool pool = new ForkJoinPool();        pool.submit(new PrintTask(0,1000));        //线程等待完成        pool.awaitTermination(2,TimeUnit.SECONDS);        //关闭线程池        pool.shutdown();    }}AI写代码java运行总结        现有的所有企业都采用的是多线程并发的方式来开发的,也要求我们能够应对在高并发场景下保证系统服务的高可用的要求,所以多线程和异步编程我们必须牢牢掌握。这几章可能会比较枯燥,难度也会比较大,荔枝也是啃了一段时间嘿嘿嘿,在学这部分之前一定要把面向对象学好,要不然会晕哈哈哈~~~今朝已然成为过去,明日依然向往未来!我是小荔枝,在技术成长的路上与你相伴,码文不易,麻烦举起小爪爪点个赞吧哈哈哈~~~ 比心心♥~~~————————————————版权声明:本文为CSDN博主「荔枝当大佬」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。原文链接:https://blog.csdn.net/qq_62706049/article/details/131848628
  • [问题求助] 咨询tbilllog表和trecordinfo表数据查询对应数据问题
     【问题来源】     内部测试环境功能测试 【问题简要】    麻烦咨询下,产生通话详单记录之后,会分别向tbilllog1-12和trecordinfo1-12表中写入数据,有种数据场景在tbilllog表中存在多条的情况,只取deveicetype=2(业务代表)的数据是能和trecordinfo表中的数据条数对应起来,但是只通过callid关联的话无法准确匹配到对应的录音数据,我在论坛上查看之前的问题求助,有看到回复过类似的问题,如下面的截图1,如果能通过callid和tbilllog表的callbegin和trecordinfo表的begintime一起关联能准确匹配到同一条数据,但是实际去查表发现有些数据的时间不是完全一样的,一条数据一样,另一条数据一个是14:07:23,另外一个是14:07:22,相差了1秒,如下面的截图2,所以想问下,这两个时间存在有些不一样的是否是写入过慢情况导致?那是否就无法通过tbilllog表的callbegin和trecordinfo表的begintime一起关联呢?【问题类别】     话单数据【AICC解决方案版本】     AICC 版本:AICC 25_300.0【期望解决时间】     尽快 【日志或错误截图】1、 2 
  • [互动交流] 如何安装SmartAssist Java 插件
    如何安装SmartAssist Java 插件CodeArts IDE for Python版本: 3.4.1提交: c3be4d08ef4b68e19b23b4136f5f8a7a960d5095日期: 2025-07-15T04:00:00.368Z (3个月前)OS: Windows_NT x64 10.0.19045
总条数:764 到第
上滑加载中