在Linux上定时执行java的方式有很多种,例如可以直接使用Spring的定时器,但是唯一一点不好的地方就是Spring的定时器需要依赖容器.如果容器没有启动,那么程序是跑不起来的.
crontab的好处是它本身是Linux的一个程序,可以直接通过java命令执行java程序,无需容器的依赖.
1、使用命令执行java
首先要知道在Ubuntu上如何使用命令跑java程序。
1、先写一个简单main类,放在/root/sh目录下:
public class JobRunnerTest {
public static void main(String[] args) {
System.out.println("start job ...");
for(String s : args){
System.out.println(s);
}
}
}
2、编译:
cd /root/sh
javac JobRunnerTest
3、执行:
java -classpath /root/sh/ JobRunnerTest
其中classpath参数是加载一些依赖的jar包以及class文件,必须要加上,否则程序将会不知道去哪找JobRunnerTest.class。
4、输出:
start job ...
说明程序跑成功了。
如果把执行java程序的命令配置到crontab中,就已经可以定时执行java了。
2、结合Spring
大多数时候执行job并不是单一的一个类,而是多个类,并且有可能需要连接数据库,这个时候使用Spring管理多个类和数据库连接是非常方便的。
1、创建job接口Job.java:
package online.pangge.exam.job;
import java.util.List;
public interface Job<T> {
/**
* 获取数据
* @return
*/
List<T> dataSource();
/**
* 针对每一条数据都执行一次
* @param t
*/
void process(T t);
}
2、创建job执行类JobExecute.java:
package online.pangge.exam.job;
import java.util.List;
/**
* Created by Pangge on 2017/11/5.
*/
public class JobExecute<T> {
public void start(Job job){
//调用job接口的dataSource方法获取数据
List<T> datas =job.dataSource();
//循环执行job的process方法,每次传入一个元素,也可以写成process方法接收List<T>,然后直接把datas传过去,这里写成循环调用process主要是考虑到可以使用callable实现并发调用process
for (T t: datas){
job.process(t);
}
}
}
3、创建job执行入口JobRunner.java
package online.pangge.exam.job;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationContext;
/**
* Created by Pangge on 2017/11/5.
*/
public class JobRunner {
public static void main(String[] args) {
//获取spring配置文件
ApplicationContext beanFactory = new FileSystemXmlApplicationContext("classpath:application.xml");
//根据job名字获取job
Job job = (Job) beanFactory.getBean(args[0]);
//初始化job执行类
JobExecute execute = new JobExecute();
//调用job执行类的start方法,传入bean
execute.start(job);
}
}
4、创建执行java的shell脚本/root/sh/start.sh:
#!/bin/sh
#java所在目录
JAVA_HOME="/root/jdk1.7.0_45"
#执行的角色,一般不推荐使用root
RUNNING_USER=root
#class文件所在目录的上一级
APP_HOME=/root/weixin/tiger/website/target/website/WEB-INF
#job入口类带全限定名的名字,例如online.pangge.exam.job.JobRunner
APP_MAINCLASS=$1
CLASSPATH=$APP_HOME/classes
#拼接classpath变量,这里会逐个jar包都拼接进去
for i in "$APP_HOME"/lib/*.jar; do
CLASSPATH="$CLASSPATH":"$i"
done
#运行时的参数,可以不提供,按照默认的参数运行,如果不提供注意下面JAVA_CMO也应该去掉$JAVA_OPTS参数
JAVA_OPTS="-ms512m -mx512m -Xmn256m -Djava.awt.headless=true -XX:MaxPermSize=128m"
#需要执行的job名称,不需要全限定名,因为是通过Spring获取的,知道名字即可
$PARAM=$2
#拼接执行的参数,>/root/sh/$PARAM-`date +%Y-%m-%d-%H%M%S`.log 2>&1 & 是重定向程序输出到/root/sh目录下,对应的job名称的.log文件中
JAVA_CMO="nohup $JAVA_HOME/bin/java $JAVA_OPT -classpath $CLASSPATH $APP_MAINCLASS $3 >/root/sh/$PARAM-`date +%Y-%m-%d-%H%M%S`.log 2>&1 &"
#执行的命令,注意- $RUNNING_USER中间是有空格的
su - $RUNNING_USER -c "$JAVA_CMO"
5、创建一个简单的job实现类TestJob.java
package online.pangge.exam.job;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Component("TestJob")
public class TestJob implements Job<String> {
private static Logger log = LoggerFactory.getLogger(TestJob.class);
@Override
public List dataSource() {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");
list.add("e");
return list;
}
@Override
public void process(String s) {
log.info("do process");
System.out.println("test job = = = = =,Time = "+new Date());
}
}
默认使用注解@Component,名称为TestJob。也可以使用xml方式配置在application.xml中。
6、编译整个项目:
这一步根据使用的编译工具,例如使用maven的,可以切换到pom.xml所在目录,使用mvn package进行打包编译。
7、使用命令执行job:
/root/sh/start.sh online.pangge.exam.job.JobRunner TestJob
可以看到/root/sh/目录下面已经有了log文件:
ls /root/sh
TestJob-2018-04-01-203609.log
cat TestJob-2018-04-01-203609.log
可以看到除了spring启动的信息外,还输出以下内容:
test job = = = = =,Time = Sun Apr 01 20:36:12 CST 2018
test job = = = = =,Time = Sun Apr 01 20:36:12 CST 2018
test job = = = = =,Time = Sun Apr 01 20:36:12 CST 2018
test job = = = = =,Time = Sun Apr 01 20:36:12 CST 2018
test job = = = = =,Time = Sun Apr 01 20:36:12 CST 2018
说明job已经成功执行。
3、将job配置到crontab中:
Ubuntu默认安装了crontab,使用crontab -e即可打开配置文件,需要注意的是这样打开的配置文件只针对当前用户。
1、打开crontab的配置文件
crontab -e
输出:
# Edit this file to introduce tasks to be run by cron.
#
# Each task to run has to be defined through a single line
# indicating with different fields when the task will be run
# and what command to run for the task
#
# To define the time you can provide concrete values for
# minute (m), hour (h), day of month (dom), month (mon),
# and day of week (dow) or use '*' in these fields (for 'any').#
# Notice that tasks will be started based on the cron's system
# daemon's notion of time and timezones.
#
# Output of the crontab jobs (including errors) is sent through
# email to the user the crontab file belongs to (unless redirected).
#
# For example, you can run a backup of all your user accounts
# at 5 a.m every week with:
# 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/
#
# For more information see the manual pages of crontab(5) and cron(8)
#
# m h dom mon dow command
2、添加以下配置:
0 0 */1 * * /root/sh/start.sh online.pangge.exam.job.JobRunner TestJob
这条命令是每天的0点0时执行TestJob。
3、crontab执行时间的格式:
另外需要注意的是通配符:
星号(*):代表所有可能的值。
例如:
* * * * * /root/sh/start.sh online.pangge.exam.job.JobRunner TestJob
代表每分钟执行一次脚本。
逗号(,):可以用逗号隔开的值指定一个列表范围。
例如:
1,3,5,7,9 * * * * /root/sh/start.sh online.pangge.exam.job.JobRunner TestJob
代表每小时的第1、3、5、7、9分钟均执行一次脚本。
中杠(-):可以用整数之间的中杠表示一个整数范围。
例如:
1-30 * * * * /root/sh/start.sh online.pangge.exam.job.JobRunner TestJob
代表每个小时的前三十分钟每分钟都执行一次脚本。
正斜线(/):可以用正斜线指定时间的间隔频率。
例如:
*/10 * * * * /root/sh/start.sh online.pangge.exam.job.JobRunner TestJob
代表每十分钟执行一次脚本。
这些通配符可以同时使用,例如:
1-30/10 * * * * /root/sh/start.sh online.pangge.exam.job.JobRunner TestJob
代表每小时的前三十分钟里面,每十分钟执行一次脚本,也就是说一个小时里面只执行三次。
4、异常解决
ClassDefFoundError: javax/servlet/ServletContext
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:334)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1214)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:543)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:482)
at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:306)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:772)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:839)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:538)
at org.springframework.context.support.FileSystemXmlApplicationContext.<init>(FileSystemXmlApplicationContext.java:140)
at org.springframework.context.support.FileSystemXmlApplicationContext.<init>(FileSystemXmlApplicationContext.java:84)
at online.pangge.exam.job.JobRunner.main(JobRunner.java:11)
Caused by: org.springframework.beans.factory.BeanCreationException: Could not autowire field: public online.pangge.exam.service.ISubjectService online.pangge.wechat.web.controller.ExamController.subjectService; nested exception is java.lang.NoClassDefFoundError: javax/servlet/ServletContext
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:573)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:88)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:331)
... 13 more
Caused by: java.lang.NoClassDefFoundError: javax/servlet/ServletContext
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java:2615)
at java.lang.Class.getDeclaredMethods(Class.java:1860)
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:609)
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:521)
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:507)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:241)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors(AbstractAutowireCapableBeanFactory.java:1069)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1042)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getSingletonFactoryBeanForTypeCheck(AbstractAutowireCapableBeanFactory.java:865)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryBean(AbstractAutowireCapableBeanFactory.java:796)
at org.springframework.beans.factory.support.AbstractBeanFactory.isTypeMatch(AbstractBeanFactory.java:544)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:447)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:423)
at org.springframework.beans.factory.BeanFactoryUtils.beanNamesForTypeIncludingAncestors(BeanFactoryUtils.java:220)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1177)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1116)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1014)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:545)
... 15 more
Caused by: java.lang.ClassNotFoundException: javax.servlet.ServletContext
at java.net.URLClassLoader$1.run(URLClassLoader.java:366)
at java.net.URLClassLoader$1.run(URLClassLoader.java:355)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:354)
at java.lang.ClassLoader.loadClass(ClassLoader.java:425)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
at java.lang.ClassLoader.loadClass(ClassLoader.java:358)
找不到类,这明显是没有加载到某一个类,之前怀疑是application.xml没有正确加载,但是考虑到没有加载application.xml文件spring应该无法启动,排除这个原因。
其中无法加载的类是javax.servlet.ServletContext,猜测应该是jar包没有正确导入,因为这些jar包在tomcat中已经有了,所以一般不会编译到lib目录中。
检查/root/weixin/tiger/website/target/website/WEB-INF/lib目录,发现有几个jar没有正确加载:
将这几个jar导入到lib目录中,重新执行job,可以正常执行。
不过每次都这样手动复制太麻烦,于是干脆在start.sh中添加复制的语句:
先把这四个jar复制到/root/jar目录中,然后在start.sh中添加以下语句:
#!/bin/sh
#java所在目录
JAVA_HOME="/root/jdk1.7.0_45"
#执行的角色,一般不推荐使用root
RUNNING_USER=root
#class文件所在目录的上一级
APP_HOME=/root/weixin/tiger/website/target/website/WEB-INF
#job入口类带全限定名的名字,例如online.pangge.exam.job.JobRunner
APP_MAINCLASS=$1
CLASSPATH=$APP_HOME/classes
#复制jar包一定要在循环前面,否则复制的jar包无法加载
cp /root/jars/* /root/weixin/tiger/website/target/website/WEB-INF/lib/
#拼接classpath变量,这里会逐个jar包都拼接进去
for i in "$APP_HOME"/lib/*.jar; do
CLASSPATH="$CLASSPATH":"$i"
done
#运行时的参数,可以不提供,按照默认的参数运行,如果不提供注意下面JAVA_CMO也应该去掉$JAVA_OPTS参数
JAVA_OPTS="-ms512m -mx512m -Xmn256m -Djava.awt.headless=true -XX:MaxPermSize=128m"
#需要执行的job名称,不需要全限定名,因为是通过Spring获取的,知道名字即可
PARAM=$2
#拼接执行的参数,>/root/sh/$PARAM-`date +%Y-%m-%d-%H%M%S`.log 2>&1 & 是重定向程序输出到/root/sh目录下,对应的job名称的.log文件中
JAVA_CMO="nohup $JAVA_HOME/bin/java $JAVA_OPT -classpath $CLASSPATH $APP_MAINCLASS $PARAM >/root/sh/$PARAM-`date +%Y-%m-%d-%H%M%S`.log 2>&1 &"
#执行的命令,注意- $RUNNING_USER中间是有空格的
su - $RUNNING_USER -c "$JAVA_CMO"