定时任务在我们的项目中或多或少都会使用到,不论你是初级程序员,还是有多年工作经验的中高级程序员,在项目中加一个定时任务去处理业务的一个aspect,为什么用了一个英文单词‘aspect’?而不是‘切面’或者‘方面’,这个绝对不是‘骚’,而是有我自己的一些考量,可能也不是很准,如果我说是切面,肯能有人就会觉得我处理的是一些与业务无关的共有逻辑,如果我说是方面又有些听着不是很专业,很准确的样子,所以使用了aspect这个词,就看你怎么理解了,其实都是没毛病的,闲话就说这么多了,接下来我们言归正传。

1.java中的定时任务实现

在互联网这个大的圈子里,其实并不是所有公司的系统架构都是围绕Spring来展开的,对于一些传统的互联网公司,某些产品的实现并不是围绕Spring,很荣幸在我刚入行的前两年就是在这样的公司里工作,很多人可能觉得的这一定是个小公司,那我很不幸的告诉你,其实是上市公司,并且不是外包的上市公司,有自己的成熟的产品体系,可以说他们是那个领域里面的龙头也不为过。这样的工作体验让我明白了一个事情,做java的不一定非得是Spring,在大多数人的观念里面好像觉得,你学java不就是Spring么?这里也没什么好解释的,送大家一句话,大家共勉:你知道的越多,你不知道的也越多。

废话说的有点儿多,自己都讨厌自己了,上段代码来缓解一下:

import java.util.concurrent.*;public class JavaScheduleDemo {private static final ScheduledExecutorService executor = Executors.newScheduledThreadPool(2, r -> {Thread thread = new Thread(r);thread.setName("javaScheduleDemo");thread.setDaemon(false);return thread;});public static void main(String[] args) {executor.scheduleWithFixedDelay(()->{System.out.println(Thread.currentThread().getName()+" execute job");},1,2, TimeUnit.SECONDS);executor.scheduleAtFixedRate(()->{System.out.println(Thread.currentThread().getName()+" execute job");},1,2,TimeUnit.SECONDS);}}

上面这个deom演示了在java中实现一个定时任务的方式,我们可以借助jdk提供的ScheduledExecutorService这个类来实现。

scheduleWithFixedDelay(Runnable comment, long initialDelay,long period,TimeUnit unit)

comment:执行任务的Runnable或者Callable接口的实现类

initialDelay:第一次执行任务的延时

period:连续执行任务的时间周期,从上一个任务全部执行完成后计算一个延时来执行下一个任务

unit:initialDelay和period的时间单位

scheduleAtFixedRate(Runnable comment, long initialDelay,long period,TimeUnit unit)

comment:执行任务的Runnable或者Callable接口的实现类

initialDelay:第一次执行任务的延时

period:连续执行任务的时间周期,从上一个任务执行开始计算延时多少时间执行下一个任务,但是也是会等待上一个任务执行完毕

unit:initialDelay和period的时间单位

上面是jdk支持的定时或者延时任务的一个类和两种实现,首先创建线程池的时候,只指定了一个核心线程数,并没有指定最大线程数,也就是说最大可能会创建Integer.MAX_VALUE个线程,这样就有可能占用大量的系统资源,甚至耗尽系统资源。还有就是所谓的固定延时或者固定频率其实也是依赖CPU的调度,可能会出现虽然延时时间到了,但是没有得到CPU分配的执行时间片,任务还是不能正常执行,只能等到CPU分配时间片后才开始执行,具有很大的不确定性。

上面jdk提供的这种延时任务的实现,存在着资源耗尽,任务执行的不确定性,还有执行任务的不确定性,基于这些缺陷,我们在生产环境要使用的时候,一定要考量好这样做对我们系统能产生的最坏的影响,切不可为了实现某个功能而忽略了系统的健壮性,稳定性。其实看过阿里巴巴java开发手册的同学,应该也看到了,阿里巴巴是明确禁止通过这样的方式来创建线程池来执行任务,所以我们可以考虑一些开源的成熟的任务调度框架来实现,比如quartz,也是我们下面重点介绍的。

2.Spring中的定时任务实现

1.在SpringBoot中提供了一种非常简单的定时任务执行方式,只需要在我们的主类上通过@EnableScheduling注解开启定时任务,然后在我们的需要定时执行的方法上通过@Scheduled注解配置任务执行的频次,延时间隔等一些参数就可以了,同时这个注解支持cron表达式来表达任务的执行,具体的配置就不展开说了,比较简单,相信大家用的一定很好了。其实这种方式也是存在任务的不可控性,一旦配置好就不能更改了,灵活性相对差一些,我们平时也是用他去做一些相对不是特别核心的功能,比如发送一些短信,发送邮件通知一些任务。

2.Quartz

什么是quartz:Quartz是OpenSymphony开源组织在Job scheduling领域又一个开源项目,完全由Java开发,可以用来执行定时任务。

有三大核心模块:

Scheduler:任务调度器

JobDetail:任务的具体信息

Trigger:任务的触发器

quartz框架提供了非常丰富的API来让我们去根据自身的需求去定制化我们的任务,相对于上面我们介绍的几种定时实现方式,quartz最大的优点就是:

  1. 支持任务的持久化,我们的每个job以及他的一些触发条件,执行频次,执行状态,生命周期等,都能够落地保存,这样即使我们的系统宕机,等系统正常恢复后,我们的任务还是可以延续之前的状态继续执行
  2. 任务的管理,得益于三大的核心模块,丰富的API,我们能够对我们的任务进行一个完全的控制。

我们可以在我们的工程中引入quartz相关的依赖来实现具体的定时功能,本着不重复造轮子的想法,SpringBoot帮我们封装成'Starter',所以我们可以直接来使用,避免一些不必要的坑。直接上货:

1.引入依赖

      <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId></dependency>

通过上面的依赖引入我们就可以开始使用quartz来实现具体的任务调度了,在开始写代码之前,我们先介绍一下具体的一些配置,之前我们说任务可以持久化,说到持久化,我们首先想到的就是存储系统,比如数据库系统,文件存储系统等。quartz支持两种存储方式,MEMORY,JDBC,默认是MEMEORY,也可以通过如下方式来配置:

spring.quartz.job-store-type=jdbc

通过这样的方式,我们就可以将我们的任务相关的一些信息储存在数据库系统中,这也是我们能管理任务的基础。我们这里演示将数据存储到mysql数据库中。涉及到数据库,我们首先想到的就是库,表,schema,SpringBoot也支持通过配置的方式来初始化或者配置

spring.quartz.jdbc.initialize-schema=always

上面的配置表示每次启动服务的时候,都会重新初始化我们的schema,当然也有可选的参数:

EMBEDED:仅初始化嵌入式的数据源

NEVER:仅在第一次的启动的时候初始化一次

根据我们自身的需求选择合适的初始化方式就可以了。

By default, the database is detected and initialized by using the standard scripts provided with the Quartz library. These scripts drop existing tables, deleting all triggers on every restart. It is also possible to provide a custom script by setting the spring.quartz.jdbc.schema property.

引自官方文档,大致的意思就是:默认情况下,通过quartz库中提供的标准的脚本去检测和初始化数据源,在每次重启服务的时候都会删除表和触发器,当然通过spring.quartz.jdbc.schema这个属性可以提供一个自定义的脚本。通过这个属性,我们就可以指定数据库的表结构等一些元信息,为我们后面实现管理任务提供了数据基础。

至此,原则上,只要我们的系统中配置了数据源,那么quartz就可以使用我们配置的数据源和脚本去初始化表等。

To have Quartz use aDataSourceother than the application’s mainDataSource, declare aDataSourcebean, annotating its@Beanmethod with@QuartzDataSource. Doing so ensures that the Quartz-specificDataSourceis used by both theSchedulerFactoryBeanand for schema initialization.

这段话的意思是,通过配置quartz自己的数据源,而不是使用我们应用程序的主数据源,这样做确保SchedulerFactoryBean和schema的初始化都使用特定的数据源。这样做可以将我们的业务数据和定时相关的数据分隔,不会因为定时任务的原因而影响业务系统的稳定性。

    @Autowiredprivate MyQuartzDataSourceConfig config;@Bean({"quartzHikariConfig"})public HikariConfig hikariConfig(){HikariConfig hikariConfig = new HikariConfig();hikariConfig.setDriverClassName(config.getDiverClassName());hikariConfig.setUsername(config.getUserName());hikariConfig.setPassword(config.getPassword());hikariConfig.setJdbcUrl(config.getJdbcUrl());hikariConfig.setMaximumPoolSize(config.getMaxNumberPoolSize());return hikariConfig;}@Bean({"quartzDataSource"})@QuartzDataSourcepublic DataSource buildDataSource(){return new HikariDataSource(this.hikariConfig());}

这样我们就注入了名称为'quartzDataSource'的quartz的数据源,启动我们的程序,我们可以在我们配置的数据源下看到如下结构:

这里我们使用了默认的脚本文件,所以这些表也是quartz框架默认的表结构,看到表是不是就放心多了,有了表我们就能通过sql语句做很多的事情,添加任务,添加触发器,更新任务,删除任务等,这些功能是不是就是信手拈来了。

上面我们初始化好了我们的存储数据源,下面我们看一下代码怎么实现,在quartz框架中的定义了一个Job接口,这个接口只有一个方法 execute(JobExecutionContext jobExecutionContext),我们所有的任务都通过这个接口去执行,Spring对这个接口进行了包装,抽象了一个QuartzJobBean类,所以我们可以通过继承这个类,重写executeInternal(JobExecutionContext jobExecutionContext)这个方法就可以了,如下:

@Component
public class CustomJob extends  QuartzJobBean{@Overrideprotected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {JobDataMap jobDataMap = jobExecutionContext.getJobDetail().getJobDataMap();String email = jobDataMap.getString("email");JobDataMap dataMap = jobExecutionContext.getTrigger().getJobDataMap();Integer number = dataMap.getInt("number");System.out.println("<<<<<<<< quartz hello word " +email+"|"+number+" >>>>>>>>");}}

JobExecutionContext这个参数见名知意,就知道是任务执行的上下文环境,通过这个参数我们可以获取到JobDetail,trigger以及任务执行的一些其他的信息,JobDataMap实现了jdk的map接口,存储key-value键值对的数据,这样我们就可以从上层传数据到任务处理层,供我们处理对应的业务。接下来我们创建一个任务并且启动运行一下:

@Component
public class MyQuartzTimer {@Autowiredprivate Scheduler scheduler;@PostConstructpublic void startScheduleJob() throws Exception {String jobKey = UUID.randomUUID().toString();String triggerKey  = UUID.randomUUID().toString();JobDetail jobDetail = JobBuilder.newJob(CustomJob.class).withIdentity(jobKey,CustomJob.class.getName()).usingJobData("email","shangyouzhi@126.com").build();Trigger trigger = TriggerBuilder.newTrigger().withIdentity(triggerKey,CustomJob.class.getName()).withSchedule(CronScheduleBuilder.cronSchedule("0 0/2 * * * ?")).usingJobData("number",1).build();Calendar calendar = new CronCalendar("0/2 * * * * ?");scheduler.addCalendar(CustomJob.class.getName(),calendar,true,true);scheduler.scheduleJob(jobDetail,trigger);scheduler.start();}
}

SpringBoot自动帮助我们配置好Scheduler执行器,所以我们直接Autowired进来就可以使用了

首先我们通过JobBuilder创建了一个JobDetail,其实这个JobDetail的一个实例就绑定了一个Job,Job才是任务执行的核心逻辑,也就是我们上面介绍的,将JobDetail和Job绑定在一起,是为了防止在并发情况下,scheduler对同一个job并发执行,JobDetail绑定了job执行的数据信息,每次访问的时候会根据JobDetail来实例化一个job,这样就避免并发访问的问题。

通过TriggerBuilder创建了一个Trigger,我们使用了cron表达式来决定任务的触发,这里是两分钟触发一次,并传了一个参数number

最后通过Scheduler就可以执行任务了。看一下运行的结果:

启动的时候可以看到,我们的执行器运行在单例模式,默认的线程池线程数为10,使用的job-store为LocalDataSourceJobStore,同时我们的数据源也是单例的。这些其实都是可以配置的,具体可以参考quartz-configuration

可以看到我们的定时任务成功执行了,对应的参数我们也获取到了。这样我们就做完了我们的一个定时任务,具体的一些执行计划我们可以根据自身的业务情况来配置,为了实现任务的管理,我们可以做一个任务管理的功能,来实现任务的添加,开始,停止,编辑等功能,比较简单,这里就不演示了,有兴趣的同学可以自己实现以下,无非就是调用scheduler的api,然后对数据库进行增删改查功能。

3.多数据源的配置

上面我们配置了quartz特定的数据源,本来我们的系统的通过默认的配置来初始化数据源的,也是就是spring.datasource.*属性来配置数据源,但是如果我们手动注入了数据源,应用程序就会使用我们手动注入的数据源,这样我们业务相关的数据源就被覆盖了,我们就无法正确访问我们的业务了,所以我们只能再配置一个主数据源,从来没有在一个应用程序中配置过多个数据源,也不知道这样是不是合理(有想法的同学可以在评论区留下自己的建议,我们一起讨论),其实我们可以把我们的定时任务单独做一个服务,通过RPC框架来在我们的业务系统中使用,这里我们就在我们的单个应用中配置两个数据源。

配置多数据源我是通过以下思路来实现:

  1. 在配置文件中配置多个数据源的属性
  2. 手动注入多个数据源,不同的名称
  3. 通过AOP的思想,来切换数据源

在我们的yml文件中添加如下配置:

quartz-datasource:diverClassName: com.mysql.cj.jdbc.DriverjdbcUrl: jdbc:mysql://localhost:3306/quartz?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/ShanghaiuserName: rootpassword: 123456maxNumberPoolSize: 10primary-datasource:diverClassName: com.mysql.cj.jdbc.DriverjdbcUrl: jdbc:mysql://localhost:3306/blog?useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/ShanghaiuserName: rootpassword: 123456maxNumberPoolSize: 10

配置了quartz datasource,primary datasource,接下来最核心的来了,我们配置多个数据源,就是在我们调用对应的接口的时候希望能切换到对应的数据源,Spring提供了AbstractRoutingDataSource这个抽象类

    protected DataSource determineTargetDataSource() {Assert.notNull(this.resolvedDataSources, "DataSource router not initialized");Object lookupKey = this.determineCurrentLookupKey();DataSource dataSource = (DataSource)this.resolvedDataSources.get(lookupKey);if (dataSource == null && (this.lenientFallback || lookupKey == null)) {dataSource = this.resolvedDefaultDataSource;}if (dataSource == null) {throw new IllegalStateException("Cannot determine target DataSource for lookup key [" + lookupKey + "]");} else {return dataSource;}}

这个方法,主要就是确定应该使用哪个数据源,阅读源码我们可以看出来,主要通过我们注入的DataSource的名称来确定应该使用哪个数据源,也就是
protected abstract Object determineCurrentLookupKey()这个方法,接下来我们创建我们自己的数据源,重写这个方法:

@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource {@Overrideprotected Object determineCurrentLookupKey() {String currentDataSource = DataSourceContextHolder.getDbType();log.info("当前使用到的数据源为:{}",currentDataSource);return currentDataSource;}
}

DataSourceContextHolder这个类,主要持有一个ThreadLocal对象,来保存不同进程中的数据源名称:

public class DataSourceContextHolder {private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();public static void setDbType(DBTypeEnum dbType){contextHolder.set(dbType.getValue());}public static String getDbType(){return contextHolder.get();}public static void clearType(){contextHolder.remove();}
}

我是使用mybatis来访问的DB的,下面我们手动注入一下我们的两个数据源:

@MapperScan("org.spring.blog.mapper")
@Configuration
@EnableTransactionManagement
public class MyBatisPlusConfig {@Beanpublic PaginationInterceptor paginationInterceptor() {return new PaginationInterceptor();}@Autowiredprivate MyPrimaryDataSourceConfig primaryDataSourceConfig;@Bean("{primaryDataSource}")public DataSource primaryDataSource(){HikariConfig hikariConfig = new HikariConfig();hikariConfig.setDriverClassName(primaryDataSourceConfig.getDiverClassName());hikariConfig.setUsername(primaryDataSourceConfig.getUserName());hikariConfig.setPassword(primaryDataSourceConfig.getPassword());hikariConfig.setJdbcUrl(primaryDataSourceConfig.getJdbcUrl());hikariConfig.setMaximumPoolSize(primaryDataSourceConfig.getMaxNumberPoolSize());HikariDataSource dataSource = new HikariDataSource(hikariConfig);return dataSource;}@Autowiredprivate MyQuartzDataSourceConfig config;@Bean({"quartzHikariConfig"})public HikariConfig hikariConfig(){HikariConfig hikariConfig = new HikariConfig();hikariConfig.setDriverClassName(config.getDiverClassName());hikariConfig.setUsername(config.getUserName());hikariConfig.setPassword(config.getPassword());hikariConfig.setJdbcUrl(config.getJdbcUrl());hikariConfig.setMaximumPoolSize(config.getMaxNumberPoolSize());return hikariConfig;}@Bean({"quartzDataSource"})@QuartzDataSourcepublic DataSource buildDataSource(){return new HikariDataSource(this.hikariConfig());}@Bean@Primarypublic DataSource multipleDataSource(){DynamicDataSource dataSource = new DynamicDataSource();Map<Object,Object> targetDataSources = Maps.newHashMap();targetDataSources.put(DBTypeEnum.primary_data_source.getValue(),primaryDataSource());targetDataSources.put(DBTypeEnum.quartz_data_source.getValue(),buildDataSource());dataSource.setTargetDataSources(targetDataSources);dataSource.setDefaultTargetDataSource(primaryDataSource());return dataSource;}@Bean("sqlSessionFactory")public SqlSessionFactory sqlSessionFactory() throws Exception{MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(multipleDataSource());MybatisConfiguration configuration = new MybatisConfiguration();configuration.setJdbcTypeForNull(JdbcType.NULL);configuration.setMapUnderscoreToCamelCase(true);configuration.setCacheEnabled(false);sqlSessionFactoryBean.setConfiguration(configuration);sqlSessionFactoryBean.setPlugins(new Interceptor[]{paginationInterceptor()});return sqlSessionFactoryBean.getObject();}
}

可以看出我们注入了一个'quartzDataSource'和'primaryDataSource'两个数据源,在multipleDataSource()这个主数据源配置方法中,我们将我们注入的两个数据源放到targetDataSources这个map中,通过AbstractRoutingDataSource这个类的determineTargetDataSource()这个方法的源码看最终的数据源是在resolvedDataSources这个map中的通过名称找到的,这个map中的数据在哪儿初始化呢,继续看源码:

    public void afterPropertiesSet() {if (this.targetDataSources == null) {throw new IllegalArgumentException("Property 'targetDataSources' is required");} else {this.resolvedDataSources = new HashMap(this.targetDataSources.size());this.targetDataSources.forEach((key, value) -> {Object lookupKey = this.resolveSpecifiedLookupKey(key);DataSource dataSource = this.resolveSpecifiedDataSource(value);this.resolvedDataSources.put(lookupKey, dataSource);});if (this.defaultTargetDataSource != null) {this.resolvedDefaultDataSource = this.resolveSpecifiedDataSource(this.defaultTargetDataSource);}}}

通过这个方法就可以看出,其实是把我们的设置的targetDataSources里面的数据源copy了一份到resolvedDataSources数据源中,然后注入一个SqlSessionFactory,把我们的数据源设置进去就可以了,这样我们的多个数据源就注入成功了,mybatis的sqlSessionFactory也配置好了。

接下来,我们写一个切面,用来确定我们到底应该使用哪一个数据源:

@Component
@Order(value = -100)
@Aspect
@Slf4j
public class DataSourceAspect {@Pointcut("execution(* org.spring.blog.service..*.*(..))")protected void primaryDatasourceAspect(){}@Pointcut("execution(* org.spring.blog.timer..*.*(..))")protected void quartzDatasourceAspect(){}@Before("primaryDatasourceAspect()")public void primaryDataSource(){log.info("切换到主数据源");DataSourceContextHolder.setDbType(DBTypeEnum.primary_data_source);}@Before("quartzDatasourceAspect()")public void quartzDataSource(){log.info("切换到定时任务数据源");DataSourceContextHolder.setDbType(DBTypeEnum.quartz_data_source);}
}

从上面可以看出,我们定义了两个切入点,可以根据自身情况来设置,一个是业务的切入点,一个是定时任务的切入点,写了一个简单地测试案例:

@Service("testService")
public class TestServiceImpl implements TestService {@Autowiredprivate IMUserService imUserService;@Overridepublic Object test1(String aa) {System.out.println("testService aa:"+aa);List<MUser> users = imUserService.list();return users;}
}

当我们调用这个service接口的时候,就会切换到我们的主数据源。看一下运行的效果:

可以看出,当我们访问我们的业务库的时候,切换到我们的primaryDataSource,定时任务使用的是quartzDataSource,这样我们的多数据源就算是配置好了,在一个应用程序中到底该不该注入多个数据源,这个因为没有实际的使用经验,所以不敢说这样是不是就可以上生产环境了,有看到的大佬可以私信或者评论指正一下。

总结:

上面我们介绍了一些定时任务的实现,包括jdk的自带的,还有quartz框架实现的,SpringBoot集成了Quartz框架,我们演示了如何在SpringBoot中创建我们自己的定时任务,最后我们配置了多数据源,一个主数据源,也就是我们业务数据源,一个是quartz特定的数据源,并正确运行了我们的程序。

一万六千多字的文章,也是佩服自己,如果文中有什么不妥的地方,烦请各位大佬指正,如果觉得文章对你或者你的朋友有帮助,望点赞收藏转发,小弟在这里感谢了!

hutool的定时任务不支持依赖注入怎么办_可调度定时任务在SpringBoot中的实践相关推荐

  1. hutool的定时任务不支持依赖注入怎么办_分布式任务调度平台xxljob的内部原理,及在转转的落地实践...

    让世界因流转更美好 值此教师节来临之际,衷心祝愿所有的老师教师节快乐,身体健康,幸福平安,工作顺利,桃李满天下.您们辛苦了! 作者简介 · 杜云杰,架构师,转转架构部负责人,负责服务治理.MQ.云平台 ...

  2. hutool的定时任务不支持依赖注入怎么办_「架构」 - 定时任务 amp; Elastic-Job基本使用...

    一.配置zookeeper A.下载配置 # 下载zookeeper wget http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.4.13/z ...

  3. hutool的定时任务不支持依赖注入怎么办_设计一个任务调度算法,时间轮算法,比优先队列更高效...

    当年我还是个学生的时候,有一次去参加欢聚时代的一个面试,有一道面试题记忆尤新,让你来实现一个定时任务,你会怎么做?为了简化问题,我们只用考虑内存方案,不用考虑数据持久化. 数组法 最简单的,我们可以把 ...

  4. 创建支持依赖注入、Serilog 日志和 AppSettings 的 .NET 5 控制台应用

    翻译自 Mohamad Lawand 2021年3月24日的文章 <.NET 5 Console App with Dependency Injection, Serilog Logging, ...

  5. ASP.NET Core Filter如何支持依赖注入

    概述 通过使用 ASP.NET Core 中的筛选器,可在请求处理管道中的特定阶段之前或之后运行代码.内置筛选器处理任务,例如:授权(防止用户访问未获授权的资源).响应缓存(对请求管道进行短路出路,以 ...

  6. 【朝夕技术专刊】Core3.1WebApi_Filter多种注册方式支持依赖注入

    欢迎大家阅读<朝夕Net社区技术专刊>第5期 我们致力于.NetCore的推广和落地,为更好的帮助大家学习,方便分享干货,特创此刊!很高兴你能成为忠实读者,文末福利不要错过哦! 01 PA ...

  7. 依赖注入有点_一文读懂Java控制反转(IOC)与依赖注入(DI)

    要了解控制反转( Inversion of Control ), 我觉得有必要先了解软件设计的一个重要思想:依赖倒置原则(Dependency Inversion Principle ). 什么是依赖 ...

  8. phalapi可以依赖注入么_[7.8]-phalapi-进阶篇2(DI依赖注入和单例模式) | PhalApi(π框架) - PHP轻量级开源接口框架...

    phalapi-进阶篇2(DI依赖注入和单例模式) 前言 先在这里感谢phalapi框架创始人@dogstar,为我们提供了这样一个优秀的开源框架. 离上一次更新过去了快两周,在其中编写了一个关于DB ...

  9. python需要依赖注入吗_是否需要使用依赖注入容器?

    译文首发于 是否需要使用依赖注入容器?,转载请注明出处. 本文是依赖注入(Depeendency Injection)系列教程的第 2 篇文章,本系列教程主要讲解如何使用 PHP 实现一个轻量级服务容 ...

最新文章

  1. C/S和B/S的异同
  2. 吴恩达 coursera ML 第十二课总结+作业答案
  3. 汇编解析(6)-二进制文件(嵌入式,纯二进制格式的文件)进行反汇编和汇编
  4. kali利用msf工具对ms08-067漏洞入侵靶机(win xp2)
  5. tkinter的可视化拖拽工具_可视化越做越丑?这五个高级图表效果实现流程分享给你...
  6. Angular4中常用管道
  7. 如何突破科研瓶颈?如何与导师自在相处?微软研究员们的读博心得
  8. 3.2 指数型生成函数
  9. 每天工作6小时,月入过万,这个新职业火了
  10. SQL Server : 使用SQL Express的User Instance(用户实例)特性
  11. php fpm 报错,PHP-FPM安装报错解决
  12. android跑马灯代码,Android中实现跑马灯效果
  13. macos复制粘贴快捷键 快速_mac复制粘贴快捷键
  14. 《心灵捕手》经典台词
  15. css画企鹅,知识点
  16. SQL数据库面试题以及答案(50例题优化版-增加图片):你必知必会的SQL语句练习
  17. RuntimeError: Sizes of tensors must match except in dimension 1. Got 61 and 60 in dimension 2
  18. 8.6 循环辅助:continue和break
  19. Makefile -fPIC 选项
  20. 插值与拟合 (一) : 拉格朗日多项式插值 、Newton插值 、分段线性插值、Hermite插值 、样条插值、 B 样条函数插值、二维插值

热门文章

  1. git查看两次提交之间的差异_如何在同一分支的两个不同提交之间区分同一文件?...
  2. wits数据格式_WITS标准
  3. crt导出服务器文件,非1元证书怎么提取生成crt文件
  4. 微型计算机中AGP指,2011江苏省计算机等级考试二级理论考试试题及答案
  5. 用libconfig读取配置文件
  6. SpringBoot重复配置数据库导致Access denied for user ‘root‘@‘localhost‘ (using password: YES)
  7. atlas mysql 安装_atlas中间件安装配置
  8. csv java 科学计数法_Java入门笔记1/0(输入与输出)
  9. unity Conditional特性 总结
  10. 计算机办公软件应用二级 考试题库,计算机二级办公软件高级应用技术考试真题题库...