概述 在Java环境下创建定时任务有多种方式: 使用while循环配合Thread.sleep(),虽然稍嫌粗陋但也勉强可用 使用Timer和TimerTask 使用ScheduledExecutorService 定时任务框架,如Quartz 在SpringBoot下执行定时任务无非也就这几种方式(主要还是后两种)。只不过SpringBoot做了许多底层的工作,我们只需要做些简单的配置就行了。 通过注解实现定时任务 在SpringBoot中仅通过注解就可以实现常用的定时任务。步骤就两步: 在启动类中添加@EnableScheduling注解 在目标方法中添加@Scheduled注解,同时在@Scheduled注解中添加触发定时任务的元数据。 注意: 目标方法需要没有任何参数,并且返回类型为void 。 这里的定时任务元数据是“fixRate=1000”,意思是固定间隔每1000毫秒即执行一次该任务。 再来看几个@Schedule注解的参数: fixedRate:设置定时任务执行的时间间隔,该值为当前任务启动时间与下次任务启动时间之差; fixedDelay:设置定时任务执行的时间间隔,该值为当前任务结束时间与下次任务启动时间之差; cron:通过cron表达式来设置定时任务启动时间,在Cron Generator网站可以直接生成cron表达式。 这样创建的定时任务存在一个问题:如存在多个定时任务,这些任务会同步执行,也就是说所有的定时任务都是在一个线程中执行。 再添几个定时任务来执行下看看: 代码中一共创建了三个定时任务,每个定时任务的执行间隔都是1000毫秒,在任务体中输出了执行任务的线程ID和执行时间。 看下执行结果: 可以看到这三个定时任务的执行有如下的特点: 所有的定时任务每次都是在同一个线程上执行; 虽然未必是job1第一个开始执行,但是每批任务的执行次序是固定的——这是由fixRate参数决定的 这样的定时任务已经能够覆盖绝大部分的使用场景了,但是它的缺点也很明显:前面的任务执行时间过长必然会影响之后的任务的执行。为了解决这个问题,我们需要异步执行定时任务。接下来的部分我们将主要着眼于如何实现异步执行定时任务。 通过@Async注解实现异步定时任务 最常用的方式是使用@Async注解来实现异步执行定时任务。启用@Async注解的步骤如下: 在启动类中添加@EnableAsync注解: 在定时任务方法上添加@Async注解 我们为前面的三个定时任务都加上@Async注解再运行看看: 通过输出信息可以看到每个定时任务都在不同的线程上执行,彼此的执行次序和执行时间也互不影响,说明配置为异步执行已经成功。 通过配置实现异步定时任务 现在我们有必要稍稍深入了解下springboot定时任务的执行机制了。 springboot的定时任务主要涉及到两个接口:TaskScheduler和TaskExecutor。在springboot的默认定时任务实现中,这两个接口的实现类是ThreadPoolTaskScheduler和ThreadPoolTaskExecutor。 ThreadPoolTaskScheduler负责实现任务的定时执行机制,而ThreadPoolTaskExecutor则负责实现任务的异步执行机制。二者中,ThreadPoolTaskScheduler执行栈更偏底层一些。 尽管在职责上有些区别,但是两者在底层上都是依赖java的线程池机制实现的:ThreadPoolTaskScheduler依赖的底层线程池是ScheduledExecutorService,springboot默认为其提供的coreSize是1,所以默认的定时任务都是在一个线程中执行;ThreadPoolTaskExecutor依赖的底层线程池是ThreadPoolExecutor,springboot默认为其提供的corePoolSize是8。 说到这里应该清楚了:我们可以不添加@Async注解,仅通过调整ThreadPoolTaskScheduler依赖的线程池的coreSize也能实现多线程异步执行;同样的,即使添加了@Async注解,将ThreadPoolTaskExecutor依赖的线程池的corePoolSize设置为1,那定时任务还是只能在一个线程上同步执行。看下springboot的相关配置项: 其中spring.task.scheduling是ThreadPoolTaskScheduler的线程池配置项,spring.task.execution是ThreadPoolExecutor的线程池配置项。 再稍稍扩展下:@Async注解的value属性就是用来指明使用的TaskExecutor实例的。默认值是空字符串,表示使用的是springboot自启动的TaskExecutor实例。如有需要,也可以使用自定义的TaskExecutor实例,如下: 此外,还有一种做法是通过提供自定义的TaskScheduler Bean实例来实现异步执行。要提供提供自定义的TaskScheduler 实例,可以直接通过@Bean注解声明创建,也可以在SchedulingConfigurer接口中配置。这些在后面我们会提到。 调用SpringBoot接口实现定时任务 有时候会需要将定时任务的定时元数据写在数据库或其他配置中心以便统一维护。这种情况就不是通过注解能够搞定的了,此时我们需要使用springboot定时任务一些组件来自行编程实现。常用的组件包括TaskScheduler、 Triger接口和SchedulingConfigurer接口。 注意:因为我们用到了springboot的定时任务组件,所以仍然需要在启动类上添加@EnableScheduling注解。 Trigger接口 Trigger接口主要用来设置定时元数据。要通过程序实现定时任务就不能不用到这个接口。这个接口有两个实现类: PeriodicTrigger用来配置固定时长的定时元数据 CronTrigger用来配置cron表达式定时元数据 使用TaskScheduler接口 TaskScheduler接口前面我们提过,这个接口需要配合Trigger接口一起使用来实现定时任务,看个例子: 在上面的代码里,我们使用@Autowired注解获取了springbootr容器里默认的TaskScheduler实例,然后通过PeriodicTrigger设置了定时元数据,定时任务的任务体则是一个Runable接口的实现(在这里只是输出一行信息)。 因为默认的TaskScheduler实例的线程池coreSize是1,所以如有多个并发任务,这些任务的执行仍然是同步的。要调整为异步可以在配置文件中配置,也可以通过提供一个自定义的TaskScheduler实例来设置: 使用SchedulingConfigurer接口 SchedulingConfigurer接口的主要用处是注册基于Trigger接口自定义实现的定时任务。 在实现SchedulingConfigurer接口后,通常还需要使用@Configuration注解(当然启动类上的@EnableScheduling注解也不能少)来声明它实现类。 这个接口唯一的一个方法就是configureTasks,字面意思是配置定时任务。这个方法最重要的参数是一个ScheduledTaskRegistrar定时任务注册类实例,该类有8个方法,允许我们以不同的方式注册定时任务。 简单做了个实现: 这里我们只使用了三种注册任务的方法,分别尝试注册了fixDelay、fixRate以及cron触发的定时任务。 springboot会自动启动注册的定时任务。看下执行结果: 在执行结果中可以看到这里的任务也是在单一线程同步执行的。要设置为异步执行也简单,因为SchedulingConfigurer接口的另一个作用就是为定时任务提供自定义的TaskScheduler实例。来看下: 在这里,我将之前注册的定时任务去掉了,目的是想验证下这里的配置是否对注解实现的定时任务有效。经检验是可行的。当然对在configureTasks方法中配置的定时任务肯定也是有效的。我就不一一贴结果了。 另外,需要注意:如SchedulingConfigurer接口实例已经注入,将无法再获取到springboot默认提供的TaskScheduler接口实例。 通过Quartz实现定时任务 Quartz是一个非常强大的定时任务管理框架。短短的一篇文章未必能介绍清楚Quartz的全部用法。所以这里只是简单地演示下如何在springboot中是如何使用Quartz的。更多的用法建议优先参考Quartz官方文档。 在spring-boot-web 2.0及之后的版本,已经自动集成了quartz,如果不使用spring-boot-web或使用较早的版本的话我们还需要加一些依赖: 添加完成这些依赖后,springboot服务在启动时也会自启动内部的quartz。事实上springboot已经为我们准备好了几乎全部的quartz的配置。我们要做的只是把自定义的任务填进去。 首先我们需要创建一个Job实例,来实现Job的具体行为。 QuartzJobBean是Spring提供的Quartz Job抽象类。在实现这个类的时候我们可以获取注入到spring中的其他Bean。 配置Job 在创建QuartzConfig类的时候实现了InitializingBean接口,目的是在QuartzConfig实例及依赖类都完成注入后可以立即执行配置组装操作。 这里面有几个关键接口需要说明下: SchedulerFactoryBean,Quartz Scheduler工厂类,springboot自动化配置实现; Scheduer,负责Quartz Job调度,可从工厂类实例获取; JobDetail,执行Quartz Job封装; Trigger,完成Quartz Job启动。 还可以在配置文件中添加Quartz的配置: 这里配置了让Quartz默认延迟启动3分钟。 看下执行结果: 好了,就这些内容了。前面用到的程序都上传到了GITHUB,有需要可以参考下。 参考文档 Spring Task Execution and Scheduling Scheduling Tasks SpringBoot Quartz Scheduler Spring Boot Quartz Scheduler Example: Building an Email Scheduling app Quartz Scheduler Tutorials
[阅读更多...]-
springboot入门03 – 配置定时任务
-
查找占用CPU时间最长的线程
记录下查找Java应用占用CPU时间最长的线程的过程。 获取进程ID 使用jps指令获取java服务进程ID: 命令结果如下: 其中19064即是目标java服务进程ID。 查询线程占用时间 使用top -H -p pid指令查询指定进程下的线程占用CPU的信息。 其中“-H”说明查询线程信息,“-p”指示进程ID, pid为进程ID。 执行命令如下: 执行结果如下: 输入大写P(shift + p)可以查看占用CPU最多的线程;输入大写T(shift + t)可以查看占用CPU时间最长的线程。 结果中第一列为线程ID。这里的线程ID是十进制的。因为jstack中的线程ID是以十六进制表示,所以需要将线程ID转为十六进制。 以第一行的线程ID 43225为例,使用如下命令进行转换: 转换结果为: 通过输出结果可知,43225对应的十六进制值是a8d9,通常记为“0xa8d9”。 找到线程进行定位 找到占用CPU较多的线程ID后,还需要将线程执行程序进行关联。 使用jstack命令打印线程堆栈信息,在堆栈信息中搜索十六进制的线程ID,如根据a8d9搜索到的结果为: 注意堆栈信息中的“nid=0xa8d9”,nid即表示十六进制线程ID。 根据堆栈信息可以看出这个线程是日志打印线程,占用CPU时间较长也是可以理解的。 就这样。 其他 如果为目标应用启用了jvm监控服务(如JSPY)的话,这套流程会简单许多——只需要先排序再查看就可以了。 再记录下top指令的一些交互命令:
[阅读更多...] -
springboot入门02 – 自定义数据源及多数据源配置
spring-boot的自动化配置中是包含数据源连接配置的。但有些时候我们需要自定义数据源连接的配置,比如: 使用的数据库连接池Spring暂时还不支持; 需要配置连接多数据源; 需要自定义一些数据库连接配置项。 这三种只是我曾经遇到的情形,当然还有些其他的情形。接下来就以曾经遇到的一个问题进行展开。 自定义数据源 我们在生产环境使用的数据库连接池是alibaba的druid。数据库连接配置大致是这样的: 这里我用h2的内存数据库做个演示。在启动的时候会遇到一个错误日志: 解决这个错误的方法按理来说是很简单,只需要在数据库的配置中添加一行validation-query就好了: 不过不幸的是SpringBoot目前的版本(2.1.7.RELEASE)中的数据库连接自动化配置中没有validation-query选项,也不支持druid数据库连接池的自动化配置。 要解决这个问题,自定义Druid的自动化配置当然是一个好办法;另一个较为简单些的办法就是使用自定义数据源连接配置了。 首先,修改下项目配置: 为了避免因自动化配置产生干扰,这里将数据库连接配置移动到了custom下。 然后,创建一个数据库连接配置类: 在配置类的MapperScan注解中,通过basePackages指明了该数据源所有相关的Mapper类的位置。另外,为了支持事务,这里还创建了DataSourceTransactionManager的Bean实例。如果不需要事务支持可以取消这个Bean方法。 MyBatis有个非常有用的配置项“mapUnderscoreToCamelCase”用来自动识别下划线并转为驼峰结构,这个配置项需要在setSqlSessionFactory方法中完成配置: 这样完成配置后,启动程序可以看到日志中的ERROR信息消失。 使用自定义数据源,还意味着需要承担一些损失,比如: 大部分配置项没有默认值,需要手动配置; springboot原生数据源一些非常有用的特性如schema和platform将不可用; MyBatis的自动化配置将不可用 如果不想承担这些损失,我的建议是:换个数据库连接池试试,如Hikari或DBCP2。 多数据源支持 多数据源通常是自定义数据源配置应用最多的场景了,接下来演示下是如何完成的。 首先,肯定是要在spring配置文件中添加另一个数据库的配置信息了: 这里添加了另一个h2内存数据库master,然后添加master数据库对应的数据源配置: 可以看到Master数据库的配置与Worker数据库的差别不大。唯一不同的是Master数据库配置中,每个Bean的set方法上多了一个@Primary注解。 @Primary注解表示同一个类出现多个可用Bean时,将绑定@Primary注解的Bean。这里的几个Bean,transactionManager的@Primary注解不使用事务的话可以省略,sqlSessionFactory的@Primary注解没有的话使用某些spring版本启动会出错。不过在我的测试应用(版本:springboot 2.1.7.RELEASE)中,没有使用@Primary也没问题。通过debug日志可以看到,使用@Primary注解会影响数据源自动化配置和MyBatis自动化配置,前者用不着,后者用不了,所以也就没啥影响了。 transactionManager的@Primary注解还是有用的。如果没有@Primary注解,为方法添加事务注解需要指明使用哪个transactionManager的Bean,如下: 如果为transactionManager添加了@Primary注解,就会默认使用@Primary注解的transactionManager。因此建议为使用事务较多的数据源的transactionManager添加@Primary注解。 还有一点,每个数据源指向的basePackages是不一样的,需要将不同数据源的Mapper类置于不同的目录下。 写了一个测试应用spring-boot-database,上传到了GitHub,有需要可以参考下。
[阅读更多...] -
springboot入门01 – 缓存的使用
前两天解决了一个Spring缓存的问题,因此就想到记录一下spring-boot缓存的使用。 开启缓存 SpringBoot开启缓存也容易,在启动类上添加@EnableCaching注解就可以了,不需要过多的配置。不过此时开启的缓存是比较简单的缓存,即基于ConcurrentHashMap实现的缓存。虽然简单,但对于负载不高的应用也足够用了。 SpringBoot缓存的两个关键类是:CacheAutoConfiguration和CacheAspectSupport。这两个类分别位于spring-boot-autoconfigure和spring-context包。也就是说不使用spring-boot-starter-cache包也能在spring-boot中使用简单的缓存。 在CacheType类中可以看到SpringBoot支持的缓存类型。懒得一个一个介绍了,直接看下代码好了: 可以看到,支持了差不多所有的主流缓存。 缓存注解 spring boot的缓存主要是由注解支持实现的。下面是几个常见的注解: @Cacheable:针对方法进行配置,根据方法的请求参数缓存返回值,如无缓存值则执行方法; @CacheEvict:用于移除缓存记录,调用方法时会执行移除操作; @CachePut:用于更新缓存记录;保证方法一定会被调用,同时缓存方法返回值;与@Cacheable的区别在于是否每次都调用方法; @CacheConfig:统一配置管理类内部所有缓存注解的属性。 @Cacheable和@CachePut注解属性说明 keyGenerator:缓存数据key生成策略; CacheManager:缓存管理器,管理缓存组件; CacheResolver:缓存解析器,根据实际情况动态解析决定使用哪个缓存实例; value:缓存实例(一个缓存实例可以存储多个缓存KV对)的名称,至少需要设置一个; key:缓存的 key,如果不为空需要要按照 SpEL 表达式编写;如果不为空则缺省按照方法的所有参数进行组合; condition:缓存条件,使用 SpEL 编写;若不为空,只在条件结果为 true 时才进行缓存; unless:缓存排除条件,当条件结果为TRUE时不会缓存; sync:是否同步操作,如为true,那么多个线程访问同一条记录时,会同步执行调用的方法。 @CacheEvict注解属性说明 @CacheEvict注解的属性大致同@Cacheable一致,不过@CacheEvict还有两个独有的属性: allEntries:是否清空所有缓存内容,缺省为 false;如果指定为 true,则方法调用后将立即清空所有缓存; beforeInvocation:是否在方法执行前就清空,缺省为 false;如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存。 @CacheConfig注解属性说明 @CacheConfig注解的属性@Cacheable都有。因为@CacheConfig注解的作用本来就是统一管理相同类下所有方法的缓存配置。 需要提一下的就是在spring4.1之前是没有@CacheConfig注解的,那时候需要在每个方法的@Cacheable注解中设置cacheNames属性。 SpEL表达式 下面简单介绍下SpEL表达式的语法——如下表所示,摘自Spring官方文档: 名称 位置 描述 示例 methodName root对象 当前被调用的方法名 #root.methodname method root对象 当前被调用的方法 #root.method.name target root对象 当前被调用的目标对象实例 #root.target targetClass root对象 当前被调用的目标对象的类 #root.targetClass args root对象 当前被调用的方法的参数列表 #root.args[0] caches root对象 当前方法调用使用的缓存列表 #root.caches[0].name Argument Name 执行上下文 当前被调用的方法的参数,如findArtisan(Artisan artisan),可以通过#artsian.id获得参数 #artsian.id result 执行上下文 方法执行后的返回值(仅当方法执行后的判断有效,如 unless cacheEvict的beforeInvocation=false) #result 注意事项: 当我们要使用root对象的属性作为key时我们也可以将“#root”省略,因为Spring默认使用的就是root对象的属性, 如@Cacheable(key = “targetClass + methodName +#p0″); 使用方法参数时我们可以直接使用“#参数名”或者“#p参数index”,如:@Cacheable(value=”users”, key=”#id”)和@Cacheable(value=”users”, key=”#p0″)。 类型 运算符 关系 <,>,<=,>=,==,!=,lt,gt,le,ge,eq,ne 算术 +,- ,* ,/,%,^ 逻辑 &&,||,!,and,or,not,between,instanceof 条件 ?: (ternary),?: (elvis) 正则表达式 matches 其他类型 ?.,?[…],![…],^[…],$[…] 使用Caffeine缓存 前面的配置,即只在在启动类上添加@EnableCaching注解的情形下,我们只能使用最基础的SIMPLE缓存。现在我们尝试使用Caffeine缓存,就需要再多做些配置了。 我在做这个配置的时候遇到了些问题,所以在这里也简单记录下解决问题的过程。 首先,需要在配置文件中指明使用Caffine缓存并添加Caffeine缓存的配置: 然后加入Caffeine缓存依赖: 然而此时调用方法会发现启动失败,报错如下: 提示缺少CacheManager,说实话一开始看到这个错误提示我是不知咋回事儿的。所以尝试从debug信息中找些提示。将spring-boot的debug配置设置为true,再重新执行,在一长串的autoconfigure解析日志中有如下提示: 提示Caffeine缓存的自动配置需要两个类com.github.benmanes.caffeine.cache.Caffeine和org.springframework.cache.caffeine.CaffeineCacheManager。前者在引入caffeine的jar以后已经不是问题,后者则需要引入新的依赖。 CaffeineCacheManager这个类位于spring-context-support中,我们可以直接引入这个依赖,不过更推荐通过spring-boot-starter-cache来间接引入,这样指向性会更明确一些。
[阅读更多...] -
FileInputStream与BufferedInputStream
程序中有一块儿功能是读取硬盘文件到内存里。这块儿代码执行得异常缓慢,读4个G左右的文件竟会用上十几分钟。平时也不着急,但是今天催得紧,就得优化下了。 上传代码大致如下: 这里的问题在于直接使用了FileInputStream,就是说没有使用任何缓冲(buffer)。因此是一个字节一个字节地从磁盘里面读取文件,又一个字节一个字节地写到内存里,这样子效率可想而知。 尝试优化下,每次多读一些数据: 这样效果明显好了些,因为不再是一个字节一个字节的读了,而是每次读取了1024个字节的数据,也就是每1024字节内存才会与硬盘进行一次交互。因为大量减少了硬盘与内存的交互,速度自然就快了。 继续优化,使用BufferedInputStream: BufferedInputStream是FilterInputStream的子类,实现了装饰设计模式。BufferedInputStream是带缓冲区的输入流,默认缓冲区大小是8M,也就是说这个类会一次读取8M数据到内存缓存里以便进行后续操作。其原理与程序二差不多。 通过这三段程序分别测试读取了一个不太大的文件(1.57M),这三段程序的耗时分别是 8825、15、5,时间单位毫秒。 做一次调整:将程序二的缓存数组长度改为2048 * 1024,会看到执行时长也是5ms。 再做一次调整:这次读取一个34.6M的文件,并将程序二的缓存数组长度调整为8 * 1024 * 1024。可以看到程序二与程序三的执行时长分别是73ms与97ms。二者的性能还是比较接近的。 就这样!
[阅读更多...] -
Metrics学习03 – Histogram
Histogram用来统计数据的分布。Histogram可以提供收集到的数据的最大值、最小值、平均值和中值,此外还能提供百分比分布,如75%,95%,99.9%等等。 Histogram是我学习Metrics的驱动之一。最初是想使用Histogram来优化接口处理统计能力。 看下下面的类图: 类图表示了Histogram需要用到的几类和个接口之间的关系,简单说明下: Sampling接口:意思是取样器,只有一个方法,作用是取出某一阶段的统计结果快照(Snapshot); Reservoir接口:数据池,所有的记录最终都会写入Reservoir实例并完成运算;Metrics提供了多种数据池来执行不同的抽样运算; Snapshot接口:快照接口,作用是对Reservoir中的数据进行二次计算并生成统计结果;Snapshot提供了统计数据的最大值、最小值、标准差、平均值、中值、95分位值等指标; Histogram类:实现了Sampling接口,是对外交互的入口。 老规矩,看一段示例代码: 这段代码中定义了一个delayedMethod()方法,该方法会随机sleep一段时间来模拟方法执行时长。代码主体就是使用Histogram报表来统计每三秒钟内这个方法的执行状态。 MetricRegistry也提供了Histogram实例的创建注册方法,不过为这里了更直观一些,还是使用了直接new关键字来构建Histogram实例。可以看到,每次创建Histogram对象都需要传入一个Reservoir接口的实例。 看下执行结果片段: 统计结果即是由Snapshot提供。 简单介绍下metrics提供的几种Reservoir: UniformReservoir:默认保存1028条记录,每次进行update操作的时候,首先会依次地将值填入1028条记录中,当记录满了之后,就会使用随机替换0 – 1027中的一条(随机抽样1028条记录)。因为是随机替换,所以也不需要进行加锁和解锁。 SlidingWindowReservoir:固定大小的数据池,从0到n-1填入数据,但是不会对数据进行更新,也不会进行加锁和解锁(固定抽样n条记录)。 SlidingTimeWindowReservoir:非固定大小的数据池,但是只会存储过去N秒的数据(抽样N秒内的记录)。使用ConcurrentSkipListMap进行存储。 ExponentiallyDecayingReservoir:固定大小的数据池。首先会逐个数据填满数据池,随后会将老的数据替换为新的数据(抽样n条最新的记录),使用ConcurrentSkipListMap进行存储。可以说是SlidingWindowReservoir与SlidingTimeWindowReservoir的结合。 就这样。
[阅读更多...] -
dependencyManagement导致版本冲突
今天遇到了一个问题: 程序中某处报了ClassNoDefineError。这个类属于jna框架。检查jna的jar,发现确实没有那个类。关键在于这个报错是在一个依赖内部发生的,jna的jar是这个依赖的内部依赖,即当前应用的一个间接依赖。因为使用的jar是一个比较小众的服务,所以第一印象就是这个服务依赖的jna版本错了。进入到服务的pom看了后,发现该服务使用的jna版本是正确的,但是当前应用的jna版本却是错误的。 首先想到的是依赖冲突,使用如下命令检查了当前应用的全部依赖: 检查后可以确认当前应用的依赖(包括间接依赖)中只有一个jna框架,可以排除依赖冲突的问题。 仔细思索了一会儿,猜测是spring框架的问题。 退到工程最外部的pom文件,找到了spring-boot-dependencies: 检查spring-boot-dependencies的pom果然看到了jna的版本变量: 这个版本和应用的版本是一致的。随后在spring-boot-dependencies的dependencyManagement中找到了jna框架: 现在问题清晰了。问题出在maven的dependencyManagement上。 dependencyManagement的作用主要是依赖版本控制。其影响的范围包括: 其范围内的未直接声明版本的依赖; 其范围内的所有间接依赖(不论声明版本与否)。 在这个应用中,spring-boot-dependencies是应用pom的dependencyManagement成员,因此spring-boot-dependencies自己的dependencyManagement成员也会传递生效。jna框架是应用通过一个依赖的间接依赖,虽然在这个依赖中指明了jna的版本号,但是受到spring-boot-dependencies的dependencyManagement的影响,还是使用了一个早期的版本。 解决方式两个: 将jna框架也添加到dependencyManagement中; 先exclude掉jna间接依赖,再直接添加jna依赖并声明版本号
[阅读更多...] -
Quartz Job类使用有参数构造方法
也许这篇文章的名字应该改成《Quartz JobFactory的使用》,因为正是使用JobFactory解决的Quartz Job类有参数构造方法的问题。同样,使用JobFactory也能解决Job已有实例重用的问题。 问题描述 问题如标题描述:就是希望为Job类的实例传递参数,结果发现找到的文档中创建Job实例的方法多是通过类反射来实现的,这显然不能满足我的需求。 也找到了一些文章建议使用jobDataMap来解决。不太喜欢这种方案,因为太难看了——只是搬个砖而已,却连底裤都漏出来了。 解决方案 翻了下官方文档,在Lesson 12: Miscellaneous Features of Quartz这一节找了关于JobFactory的描述,虽然寥寥几行,却指明了解决问题的方向。 JobFactory,根据文档描述,是Scheduler的一个配置项,主要用来完成Job实例的注入。默认使用的JobFactory只是简单地调用了Job 类的newInstance()方法来创建了实例。 要解决我的问题需要创建自定义的JobFactory实现。JobFactory的实现可以参考默认JobFactory的实现SimpleJobFactory的一些代码: 相当简单的内容,只需要在创建实例那块儿填写自定义的内容就可以。 写了一个示例程序来进行说明。下面是一些核心类的类图: 查看完整代码请移步GitHub/zhyea 介绍下类图中类的作用: AbstractJob:抽象类,实现了Job接口,并提供了部分自定义方法,以及一个MyConfig参数的构造器 MyConfig:全局配置类,提供配置信息 JobRegistry:负责创建并维护AbstractJob实例 MyJobFactory:自定义的JobFactory实现类 MyJob:AbstractJob的一个子类,实现了Job的execute()方法 如上面的解释中所说,在MyJobFactory的实现中,JobRegistry是最重要的角色,在这个类中完成了AbstractJob实现类的实例的创建、维护和获取: MyJobFactory需要提供类的实例时,可以根据类名通过JobRegistry.getInstance获取到AbstractJob子类的实例: 核心部分就是这样了。再看下AbstractJob及启动类的内容。 AbstractJob近似于是是一个抽象模板类。在这个类里完成了JobDetail和Trigger实例的创建。当然Job接口的execute方法还是需要子类来实现。 下面是启动类的内容,在这个类里通过scheduler.setJobFactory完成了MyJobFactory实例的装配: 就这样了。
[阅读更多...] -
Metrics学习02 – Counter
Counter是要学习的Metrics的第二个工具,顾名思义即是计数器,通常用来执行统计之类的工作。 Counter比Gauge也复杂不了多少,直接看代码好了: 这里的代码较Gauge的那段稍稍有些不同:主要是在Counter实例的创建上 —— 这里使用了MetricRegistry实例的一个工具方法counter()。 MetricRegistry的实例为各种指标工具都提供了快速创建实例的方法,通过MetricRegistry提供的方法创建完成指标实例后可以自动完成注册。所以在上面的代码中没有再显式地将counter实例注册到MetricRegistry中。 另外值得细细品一下的是Counter的实现:Counter的计数能力主要依赖LongAdder类完成。 一般执行计数统计,最先想到的是AtomicLong/AtomicInt类。AtmoicXXX使用硬件级别的指令 CAS 来更新计数器的值,这样可以避免加锁,机器直接支持的指令,效率也很高。但是AtomicXXX中的 CAS 操作在出现线程竞争时,失败的线程会白白地循环一次,在并发很大的情况下,因为每次CAS都只有一个线程能成功,竞争失败的线程会非常多。失败次数越多,循环次数就越多,很多线程的CAS操作越来越接近 自旋锁(spin lock)。计数操作本来是一个很简单的操作,实际需要耗费的cpu时间应该是越少越好,AtomicXXX在高并发计数时,大量的cpu时间都浪费会在 自旋 上了,这很浪费,也降低了实际的计数效率。 使用LongAdder计数器可以避免这个问题。LongAdder采用了锁分段的思想,每个LongAdder实例都维护了一组计数单元Cell[],并发计数时,不同的线程可以在不同的计数单元cell[threadId]上进行计数,这样减少了线程竞争,提高了并发效率。本质上是用空间换时间的思想。 不过LongAdder一开始并不会直接使用计数单元Cell[],而是先使用一个long类型的base存储,当casBase()出现失败时,则会创建计数单元Cell[]。此时,如果在单个计数单元面出现了更新冲突,那么会尝试创建新的计数单元Cell,或者将Cell[]扩容为2倍。代码如下: 在高并发的情况下,LongAdder较之AtomicXXX有着数倍的性能优势。因此,通常建议使用LongAdder替换AtomicXXX。 再看下Counter的测试结果:
[阅读更多...] -
Metrics学习01 – Gauge
最近在写一个小应用,有些计量方式觉得可以参考一下Metrics,所以打算花两天的时间学习一下这个工具。 Overview Metrics是一个java监控计量工具包。在Spark、Hadoop、Spring等软件中都可以看到它的影子。Metrics提供了多种指标工具,如Gauge、Counter、Metrer、Timer、Histogram以及HealthCheck等。 这次先看一下Gauge,其他的看时间再逐个学习。 Gauge可以说是Metrics的最简单的一个指标:它的作用就是引用一个值。 来看个例子: 代码中通过匿名类的形式创建了一个Gauge接口的实例,作用是获取当前的时间。实现得非常简单,不需要多做解释。 因为Gauge接口只有一个方法getValue,是一个函数接口,所以可以考虑用lambda表达式创建Gauge接口实例: 既然Gauge这么简单,为什么不直接使用Gauge的值,还偏要用Gauge接口封装一下?是为了能在Metrics框架中记录并表示这个值。 Metrics框架中有几个基础概念:MetricRegistry、Reporter以及Metric。Metric前面提过两句,也演示了Metric之一的Gauge的用法。接下来简单介绍下MetricRegistry和Reporter。 MetricRegistry MetricRegistry的作用从类名就可以看出来:是Metric的注册中心(或者说是Metric容器),负责管理用户创建的所有Metric实例。 MetricRegistry主要提供了几种工具方法: 指标名称创建 创建Metric实例并自动注册 增删Metric实例 对注册的Metric实例应用监听器和过滤器 Reporter接口 从接口名称看起来,Reporter的作用应该是汇总指标实例的数据并生成报表。 Reporter接口的主要子类是ScheduledReporter,其核心是ScheduledExecutorService和ScheduledFuture,用来管理报表的定时输出。ScheduledReporter的子类包括ConsoleReporter、CsvReporter和Slf4jReporter,可以以不同的形式展示报表数据。 在4.x版本以前,Reporter接口还有实现一个类JmxReporter,可以通过JMX的形式输出报表数据。 扫了几个Reporter的实现,看出Reporter确实主要用来生成报表。不过也许是Metrics框架想要提供更多的自由,Reporter接口里并没有定义任何需要实现的方法: 如果需要以自定义的形式输出报表数据,可以继承ScheduledReporter类或实现Report接口来实现自己的需求,比如将报表数据以HTTP发送给统计应用。 Other 最后,看一下示例代码的执行结果:
[阅读更多...]