展開說說:Android服務之bindService解析

前面兩篇文章我們分別總結了Android四種Service的基本使用以及源碼層面總結一下startService的執行過程,本篇繼續從源碼層面總結bindService的執行過程。

本文依然按著是什么?有什么?怎么用?啥原理?的步驟來分析。

bindService使用方法和調用流程都與startService時有很多相似之處,方便的話請先閱讀上一篇《展開說說:Android服務之startService解析》。

  1. 是什么

調用bindService()來創建,調用方可以通過一個IBinder接口和service進行通信,需要通過ServiceConnection建立連接多用于有交互的場景。

只能調用方通過unbindService()方法來斷開連接。調用方可以和Service通訊,并且一個service可以同時和多個調用方存在綁定關系解除綁定也需要所有調用全部解除綁定之后系統會銷毀service

2、有什么

Service和Activity一樣也有自己的生命周期,也需要在AndroidManifest.xml中注冊。另外bindService的使用比startService要復雜一些:第一需要中創建一個Binder子類并定義方法來給使用者調用在onBind方法中返回它的實例;第二使用者需要創建ServiceConnection對象,并在onServiceConnected回調方法調用Binder子類中定義方法。

2.1 在AndroidManifest.xml中注冊

和startService注冊流程一樣:

?<service android:name="com.example.testdemo.service.ServiceJia" />

2.2 bindService時Service的生命周期

與startService時執行的生命周期有些不同。

onCreate???

它只在Service剛被創建的時刻被調用,Service在運行中,這個方法將不會被調用。也就是只有經歷過onDestroy生命周期以后再次。

onBind

當另一個組件調用?bindService()想要與Service綁定(例如執行 RPC)時執行,在此方法的實現中,必須通過返回?IBinder?提供一個接口,供客戶端用來與服務通信。您必須始終實現此方法;但是,如果您不想允許綁定,則應返回 null。這個方法默認時返回null。

onUnbind

調用方調用?unbindService()?來解除Service綁定時執行

onDestroy

所有綁定到Service的調用方都解綁以后,則系統會銷毀該服務。

onRebind

當Service中的onUnbind方法返回true,并且Service調用unbindService之后并沒有銷毀,此時重新綁定時將會觸發onRebind。Service執行過onBindonUnbind返回true并且還沒執行onDestroy,等再次bindService就會執行它。

日志打印

bindService
2024-12-01 11:19:14.434 30802-30802/com.example.testdemo3?E/com.example.testdemo3.service.ServiceJia: onCreate:


2023-12-01 11:19:14.436 30802-30802/com.example.testdemo3 E/com.example.testdemo3.activity.ServiceActivity: onServiceConnected:


2023-12-01 11:19:14.436 30802-30802/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: JiaBinder ?--doSomething: start conncetion ???//這里不是生命周期,是binder對象調用binder內方法的打印證明完成交互


unbindService
2023-12-01 11:21:10.705 12765-12765/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: onUnbind:


2023-12-01 11:21:10.705 12765-12765/com.example.testdemo3 E/com.example.testdemo3.service.ServiceJia: onDestroy:

  1. 怎么用

因為是有交互的嘛,因此肯定比那些啟動以后就成了甩手掌柜的startService使用稍微負責一些,第一需要中創建一個Binder子類并定義方法來給使用者調用在onBind方法中返回它的實例;第二使用者需要創建ServiceConnection對象,并在onServiceConnected回調方法調用Binder子類中定義方法。

具體可以參考前面的《展開說說:Android四大組件之Service使用》已經總結了使用方法,這里不在贅述。

  1. 啥原理,SDK版本API 30

bindService調用流程都與startService時有很多相似之處,方便的話請先閱讀上一篇《展開說說:Android服務之startService解析》。

bindService的啟動方法是調用

Intent serviceIntent = new Intent(ServiceActivity.this, ServiceJia.class);
bindService(serviceIntent,serviceConnection, Context.BIND_AUTO_CREATE)

然后我們順著bindService方法開始解析源碼,Go :

4.1 從ContexWrapper的bindService開始,同startService:

@Override
public boolean bindService(Intent service, ServiceConnection conn,int flags) {return mBase.bindService(service, conn, flags);
}

4.2 ContextImpl類bindService

mBase的類型是Context,但實際代碼邏輯是在它的實現類ContextImpl類。

 @Overridepublic boolean bindService(Intent service, ServiceConnection conn, int flags) {warnIfCallingFromSystemProcess();return bindServiceCommon(service, conn, flags, null, mMainThread.getHandler(), null,getUser());
}private boolean bindServiceCommon(Intent service, ServiceConnection conn, int flags,String instanceName, Handler handler, Executor executor, UserHandle user) {// Keep this in sync with DevicePolicyManager.bindDeviceAdminServiceAsUser.IServiceConnection sd;if (conn == null) {throw new IllegalArgumentException("connection is null");}if (handler != null && executor != null) {throw new IllegalArgumentException("Handler and Executor both supplied");}if (mPackageInfo != null) {if (executor != null) {sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), executor, flags);} else {sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), handler, flags);}} else {throw new RuntimeException("Not supported in system context");}validateServiceIntent(service);try {IBinder token = getActivityToken();if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mPackageInfo != null&& mPackageInfo.getApplicationInfo().targetSdkVersion< android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) {flags |= BIND_WAIVE_PRIORITY;}service.prepareToLeaveProcess(this);int res = ActivityManager.getService().bindIsolatedService(mMainThread.getApplicationThread(), getActivityToken(), service,service.resolveTypeIfNeeded(getContentResolver()),sd, flags, instanceName, getOpPackageName(), user.getIdentifier());if (res < 0) {throw new SecurityException("Not allowed to bind to service " + service);}return res != 0;} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}

bindService調用bindServiceCommon方法。將ServiceConnection 轉為Binder的實現類IServiceConnection方便跨進程的遠程服務的回調自己定義的方法。

4.3 來到LoadedApk

?final @NonNull LoadedApk mPackageInfo;

因此來到LoadedApk 查看getServiceDispatcher方法:

@UnsupportedAppUsage
public final IServiceConnection getServiceDispatcher(ServiceConnection c,Context context, Handler handler, int flags) {return getServiceDispatcherCommon(c, context, handler, null, flags);
}private IServiceConnection getServiceDispatcherCommon(ServiceConnection c,Context context, Handler handler, Executor executor, int flags) {synchronized (mServices) {LoadedApk.ServiceDispatcher sd = null;ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher> map = mServices.get(context);if (map != null) {if (DEBUG) Slog.d(TAG, "Returning existing dispatcher " + sd + " for conn " + c);sd = map.get(c);}if (sd == null) {if (executor != null) {sd = new ServiceDispatcher(c, context, executor, flags);} else {sd = new ServiceDispatcher(c, context, handler, flags);}if (DEBUG) Slog.d(TAG, "Creating new dispatcher " + sd + " for conn " + c);if (map == null) {map = new ArrayMap<>();mServices.put(context, map);}map.put(c, sd);} else {sd.validate(context, handler, executor);}return sd.getIServiceConnection();}
}

private final ArrayMap<Context, ArrayMap<ServiceConnection, LoadedApk.ServiceDispatcher>> mServices
????= new ArrayMap<>();

mServices記錄了應用當前活動的ServiceConnection和ServiceDispatcher的映射關系,不知是否記得ActivityThread中也有一個final ArrayMap<IBinder, Service> mServices = new ArrayMap<>(); 記錄了IBinder,和Service的映射關系。

繼續說LoadedApk中的service哈,上面代碼會判斷是否存在相同的ServiceConnection,如果不存在就創建新ServiceDispatcher實例并將其存儲在mService中,key時ServiceConnection,value為ServiceDispatcher,ServiceDispatcher內部存儲了ServiceConnection和InnerConnection對象。在調用bindService以后Service和調用方成功建立連接時系統會通過InnerConnection調用ServiceConnection中的onServiceConnected方法,此時我們就可以利用傳過來的IBinder調用Service中的方法完成交互了。這個過程支持跨進程IPC通信,比如兩個進程使用AIDL通信。

4.4 ContextImpl類bindService的bindIsolatedService

返回頭看上面4.2中的bindService方法,繼續向下看會調用ActivityManagerService的bindIsolatedService方法:

synchronized(this) {return mServices.bindServiceLocked(caller, token, service,resolvedType, connection, flags, instanceName, callingPackage, userId);}

4.5 來到ActiveService類的bindServiceLocked

繼續調用本類的bringUpServiceLocked

bringUpServiceLocked(serviceRecord,
????????serviceIntent.getFlags(),
????????callerFg, false, false);

在調用本類realStartServiceLocked

realStartServiceLocked(r, app, execInFg);

一般來說源碼中當一個方法多次穿梭調用之后突然帶上了real,那一定是離真相不遠了。

app.thread.scheduleCreateService(r, r.serviceInfo,
????????mAm.compatibilityInfoForPackage(r.serviceInfo.applicationInfo),
????????app.getReportedProcState());

這一行就很熟悉了,和上一篇startService一樣,調用的ApplicationThread來創建Service實例并調用它的onCreate生命周期。

上一篇分析這個方法之下是調用onStartCommand生命周期

,沒錯這里也不例外下面requestServiceBindingsLocked(r, execInFg)也會去調用ApplicationThread的scheduleBindService:

r.app.thread.scheduleBindService(r, i.intent.getIntent(), rebind,
????????r.app.getReportedProcState());

4.6來到ApplicationThread

利用它封裝的handler發送BIND_SERVICE消息:

public final void scheduleBindService(IBinder token, Intent intent,boolean rebind, int processState) {updateProcessState(processState, false);BindServiceData s = new BindServiceData();s.token = token;s.intent = intent;s.rebind = rebind;if (DEBUG_SERVICE)Slog.v(TAG, "scheduleBindService token=" + token + " intent=" + intent + " uid="+ Binder.getCallingUid() + " pid=" + Binder.getCallingPid());sendMessage(H.BIND_SERVICE, s);}

接收消息:

case BIND_SERVICE:Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "serviceBind");handleBindService((BindServiceData)msg.obj);Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);break;

關鍵來咯:

 private void handleBindService(BindServiceData data) {Service s = mServices.get(data.token);if (DEBUG_SERVICE)Slog.v(TAG, "handleBindService s=" + s + " rebind=" + data.rebind);if (s != null) {try {data.intent.setExtrasClassLoader(s.getClassLoader());data.intent.prepareToEnterProcess();try {if (!data.rebind) {IBinder binder = s.onBind(data.intent);ActivityManager.getService().publishService(data.token, data.intent, binder);} else {s.onRebind(data.intent);ActivityManager.getService().serviceDoneExecuting(data.token, SERVICE_DONE_EXECUTING_ANON, 0, 0);}} catch (RemoteException ex) {throw ex.rethrowFromSystemServer();}} catch (Exception e) {if (!mInstrumentation.onException(s, e)) {throw new RuntimeException("Unable to bind to service " + s+ " with " + data.intent + ": " + e.toString(), e);}}}
}

上面代碼顯示根據token獲取Service對象,然后判斷首次綁定就調用onBind生命周期,已經綁定過就調用onReBind生命周期,返回的IBinder對象就可以用來調用Service中的方法了。但是為了讓調用方拿到這個IBinder就同過onServiceConnected方法回調回去,這個工作就有ActivityManagerService的publishService方法來完成。

4.6 來到ActivityManagerService

 public void publishService(IBinder token, Intent intent, IBinder service) {// Refuse possible leaked file descriptorsif (intent != null && intent.hasFileDescriptors() == true) {throw new IllegalArgumentException("File descriptors passed in Intent");}synchronized(this) {if (!(token instanceof ServiceRecord)) {throw new IllegalArgumentException("Invalid service token");}mServices.publishServiceLocked((ServiceRecord)token, intent, service);}}

然后它有調用了ActiveService的publishServiceLocked方法來處理:

void publishServiceLocked(ServiceRecord r, Intent intent, IBinder service) {final long origId = Binder.clearCallingIdentity();try {if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "PUBLISHING " + r+ " " + intent + ": " + service);if (r != null) {Intent.FilterComparison filter= new Intent.FilterComparison(intent);IntentBindRecord b = r.bindings.get(filter);if (b != null && !b.received) {b.binder = service;b.requested = true;b.received = true;ArrayMap<IBinder, ArrayList<ConnectionRecord>> connections = r.getConnections();for (int conni = connections.size() - 1; conni >= 0; conni--) {ArrayList<ConnectionRecord> clist = connections.valueAt(conni);for (int i=0; i<clist.size(); i++) {ConnectionRecord c = clist.get(i);if (!filter.equals(c.binding.intent.intent)) {if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Not publishing to: " + c);if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Bound intent: " + c.binding.intent.intent);if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Published intent: " + intent);continue;}if (DEBUG_SERVICE) Slog.v(TAG_SERVICE, "Publishing to: " + c);try {c.conn.connected(r.name, service, false);} catch (Exception e) {Slog.w(TAG, "Failure sending service " + r.shortInstanceName+ " to connection " + c.conn.asBinder()+ " (in " + c.binding.client.processName + ")", e);}}}}serviceDoneExecutingLocked(r, mDestroyingServices.contains(r), false);}} finally {Binder.restoreCallingIdentity(origId);}
}

它在for循環里調用一行代碼:

c.conn.connected(r.name, service, false);

順著代碼看:

Conn的類型是是在ConnectionRecord類定義的IServiceConnection

final IServiceConnection conn; ?// The client connection.

Service就是建立連接的Ibinder實例。

4.7再次來到LoadedApk類

看一下IServiceConnectionconnected方法:

private static class InnerConnection extends IServiceConnection.Stub {@UnsupportedAppUsagefinal WeakReference<LoadedApk.ServiceDispatcher> mDispatcher;InnerConnection(LoadedApk.ServiceDispatcher sd) {mDispatcher = new WeakReference<LoadedApk.ServiceDispatcher>(sd);}public void connected(ComponentName name, IBinder service, boolean dead)throws RemoteException {LoadedApk.ServiceDispatcher sd = mDispatcher.get();if (sd != null) {sd.connected(name, service, dead);}}
}

而它又調用了ActivityThread類,mActivityThread就是其中Handler子類 H ,這一步就是為了利用Handler在主線程回調給調用方的onServiceConnected

public void connected(ComponentName name, IBinder service, boolean dead) {if (mActivityExecutor != null) {mActivityExecutor.execute(new RunConnection(name, service, 0, dead));} else if (mActivityThread != null) {mActivityThread.post(new RunConnection(name, service, 0, dead));} else {doConnected(name, service, dead);}
}

RunConnection 的實現:

private final class RunConnection implements Runnable {RunConnection(ComponentName name, IBinder service, int command, boolean dead) {mName = name;mService = service;mCommand = command;mDead = dead;}public void run() {if (mCommand == 0) {doConnected(mName, mService, mDead);} else if (mCommand == 1) {doDeath(mName, mService);}}final ComponentName mName;final IBinder mService;final int mCommand;final boolean mDead;
}

回調onServiceConnected徹底呼應上了

public void doConnected(ComponentName name, IBinder service, boolean dead) {ServiceDispatcher.ConnectionInfo old;ServiceDispatcher.ConnectionInfo info;synchronized (this) {if (mForgotten) {// We unbound before receiving the connection; ignore// any connection received.return;}old = mActiveConnections.get(name);if (old != null && old.binder == service) {// Huh, already have this one.  Oh well!return;}if (service != null) {// A new service is being connected... set it all up.info = new ConnectionInfo();info.binder = service;info.deathMonitor = new DeathMonitor(name, service);try {service.linkToDeath(info.deathMonitor, 0);mActiveConnections.put(name, info);} catch (RemoteException e) {// This service was dead before we got it...  just// don't do anything with it.mActiveConnections.remove(name);return;}} else {// The named service is being disconnected... clean up.mActiveConnections.remove(name);}if (old != null) {old.binder.unlinkToDeath(old.deathMonitor, 0);}}// If there was an old service, it is now disconnected.if (old != null) {mConnection.onServiceDisconnected(name);}if (dead) {mConnection.onBindingDied(name);}// If there is a new viable service, it is now connected.if (service != null) {mConnection.onServiceConnected(name, service);} else {// The binding machinery worked, but the remote returned null from onBind().mConnection.onNullBinding(name);}
}

至此bindService的綁定流程分析完畢!

才疏學淺,如有錯誤,歡迎指正,多謝。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/diannao/43089.shtml
繁體地址,請注明出處:http://hk.pswp.cn/diannao/43089.shtml
英文地址,請注明出處:http://en.pswp.cn/diannao/43089.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Splunk Enterprise 任意文件讀取漏洞(CVE-2024-36991)

文章目錄 前言漏洞描述影響版本漏洞復現POC批量檢測-nuclei腳本 修復建議 前言 Splunk Enterprise 是一款強大的機器數據管理和分析平臺&#xff0c;能夠實時收集、索引、搜索、分析和可視化來自各種數據源的日志和數據&#xff0c;幫助企業提升運營效率、增強安全性和優化業務…

數據庫作業3

DELETE FROM student WHERE grade IS NULL; 一、數據庫操作部分 1. 向 student 表中添加一條新記錄&#xff1a; INSERT INTO student (id, name, grade) VALUES (1, monkey, 98.5); 2. 向 student 表中添加多條新記錄&#xff1a; INSERT INTO student (id, name, grade) V…

【MYSQL】如何解決 bin log 與 redo log 的一致性問題

該問題問的其實就是redo log 的兩階段提交 為什么說redo log 具有崩潰恢復的能力 MySQL Server 層擁有的 bin log 只能用于歸檔&#xff0c;不足以實現崩潰恢復&#xff08;crash-safe&#xff09;&#xff0c;需要借助 InnoDB 引擎的 redo log 才能擁有崩潰恢復的能力。所謂崩…

PHP的發展歷程以及功能使用場景

PHP的發展歷程 PHP的發展歷程可以追溯到1994年&#xff0c;由丹麥計算機程序員拉斯穆斯勒多夫&#xff08;Rasmus Lerdorf&#xff09;出于個人網站統計訪問者信息的需求而創建。以下是PHP發展歷程中的幾個重要里程碑&#xff1a; 初創階段&#xff08;1994-1995年&#xff09…

二刷力扣——單調棧

739. 每日溫度 單調棧應該從棧底到棧頂 是遞減的。 找下一個更大的 &#xff0c;用遞減單調棧&#xff0c;就可以確定在棧里面的每個比當前元素i小的元素&#xff0c;下一個更大的就是這個i&#xff0c;然后彈出并記錄&#xff1b;然后當前元素i入棧&#xff0c;仍然滿足遞減…

數學基礎 -- 三角學

三角學 三角學&#xff08;Trigonometry&#xff09;是數學的一個分支&#xff0c;主要研究三角形的邊長與角度之間的關系。三角學在幾何學、物理學、工程學等多個領域中有廣泛的應用。以下是三角學的一些基本概念和公式&#xff1a; 基本概念 直角三角形&#xff1a;一個角…

Java進階----繼承

繼承 一.繼承概述 繼承是可以通過定義新的類&#xff0c;在已有類的基礎上擴展屬性和功能的一種技術. 案例&#xff1a;優化 貓、狗JavaBean類的設計 狗類&#xff1a;Dog 屬性&#xff1a;名字 name&#xff0c;年齡 age 方法&#xff1a;看家 watchHome()&#xff0c;Gett…

防抖和字節流

防抖&#xff08;Debouncing&#xff09;和字節流&#xff08;Byte Stream&#xff09;是兩個不同的概念&#xff0c;分別在編程和數據傳輸領域中使用。 防抖&#xff08;Debouncing&#xff09; 防抖是一種在前端開發中常用的技術&#xff0c;用于控制事件處理函數的執行頻率…

Android多開應用軟件系統設計

設計一個支持Android多開應用的軟件系統&#xff0c;主要涉及到以下幾個關鍵技術點和設計考慮&#xff1a; 1. 虛擬化技術 容器技術&#xff1a;與傳統的虛擬機不同&#xff0c;可以采用更輕量級的容器技術&#xff0c;為每個應用實例創建獨立的運行環境。這包括分配獨立的用…

Ubuntu配置sendmail client,用sendmail命令來發送郵件

參考文檔 https://mailoutgoing.com/support/mailrelay/sendmail.html https://www.sendmail.org/~ca/email/auth.html https://docs.oracle.com/en/operating-systems/oracle-linux/6/admin/configure-sendmail.html 總結 1、ubuntu環境下&#xff0c;sendmail服務位于/etc/i…

HTTP 請求走私漏洞詳解

超詳細的HTTP請求走私漏洞教程&#xff0c;看完還不會你來找我。 1. 簡介 HTTP請求走私漏洞&#xff08;HTTP Request Smuggling&#xff09;發生在前端服務器&#xff08;也稱代理服務器&#xff0c;一般會進行身份驗證或訪問控制&#xff09;和后端服務器在解析HTTP請求時&…

上位機圖像處理和嵌入式模塊部署(mcu項目2:串口日志記錄器)

【 聲明&#xff1a;版權所有&#xff0c;歡迎轉載&#xff0c;請勿用于商業用途。 聯系信箱&#xff1a;feixiaoxing 163.com】 淘寶上面有一個商品蠻好玩的&#xff0c;那就是日志記錄器。說是記錄器&#xff0c;其實就是一個模塊&#xff0c;這個模塊的輸入是一個ttl串口&am…

利用Python進行數據分析PDF下載經典數據分享推薦

本書由Python pandas項目創始人Wes McKinney親筆撰寫&#xff0c;詳細介紹利用Python進行操作、處理、清洗和規整數據等方面的具體細節和基本要點。第2版針對Python 3.6進行全面修訂和更新&#xff0c;涵蓋新版的pandas、NumPy、IPython和Jupyter&#xff0c;并增加大量實際案例…

Docker Desktop如何換鏡像源?

docker現在很多鏡像源都出現了問題,導致無法拉取鏡像,所以找到一個好的鏡像源,尤為重要。 一、阿里鏡像源 經過測試,目前,阿里云鏡像加速地址還可以使用。如果沒有阿里云賬號,需要先注冊一個賬號。 地址:https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors 二…

基于Java技術的B/S模式書籍學習平臺

你好&#xff0c;我是專注于計算機科學領域的學姐碼農小野。如果你對書籍學習平臺開發感興趣或有相關需求&#xff0c;歡迎私信聯系我。 開發語言&#xff1a; Java 數據庫&#xff1a; MySQL 技術&#xff1a; B/S模式、Java技術 工具&#xff1a; Eclipse、Navicat、Mave…

【Go】函數的使用

目錄 函數返回多個值 init函數和import init函數 main函數 函數的參數 值傳遞 引用傳遞&#xff08;指針&#xff09; 函數返回多個值 用法如下&#xff1a; package mainimport ("fmt""strconv" )// 返回多個返回值&#xff0c;無參數名 func Mu…

相鄰不同數字的標記

鏈接&#xff1a;登錄—專業IT筆試面試備考平臺_牛客網 來源&#xff1a;牛客網 時間限制&#xff1a;C/C 1秒&#xff0c;其他語言2秒 空間限制&#xff1a;C/C 262144K&#xff0c;其他語言524288K 64bit IO Format: %lld 題目描述 小紅拿到了一個數組&#xff0c;每個數…

ctfshow web入門 nodejs web334--web337

web334 有個文件下載之后改后綴為zip加壓就可以得到兩個文件 一個文件類似于index.php 還有一個就是登錄密碼登錄成功就有flag username:ctfshow password:123456因為 return name!CTFSHOW && item.username name.toUpperCase() && item.password passwor…

SpringBoot的熱部署和日志體系

SpringBoot的熱部署 每次修改完代碼&#xff0c;想看效果的話&#xff0c;不用每次都重新啟動代碼&#xff0c;等待項目重啟 這樣就可以了 JDK官方提出的日志框架&#xff1a;Jul log4j的使用方式&#xff1a; &#xff08;1&#xff09;引入maven依賴 &#xff08;2&#x…

軟件開發語言都有哪些?

構建高效、穩定且安全的服務器應用&#xff0c;開發者們需要選擇合適的編程語言。以下是幾種流行的網絡服務器開發語言&#xff0c;每種語言都有其獨特的特性和適用場景。 Java Java是一種廣泛使用的高級編程語言&#xff0c;以其“一次編寫&#xff0c;到處運行”的理念而著稱…