【毕设扫描器】【参数Fuzz】第二篇:动态爬虫的创建、启动和协程池

本文主要是介绍【毕设扫描器】【参数Fuzz】第二篇:动态爬虫的创建、启动和协程池,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 前言
    • Crawlergo发送请求的代码节点
        • 设置代理调试寻找(未抓到包)
        • WireShark本地抓包(找到代码节点)
    • CrawlerGo设置多线程爬虫
        • 爬虫调用代码
        • 创建爬虫任务
        • 启动爬虫任务
        • 把任务添加到线程池
        • 重要函数表格
    • 接着梳理协程任务的执行
        • 启动 goroutine 执行操作
        • goroutine 的操作:task.Task
        • 配置引擎的库:/pkg/engine
        • 发送请求的 tab 任务
            • 结构体:tab
        • 执行操作函数:tab.Start()
        • 处理响应信息:collect_links.go(分析方向的转折点)
    • 小结

前言

在 《第一篇:数据的定义、读取和装配(爬虫数据和Payload数据)》文章中,我们完成了 URL 数据的读取、以及 Payload 的加载部分。

接下来要做的是发送请求对象,根据响应对象判断是否存在漏洞。

本篇文章要探究的,就是 CrawlerGo 动态爬虫发送请求和处理的过程,以求为参数 Fuzz 发送请求和处理响应提供参考和借鉴。

Crawlergo发送请求的代码节点

CrawlerGo 为动态爬虫装配数据的代码,前面我们基本阅读完毕。接下来看创建爬虫任务、以及启动爬虫部分的代码,查看具体的实现细节。

// crawlergo_cmd.go 代码 311 行// 开始爬虫任务task, err := pkg.NewCrawlerTask(targets, taskConfig)if err != nil {logger.Logger.Error("create crawler task failed.")os.Exit(-1)}……go handleExit(task)logger.Logger.Info("Start crawling.")task.Run()result := task.Result// 追踪 task.Run()
// task_main.go 代码 173行
/*
开始当前任务
*/
func (t *CrawlerTask) Run() {defer t.Pool.Release()  // 释放协程池defer t.Browser.Close() // 关闭浏览器// 为 Targets 设置 reqsFromRobots 变量if t.Config.PathFromRobots {reqsFromRobots := GetPathsFromRobots(*t.Targets[0])logger.Logger.Info("get paths from robots.txt: ", len(reqsFromRobots))t.Targets = append(t.Targets, reqsFromRobots...)}// 为 Targets 设置 reqsByFuzz 变量if t.Config.FuzzDictPath != "" {if t.Config.PathByFuzz {logger.Logger.Warn("`--fuzz-path` is ignored, using `--fuzz-path-dict` instead")}reqsByFuzz := GetPathsByFuzzDict(*t.Targets[0], t.Config.FuzzDictPath)t.Targets = append(t.Targets, reqsByFuzz...)} else if t.Config.PathByFuzz {reqsByFuzz := GetPathsByFuzz(*t.Targets[0])logger.Logger.Info("get paths by fuzzing: ", len(reqsByFuzz))t.Targets = append(t.Targets, reqsByFuzz...)}t.Result.AllReqList = t.Targets[:]// 创建 Request 对象用于 task 的初始化var initTasks []*model.Request// 遍历 Targets 数组for _, req := range t.Targets {if t.smartFilter.DoFilter(req) {logger.Logger.Debugf("filter req: " + req.URL.RequestURI())continue}initTasks = append(initTasks, req)t.Result.ReqList = append(t.Result.ReqList, req)}logger.Logger.Info("filter repeat, target count: ", len(initTasks))for _, req := range initTasks {if !engine2.IsIgnoredByKeywordMatch(*req, t.Config.IgnoreKeywords) {t.addTask2Pool(req)}}t.taskWG.Wait()// 对全部请求进行唯一去重todoFilterAll := make([]*model.Request, len(t.Result.AllReqList))for index := range t.Result.AllReqList {todoFilterAll[index] = t.Result.AllReqList[index]}t.Result.AllReqList = []*model.Request{}var simpleFilter filter2.SimpleFilterfor _, req := range todoFilterAll {if !simpleFilter.UniqueFilter(req) {t.Result.AllReqList = append(t.Result.AllReqList, req)}}// 全部域名t.Result.AllDomainList = AllDomainCollect(t.Result.AllReqList)// 子域名t.Result.SubDomainList = SubDomainCollect(t.Result.AllReqList, t.RootDomain)
}
设置代理调试寻找(未抓到包)

(1)设置代理参数,开启 Burpsuite,没抓到 HTTP 数据包。

--request-proxy http://127.0.0.1:8080 --output-json debug.json http://127.0.0.1/sqli-labs-master/Less-1/?id=1

(2)尝试内置 request-proxy 参数,添加 Value 字段设置参数默认值,从控制台截图可以看到设置成功了,但 Burp 仍然没有收到 HTTP 数据包。

&cli.StringFlag{Name:        "request-proxy",Usage:       "all requests connect through defined proxy server.",Value:       "http://127.0.0.1:8080",Destination: &taskConfig.Proxy,},

在这里插入图片描述

查看 task_main.go 文件,搜索 Proxy,可以看到是在初始化浏览器时加载了代理。

	crawlerTask.Browser = engine2.InitBrowser(taskConf.ChromiumPath, taskConf.IncognitoContext, taskConf.ExtraHeaders, taskConf.Proxy, taskConf.NoHeadless)crawlerTask.RootDomain = targets[0].URL.RootDomain()
WireShark本地抓包(找到代码节点)

参考文章:wireShark抓取本地http包,分析状态。

前往 Npcap官网 下载安装包,安装时记得勾选 Adapter for lookback traffic capture 选项。打开 WireShark 选择该过滤器监听。

调试 CrawlerGo,观察执行哪一部分代码后 HTTP 请求中出现了目标网站的访问记录。

在这里插入图片描述

经过简单对比,发现执行 t.taskWG.Wait() 代码后发送HTTP请求。

// 追踪 task.Run()
// task_main.go 代码 173行
/*
开始当前任务
*/
func (t *CrawlerTask) Run() {defer t.Pool.Release()  // 释放协程池defer t.Browser.Close() // 关闭浏览器// 为 Targets 设置 reqsFromRobots 变量// 为 Targets 设置 reqsByFuzz 变量t.Result.AllReqList = t.Targets[:]// 创建 Request 对象用于 task 的初始化// 遍历 Targets 数组// 遍历初始化任务for _, req := range initTasks {if !engine2.IsIgnoredByKeywordMatch(*req, t.Config.IgnoreKeywords) {t.addTask2Pool(req)}}t.taskWG.Wait()// 对全部请求进行唯一去重todoFilterAll := make([]*model.Request, len(t.Result.AllReqList))for index := range t.Result.AllReqList {todoFilterAll[index] = t.Result.AllReqList[index]}

CrawlerGo设置多线程爬虫

爬虫调用代码
// crawlergo_cmd.go 代码 312 行
// 开始爬虫任务task, err := pkg.NewCrawlerTask(targets, taskConfig)if err != nil {logger.Logger.Error("create crawler task failed.")os.Exit(-1)}……go handleExit(task)logger.Logger.Info("Start crawling.")task.Run()result := task.Result
创建爬虫任务

分析代码,该部分主要操作了如下三部分内容:

  • 给爬虫任务装配数据
  • 以无头模式启动浏览器
  • 创建协程池并设置最大协程数量
	// 开始爬虫任务task, err := pkg.NewCrawlerTask(targets, taskConfig)// task_main.go 代码 70 行
/**新建爬虫任务接收配置好的 Request 、TaskConfig 对象
*/
func NewCrawlerTask(targets []*model.Request, taskConf TaskConfig) (*CrawlerTask, error) {// 定义爬虫任务的数据类型:字典crawlerTask := CrawlerTask{Result: &Result{},Config: &taskConf,smartFilter: filter2.SmartFilter{SimpleFilter: filter2.SimpleFilter{HostLimit: targets[0].URL.Host,},},}// 单个目标,测试也使用单目标站点// 定义 newReq 接收 targets[0],确定通信协议if len(targets) == 1 {_newReq := *targets[0]newReq := &_newReq_newURL := *_newReq.URLnewReq.URL = &_newURL// 首先确定站点使用的通信协议if targets[0].URL.Scheme == "http" {newReq.URL.Scheme = "https"} else {newReq.URL.Scheme = "http"}// 把配置好的 newReq 再添加到 targets,此时 Targets 有两个目标信息的元素targets = append(targets, newReq)}// 把 Targets 添加到 crawlerTask/爬虫任务的字典crawlerTask.Targets = targets[:]// 设置请求对象的 source 为常量 "Target"for _, req := range targets {req.Source = config.FromTarget}// 装配形式参数变量 Taskconfig// 设置 TaskConfig 即 taskConf 的变量,如果未设置则使用默认值设置 taskConf 的 变量.TabRunTimeout.MaxTabsCount.FilterMode.MaxCrawlCount.DomContentLoadedTimeout.EventTriggerInterval.BeforeExitDelay.EventTriggerMode.IgnoreKeywords// 设置请求头变量if taskConf.ExtraHeadersString != "" {err := json.Unmarshal([]byte(taskConf.ExtraHeadersString), &taskConf.ExtraHeaders)if err != nil {logger.Logger.Error("custom headers can't be Unmarshal.")return nil, err}}// 以无头模式开启 chrome.execrawlerTask.Browser = engine2.InitBrowser(taskConf.ChromiumPath, taskConf.IncognitoContext, taskConf.ExtraHeaders, taskConf.Proxy, taskConf.NoHeadless)// 设置爬虫任务的根域名crawlerTask.RootDomain = targets[0].URL.RootDomain()// 初始化智能过滤器crawlerTask.smartFilter.Init()// 创建协程池// 创建协程池,并且把协程池保存到爬虫任务的 Pool 键p, _ := ants.NewPool(taskConf.MaxTabsCount)crawlerTask.Pool = p// 爬虫任务完成数据装配、无头模式启动浏览器、创建协程池return &crawlerTask, nil
}
启动爬虫任务

代码把请求对象添加到线程池,然后等待异步线程执行结束。

task_main.go 代码 210 行的函数 t.addTask2Pool(req),继续追溯。

	// crawlergo_cmd.gotask.Run()/*
开始当前任务
*/
func (t *CrawlerTask) Run() {defer t.Pool.Release()  // 释放协程池defer t.Browser.Close() // 关闭浏览器是否从 robots.txt 获取信息,如果是则把信息添加到 t.Targets 对象是否 Fuzz 目录,如果是则读取字典文件并添加到 t.Targets 对象// 创建数组对象 AllReqListt.Result.AllReqList = t.Targets[:]// 创建 Request 类型的初始化任务变量var initTasks []*model.Requestfor _, req := range t.Targets {if t.smartFilter.DoFilter(req) {logger.Logger.Debugf("filter req: " + req.URL.RequestURI())continue}// 从 t.Targets 对象中获取请求对象initTasks = append(initTasks, req)t.Result.ReqList = append(t.Result.ReqList, req)}logger.Logger.Info("filter repeat, target count: ", len(initTasks))// 遍历初始化任务数组的请求对象,逐个添加到协程池for _, req := range initTasks {if !engine2.IsIgnoredByKeywordMatch(*req, t.Config.IgnoreKeywords) {t.addTask2Pool(req)}}// 运行则发送请求// 等待异步线程执行完成t.taskWG.Wait()// 对全部请求进行唯一去重todoFilterAll := make([]*model.Request, len(t.Result.AllReqList))for index := range t.Result.AllReqList {todoFilterAll[index] = t.Result.AllReqList[index]}t.Result.AllReqList = []*model.Request{}var simpleFilter filter2.SimpleFilterfor _, req := range todoFilterAll {if !simpleFilter.UniqueFilter(req) {t.Result.AllReqList = append(t.Result.AllReqList, req)}}// 全部域名t.Result.AllDomainList = AllDomainCollect(t.Result.AllReqList)// 子域名t.Result.SubDomainList = SubDomainCollect(t.Result.AllReqList, t.RootDomain)
}
把任务添加到线程池
// task_main.go,t.addTask2Pool()位于 210 行
/*开始当前任务*/
func (t *CrawlerTask) Run() {……// 遍历初始化任务数组的请求对象,逐个添加到协程池for _, req := range initTasks {if !engine2.IsIgnoredByKeywordMatch(*req, t.Config.IgnoreKeywords) {t.addTask2Pool(req)}}// 追溯 t.addTask2Pool(req),同文件代码 240 行
/**
添加任务到协程池
添加之前实时过滤
*/
func (t *CrawlerTask) addTask2Pool(req *model.Request) {// 协程相关代码t.taskCountLock.Lock()if t.crawledCount >= t.Config.MaxCrawlCount {t.taskCountLock.Unlock()return} else {t.crawledCount += 1}t.taskCountLock.Unlock()t.taskWG.Add(1)// 传入请求对象(字典类型),追溯 t.generateTabTask(req) ,发现根据请求列表生成tabTask协程任务列表/* 实际上又嵌套了一层字典,字典结构如下所示task := tabTask{crawlerTask: t,browser:     t.Browser,req:         req,}*/task := t.generateTabTask(req)// 测试时F7单步调试,发现跳过了该部分代码// 查询资料发现,该部分代码作用是:以并发的方式调用匿名函数funcgo func() {// 提交协程任务执行err := t.Pool.Submit(task.Task)if err != nil {t.taskWG.Done()logger.Logger.Error("addTask2Pool ", err)}}()
}// 追溯关键代码 t.Pool.submit(task.Task)在 t.Pool.submit(task.Task) 后添加打印提示信息的代码,发现打印了提示信息在该处下断点,进行单步调试发现使用F7或F8进行单步调试时,不会在该段代码的断点停留使用"运行到光标处",才会在该段代码的断点停留,然后就会执行 Run() 里面的代码:t.taskWG.Wait(),此时请求已经发送单步调试跟踪 t.taskWG.Wait(),发现的函数调用如下:task.Run()								crawlergo_cmd.go->t.taskWG.Wait()						task_main.go-> Wait()							src/sync/waitgroup.go->runtime_Semacquire(semap)			src/sync/waitgroup.go->sync_runtime_Semacquire()		src/runtime/sema.goF7步入执行该代码,此时会运行t.Pool.submit()并发送请求。也就是说,执行 t.taskWG.Wait()函数过程中,会执行 t.Pool.submit(),然后会执行协程任务。插个眼:注意func (t *tabTask) Task()
重要函数表格

创建爬虫任务

所属文件函数说明
crawlergo_cmd.gotask, err := pkg.NewCrawlerTask(targets, taskConfig)创建爬虫任务
task_main.goengine2.InitBrowser(xx)以无头模式启动浏览器 chrome.exe
-ants.NewPool(taskConf.MaxTabsCount)创建协程池

接着梳理协程任务的执行

关键代码:task_main.go 文件 代码 253 行。

go func() {// 每个 goroutine 调用 task.Task() 函数,目前不清楚为什么不是使用 task.Task(),可能是语法err := t.Pool.Submit(task.Task)fmt.Println(task.Task)fmt.Println("t.Pool.Submit")if err != nil {t.taskWG.Done()logger.Logger.Error("addTask2Pool ", err)}}()
启动 goroutine 执行操作

参考《Go in Action》第二章:一个 goroutine 是一个独立于其他函数运行的函数。使用关键字 go 启动一个 goroutine,并对这个 goroutine 做并发调度。代码使用关键字 go 启动了一个匿名函数作为 goroutine,这样可以并发地独立处理每个数据源的数据。

匿名函数内部包含了每个 goroutine 要完成的任务,此处是 t.Pool.Submit(task.Task)。

参考文章:ants:在Submit中再调用当前Pool的Submit可能导致阻塞,关于协程池 Pool。
goroutine pool减小开销的主要思路就是复用。即创建出的goroutine在做完一个task后不退出,而是等待下一个task,这样来减少goroutine反复创建和销毁带来的开销。

goroutine 的操作:task.Task

查看每个 goroutine 执行的操作,分析代码部分见注释内容。

// task_main.go 代码 264 行
/**
单个运行的tab标签任务,实现了workpool的接口
*/
func (t *tabTask) Task() {// 《Go in Action》第二章:// 关键字 defer 会安排随后的函数调用在函数返回时才执行。// taskWG.Done():任务完成后计数器减一defer t.crawlerTask.taskWG.Done()// 调用 engine2.NewTab()配置 tab,然后执行 tab// 这些参数不是与请求强相关的参数,可以忽略不看tab := engine2.NewTab(t.browser, *t.req, engine2.TabConfig{TabRunTimeout:           t.crawlerTask.Config.TabRunTimeout,DomContentLoadedTimeout: t.crawlerTask.Config.DomContentLoadedTimeout,EventTriggerMode:        t.crawlerTask.Config.EventTriggerMode,EventTriggerInterval:    t.crawlerTask.Config.EventTriggerInterval,BeforeExitDelay:         t.crawlerTask.Config.BeforeExitDelay,EncodeURLWithCharset:    t.crawlerTask.Config.EncodeURLWithCharset,IgnoreKeywords:          t.crawlerTask.Config.IgnoreKeywords,CustomFormValues:        t.crawlerTask.Config.CustomFormValues,CustomFormKeywordValues: t.crawlerTask.Config.CustomFormKeywordValues,})// 执行 tab,具体分析查看本章第四小节:发送请求的 tab 任务tab.Start()// 收集结果t.crawlerTask.Result.resultLock.Lock()t.crawlerTask.Result.AllReqList = append(t.crawlerTask.Result.AllReqList, tab.ResultList...)t.crawlerTask.Result.resultLock.Unlock()for _, req := range tab.ResultList {if t.crawlerTask.Config.FilterMode == config.SimpleFilterMode {if !t.crawlerTask.smartFilter.SimpleFilter.DoFilter(req) {t.crawlerTask.Result.resultLock.Lock()t.crawlerTask.Result.ReqList = append(t.crawlerTask.Result.ReqList, req)t.crawlerTask.Result.resultLock.Unlock()if !engine2.IsIgnoredByKeywordMatch(*req, t.crawlerTask.Config.IgnoreKeywords) {t.crawlerTask.addTask2Pool(req)}}} else {if !t.crawlerTask.smartFilter.DoFilter(req) {t.crawlerTask.Result.resultLock.Lock()t.crawlerTask.Result.ReqList = append(t.crawlerTask.Result.ReqList, req)t.crawlerTask.Result.resultLock.Unlock()if !engine2.IsIgnoredByKeywordMatch(*req, t.crawlerTask.Config.IgnoreKeywords) {t.crawlerTask.addTask2Pool(req)}}}}
}
配置引擎的库:/pkg/engine

查看 engine 库的目录结构,结合网上搜索结果,推测是作者自己编写的库。简单查看后,发现该库不重要,接下来看 tab 任务。

在这里插入图片描述

查看 task_main.go 文件与 engine 相关的调用代码。

import (engine2 "crawlergo/pkg/engine")type CrawlerTask struct {Browser       *engine2.Browser    //
}type tabTask struct {crawlerTask *CrawlerTaskbrowser     *engine2.Browser
}// 代码147行
crawlerTask.Browser = engine2.InitBrowser(taskConf.ChromiumPath, taskConf.IncognitoContext, taskConf.ExtraHeaders, taskConf.Proxy, taskConf.NoHeadless)
发送请求的 tab 任务
结构体:tab

定义的 tab 结构体如下,先不管执行到 tab.Start() 时装配了哪些变量。

// task_main.go 文件
type tabTask struct {crawlerTask *CrawlerTaskbrowser     *engine2.Browserreq         *model.Requestpool        *ants.Pool
}
执行操作函数:tab.Start()

函数位置:/pkg/engine/tab.go 代码 188 行。

处理返回结果的主要代码:

  • tab.collectLinkWG.Add(3)
  • go tab.collectLinks(),函数位于 colletc_links.go
  • tab.collectLinkWG.Wait()
func (tab *Tab) Start() {// 打印提示信息logger.Logger.Info("Crawling " + tab.NavigateReq.Method + " " + tab.NavigateReq.URL.String())// 执行之后调用 tab.Cancel()defer tab.Cancel()if err := chromedp.Run(*tab.Ctx,RunWithTimeOut(tab.Ctx, tab.config.DomContentLoadedTimeout, chromedp.Tasks{//runtime.Enable(),// 开启网络层APInetwork.Enable(),// 开启请求拦截APIfetch.Enable().WithHandleAuthRequests(true),// 添加回调函数绑定// XSS-Scan 使用的回调runtime.AddBinding("addLink"),runtime.AddBinding("Test"),// 初始化执行JSchromedp.ActionFunc(func(ctx context.Context) error {var err error_, err = page.AddScriptToEvaluateOnNewDocument(js.TabInitJS).Do(ctx)if err != nil {return err}return nil}),network.SetExtraHTTPHeaders(tab.ExtraHeaders),// 执行导航//chromedp.Navigate(tab.NavigateReq.URL.String()),chromedp.ActionFunc(func(ctx context.Context) error {_, _, _, err := page.Navigate(tab.NavigateReq.URL.String()).Do(ctx)if err != nil {return err}return waitNavigateDone(ctx)}),}),); err != nil {if err.Error() == "context canceled" {return}logger.Logger.Warn("navigate timeout ", tab.NavigateReq.URL.String())}go func() {// 等待所有协程任务结束tab.WG.Wait()tab.NavDone <- 1}()select {case <-tab.NavDone:logger.Logger.Debug("all navigation tasks done.")case <-time.After(tab.config.DomContentLoadedTimeout + time.Second*10):logger.Logger.Warn("navigation tasks TIMEOUT.")}// 等待收集所有链接logger.Logger.Debug("collectLinks start.")tab.collectLinkWG.Add(3)go tab.collectLinks()tab.collectLinkWG.Wait()logger.Logger.Debug("collectLinks end.")// 识别页面编码 并编码所有URLif tab.config.EncodeURLWithCharset {tab.DetectCharset()tab.EncodeAllURLWithCharset()}//fmt.Println(tab.NavigateReq.URL.String(), len(tab.ResultList))//for _, v := range tab.ResultList {//	v.SimplePrint()//}// fmt.Println("Finished " + tab.NavigateReq.Method + " " + tab.NavigateReq.URL.String())
}
处理响应信息:collect_links.go(分析方向的转折点)

查看相关的调用函数:tab.GetExecutor(),追溯到 tab.go 文件 327 行,看不太懂。

下列代码看了半天,没发现类似 http.Get() 的函数 调用。全文搜索 .Get(.Post(,发现只有获取 robots.txt 和 Fuzz字典时会用到这些请求。

简单看了一些对响应信息的处理,突然意识到以无头模式启动 Google 并且进行复杂的处理方式,是针对爬虫而言的,参数FuzzPoc验证 没必要使用无头模式,唯一需要的是借鉴项目的并发代码 。

/**
最后收集所有的链接
*/
func (tab *Tab) collectLinks() {go tab.collectHrefLinks()go tab.collectObjectLinks()go tab.collectCommentLinks()
}func (tab *Tab) collectHrefLinks() {defer tab.collectLinkWG.Done()ctx := tab.GetExecutor()fmt.Println("打印信息:")// 打印结果:context.Background.WithCancel.WithValue(type chromedp.contextKey, val <not Stringer>).WithCancel.WithValue(type chromedp.contextKey, val <not Stringer>).WithCancel.WithValue(type chromedp.contextKey, val <not Stringer>).WithDeadline(2022-04-13 19:16:13.5023884 +0800 CST m=+20.528509501 [16.1644673s]).WithValue(type cdp.contextKey, val <not Stringer>)//fmt.Println(ctx)// 收集 src href data-url 属性值attrNameList := []string{"src", "href", "data-url", "data-href"}for _, attrName := range attrNameList {tCtx, cancel := context.WithTimeout(ctx, time.Second*1)var attrs []map[string]string_ = chromedp.AttributesAll(fmt.Sprintf(`[%s]`, attrName), &attrs, chromedp.ByQueryAll).Do(tCtx)/* 打印结果[map[src:../images/Less-1.jpg]][][][]*/fmt.Println(attrs)cancel()for _, attrMap := range attrs {tab.AddResultUrl(config.GET, attrMap[attrName], config.FromDOM)}}
}

小结

简单看了一些对响应信息的处理,突然意识到以无头模式启动 Google 并且进行复杂的处理方式,是针对爬虫而言的,参数FuzzPoc验证 没必要使用无头模式,唯一需要的是借鉴项目的并发代码 。

接下来另开一篇文章,搞清除并发代码,然后应用到 参数Fuzz 模块即可。

这篇关于【毕设扫描器】【参数Fuzz】第二篇:动态爬虫的创建、启动和协程池的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/621799

相关文章

mybatis-plus 实现查询表名动态修改的示例代码

《mybatis-plus实现查询表名动态修改的示例代码》通过MyBatis-Plus实现表名的动态替换,根据配置或入参选择不同的表,本文主要介绍了mybatis-plus实现查询表名动态修改的示... 目录实现数据库初始化依赖包配置读取类设置 myBATis-plus 插件测试通过 mybatis-plu

C#原型模式之如何通过克隆对象来优化创建过程

《C#原型模式之如何通过克隆对象来优化创建过程》原型模式是一种创建型设计模式,通过克隆现有对象来创建新对象,避免重复的创建成本和复杂的初始化过程,它适用于对象创建过程复杂、需要大量相似对象或避免重复初... 目录什么是原型模式?原型模式的工作原理C#中如何实现原型模式?1. 定义原型接口2. 实现原型接口3

SpringBoot接收JSON类型的参数方式

《SpringBoot接收JSON类型的参数方式》:本文主要介绍SpringBoot接收JSON类型的参数方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、jsON二、代码准备三、Apifox操作总结一、JSON在学习前端技术时,我们有讲到过JSON,而在

SpringBoot项目启动错误:找不到或无法加载主类的几种解决方法

《SpringBoot项目启动错误:找不到或无法加载主类的几种解决方法》本文主要介绍了SpringBoot项目启动错误:找不到或无法加载主类的几种解决方法,具有一定的参考价值,感兴趣的可以了解一下... 目录方法1:更改IDE配置方法2:在Eclipse中清理项目方法3:使用Maven命令行在开发Sprin

JAVA虚拟机中 -D, -X, -XX ,-server参数使用

《JAVA虚拟机中-D,-X,-XX,-server参数使用》本文主要介绍了JAVA虚拟机中-D,-X,-XX,-server参数使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有... 目录一、-D参数二、-X参数三、-XX参数总结:在Java开发过程中,对Java虚拟机(JVM)的启动参数进

基于Canvas的Html5多时区动态时钟实战代码

《基于Canvas的Html5多时区动态时钟实战代码》:本文主要介绍了如何使用Canvas在HTML5上实现一个多时区动态时钟的web展示,通过Canvas的API,可以绘制出6个不同城市的时钟,并且这些时钟可以动态转动,每个时钟上都会标注出对应的24小时制时间,详细内容请阅读本文,希望能对你有所帮助...

Vue中动态权限到按钮的完整实现方案详解

《Vue中动态权限到按钮的完整实现方案详解》这篇文章主要为大家详细介绍了Vue如何在现有方案的基础上加入对路由的增、删、改、查权限控制,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、数据库设计扩展1.1 修改路由表(routes)1.2 修改角色与路由权限表(role_routes)二、后端接口设计

解读docker运行时-itd参数是什么意思

《解读docker运行时-itd参数是什么意思》在Docker中,-itd参数组合用于在后台运行一个交互式容器,同时保持标准输入和分配伪终端,这种方式适合需要在后台运行容器并保持交互能力的场景... 目录docker运行时-itd参数是什么意思1. -i(或 --interactive)2. -t(或 --

Python中conda虚拟环境创建及使用小结

《Python中conda虚拟环境创建及使用小结》本文主要介绍了Python中conda虚拟环境创建及使用小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们... 目录0.前言1.Miniconda安装2.conda本地基本操作3.创建conda虚拟环境4.激活c

使用Python创建一个能够筛选文件的PDF合并工具

《使用Python创建一个能够筛选文件的PDF合并工具》这篇文章主要为大家详细介绍了如何使用Python创建一个能够筛选文件的PDF合并工具,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录背景主要功能全部代码代码解析1. 初始化 wx.Frame 窗口2. 创建工具栏3. 创建布局和界面控件4