基于quartz实现定时任务管理系统

news2024/11/15 11:45:22

基于quartz实现定时任务管理系统

背景

说起定时任务框架,首先想到的是Quartz。这是定时任务的老牌框架了,它的优缺点都很明显。借助PowerJob 的readme文档的内容简单带过一下这部分。

在这里插入图片描述

除了上面提到,还有elastic-job-litequartzui也是相当火热的,可以自行了解一下。

那么都这么多后起之秀,个个吊打quartz,为什么我还选择用quartz来做定时任务?技术是服务于业务的,肯定是有需求呗。公司有个统一管理平台,现在需要开发一个定时任务管理平台,可以动态去管理配置定时任务、查看运行日志。

为什么不考虑其它的框架?由于需求要定制化ui界面和定时任务执行结果接入统一的通知中心等等需求,上面大多数框架都是通过简单配置开箱即用,定制化需要对源码有一定熟悉程度,而quartz我在大学就用了很多次了,非常熟悉,改造相对容易。

需求分析

  1. 定时任务的增删改查
  2. 定时任务执行日志查看

详细设计

开发环境

jdk 1.8

spring boot 2.7.7

quartz 单机版

Mysql 8.0

1. 定时任务的实现

quartz的任务实际是通过内置Java类实现job接口或者继承QuartzJobBean实现executeInternal来实现定时任务的添加。所以我们在管理页面上所做的增删改查操作都不可能是真正意义上地去修改了任务类,而是修改了映射类。举个例子来说,比如Power Job的管理界面

在这里插入图片描述

我们修改的这些任务名称、定时信息的持久化操作都不会是操作了真正的任务类,而是修改了一个绑定这个任务类的映射类。如下图中的类就是任务类

在这里插入图片描述

而图片中的这些就是属性就是映射类的属性。

你可以先是觉得任务类和映射类之间是一对一的关系,映射类记录了定时任务的执行频率(cron)、名称、任务类完整类名等其它的信息,然后在执行过程中通过完整类名反射获得任务类,任务类再根据这些信息去执行。

但如果每个定时任务,我们都要去实现Job接口创建一个类,相同的那些代码比如获取trigger、scheduler等等,都要出现在每个类中,每次添加一个定时任务都要多一个专门的定时任务类。每次开发时都要关注业务和定时任务类之间的关系,多了之后是有点烦。

我推荐的做法是,创建一个具备http请求功能的任务类,将业务操作开发成一个接口,在配置映射类时,将http链接配置上去,这样一到时间 ,就会请求到对应的业务接口。这样使得后续的定时任务功能开发更专注于业务开发,方便使用。尤其是团队开发中,对一些不熟悉quartz的朋友格外友好。

编码实现

引入依赖

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-quartz</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.31</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.11</version>
        </dependency>

    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

映射实体类

@TableName
@Data
public class ScheduleJob {
    @TableId
    private Long id;
    @TableField
    private String jobName;
    @TableField
    private String cronExpr;    // cron表达式
    @TableField
    private String jobStatus;   // 任务状态
    @TableField
    private String url;         // 业务接口
}

必不可少的属性大概就是上面这几个,后续需要再添加

service层与mapper层我偷个懒,直接用mybatisplus的api

ScheduleJobService

@Service
public class ScheduleJobService extends ServiceImpl<ScheduleJobMapper, ScheduleJob> {
}

ScheduleJobMapper

@Mapper
public interface ScheduleJobMapper extends BaseMapper<ScheduleJob> {
}

有了这些就可以先做个添加定时任务的接口测试一下流程是否通畅

ScheduleJobController

@RestController
public class ScheduleJobController {

    @Resource
    private ScheduleJobService scheduleJobService;

    @PostMapping("/scheduleJob")
    public ResultBean createScheduleJob(@RequestBody ScheduleJob scheduleJob) {
        scheduleJobService.save(scheduleJob);
        return ResultBean.SUCCESS();
    }


}

这个ResultBean是我封装的一个返回对象

@Data
@AllArgsConstructor
public class ResultBean {
    private String code;
    private String msg;
    private Object data;

    public static ResultBean SUCCESS(Object... data) {
        String code = "200";
        String msg = "操作成功!";
        return new ResultBean(code, msg, data);
    }
}

简单测试了一下,能写入库

在这里插入图片描述

目前为止的操作和定时任务还没有一分钱关系,接下来我们来接入定时任务。

创建任务类,这个任务类要支持发送http请求,所以取名为RestRequestJob,不过我们还不知道能不能真的按着指定的时间的执行,先不写太复杂。

@Slf4j
public class RestRequestJob extends QuartzJobBean {

    @Override
    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        log.info("hello quartz");
    }
}

映射实体类有了,任务类也有了,就差任务类怎么按照映射实体类的信息去执行定时任务了

@Service
public class ScheduleJobManageService {
    @Autowired
    private ScheduleJobService scheduleJobService;
    @Autowired
    private Scheduler scheduler;

    public void createScheduleJob(ScheduleJob scheduleJob) {
        JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName() + scheduleJob.getId());
        JobDetail jobDetail = JobBuilder.newJob(RestRequestJob.class).withIdentity(jobKey).build();

        // 表达式调度构建器
        CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(scheduleJob.getCronExpr());
        // 按新的cronExpression表达式构建一个新的trigger
        CronTrigger trigger = TriggerBuilder.newTrigger()
                .withIdentity(TriggerKey.triggerKey(scheduleJob.getId().toString()))
                .withSchedule(cronScheduleBuilder).build();
        jobDetail.getJobDataMap().put("TASK_PROPERTIES", scheduleJob);
        try {
            scheduler.scheduleJob(jobDetail, trigger);
            if (scheduleJob.getJobStatus().equals("0")) {
                scheduler.pauseJob(jobKey);
            }
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }

    }

}

jobKey是定时任务的唯一标识

jobDetail是任务类

trigger是执行时间

jobDetail.getJobDataMap().put("TASK_PROPERTIES", scheduleJob);是把目前的映射类信息保存到缓存中,可提供给其它地方用。

scheduler.scheduleJob是开始定时任务的线程

scheduler.pauseJob如果发现状态是暂停的话,则暂停定时任务

回到ScheduleJobController类的createScheduleJob方法,调用scheduleJobManageService类中的创建定时任务方法

    @PostMapping("/scheduleJob")
    public ResultBean createScheduleJob(@RequestBody ScheduleJob scheduleJob) {
        scheduleJobService.save(scheduleJob);
        scheduleJobManageService.createScheduleJob(scheduleJob);
        return ResultBean.SUCCESS();
    }

到此再来测试一下,添加定时任务时,能不能启动任务类

在这里插入图片描述

定时任务启动成功了,但是目前除了停止应用,我们完全没办法能让它停下来。得写个暂停方法

@RestController
public class ScheduleJobManagerController {

    @Resource
    private ScheduleJobManageService scheduleJobManageService;

    @GetMapping("/scheduleJobManage/changeStatus/{id}")
    public ResultBean changeStatus(@PathVariable("id") Long id) {
        try {
            scheduleJobManageService.changeStatus(id);
            return ResultBean.SUCCESS();
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }
}

在ScheduleJobManageService类中添加changeStatus方法

    @Transactional
    public void changeStatus(Long id) throws SchedulerException {
        ScheduleJob scheduleJob = scheduleJobService.getById(id);
        String status = scheduleJob.getJobStatus().equals("1") ? "0" : "1";
        scheduleJob.setJobStatus(status);
        scheduleJobService.saveOrUpdate(scheduleJob);

        JobKey jobKey = JobKey.jobKey(scheduleJob.getJobName() + scheduleJob.getId());
        switch (status) {
            case "1":
                log.info("已启动任务"+scheduleJob.getId());
                scheduler.resumeJob(jobKey);
                break;
            case "0":
                log.info("已暂停任务"+scheduleJob.getId());
                scheduler.pauseJob(jobKey);
                break;
        }
    }

测试一下,暂停启动都能成功

在这里插入图片描述

那我们这些定时任务遭遇了服务器重启之后都没法再启动了吗?那肯定不是,我们在服务启动过后再加载回来这些定时任务不就ok了嘛。在ScheduleJobManageService类中加多一个init方法

    @PostConstruct
    public void init() throws SchedulerException{
        scheduler.clear();
        List<ScheduleJob> jobs = scheduleJobService.list();
        jobs.parallelStream().forEach(this::createScheduleJob);
    }

一启动之后,那堆没暂停的定时任务一直在跑

在这里插入图片描述

现在该实现http请求了,回到RestRequestJob类的executeInternal

    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
//        log.info("hello quartz");
        ScheduleJob scheduleJob = new ScheduleJob();
        BeanUtils.copyProperties(context.getMergedJobDataMap().get("TASK_PROPERTIES"), scheduleJob);
        log.info("calling...{}",scheduleJob.getUrl());
        String s = HttpUtil.get(scheduleJob.getUrl());
        log.info(s);
    }

context.getMergedJobDataMap().get("TASK_PROPERTIES")这个就是刚才存进去的scheduleJob对象,这样就可以将url取出来,去访问了。

我做了一个很简单的接口用于测试是否url可以访问通的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UEO95tVV-1678082698242)(image-20230303154257116.png)]

调用成功

目前还缺定时任务修改、移除,执行日志记录

定时任务修改

ScheduleJobController

    @PutMapping("/scheduleJob")
    public ResultBean updateScheduleJob(@RequestBody ScheduleJob scheduleJob) {
        try {
            scheduleJobManageService.updateScheduleJob(scheduleJob);
            return ResultBean.SUCCESS();
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }

ScheduleJobManageService

    @Transactional
    public void updateScheduleJob(ScheduleJob scheduleJob) throws SchedulerException {
        ScheduleJob oldInfo = scheduleJobService.getById(scheduleJob.getId());
        scheduleJobService.saveOrUpdate(scheduleJob);
        if (!oldInfo.getCronExpr().equals(scheduleJob.getCronExpr()) || !oldInfo.getUrl().equals(scheduleJob.getUrl())) {
            JobKey jobKey = JobKey.jobKey(scheduleJob.getId().toString());
            if (scheduler.checkExists(jobKey))
            {
                // 防止创建时存在数据问题 先移除,然后在执行创建操作
                scheduler.deleteJob(jobKey);
            }
            createScheduleJob(scheduleJob);
        }
        log.info("更新成功!");
    }

删除定时任务

ScheduleJobController

    @DeleteMapping("/scheduleJob/{id}")
    public ResultBean deleteScheduleJob(@PathVariable Long id) {
        try {
            scheduleJobManageService.deleteScheduleJob(id);
            return ResultBean.SUCCESS();
        } catch (SchedulerException e) {
            throw new RuntimeException(e);
        }
    }

ScheduleJobManageService

    @Transactional
    public void deleteScheduleJob(Long id) throws SchedulerException {
        boolean b = scheduleJobService.removeById(id);
        if (b) {
            scheduler.deleteJob(JobKey.jobKey(id.toString()));
        }
    }

2. 日志写入

实体类

@Data
@TableName
public class ScheduleJobLog {
    @TableId(type = IdType.AUTO)
    private Long id;
    @TableField
    private Long schdJobId;
    @TableField
    private Date startTime;
    @TableField
    private Date endTime;
    @TableField
    private Integer executeRes;
    @TableField
    private String errorInfo;
}

Mapper和Service层都是用了mybatisplus偷工减料,就不累赘了。

重点是,我们的日志怎么切入?切入在什么地方?quartz实际上当trigger时间到了的时候,他去执行Job的实现类。也就是说我们的日志应该切入在RestRequestJob类中,那接下来改装一下executeInternal方法

protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
//        log.info("hello quartz");
        ScheduleJobLog scheduleJobLog = new ScheduleJobLog();
        ScheduleJob scheduleJob = null;
        try {
            scheduleJobLog.setStartTime(new Date());
            scheduleJob = new ScheduleJob();
            BeanUtils.copyProperties(context.getMergedJobDataMap().get("TASK_PROPERTIES"), scheduleJob);
            log.info("calling...{}",scheduleJob.getUrl());
//            String s = HttpUtil.get(scheduleJob.getUrl());
            HttpResponse httpRes = HttpRequest.get(scheduleJob.getUrl()).execute();
            log.info(String.valueOf(httpRes.getStatus()));
            if (httpRes.getStatus() == HttpStatus.HTTP_NOT_FOUND) {
                throw new Exception(String.valueOf(HttpStatus.HTTP_NOT_FOUND));
            }
            scheduleJobLog.setExecuteRes(1);    // 执行成功
        } catch (Exception e) {
            scheduleJobLog.setExecuteRes(-1);    // 执行失败
            scheduleJobLog.setErrorInfo(e.getMessage());
            throw new RuntimeException(e);
        } finally {
            scheduleJobLog.setEndTime(new Date());
            scheduleJobLog.setSchdJobId(scheduleJob.getId());
            ScheduleJobLogService logService = SpringUtil.getBean(ScheduleJobLogService.class);
            logService.save(scheduleJobLog);
        }

    }

SpringUtil.getBean是使用了hutool的工具,目的是在不被spring管理的类中也能拿到Bean。

到这里看起来貌似没什么问题,能增删改定时任务了,日志也能插入了。但是经验告诉我,如果碰到那种定时任务需要同步几万数据入库的,http的长连接也扛不住,而且一直在等待也对性能造成极大的影响。

那么能不能说,发出去的请求,到对应业务接口自己执行得了,不管你的死活,你执行完之后告诉我一声执行结果得了。这就是异步思想。当http请求过来的时候,任务类中把执行的日志中的id也带过去,业务接口这边一旦收到,马上就返回结果,告诉任务类这边,收到执行指令,你安就得了。任务类把执行日志的状态设置为【执行中】。业务接口执行结束之后调用一下,定时任务日志的接口,把状态更新即可。

  1. 新增更新执行状态接口
@RestController
public class ScheduleJobLogController {
    @Autowired
    private ScheduleJobLogService logService;

    @PutMapping("/scheduleJobLog")
    public ResultBean updateStatus (@RequestBody ScheduleJobLog scheduleJobLog) {
        logService.updateStatus(scheduleJobLog);
        return ResultBean.SUCCESS();
    }
}

2.实现更新状态方法updateStatus

@Service
public class ScheduleJobLogService extends ServiceImpl<ScheduleJobLogMapper, ScheduleJobLog> {

    public void updateStatus(ScheduleJobLog scheduleJobLog) {
        ScheduleJobLog jobLog = this.getById(scheduleJobLog.getId());
        jobLog.setExecuteRes(scheduleJobLog.getExecuteRes());
        jobLog.setErrorInfo(scheduleJobLog.getErrorInfo());
        jobLog.setEndTime(new Date());  // 更新执行结束时间
        this.saveOrUpdate(jobLog);
    }

}

在业务执行接口上有比较大的变动

@RestController
public class ExecuteJobController {

    @GetMapping("/scheduleJob/helloWorld")
    public ResultBean sayHelloWorld(Long logId) {
        ScheduleJobLog jobLog = new ScheduleJobLog();
        jobLog.setId(logId);
        new Thread(() -> {
            try {
                // 模拟超时长业务
//                Thread.sleep(2000);
                jobLog.setExecuteRes(1);
                //throw new Exception("just test");
            } catch (Exception e) {
                jobLog.setExecuteRes(-1);
                jobLog.setErrorInfo(e.getMessage());
                throw new RuntimeException(e);
            } finally {
                HttpRequest.put("http://localhost:8080/scheduleJobLog").body(JSONUtil.toJsonStr(jobLog)).execute();
            }
        }).start();
        return ResultBean.SUCCESS("hello world!");
    }
}

在实际的测试过程中,RestRequestJob的实现方法中,由于入库操作在finally中,当业务接口比较简单时,会出现数据库的并发问题,日志记录未入库,导致scheduleJobLogService.getById()方法空指针,因此要先入库再发送http请求业务接口

    protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
        ScheduleJobLog scheduleJobLog = new ScheduleJobLog();
        ScheduleJob scheduleJob =  new ScheduleJob();
        long snowflakeNextId = IdUtil.getSnowflakeNextId();
        ScheduleJobLogService logService = SpringUtil.getBean(ScheduleJobLogService.class);
        try {
            BeanUtils.copyProperties(context.getMergedJobDataMap().get("TASK_PROPERTIES"), scheduleJob);

            scheduleJobLog.setExecuteRes(0);    // 正在执行
            scheduleJobLog.setStartTime(new Date());
            scheduleJobLog.setId(snowflakeNextId);
            scheduleJobLog.setSchdJobId(scheduleJob.getId());
            logService.save(scheduleJobLog);    // 避免出现并发问题,应该先入库

            log.info("calling...{}", scheduleJob.getUrl()+"?logId="+snowflakeNextId);
            String reqUrl = scheduleJob.getUrl()+"?logId="+snowflakeNextId;
            HttpResponse httpRes = HttpRequest.get(reqUrl).timeout(10000).execute();
            log.info("logId" + snowflakeNextId + "; url: " + reqUrl);
            if (httpRes.getStatus() == HttpStatus.HTTP_NOT_FOUND) {
                throw new Exception(String.valueOf(HttpStatus.HTTP_NOT_FOUND));
            }
        } catch (Exception e) {
            scheduleJobLog.setExecuteRes(-1);    // 执行失败
            scheduleJobLog.setErrorInfo(e.getMessage());
            logService.saveOrUpdate(scheduleJobLog);
            throw new RuntimeException(e);
        }

    }

总结

在本文中重点是讲解实现思路,代码并不算怎么优雅,格式优化的地方还有很多,参数校验也不够严谨,可以根据自己的需求改造。

代码拉取

开源项目,最好就是各路大神能一起维护这个项目。

仓库地址:https://gitcode.net/interestANd/quartz-job

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/391322.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【C++】仿函数 -- priority_queue

文章目录一、priority_queue 的介绍和使用1、priority_queue 的介绍2、priority_queue 的使用3、priority_queue 相关 OJ 题二、仿函数1、什么是仿函数2、仿函数的作用三、priority_queue 的模拟实现一、priority_queue 的介绍和使用 1、priority_queue 的介绍 priority_queu…

vue3 transition动画

Vue 提供了 transition 的封装组件&#xff0c;通过它可以给任何元素和组件添加进入/离开过渡动画 一、vue怎么实现单组件/元素的过渡动画 Vue 在插入、更新或者移除 DOM 时&#xff0c;提供多种不同方式的应用过渡效果。------vue官网 vue的transition组件通过观察元素的DOM状…

全网最全整理,自动化测试10种场景处理(超详细)解决方案都在这......

目录&#xff1a;导读前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09;前言 自动化工作流程 自动…

三种让DIV标签中的P标签水平和垂直都居中方法

效果如下图 红色1块是span&#xff0c;属于行内元素。 绿色2块和蓝色4块是p&#xff0c;属于块级元素。 黄色3块h3&#xff0c;属于块 都是块级元素方法是可以通用的 这里两个类别元素一起来展示主要是为了区别 1行内块元素水平居中垂直居中 行内元素和行内块元素水平居中…

docker项目自动化部署脚本(认真排版、工作积累)

要解决什么问题&#xff1f; 把日益复杂化、工程化的开发环境&#xff0c;以及生产环境&#xff0c;变得简单&#xff0c;自动化部署。 达到什么效果&#xff1f; 环境处处一致&#xff0c;并且自动化部署&#xff0c;提升生产力&#xff0c;又快又好。 当您更换电脑、更换…

Vue2.0开发之——购物车案例-Goods组件封装-修改商品的勾选状态(49)

一 概述 如何修改商品的勾选状态自定义state-change事件修改对应商品的勾选状态 二 如何修改商品的勾选状态 2.1 App.vue中data每个Item中goods_state的变化伴随商品勾选状态变化 2.2 Goods.vue中复选框的值是props属性 <inputtype"checkbox"class"custom…

LeeCode:回文子串个数(动态规划)

文章目录一、题目二、算法思路三、代码实现四、复杂度分析一、题目 给你一个字符串 s &#xff0c;请你统计并返回这个字符串中 回文子串 的数目。具有不同开始位置或结束位置的子串&#xff0c;即使是由相同的字符组成&#xff0c;也会被视作不同的子串。 回文字符串 是正着读…

二值图像骨架线提取

二值图像骨架线提取HilditchThin算法Rosenfeld算法OpenCV_Contrib中的算法示例其他细化算法查表法HilditchThin的另一种算法参考二值图像骨架线提取算法&#xff1a;HilditchThin算法、Rosenfeld算法、OpenCV_Contrib中的算法 HilditchThin算法 1、使用的8邻域标记为&#xff…

Java+ElasticSearch+Pytorch实现以图搜图

以图搜图&#xff0c;涉及两大功能&#xff1a;1、提取图像特征向量。2、相似向量检索。第一个功能我通过编写pytorch模型并在java端借助djl调用实现&#xff0c;第二个功能通过elasticsearch7.6.2的dense_vector、cosineSimilarity实现。一、准备模型创建demo.py&#xff0c;输…

cuda2D FDTD——share

https://www.coder.work/article/30133 shared memory只能在block内共享&#xff0c;之间无法互相通信 对于2D TM波动方程计算&#xff0c;我们可以使用以下策略来处理共享内存的边界&#xff1a; 将全局内存中的数据复制到共享内存中时&#xff0c;除了将每个线程需要的数据…

Python爬虫实践:优志愿 院校列表

https://www.youzy.cn/tzy/search/colleges/collegeList获取目标网址等信息打开开发人员工具&#xff08;F12&#xff09;&#xff0c;拿到调用接口的地址&#xff0c;以及接口请求参数等信息&#xff0c;如下curl https://uwf7de983aad7a717eb.youzy.cn/youzy.dms.basiclib.ap…

假如你知道这样的MySQL性能优化

1. 为查询缓存优化你的查询 大多数的 MySQL 服务器都开启了查询缓存。这是提高性最有效的方法之 一&#xff0c;而且这是被 MySQL 的数据库引擎处理的。当有很多相同的查询被执行了多次的时候&#xff0c;这些查询结果会被放到一个缓存中&#xff0c;这样&#xff0c;后续的相同…

Kogito -- 入门详解

Kogito -- 入门详解1. Introduction1.1 Version1.2 Introduction2.Environment Install2.1 JDK Install2.2 Maven Install&#xff08;3.8.6&#xff09;2.3 Idea2.4 VSCode3. Run Code3.1 Dependency3.2 Run3.3 Swagger4.Awakening4.1 Big Data -- Postgres5.Awakening5.1 Big…

如何做一个高级的文本编辑器 textarea,拥有快捷键操作

如何做一个高级的文本编辑器 textarea&#xff0c;拥有快捷键操作 最近想做一个高级点的 textarea &#xff0c;支持 JetBrains 系列软件的快捷键&#xff0c;比如&#xff1a; CTRL D 复制当前行。Tab 在前面插入 4 个空格。Shift Tab 删除行前的空格&#xff0c;多于4个&a…

google独立站和与企业官网的区别是什么?

google独立站和与企业官网的区别是什么&#xff1f; 答案是&#xff1a;独立站通过谷歌SEO优化可以更好的获取自然排名的流量。 随着互联网的不断发展&#xff0c;企业越来越重视自身网站的建设和优化&#xff0c;而在企业网站建设中&#xff0c;很多人会犯一个常见的错误&am…

模块、包和异常

目录1.模块import 导入from...import 导入2. 模块的搜索顺序3. __name__属性的使用4. 包包的使用步骤5. 发布模块6. 安装模块7. 卸载模块8. pip 安装第三方模块9. 异常处理异常捕获异常的传递抛出 raise 异常1.模块 模块是 Python 程序架构的一个核心概念 每一个以扩展名 py …

LPDDR4x 的 学习总结(4) - SDRAM chip的组织结构

上节总结cell的结构和基本操作 本节基于cell组合起来的DRAM组织结构 DDR Device 的组织结构 Cells 以特定的方式组成 Column/Row/Bank/Chip/Rank/DIMM/Channel等多层级组织结构如下图&#xff1a; 图1 - DRAM的组织结构 图2 - DRAM容量的组织结构图 Channel: 同1个DDR控制器 …

GIT基础常用命令-1 GIT基础篇

git基础常用命令-1 GIT基础篇1.git简介及配置1.1 git简介1.2 git配置config1.2.1 查看配置git config1.2.2 配置设置1.2.3 获取帮助git help2 GIT基础常用命令2.1 获取镜像仓库2.1.1 git init2.1.2 git clone2.2 本地仓库常用命令2.2.1 git status2.2.2 git add2.2.3 git diff2…

seata1.5.2使用从零快速上手(提供代码与安装包)

1.软件准备&#xff1a; 1.1 seata1.5.2 官网下载&#xff1a;地址:http://seata.io/zh-cn/ server源码:https://github.com/seata/seata 百度云下载&#xff08;建议&#xff09;: 百度下载 链接&#xff1a;https://pan.baidu.com/s/1eilbSI0YdmupHYI7FroTsw 提取码&…

【编程基础之Python】10、Python中的运算符

【编程基础之Python】10、Python中的运算符Python中的运算符算术运算符赋值运算符比较运算符逻辑运算符位运算符成员运算符身份运算符运算符优先级运算符总结Python中的运算符 Python是一门非常流行的编程语言&#xff0c;它支持各种运算符来执行各种操作。这篇文章将详细介绍…