一个jinfo使用问题的解决 can’t determine target’s VM version

1. 问题

在使用jinfo查看进程所运行的环境变量信息时,报can't determine target's VM version的错误。

$ jinfo -sysprops 15444
Attaching to process ID 15444, please wait...
Error attaching to process: java.lang.RuntimeException: can't determine target's VM version : field "_reserve_for_allocation_prefetch" not found in type Abstract_VM_Version
sun.jvm.hotspot.debugger.DebuggerException: java.lang.RuntimeException: can't determine target's VM version : field "_reserve_for_allocation_prefetch" not found in type Abstract_VM_Version
        at sun.jvm.hotspot.HotSpotAgent.setupVM(HotSpotAgent.java:435)
        at sun.jvm.hotspot.HotSpotAgent.go(HotSpotAgent.java:305)
        at sun.jvm.hotspot.HotSpotAgent.attach(HotSpotAgent.java:140)
        at sun.jvm.hotspot.tools.Tool.start(Tool.java:185)
        at sun.jvm.hotspot.tools.Tool.execute(Tool.java:118)
        at sun.jvm.hotspot.tools.JInfo.main(JInfo.java:138)
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at java.lang.reflect.Method.invoke(Method.java:498)
        at sun.tools.jinfo.JInfo.runTool(JInfo.java:108)
        at sun.tools.jinfo.JInfo.main(JInfo.java:76)
Caused by: java.lang.RuntimeException: can't determine target's VM version : field "_reserve_for_allocation_prefetch" not found in type Abstract_VM_Version
        at sun.jvm.hotspot.runtime.VM.<init>(VM.java:291)
        at sun.jvm.hotspot.runtime.VM.initialize(VM.java:370)
        at sun.jvm.hotspot.HotSpotAgent.setupVM(HotSpotAgent.java:431)
        ... 11 more

2. 解决方法

  1. 刚开始认为是应用程序打包时没有指定编译的目标JDK,检查后排除了这个问题。
  2. 然后网上查找,有文说是权限问题,在Linux下使用su命令可以解决问题,但是我的是在windows下,应该不是权限问题。
  3. 然后想了想,会不会是jdk8/jdk9的配置不对,重新配置后,问题依然在,无论使用jdk8的jinfo,还是jdk9的jinfo都报同样问题。
  4. 最后觉得会不会是环境变量配置问题,然后使用set命令查看了下环境变量,发现path对应有如下设置,
    Path=...;G:\local\java-1.8.0-openjdk-1.8.0.151\bin;...
    

    在path中定义的java路径为open jdk 8。

    将oracle jdk 9的路径指定到path中后,问题得到解决。

    set JDK_HOME=G:\local\java\jdk-9.0.1
    set PATH=%JDK_HOME%\bin;%PATH%
    

    上面看到的问题应该是open jdk 8和oracle jdk 9之间的匹配问题。

3. 总结

依次类推,在使用jdk tools查看进程和内存时,下面的查看可能出现类似问题,

  • 在path中定义java路径为openjdk 8,使用oracle jdk 8/9 tools查看运行在oracle jdk 8/9环境中的java应用程序
  • 使用oracle jdk 8 tools查看运行在oracle jdk 9环境中的应用程序

下面的匹配将运行正常,

  • 在path中定义java路径为oracle jdk 9,使用oracle jdk 9 tools 查看运行在oracle jdk 8/9环境中的应用程序
  • 在path中定义java路径为oracle jdk 8,使用oracle jdk 8 tools 查看运行在oracle jdk 8环境中的应用程序

此记以参考。

一个spring boot程序的启动报错问题: Unable to read meta-data for class

1. 问题描述

Spring Boot Application在启动时报如下错误,

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.8.RELEASE)

 ...
 2018-08-02 10:46:59.333  WARN 12168 --- [           main] ationConfigEmbeddedWebApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [com.pphh.oauth.sample.App]; nested exception is java.lang.IllegalStateException: Unable to read meta-data for class  com.pphh.oauth.config.FilterAutoConfiguration

 org.springframework.beans.factory.BeanDefinitionStoreException: Failed to process import candidates for configuration class [com.pphh.oauth.sample.App]; nested exception is java.lang.IllegalStateException: Unable to read meta-data for class  com.pphh.oauth.config.FilterAutoConfiguration

 Caused by: java.lang.IllegalStateException: Unable to read meta-data for class  com.pphh.oauth.config.FilterAutoConfiguration
     at org.springframework.boot.autoconfigure.AutoConfigurationSorter$AutoConfigurationClass.getAnnotationMetadata(AutoConfigurationSorter.java:217) ~[spring-boot-autoconfigure-1.5.8.RELEASE.jar:1.5.8.RELEASE]
     ... 14 common frames omitted
 Caused by: java.io.FileNotFoundException: class path resource [ com/pphh/oauth/config/FilterAutoConfiguration.class] cannot be opened because it does not exist
     at org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:172) ~[spring-core-4.3.12.RELEASE.jar:4.3.12.RELEASE]
     ... 22 common frames omitted

 Disconnected from the target VM, address: '127.0.0.1:64322', transport: 'socket'

 Process finished with exit code 1

2. 问题查看和解决

错误消息中告知无法找到com/pphh/oauth/config/FilterAutoConfiguration.class这个class。但是,这个依赖包肯定是加载进来了,这个类也一定有,但是为什么找不到?

先看异常抛出的地方,

org.springframework.core.io.ClassPathResource.getInputStream(ClassPathResource.java:172)

执行的代码如下,

InputStream is;
if(this.clazz != null) {
    is = this.clazz.getResourceAsStream(this.path);
} else if(this.classLoader != null) {
    is = this.classLoader.getResourceAsStream(this.path);
} else {
    is = ClassLoader.getSystemResourceAsStream(this.path);
}

if(is == null) {
    throw new FileNotFoundException(this.getDescription() + " cannot be opened because it does not exist");
} else {
    return is;
}

可以看到,原因就是无法加载类文件,这里设置一个断点,准备调试看下。

在调试模式中,进入上面的执行代码,发现确实is加载后为null。我觉得很奇怪,类明明在class加载路径上,怎么为null。于是我在调试框手动执行类加载,

ClassLoader.getSystemResourceAsStream("com/pphh/oauth/config/FilterAutoConfiguration.class");

上面的手动执行能够正确加载。

最后比较了下两次ClassLoader命令的文件路径,发现了问题,this.path所指向的class path resource路径前面有一个空格。

  • 错误的class path resource [ com/pphh/oauth/config/..Configuration.class]
  • 正常的class path resource [com/pphh/oauth/config/..Configuration.class]

然后定位问题到相应的spring.factories配置文件中,

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pphh.oauth.config.ClientAutoConfiguration, \
com.pphh.oauth.config.FilterAutoConfiguration

将其更新为,

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pphh.oauth.config.ClientAutoConfiguration,\
com.pphh.oauth.config.FilterAutoConfiguration

问题得到解决,此记以参考。

3. 后记

很多时候,代码没有问题,却由于配置出错导致应用启动失败,这种问题非常多,防不胜防,也是比较头疼的问题。多加细心,同时对于配置问题的容错报错,也有助于问题的解决。

就以上面的例子,spring boot在加载auto configuration类之前,若能够自动去掉类名的前后空格,就不会导致上述问题。毕竟,在编写spring.factories的时候,很难确保不人为的添加空格。

Quartz任务调度框架的四种应用方法

图片来自pixabay.com的maxmann-665103会员

Quartz是一个任务调度框架,其框架中定义了如下三个基本组件,

  • Job 执行任务
  • Trigger 触发器,任务触发条件(周期性、定时等)
  • Scheduler 调度器,接受触发器和执行任务对象,实现任务的定时、远程、分布式调度

Quartz作为一个优秀的任务调度框架,其支持从简单的单一应用定时任务,到复杂的远程任务和集群(分布式)任务调度,支持任务调度功能的高可用、高性能、高可扩展性部署。

本文将给出Quartz的代码使用样例,演示了从简单到复杂应用场景的使用方法,供快速应用时参考,

  1. 简单的定时任务
  2. Cron定时任务
  3. 远程任务调度
  4. 集群(分布式)任务调度

1. 简单的定时任务

一个简单的定时任务样例代码见这里,其分三个主要步骤,

  • 创建job/trigger/scheduler三个对象
  • 初始化任务调度器,通过scheduler.scheduleJob(job, trigger)绑定触发器和执行任务
  • 启动调度器scheduler,通过scheduler.start()实现任务的定时触发。

详细演示步骤见下文的完整代码样例。

下面是简略代码,

JobDetail job = JobBuilder.newJob(HelloJob.class)
        .withIdentity("MyJob", "MyGroup")
        .build();

// a trigger at an interval of 10 seconds with 10 repeated times
Trigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("MyTrigger", "MyGroup")
        .startAt(new Date())
        .withSchedule(SimpleScheduleBuilder.simpleSchedule()
                .withIntervalInSeconds(10)
                .withRepeatCount(10))
        .build();

// scheduler job and start
Scheduler scheduler = new StdSchedulerFactory().getScheduler();
scheduler.scheduleJob(job, trigger);
scheduler.start();

//sleeping for scheduler to run job
Thread.sleep(60 * 1000L);

// shutdown the scheduler gracefully
scheduler.shutdown(true);

这里需要说明的是,

1) Job/Trigger的唯一标识

Job/Trigger有组group和名字name的联合唯一标识,在一个触发器中不允许有两个相同标识的Job/Trigger存在。若有的话,会抛出如下异常信息,

Exception in thread "main" org.quartz.ObjectAlreadyExistsException: Unable to store Job : 'MyGroup.MyJob', because one already exists with this identification.

2) Scheduler任务线程池

一旦执行scheduler.scheduleJob(job, trigger)之后,则会默认启动10个任务工作线程,整个scheduler的状态为,

Scheduler class: 'org.quartz.core.QuartzScheduler' - running locally.
NOT STARTED.
Currently in standby mode.
Number of jobs executed: 0
Using thread pool 'org.quartz.simpl.SimpleThreadPool' - with 10 threads.

这个时候调度器还未正式启动任务调度,需要执行scheduler.start()后才真正开始运行。

通过jvisualvm.exe工具可以查看scheduler.scheduleJob()之后的线程情况,

注意的是,若任务结束后,程序没有调用scheduler.shutdown()的话,则整个程序还会一直在运行中。

2. Cron定时任务

一个cron定时任务样例代码见这里,其步骤和上述代码样例差不多,不一样之处在于触发器的定义从SimpleTrigger变成了CronTrigger,

// a cron scheduler at every 10 seconds
CronTrigger trigger = TriggerBuilder.newTrigger()
        .withIdentity("MyTrigger", "MyGroup")
        .startAt(DateBuilder.evenMinuteDate(new Date()))
        .endAt(new Date(System.currentTimeMillis() + 60 * 1000))
        .withSchedule(CronScheduleBuilder.cronSchedule("0/10 * * * * ?"))
        .build();

详细演示步骤见下文的完整代码样例。

Cron表达式非常灵活,其可以覆盖广泛的定时任务定义,下面是一些常见的任务定时表达式样例,

Cron表达式 含义
0/20 * * * * ? 每隔20秒
15 0/2 * * * ? 每隔2分钟的第15秒,例如00:02:15,00:04:15,依次类推
0 0/2 8-17 * * ? 每天08-17点,每隔2分钟
0 0 10am 1,15 * ? 每月的第1天和第15天,上午10:00触发
0,30 * * ? * MON-FRI 周一到周五,每到0,30秒时触发,比如周一的00:00:00,00:00:30,依次类推。
更多周天代码,周一MON,周二 TUE,周三 WED,周四 THU,周五 FRI,周六SAT,周日 SUN
0,30 * * ? * SAT,SUN 周六和周日,每到0,30秒时触发

3. 远程任务调度

在很多情况,任务的调度和执行并不在同一台机器,这个时候可以使用远程任务调度功能。一个远程任务调度的代码样例见这里。其主要包括两个步骤,

  1. 启动调度服务器,准备接受调度任务,代码样例
  2. 启动任务调度客户端,发送任务到远程调度服务器,代码样例

若从代码上分析,其和上述的单机版本任务调度并无区别,远程任务调度的不一样之处在于启动配置文件。下面为两个简略启动脚本,

java -Dorg.quartz.properties=server.properties -classpath "./demo-quartz.jar" com.pphh.demo.remote.RemoteSchedulerServer
java -Dorg.quartz.properties=client.properties -classpath "./demo-quartz.jar" com.pphh.demo.remote.RemoteSchedulerClient

可以看到,启动配置直接影响着调度器的行为,配置调度器实现远程调度功能,包括客户端和服务端。

1) 客户端配置

在配置文件client.properties中主要有如下三个配置项,

org.quartz.scheduler.rmi.proxy: true
org.quartz.scheduler.rmi.registryHost: localhost
org.quartz.scheduler.rmi.registryPort: 1099

上述配置项定义了客户端的远程服务代理。

2) 服务器配置

在配置文件client.properties中主要有如下四个配置项,

org.quartz.scheduler.rmi.export: true
org.quartz.scheduler.rmi.registryHost: localhost
org.quartz.scheduler.rmi.registryPort: 1099
org.quartz.scheduler.rmi.createRegistry: true

上述配置项定义了服务器端的远程服务接受。

详细的远程任务调度演示步骤见下文的完整代码样例。

4. 集群(分布式)任务调度

集群任务调度也叫做分布式任务调度,主要为了满足任务调度的高可用、高性能、高可扩展性的需求。

一个基于Mysql数据库的集群任务调度代码样例见这里。其主要包括三个步骤,
- 初始化数据库表字段,准备存储任务调度信息,mysql初始化脚本
- 创建任务调度,保存到数据库中,代码样例
- 启动多个集群实例,实现任务的集群调度,代码样例

Quartz的集群调度依赖数据库实现任务的调度一致性,支持的数据库非常广泛。不同数据库的初始化脚本可以下载官方的发布包,解压后在文件目录./docs/dbTables中获取。

详细的集群任务调度演示步骤见下文的完整代码样例。

5. 完整代码样例和演示

演示代码仓库地址:https://gitee.com/pphh/simple-demo,可以通过如下git clone命令获取仓库代码,

git clone git@gitee.com:pphh/simple-demo.git

完整演示项目代码位于文件目录./demo-quartz-scheduler中。

整个项目的演示步骤请见README文件,请参考说明文件对项目进行编译构建,并运行演示程序。

6. 参考文档

  1. Quartz-Scheduler官方代码仓库:https://github.com/quartz-scheduler/quartz
  2. Quartz-Scheduler官方发布包:http://www.quartz-scheduler.org/downloads/