一、定时任务
1、定时任务的几种实现方式
1)TimerTask
Java自带的java.util.Timer类,这个类允许你调度一个java.util.TimerTask任务。使用这种方式可以让你的程序按照某一个频度执行。
因为它不能在指定时间执行, 仅仅能让程序依照某一个频度执行,所以TimerTask在实际项目中用的不多。
这里只列一个简单的示例,说明一下Timer的使用方式,不作深入探讨:
a、约定
需要: spring.jar, commons-logging.jar 两个包
b、配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="taskService" class="com.zdp.service.TaskService"></bean>
<bean id="scheduledTimerTask" class="org.springframework.scheduling.timer.ScheduledTimerTask">
<property name="timerTask" ref="taskService" />
<!-- 每隔一天运行一次配置: 24*60*60*1000 -->
<!-- 每1秒钟程序运行一次 -->
<property name="period" value="1000" />
<!-- 程序启动4秒钟后開始运行 -->
<property name="delay" value="4000" />
</bean>
<bean id="timerFactoryBean" class="org.springframework.scheduling.timer.TimerFactoryBean">
<property name="scheduledTimerTasks">
<list>
<ref bean="scheduledTimerTask" />
</list>
</property>
</bean>
</beans>
c、编码
实现方式就是继承TimerTask 类,重写run方法即可。
/**
* 定时调度业务类
*/
public class TaskService extends TimerTask {
@Override
public void run() {
String currentTime = new SimpleDateFormat("yyy-MM-dd hh:mm:ss").format(new Date());
System.out.println(currentTime + " 定时任务正在运行...");
}
}
2)(比较常用)Spring自带的task
Spring3.0以后自带的task,可以将它看成一个轻量级的Quartz,而且使用起来比Quartz简单许多。它的好处是,不需要额外引入jar包,配置比较简单,而且功能也比较全面。
3)Quartz
Quartz是一个功能强大的调度器,可以让你的程序在指定时间执行,也可以按照某一个频度执行,但是配置起来稍显复杂。
2、应用实例
1)用sping提供的task实现定时任务(基于注解方式)
【配置】
在applicationContext.xml中作如下配置
配置组件扫描
<context:component-scan base-package="com.xxx.yyy.schedule">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Component"/>
</context:component-scan>
开启task任务注解扫描
<task:annotation-driven/>
成功添加此配置后,applicationContext.xml的命名空间,会新增以下三项内容,注意检查有没有
xmlns:task="http://www.springframework.org/schema/task
http://www.springframework.org/schema/task
http://www.springframework.org/schema/task/spring-task-4.1.xsd
如上是最简单的配置,可以满足基础的定时任务需求。
【编码】
在包com.xxx.yyy.schedule下(与配置base-package的值对应),新建定时任务类
@Component
public class WecomMarkTagExecutor {
private Logger logger = LoggerFactory.getLogger(WecomMarkTagExecutor.class);
@Autowired
private WeComMarkTagService weComMarkTagService;
@Scheduled(cron = "0 0/2 * * * ? ")
public void task01() {
try {
logger.info("wecom打标签任务-start");
weComMarkTagService.wecomMarkTag();
logger.info("wecom打标签任务-end");
} catch (Exception e) {
e.printStackTrace();
}
}
}
2)开发中遇到的问题
a、注意spring的组件扫描配置
添加包扫描比较简单,就像1)中示例的配置一样。
有时候,定时任务实现类所在包下、有些我们不想放入容器的类,如何配置能实现只把想放入容器的类放进去、不想放的不放进去呢?
这里说一个通过正则表达式不把指定类放入容器的配置,假设schedule包下有两个定时任务:EveryDayExecutor和WecomMarkTagExecutor,其EveryDayExecutor是其他模块的定时任务,当前模块不能将它放入容器,我们可以作如下配置
<context:component-scan base-package="com.haoys.bdp.*">
<context:exclude-filter type="regex" expression="com.xxx.yyy.schedule.EveryDayExecutor"/>
</context:component-scan>
这个配置的意思是,扫满schedule包下的组件(放入容器),但排除EveryDayExecutor。
b、注意corn表达式的书写
corn表达式网上很多讲解,一定要注意检查自己写的对不对;我就是由于corn写错了、导致定时任务不能按预想的执行,以为配置错误,改来改去也改不好,不仅浪费时间、还特别影响心情。
注意cron = "0 0/2 * * * ? ",才是每隔2分钟执行一次;
cron = "0 2 * * * ? ",意思是每个小时的第二分钟执行!
c、注意配置
必须开启task注解扫描<task:annotation-driven/>
,才能使@Scheduled注解被扫到,才能让定时任务生效。
2)为定时任务配置线程池
二、AOP
1、AOP的基础概念
2、实际应用中遇到的问题
1)aop与事务的执行顺序问题
a、问题描述
在使用aop时遇到一个这样问题:需求是用户信息更新后,要使用aop将更新后的信息,推送到其他系统;但是,实际使用时用户更新后,推送功能推的用户数据却是更新前的数据。
我们用的是@AfterReturning通知,按理说是在更新方法执行后才开始推送的,这时期望的是数据库中的用户信息已经更新,我们查出用户信息再推送、应该是最新的数据。
问题出在,更新方法要进行多个表的数据修改、使用了事务。所以,虽然更新方法先执行(更新用户和其他表)、aop通知方法后执行(查询用户和推送),但有些情景、比如数表中数据过多时,更新方法先执行但是在多个数据库修改操作没有完成前,事务不提交、表中用户数据不更新;这时aop查询方法“后发先至”,在用户数据更新之前先完成查询,就得到了错误数据。
b、解决方法
应该有很多解决方法,我这里是这样实现的:
首先,是否有和当前线程有关联的事务正在执行、是可以知道的,
boolean flag = TransactionSynchronizationManager.isActualTransactionActive();
spring提供的这个方法,当有相关的、正在执行事务时,返回true;
其次,TransactionSynchronizationManager提供了多个方法,可以让我们实现在事务提交过程的各个时间节点,执行我们的方法
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void suspend() {
}
@Override
public void resume() {
}
@Override
public void flush() {
}
@Override
public void beforeCommit(boolean b) {
}
@Override
public void beforeCompletion() {
}
@Override
public void afterCommit() {
//执行事务提交后流程
}
@Override
public void afterCompletion(int i) {
//执行事务完成后流程
}
});
所以我先判断当前线程是否有关联的事务正在执行?
如果有就在事务完成后再执行我的方法(事务提交后就执行应该就行),
如果没有,那就马上执行的方法。
注意:这些逻辑,要在aop通知方法中执行
具体代码如下
/**
* @Description 用户数据推送
**/
@Aspect
@Component
public class PushUserInfoAspect{
private static ExecutorService threadPool = Executors.newFixedThreadPool(500);
@Around("@annotation(pushDataToShop)")
public Object around(ProceedingJoinPoint joinPoint, PushDataToShop pushDataToShop) throws Throwable{
//JSONObject param = getNameAndValue(joinPoint);
String event = pushDataToShop.event();
String dataPlace = pushDataToShop.value();
JSONObject argObj = getNameAndValue(joinPoint);
Object returnValue = joinPoint.proceed();
JSONObject returnJsonOb = (JSONObject) JSONObject.toJSON(returnValue);
//获取
boolean flag = TransactionSynchronizationManager.isActualTransactionActive();
//判断是否存在事务正在执行
if(flag){
//1.1 存在事务
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void suspend() {
}
@Override
public void resume() {
}
@Override
public void flush() {
}
@Override
public void beforeCommit(boolean b) {
}
@Override
public void beforeCompletion() {
}
@Override
public void afterCommit() {
//执行事务提交后流程
}
@Override
public void afterCompletion(int i) {
try {
excutePush(returnJsonOb,argObj,dataPlace,event);
} catch (Exception e) {
e.printStackTrace();
}
}
});
} else {
//1.2 没有事务
excutePush(returnJsonOb,argObj,dataPlace,event);
}
return returnValue;
}
/**
* @Description 执行用户推送
**/
public void excutePush(JSONObject returnObj,JSONObject argObj,String dataPlace,String event) throws Exception{
。。。
}
}
补充:因为事务底层,也是通过aop实现的,而aop的执行顺序可以配置,所以还能通过直接配置事务与aop的执行顺序,解决上述问题。
3、应用实例(基于自定义注解)
1)应用场景
需求是:在a系统每次字典数据变更时,都需要给b系统同步一次数据,以保持两个系统字典数据相同。
字典的增、删、改、合并接口,都需要执行数据推送操作,如果不用AOP、这些接口都需要增加推送操作的代码,这样会大大增加主业务与推送的耦合度,而且改起来很麻烦、代码也很不优雅。
下面的应用实例
- 我们使用AOP来降低耦合度;
- 同时为了不影响主业务的执行效率,采用多线程异步执行推送;
- 为了优雅,采用自定义注解标记连接点。
2)代码
a、约定
b、配置
c、自定义注解
@Target(ElementType.METHOD) /*注解作用范围*/
@Retention(RetentionPolicy.RUNTIME) /*注解生命周期*/
public @interface PushDataToB {
/*加两个变量,方便业务功能拓展*/
//数据类型:1=机构,2=字典
String dataType() default "1";
//方法类型:mm=合并
//合并时给b系统两次,一次传删除的、一次传留下的
String methodType() default "nmm";
}
d、写切面类
@Component
@Aspect
public class PushDataToBAspect {
@Resource(name = "threadPool")
private ThreadPoolTaskExecutor taskExecutor;
/*@Pointcut("@annotation(com.hhh.bbb.annotation.PushDataToB)")
public void annotationPointCut() {
}*/
@AfterReturning("@annotation(pushDataToB)")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
//获取参数
String dataType = pushDataToB.dataType();
String methodType = pushDataToB.methodType();
Object argObj = getParams(joinPoint);
//执行任务
PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
/*Thread t = new Thread(pushDataToBExecute);
t.start();*/
taskExecutor.execute(pushDataToBExecute);
}
//获取连接点参数
private Object getParams(JoinPoint joinPoint) {
/*Map<String, Object> param = new HashMap<>();
//获取参数值数组
Object[] paramValues = joinPoint.getArgs();
//获取参数名数组
String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
//组装,返回jsonobject
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return JSONObject.parseObject(JSON.toJSONString(param));*/
//获取参数值数组
Object[] paramValues = joinPoint.getArgs();
return paramValues[0];
}
}
代码基本可以分为以下几个部分:
@Pointcut
第一部分,是@Pointcut注解及其方法,用来定义切入点、即我们的通知要在什么地方执行;我这里用的注解标记,表示在用了@PushDataToB这个注解的地方,按照通知类型织入我们的通知。
其实,这个方法并不是必须的,因为通知注解也可以指明通知在什么地方执行。
那么@Pointcut在什么时候用呢?
当类中有多个方法定义的切面相同时,可以单独定义一个方法,不必写方法体,统一设置@Pointcut;当单独指定了一个方法设置好@Pointcut后,类中的其他方法如果要跟单独指定的方法设置相同的切面时,只需要引用设置@Pointcut方法的名字即可。所以上面的示例代码还可以是以下形式的
@Component
@Aspect
public class PushDataToBAspect {
...
@Pointcut("@annotation(com.hhh.bbb.annotation.PushDataToB)")
public void annotationPointCut() {
}
@Before("annotationPointCut()")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
xxx方法代码
}
@AfterReturning("annotationPointCut()")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
yyy方法代码
}
...
}
可以看到代码中我给注掉了,因为我的需求只要一个通知方法就能实现,不需要公用切入点(而且即使公用也不是必须的)。
通知方法
第二个部分是通知方法
@AfterReturning("@annotation(pushDataToB)")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
//获取参数
String dataType = pushDataToB.dataType();
String methodType = pushDataToB.methodType();
Object argObj = getParams(joinPoint);
//执行任务
PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
/*Thread t = new Thread(pushDataToBExecute);
t.start();*/
taskExecutor.execute(pushDataToBExecute);
}
五类通知对应五类通知注解
前置通知(@Before):在目标方法运行之前运行
后置通知(@After):在目标方法运行结束之后运行(无论方法正常结束还是异常结束)
返回通知(@AfterReturning):在目标方法正常返回之后运行
异常通知(@AfterThrowing):在目标方法出现异常以后运行
环绕通知(@Around):动态代理,手动推进目标方法运行(joinPoint.procced())
可以在通知实现方法中增加JoinPoint类型的参数,用以接收实际业务(连接点方法)的方法名称和参数列表等信息,举例说明一下:
下面代码一个字典新增方法的实现方法,按照需求、字典新增我们要把数据推送到B系统,于是我们在方法上加自定义注解
@PushDataToB(dataType = "2")
@Override
public void insertSelective(Dictionary dictionary) {
xxx新增字典的实际代码,校验、新增等
dictionaryDAO.insertSelective(dictionary);
}
在切面类通知方法中,我们需要获取入参dictionary,所以通知方法如下:
我们使用最终通知@AfterReturning,因为这是一个新增,在新增执行成功前,没有字典id等数据,所以选择最终通知、在字典插入方法执行成功后,再获取参数、执行推送任务。
@AfterReturning("@annotation(pushDataToB)")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
//获取参数
String dataType = pushDataToB.dataType();
String methodType = pushDataToB.methodType();
Object argObj = getParams(joinPoint);
//执行任务
PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
taskExecutor.execute(pushDataToBExecute);
}
dataType、methodType是注解自定义的参数,如果需要正常获取就行;
JoinPoint
argObj参数是通过JoinPoint参数、以及动态代理实现的,具体代码如下:
//获取连接点参数
private Object getParams(JoinPoint joinPoint) {
//获取参数值数组
Object[] paramValues = joinPoint.getArgs();
return paramValues[0];
}
因为示例中的主业务方法只有一个参数,所以我们直接获取第一个参数值就满足需求了;如果主业务方法有多个参数时,也可以获取,可以这样:
//获取连接点参数
private JSONObject getParams(JoinPoint joinPoint) {
Map<String, Object> param = new HashMap<>();
//获取参数值数组
Object[] paramValues = joinPoint.getArgs();
//获取参数名数组
String[] paramNames = ((CodeSignature)joinPoint.getSignature()).getParameterNames();
//组装,返回jsonobject
for (int i = 0; i < paramNames.length; i++) {
param.put(paramNames[i], paramValues[i]);
}
return JSONObject.parseObject(JSON.toJSONString(param));
}
上面是得到JoinPoint以后,通过它获取主业务参数的代码,有一点需要注意
无论通知实现方法有多少个参数,JoinPoint类型的参数必须是第一个参数,否则Spring将无法识别。
ProceedingJoinPoint
还有一个ProceedingJoinPoint类,它继承了JoinPoint,而且添加了proceed()等方法,proceed()方法在环绕通知时会用到,通过它可以控制连接点方法的执行时机,即可以主动控制环绕通知方法的代码,哪些在连接点方法执行前要做,哪些在连接点方法执行后要做,应用示例如下:
@Around("@annotation(pushDataToB)")
public Object around(ProceedingJoinPoint joinPoint, PushDataToB pushDataToB) {
//获取参数
String event = pushDataToB.event();
String dataPlace = pushDataToB.value();
JSONObject argObj = getNameAndValue(joinPoint);
Object returnValue = joinPoint.proceed();
//
JSONObject returnJsonOb = (JSONObject) JSONObject.toJSON(returnValue);
excutePush(returnJsonOb,argObj,dataPlace,event);
return returnValue;
}
上面代码是一个环绕通知方法,前三行代码是在主业务方法(也就是连接点方法)执行前执行的,最后三行代码是在主业务方法执行后执行的,这个功能在有些应用场景下非常好用。
e、多线程异步调用
在applicationContext.xml中配置一个线程池
<!-- 线程池配置 -->
<bean id="threadPool" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<!-- 核心线程数 -->
<property name="corePoolSize" value="5" />
<!-- 最大线程数 -->
<property name="maxPoolSize" value="30" />
<!-- 队列最大长度 >=mainExecutor.maxSize -->
<property name="queueCapacity" value="500" />
<!-- 线程池维护线程所允许的空闲时间 -->
<property name="keepAliveSeconds" value="200" />
<!-- 线程池对拒绝任务(无线程可用)的处理策略 -->
<property name="rejectedExecutionHandler">
<bean class="java.util.concurrent.ThreadPoolExecutor$CallerRunsPolicy" />
</property>
</bean>
推动任务放在run()方法中
写一个通知执行类,实现Runnable,将通知任务(也就是推送业务)放在run()方法中
public class PushDataToBExecute implements Runnable{
private static Logger logger = LogManager.getLogger(PushDataToBExecute.class.getName());
//服务器地址
private String bServerUrl = PropertiesSource.getProperty("B_SERVER");
private String headPicHost = PropertiesSource.getProperty("HEAD_PIC_HOST");
private Object data = null;
private String dataType;
private String methodType;
@Override
public void run() {
try {
//判断是否是合并操作,合并推送两次:一次传删除的、一次传留下的
if ("mm".equals(methodType)){
xxx代码
}else {
String pwd = Md5Utils.hash("xxx");
Map<String, String> params = new HashMap<String, String>();//请求参数集合
params.put("type", dataType);
params.put("pwd", pwd);
params.put("json", JSONObject.toJSONString(data));
logger.info("Thread:{},推送数据到B系统Param :{}", Thread.currentThread(), JSONObject.toJSONString(params));
String res = HttpUtils.doPostWithJson("http://xxxx", params);
System.out.print(res);
}
}catch (Exception e){
logger.error("Thread:{},推送数据到B系统出现错误:{},", Thread.currentThread(), e);
}
}
public PushDataToBExecute(Object data, String dataType, String methodType){
this.data = data;
this.dataType = dataType;
this.methodType = methodType;
}
}
在切面类注入线程池
@Resource(name = "threadPool")
private ThreadPoolTaskExecutor taskExecutor;
异步执行推送任务
@AfterReturning("@annotation(pushDataToB)")
public void AfterReturning(JoinPoint joinPoint, PushDataToB pushDataToB) {
xxx代码
//异步执行推送任务
PushDataToBExecute pushDataToBExecute = new PushDataToBExecute(argObj, dataType, methodType);
taskExecutor.execute(pushDataToBExecute);
}
X、工作中遇到的问题汇总
1、java.sql.SQLException: connection holder is null
1)问题描述:
对于大表进行查询、修改操作时,有时sql需要执行很长时间,这时就可能在执行到半路时、报错SQLException: connection holder is null
。
意思是,连接数据库的对象为null,就是连接断开了、过期了、没了。
2)原因:
在spring配置数据源时,一般都会配置以下两项
<!-- 是否自动回收超时连接 -->
<property name="removeAbandoned" value="true" />
<!-- 超时时间,单位:秒(s) -->
<property name="removeAbandonedTimeout" value="10" />
意思是,一次对数据库的连接,当它超过设定时间时,spring就给他断开。
第一个是配置开不开这个功能;
第二个是配置限定时间。
3)解决:
方案一:
<!-- 直接关闭这个 自动回收超时连接 -->
<property name="removeAbandoned" value="false" />
方案二:延长超时时间
<!-- 是否自动回收超时连接 -->
<property name="removeAbandoned" value="true" />
<!-- 超时时间,单位:秒(s) -->
<property name="removeAbandonedTimeout" value="1800" />
方案三:
关闭自动回收,然后再代码中手动设置回收。
这个我没用过
4)心得
这个回收还是有必要的,过长时间的sql连接,会影响其他功能、让系统变卡;严重的,还会耗尽数据库内存,导致数据源奔溃、系统停用。
但是有时候,又会有那种大批量的数据,需要统一处理;这种情况,建议把生产数据导到线下一份,在本地执行完成后、再把结果更新到生产,一定要保证生产环境稳定安全运行。
在本地执行的过程,可以暂时把“超时回收”关闭 或者 延长超时时间,执行完再改回来就行。