苏三说技术
「苏三说技术」 维护者目前就任于某出名互联网公司,从事开发、架构和局部管理工作。实战经验丰盛,对jdk、spring、springboot、springcloud、mybatis等开源框架源码有肯定钻研,欢送关注,和我一起交换。
30篇原创内容
公众号
大家好我是苏三,又跟大家见面了。
===================
前言
最近有几个读者私信给我,问我他们的业务场景,要用什么样的定时工作。的确,在不必的业务场景下要用不同的定时工作,其实咱们的抉择还是挺多的。我明天给大家总结10种十分实用的定时工作,总有一种是适宜你的。
一. linux自带的定时工作
crontab
不晓得你有没有遇到过这种场景:有时须要长期统计线上的数据,而后导出到excel表格中。这种需要有时较为简单,光靠写sql语句是无奈满足需要的,这就须要写java代码了。而后将该程序打成一个jar包,在线上环境执行,最初将生成的excel文件下载到本地。
为了减小对线上环境的影响,咱们个别会抉择在凌晨1-2点
,趁用户量少的时候,执行统计程序。(其实凌晨4点左右,用户才是起码的)
因为工夫太晚了,咱们齐全没必要守在那里等执行后果,一个定时工作就能能够搞定。
那么,这种状况用哪种定时工作更适合呢?
答案是:linux
零碎的crontab
。(不过也不排除有些我的项目没部署在linux零碎中)
运行crontab -e
,能够编辑定时器,而后退出如下命令:
`0 2 * * * /usr/local/java/jdk1.8/bin/java -jar /data/app/tool.jar > /logs/tool.log &`
就能够在每天凌晨2点
,定时执行tool.jar
程序,并且把日志输入到tool.log
文件中。当然你也能够把前面的执行java程序的命令写成shell脚本,更不便保护。
应用这种定时工作反对不便批改定时规定,有界面能够对立治理配置的各种定时脚本。
crontab命令的根本格局如下:
`crontab [参数] [文件名]`
如果没有指定文件名,则接管键盘上输出的命令,并将它载入到crontab
。
参数性能对照表如下:
以上参数,如果没有应用-u
指定用户,则默认应用的以后用户。
通过crontab -e
命令编辑文件内容,具体语法如下:
`[分] [小时] [日期] [月] [星期] 具体任务`
其中:
分,示意多少分钟,范畴:0-59
小时,示意多少小时,范畴:0-23
日期,示意具体在哪一天,范畴:1-31
月,示意多少月,范畴:1-12
星期,示意多少周,范畴:0-7,0和7都代表星期日
还有一些特殊字符,比方:
*
代表如何工夫,比方:*1***
示意每天凌晨1点执行。/
代表每隔多久执行一次,比方:*/5 ****
示意每隔5分钟执行一次。,
代表反对多个,比方:10 7,9,12 ***
示意在每天的7、9、12点10分各执行一次。-
代表反对一个范畴,比方:10 7-9 ***
示意在每天的7、8、9点10分各执行一次。
此外,顺便说一下crontab
须要crond
服务反对,crond
是linux
下用来周期地执行某种工作的一个守护过程,在装置linux
操作系统后,默认会装置crond
服务工具,且crond
服务默认就是自启动的。crond
过程每分钟会定期检查是否有要执行的工作,如果有,则会主动执行该工作。
能够通过以下命令操作相干服务:
`service crond status // 查看运行状态``service crond start //启动服务``service crond stop //敞开服务``service crond restart //重启服务``service crond reload //从新载入配置`
应用crontab
的优缺点:
长处:不便批改定时规定,反对一些较简单的定时规定,通过文件能够对立治理配好的各种定时脚本。
毛病:如果定时工作十分多,不太好找,而且必须要求操作系统是
linux
,否则无奈执行。
二. jdk自带的定时工作
1.Thread
各位敬爱的敌人,你没看错,Thread
类真的能做定时工作。如果你看过一些定时工作框架的源码,你最初会发现,它们的底层也会应用Thread
类。
实现这种定时工作的具体代码如下:
`public static void init() {` `new Thread(() -> {` `while (true) {` `try {` `System.out.println("doSameThing");` `Thread.sleep(1000 * 60 * 5);` `} catch (Exception e) {` `log.error(e);` `}` `}` `}).start();``}`
应用Thread
类能够做最简略的定时工作,在run
办法中有个while
的死循环(当然还有其余形式),执行咱们本人的工作。有个须要特地留神的中央是,须要用try...catch
捕捉异样,否则如果出现异常,就间接退出循环,下次将无奈继续执行了。
这种形式做的定时工作,只能周期性执行,不能反对定时在某个工夫点执行。
此外,该线程能够定义成守护线程
,在后盾默默执行就好。
应用场景:比方我的项目中有时须要每隔10分钟去下载某个文件,或者每隔5分钟去读取模板文件生成动态html页面等等,一些简略的周期性工作场景。
应用Thread
类的优缺点:
长处:这种定时工作非常简单,学习成本低,容易动手,对于那些简略的周期性工作,是个不错的抉择。
毛病:不反对指定某个工夫点执行工作,不反对提早执行等操作,性能过于繁多,无奈应答一些较为简单的场景。
2.Timer
Timer
类是jdk专门提供的定时器工具,用来在后盾线程打算执行指定工作,在java.util
包下,要跟TimerTask
一起配合应用。
Timer
类其实是一个任务调度器,它外面蕴含了一个TimerThread
线程,在这个线程中有限循环从TaskQueue
中获取TimerTask
(该类实现了Runnable接口),调用其run
办法,就能异步执行定时工作。咱们须要继承TimerTask
类,实现它的run
办法,在该办法中加上本人的业务逻辑。
实现这种定时工作的具体代码如下:
`public class TimerTest {` `public static void main(String[] args) {` `Timer timer = new Timer();` `timer.schedule(new TimerTask() {` `@Override` `public void run() {` `System.out.println("doSomething");` `}` `},2000,1000);` `}``}`
先实例化一个Timer
类,而后调用它的schedule
办法,在该办法中实例化TimerTask
类,业务逻辑写在run
办法中。schedule
办法最初的两次参数别离示意:延迟时间
和 间隔时间
,单位是毫秒。下面例子中,设置的定时工作是每隔1秒执行一次,提早2秒执行。
次要蕴含6个办法:
schedule(TimerTask task, Date time)
, 指定工作task在指定工夫time执行schedule(TimerTask task, long delay)
, 指定工作task在指定提早delay后执行schedule(TimerTask task, Date firstTime,long period)
,指定工作task在指定工夫firstTime执行后,进行反复固定提早频率peroid的执行schedule(TimerTask task, long delay, long period)
, 指定工作task 在指定提早delay 后,进行反复固定提早频率peroid的执行scheduleAtFixedRate(TimerTask task,Date firstTime,long period)
, 指定工作task在指定工夫firstTime执行后,进行反复固定提早频率peroid的执行scheduleAtFixedRate(TimerTask task, long delay, long period)
, 指定工作task 在指定提早delay 后,进行反复固定提早频率peroid的执行
不过应用Timer
实现定时工作有以下问题:
因为
Timer
是单线程执行工作,如果其中一个工作耗时十分长,会影响其余工作的执行。如果
TimerTask
抛出RuntimeException
,Timer会进行所有工作的运行。
应用Timer
类的优缺点:
长处:十分不便实现多个周期性的定时工作,并且反对提早执行,还反对在指定工夫之后反对,性能还算弱小。
毛病:如果其中一个工作耗时十分长,会影响其余工作的执行。并且如果
TimerTask
抛出RuntimeException
,Timer
会进行所有工作的运行,所以阿里巴巴开发者标准中不倡议应用它。
3.ScheduledExecutorService
ScheduledExecutorService
是JDK1.5+版本引进的定时工作,该类位于java.util.concurrent
并发包下。
ScheduledExecutorService
是基于多线程的,设计的初衷是为了解决Timer
单线程执行,多个工作之间会相互影响的问题。
它次要蕴含4个办法:
schedule(Runnable command,long delay,TimeUnit unit)
,带延迟时间的调度,只执行一次,调度之后可通过Future.get()阻塞直至工作执行结束。schedule(Callable<V> callable,long delay,TimeUnit unit)
,带延迟时间的调度,只执行一次,调度之后可通过Future.get()阻塞直至工作执行结束,并且能够获取执行后果。scheduleAtFixedRate
,示意以固定频率执行的工作,如果当前任务耗时较多,超过定时周期period,则当前任务完结后会立刻执行。scheduleWithFixedDelay
,示意以固定延时执行工作,延时是绝对当前任务完结为终点计算开始工夫。
实现这种定时工作的具体代码如下:
`public class ScheduleExecutorTest {` `public static void main(String[] args) {` `ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);` `scheduledExecutorService.scheduleAtFixedRate(() -> {` `System.out.println("doSomething");` `},1000,1000, TimeUnit.MILLISECONDS);` `}``}`
调用ScheduledExecutorService
类的scheduleAtFixedRate
办法实现周期性工作,每隔1秒钟执行一次,每次提早1秒再执行。
这种定时工作是阿里巴巴开发者标准中用来代替Timer
类的计划,对于多线程执行周期性工作,是个不错的抉择。
ScheduledExecutorService的优缺点:
长处:基于多线程的定时工作,多个工作之间不会相干影响,反对周期性的执行工作,并且带提早性能。
毛病:不反对一些较简单的定时规定。
三. spring反对的定时工作
1.spring task
spring task
是spring3
以上版本自带的定时工作,实现定时工作的性能时,须要引入spring-context
包,目前它反对:xml
和 注解
两种形式。
1. 我的项目实战
因为xml形式太古老了,咱们以springboot我的项目中注解形式为例。
第一步,在pom.xml文件中引入spring-context
相干依赖。
`<dependency>` `<groupId>org.springframework</groupId>` `<artifactId>spring-context</artifactId>``</dependency>`
第二步,在springboot启动类上加上@EnableScheduling
注解。
`@EnableScheduling``@SpringBootApplication``public class Application {` `public static void main(String[] args) {` `new SpringApplicationBuilder(Application.class).web(WebApplicationType.SERVLET).run(args);` `}``}`
第三步,应用@Scheduled
注解定义定时规定。
`@Service``public class SpringTaskTest {` `@Scheduled(cron = "${sue.spring.task.cron}")` `public void fun() {` `System.out.println("doSomething");` `}``}`
第四步,在applicationContext.properties
文件中配置参数:
`sue.spring.task.cron=*/10 * * * * ?`
这样就能每隔10秒执行一次fun办法了。
2. cron规定
spring4以上的版本中,cron表达式蕴含6个参数:
`[秒] [分] [时] [日期] [月] [星期]`
还反对几个罕用的特殊符号:
*
:示意任何工夫触发工作,
:示意指定的工夫触发工作-
:示意一段时间内触发工作/
:示意从哪一个时刻开始,每隔多长时间触发一次工作。?
:示意用于月中的天和周中的天两个子表达式,示意不指定值。
cron表达式参数具体含意:
秒,取值范畴:0-59,反对
*
、,
、-
、/
。分,取值范畴:0-59,反对
*
、,
、-
、/
。时,取值范畴:0-23,反对
*
、,
、-
、/
。日期,取值范畴:1-31,反对
*
、,
、-
、/
。比秒多了?
,示意如果指定的星期
触发了,则配置的日期
变成有效。月,取值范畴:1-12,反对
*
、,
、-
、/
。星期,取值范畴:1~7,1代表星期天,6代表星期六,其余的以此类推。反对
*
、,
、-
、/
、?
。比秒多了?
,示意如果指定的日期
触发了,则配置的星期
变成有效。
常见cron表达式应用举例:
0 0 0 1 * ?
每月1号零点执行0 0 2 * * ?
每天凌晨2点执行0 0 2 * * ?
每天凌晨2点执行0 0/5 11 * * ?
每天11点-11点55分,每隔5分钟执行一次0 0 18 ? * WED
每周三下午6点执行
spring task先通过ScheduledAnnotationBeanPostProcessor类的processScheduled办法,解析和收集Scheduled
注解中的参数,蕴含:cron表达式。
而后在ScheduledTaskRegistrar类的afterPropertiesSet办法中,默认初始化一个单线程的ThreadPoolExecutor
执行工作。
对spring task感兴趣的小伙伴,能够加我微信找我私聊。
应用spring task
的优缺点:
长处:spring框架自带的定时性能,springboot做了十分好的封装,开启和定义定时工作非常容易,反对简单的
cron
表达式,能够满足绝大多数单机版的业务场景。单个工作时,以后次的调度实现后,再执行下一次任务调度。毛病:默认单线程,如果后面的工作执行工夫太长,对前面工作的执行有影响。不反对集群形式部署,不能做数据存储型定时工作。
2.spring quartz
quartz
是OpenSymphony
开源组织在Job scheduling
畛域的开源我的项目,是由java开发的一个开源的工作日程管理系统。
quartz能做什么?
作业调度:调用各种框架的作业脚本,例如shell,hive等。
定时工作:在某一预约的时刻,执行你想要执行的工作。
架构图如下:
quartz蕴含的次要接口如下:
Scheduler
代表调度容器,一个调度容器中能够注册多个JobDetail和Trigger。Job
代表工作,即要执行的具体内容。JobDetail
代表具体的可执行的调度程序,Job是这个可执行程调度程序所要执行的内容。JobBuilder
用于定义或构建JobDetail实例。Trigger
代表调度触发器,决定什么时候去调。TriggerBuilder
用于定义或构建触发器。JobStore
用于存储作业和任务调度期间的状态。
1. 我的项目实战
咱们还是以springboot
集成quartz
为例。
第一步,在pom.xml文件中引入quartz
相干依赖。
`<dependency>` `<groupId>org.springframework.boot</groupId>` `<artifactId>spring-boot-starter-quartz</artifactId>``</dependency>`
第二步,创立真正的定时工作执行类,该类继承QuartzJobBean
。
`public class QuartzTestJob extends QuartzJobBean {` `@Override` `protected void executeInternal(JobExecutionContext context) throws JobExecutionException {` `String userName = (String) context.getJobDetail().getJobDataMap().get("userName");` `System.out.println("userName:" + userName);` `}``}`
第三步,创立调度程序JobDetail
和调度器Trigger
。
`@Configuration``public class QuartzConfig {` `@Value("${sue.spring.quartz.cron}")` `private String testCron;` `/**` `* 创立定时工作` `*/` `@Bean` `public JobDetail quartzTestDetail() {` `JobDetail jobDetail = JobBuilder.newJob(QuartzTestJob.class)` `.withIdentity("quartzTestDetail", "QUARTZ_TEST")` `.usingJobData("userName", "susan")` `.storeDurably()` `.build();` `return jobDetail;` `}` `/**` `* 创立触发器` `*/` `@Bean` `public Trigger quartzTestJobTrigger() {` `//每隔5秒执行一次` `CronScheduleBuilder cronScheduleBuilder = CronScheduleBuilder.cronSchedule(testCron);` `//创立触发器` `Trigger trigger = TriggerBuilder.newTrigger()` `.forJob(quartzTestDetail())` `.withIdentity("quartzTestJobTrigger", "QUARTZ_TEST_JOB_TRIGGER")` `.withSchedule(cronScheduleBuilder)` `.build();` `return trigger;` `}``}`
第四步,在applicationContext.properties
文件中配置参数:
`sue.spring.quartz.cron=*/5 * * * * ?`
这样就能每隔5秒执行一次QuartzTestJob类的executeInternal办法了。
CronTrigger配置格局:
`[秒] [分] [小时] [日] [月] [周] [年]`
spring quartz
跟spring task
的cron
表达式规定基本一致,只是spring4
以上的版本去掉了前面的年
,而quartz
的CronTrigger
的年
是非必填的,这里我就不做过多介绍了。
应用spring quartz
的优缺点:
长处:默认是多线程异步执行,单个工作时,在上一个调度未实现时,下一个调度工夫到时,会另起一个线程开始新的调度,多个工作之间互不影响。反对简单的
cron
表达式,它能被集群实例化,反对分布式部署。毛病:绝对于spring task实现定时工作老本更高,须要手动配置
QuartzJobBean
、JobDetail
和Trigger
等。须要引入了第三方的quartz
包,有肯定的学习老本。不反对并行调度,不反对失败解决策略和动静分片的策略等。
四. 分布式定时工作
1.xxl-job
xxl-job
是公众点评(许雪里)开发的一个分布式任务调度平台,其外围设计指标是开发迅速、学习简略、轻量级、易扩大。现已凋谢源代码并接入多家公司线上产品线,开箱即用。
xxl-job
框架对quartz
进行了扩大,应用mysql
数据库存储数据,并且内置jetty作为RPC
服务调用。
次要特点如下:
有界面保护定时工作和触发规定,非常容易治理。
能动静启动或进行工作
反对弹性扩容缩容
反对工作失败报警
反对动静分片
反对故障转移
Rolling实时日志
反对用户和权限治理
治理界面:
整体架构图如下:
应用quartz架构图如下:
我的项目实战
xxl-admin
治理后盾部署和mysql脚本执行等这些后期筹备工作,我就不过多介绍了,有需要的敌人能够找我私聊,这些更偏差于运维的事件。
假如前期工作曾经OK了,接下来咱们须要:
第一步,在pom.xml文件中引入xxl-job
相干依赖。
`<dependency>` `<groupId>com.xuxueli</groupId>` `<artifactId>xxl-job-core</artifactId>``</dependency>`
第二步,在applicationContext.properties
文件中配置参数:
`xxl.job.admin.address: http://localhost:8088/xxl-job-admin/``xxl.job.executor.appname: xxl-job-executor-sample``xxl.job.executor.port: 8888``xxl.job.executor.logpath: /data/applogs/xxl-job/`
第三步,创立HelloJobHandler类继承IJobHandler
类:
`@JobHandler(value = "helloJobHandler")``@Component``public class HelloJobHandler extends IJobHandler {` `@Override` `public ReturnT<String> execute(String param) {` `System.out.println("XXL-JOB, Hello World.");` `return SUCCESS;` `}``}`
这样定时工作就配置好了。
倡议把定时工作独自部署到另外一个服务中,跟api服务离开。依据我以往的教训,job大部分状况下,会对数据做批量操作,如果操作的数据量太大,可能会对服务的内存和cpu资源造成肯定的影响。
应用xxl-job
的优缺点:
长处:有界面治理定时工作,反对弹性扩容缩容、动静分片、故障转移、失败报警等性能。它的性能十分弱小,很多大厂在用,能够满足绝大多数业务场景。
毛病:和
quartz
一样,通过数据库分布式锁,来管制工作不能反复执行。在工作十分多的状况下,有一些性能问题。
2.elastic-job
elastic-job
是当当网开发的弹性分布式任务调度零碎,功能丰富弱小,采纳zookeeper实现分布式协调,实现工作高可用以及分片。它是专门为高并发和简单业务场景开发。
elastic-job
目前是apache
的shardingsphere
我的项目下的一个子项目,官网地址:http://shardingsphere.apache....
elastic-job
在2.x之后,出了两个产品线:Elastic-Job-Lite
和Elastic-Job-Cloud
,而咱们个别应用Elastic-Job-Lite就可能满足需要。Elastic-Job-Lite定位为轻量级无中心化解决方案,应用jar包的模式提供分布式工作的协调服务,内部仅依赖于Zookeeper。。
次要特点如下:
散布式调度协调
弹性扩容缩容
生效转移
错过执行作业重触发
作业分片一致性,保障同一分片在分布式环境中仅一个执行实例
自诊断并修复分布式不稳固造成的问题
反对并行调度
整体架构图:
我的项目实战
第一步,在pom.xml文件中引入elastic-job
相干依赖。
`<dependency>` `<groupId>com.dangdang</groupId>` `<artifactId>elastic-job-lite-core</artifactId>``</dependency>``<dependency>` `<groupId>com.dangdang</groupId>` `<artifactId>elastic-job-lite-spring</artifactId>``</dependency>`
第二步,减少ZKConfig类,配置zookeeper
:
`@Configuration``@ConditionalOnExpression("'${zk.serverList}'.length() > 0")``public class ZKConfig {` `@Bean` `public ZookeeperRegistryCenter registry(@Value("${zk.serverList}") String serverList,` `@Value("${zk.namespace}") String namespace) {` `return new ZookeeperRegistryCenter(new ZookeeperConfiguration(serverList, namespace));` `}``}`
第三步,定义一个类实现SimpleJob
接口:
`public class TestJob implements SimpleJob {` `@Override` `public void execute(ShardingContext shardingContext){` `System.out.println("ShardingTotalCount:"+shardingContext.getShardingTotalCount());` `System.out.println("ShardingItem:"+shardingContext.getShardingItem());` `}``}`
第四步,减少JobConfig配置工作:
`@Configuration``public class JobConfig {` `@Value("${sue.spring.elatisc.cron}")` `private String testCron;` `@Value("${sue.spring.elatisc.itemParameters}")` `private String shardingItemParameters;` `@Value("${sue.spring.elatisc.jobParameters}")` `private String jobParameters =;` `@Value("${sue.spring.elatisc.shardingTotalCount}")` `private int shardingTotalCount;` `@Autowired` `private ZookeeperRegistryCenter registryCenter;` `@Bean` `public SimpleJob testJob() {` `return new TestJob();` `}` `@Bean` `public JobScheduler simpleJobScheduler(final SimpleJob simpleJob) {` `return new SpringJobScheduler(simpleJob, registryCenter, getConfiguration(simpleJob.getClass(),` `cron, shardingTotalCount, shardingItemParameters, jobParameters));` `}` `private geConfiguration getConfiguration(Class<? extends SimpleJob> jobClass,String cron,int shardingTotalCount,String shardingItemParameters,String jobParameters) {` `JobCoreConfiguration simpleCoreConfig = JobCoreConfiguration.newBuilder(jobClass.getName(), testCron, shardingTotalCount).` `shardingItemParameters(shardingItemParameters).jobParameter(jobParameters).build();` `SimpleJobConfiguration simpleJobConfig = new SimpleJobConfiguration(simpleCoreConfig, jobClass.getCanonicalName());` `LiteJobConfiguration jobConfig = LiteJobConfiguration.newBuilder(simpleJobConfig).overwrite(true).build();` `return jobConfig;` `}``}`
其中:
cron:cron表达式,定义触发规定。
shardingTotalCount:定义作业分片总数
shardingItemParameters:定义调配项参数,个别用分片序列号和参数用等号分隔,多个键值对用逗号分隔,分片序列号从0开始,不可大于或等于作业分片总数。
jobParameters:作业自定义参数
第五步,在applicationContext.properties
文件中配置参数:
`spring.application.name=elasticjobDemo``zk.serverList=localhost:2181``zk.namespace=elasticjobDemo``sue.spring.elatisc.cron=0/5 * * * * ?``sue.spring.elatisc.itemParameters=0=A,1=B,2=C,3=D``sue.spring.elatisc.jobParameters=test``sue.spring.elatisc.shardingTotalCount=4`
这样定时工作就配置好了,创立定时工作的步骤,绝对于xxl-job
来说要繁琐一些。
应用elastic-job
的优缺点:
长处:反对散布式调度协调,反对分片,适宜高并发,和一些业务相对来说较简单的场景。
毛病:须要依赖于zookeeper,实现定时工作绝对于
xxl-job
要简单一些,要对分片规定十分相熟。
3.其余分布式定时工作
1. Saturn
Saturn是唯品会开源的一个分布式任务调度平台。取代传统的Linux Cron/Spring Batch Job的形式,做到全域对立配置,对立监控,工作高可用以及分片并发解决。
Saturn是在当当开源的Elastic-Job根底上,联合各方需要和咱们的实际见解改进而成。应用案例:唯品会、酷狗音乐、新网银行、海融易、航美在线、量富征信等。
github地址:https://github.com/vipshop/Sa...
2. TBSchedule
TBSchedule是阿里开发的一款分布式任务调度平台,旨在将调度作业从业务零碎中分离出来,升高或者是打消和业务零碎的耦合度,进行高效异步工作解决。
目前被广泛应用在阿里巴巴、淘宝、支付宝、京东、聚美、汽车之家、国美等很多互联网企业的流程调度零碎中。
github地址:https://github.com/taobao/TBS...
诚实说优良的定时工作还是挺多的,不是说哪种定时工作牛逼咱们就肯定要用哪种,而是要依据理论业务需要抉择。每种定时工作都有优缺点,正当抉择既能满足业务需要,又能防止资源节约,才是上上策。当然在理论的业务场景,通常会多种定时工作一起配合应用。
顺便说一句,欢送敬爱的小伙伴们,找我一起聊聊:你用过哪些定时工作,遇到过哪些问题,以及如何解决问题的。如果有相干问题也能够问我。
心愿咱们可能共同进步,一起成长。
最初说一句(求关注,别白嫖我)
如果这篇文章对您有所帮忙,或者有所启发的话,帮忙扫描下发二维码关注一下,您的反对是我保持写作最大的能源。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:面试、代码神器、开发手册、工夫治理有超赞的粉丝福利,另外回复:加群,能够跟很多BAT大厂的前辈交换和学习。
集体公众号
集体微信