|
| 1 | +# 日常开发笔记 |
| 2 | + |
| 3 | +## 2025/10/21 - MyBatis-Plus JSON字段查询返回null问题 |
| 4 | + |
| 5 | +### 问题 |
| 6 | +使用 MyBatis-Plus 查询时,实体类中配置了 TypeHandler 的 JSON 字段始终返回 null,但新增/编辑操作正常。 |
| 7 | + |
| 8 | +### 原因 |
| 9 | +- MyBatis-Plus 默认不为实体类生成 ResultMap |
| 10 | +- 没有 ResultMap 时,字段上的 `typeHandler` 配置在查询时不生效 |
| 11 | +- SQL 日志显示该字段被识别为 `<<BLOB>>` 类型 |
| 12 | + |
| 13 | +### 解决方法 |
| 14 | +在实体类的 `@TableName` 注解中添加 `autoResultMap = true`: |
| 15 | + |
| 16 | +```java |
| 17 | +@TableName(value = "table_name", autoResultMap = true) |
| 18 | +public class Entity { |
| 19 | + @TableField(typeHandler = JacksonTypeHandler.class) |
| 20 | + private List<String> jsonField; |
| 21 | +} |
| 22 | +``` |
| 23 | + |
| 24 | +### 关键点 |
| 25 | +- `autoResultMap = true` 让 MyBatis-Plus 自动生成 ResultMap |
| 26 | +- 生成的 ResultMap 会正确应用字段上的 typeHandler 配置 |
| 27 | +- 写入时 typeHandler 默认生效,但查询时需要 ResultMap 支持 |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +## 2025/10/21 - Spring的@Transactional注解 |
| 32 | + |
| 33 | +`@Transactional` 是 Spring 框架提供的声明式事务管理注解,用于控制数据库事务的行为。 |
| 34 | + |
| 35 | +### 主要作用 |
| 36 | +- 自动管理事务的开启、提交和回滚 |
| 37 | +- 确保方法内的数据库操作要么全部成功,要么全部失败(原子性) |
| 38 | + |
| 39 | +### 常用属性 |
| 40 | + |
| 41 | +#### 1. `rollbackFor` |
| 42 | +指定哪些异常会触发事务回滚: |
| 43 | +```java |
| 44 | +@Transactional(rollbackFor = Exception.class) // 所有 Exception 都回滚 |
| 45 | +``` |
| 46 | + |
| 47 | +#### 2. `propagation` |
| 48 | +事务传播行为: |
| 49 | +- `REQUIRED`(默认):如果当前存在事务,则加入;否则创建新事务 |
| 50 | +- `REQUIRES_NEW`:总是创建新事务,挂起当前事务 |
| 51 | +- `NESTED`:嵌套事务,支持部分回滚 |
| 52 | + |
| 53 | +#### 3. `isolation` |
| 54 | +事务隔离级别: |
| 55 | +- `READ_UNCOMMITTED`:最低隔离级别,可能脏读 |
| 56 | +- `READ_COMMITTED`:防止脏读 |
| 57 | +- `REPEATABLE_READ`(MySQL默认):防止脏读和不可重复读 |
| 58 | +- `SERIALIZABLE`:最高隔离级别,完全串行化 |
| 59 | + |
| 60 | +#### 4. `readOnly` |
| 61 | +标记为只读事务(优化性能): |
| 62 | +```java |
| 63 | +@Transactional(readOnly = true) |
| 64 | +``` |
| 65 | + |
| 66 | +#### 5. `timeout` |
| 67 | +事务超时时间(秒): |
| 68 | +```java |
| 69 | +@Transactional(timeout = 30) |
| 70 | +``` |
| 71 | + |
| 72 | +### 使用位置 |
| 73 | +- **类级别**:对该类的所有 public 方法生效 |
| 74 | +- **方法级别**:仅对该方法生效(会覆盖类级别配置) |
| 75 | + |
| 76 | +### 注意事项 |
| 77 | +1. **只对 public 方法生效** |
| 78 | +2. **自调用失效**:同一个类内部方法调用不会触发事务(因为没有走代理) |
| 79 | +3. **异常类型**:只写 `@Transactional` 不加任何参数,默认只对 `RuntimeException` 和 `Error` 回滚,checked 异常不会回滚(需要指定 `rollbackFor`) |
| 80 | + |
| 81 | + |
| 82 | + |
| 83 | + |
| 84 | + |
| 85 | +## 2025/10/21 - 分页表格跨页选择数据丢失问题 |
| 86 | + |
| 87 | +### 问题 |
| 88 | +在使用 Ant Design Vue 的表格组件实现多选功能时,发现每次翻页后,之前页面中已选择的数据会被清空,无法实现跨页面的多选保持。 |
| 89 | + |
| 90 | +### 问题 |
| 91 | +Ant Design Vue 的 `<a-table>` 组件在数据源(`dataSource`)发生变化时,默认会重置 `selectedRowKeys`。当用户翻页时,虽然视觉上看起来是同一个表格,但实际上表格的 `dataSource` 已经更新为新的数据,这会触发组件的内部重置逻辑,导致之前的选择状态丢失。 |
| 92 | + |
| 93 | +### 解决方案 |
| 94 | + |
| 95 | +#### 1. 启用跨页选择保持 |
| 96 | +在表格的 `:row-selection` 配置中添加 `preserveSelectedRowKeys: true`: |
| 97 | + |
| 98 | +```vue |
| 99 | +<a-table |
| 100 | + :columns="columns" |
| 101 | + :data-source="dataList" |
| 102 | + :row-selection="{ |
| 103 | + selectedRowKeys: selectedKeys, |
| 104 | + onChange: handleSelectionChange, |
| 105 | + preserveSelectedRowKeys: true // 关键配置:保持选中的 key |
| 106 | + }" |
| 107 | + :pagination="paginationConfig" |
| 108 | +/> |
| 109 | +``` |
| 110 | + |
| 111 | +#### 2. 使用数组展开避免引用问题 |
| 112 | +在选择变化的回调函数中,使用数组展开运算符创建新数组,而不是直接引用: |
| 113 | + |
| 114 | +```javascript |
| 115 | +// ❌ 错误写法 - 直接引用 |
| 116 | +const handleSelectionChange = (keys) => { |
| 117 | + formData.value.selectedIds = keys |
| 118 | +} |
| 119 | + |
| 120 | +// ✅ 正确写法 - 创建新数组 |
| 121 | +const handleSelectionChange = (keys) => { |
| 122 | + formData.value.selectedIds = [...keys] |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +#### 3. 初始化数组避免 undefined 错误 |
| 127 | +在表单打开或初始化时,确保选中的数组字段被初始化为空数组: |
| 128 | + |
| 129 | +```javascript |
| 130 | +const openForm = () => { |
| 131 | + formData.value = { |
| 132 | + selectedIds: [], // 初始化为空数组,避免 undefined |
| 133 | + // ...其他字段 |
| 134 | + } |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +### 完整示例 |
| 139 | + |
| 140 | +```vue |
| 141 | +<template> |
| 142 | + <a-table |
| 143 | + :columns="columns" |
| 144 | + :data-source="dataList" |
| 145 | + :row-selection="{ |
| 146 | + selectedRowKeys: formData.selectedIds, |
| 147 | + onChange: handleSelectionChange, |
| 148 | + preserveSelectedRowKeys: true |
| 149 | + }" |
| 150 | + :pagination="{ |
| 151 | + current: pagination.current, |
| 152 | + pageSize: pagination.pageSize, |
| 153 | + total: pagination.total, |
| 154 | + showSizeChanger: true, |
| 155 | + showTotal: (total) => `共 ${total} 条` |
| 156 | + }" |
| 157 | + @change="handleTableChange" |
| 158 | + /> |
| 159 | +</template> |
| 160 | +
|
| 161 | +<script setup> |
| 162 | +import { ref, watch } from 'vue' |
| 163 | +
|
| 164 | +const formData = ref({ |
| 165 | + selectedIds: [] |
| 166 | +}) |
| 167 | +
|
| 168 | +const dataList = ref([]) |
| 169 | +const pagination = ref({ |
| 170 | + current: 1, |
| 171 | + pageSize: 10, |
| 172 | + total: 0 |
| 173 | +}) |
| 174 | +
|
| 175 | +// 选择变化处理 |
| 176 | +const handleSelectionChange = (selectedRowKeys) => { |
| 177 | + formData.value.selectedIds = [...selectedRowKeys] |
| 178 | +} |
| 179 | +
|
| 180 | +// 分页变化处理 |
| 181 | +const handleTableChange = (paginationInfo) => { |
| 182 | + pagination.value.current = paginationInfo.current |
| 183 | + pagination.value.pageSize = paginationInfo.pageSize |
| 184 | + loadData() |
| 185 | +} |
| 186 | +
|
| 187 | +// 加载数据 |
| 188 | +const loadData = async () => { |
| 189 | + const response = await api.getData({ |
| 190 | + current: pagination.value.current, |
| 191 | + size: pagination.value.pageSize |
| 192 | + }) |
| 193 | + dataList.value = response.data.records |
| 194 | + pagination.value.total = response.data.total |
| 195 | +} |
| 196 | +</script> |
| 197 | +``` |
| 198 | + |
| 199 | +### 关键要点 |
| 200 | + |
| 201 | +1. **preserveSelectedRowKeys**: 这是核心配置,告诉 Ant Design 在数据源变化时保持已选择的 key |
| 202 | +2. **数组展开**: 使用 `[...array]` 创建新数组引用,确保 Vue 的响应式系统能正确追踪变化 |
| 203 | +3. **初始化**: 确保数组字段初始化为空数组而不是 undefined,避免运行时错误 |
| 204 | +4. **rowKey**: 确保表格数据有唯一的 key 字段(默认是 `key`,也可以通过 `:row-key` 指定) |
| 205 | + |
| 206 | +### 适用场景 |
| 207 | + |
| 208 | +- 需要跨页面选择多条记录的场景 |
| 209 | +- 批量操作功能(如批量删除、批量导出等) |
| 210 | +- 复杂表单中的关联数据选择 |
| 211 | + |
| 212 | + |
| 213 | + |
| 214 | +## Spring 异步方法不生效问题排查笔记 |
| 215 | + |
| 216 | +### 问题 |
| 217 | + |
| 218 | +在使用 Spring 的 `@Async` 注解实现异步方法时,发现方法并未真正异步执行。通过日志观察,异步方法与调用方法的线程名称相同,都在 HTTP 请求线程中执行,而不是在独立的异步线程池中运行。 |
| 219 | + |
| 220 | +**现象:** |
| 221 | +- 调用方法日志线程:`[http-nio-82-exec-2]` |
| 222 | +- 异步方法日志线程:`[http-nio-82-exec-2]` (期望应该是 `[task-1]` 等异步线程) |
| 223 | +- HTTP 请求阻塞等待任务完成,导致超时错误 |
| 224 | + |
| 225 | +### 原因 |
| 226 | + |
| 227 | +Spring 的 `@Async` 注解基于 AOP 代理实现,存在以下限制条件: |
| 228 | + |
| 229 | +#### 1. 缺少启用注解 |
| 230 | +Spring Boot 应用需要在配置类或启动类上添加 `@EnableAsync` 注解来启用异步功能。 |
| 231 | + |
| 232 | +#### 2. 方法修饰符限制 |
| 233 | +**Spring AOP 无法代理 private 方法**,因为: |
| 234 | +- Spring 使用 CGLIB 或 JDK 动态代理 |
| 235 | +- **代理只能拦截 public 方法** |
| 236 | +- private 方法无法被子类覆盖,AOP 切面无法介入 |
| 237 | + |
| 238 | +#### 3. 自调用问题 |
| 239 | +在同一个类内部直接调用异步方法(`this.asyncMethod()`)会绕过代理,导致 `@Async` 失效。 |
| 240 | + |
| 241 | +### 解决方案 |
| 242 | + |
| 243 | +#### 步骤 1:启用异步支持 |
| 244 | + |
| 245 | +在 Spring Boot 启动类添加 `@EnableAsync` 注解: |
| 246 | + |
| 247 | +```java |
| 248 | +@SpringBootApplication |
| 249 | +@EnableAsync // 启用异步功能 |
| 250 | +public class Application { |
| 251 | + public static void main(String[] args) { |
| 252 | + SpringApplication.run(Application.class, args); |
| 253 | + } |
| 254 | +} |
| 255 | +``` |
| 256 | + |
| 257 | +#### 步骤 2:修改方法访问修饰符 |
| 258 | + |
| 259 | +将异步方法的访问修饰符从 `private` 改为 `public`: |
| 260 | + |
| 261 | +```java |
| 262 | +// ❌ 错误:private 方法,AOP 无法代理 |
| 263 | +@Async |
| 264 | +private void processAsync() { |
| 265 | + // 异步逻辑 |
| 266 | +} |
| 267 | + |
| 268 | +// ✅ 正确:public 方法,AOP 可以代理 |
| 269 | +@Async |
| 270 | +public void processAsync() { |
| 271 | + // 异步逻辑 |
| 272 | +} |
| 273 | +``` |
| 274 | + |
| 275 | +#### 步骤 3:验证异步执行 |
| 276 | + |
| 277 | +通过日志确认异步方法在独立线程中执行: |
| 278 | + |
| 279 | +```java |
| 280 | +public void callMethod() { |
| 281 | + log.info("调用方法,线程:{}", Thread.currentThread().getName()); |
| 282 | + // 输出:调用方法,线程:http-nio-82-exec-2 |
| 283 | + |
| 284 | + asyncMethod(); |
| 285 | + log.info("已提交异步任务"); |
| 286 | +} |
| 287 | + |
| 288 | +@Async |
| 289 | +public void asyncMethod() { |
| 290 | + log.info("异步方法执行,线程:{}", Thread.currentThread().getName()); |
| 291 | + // 输出:异步方法执行,线程:task-1 |
| 292 | +} |
| 293 | +``` |
| 294 | + |
| 295 | +### 异步方法写法 |
| 296 | + |
| 297 | +#### 1. 自定义线程池 |
| 298 | + |
| 299 | +默认情况下,Spring 使用 `SimpleAsyncTaskExecutor`,建议自定义线程池: |
| 300 | + |
| 301 | +```java |
| 302 | +@Configuration |
| 303 | +@EnableAsync |
| 304 | +public class AsyncConfig implements AsyncConfigurer { |
| 305 | + |
| 306 | + @Override |
| 307 | + public Executor getAsyncExecutor() { |
| 308 | + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); |
| 309 | + executor.setCorePoolSize(5); |
| 310 | + executor.setMaxPoolSize(10); |
| 311 | + executor.setQueueCapacity(100); |
| 312 | + executor.setThreadNamePrefix("async-task-"); |
| 313 | + executor.initialize(); |
| 314 | + return executor; |
| 315 | + } |
| 316 | +} |
| 317 | +``` |
| 318 | + |
| 319 | +#### 2. 异常处理 |
| 320 | + |
| 321 | +异步方法的异常不会传播到调用方,需要自定义异常处理器: |
| 322 | + |
| 323 | +```java |
| 324 | +@Configuration |
| 325 | +@EnableAsync |
| 326 | +public class AsyncConfig implements AsyncConfigurer { |
| 327 | + |
| 328 | + @Override |
| 329 | + public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { |
| 330 | + return (ex, method, params) -> { |
| 331 | + log.error("异步方法执行异常 - 方法: {}, 参数: {}", |
| 332 | + method.getName(), Arrays.toString(params), ex); |
| 333 | + }; |
| 334 | + } |
| 335 | +} |
| 336 | +``` |
| 337 | + |
| 338 | +#### 3. 返回值处理 |
| 339 | + |
| 340 | +异步方法可以返回 `Future`、`CompletableFuture` 或 `ListenableFuture`: |
| 341 | + |
| 342 | +```java |
| 343 | +@Async |
| 344 | +public CompletableFuture<String> asyncMethodWithReturn() { |
| 345 | + // 执行异步任务 |
| 346 | + String result = doSomething(); |
| 347 | + return CompletableFuture.completedFuture(result); |
| 348 | +} |
| 349 | +``` |
| 350 | + |
0 commit comments