news 2026/10/12 18:31:41

Spring Boot集成MyBatis全攻略:从环境配置到分页缓存与避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot集成MyBatis全攻略:从环境配置到分页缓存与避坑

1. 先从真实经历说起:集成MyBatis为什么容易“跑通容易,深入就懵”

我第一次在Spring Boot项目里集成MyBatis,是在一个校园讲座预约系统的开发任务中。当时跟着一篇教程,十分钟就把依赖加好、Mapper写好、接口调通了,感觉“也就这样”。可等真正进入业务开发,遇到了几个很现实的问题:控制台里死活看不到SQL日志,排查问题时两眼一抹黑;分页插件用起来总是分页失效,查出来的是全表数据;LocalDateTime类型映射到Oracle数据库时直接报错;甚至因为一个字段名和数据库关键字撞了,程序在启动时就挂了。回头看,这些问题都不是MyBatis本身复杂,而是集成过程中缺少一套完整的认知框架。

这篇文章就围绕“Spring Boot中集成MyBatis操作数据库”这条主线,把我实际项目中沉淀下来的完整链路写清楚:从环境准备、映射写法、分页、日志、缓存,到生产环境常见的排查手法。无论你是第一次在Spring Boot里引入MyBatis,还是已经被分页失效、缓存混乱、时间类型报错折磨过的开发者,这篇文章应该都能帮你少走几个弯路。

先说清楚一个基本定位:Spring Boot集成MyBatis并不是“加个依赖写个接口”这么简单,它涉及配置项的作用范围、Mapper的加载机制、SQL会话的生命周期等多个层面。我尽量用实际案例来讲,而不是堆配置。

2. 环境准备:版本搭配与依赖引入的隐形规则

2.1 不要小看版本兼容性

我在项目里最初用的是Spring Boot 2.x搭配MyBatis官方starter 2.1.x,整体稳定。后来有个新项目升级到Spring Boot 3.x,随手就把mybatis-spring-boot-starter升到2.3.x,结果启动直接报错,原因就是Spring Boot 3基于Jakarta命名空间,旧版starter没有适配。这类问题在本地开发机上可能不明显,但一旦打包部署就会集中爆发。

这里给出一组经过验证的版本搭配:

Spring Boot版本MyBatis starter建议版本JDK建议说明
2.7.x2.3.x8/11经典组合,资料多,坑少
3.0.x-3.1.x3.0.x17注意Jakarta迁移
3.2.x3.0.3+17/21可以配合虚拟线程测试
3.3.x/3.4.x3.0.4+17/21新项目推荐,兼容性稳定

构建工具我用的是Maven,核心依赖如下:

<dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>

很多教程会让你直接引入org.mybatis:mybatis加org.mybatis:mybatis-spring,然后手动配置SqlSessionFactory。说实话,如果你不是有特殊定制需求,用官方starter就好——它已经把SqlSessionFactory、SqlSessionTemplate、Mapper扫描这些基础设施自动装配好了,踩坑成本更低。

2.2 数据源与连接池的选型逻辑

数据源层面,我推荐用HikariCP。Spring Boot 2.x以上版本默认就是HikariCP,不需要额外引入,它的性能在同级连接池中表现很强,而且配置项直观。如果你从Druid迁移过来,会发现HikariCP的“零配置可用”体验非常舒服。

我常用的数据源配置长这样:

spring: datasource: url: jdbc:mysql://localhost:3306/lecture_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: yourpassword driver-class-name: com.mysql.cj.jdbc.Driver hikari: minimum-idle: 5 maximum-pool-size: 20 connection-timeout: 30000 idle-timeout: 600000 max-lifetime: 1800000

注意URL里的serverTimezone=Asia/Shanghai。如果不加,你很可能在插入时间字段时出现“Cannot convert value 'yyyy-MM-dd HH:mm:ss' from JDBC type TIMESTAMP”之类的诡异报错。allowPublicKeyRetrievalKey=true是MySQL 8.x配合非SSL连接时的常见要求,否则连接阶段可能抛Public Key Retrieval is not allowed。

2.3 必须打开的MyBatis核心配置

这里是我反复强调的一个点:不要省略MyBatis的全局配置项。网上很多示例只贴了mapper-locations就完事,但实际开发中还有三个配置直接影响你的调试效率:

mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl cache-enabled: true

map-underscore-to-camel-case开启后,数据库字段user_name就能自动映射到userName属性,省去大量resultMap手动映射。log-impl可以让SQL在控制台直接输出,这是排查问题的第一手段,后面我单独细讲。cache-enabled是二级缓存总开关,默认是true,但如果你的项目还没完全理解缓存机制,我建议先设为false,等理清了再打开。

2.4 第一个Mapper:从零到接口调通

写完配置,先创建一个实体类和一个Mapper接口,验证集成是否成功。我习惯用一个最简单的用户表来验证链路。

实体类:

public class User { private Long id; private String name; private String email; private LocalDateTime createTime; // getter / setter 省略 }

Mapper接口:

@Mapper public interface UserMapper { User selectById(@Param("id") Long id); }

XML文件UserMapper.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"> <mapper namespace="com.example.mapper.UserMapper"> <select id="selectById" resultType="com.example.entity.User"> SELECT * FROM user WHERE id = #{id} </select> </mapper>

启动类上别忘了加@MapperScan,或者每个Mapper接口上加@Mapper注解。两者选一个就行,我习惯用@MapperScan,因为可以统一指定扫描包,后续新增Mapper不用重复加注解。

@SpringBootApplication @MapperScan("com.example.mapper") public class LectureSystemApplication { public static void main(String[] args) { SpringApplication.run(LectureSystemApplication.class, args); } }

到这里,一个最基础的集成链路就跑通了。验证标准是:写一个Controller调用userMapper.selectById(1L),能够在日志里看到完整SQL,并且返回正确对象。

3. 映射器的两种写法:注解与XML的取舍逻辑

3.1 注解SQL在什么场景下真能救急

MyBatis支持在Mapper接口方法上直接写@Select、@Insert等注解,例如:

@Select("SELECT * FROM user WHERE id = #{id}") User selectById(@Param("id") Long id);

注解写法的最大优点是快、直观,适合单表简单操作。比如一个只有两三个字段的基础字典表,用注解一条SQL搞定,完全不需要维护XML文件。我在项目里,像数据字典、枚举值表这类几乎不变化的查询,都会用注解。

但它有两个明显短板:

  • 动态SQL一复杂,字符串拼接的可读性直线下降。<if>标签在注解里需要用<script>包起来,写起来非常反人类。
  • 大规模重构时,SQL散落在接口方法上,没法像XML那样集中审查join逻辑和结果映射。

3.2 XML写法的核心优势与适用边界

XML写法最大的价值在于“SQL与Java代码分离”。当一条查询涉及五张表join、十几个查询条件、动态排序、嵌套结果映射时,XML可以借助<sql>片段复用、<include>引入、<foreach>循环等能力写出可维护性明显更好的SQL。另外一个实际优势是:调整SQL不需要重新编译Java代码,在部分需要快速热修复的部署场景里,改XML再重启的效率高很多。

以动态条件查询为例:

<select id="searchLectures" resultType="com.example.entity.Lecture"> SELECT * FROM lecture <where> <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="speakerId != null"> AND speaker_id = #{speakerId} </if> <if test="startTime != null"> AND start_time &gt;= #{startTime} </if> </where> ORDER BY create_time DESC </select>

<where>标签会自动处理掉第一个条件前面的AND,避免你手动拼SQL时出现“WHERE AND”这类语法错误。注意&gt;=是XML里的>=,直接写>=会被XML解析器当成标签开始,这是新手最容易忽略的细节。

3.3 一个项目里两种写法如何共存

我用下来的经验是:单表基础CRUD用注解,多表关联和复杂动态查询用XML。这不算什么“规范”,但能让代码整洁度显著提升。有个小技巧,注解SQL里如果需要拼接动态条件,可以用<script>标签包住。

另外,不管用哪种写法,@Param注解我建议始终显式标注。接口方法有多个参数时,MyBatis默认会按param1、param2这种位置命名,一旦SQL里参数顺序写错,排查成本极高。显式写@Param("userId")至少让XML里的#{userId}能和代码里对应上。

3.4${}与#{}的本质区别

这个点我单独拿出来说,因为它是MyBatis面试题里的高频点,更是生产事故的高发区。

  • #{}:预编译占位符。MyBatis会把它替换成?,再用PreparedStatement绑定参数,能够有效防止SQL注入。
  • ${}:字符串拼接。直接把参数值拼进SQL语句中,不做安全处理。

我见过一个实际案例:排序字段由前端传入,代码里用了ORDER BY ${sortField}。正常情况下没毛病,但攻击者如果把sortField传成id; DROP TABLE user;--,后果无法想象。凡是用户可控的输入,一律用#{}。动态排序、动态表名这类必须用${}的场景,需要做白名单校验。

可以这样处理:

private static final Set<String> ALLOWED_SORT_FIELDS = Set.of("create_time", "update_time", "id"); public String sanitizeSortField(String input) { return ALLOWED_SORT_FIELDS.contains(input) ? input : "id"; }

3.5 resultMap与自动映射的优先级

当实体类属性名和数据库字段名不完全对应时,很多人第一反应是写resultMap。但如果你已经开启了map-underscore-to-camel-case,大部分情况根本不需要resultMap。自动映射的优先级低于resultMap,也就是说,只要写了resultMap,MyBatis会以resultMap定义为准,未定义的字段即使名字对上了也不会自动映射,这个行为容易埋坑。

我建议:基础单表查询用自动映射,join多表且需要给不同表的同名字段取别名时,用resultMap显式管理。比如:

<resultMap id="LectureWithSpeakerMap" type="com.example.entity.LectureVO"> <id property="id" column="lecture_id"/> <result property="title" column="lecture_title"/> <result property="speakerName" column="speaker_name"/> <result property="orgName" column="org_name"/> </resultMap>

配合SQL里的别名,映射关系一目了然。

4. 分页插件与批量操作:性能与陷阱并存

4.1 PageHelper分页的ThreadLocal机制与失效场景

分页插件我有两个选择:PageHelper和MyBatis-Plus自带的分页插件。这里只讲PageHelper,因为MyBatis-Plus的分页逻辑和它不一样,容易混淆。

PageHelper的原理是:在执行分页查询前,调用PageHelper.startPage(pageNum, pageSize)会把分页参数放进线程私有的ThreadLocal中;等下一次查询执行时,插件拦截到该查询,自动改写SQL,生成带LIMIT的语句,并在查询结束后清除ThreadLocal。这个机制在你只做一次查询时非常好用,但一旦查询不是紧接着startPage发生,或者查询被嵌套在其他逻辑里,分页就会悄悄失效。

我在实际项目里遇到了三种典型失效场景:

场景一:startPage后的第一条SQL不是你想分页的那条。比如,你在service方法开头调用了startPage,但中间先执行了另一条查询,结果分页帽被那条查询“戴”走了。

场景二:startPage与查询之间发生了其他Mapper调用。这种情况多出现在你从缓存工具里先查了点数据,再执行主查询,线程切换或额外查询都会打乱顺序。

场景三:使用了嵌套结果映射的collection查询。如果一条查询的结果集里还有子查询映射,分页插件只能作用于外层SQL,子查询部分不受分页控制,导致返回条数少于期望。

规避原则其实很简单:startPage和要分页的Mapper查询必须紧紧挨着,中间不要插入任何其他Mapper操作。最好的写法是:

PageHelper.startPage(pageNum, pageSize); List<LectureVO> list = lectureMapper.selectLecturePage(condition); PageInfo<LectureVO> pageInfo = new PageInfo<>(list);

用PageInfo包装后,total、pages这些分页元数据会自动填充,前端要的分页信息一次就齐了。

4.2 分页插件配合排序字段的白名单校验

分页插件真正帮到你之后,下一步就会遇到排序问题。我在查询讲座列表时希望按时间倒序、按热度排序,排序字段从前端传过来。注意,PageHelper的orderBy参数最终会拼到SQL里,它和${}一样存在注入风险,也需要做白名单校验,不能直接透传。

一个稳妥方案是后端把允许排序的字段定义成枚举,前端传字段名,后端映射成数据库列名。

4.3 批量插入的三种方式与真实性能对比

批量插入是MyBatis里一个非常经典的性能考题。我实际对比过三种方式。

方式一:foreach拼接单条insert。

<insert id="batchInsert"> INSERT INTO user (name, email, create_time) VALUES <foreach collection="list" item="item" separator=","> (#{item.name}, #{item.email}, #{item.createTime}) </foreach> </insert>

这个方式简单直观,在数据量几百条时性能表现不错。但如果一次插入上万条,拼接出来的SQL会非常长,可能超过MySQLmax_allowed_packet限制,而且解析SQL本身也有开销。

方式二:ExecutorType.BATCH。通过设置SqlSession的执行器类型为BATCH,可以让多条INSERT在同一个PreparedStatement中批量执行,减少网络往返。实测中,当数据量超过几千条时,这个方式的性能优于方式一,尤其适合大批量导入场景。

方式三:MyBatis-Plus的saveBatch。如果你引入了MyBatis-Plus,它内部自己做了批量提交,底层也是分批执行PreparedStatement,代码最简洁。

我个人的经验是:日常业务批量插入一千条以内,foreach足够;真正的大批量导入,用ExecutorType.BATCH,并且分批提交,每批500到1000条。一次性提交两万条,事务日志压力、锁竞争、内存占用都不小,出了问题时回滚代价也高。

4.4 插件之间的相互影响

还有一个小概率但很现实的坑:分页插件和MyBatis-Plus插件共存时,拦截器顺序可能出问题。如果你的项目里同时有PageHelper和MyBatis-Plus的乐观锁插件、多租户插件,它们的Interceptor顺序会导致分页SQL被二次改写。表现就是分页正常但total统计不对,或者count查询里出现了多余的where条件。遇到这类现象,先检查拦截器顺序,因为不同类型插件对SQL的改写是叠加的,顺序错了结果就是错的。

5. SQL日志、缓存机制与常见调试手段

5.1 让SQL在日志中彻底暴露

排查数据库问题,第一步永远是看SQL长什么样。mybatis.configuration.log-impl直接写StdOutImpl可以把SQL打到控制台,但生产环境不太适合,因为输出格式和日志框架不统一。我推荐用SLF4J:

logging: level: com.example.mapper: debug

把Mapper接口包名设为debug级别后,MyBatis的日志输出会在你统一搭建的日志框架里展示,问题定位方便很多。注意这里配置的包路径一定要和你@MapperScan扫描的包一致,很多人配了com.example.mapper,但Mapper接口实际放在com.example.mapper.user,导致级别配置不生效。

5.2 一级缓存:会话内的默认加速器

MyBatis的一级缓存是SqlSession级别的,默认开启且无法关闭(局部调整可以通过localCacheScope参数改成STATEMENT级别)。开发时你可能没感觉,但用一个线程里反复查同一行数据时,一级缓存可以让第二次查询直接命中本地Map,默认是不错的。

但它有个经典坑:当你跨事务操作时,一级缓存可能让你的“重复查询”拿到过期数据。同一个SqlSession先查了一次user,然后又UPDATE了该用户,再查一次,因为一级缓存里有旧值,第二次查询可能不会走数据库,结果你拿到的是修改前的数据。如果项目里用了Spring管理事务,同一个事务内使用的是同一个SqlSession,这种情况很容易发生。遇到奇怪数据不一致时,先考虑一级缓存是否介入了。

强制清除缓存的方式是调用sqlSession.clearCache(),或者在Service层通过SqlSessionTemplate的清理方法操作。实际开发中,很多团队选择把localCacheScope设为STATEMENT,避免这类意外,代价就是同会话内重复查询会真实落到数据库,性能上有些许损失。

5.3 二级缓存:要开就开得明明白白

二级缓存是Mapper级别的,跨SqlSession共享。MyBatis官方文档建议让实体类实现Serializable接口,因为默认的二级缓存实现需要序列化对象。XML里开启方式:

<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>

实际项目中,我很少给所有Mapper都开二级缓存。原因有二:

  • 二级缓存粒度是Mapper命名空间级的,一旦多表join的查询涉及多个缓存区域,某个表的更新可能无法让其他缓存区域失效,导致脏读。
  • 缓存失效的时机由flushCache行为决定,插入、更新、删除操作默认会刷新对应Mapper的缓存,但跨Mapper的数据变更不会触发其他Mapper缓存更新。

建议是:只有数据变化频率极低、且访问量高的查询,比如数据字典、地区表,才开启二级缓存。业务数据坚决不开,或者等团队对缓存失效策略有清晰认知后再开。

5.4 用代码直接测试缓存行为

一个简单有效的测试方法:写一个单元测试,先执行一次selectById,再执行一次selectById,观察SQL日志中第二次查询是否真的走了数据库。如果日志里只能看到一条SELECT,说明命中了缓存。再执行一次update,再查,看缓存是否被清空。这个实验能帮你很直观地理解MyBatis的缓存行为。

5.5 常见调试手段:打印参数与结果映射

调试MyBatis时,最痛苦的是“SQL对,参数对,但结果不对”。我通常这样排查:

  • 先通过日志确认PARAMETERS里每个?的实际值。
  • 再确认resultType或resultMap,看看属性映射是否正确。
  • 最后直接拿日志里输出的完整SQL去数据库客户端执行,对比返回结果。

如果结果集的列名和实体属性对不上,优先检查是否开了驼峰映射。如果字段映射真的复杂,我会写一个临时resultMap来测试,逐步缩小范围。

6. 实战踩坑记录:时间类型、关键字冲突与流式查询

这一节的内容全部来自我实际项目里处理过的线上问题,虽然不是每个项目都会遇到,但遇到一个就能耽误你半天。

6.1 LocalDateTime与Oracle时间字段的映射之痛

在MySQL里,LocalDateTime和datetime字段映射很顺畅,因为MySQL的JDBC驱动做了默认转换。但在Oracle里,我遇到过程度不小的报错:插入LocalDateTime时提示“无效的列类型”,或者查询时返回的java.sql.Timestamp无法自动转成LocalDateTime。

解决办法通常是给字段加typeHandler或jdbcType:

<insert id="insertLecture"> INSERT INTO lecture (title, start_time, create_time) VALUES (#{title}, #{startTime, jdbcType=TIMESTAMP}, #{createTime, jdbcType=TIMESTAMP}) </insert>

如果还是不行,可以自定义一个TypeHandler,在setParameter里手动把LocalDateTime转成Timestamp,在getResult里把Timestamp转成LocalDateTime。这种方案通用性很好,遇到不同数据库都能覆盖。

6.2 MySQL关键字与实体字段撞名

假设你有张表有个字段叫rank,在MySQL里RANK是窗口函数关键字。你直接写SELECT id, rank FROM user可能没问题,但把它放进子查询、或者和某些MySQL版本行为相遇时,就会抛语法错误。

我建议所有SQL里的关键字字段都自动带上反引号:

<select id="selectUserRank" resultType="map"> SELECT id, `rank` FROM user_rank WHERE id = #{id} </select>

还有一种更稳妥的方式:写SQL时把关键字字段加别名,比如SELECT id, rank AS user_rank FROM user_rank,让Java侧也对应userRank属性,绕开关键字本身。前面提到的白名单方式也能用在这里——把别名映射做在后端代码层。

6.3 大结果集场景下的流式查询

当查询结果集很大(比如几十万行),一次性全部加载到内存会导致GC压力和内存溢出。MyBatis支持流式查询,配合Cursor接口可以一行一行或分批读取。实现方式有两种:

一种是在Mapper接口方法中返回Cursor<T>:

Cursor<User> scanAllUsers();

XML里就是普通select,没有特殊写法。但使用Cursor时有几个关键要求:必须在事务内读取,否则连接会提前关闭;读取过程中不能执行其他SQL,因为Cursor持有数据库游标。这种方式适合数据迁移、导出全量数据的场景。

另一种更实用的是分页拉取,结合前面讲的分页插件,每批拉1000条,处理完再拉下一批。这种方式虽然多了几次查询,但内存占用稳定,可操作性强。我在做数据同步时,经常就是用分页拉取加批处理去完成。

6.4 分页count查询与大数据量的优化

PageHelper在分页时会自动执行一个count查询,如果你的主查询非常复杂,比如多个join、多个子查询,count查询也会变得很重,导致接口响应很慢。这时候可以通过手写count SQL来优化:

PageHelper.startPage(pageNum, pageSize, false); // 不执行count List<LectureVO> list = lectureMapper.selectLecturePage(condition); long total = lectureMapper.countLecturePage(condition); // 手动count

把count查询简化成只查最小粒度的id或统计聚合,性能往往能提升数倍。另外,MyBatis的count查询生成逻辑也可以通过countSuffix等参数调整,但效果有限,最直接的还是手动控制。

7. 从集成到工程化:我最后想给你的几条实在建议

写到这里,基础链路和常见坑都过了一遍。最后分享几条我在实际项目里形成的习惯,这些习惯让整个集成的可维护性明显提高。

第一,实体类字段命名统一用驼峰,数据库字段统一用下划线。这句话看着简单,但很多项目的混乱恰恰败在命名不统一上。开启map-underscore-to-camel-case后,只要约定一致,大部分映射都不用手写。

第二,每个复杂SQL在XML里都要写清楚注释。MyBatis的XML文件是SQL的藏身之处,没有注释的的join SQL,三个月后你自己都看不懂。我习惯在每条SQL上方标注业务场景、涉及表关系、关键查询条件。

第三,埋点日志必不可少。集成MyBatis后,至少要在Service层给关键查询打点,记录耗时和参数摘要。MyBatis的SQL日志能告诉你SQL执行情况,但定位慢查询还是需要业务层的时间和上下文。

第四,生产环境慎用二级缓存,优先考虑Redis这类独立缓存中间件。MyBatis的二级缓存定位是进程内缓存,不适合分布式环境,一旦多实例部署就可能出现各实例缓存不一致。如果项目已经引入了Redis,业务缓存直接交给Redis,MyBatis的二级缓存留给真正低频、单机应用去玩。

第五,不要神化MyBatis,也不要贬低它。MyBatis最典型的优势是SQL可控性高、上手曲线平缓、适合ORM洁癖人群。它的缺点也很明显:没有自动建表能力,没有SQL标准化的强约束,复杂查询需要人工优化。选择它的前提是团队愿意在SQL层面做长期投入。

如果你是把Spring Boot和MyBatis当作一个“技术栈组合”来学习,建议在学完集成之后,马上用一个真实业务场景把分页、条件查询、联表、事务这几个能力串一遍。只有当你自己动手解决过一个分页失效、一个缓存不一致和一个时间映射错误,你才算真正掌握这套集成方案。

我个人在完成校园讲座预约系统后,又把同样的结构迁移到企业办公用品管理系统,踩过的坑几乎一模一样。把这些经验沉淀下来,希望你能一次少踩几个。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/12 18:31:04

软件需求分析报告模板详解:从编写到验收的完整指南

简介&#xff1a;这是一份软件需求分析报告模板&#xff0c;面向软件需求分析师、产品经理及项目管理者&#xff0c;旨在解决需求文档结构混乱、关键内容遗漏、评审流程缺失等问题。模板沿用标准章节划分&#xff0c;依次覆盖项目范围、总体功能要求、开发平台、实施过程管理、…

作者头像 李华
网站建设 2026/10/12 18:13:43

JSTL 实战指南:从 JSP 脚本到标签化视图的改造与避坑

1. 为什么今天还要回头啃 JSTL刚入行那会儿&#xff0c;我接手的第一个老项目就是一套典型的 JSP Servlet 架构&#xff0c;页面里到处是<% %>脚本片段&#xff0c;一个列表页能塞进去两百多行 Java 代码&#xff0c;改一个字段要在 HTML 和 Java 之间来回跳。后来带我的…

作者头像 李华
网站建设 2026/10/12 18:09:57

Surface Studio 1代换SSD全记录:拆屏、选盘、迁移避坑指南

还记得那台陪伴我画了无数张草图的 Surface Studio 1 代吗&#xff1f;今年初它终于出现了“硬盘已满”的红色警告&#xff0c;开机速度从当年的惊艳如今也被拖到了让人想摔笔的节奏。这台 2016 年出生的老家伙&#xff0c;最让我不满的不是那块 28 寸的 PixelSense 触摸屏&…

作者头像 李华
网站建设 2026/10/12 18:09:50

ENVI Layer Stack批量处理与IDL自动化实战指南

1. 从一次多光谱数据批处理说起&#xff1a;Layer Stack到底在解决什么问题如果你手头有一批Landsat或者Sentinel-2的影像&#xff0c;每个波段单独存成一个文件&#xff0c;现在需要把它们合成一个多波段数据集再去做后续分析&#xff0c;那你大概率绕不开ENVI里的Layer Stack…

作者头像 李华
网站建设 2026/10/12 18:06:29

大模型API Key体系设计:身份、鉴权与计量全解析

1. 为什么需要自己造一套大模型Key体系1.1 从共享Key的混乱说起我之前在某公司内部做模型开放平台的时候&#xff0c;最开始偷了个懒&#xff1a;直接申请了一个大模型供应商的密钥&#xff0c;把密钥写死在业务代码里&#xff0c;或者放在配置中心让大家共用。结果上线不到一周…

作者头像 李华