基于SpringBoot多線程@Async的使用體驗
多線程@Async的使用體驗
場景
導入:可以將大批量的數(shù)據(jù)insert操作采用多線程的方式并行執(zhí)行
第三方服務的接口調(diào)用:由于存在個別第三方服務調(diào)用比較耗時的場景,此時就可以與自身服務的邏輯并行執(zhí)行
簡而言之:接口中部份業(yè)務邏輯可以通過并行的方式來優(yōu)化接口性能
1.線程池配置
@Configuration @EnableAsync public class TaskPoolConfig { @Bean("taskExecutor") // bean 的名稱,默認為首字母小寫的方法名 public Executor taskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //核心線程數(shù)(CPU核心數(shù)+1) executor.setCorePoolSize(10); //最大線程數(shù)(2*CPU核心數(shù)+1) executor.setMaxPoolSize(20); //緩沖隊列數(shù) executor.setQueueCapacity(200); //允許線程空閑時間(單位:默認為秒) executor.setKeepAliveSeconds(60); //線程池名前綴 executor.setThreadNamePrefix("sub-thread-"); // 增加 TaskDecorator 屬性的配置 executor.setTaskDecorator(new ContextDecorator()); // 線程池對拒絕任務的處理策略:CallerRunsPolicy:不在新線程中執(zhí)行任務,而是由調(diào)用者所在的線程來執(zhí)行 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } }
2.子父線程之間共享一個Request的配置方案
1.實現(xiàn)TaskDecorator接口
/** * 子線程裝飾器 * * @author Da Shuai * @date 2021-06-10 18:28:17 */ public class SubThreadTaskDecorator implements TaskDecorator { @Override public Runnable decorate(Runnable runnable) { RequestAttributes context = RequestContextHolder.currentRequestAttributes(); return () -> { try { RequestContextHolder.setRequestAttributes(context); runnable.run(); } finally { RequestContextHolder.resetRequestAttributes(); } }; } }
2.之前的線程池配置加如下代碼使其生效
// 增加 TaskDecorator 屬性的配置 executor.setTaskDecorator(new ContextDecorator());
3.阻塞主線程,等待所有子線程執(zhí)行完畢后繼續(xù)執(zhí)行主線程
1.CountDownLatch
思路:
- 實例化CountDownLatch對象,同時傳入x(線程數(shù)量:這個數(shù)量必須等于子線程數(shù)量)進行構(gòu)造
- 每個子線程執(zhí)行完畢后會調(diào)用countDown()方法
- 子線程邏輯后方調(diào)用await()方法
這樣線程計數(shù)器為0之前,主線程就一直處于pending狀態(tài)
主線程邏輯
new CountDownLatch(X) latch.await()
@Override @Transactional public void importExcel(File file) { CountDownLatch latch = new CountDownLatch(3); for (int i = 0; i < 3; i++) { VoteDO voteDO = new VoteDO(); voteDO.setTitle(i + ""); asyncManager.asyncSaveVote(voteDO); } //System.out.println(1/0); try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } }
子線程邏輯
latch.countDown()
@Override @Async public void asyncSaveVote(VoteDO voteDO, CountDownLatch latch) { log.info("當前線程為 {},休眠10s開始", Thread.currentThread().getName()); try { Thread.sleep(10000L); } catch (InterruptedException e) { e.printStackTrace(); } log.info("當前線程為 {},休眠10s結(jié)束", Thread.currentThread().getName()); log.info("當前線程為 {},保存開始", Thread.currentThread().getName()); voteDO.setDesc(Thread.currentThread().getName()); voteDao.insert(voteDO); latch.countDown(); log.info("當前線程為 {},保存結(jié)束", Thread.currentThread().getName()); }
日志
2021-06-11 16:31:08.653 INFO 27516 --- [nio-8080-exec-1] com.zhdj.config.LogAspect : ===============請求內(nèi)容===============
2021-06-11 16:31:08.653 INFO 27516 --- [nio-8080-exec-1] com.zhdj.config.LogAspect : 請求地址:http://localhost:8080/api/import
2021-06-11 16:31:08.653 INFO 27516 --- [nio-8080-exec-1] com.zhdj.config.LogAspect : 請求方式:POST
2021-06-11 16:31:08.655 INFO 27516 --- [nio-8080-exec-1] com.zhdj.config.LogAspect : 請求類方法:com.zhdj.controller.ImportController.importExcel
2021-06-11 16:31:08.655 INFO 27516 --- [nio-8080-exec-1] com.zhdj.config.LogAspect : 請求類方法參數(shù):[org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@42c3f403]
2021-06-11 16:31:08.655 INFO 27516 --- [nio-8080-exec-1] com.zhdj.config.LogAspect : ===============請求內(nèi)容===============
2021-06-11 16:31:08.676 INFO 27516 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2021-06-11 16:31:08.894 INFO 27516 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2021-06-11 16:31:08.921 INFO 27516 --- [ sub-thread-3] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-3,休眠10s開始
2021-06-11 16:31:08.921 INFO 27516 --- [ sub-thread-1] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-1,休眠10s開始
2021-06-11 16:31:08.921 INFO 27516 --- [ sub-thread-2] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-2,休眠10s開始
2021-06-11 16:31:18.921 INFO 27516 --- [ sub-thread-2] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-2,休眠10s結(jié)束
2021-06-11 16:31:18.921 INFO 27516 --- [ sub-thread-3] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-3,休眠10s結(jié)束
2021-06-11 16:31:18.921 INFO 27516 --- [ sub-thread-2] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-2,保存開始
2021-06-11 16:31:18.921 INFO 27516 --- [ sub-thread-1] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-1,休眠10s結(jié)束
2021-06-11 16:31:18.921 INFO 27516 --- [ sub-thread-3] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-3,保存開始
2021-06-11 16:31:18.921 INFO 27516 --- [ sub-thread-1] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-1,保存開始
2021-06-11 16:31:19.080 DEBUG 27516 --- [ sub-thread-3] com.zhdj.dao.VoteDao.insert : ==> Preparing: INSERT INTO vote ( title, `desc`, gmt_create, gmt_modified ) VALUES ( ?, ?, ?, ? )
2021-06-11 16:31:19.080 DEBUG 27516 --- [ sub-thread-1] com.zhdj.dao.VoteDao.insert : ==> Preparing: INSERT INTO vote ( title, `desc`, gmt_create, gmt_modified ) VALUES ( ?, ?, ?, ? )
2021-06-11 16:31:19.080 DEBUG 27516 --- [ sub-thread-2] com.zhdj.dao.VoteDao.insert : ==> Preparing: INSERT INTO vote ( title, `desc`, gmt_create, gmt_modified ) VALUES ( ?, ?, ?, ? )
2021-06-11 16:31:19.156 DEBUG 27516 --- [ sub-thread-1] com.zhdj.dao.VoteDao.insert : ==> Parameters: 0(String), sub-thread-1(String), 2021-06-11T16:31:19.032(LocalDateTime), 2021-06-11T16:31:19.037(LocalDateTime)
2021-06-11 16:31:19.156 DEBUG 27516 --- [ sub-thread-3] com.zhdj.dao.VoteDao.insert : ==> Parameters: 2(String), sub-thread-3(String), 2021-06-11T16:31:19.032(LocalDateTime), 2021-06-11T16:31:19.037(LocalDateTime)
2021-06-11 16:31:19.156 DEBUG 27516 --- [ sub-thread-2] com.zhdj.dao.VoteDao.insert : ==> Parameters: 1(String), sub-thread-2(String), 2021-06-11T16:31:19.032(LocalDateTime), 2021-06-11T16:31:19.037(LocalDateTime)
2021-06-11 16:31:19.172 DEBUG 27516 --- [ sub-thread-3] com.zhdj.dao.VoteDao.insert : <== Updates: 1
2021-06-11 16:31:19.178 DEBUG 27516 --- [ sub-thread-2] com.zhdj.dao.VoteDao.insert : <== Updates: 1
2021-06-11 16:31:19.187 DEBUG 27516 --- [ sub-thread-1] com.zhdj.dao.VoteDao.insert : <== Updates: 1
2021-06-11 16:31:19.224 INFO 27516 --- [ sub-thread-3] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-3,保存結(jié)束
2021-06-11 16:31:19.224 INFO 27516 --- [ sub-thread-1] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-1,保存結(jié)束
2021-06-11 16:31:19.224 INFO 27516 --- [ sub-thread-2] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-2,保存結(jié)束
2021-06-11 16:31:19.226 INFO 27516 --- [nio-8080-exec-1] com.zhdj.config.LogAspect : --------------返回內(nèi)容----------------
2021-06-11 16:31:19.328 INFO 27516 --- [nio-8080-exec-1] com.zhdj.config.LogAspect : Response內(nèi)容:null
2021-06-11 16:31:19.328 INFO 27516 --- [nio-8080-exec-1] com.zhdj.config.LogAspect : --------------返回內(nèi)容----------------
2.Future
思路:
1.子線程邏輯返回Future對象
2.主線程邏輯循環(huán)判斷每個子線程返回的Future對象isDone()是否為true
主線程邏輯
循環(huán)判斷future.isDone()是否為true
@Override @Transactional public void importExcel(File file) { List<Future> futureList = new ArrayList<>(); for (int i = 0; i < 3; i++) { VoteDO voteDO = new VoteDO(); voteDO.setTitle(i + ""); Future future = asyncManager.asyncSaveVote(voteDO); futureList.add(future); } //檢查所有子線程是否均執(zhí)行完畢 while (true) { boolean isAllDone = true; for (Future future : futureList) { if (null == future || !future.isDone()) { isAllDone = false; } } if (isAllDone) { log.info("所有子線程執(zhí)行完畢"); break; } } }
子線程邏輯
返回Future對象
@Override public Future asyncSaveVote(VoteDO voteDO) { log.info("當前線程為 {},休眠10s開始", Thread.currentThread().getName()); try { Thread.sleep(10000L); } catch (InterruptedException e) { e.printStackTrace(); } log.info("當前線程為 {},休眠10s結(jié)束", Thread.currentThread().getName()); log.info("當前線程為 {},保存開始", Thread.currentThread().getName()); voteDO.setDesc(Thread.currentThread().getName()); voteDao.insert(voteDO); log.info("當前線程為 {},保存結(jié)束", Thread.currentThread().getName()); //返回需要用AsyncResult類 return new AsyncResult<>(true); }
日志
2021-06-11 16:42:28.974 INFO 20492 --- [nio-8080-exec-2] com.zhdj.config.LogAspect : ===============請求內(nèi)容===============
2021-06-11 16:42:28.974 INFO 20492 --- [nio-8080-exec-2] com.zhdj.config.LogAspect : 請求地址:http://localhost:8080/api/import
2021-06-11 16:42:28.974 INFO 20492 --- [nio-8080-exec-2] com.zhdj.config.LogAspect : 請求方式:POST
2021-06-11 16:42:28.975 INFO 20492 --- [nio-8080-exec-2] com.zhdj.config.LogAspect : 請求類方法:com.zhdj.controller.ImportController.importExcel
2021-06-11 16:42:28.975 INFO 20492 --- [nio-8080-exec-2] com.zhdj.config.LogAspect : 請求類方法參數(shù):[org.springframework.web.multipart.support.StandardMultipartHttpServletRequest$StandardMultipartFile@7e23bacc]
2021-06-11 16:42:28.975 INFO 20492 --- [nio-8080-exec-2] com.zhdj.config.LogAspect : ===============請求內(nèi)容===============
2021-06-11 16:42:28.979 INFO 20492 --- [ sub-thread-5] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-5,休眠10s開始
2021-06-11 16:42:28.979 INFO 20492 --- [ sub-thread-4] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-4,休眠10s開始
2021-06-11 16:42:28.979 INFO 20492 --- [ sub-thread-6] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-6,休眠10s開始
2021-06-11 16:42:38.980 INFO 20492 --- [ sub-thread-6] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-6,休眠10s結(jié)束
2021-06-11 16:42:38.980 INFO 20492 --- [ sub-thread-4] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-4,休眠10s結(jié)束
2021-06-11 16:42:38.980 INFO 20492 --- [ sub-thread-5] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-5,休眠10s結(jié)束
2021-06-11 16:42:38.980 INFO 20492 --- [ sub-thread-6] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-6,保存開始
2021-06-11 16:42:38.980 INFO 20492 --- [ sub-thread-5] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-5,保存開始
2021-06-11 16:42:38.980 INFO 20492 --- [ sub-thread-4] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-4,保存開始
2021-06-11 16:42:38.981 DEBUG 20492 --- [ sub-thread-4] com.zhdj.dao.VoteDao.insert : ==> Preparing: INSERT INTO vote ( title, `desc`, gmt_create, gmt_modified ) VALUES ( ?, ?, ?, ? )
2021-06-11 16:42:38.981 DEBUG 20492 --- [ sub-thread-5] com.zhdj.dao.VoteDao.insert : ==> Preparing: INSERT INTO vote ( title, `desc`, gmt_create, gmt_modified ) VALUES ( ?, ?, ?, ? )
2021-06-11 16:42:38.981 DEBUG 20492 --- [ sub-thread-6] com.zhdj.dao.VoteDao.insert : ==> Preparing: INSERT INTO vote ( title, `desc`, gmt_create, gmt_modified ) VALUES ( ?, ?, ?, ? )
2021-06-11 16:42:38.982 DEBUG 20492 --- [ sub-thread-5] com.zhdj.dao.VoteDao.insert : ==> Parameters: 1(String), sub-thread-5(String), 2021-06-11T16:42:38.980(LocalDateTime), 2021-06-11T16:42:38.981(LocalDateTime)
2021-06-11 16:42:38.982 DEBUG 20492 --- [ sub-thread-4] com.zhdj.dao.VoteDao.insert : ==> Parameters: 0(String), sub-thread-4(String), 2021-06-11T16:42:38.980(LocalDateTime), 2021-06-11T16:42:38.981(LocalDateTime)
2021-06-11 16:42:38.982 DEBUG 20492 --- [ sub-thread-6] com.zhdj.dao.VoteDao.insert : ==> Parameters: 2(String), sub-thread-6(String), 2021-06-11T16:42:38.980(LocalDateTime), 2021-06-11T16:42:38.981(LocalDateTime)
2021-06-11 16:42:38.988 DEBUG 20492 --- [ sub-thread-5] com.zhdj.dao.VoteDao.insert : <== Updates: 1
2021-06-11 16:42:38.989 INFO 20492 --- [ sub-thread-5] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-5,保存結(jié)束
2021-06-11 16:42:38.993 DEBUG 20492 --- [ sub-thread-6] com.zhdj.dao.VoteDao.insert : <== Updates: 1
2021-06-11 16:42:38.993 INFO 20492 --- [ sub-thread-6] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-6,保存結(jié)束
2021-06-11 16:42:39.004 DEBUG 20492 --- [ sub-thread-4] com.zhdj.dao.VoteDao.insert : <== Updates: 1
2021-06-11 16:42:39.005 INFO 20492 --- [ sub-thread-4] com.zhdj.AsyncManagerImpl : 當前線程為 sub-thread-4,保存結(jié)束
2021-06-11 16:42:39.005 INFO 20492 --- [nio-8080-exec-2] com.zhdj.service.impl.VoteServiceImpl : 所有子線程執(zhí)行完畢
2021-06-11 16:42:39.005 INFO 20492 --- [nio-8080-exec-2] com.zhdj.config.LogAspect : --------------返回內(nèi)容----------------
2021-06-11 16:42:39.005 INFO 20492 --- [nio-8080-exec-2] com.zhdj.config.LogAspect : Response內(nèi)容:null
2021-06-11 16:42:39.005 INFO 20492 --- [nio-8080-exec-2] com.zhdj.config.LogAspect : --------------返回內(nèi)容----------------
4.多線程共用一個事務
暫時無解決方案,這是弊端
異步調(diào)用@Async問題
1.使用背景
在項目中,當訪問其他人的接口較慢或者做耗時任務時,不想程序一直卡在耗時任務上,想程序能夠并行執(zhí)行,我們可以使用多線程來并行的處理任務,也可以使用spring提供的異步處理方式@Async。
2.異步處理方式
調(diào)用之后,不返回任何數(shù)據(jù)。
調(diào)用之后,返回數(shù)據(jù),通過Future來獲取返回數(shù)據(jù)
3.@Async不返回數(shù)據(jù)
使用@EnableAsync啟用異步注解
@Configuration @EnableAsync @Slf4j public class AsyncConfig{ }
在異步處理的方法dealNoReturnTask上添加注解@Async
@Component @Slf4j public class AsyncTask { @Async public void dealNoReturnTask(){ log.info("Thread {} deal No Return Task start", Thread.currentThread().getName()); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } log.info("Thread {} deal No Return Task end at {}", Thread.currentThread().getName(), System.currentTimeMillis()); } }
Test測試類:
@SpringBootTest(classes = SpringbootApplication.class) @RunWith(SpringJUnit4ClassRunner.class) @Slf4j public class AsyncTest { @Autowired private AsyncTask asyncTask; @Test public void testDealNoReturnTask(){ asyncTask.dealNoReturnTask(); try { log.info("begin to deal other Task!"); Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } }
日志打印結(jié)果為:
begin to deal other Task!
AsyncExecutorThread-1 deal No Return Task start
AsyncExecutorThread-1 deal No Return Task end at 1499751227034
從日志中我們可以看出,方法dealNoReturnTask()是異步執(zhí)行完成的。
dealNoReturnTask()設置sleep 3s是為了模擬耗時任務
testDealNoReturnTask()設置sleep 10s是為了確認異步是否執(zhí)行完成
4.@Async返回數(shù)據(jù)
異步調(diào)用返回數(shù)據(jù),F(xiàn)uture表示在未來某個點獲取執(zhí)行結(jié)果,返回數(shù)據(jù)類型可以自定義
@Async public Future<String> dealHaveReturnTask() { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } JSONObject jsonObject = new JSONObject(); jsonObject.put("thread", Thread.currentThread().getName()); jsonObject.put("time", System.currentTimeMillis()); return new AsyncResult<String>(jsonObject.toJSONString()); }
測試類用isCancelled判斷異步任務是否取消,isDone判斷任務是否執(zhí)行結(jié)束
@Test public void testDealHaveReturnTask() throws Exception { Future<String> future = asyncTask.dealHaveReturnTask(); log.info("begin to deal other Task!"); while (true) { if(future.isCancelled()){ log.info("deal async task is Cancelled"); break; } if (future.isDone() ) { log.info("deal async task is Done"); log.info("return result is " + future.get()); break; } log.info("wait async task to end ..."); Thread.sleep(1000); } }
日志打印如下,我們可以看出任務一直在等待異步任務執(zhí)行完畢,用future.get()來獲取異步任務的返回結(jié)果
begin to deal other Task!
wait async task to end ...
wait async task to end ...
wait async task to end ...
wait async task to end ...
deal async task is Done
return result is {"thread":"AsyncExecutorThread-1","time":1499752617330}
5.異常處理
我們可以實現(xiàn)AsyncConfigurer接口,也可以繼承AsyncConfigurerSupport類來實現(xiàn)
在方法getAsyncExecutor()中創(chuàng)建線程池的時候,必須使用 executor.initialize(),
不然在調(diào)用時會報線程池未初始化的異常。
如果使用threadPoolTaskExecutor()來定義bean,則不需要初始化
@Configuration @EnableAsync @Slf4j public class AsyncConfig implements AsyncConfigurer { // @Bean // public ThreadPoolTaskExecutor threadPoolTaskExecutor(){ // ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); // executor.setCorePoolSize(10); // executor.setMaxPoolSize(100); // executor.setQueueCapacity(100); // return executor; // } @Override public Executor getAsyncExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(100); executor.setQueueCapacity(100); executor.setThreadNamePrefix("AsyncExecutorThread-"); executor.initialize(); //如果不初始化,導致找到不到執(zhí)行器 return executor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return new AsyncExceptionHandler(); } }
異步異常處理類:
@Slf4j public class AsyncExceptionHandler implements AsyncUncaughtExceptionHandler { @Override public void handleUncaughtException(Throwable ex, Method method, Object... params) { log.info("Async method: {} has uncaught exception,params:{}", method.getName(), JSON.toJSONString(params)); if (ex instanceof AsyncException) { AsyncException asyncException = (AsyncException) ex; log.info("asyncException:{}",asyncException.getErrorMessage()); } log.info("Exception :"); ex.printStackTrace(); } }
異步處理異常類:
@Data @AllArgsConstructor public class AsyncException extends Exception { private int code; private String errorMessage; }
在無返回值的異步調(diào)用中,異步處理拋出異常,AsyncExceptionHandler的handleUncaughtException()會捕獲指定異常,原有任務還會繼續(xù)運行,直到結(jié)束。
在有返回值的異步調(diào)用中,異步處理拋出異常,會直接拋出異常,異步任務結(jié)束,原有處理結(jié)束執(zhí)行。
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
使用Postman傳遞arraylist數(shù)據(jù)給springboot方式
這篇文章主要介紹了使用Postman傳遞arraylist數(shù)據(jù)給springboot方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12SpringBoot Mybatis動態(tài)數(shù)據(jù)源切換方案實現(xiàn)過程
這篇文章主要介紹了SpringBoot+Mybatis實現(xiàn)動態(tài)數(shù)據(jù)源切換方案過程,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以參考下2020-04-04Java模擬HTTP Get Post請求 輕松實現(xiàn)校園BBS自動回帖
這篇文章主要介紹了Java模擬HTTP Get Post請求,輕松實現(xiàn)校園BBS自動回帖,感興趣的小伙伴們可以參考一下2015-12-12SpringBoot手動開啟事務:DataSourceTransactionManager問題
這篇文章主要介紹了SpringBoot手動開啟事務:DataSourceTransactionManager問題,具有很好的價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07