網(wǎng)站首頁(yè) 編程語(yǔ)言 正文
引言
相信大家對(duì)于Binder這個(gè)概念是非常熟悉了,這是Android系統(tǒng) 獨(dú)有的進(jìn)程間通信框架,而對(duì)于Binder底層是如何實(shí)現(xiàn)進(jìn)程間通信,大家熟悉嗎,包括Proxy和Stub機(jī)制,那么從本章開始就開始介紹Binder進(jìn)程間通信機(jī)制。
1 system_server和service_manager的關(guān)系
我們知道,Android系統(tǒng)起始于init進(jìn)程,我們通過(guò)adb shell ps -ef命令可以查看當(dāng)前系統(tǒng)運(yùn)行的全部進(jìn)程,init進(jìn)程它的進(jìn)程號(hào)是1
我們接著去找system_server進(jìn)程和service_manager進(jìn)程
我們通過(guò)上圖可以看到,service_manager進(jìn)程的父進(jìn)程是init進(jìn)程,而system_server進(jìn)程的父進(jìn)程是zygote進(jìn)程,那么我們可以看下圖
也就是說(shuō),當(dāng)init進(jìn)程fork出zygote進(jìn)程之后,通過(guò)zygote進(jìn)程創(chuàng)建了system_server進(jìn)程
我們看下system_server的源碼
//------SystemServer的main函數(shù)-------//
// The main entry point from zygote.
public static void main(String[] args) {
new SystemServer().run();
}
在SystemServer源碼的main函數(shù)注釋中,已經(jīng)提示了這個(gè)是zygote進(jìn)程調(diào)用main方法,并啟動(dòng)了SystemServer進(jìn)程
我們知道,在SystemServer中,持有了像AMS、PMS、WMS等系統(tǒng)服務(wù),但是我們?cè)谑褂玫臅r(shí)候能直接使用這些服務(wù)嗎?不是的,SystemServer只是持有了這些服務(wù),并不對(duì)外暴露;
ServiceManager.addService("package", m);
final PackageManagerNative pmn = m.new PackageManagerNative();
ServiceManager.addService("package_native", pmn);
而service_manager則是管理這些服務(wù)類,例如PMS,在創(chuàng)建了Service之后還是將Service放到了service_manager中,而且只負(fù)責(zé)運(yùn)行Binder,也就是說(shuō)當(dāng)service_manager要調(diào)用某個(gè)服務(wù)的時(shí)候,是通過(guò)進(jìn)程間通信的方式來(lái)獲取的。
2 傳統(tǒng)IPC與Binder之間的區(qū)別
我們看下FileOutputStream的write方法是如何把數(shù)據(jù)寫入磁盤的:
public void write(byte b[], int off, int len) throws IOException {
// Android-added: close() check before I/O.
if (closed && len > 0) {
throw new IOException("Stream Closed");
}
// Android-added: Tracking of unbuffered I/O.
tracker.trackIo(len);
// Android-changed: Use IoBridge instead of calling native method.
IoBridge.write(fd, b, off, len);
}
在write方法中,核心方法就是調(diào)用了IoBridge的write方法,看注釋就是說(shuō)IoBridge代替了之前調(diào)用native方法,但最終還是調(diào)用了native的方法。
像傳統(tǒng)的IPC,在用戶空間發(fā)送寫入數(shù)據(jù)的指令,真正的數(shù)據(jù)寫入是發(fā)生在內(nèi)核空間,通過(guò)ioctl的讀寫操作,寫入數(shù)據(jù)緩沖區(qū),另一個(gè)進(jìn)程如果需要獲取這個(gè)數(shù)據(jù),在通過(guò)ioctl將數(shù)據(jù)拷貝到進(jìn)程2的內(nèi)存空間中,所以傳統(tǒng)的IPC進(jìn)程間通信需要2次拷貝;
而Binder的優(yōu)勢(shì)在哪呢?Binder只需要一次拷貝,這里就是用了mmap的方式,那么mmap是如何工作的呢?我們知道所有的讀寫操作都是在內(nèi)核空間完成的,那么mmap就是開辟一塊物理內(nèi)存,與內(nèi)核空間完成映射,并且所有的進(jìn)程內(nèi)存空間與這塊物理內(nèi)存也存在映射關(guān)系。
當(dāng)進(jìn)程1拿到這塊物理內(nèi)存的地址之后,便可以將數(shù)據(jù)拷貝到這塊物理內(nèi)存,因?yàn)檫M(jìn)程2和這塊內(nèi)存存在映射關(guān)系,因此進(jìn)程2便可以拿到進(jìn)程1的數(shù)據(jù),騰訊的MMKV便是基于mmap實(shí)現(xiàn)的。
所以相較于傳統(tǒng)的IPC,Binder進(jìn)程間通信只需要一次拷貝,因此Binder的性能更優(yōu)。
3 物理內(nèi)存和虛擬內(nèi)存
對(duì)于物理內(nèi)存和虛擬內(nèi)存,可能很多小伙伴對(duì)于這個(gè)概念比較模糊;這個(gè)概念是源自于Linux,其中物理內(nèi)存是系統(tǒng)硬件提供的內(nèi)存,這才是真正的內(nèi)存,例如系統(tǒng)有32M的物理內(nèi)存,運(yùn)行33M內(nèi)存的應(yīng)用肯定不能work的,這個(gè)時(shí)候虛擬內(nèi)存就出現(xiàn)了,目的就是為了解決物理內(nèi)存不足的情況,因此當(dāng)一個(gè)系統(tǒng)物理內(nèi)存用盡之后,意味著離崩潰就不遠(yuǎn)了。
因此現(xiàn)在大多數(shù)的程序就是運(yùn)行在虛擬內(nèi)存,而且在應(yīng)用層是絕對(duì)不可能取到物理內(nèi)存的,例如:
val a:Int = 10
int a = 10
int *addr = &a
那么我們的代碼是存在虛擬內(nèi)存還是物理內(nèi)存呢?首先,因?yàn)?strong>我們的代碼在某一時(shí)間并不是全部執(zhí)行的,在一個(gè)類中有1000個(gè)方法,可能只有1個(gè)方法被執(zhí)行,這就是程序的局部性原則; 所以只有當(dāng)部分代碼被CPU執(zhí)行的時(shí)候,才會(huì)將代碼加載到物理內(nèi)存,剩下的大部分代碼會(huì)存儲(chǔ)在磁盤中,因此128M的物理內(nèi)存,可以加載10G的程序代碼。
4 Binder驅(qū)動(dòng)源碼分析
因?yàn)閟ervice_manager主要負(fù)責(zé)Binder運(yùn)行,那么Binder驅(qū)動(dòng)的初始化必然也是在其中,所以我們先去看一下service_manager的源碼;我這邊看的是Android 9.0的源碼,因?yàn)榈讓釉创a很少會(huì)有改動(dòng),所以每個(gè)版本基本一致
https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/native/cmds/servicemanager ==> service_manager.c
int main(int argc, char** argv)
{
struct binder_state *bs;
union selinux_callback cb;
char *driver;
if (argc > 1) {
driver = argv[1];
} else {
driver = "/dev/binder";
}
//開啟binder驅(qū)動(dòng) ==> /dev/binder
bs = binder_open(driver, 128*1024);
if (!bs) {
#ifdef VENDORSERVICEMANAGER
ALOGW("failed to open binder driver %s\n", driver);
while (true) {
sleep(UINT_MAX);
}
#else
ALOGE("failed to open binder driver %s\n", driver);
#endif
return -1;
}
if (binder_become_context_manager(bs)) {
ALOGE("cannot become context manager (%s)\n", strerror(errno));
return -1;
}
cb.func_audit = audit_callback;
selinux_set_callback(SELINUX_CB_AUDIT, cb);
cb.func_log = selinux_log_callback;
selinux_set_callback(SELINUX_CB_LOG, cb);
#ifdef VENDORSERVICEMANAGER
sehandle = selinux_android_vendor_service_context_handle();
#else
sehandle = selinux_android_service_context_handle();
#endif
selinux_status_open(true);
if (sehandle == NULL) {
ALOGE("SELinux: Failed to acquire sehandle. Aborting.\n");
abort();
}
if (getcon(&service_manager_context) != 0) {
ALOGE("SELinux: Failed to acquire service_manager context. Aborting.\n");
abort();
}
//開啟循環(huán)
binder_loop(bs, svcmgr_handler);
return 0;
}
首先,我們先看service_manager的源碼,一般C/C++的源碼首先找main函數(shù),這個(gè)是程序的入口,首先調(diào)用了binder_open,打開了/dev/binder路徑下的驅(qū)動(dòng)driver,我們看下binder_open的實(shí)現(xiàn)。
// https://www.androidos.net.cn/android/9.0.0_r8/xref/frameworks/native/cmds/servicemanager ==> binder.c
struct binder_state *binder_open(const char* driver, size_t mapsize)
{
struct binder_state *bs;
struct binder_version vers;
bs = malloc(sizeof(*bs));
if (!bs) {
errno = ENOMEM;
return NULL;
}
//① 打開binder驅(qū)動(dòng)文件,類似于打開一個(gè)apk,驅(qū)動(dòng)文件是由代碼生成的
bs->fd = open(driver, O_RDWR | O_CLOEXEC);
if (bs->fd < 0) {
fprintf(stderr,"binder: cannot open %s (%s)\n",
driver, strerror(errno));
goto fail_open;
}
if ((ioctl(bs->fd, BINDER_VERSION, &vers) == -1) ||
(vers.protocol_version != BINDER_CURRENT_PROTOCOL_VERSION)) {
fprintf(stderr,
"binder: kernel driver version (%d) differs from user space version (%d)\n",
vers.protocol_version, BINDER_CURRENT_PROTOCOL_VERSION);
goto fail_open;
}
bs->mapsize = mapsize;
//② 內(nèi)存映射
bs->mapped = mmap(NULL, mapsize, PROT_READ, MAP_PRIVATE, bs->fd, 0);
if (bs->mapped == MAP_FAILED) {
fprintf(stderr,"binder: cannot map device (%s)\n",
strerror(errno));
goto fail_map;
}
return bs;
fail_map:
close(bs->fd);
fail_open:
free(bs);
return NULL;
}
在binder_open方法中,首先初始化一個(gè)binder_state對(duì)象,這個(gè)會(huì)作為binder_open的返回值,并在一開始為其分配內(nèi)存空間
①:調(diào)用open方法,這里是把/dev/binder傳進(jìn)來(lái),相當(dāng)于將驅(qū)動(dòng)打開,那么接下來(lái)移步至4.1小節(jié),看Binder驅(qū)動(dòng)在內(nèi)核空間做了什么事?
②:打開驅(qū)動(dòng)之后,調(diào)用了mmap方法,通過(guò)4.1小節(jié)我們知道,這個(gè)其實(shí)是調(diào)用了binder_mmap,那么移步至4.2小節(jié),看下binder_mmap的源碼
4.1 binder_init
接下來(lái),我們看下Binder驅(qū)動(dòng)的源碼,在Binder驅(qū)動(dòng)中也有一個(gè)binder.c文件,看下它的初始化方法,在device_initcall中傳入一個(gè)方法binder_init,這個(gè)方法就是Binder驅(qū)動(dòng)初始化的開始
//http://androidxref.com/kernel_3.18/xref/drivers/staging/android/binder.c
static int __init binder_init(void)
{
int ret;
binder_deferred_workqueue = create_singlethread_workqueue("binder");
if (!binder_deferred_workqueue)
return -ENOMEM;
binder_debugfs_dir_entry_root = debugfs_create_dir("binder", NULL);
if (binder_debugfs_dir_entry_root)
binder_debugfs_dir_entry_proc = debugfs_create_dir("proc",
binder_debugfs_dir_entry_root);
//注冊(cè)Binder設(shè)備
ret = misc_register(&binder_miscdev);
if (binder_debugfs_dir_entry_root) {
debugfs_create_file("state",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_state_fops);
debugfs_create_file("stats",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_stats_fops);
debugfs_create_file("transactions",
S_IRUGO,
binder_debugfs_dir_entry_root,
NULL,
&binder_transactions_fops);
debugfs_create_file("transaction_log",
S_IRUGO,
binder_debugfs_dir_entry_root,
&binder_transaction_log,
&binder_transaction_log_fops);
debugfs_create_file("failed_transaction_log",
S_IRUGO,
binder_debugfs_dir_entry_root,
&binder_transaction_log_failed,
&binder_transaction_log_fops);
}
return ret;
}
//初始化的位置
device_initcall(binder_init);
在binder_init方法中,調(diào)用了misc_register,傳入了一個(gè)對(duì)象binder_miscdev
static struct miscdevice binder_miscdev = {
.minor = MISC_DYNAMIC_MINOR,
.name = "binder",
.fops = &binder_fops
};
static const struct file_operations binder_fops = {
.owner = THIS_MODULE,
.poll = binder_poll,
.unlocked_ioctl = binder_ioctl,
.compat_ioctl = binder_ioctl,
.mmap = binder_mmap,
.open = binder_open,
.flush = binder_flush,
.release = binder_release,
};
其實(shí)這里主要就是干了一件事,對(duì)外暴露對(duì)驅(qū)動(dòng)的操作,并與驅(qū)動(dòng)內(nèi)部的方法做映射;這句話可能比較繞,但是看本節(jié)開頭的①部分,這里調(diào)用了open方法,其實(shí)在驅(qū)動(dòng)中就是調(diào)用了binder_open方法,只不過(guò)外部是無(wú)法直接調(diào)用binder_open方法
我們看這里注冊(cè)了幾個(gè)方法,都比較重要:binder_open、binder_mmap、binder_ioctl,我們一個(gè)一個(gè)來(lái)看
4.2 binder_open
這個(gè)方法,才是用戶空間真正地打開驅(qū)動(dòng)的位置
static int binder_open(struct inode *nodp, struct file *filp)
{
struct binder_proc *proc;
binder_debug(BINDER_DEBUG_OPEN_CLOSE, "binder_open: %d:%d\n",
current->group_leader->pid, current->pid);
//① 分配內(nèi)存
proc = kzalloc(sizeof(*proc), GFP_KERNEL);
if (proc == NULL)
return -ENOMEM;
//②
get_task_struct(current);
proc->tsk = current;
INIT_LIST_HEAD(&proc->todo);
init_waitqueue_head(&proc->wait);
proc->default_priority = task_nice(current);
binder_lock(__func__);
binder_stats_created(BINDER_STAT_PROC);
hlist_add_head(&proc->proc_node, &binder_procs);
proc->pid = current->group_leader->pid;
INIT_LIST_HEAD(&proc->delivered_death);
filp->private_data = proc;
binder_unlock(__func__);
if (binder_debugfs_dir_entry_proc) {
char strbuf[11];
snprintf(strbuf, sizeof(strbuf), "%u", proc->pid);
proc->debugfs_entry = debugfs_create_file(strbuf, S_IRUGO,
binder_debugfs_dir_entry_proc, proc, &binder_proc_fops);
}
return 0;
}
在這個(gè)方法中,首先定義了一個(gè)binder_proc引用,這個(gè)binder_proc是什么?它是Binder中維護(hù)的一個(gè)雙向鏈表,用于記錄每個(gè)進(jìn)程的信息,我們看下圖:
因?yàn)槲覀冎溃總€(gè)進(jìn)程只要調(diào)用服務(wù),那么service_manager都會(huì)調(diào)用binder_open方法,將這個(gè)進(jìn)程信息存儲(chǔ)在binder_proc鏈表中。
①:所以在調(diào)用binder_open之后,調(diào)用kzalloc在內(nèi)核空間為這個(gè)進(jìn)程分配一塊內(nèi)存
②:然后獲取當(dāng)前進(jìn)程信息,并將其放置在binder_proc鏈表的頭部\
打開了驅(qū)動(dòng),就有了進(jìn)程間通信的能力。
4.2 binder_mmap
binder_mmap,我們之前簡(jiǎn)單介紹過(guò)mmap的原理,那么這里我們看下,Binder驅(qū)動(dòng)內(nèi)部是如何做的
static int binder_mmap(struct file *filp, struct vm_area_struct *vma)
{
int ret;
//內(nèi)核空間
struct vm_struct *area;
//當(dāng)前進(jìn)程信息
struct binder_proc *proc = filp->private_data;
const char *failure_string;
struct binder_buffer *buffer;
if (proc->tsk != current)
return -EINVAL;
//①
if ((vma->vm_end - vma->vm_start) > SZ_4M)
vma->vm_end = vma->vm_start + SZ_4M;
mutex_lock(&binder_mmap_lock);
if (proc->buffer) {
ret = -EBUSY;
failure_string = "already mapped";
goto err_already_mapped;
}
......
//②
area = get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);
if (area == NULL) {
ret = -ENOMEM;
failure_string = "get_vm_area";
goto err_get_vm_area_failed;
}
proc->buffer = area->addr;
proc->user_buffer_offset = vma->vm_start - (uintptr_t)proc->buffer;
mutex_unlock(&binder_mmap_lock);
#ifdef CONFIG_CPU_CACHE_VIPT
if (cache_is_vipt_aliasing()) {
while (CACHE_COLOUR((vma->vm_start ^ (uint32_t)proc->buffer))) {
pr_info("binder_mmap: %d %lx-%lx maps %p bad alignment\n", proc->pid, vma->vm_start, vma->vm_end, proc->buffer);
vma->vm_start += PAGE_SIZE;
}
}
#endif
proc->pages = kzalloc(sizeof(proc->pages[0]) * ((vma->vm_end - vma->vm_start) / PAGE_SIZE), GFP_KERNEL);
if (proc->pages == NULL) {
ret = -ENOMEM;
failure_string = "alloc page array";
goto err_alloc_pages_failed;
}
proc->buffer_size = vma->vm_end - vma->vm_start;
vma->vm_ops = &binder_vm_ops;
vma->vm_private_data = proc;
//③
if (binder_update_page_range(proc, 1, proc->buffer, proc->buffer + PAGE_SIZE, vma)) {
ret = -ENOMEM;
failure_string = "alloc small buf";
goto err_alloc_small_buf_failed;
}
buffer = proc->buffer;
INIT_LIST_HEAD(&proc->buffers);
list_add(&buffer->entry, &proc->buffers);
buffer->free = 1;
binder_insert_free_buffer(proc, buffer);
proc->free_async_space = proc->buffer_size / 2;
barrier();
proc->files = get_files_struct(current);
proc->vma = vma;
proc->vma_vm_mm = vma->vm_mm;
/*pr_info("binder_mmap: %d %lx-%lx maps %p\n",
proc->pid, vma->vm_start, vma->vm_end, proc->buffer);*/
return 0;
err_alloc_small_buf_failed:
kfree(proc->pages);
proc->pages = NULL;
err_alloc_pages_failed:
mutex_lock(&binder_mmap_lock);
vfree(proc->buffer);
proc->buffer = NULL;
err_get_vm_area_failed:
err_already_mapped:
mutex_unlock(&binder_mmap_lock);
err_bad_arg:
pr_err("binder_mmap: %d %lx-%lx %s failed %d\n",
proc->pid, vma->vm_start, vma->vm_end, failure_string, ret);
return ret;
}
我們先看下binder_mmap的兩個(gè)入?yún)ⅲ菑膕ervice_manager那邊傳過(guò)來(lái)的,我們重點(diǎn)關(guān)注第二個(gè)參數(shù):vma,我們可以把它看做是用戶空間,然后在binder_mmap中創(chuàng)建了一個(gè)area,就是內(nèi)核空間
①:首先,會(huì)判斷用戶空間大小是否超過(guò)4M,我們可以往前看,當(dāng)service_manager調(diào)用open方法時(shí),傳入的mapsize大小為128 * 1024,也就是128K,也就是說(shuō)在內(nèi)核空間開辟了一塊128K的用戶空間內(nèi)存
②:get_vm_area(vma->vm_end - vma->vm_start, VM_IOREMAP);調(diào)用get_vm_area方法,就是在內(nèi)核空間尋找一塊連續(xù)的內(nèi)存,多大呢?就是傳進(jìn)來(lái)的用戶空間的大小;然后將內(nèi)核空間的虛擬地址賦值給用戶進(jìn)程
③:調(diào)用binder_update_page_range方法,這個(gè)方法中主要工作就是創(chuàng)建物理內(nèi)存并做映射關(guān)系,看下源碼
static int binder_update_page_range(struct binder_proc *proc, int allocate,
void *start, void *end,
struct vm_area_struct *vma)
{
void *page_addr;
unsigned long user_page_addr;
struct vm_struct tmp_area;
struct page **page;
struct mm_struct *mm;
//......
if (allocate == 0)
goto free_range;
if (vma == NULL) {
pr_err("%d: binder_alloc_buf failed to map pages in userspace, no vma\n",
proc->pid);
goto err_no_vma;
}
for (page_addr = start; page_addr < end; page_addr += PAGE_SIZE) {
int ret;
page = &proc->pages[(page_addr - proc->buffer) / PAGE_SIZE];
BUG_ON(*page);
//分配一頁(yè)的物理內(nèi)存 4K
*page = alloc_page(GFP_KERNEL | __GFP_HIGHMEM | __GFP_ZERO);
if (*page == NULL) {
pr_err("%d: binder_alloc_buf failed for page at %p\n",
proc->pid, page_addr);
goto err_alloc_page_failed;
}
tmp_area.addr = page_addr;
tmp_area.size = PAGE_SIZE + PAGE_SIZE /* guard page? */;
//將內(nèi)核空間與其建立映射關(guān)系
ret = map_vm_area(&tmp_area, PAGE_KERNEL, page);
if (ret) {
pr_err("%d: binder_alloc_buf failed to map page at %p in kernel\n",
proc->pid, page_addr);
goto err_map_kernel_failed;
}
user_page_addr =
(uintptr_t)page_addr + proc->user_buffer_offset;
//將用戶空間與其建立映射關(guān)系
ret = vm_insert_page(vma, user_page_addr, page[0]);
if (ret) {
pr_err("%d: binder_alloc_buf failed to map page at %lx in userspace\n",
proc->pid, user_page_addr);
goto err_vm_insert_page_failed;
}
/* vm_insert_page does not seem to increment the refcount */
}
if (mm) {
up_write(&mm->mmap_sem);
mmput(mm);
}
return 0;
這里我們看到就是,首先會(huì)分配一頁(yè)的物理內(nèi)存4K,然后調(diào)用map_vm_area將內(nèi)核空間虛擬地址與物理內(nèi)存映射;調(diào)用vm_insert_page方法,將用戶空間與物理內(nèi)存映射,見下圖:
就這樣,完成了物理內(nèi)存與用戶空間和內(nèi)核空間的映射,binder_mmap完成了自己的工作。
接著再回到service_manager的main方法中,我們看到調(diào)用了binder_open之后,會(huì)調(diào)用binder_loop方法,這個(gè)有點(diǎn)兒類似Android的Handler,也是開啟循環(huán),接收命令去執(zhí)行任務(wù)。
原文鏈接:https://juejin.cn/post/7150495774290214919
相關(guān)推薦
- 2022-07-26 Python使用shutil操作文件、subprocess運(yùn)行子程序_python
- 2022-11-21 詳解React獲取DOM和獲取組件實(shí)例的方式_React
- 2022-09-29 Python組合數(shù)據(jù)類型詳解_python
- 2022-06-06 typescript使用class關(guān)鍵字定義一個(gè)類、static、readonly
- 2022-12-23 C++中關(guān)于getchar()的使用方法_C 語(yǔ)言
- 2022-09-24 教你創(chuàng)建一個(gè)帶診斷工具的.NET鏡像_C#教程
- 2023-12-18 Mybatisplus的增刪改查
- 2022-07-25 Android?嵌套?Intent?隱患及解決方案_Android
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過(guò)濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支