0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

用這4招 優(yōu)雅的實(shí)現(xiàn)Spring Boot異步線程間數(shù)據(jù)傳遞

jf_ro2CN3Fa ? 來源:碼猿技術(shù)專欄 ? 2023-01-30 10:40 ? 次閱讀

Spring Boot 自定義線程池實(shí)現(xiàn)異步開發(fā)相信看過陳某的文章都了解,但是在實(shí)際開發(fā)中需要在父子線程之間傳遞一些數(shù)據(jù),比如用戶信息,鏈路信息等等

比如用戶登錄信息使用ThreadLocal存放保證線程隔離,代碼如下:

/**
*@description用戶上下文信息
*/
publicclassOauthContext{
privatestaticfinalThreadLocalloginValThreadLocal=newThreadLocal<>();

publicstaticLoginValget(){
returnloginValThreadLocal.get();
}
publicstaticvoidset(LoginValloginVal){
loginValThreadLocal.set(loginVal);
}
publicstaticvoidclear(){
loginValThreadLocal.remove();
}
}

那么子線程想要獲取這個(gè)LoginVal如何做呢?

今天就來介紹幾種優(yōu)雅的方式實(shí)現(xiàn)Spring Boot 內(nèi)部的父子線程的數(shù)據(jù)傳遞。

7bd85a70-9688-11ed-bfe3-dac502259ad0.png

1. 手動(dòng)設(shè)置

每執(zhí)行一次異步線程都要分為兩步:

獲取父線程的LoginVal

將LoginVal設(shè)置到子線程,達(dá)到復(fù)用

代碼如下:

publicvoidhandlerAsync(){
//1.獲取父線程的loginVal
LoginValloginVal=OauthContext.get();
log.info("父線程的值:{}",OauthContext.get());
CompletableFuture.runAsync(()->{
//2.設(shè)置子線程的值,復(fù)用
OauthContext.set(loginVal);
log.info("子線程的值:{}",OauthContext.get());
});
}

雖然能夠?qū)崿F(xiàn)目的,但是每次開異步線程都需要手動(dòng)設(shè)置,重復(fù)代碼太多,看了頭疼,你認(rèn)為優(yōu)雅嗎?

基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

項(xiàng)目地址:https://github.com/YunaiV/ruoyi-vue-pro

2. 線程池設(shè)置TaskDecorator

TaskDecorator是什么?官方api的大致意思:這是一個(gè)執(zhí)行回調(diào)方法的裝飾器,主要應(yīng)用于傳遞上下文,或者提供任務(wù)的監(jiān)控/統(tǒng)計(jì)信息。

知道有這么一個(gè)東西,如何去使用?

TaskDecorator是一個(gè)接口,首先需要去實(shí)現(xiàn)它,代碼如下:

/**
*@description上下文裝飾器
*/
publicclassContextTaskDecoratorimplementsTaskDecorator{
@Override
publicRunnabledecorate(Runnablerunnable){
//獲取父線程的loginVal
LoginValloginVal=OauthContext.get();
return()->{
try{
//將主線程的請(qǐng)求信息,設(shè)置到子線程中
OauthContext.set(loginVal);
//執(zhí)行子線程,這一步不要忘了
runnable.run();
}finally{
//線程結(jié)束,清空這些信息,否則可能造成內(nèi)存泄漏
OauthContext.clear();
}
};
}
}

這里我只是設(shè)置了LoginVal,實(shí)際開發(fā)中其他的共享數(shù)據(jù),比如SecurityContext,RequestAttributes....

TaskDecorator需要結(jié)合線程池使用,實(shí)際開發(fā)中異步線程建議使用線程池,只需要在對(duì)應(yīng)的線程池配置一下,代碼如下:

@Bean("taskExecutor")
publicThreadPoolTaskExecutortaskExecutor(){
ThreadPoolTaskExecutorpoolTaskExecutor=newThreadPoolTaskExecutor();
poolTaskExecutor.setCorePoolSize(xx);
poolTaskExecutor.setMaxPoolSize(xx);
//設(shè)置線程活躍時(shí)間(秒)
poolTaskExecutor.setKeepAliveSeconds(xx);
//設(shè)置隊(duì)列容量
poolTaskExecutor.setQueueCapacity(xx);
//設(shè)置TaskDecorator,用于解決父子線程間的數(shù)據(jù)復(fù)用
poolTaskExecutor.setTaskDecorator(newContextTaskDecorator());
poolTaskExecutor.setRejectedExecutionHandler(newThreadPoolExecutor.CallerRunsPolicy());
//等待所有任務(wù)結(jié)束后再關(guān)閉線程池
poolTaskExecutor.setWaitForTasksToCompleteOnShutdown(true);
returnpoolTaskExecutor;
}

此時(shí)業(yè)務(wù)代碼就不需要去設(shè)置子線程的值,直接使用即可,代碼如下:

publicvoidhandlerAsync(){
log.info("父線程的用戶信息:{}",OauthContext.get());
//執(zhí)行異步任務(wù),需要指定的線程池
CompletableFuture.runAsync(()->log.info("子線程的用戶信息:{}",OauthContext.get()),taskExecutor);
}

來看一下結(jié)果,如下圖:

7bee3aca-9688-11ed-bfe3-dac502259ad0.png

這里使用的是CompletableFuture執(zhí)行異步任務(wù),使用@Async這個(gè)注解同樣是可行的。

注意 :無論使用何種方式,都需要指定線程池

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

項(xiàng)目地址:https://github.com/YunaiV/yudao-cloud

3. InheritableThreadLocal

這種方案不建議使用,InheritableThreadLocal雖然能夠?qū)崿F(xiàn)父子線程間的復(fù)用,但是在線程池中使用會(huì)存在復(fù)用的問題。

這種方案使用也是非常簡(jiǎn)單,直接用InheritableThreadLocal替換ThreadLocal即可,代碼如下:

/**
*@description用戶上下文信息
*/
publicclassOauthContext{
privatestaticfinalInheritableThreadLocalloginValThreadLocal=newInheritableThreadLocal<>();

publicstaticLoginValget(){
returnloginValThreadLocal.get();
}
publicstaticvoidset(LoginValloginVal){
loginValThreadLocal.set(loginVal);
}
publicstaticvoidclear(){
loginValThreadLocal.remove();
}
}

4. TransmittableThreadLocal

TransmittableThreadLocal是阿里開源的工具,彌補(bǔ)了InheritableThreadLocal的缺陷,在使用線程池等會(huì)池化復(fù)用線程的執(zhí)行組件情況下,提供ThreadLocal值的傳遞功能,解決異步執(zhí)行時(shí)上下文傳遞的問題。

使用起來也是非常簡(jiǎn)單,添加依賴如下:


com.alibaba
transmittable-thread-local
2.14.2

OauthContext改造代碼如下:

/**
*@description用戶上下文信息
*/
publicclassOauthContext{
privatestaticfinalTransmittableThreadLocalloginValThreadLocal=newTransmittableThreadLocal<>();

publicstaticLoginValget(){
returnloginValThreadLocal.get();
}
publicstaticvoidset(LoginValloginVal){
loginValThreadLocal.set(loginVal);
}
publicstaticvoidclear(){
loginValThreadLocal.remove();
}
}

TransmittableThreadLocal原理

從定義來看,TransimittableThreadLocal繼承于InheritableThreadLocal,并實(shí)現(xiàn)TtlCopier接口,它里面只有一個(gè)copy方法。所以主要是對(duì)InheritableThreadLocal的擴(kuò)展。

publicclassTransmittableThreadLocalextendsInheritableThreadLocalimplementsTtlCopier

在TransimittableThreadLocal中添加holder屬性。這個(gè)屬性的作用就是被標(biāo)記為具備線程傳遞資格的對(duì)象都會(huì)被添加到這個(gè)對(duì)象中。

要標(biāo)記一個(gè)類,比較容易想到的方式,就是給這個(gè)類新增一個(gè)Type字段,還有一個(gè)方法就是將具備這種類型的的對(duì)象都添加到一個(gè)靜態(tài)全局集合中。之后使用時(shí),這個(gè)集合里的所有值都具備這個(gè)標(biāo)記。

//1.holder本身是一個(gè)InheritableThreadLocal對(duì)象
//2.這個(gè)holder對(duì)象的value是WeakHashMap,?>
//2.1WeekHashMap的value總是null,且不可能被使用。
//2.2WeekHasshMap支持value=null
privatestaticInheritableThreadLocal,?>>holder=newInheritableThreadLocal,?>>(){
@Override
protectedWeakHashMap,?>initialValue(){
returnnewWeakHashMap,Object>();
}

/**
*重寫了childValue方法,實(shí)現(xiàn)上直接將父線程的屬性作為子線程的本地變量對(duì)象。
*/
@Override
protectedWeakHashMap,?>childValue(WeakHashMap,?>parentValue){
returnnewWeakHashMap,Object>(parentValue);
}
};

應(yīng)用代碼是通過TtlExecutors工具類對(duì)線程池對(duì)象進(jìn)行包裝。工具類只是簡(jiǎn)單的判斷,輸入的線程池是否已經(jīng)被包裝過、非空校驗(yàn)等,然后返回包裝類ExecutorServiceTtlWrapper。根據(jù)不同的線程池類型,有不同和的包裝類。

@Nullable
publicstaticExecutorServicegetTtlExecutorService(@NullableExecutorServiceexecutorService){
if(TtlAgent.isTtlAgentLoaded()||executorService==null||executorServiceinstanceofTtlEnhanced){
returnexecutorService;
}
returnnewExecutorServiceTtlWrapper(executorService);
}

進(jìn)入包裝類ExecutorServiceTtlWrapper??梢宰⒁獾讲徽撌峭ㄟ^ExecutorServiceTtlWrapper#submit方法或者是ExecutorTtlWrapper#execute方法,都會(huì)將線程對(duì)象包裝成TtlCallable或者TtlRunnable,用于在真正執(zhí)行run方法前做一些業(yè)務(wù)邏輯。

/**
*在ExecutorServiceTtlWrapper實(shí)現(xiàn)submit方法
*/
@NonNull
@Override
publicFuturesubmit(@NonNullCallabletask){
returnexecutorService.submit(TtlCallable.get(task));
}

/**
*在ExecutorTtlWrapper實(shí)現(xiàn)execute方法
*/
@Override
publicvoidexecute(@NonNullRunnablecommand){
executor.execute(TtlRunnable.get(command));
}

所以,重點(diǎn)的核心邏輯應(yīng)該是在TtlCallable#call()或者TtlRunnable#run()中。以下以TtlCallable為例,TtlRunnable同理類似。在分析call()方法之前,先看一個(gè)類Transmitter

publicstaticclassTransmitter{
/**
*捕獲當(dāng)前線程中的是所有TransimittableThreadLocal和注冊(cè)ThreadLocal的值。
*/
@NonNull
publicstaticObjectcapture(){
returnnewSnapshot(captureTtlValues(),captureThreadLocalValues());
}

/**
*捕獲TransimittableThreadLocal的值,將holder中的所有值都添加到HashMap后返回。
*/
privatestaticHashMap,Object>captureTtlValues(){
HashMap,Object>ttl2Value=
newHashMap,Object>();
for(TransmittableThreadLocalthreadLocal:holder.get().keySet()){
ttl2Value.put(threadLocal,threadLocal.copyValue());
}
returnttl2Value;
}

/**
*捕獲注冊(cè)的ThreadLocal的值,也就是原本線程中的ThreadLocal,可以注冊(cè)到TTL中,在
*進(jìn)行線程池本地變量傳遞時(shí)也會(huì)被傳遞。
*/
privatestaticHashMap,Object>captureThreadLocalValues(){
finalHashMap,Object>threadLocal2Value=
newHashMap,Object>();
for(Map.Entry,TtlCopier>entry:threadLocalHolder.entrySet()){
finalThreadLocalthreadLocal=entry.getKey();
finalTtlCopiercopier=entry.getValue();
threadLocal2Value.put(threadLocal,copier.copy(threadLocal.get()));
}
returnthreadLocal2Value;
}

/**
*將捕獲到的本地變量進(jìn)行替換子線程的本地變量,并且返回子線程現(xiàn)有的本地變量副本backup。
*用于在執(zhí)行run/call方法之后,將本地變量副本恢復(fù)。
*/
@NonNull
publicstaticObjectreplay(@NonNullObjectcaptured){
finalSnapshotcapturedSnapshot=(Snapshot)captured;
returnnewSnapshot(replayTtlValues(capturedSnapshot.ttl2Value),
replayThreadLocalValues(capturedSnapshot.threadLocal2Value));
}

/**
*替換TransmittableThreadLocal
*/
@NonNull
privatestaticHashMap,Object>replayTtlValues(@NonNullHashMap,Object>captured){
//創(chuàng)建副本backup
HashMap,Object>backup=
newHashMap,Object>();

for(finalIterator>iterator=holder.get().keySet().iterator();iterator.hasNext();){
TransmittableThreadLocalthreadLocal=iterator.next();
//對(duì)當(dāng)前線程的本地變量進(jìn)行副本拷貝
backup.put(threadLocal,threadLocal.get());

//若出現(xiàn)調(diào)用線程中不存在某個(gè)線程變量,而線程池中線程有,則刪除線程池中對(duì)應(yīng)的本地變量
if(!captured.containsKey(threadLocal)){
iterator.remove();
threadLocal.superRemove();
}
}
//將捕獲的TTL值打入線程池獲取到的線程TTL中。
setTtlValuesTo(captured);
//是一個(gè)擴(kuò)展點(diǎn),調(diào)用TTL的beforeExecute方法。默認(rèn)實(shí)現(xiàn)為空
doExecuteCallback(true);
returnbackup;
}

privatestaticHashMap,Object>replayThreadLocalValues(@NonNullHashMap,Object>captured){
finalHashMap,Object>backup=
newHashMap,Object>();
for(Map.Entry,Object>entry:captured.entrySet()){
finalThreadLocalthreadLocal=entry.getKey();
backup.put(threadLocal,threadLocal.get());
finalObjectvalue=entry.getValue();
if(value==threadLocalClearMark)threadLocal.remove();
elsethreadLocal.set(value);
}
returnbackup;
}

/**
*清除單線線程的所有TTL和TL,并返回清除之氣的backup
*/
@NonNull
publicstaticObjectclear(){
finalHashMap,Object>ttl2Value=
newHashMap,Object>();

finalHashMap,Object>threadLocal2Value=
newHashMap,Object>();
for(Map.Entry,TtlCopier>entry:threadLocalHolder.entrySet()){
finalThreadLocalthreadLocal=entry.getKey();
threadLocal2Value.put(threadLocal,threadLocalClearMark);
}
returnreplay(newSnapshot(ttl2Value,threadLocal2Value));
}

/**
*還原
*/
publicstaticvoidrestore(@NonNullObjectbackup){
finalSnapshotbackupSnapshot=(Snapshot)backup;
restoreTtlValues(backupSnapshot.ttl2Value);
restoreThreadLocalValues(backupSnapshot.threadLocal2Value);
}

privatestaticvoidrestoreTtlValues(@NonNullHashMap,Object>backup){
//擴(kuò)展點(diǎn),調(diào)用TTL的afterExecute
doExecuteCallback(false);

for(finalIterator>iterator=holder.get().keySet().iterator();iterator.hasNext();){
TransmittableThreadLocalthreadLocal=iterator.next();

if(!backup.containsKey(threadLocal)){
iterator.remove();
threadLocal.superRemove();
}
}

//將本地變量恢復(fù)成備份版本
setTtlValuesTo(backup);
}

privatestaticvoidsetTtlValuesTo(@NonNullHashMap,Object>ttlValues){
for(Map.Entry,Object>entry:ttlValues.entrySet()){
TransmittableThreadLocalthreadLocal=entry.getKey();
threadLocal.set(entry.getValue());
}
}

privatestaticvoidrestoreThreadLocalValues(@NonNullHashMap,Object>backup){
for(Map.Entry,Object>entry:backup.entrySet()){
finalThreadLocalthreadLocal=entry.getKey();
threadLocal.set(entry.getValue());
}
}

/**
*快照類,保存TTL和TL
*/
privatestaticclassSnapshot{
finalHashMap,Object>ttl2Value;
finalHashMap,Object>threadLocal2Value;

privateSnapshot(HashMap,Object>ttl2Value,
HashMap,Object>threadLocal2Value){
this.ttl2Value=ttl2Value;
this.threadLocal2Value=threadLocal2Value;
}
}

進(jìn)入TtlCallable#call()方法。

@Override
publicVcall()throwsException{
Objectcaptured=capturedRef.get();
if(captured==null||releaseTtlValueReferenceAfterCall&&
!capturedRef.compareAndSet(captured,null)){
thrownewIllegalStateException("TTLvaluereferenceisreleasedaftercall!");
}
//調(diào)用replay方法將捕獲到的當(dāng)前線程的本地變量,傳遞給線程池線程的本地變量,
//并且獲取到線程池線程覆蓋之前的本地變量副本。
Objectbackup=replay(captured);
try{
//線程方法調(diào)用
returncallable.call();
}finally{
//使用副本進(jìn)行恢復(fù)。
restore(backup);
}
}

到這基本上線程池方式傳遞本地變量的核心代碼已經(jīng)大概看完了??偟膩碚f在創(chuàng)建TtlCallable對(duì)象是,調(diào)用capture()方法捕獲調(diào)用方的本地線程變量,在call()執(zhí)行時(shí),將捕獲到的線程變量,替換到線程池所對(duì)應(yīng)獲取到的線程的本地變量中,并且在執(zhí)行完成之后,將其本地變量恢復(fù)到調(diào)用之前。

總結(jié)

上述列舉了4種方案,陳某這里推薦方案2和方案4,其中兩種方案的缺點(diǎn)非常明顯,實(shí)際開發(fā)中也是采用的方案2或者方案4。

審核編輯:湯梓紅
聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 接口
    +關(guān)注

    關(guān)注

    33

    文章

    8355

    瀏覽量

    150514
  • spring
    +關(guān)注

    關(guān)注

    0

    文章

    335

    瀏覽量

    14277
  • Boot
    +關(guān)注

    關(guān)注

    0

    文章

    149

    瀏覽量

    35731
  • 線程
    +關(guān)注

    關(guān)注

    0

    文章

    502

    瀏覽量

    19613
  • 數(shù)據(jù)傳遞
    +關(guān)注

    關(guān)注

    1

    文章

    3

    瀏覽量

    1755

原文標(biāo)題:用這4招 優(yōu)雅的實(shí)現(xiàn)Spring Boot 異步線程間數(shù)據(jù)傳遞

文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    Spring Boot如何實(shí)現(xiàn)異步任務(wù)

    Spring Boot 提供了多種方式來實(shí)現(xiàn)異步任務(wù),這里介紹三種主要實(shí)現(xiàn)方式。 1、基于注解 @Async @Async 注解是
    的頭像 發(fā)表于 09-30 10:32 ?1317次閱讀

    Spring Boot虛擬線程和Webflux性能對(duì)比

    早上看到一篇關(guān)于Spring Boot虛擬線程和Webflux性能對(duì)比的文章,覺得還不錯(cuò)。內(nèi)容較長(zhǎng),抓重點(diǎn)給大家介紹一下這篇文章的核心內(nèi)容,方便大家快速閱讀。
    發(fā)表于 09-24 14:54 ?802次閱讀
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>虛擬<b class='flag-5'>線程</b>和Webflux性能對(duì)比

    LabVIEW多線程編程數(shù)據(jù)傳遞教程

    很多時(shí)候在一個(gè)VI的不同線程或者不同VI的不同線程中需要有一些交互——這些線程并不能完全獨(dú)立運(yùn)行,需要一定的數(shù)據(jù)通信才能正確執(zhí)行,這時(shí)就需要在編程時(shí)使用LabVIEW提供的
    的頭像 發(fā)表于 11-24 10:05 ?5559次閱讀
    LabVIEW多<b class='flag-5'>線程</b>編程<b class='flag-5'>數(shù)據(jù)傳遞</b>教程

    通過隊(duì)列實(shí)現(xiàn)vi之間數(shù)據(jù)傳遞

    `各位高手,請(qǐng)教下如何用隊(duì)列實(shí)現(xiàn)vi之間的數(shù)據(jù)傳遞,最好能給出個(gè)例子,我是初學(xué)者,謝謝`
    發(fā)表于 09-08 11:01

    請(qǐng)問C6678核間數(shù)據(jù)傳遞方式是什么?為什么是這樣?

    ,但是給的例子都是SYS/BIOS下面使用的。我想請(qǐng)問一下是否qmss,CPPI只能在操作系統(tǒng)下才能使用,沒有操作系統(tǒng)可以嗎?還有別的核間數(shù)據(jù)傳遞方式嗎?謝謝,請(qǐng)指教!
    發(fā)表于 06-19 02:42

    啟動(dòng)Spring Boot項(xiàng)目應(yīng)用的三種方法

    ,從而使開發(fā)人員不再需要定義樣板化的配置。我的話來理解,就是spring boot其實(shí)不是什么新的框架,它默認(rèn)配置了很多框架的使用方式,就像maven整合了所有的jar包,spring
    發(fā)表于 01-14 17:33

    基于Spring Cloud和Euraka的優(yōu)雅下線以及灰度發(fā)布

    該方式借助的是 Spring Boot 應(yīng)用的 Shutdown hook,應(yīng)用本身的下線也是優(yōu)雅的,但如果你的服務(wù)發(fā)現(xiàn)組件使用的是 Eureka,那么默認(rèn)最長(zhǎng)會(huì)有 90 秒的延遲,其他應(yīng)用才會(huì)感知到該服務(wù)下線
    的頭像 發(fā)表于 04-20 09:52 ?1814次閱讀

    Spring Boot Web相關(guān)的基礎(chǔ)知識(shí)

    Boot的第一個(gè)接口。接下來將會(huì)將會(huì)介紹使用Spring Boot開發(fā)Web應(yīng)用的相關(guān)內(nèi)容,其主要包括使用spring-boot-starter-web組件來
    的頭像 發(fā)表于 03-17 15:03 ?592次閱讀

    簡(jiǎn)述Spring Boot數(shù)據(jù)校驗(yàn)

    上一篇文章我們了解了Spring Boot Web相關(guān)的知識(shí),初步了解了spring-boot-starter-web,還了解了@Contrler和@RestController的差別,如果
    的頭像 發(fā)表于 03-17 15:07 ?708次閱讀

    Spring Boot如何優(yōu)雅實(shí)現(xiàn)數(shù)據(jù)加密存儲(chǔ)、模糊匹配和脫敏

    近來我們都在圍繞著使用Spring Boot開發(fā)業(yè)務(wù)系統(tǒng)時(shí)如何保證數(shù)據(jù)安全性這個(gè)主題展開總結(jié),當(dāng)下大部分的B/S架構(gòu)的系統(tǒng)也都是基于Spring B
    的頭像 發(fā)表于 06-19 14:42 ?1789次閱讀
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>如何<b class='flag-5'>優(yōu)雅</b><b class='flag-5'>實(shí)現(xiàn)</b><b class='flag-5'>數(shù)據(jù)</b>加密存儲(chǔ)、模糊匹配和脫敏

    Spring Boot Actuator快速入門

    不知道大家在寫 Spring Boot 項(xiàng)目的過程中,使用過 Spring Boot Actuator 嗎?知道 Spring
    的頭像 發(fā)表于 10-09 17:11 ?572次閱讀

    Spring Boot啟動(dòng) Eureka流程

    在上篇中已經(jīng)說過了 Eureka-Server 本質(zhì)上是一個(gè) web 應(yīng)用的項(xiàng)目,今天就來看看 Spring Boot 是怎么啟動(dòng) Eureka 的。 Spring Boot 啟動(dòng) E
    的頭像 發(fā)表于 10-10 11:40 ?790次閱讀
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>啟動(dòng) Eureka流程

    Spring Boot的啟動(dòng)原理

    可能很多初學(xué)者會(huì)比較困惑,Spring Boot 是如何做到將應(yīng)用代碼和所有的依賴打包成一個(gè)獨(dú)立的 Jar 包,因?yàn)閭鹘y(tǒng)的 Java 項(xiàng)目打包成 Jar 包之后,需要通過 -classpath 屬性
    的頭像 發(fā)表于 10-13 11:44 ?581次閱讀
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b>的啟動(dòng)原理

    Spring Boot 的設(shè)計(jì)目標(biāo)

    什么是Spring Boot Spring BootSpring 開源組織下的一個(gè)子項(xiàng)目,也是 S
    的頭像 發(fā)表于 10-13 14:56 ?520次閱讀
    <b class='flag-5'>Spring</b> <b class='flag-5'>Boot</b> 的設(shè)計(jì)目標(biāo)

    Spring Boot 3.2支持虛擬線程和原生鏡像

    Spring Boot 3.2 前幾日發(fā)布,讓我們 Java 21、GraalVM 和虛擬線程來嘗試一下。
    的頭像 發(fā)表于 11-30 16:22 ?629次閱讀