SpringBoot實(shí)現(xiàn)輕量級(jí)動(dòng)態(tài)定時(shí)任務(wù)管控及組件化的操作步驟
關(guān)于動(dòng)態(tài)定時(shí)任務(wù)
關(guān)于在SpringBoot中使用定時(shí)任務(wù),大部分都是直接使用SpringBoot的@Scheduled注解,如下:
@Component public class TestTask { @Scheduled(cron="0/5 * * * * ? ") //每5秒執(zhí)行一次 public void execute(){ SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); log.info("任務(wù)執(zhí)行" + df.format(new Date())); } }
或者或者使用第三方的工具,例如XXL-Job等。就XXL-Job而言,如果說(shuō)是大型項(xiàng)目,硬件資源和項(xiàng)目環(huán)境都具備,XXL-Job確實(shí)是最佳的選擇,可是對(duì)于項(xiàng)目體量不大,又不想過(guò)多的引入插件;使用XXL-Job多少有點(diǎn)“殺雞用牛刀”的意思;
之所以這樣說(shuō),是因?yàn)樵赟pringBoot中集成使用XXL-Job的步驟如下:
- 引入依賴,配置執(zhí)行器Bean
- 在自己項(xiàng)目中編寫(xiě)定時(shí)任務(wù)代碼
- 部署XXL-Job
- 登錄XXL-Job調(diào)度中心的 Web 控制臺(tái),創(chuàng)建一個(gè)新的任務(wù),選擇剛才配置的執(zhí)行器和任務(wù)處理器,設(shè)置好觸發(fā)條件(如 Cron 表達(dá)式)和其他選項(xiàng)后保存
- 生產(chǎn)環(huán)境下,還要把配置好任務(wù)的XXL-Job和項(xiàng)目一起打包
@Component public class SampleXxlJob { @XxlJob("sampleJobHandler") public void sampleJobHandler() throws Exception { // 業(yè)務(wù)邏輯 System.out.println("Hello XXL-JOB!"); } }
這一套步驟在中小型項(xiàng)目中,明顯成本大于效果,而使用XXL-Job無(wú)非就是想動(dòng)態(tài)的去管理定時(shí)任務(wù),可以在運(yùn)行狀態(tài)下隨意的執(zhí)行、中斷、調(diào)整執(zhí)行周期、查看運(yùn)行結(jié)果,而不是像基于@Scheduled注解實(shí)現(xiàn)后,無(wú)法改變。
所以,這里就基于SpringBoot實(shí)現(xiàn)動(dòng)態(tài)調(diào)度定時(shí)任務(wù),之前針對(duì)這個(gè)問(wèn)題,我寫(xiě)過(guò)一篇CSDN文章(連接放在下方),最近博主將相關(guān)的代碼進(jìn)行了匯總,并且在易用性和擴(kuò)展性上進(jìn)行了加強(qiáng),基于COLA架構(gòu)理論,封裝到了組件層
這次加強(qiáng)主要包括:
- 剝離了任務(wù)的持久化,使其依賴更簡(jiǎn)潔,真正以starter的形式開(kāi)箱即用
- 擴(kuò)展了方法級(jí)的定時(shí)任務(wù)注冊(cè),能夠像xxl-job一樣,一個(gè)注解搞定動(dòng)態(tài)定時(shí)任務(wù)的注冊(cè)及管理
2|0動(dòng)態(tài)定時(shí)任務(wù)實(shí)現(xiàn)思路
關(guān)于動(dòng)態(tài)定時(shí)任務(wù)的核心思路是反射+定時(shí)任務(wù)模板,這兩部分的核心框架不變,相關(guān)內(nèi)容可以回顧我之前的博客:
輕量級(jí)動(dòng)態(tài)定時(shí)任務(wù)調(diào)度
這里主要是針對(duì)強(qiáng)化的第二點(diǎn)進(jìn)行思路解釋,第二點(diǎn)的強(qiáng)化是加入了類掃描機(jī)制,通過(guò)掃描,實(shí)現(xiàn)了自動(dòng)注冊(cè),規(guī)避了之前每新增一個(gè)定時(shí)任務(wù)都必須得預(yù)制SQL的步驟:
類級(jí)別定時(shí)任務(wù)實(shí)現(xiàn)思路:在原模板模式的基礎(chǔ)下,基于AbstractBaseCronTask類自定義的定時(shí)任務(wù)子類作為類級(jí)別定時(shí)任務(wù),即一個(gè)類為一個(gè)定時(shí)任務(wù),初始時(shí)由包掃描所有的子類,并使用反射將其實(shí)例化,逐一加入到進(jìn)程管理中,并激活定時(shí)調(diào)度。
基于@MethodJob的方法級(jí)別任務(wù)實(shí)現(xiàn)思路:以 AbstractBaseCronTask類為基礎(chǔ),定義一個(gè)固定的子類BaseMethodLevelTask,并在其內(nèi)部限定任務(wù)的執(zhí)行方式,掃描所有標(biāo)注了@MethodJob的方法及其所屬的Bean,連同Bean及方法的反射類作為構(gòu)造函數(shù),生成BaseMethodLevelTask對(duì)象,因?yàn)锽aseMethodLevelTask也是AbstractBaseCronTask的子類,則可以以類級(jí)別定時(shí)任務(wù)的方式,將其生成定時(shí)任務(wù),并進(jìn)行管理。
本質(zhì)還是管理的AbstractBaseCronTask子類在線程池中的具體對(duì)象,不同的地方是類級(jí)別定時(shí)任務(wù)是一個(gè)具體的任務(wù)類僅生成一個(gè)對(duì)象,class路徑即是唯一的標(biāo)識(shí),而方法級(jí)別的定時(shí)任務(wù)均基于BaseMethodLevelTask生成無(wú)數(shù)個(gè)對(duì)象,具體標(biāo)識(shí)則是構(gòu)造函數(shù)傳入的Bean的反射對(duì)象和方法名。
對(duì)此部分感興趣的可以一起參與開(kāi)發(fā),該項(xiàng)目地址:Gitee源碼,主要為其中task-component模塊。
3|0組件使用方法
根據(jù)Git地址,將源碼down下,編譯安裝到本地私倉(cāng)中,以Maven的形式引入即可,該組件遵循Spring-starter規(guī)范,開(kāi)箱即用。
<dependency> <groupId>com.gcc.container.components</groupId> <artifactId>task-component</artifactId> <version>1.0.0</version> </dependency>
3|1Yaml配置說(shuō)明
給出一個(gè)yaml模板:
gcc-task: task-class-package : * data-file-path: /opt/myproject/task_info.json
對(duì)于task-component的配置只有兩個(gè),task-class-package和data-file-path
屬性 | 說(shuō)明 | 是否必須 | 缺省值 |
task-class-package | 指定定時(shí)任務(wù)存放的包路徑,不填寫(xiě)或無(wú)此屬性則默認(rèn)全項(xiàng)目掃描,填寫(xiě)后可有效減少初始化時(shí)間 | 否 | * |
data-file-path | 定時(shí)任務(wù)管理的數(shù)據(jù)存放文件及其路徑,默認(rèn)不填寫(xiě)則存放項(xiàng)目運(yùn)行目錄下,如自行擴(kuò)展實(shí)現(xiàn),則該配置自動(dòng)失效 | 否 | class:db/task_info.json |
此處的兩個(gè)配置項(xiàng)都包含默認(rèn)值,所以不進(jìn)行yml的gcc-task
仍舊可以使用該組件
3|2新建定時(shí)任務(wù)
對(duì)于該組件在進(jìn)行定時(shí)任務(wù)的創(chuàng)建時(shí),有兩種,分別是類級(jí)定時(shí)任務(wù)和方法級(jí)定時(shí)任務(wù),兩者的區(qū)別在于是以類為基本的單位還是以方法為一個(gè)定時(shí)任務(wù)的單位,對(duì)于運(yùn)行效果則并無(wú)區(qū)別,根據(jù)個(gè)人喜好,選擇使用哪一種即可
類級(jí)定時(shí)任務(wù)
新增一個(gè)定時(shí)任務(wù)邏輯,則需要實(shí)現(xiàn)基類AbstractBaseCronTask
,并加入注解 @ClassJob
ClassJob的參數(shù)如下:
參數(shù) | 說(shuō)明 | 樣例 |
---|---|---|
cron | 定時(shí)任務(wù)默認(rèn)的執(zhí)行周期,僅在首次初始化該任務(wù)使用(必填) | 10 0/2 * * * ? |
desc | 任務(wù)描述,非必填 | 這是測(cè)試任務(wù) |
bootup | 是否開(kāi)機(jī)自啟動(dòng),缺省值為 false | false |
cron
屬性僅在第一次新增該任務(wù)時(shí)提供一個(gè)默認(rèn)的執(zhí)行周期,必須填寫(xiě),后續(xù)任務(wù)加載后,定時(shí)任務(wù)相關(guān)數(shù)據(jù)會(huì)被存放在文件或數(shù)據(jù)庫(kù)中,此時(shí)則以文件或數(shù)據(jù)庫(kù)中該任務(wù)的cron為主,代碼中的注解則不會(huì)生效,如果想重置,則刪除已經(jīng)持久化的任務(wù)即可。
一個(gè)完整的Demo如下:
@TaskJob(cron = "10 0/2 * * * ?" ,desc = "這是一個(gè)測(cè)試任務(wù)",bootup = true) public class TaskMysqlOne extends AbstractBaseCronTask { public TaskMysqlOne(TaskEntity taskEntity) { super(taskEntity); } @Override public void beforeJob() { } @Override public void startJob() { } @Override public void afterJob() { } }
繼承AbstractBaseCronTask
必須要實(shí)現(xiàn)攜帶TaskEntity參數(shù)的構(gòu)造函數(shù)
、beforeJob()
、startJob()
、afterJob()
三個(gè)方法即可。原則上這三個(gè)方法是規(guī)范定時(shí)任務(wù)的執(zhí)行,實(shí)際使用,只需要把核心邏輯放在三個(gè)方法中任何一個(gè)即可。
因定時(shí)任務(wù)類是非SpringBean管理的類,所以在自定義的定時(shí)任務(wù)類內(nèi)無(wú)法使用任何Spring相關(guān)的注解(如@Autowired),但是卻可以通過(guò)自帶的getServer(Class<T> className)
方法來(lái)獲取任何Spring上下文中的Bean
例如,你有一個(gè)UserService的接口及其Impl的實(shí)現(xiàn)類,想在定時(shí)任務(wù)類中使用該Bean,則可以:
@TaskJob(cron = "10 0/2 * * * ?" ,desc = "這是一個(gè)測(cè)試任務(wù)",bootup = true) public class TaskMysqlOne extends AbstractBaseCronTask { public TaskMysqlOne(TaskEntity taskEntity) { super(taskEntity); } @Override public void beforeJob() { } @Override public void startJob() { List<String> names = getServer(UserService.class).searchAllUserName(); //后續(xù)邏輯…… //其他邏輯 } @Override public void afterJob() { } }
方法級(jí)定時(shí)任務(wù)
如果不想新建類,或者不想受限于AbstractBaseCronTask的束縛,則可以像xxl-job定義定時(shí)任務(wù)一樣,直接在某個(gè)方法上標(biāo)注@MethodJob注解即可。
@MethodJob的參數(shù)如下:
參數(shù) | 說(shuō)明 | 樣例 |
---|---|---|
cron | 定時(shí)任務(wù)默認(rèn)的執(zhí)行周期,僅在首次初始化該任務(wù)使用(必填) | 10 0/2 * * * ? |
desc | 任務(wù)描述,非必填 | 這是測(cè)試任務(wù) |
bootup | 是否開(kāi)機(jī)自啟動(dòng),缺省值為 false | false |
通ClassJob一樣,cron
屬性僅在第一次新增該任務(wù)時(shí)提供一個(gè)默認(rèn)的執(zhí)行周期,必須填寫(xiě),后續(xù)任務(wù)加載后,定時(shí)任務(wù)相關(guān)數(shù)據(jù)會(huì)被存放在文件或數(shù)據(jù)庫(kù)中,此時(shí)則以文件或數(shù)據(jù)庫(kù)中該任務(wù)的cron為主,代碼中的注解則不會(huì)生效,如果想重置,則刪除已經(jīng)持久化的任務(wù)即可。
下面是一個(gè)例子:
//正常定義的Service接口 public interface AsyncTestService { void taskJob(); } //Service接口實(shí)現(xiàn)類 @Service public class AsyncTestServiceImpl implements AsyncTestService { @MethodJob(cron = "11 0/1 * * * ?",desc = "這是個(gè)方法級(jí)任務(wù)") @Override public void taskJob() { log.info("方法級(jí)別任務(wù)查詢關(guān)鍵字為企業(yè)的數(shù)據(jù)"); QueryWrapper<ArticleEntity> query = new QueryWrapper<>(); query.like("art_name","企業(yè)"); List<ArticleEntity> data = articleMapper.selectList(query); log.info("查出條數(shù)為{}",data.size()); } }
注:該注解僅支持SpringBoot中標(biāo)注為@Component
、@Service
、@Repository
的Bean
3|3動(dòng)態(tài)調(diào)度任務(wù)接口說(shuō)明
定時(shí)任務(wù)相關(guān)的管理操作均封裝在TaskScheduleManagerService
接口中,接口內(nèi)容如下:
public interface TaskScheduleManagerService { /** * 查詢?cè)谟萌蝿?wù) * @return */ List<TaskVo> searchTask(SearchTaskDto dto); /** * 查詢?nèi)蝿?wù)詳情 * @param taskId 任務(wù)id * @return TaskEntity */ TaskVo searchTaskDetail(String taskId); /** * 運(yùn)行指定任務(wù) * @param taskId 任務(wù)id * @return TaskRunRetDto */ TaskRunRetVo runTask(String taskId); /** * 關(guān)停任務(wù) * @param taskId 任務(wù)id * @return TaskRunRetDto */ TaskRunRetVo shutdownTask(String taskId); /** * 開(kāi)啟任務(wù) * @param taskId 任務(wù)id * @return TaskRunRetDto */ TaskRunRetVo openTask(String taskId); /** * 更新任務(wù)信息 * @param entity 實(shí)體 * @return TaskRunRetDto */ TaskRunRetVo updateTaskBusinessInfo(TaskEntity entity); }
可直接在外部項(xiàng)目中注入使用即可:
@RestController @RequestMapping("/myself/manage") public class TaskSchedulingController { //引入依賴后可直接使用注入 @Autowired private TaskScheduleManagerService taskScheduleManagerService; @GetMapping("/detail") @Operation(summary = "具體任務(wù)對(duì)象") public Response searchDetail(String taskId){ return Response.success(taskScheduleManagerService.searchTaskDetail(taskId)); } @GetMapping("/shutdown") @Operation(summary = "關(guān)閉指定任務(wù)") public Response shutdownTask(String taskId){ return Response.success(taskScheduleManagerService.shutdownTask(taskId)); } @GetMapping("/open") @Operation(summary = "開(kāi)啟指定任務(wù)") public Response openTask(String taskId){ return Response.success(taskScheduleManagerService.openTask(taskId)); } }
接口效果:
可使用該接口,進(jìn)行UI頁(yè)面開(kāi)發(fā)。
3|4關(guān)于任務(wù)持久化的擴(kuò)展
在實(shí)現(xiàn)思路中提到過(guò),task-component的執(zhí)行原理需要將注冊(cè)后的任務(wù)持久化,下次再啟動(dòng)項(xiàng)目時(shí),則直接使用持久化的TaskEntity來(lái)加載定時(shí)任務(wù)。
考慮到定時(shí)任務(wù)的數(shù)量不大,且對(duì)于交互要求不高,另外考慮封裝成組件的獨(dú)立性和普適性,不想額外引入數(shù)據(jù)庫(kù)依賴和ORM框架,所以組件默認(rèn)的是以JSON文件的形式進(jìn)行存儲(chǔ),實(shí)際使用中,考慮到便利性,可以自行對(duì)持久化部分進(jìn)行擴(kuò)展。
所謂持久化,其實(shí)本制是持久化TaskEntity對(duì)象,TaskEntity對(duì)象如下:
@Data public class TaskEntity implements Serializable { /** * 任務(wù)ID(唯一) */ private String taskId; /** * 任務(wù)名稱 */ private String taskName; /** * 任務(wù)描述 */ private String taskDesc; /** * 遵循cron 表達(dá)式 */ private String taskCron; /** * 類路徑 */ private String taskClass; /** * 任務(wù)級(jí)別 CLASS_LEVEL 類級(jí)別,METHOD_LEVEL 方法級(jí)別 */ private TaskLevel taskLevel; /** * 任務(wù)注冊(cè)時(shí)間 */ private String taskCreateTime; /** * 是否啟用,1啟用,0不啟用 */ private Integer taskIsUse; /** * 是否系統(tǒng)啟動(dòng)后立刻運(yùn)行 1是。0否 */ private Integer taskBootUp; /** * 上次運(yùn)行狀態(tài) 1:成功,0:失敗 */ private Integer taskLastRun; /** * 任務(wù)是否在內(nèi)存中 1:是,0:否 */ private Integer taskRamStatus; /** * 外部配置 (擴(kuò)展待使用字段) */ private String taskOutConfig; /** * 加載配置 */ private String loadConfigure; }
擴(kuò)展的主要操作為兩步:
- 新建存放taskEntity的表
- 實(shí)現(xiàn)TaskRepository接口
首先新建數(shù)據(jù)庫(kù)表,這里以Mysql為例給出建表語(yǔ)句:
DROP TABLE IF EXISTS `tb_task_info`; CREATE TABLE `tb_task_info` ( `task_id` varchar(100) NOT NULL PRIMARY KEY , `task_name` varchar(255) DEFAULT NULL, `task_desc` text, `task_cron` varchar(20) DEFAULT NULL, `task_class` varchar(100) DEFAULT NULL COMMENT '定時(shí)任務(wù)類路徑', `task_level` varchar(50) DEFAULT NULL COMMENT '任務(wù)級(jí)別:類級(jí)別(CLASS_LEVEL)、方法級(jí)別(METHOD_LEVEL)', `task_is_use` tinyint DEFAULT NULL COMMENT '是否啟用該任務(wù),1:?jiǎn)⒂茫?禁用', `task_boot_up` tinyint DEFAULT NULL COMMENT '是否為開(kāi)機(jī)即運(yùn)行,1:初始化即運(yùn)行,0,初始化不運(yùn)行', `task_out_config` text CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci COMMENT '定時(shí)任務(wù)額外配置項(xiàng),采用json結(jié)構(gòu)存放', `task_create_time` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL COMMENT '定時(shí)任務(wù)追加時(shí)間', `task_last_run` tinyint DEFAULT NULL COMMENT '任務(wù)上次執(zhí)行狀態(tài);1正常,0執(zhí)行失敗,null未知', `task_ram_status` tinyint DEFAULT NULL COMMENT '任務(wù)當(dāng)前狀態(tài);1內(nèi)存運(yùn)行中,0內(nèi)存移除', `loadConfigure` text COMMENT '加載相關(guān)配置', PRIMARY KEY (`task_id`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 ROW_FORMAT=DYNAMIC;
實(shí)現(xiàn)TaskEntityRepository 接口,接口內(nèi)容為:
public interface TaskEntityRepository { /** * 新增數(shù)據(jù) * @param entity * @return int */ int save(TaskEntity entity); /** * 刪除任務(wù),根據(jù)TaskId * @param id id * @return int */ int removeByTaskId(String id); /** * 更新任務(wù)(除taskId外,所有字段必須更新) * @param entity 實(shí)體 * @return int */ int update(TaskEntity entity); /** * 查詢?nèi)咳蝿?wù) * @return List<TaskEntity> */ List<TaskEntity> queryAllTask(); }
只需要新建類,然后實(shí)現(xiàn)該接口即可,如下是基于Mybatis-Plus進(jìn)行的實(shí)現(xiàn)樣例:
@Mapper public interface TaskDataMapper extends BaseMapper<TaskEntity> { } @Repository //此處一定要用@Primary注解進(jìn)行固定 @Primary public class TaskEntityReponsitoryImpl implements TaskEntityRepository { @Autowired private TaskDataMapper taskDataMapper; @Override public int save(TaskEntity entity) { entity.setTaskCreateTime(DateUtil.now()); return taskDataMapper.insert(entity); } @Override public int removeByTaskId(String id) { QueryWrapper<TaskEntity> query = new QueryWrapper<>(); query.eq("task_id",id); return taskDataMapper.delete(query); } @Override public int update(TaskEntity entity) { UpdateWrapper<TaskEntity> update = new UpdateWrapper<>(); update.eq("task_id",entity.getTaskId()); return taskDataMapper.update(entity,update); } @Override public List<TaskEntity> queryAllTask() { return taskDataMapper.selectList(new QueryWrapper<>()); } }
至此,項(xiàng)目中則可以以數(shù)據(jù)庫(kù)表的形式來(lái)管理定時(shí)任務(wù):
任務(wù)運(yùn)行日志:
[main] com.web.test.Application : Started Application in 3.567 seconds (JVM running for 4.561) [main] .g.c.c.t.c.InitTaskSchedulingApplication : 【定時(shí)任務(wù)初始化】 容器初始化 [main] o.s.s.c.ThreadPoolTaskScheduler : Initializing ExecutorService [main] .g.c.c.t.c.InitTaskSchedulingApplication : 【定時(shí)任務(wù)初始化】定時(shí)任務(wù)初始化任務(wù)開(kāi)始 [main] c.g.c.c.task.compont.TaskScheduleImpl : 【定時(shí)任務(wù)初始化】裝填任務(wù):TaskTwoPerson [ 任務(wù)執(zhí)行周期:15 0/2 * * * ? ] [ bootup:0] [main] c.g.c.c.task.compont.TaskScheduleImpl : 【定時(shí)任務(wù)初始化】裝填任務(wù):TaskMysqlOne [ 任務(wù)執(zhí)行周期:10 0/2 * * * ? ] [ bootup:1] [main] c.g.c.c.task.compont.TaskScheduleImpl : 【定時(shí)任務(wù)初始化】裝填任務(wù):taskJob [ 任務(wù)執(zhí)行周期:11 0/1 * * * ? ] [ bootup:0] [main] .g.c.c.t.c.InitTaskSchedulingApplication : 【定時(shí)任務(wù)初始化】定時(shí)任務(wù)初始化任務(wù)完成 [main] c.g.c.c.task.service.AfterAppStarted : 【定時(shí)任務(wù)自運(yùn)行】運(yùn)行開(kāi)機(jī)自啟動(dòng)任務(wù) [main] TaskMysqlOne : ---------------------任務(wù) TaskMysqlOne 開(kāi)始執(zhí)行----------------------- [main] TaskMysqlOne : 任務(wù)描述:這是一個(gè)測(cè)試任務(wù) [main] TaskMysqlOne : 我是張三 [main] TaskMysqlOne : 任務(wù)耗時(shí):約 0.0 s [main] TaskMysqlOne : ---------------------任務(wù) TaskMysqlOne 結(jié)束執(zhí)行----------------------- [task-thread-1] taskJob : ---------------------任務(wù) taskJob 開(kāi)始執(zhí)行----------------------- [task-thread-1] taskJob : 任務(wù)描述:這是個(gè)方法級(jí)任務(wù) [task-thread-1] c.w.t.service.impl.AsyncTestServiceImpl : 方法級(jí)別任務(wù)查詢關(guān)鍵字為企業(yè)的數(shù)據(jù) [task-thread-1] c.w.t.service.impl.AsyncTestServiceImpl : 查出條數(shù)為1 [task-thread-1] taskJob : 任務(wù)耗時(shí):約 8.45 s [task-thread-1] taskJob : ---------------------任務(wù) taskJob 結(jié)束執(zhí)行-------------------
到此這篇關(guān)于SpringBoot實(shí)現(xiàn)輕量級(jí)動(dòng)態(tài)定時(shí)任務(wù)管控及組件化的文章就介紹到這了,更多相關(guān)SpringBoot定時(shí)任務(wù)管控內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java數(shù)據(jù)結(jié)構(gòu)之棧的詳解
這篇文章主要為大家詳細(xì)介紹了Java數(shù)據(jù)結(jié)構(gòu)的棧的應(yīng)用,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下,希望能給你帶來(lái)幫助2021-08-08Java進(jìn)階之高并發(fā)核心Selector詳解
前幾篇文章介紹了Java高并發(fā)的一些基礎(chǔ)內(nèi)容,認(rèn)識(shí)了Channel,Buffer和Selector的基本用法,有了感性認(rèn)識(shí)之后,來(lái)看看Selector的底層是如何實(shí)現(xiàn)的。,需要的朋友可以參考下2021-05-05springboot啟動(dòng)時(shí)運(yùn)行代碼詳解
在本篇內(nèi)容中我們給大家整理了關(guān)于在springboot啟動(dòng)時(shí)運(yùn)行代碼的詳細(xì)圖文步驟以及需要注意的地方講解,有興趣的朋友們學(xué)習(xí)下。2019-06-06Maven和MyBatis框架簡(jiǎn)單實(shí)現(xiàn)數(shù)據(jù)庫(kù)交互的示例
本文主要介紹了Maven和MyBatis框架簡(jiǎn)單實(shí)現(xiàn)數(shù)據(jù)庫(kù)交互的示例,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-01-01Spring 實(shí)現(xiàn)給Bean屬性注入null值
這篇文章主要介紹了Spring 實(shí)現(xiàn)給Bean屬性注入null值的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08